prepshot._model.storage 源代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""This module contains energy storage related functions. Symmetrical energy 
storage system is considered in this module. It means that the energy storage
system has the same power capacity for charging and discharging.

Similar to the power discharging process, the charging power of storage
technology :math:`e` (:math:`{\\rm{power}}_{h,m,y,z,e}^{{c}}`) is also limited
by the existing installed capacity and technical minimum charging power
(:math:`{\\underline{{\\rm{POWER}}}}_{h,m,y,z,e}^{{c}}`) as follows:

.. math::

    {\\underline{{\\rm{POWER}}}}_{h,m,y,z,e}^{{c}}\\times
    {\\rm{cap}}_{y,z,e}^{\\rm{existingtech}}\\le{\\rm{power}}_{h,m,y,z,e}^{{c}}
    \\le {\\rm{cap}}_{y,z,e}^{\\rm{existingtech}}\\quad\\forall h,m,y,z,e
    \\in {\\mathcal{STOR}}

The charging generation (:math:`{\\rm{charge}}_{h,m,y,z,e}`) and
:math:`{\\rm{power}}_{h,m,y,z,e}^{c}` need to meet the following formula:

.. math::
    
    {\\rm{charge}}_{h,m,y,z,e}={\\rm{power}}_{h,m,y,z,e}^{\\rm{c}}\\times
    \\Delta h{\\times\\eta}_{y,e}^{{\\rm{in}}}
    \\quad\\forall h,m,y,z,e\\in {\\mathcal{STOR}}

Changes in stored electricity
(:math:`{\\rm{storage}}_{h,m,y,z,e}^{\\rm{energy}}`) in two successive periods
should be balanced by the charging (:math:`{\\rm{charge}}_{h,m,y,z,e}`) and
discharging (:math:`{\\rm{gen}}_{h,m,y,z,e}`) processes:

.. math::
    
    {\\rm{storage}}_{h,m,y,z,e}^{\\rm{energy}}-
    {\\rm{storage}}_{h-1,m,y,z,e}^{\\rm{energy}}
    ={\\rm{charge}}_{h,m,y,z,e}-{\\rm{gen}}_{h,m,y,z,e}

In addition, the initial (when :math:`h=h_{\rm{start}}`) stored electricity
(:math:`{\\rm{storage}}_{h=h_{\\rm{start}},m,y,z,e}^{\\rm{energy}}`) of
storage technology :math:`e` in each month of each year can be calculated
based on the proportion of the maximum storage capacity, as follows:

.. math::

    {\\rm{storage}}_{h=h_{\\rm{start}},m,y,z,e}^{\\rm{energy}}
    ={{\\rm{STORAGE}}}_{m,y,z,e}^{\\rm{energy}}\\times{{\\rm{EP}}}_e\\times
    {\\rm{cap}}_{y,z,e}^{\\rm{existingtech}}\\quad\\forall m,y,z,e\\in
    {\\mathcal{STOR}}

The instantaneous storage energy level
(:math:`{\\rm{storage}}_{h,m,y,z,e}^{\\rm{energy}}`) of storage technology
:math:`e` should not exceed the maximum energy storage capacity, as follows:

.. math::

    {\\rm{storage}}_{h,m,y,z,e}^{\\rm{energy}}\\le{{\\rm{EP}}}_e\\times
    {\\rm{cap}}_{y,z,e}^{\\rm{existingtech}}
    \\quad\\forall h,m,y,z,e\\in {\\mathcal{STOR}}
"""

from typing import Union

import pyoptinterface as poi

from prepshot.utils import sparse_tupledict

[文档]class AddStorageConstraints: """Energy storage class. """
[文档] def __init__(self, model : object) -> None: """Initialize the class and add constraints. Parameters ---------- model : object Model object depending on the solver. """ self.model = model model.energy_storage_balance_cons = poi.make_tupledict( model.hour, model.month, model.year, model.zone, model.storage_tech, rule=self.energy_storage_balance_rule ) model.init_energy_storage_cons = poi.make_tupledict( model.month, model.year, model.zone, model.storage_tech, rule=self.init_energy_storage_rule ) model.end_energy_storage_cons = poi.make_tupledict( model.month, model.year, model.zone, model.storage_tech, rule=self.end_energy_storage_rule ) model.energy_storage_up_bound_cons = poi.make_tupledict( model.hour, model.month, model.year, model.zone, model.storage_tech, rule=self.energy_storage_up_bound_rule ) model.energy_storage_gen_cons = poi.make_tupledict( model.hour, model.month, model.year, model.zone, model.storage_tech, rule=self.energy_storage_gen_rule ) # Iterate sparsely over (h, m, y, z, te) where (z, te) is in # ``model.active_zt_storage``. Matches the sparse creation of # ``model.charge`` in model.py: accessing inactive pairs would # KeyError, and the bound on those would be ``0 <= 0`` anyway. model.charge_up_bound_cons = sparse_tupledict( model.active_hmyzte_storage, self.charge_up_bound_rule )
[文档] def energy_storage_balance_rule( self, h : int, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """Energy storage balance. Parameters ---------- h : int Hour. m : int Month. y : int Year. z : str Zone. te : str Technology. Returns ------- poi.ConstraintIndex The constraint of the model. """ model = self.model de = model.params['discharge_efficiency'][te, y] ce = model.params['charge_efficiency'][te, y] return model.add_linear_constraint( model.storage[h, m, y, z, te] - ( model.storage[h-1, m, y, z, te] - model.gen[h, m, y, z, te] / de + model.charge[h, m, y, z, te] * ce ), poi.Eq, 0 )
[文档] def init_energy_storage_rule( self, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """Initial energy storage. Parameters ---------- m : int Month. y : int Year. z : str Zone. te : str Technology. Returns ------- poi.ConstraintIndex The constraint of the model. """ model = self.model esl = model.params['initial_energy_storage_level'][te, z] epr = model.params['energy_to_power_ratio'][te] dt = model.params['dt'] # storage[hour_p[0], ...] is the prior-hour anchor: hour 0 in # CEM mode (where model.hour starts at 1), or h_first - 1 in a # PCM rolling-horizon window. lhs = ( model.storage[model.hour_p[0], m, y, z, te] - esl * model.cap_existing[y, z, te] * epr * dt ) return model.add_linear_constraint(lhs, poi.Eq, 0)
[文档] def end_energy_storage_rule( self, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """End energy storage. Parameters ---------- m : int Month. y : int Year. z : str Zone. te : str Technology. Returns ------- poi.ConstraintIndex The constraint of the model. """ model = self.model # PCM windows want SOC to flow into the next window, so skip # the cyclical close-the-loop equality. Storage upper / lower # bounds keep the trajectory inside [0, cap]. if model.params.get('skip_end_storage', False): return None h_init = model.params['hour'][-1] lhs = ( model.storage[h_init, m, y, z, te] - model.storage[model.hour_p[0], m, y, z, te] ) return model.add_linear_constraint(lhs, poi.Eq, 0)
[文档] def energy_storage_up_bound_rule( self, h : int, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """Energy storage upper bound. Parameters ---------- h : int Hour. m : int Month. y : int Year. z : str Zone. te : str Technology. Returns ------- poi.ConstraintIndex The constraint of the model. """ model = self.model epr = model.params['energy_to_power_ratio'][te] dt = model.params['dt'] lhs = ( model.storage[h, m, y, z, te] - model.cap_existing[y, z, te] * epr * dt ) return model.add_linear_constraint(lhs, poi.Leq, 0)
[文档] def charge_up_bound_rule( self, h : int, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """charge[h,m,y,z,te] <= cap_existing * dt. Symmetric to gen_up_bound_rule on the discharge side. Bounds grid-side charging energy per timestep by the installed power capacity (storage is assumed symmetric: same power rating for charge and discharge, per the module docstring). Without this rule, charge is implicitly capped only by the SOC ceiling -- letting a 1 MW / 10 MWh battery draw 10 MW for one hour. """ model = self.model lhs = ( model.charge[h, m, y, z, te] - model.cap_existing[y, z, te] * model.params['dt'] ) return model.add_linear_constraint(lhs, poi.Leq, 0)
[文档] def energy_storage_gen_rule( self, h : int, m : int, y : int, z : str, te : str ) -> poi.ConstraintIndex: """Energy storage generation. Parameters ---------- h : int Hour. m : int Month. y : int Year. z : str Zone. te : str Technology. Returns ------- poi.ConstraintIndex The constraint of the model. """ model = self.model de = model.params['discharge_efficiency'][te, y] lhs = model.gen[h, m, y, z, te] / de \ - model.storage[h-1, m, y, z, te] return model.add_linear_constraint(lhs, poi.Leq, 0)