Source code for oemof.solph.custom.sink_dsm

# -*- coding: utf-8 -*-

"""
In-development functionality for demand-side management.

SPDX-FileCopyrightText: Uwe Krien <krien@uni-bremen.de>
SPDX-FileCopyrightText: Simon Hilpert
SPDX-FileCopyrightText: Cord Kaldemeyer
SPDX-FileCopyrightText: Patrik Schönfeldt
SPDX-FileCopyrightText: Johannes Röder
SPDX-FileCopyrightText: jakob-wo
SPDX-FileCopyrightText: gplssm
SPDX-FileCopyrightText: jnnr
SPDX-FileCopyrightText: Johannes Kochems (jokochems)

SPDX-License-Identifier: MIT

"""
import itertools

from numpy import mean
from pyomo.core.base.block import SimpleBlock
from pyomo.environ import BuildAction
from pyomo.environ import Constraint
from pyomo.environ import Expression
from pyomo.environ import NonNegativeReals
from pyomo.environ import Set
from pyomo.environ import Var

from oemof.solph.network import Sink
from oemof.solph.options import Investment
from oemof.solph.plumbing import sequence


[docs]class SinkDSM(Sink): r""" Demand Side Management implemented as Sink with flexibility potential. There are several approaches possible which can be selected: - DIW: Based on the paper by Zerrahn, Alexander and Schill, Wolf-Peter (2015): `On the representation of demand-side management in power system models <https://doi.org/10.1016/j.energy.2015.03.037>`_, in: Energy (84), pp. 840-845, 10.1016/j.energy.2015.03.037, accessed 08.01.2021, pp. 842-843. - DLR: Based on the PhD thesis of Gils, Hans Christian (2015): `Balancing of Intermittent Renewable Power Generation by Demand Response and Thermal Energy Storage`, Stuttgart, <http://dx.doi.org/10.18419/opus-6888>, accessed 08.01.2021, pp. 67-70. - oemof: Created by Julian Endres. A fairly simple DSM representation which demands the energy balance to be levelled out in fixed cycles An evaluation of different modeling approaches has been carried out and presented at the INREC 2020. Some of the results are as follows: - DIW: A solid implementation with the tendency of slight overestimization of potentials since a shift_time is not accounted for. It may get computationally expensive due to a high time-interlinkage in constraint formulations. - DLR: An extensive modeling approach for demand response which neither leads to an over- nor underestimization of potentials and balances modeling detail and computation intensity. :attr:`fixes` and :attr:`addition` should both be set to True which is the default value. - oemof: A very computationally efficient approach which only requires the energy balance to be levelled out in certain intervals. If demand response is not at the center of the research and/or parameter availability is limited, this approach should be chosen. Note that approach `oemof` does allow for load shedding, but does not impose a limit on maximum amount of shedded energy. SinkDSM adds additional constraints that allow to shift energy in certain time window constrained by :attr:`~capacity_up` and :attr:`~capacity_down`. Parameters ---------- demand: numeric original electrical demand (normalized) For investment modeling, it is advised to use the maximum of the demand timeseries and the cumulated (fixed) infeed time series for normalization, because the balancing potential may be determined by both. Elsewhise, underinvestments may occur. capacity_up: int or array maximum DSM capacity that may be increased (normalized) capacity_down: int or array maximum DSM capacity that may be reduced (normalized) approach: 'oemof', 'DIW', 'DLR' Choose one of the DSM modeling approaches. Read notes about which parameters to be applied for which approach. oemof : Simple model in which the load shift must be compensated in a predefined fixed interval (:attr:`~shift_interval` is mandatory). Within time windows of the length :attr:`~shift_interval` DSM up and down shifts are balanced. See :class:`~SinkDSMOemofBlock` for details. DIW : Sophisticated model based on the formulation by Zerrahn & Schill (2015a). The load shift of the component must be compensated in a predefined delay time (:attr:`~delay_time` is mandatory). For details see :class:`~SinkDSMDIWBlock`. DLR : Sophisticated model based on the formulation by Gils (2015). The load shift of the component must be compensated in a predefined delay time (:attr:`~delay_time` is mandatory). For details see :class:`~SinkDSMDLRBlock`. shift_interval: int Only used when :attr:`~approach` is set to 'oemof'. Otherwise, can be None. It's the interval in which between :math:`DSM_{t}^{up}` and :math:`DSM_{t}^{down}` have to be compensated. delay_time: int Only used when :attr:`~approach` is set to 'DIW' or 'DLR'. Otherwise, can be None. Length of symmetrical time windows around :math:`t` in which :math:`DSM_{t}^{up}` and :math:`DSM_{t,tt}^{down}` have to be compensated. Note: For approach 'DLR', an iterable is constructed in order to model flexible delay times shift_time: int Only used when :attr:`~approach` is set to 'DLR'. Duration of a single upwards or downwards shift (half a shifting cycle if there is immediate compensation) shed_time: int Only used when :attr:`~shed_eligibility` is set to True. Maximum length of a load shedding process at full capacity (used within energy limit constraint) max_demand: numeric Maximum demand prior to demand response max_capacity_down: numeric Maximum capacity eligible for downshifts prior to demand response (used for dispatch mode) max_capacity_up: numeric Maximum capacity eligible for upshifts prior to demand response (used for dispatch mode) flex_share_down: float Flexible share of installed capacity eligible for downshifts (used for invest mode) flex_share_up: float Flexible share of installed capacity eligible for upshifts (used for invest mode) cost_dsm_up : int Cost per unit of DSM activity that increases the demand cost_dsm_down_shift : int Cost per unit of DSM activity that decreases the demand for load shifting cost_dsm_down_shed : int Cost per unit of DSM activity that decreases the demand for load shedding efficiency : float Efficiency factor for load shifts (between 0 and 1) recovery_time_shift : int Only used when :attr:`~approach` is set to 'DIW'. Minimum time between the end of one load shifting process and the start of another for load shifting processes recovery_time_shed : int Only used when :attr:`~approach` is set to 'DIW'. Minimum time between the end of one load shifting process and the start of another for load shedding processes ActivateYearLimit : boolean Only used when :attr:`~approach` is set to 'DLR'. Control parameter; activates constraints for year limit if set to True ActivateDayLimit : boolean Only used when :attr:`~approach` is set to 'DLR'. Control parameter; activates constraints for day limit if set to True n_yearLimit_shift : int Only used when :attr:`~approach` is set to 'DLR'. Maximum number of load shifts at full capacity per year, used to limit the amount of energy shifted per year. Optional parameter that is only needed when ActivateYearLimit is True n_yearLimit_shed : int Only used when :attr:`~approach` is set to 'DLR'. Maximum number of load sheds at full capacity per year, used to limit the amount of energy shedded per year. Mandatory parameter if load shedding is allowed by setting shed_eligibility to True t_dayLimit: int Only used when :attr:`~approach` is set to 'DLR'. Maximum duration of load shifts at full capacity per day, used to limit the amount of energy shifted per day. Optional parameter that is only needed when ActivateDayLimit is True addition : boolean Only used when :attr:`~approach` is set to 'DLR'. Boolean parameter indicating whether or not to include additional constraint (which corresponds to Eq. 10 from Zerrahn and Schill (2015a) fixes : boolean Only used when :attr:`~approach` is set to 'DLR'. Boolean parameter indicating whether or not to include additional fixes. These comprise prohibiting shifts which cannot be balanced within the optimization timeframe shed_eligibility : boolean Boolean parameter indicating whether unit is eligible for load shedding shift_eligibility : boolean Boolean parameter indicating whether unit is eligible for load shifting Note ---- * :attr:`method` has been renamed to :attr:`approach`. * As many constraints and dependencies are created in approach 'DIW', computational cost might be high with a large 'delay_time' and with model of high temporal resolution * The approach 'DLR' preforms better in terms of calculation time, compared to the approach 'DIW' * Using :attr:`~approach` 'DIW' or 'DLR' might result in demand shifts that exceed the specified delay time by activating up and down simultaneously in the time steps between to DSM events. Thus, the purpose of this component is to model demand response portfolios rather than individual demand units. * It's not recommended to assign cost to the flow that connects :class:`~SinkDSM` with a bus. Instead, use :attr:`~SinkDSM.cost_dsm_up` or :attr:`~cost_dsm_down_shift` * Variable costs may be attributed to upshifts, downshifts or both. Costs for shedding may deviate from that for shifting (usually costs for shedding are much larger and equal to the value of lost load). """ def __init__( self, demand, capacity_up, capacity_down, approach, shift_interval=None, delay_time=None, shift_time=None, shed_time=None, max_demand=None, max_capacity_down=None, max_capacity_up=None, flex_share_down=None, flex_share_up=None, cost_dsm_up=0, cost_dsm_down_shift=0, cost_dsm_down_shed=0, efficiency=1, recovery_time_shift=None, recovery_time_shed=None, ActivateYearLimit=False, ActivateDayLimit=False, n_yearLimit_shift=None, n_yearLimit_shed=None, t_dayLimit=None, addition=True, fixes=True, shed_eligibility=True, shift_eligibility=True, **kwargs, ): super().__init__(**kwargs) self.capacity_up = sequence(capacity_up) self.capacity_down = sequence(capacity_down) self.demand = sequence(demand) self.approach = approach self.shift_interval = shift_interval if not approach == "DLR": self.delay_time = delay_time else: self.delay_time = [el for el in range(1, delay_time + 1)] self.shift_time = shift_time self.shed_time = shed_time # Attributes are only needed if no investments occur self.max_capacity_down = max_capacity_down self.max_capacity_up = max_capacity_up self.max_demand = max_demand # Attributes for investment modeling if flex_share_down is not None: if max_capacity_down is None and max_demand is None: self.flex_share_down = flex_share_down else: e1 = ( "Please determine either **flex_share_down " "(investment modeling)\n or set " "**max_demand and **max_capacity_down " "(dispatch modeling).\n" "Otherwise, overdetermination occurs." ) raise AttributeError(e1) else: if max_capacity_down is None or max_demand is None: e2 = ( "If you do not specify **flex_share_down\n" "which should be used for investment modeling,\n" "you have to specify **max_capacity_down " "and **max_demand\n" "instead which should be used for dispatch modeling." ) raise AttributeError(e2) else: self.flex_share_down = self.max_capacity_down / self.max_demand if flex_share_up is not None: if max_capacity_up is None and max_demand is None: self.flex_share_up = flex_share_up else: e3 = ( "Please determine either flex_share_up " "(investment modeling)\n or set " "max_demand and max_capacity_up (dispatch modeling).\n" "Otherwise, overdetermination occurs." ) raise AttributeError(e3) else: if max_capacity_up is None or max_demand is None: e4 = ( "If you do not specify **flex_share_up\n" "which should be used for investment modeling,\n" "you have to specify **max_capacity_up " "and **max_demand\n" "instead which should be used for dispatch modeling." ) raise AttributeError(e4) else: self.flex_share_up = self.max_capacity_up / self.max_demand self.cost_dsm_up = sequence(cost_dsm_up) self.cost_dsm_down_shift = sequence(cost_dsm_down_shift) self.cost_dsm_down_shed = sequence(cost_dsm_down_shed) self.efficiency = efficiency self.capacity_down_mean = mean(capacity_down) self.capacity_up_mean = mean(capacity_up) self.recovery_time_shift = recovery_time_shift self.recovery_time_shed = recovery_time_shed self.ActivateYearLimit = ActivateYearLimit self.ActivateDayLimit = ActivateDayLimit self.n_yearLimit_shift = n_yearLimit_shift self.n_yearLimit_shed = n_yearLimit_shed self.t_dayLimit = t_dayLimit self.addition = addition self.fixes = fixes self.shed_eligibility = shed_eligibility self.shift_eligibility = shift_eligibility # Check whether investment mode is active or not self.investment = kwargs.get("investment") self._invest_group = isinstance(self.investment, Investment) if ( self.max_demand is None or self.max_capacity_up is None or self.max_capacity_down is None ) and not self._invest_group: e5 = ( "If you are setting up a dispatch model, " "you have to specify **max_demand**, **max_capacity_up** " "and **max_capacity_down**.\n" "The values you might have passed for **flex_share_up** " "and **flex_share_down** will be ignored and only used in " "an investment model." ) raise AttributeError(e5) if self._invest_group: self._check_invest_attributes() def _check_invest_attributes(self): if ( self.investment is not None and ( self.max_demand or self.max_capacity_down or self.max_capacity_up ) is not None ): e6 = ( "If an investment object is defined, the invest variable " "replaces the **max_demand, the **max_capacity_down " "as well as\n" "the **max_capacity_up values. Therefore, **max_demand,\n" "**max_capacity_up and **max_capacity_down values should be " "'None'.\n" ) raise AttributeError(e6)
[docs] def constraint_group(self): possible_approaches = ["DIW", "DLR", "oemof"] if self.approach in [possible_approaches[0], possible_approaches[1]]: if self.delay_time is None: raise ValueError( "Please define: **delay_time" " is a mandatory parameter" ) if not self.shed_eligibility and not self.shift_eligibility: raise ValueError( "At least one of **shed_eligibility" " and **shift_eligibility must be True" ) if self.shed_eligibility: if self.recovery_time_shed is None: raise ValueError( "If unit is eligible for load shedding," " **recovery_time_shed must be defined" ) if self.approach == possible_approaches[0]: if self._invest_group is True: return SinkDSMDIWInvestmentBlock else: return SinkDSMDIWBlock elif self.approach == possible_approaches[1]: if self._invest_group is True: return SinkDSMDLRInvestmentBlock else: return SinkDSMDLRBlock elif self.approach == possible_approaches[2]: if self.shift_interval is None: raise ValueError( "Please define: **shift_interval" " is a mandatory parameter" ) if self._invest_group is True: return SinkDSMOemofInvestmentBlock else: return SinkDSMOemofBlock else: raise ValueError( 'The "approach" must be one of the following set: ' '"{}"'.format('" or "'.join(possible_approaches)) )
[docs]class SinkDSMOemofBlock(SimpleBlock): r"""Constraints for SinkDSM with "oemof" approach **The following constraints are created for approach = 'oemof':** .. _SinkDSMOemof equations: .. math:: & (1) \quad DSM_{t}^{up} = 0 \quad \forall t \quad if \space eligibility_{shift} = False \\ & (2) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \quad if \space eligibility_{shed} = False \\ & (3) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max} + DSM_{t}^{up} - DSM_{t}^{do, shift} - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (4) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot E_{up, max} \quad \forall t \in \mathbb{T} \\ & (5) \quad DSM_{t}^{do, shift} + DSM_{t}^{do, shed} \leq E_{t}^{do} \cdot E_{do, max} \quad \forall t \in \mathbb{T} \\ & (6) \quad \sum_{t=t_s}^{t_s+\tau} DSM_{t}^{up} \cdot \eta = \sum_{t=t_s}^{t_s+\tau} DSM_{t}^{do, shift} \quad \forall t_s \in \{k \in \mathbb{T} \mid k \mod \tau = 0\} \\ & **The following parts of the objective function are created:** .. math:: DSM_{t}^{up} \cdot cost_{t}^{dsm, up} + DSM_{t}^{do, shift} \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} \\ **Table: Symbols and attribute names of variables and parameters** .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`DSM_{t}^{up}` ", ":attr:`~SinkDSM.dsm_up[g, t]` ","V", "DSM up shift (capacity shifted upwards)" ":math:`DSM_{t}^{do, shift}` ", ":attr:`~SinkDSM.dsm_do_shift[g, t]` ", "V","DSM down shift (capacity shifted downwards)" ":math:`DSM_{t}^{do, shed}` ", ":attr:`~SinkDSM.dsm_do_shed[g, t]` ", "V","DSM shedded (capacity shedded, i.e. not compensated for)" ":math:`\dot{E}_{t}`",":attr:`~SinkDSM.inputs`","V", "Energy flowing in from (electrical) inflow bus" ":math:`demand_{t}`",":attr:`~SinkDSM.demand[t]`","P", "(Electrical) demand series (normalized)" ":math:`demand_{max}`",":attr:`~SinkDSM.max_demand`","P", "Maximum demand value" ":math:`E_{t}^{do}`",":attr:`~SinkDSM.capacity_down[t]`","P", "Capacity allowed for a load adjustment downwards (normalized) (DSM down shift + DSM shedded)" ":math:`E_{t}^{up}`",":attr:`~SinkDSM.capacity_up[t]`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`E_{do, max}`",":attr:`~SinkDSM.max_capacity_down`","P", "Maximum capacity allowed for a load adjustment downwards (DSM down shift + DSM shedded)" ":math:`E_{up, max}`",":attr:`~SinkDSM.max_capacity_up`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`\tau`",":attr:`~SinkDSM.shift_interval`","P", "Shift interval (time within which the energy balance must be levelled out" ":math:`\eta`",":attr:`~SinkDSM.efficiency`","P", "Efficiency loss forload shifting processes" ":math:`\mathbb{T}` "," ","P", "Time steps" ":math:`eligibility_{shift}` ", ":attr:`~SinkDSM.shift_eligibility`","P", "Boolean parameter indicating if unit can be used for load shifting" ":math:`eligibility_{shed}` ", ":attr:`~SinkDSM.shed_eligibility`","P", "Boolean parameter indicating if unit can be used for load shedding" ":math:`cost_{t}^{dsm, up}` ", ":attr:`~SinkDSM.cost_dsm_up[t]`", "P", "Variable costs for an upwards shift" ":math:`cost_{t}^{dsm, do, shift}` ", ":attr:`~SinkDSM.cost_dsm_down_shift[t]`","P", "Variable costs for a downwards shift (load shifting)" ":math:`cost_{t}^{dsm, do, shed}` ", ":attr:`~SinkDSM.cost_dsm_down_shed[t]`","P", "Variable costs for shedding load" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* # Set of DSM Components self.dsm = Set(initialize=[n for n in group]) # ************* VARIABLES ***************************** # Variable load shift down self.dsm_do_shift = Var( self.dsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shedding self.dsm_do_shed = Var( self.dsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up self.dsm_up = Var( self.dsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: if not g.shift_eligibility: lhs = self.dsm_up[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) self.shift_shed_vars = Constraint(group, m.TIMESTEPS, noruleinit=True) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Demand Production Relation def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DSM. Generator Production == Demand_el +- DSM """ for t in m.TIMESTEPS: for g in group: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand + DSM_up - DSM_down rhs = ( g.demand[t] * g.max_demand + self.dsm_up[g, t] - self.dsm_do_shift[g, t] - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Upper bounds relation def dsm_up_constraint_rule(block): """Realised upward load shift at time t has to be smaller than upward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM up lhs = self.dsm_up[g, t] # Capacity dsm_up rhs = g.capacity_up[t] * g.max_capacity_up # add constraint block.dsm_up_constraint.add((g, t), (lhs <= rhs)) self.dsm_up_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_up_constraint_build = BuildAction(rule=dsm_up_constraint_rule) # Upper bounds relation def dsm_down_constraint_rule(block): """Realised downward load shift at time t has to be smaller than downward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM down lhs = self.dsm_do_shift[g, t] + self.dsm_do_shed[g, t] # Capacity dsm_down rhs = g.capacity_down[t] * g.max_capacity_down # add constraint block.dsm_down_constraint.add((g, t), (lhs <= rhs)) self.dsm_down_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_down_constraint_build = BuildAction( rule=dsm_down_constraint_rule ) def dsm_sum_constraint_rule(block): """Relation to compensate the total amount of positive and negative DSM in between the shift_interval. This constraint is building balance in full intervals starting with index 0. The last interval might not be full. """ for g in group: intervals = range( m.TIMESTEPS[1], m.TIMESTEPS[-1], g.shift_interval ) for interval in intervals: if (interval + g.shift_interval - 1) > m.TIMESTEPS[-1]: timesteps = range(interval, m.TIMESTEPS[-1] + 1) else: timesteps = range( interval, interval + g.shift_interval ) # DSM up/down lhs = ( sum(self.dsm_up[g, tt] for tt in timesteps) * g.efficiency ) # value rhs = sum(self.dsm_do_shift[g, tt] for tt in timesteps) # add constraint block.dsm_sum_constraint.add((g, interval), (lhs == rhs)) self.dsm_sum_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_sum_constraint_build = BuildAction( rule=dsm_sum_constraint_rule ) def _objective_expression(self): r"""Objective expression with variable costs for DSM activity""" m = self.parent_block() dsm_cost = 0 for t in m.TIMESTEPS: for g in self.dsm: dsm_cost += ( self.dsm_up[g, t] * g.cost_dsm_up[t] * m.objective_weighting[t] ) dsm_cost += ( self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=dsm_cost) return self.cost
[docs]class SinkDSMOemofInvestmentBlock(SimpleBlock): r"""Constraints for SinkDSM with "oemof" approach and :attr:`investment` **The following constraints are created for approach = 'oemof' with an investment object defined:** .. _SinkDSMOemof equations: .. math:: & (1) \quad invest_{min} \leq invest \leq invest_{max} \\ & (2) \quad DSM_{t}^{up} = 0 \quad \forall t \quad if \space eligibility_{shift} = False \\ & (3) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \quad if \space eligibility_{shed} = False \\ & (4) \quad \dot{E}_{t} = demand_{t} \cdot (invest + E_{exist}) + DSM_{t}^{up} - DSM_{t}^{do, shift} - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (5) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot (invest + E_{exist}) \cdot s_{flex, up} \quad \forall t \in \mathbb{T} \\ & (6) \quad DSM_{t}^{do, shift} + DSM_{t}^{do, shed} \leq E_{t}^{do} \cdot (invest + E_{exist}) \cdot s_{flex, do} \quad \forall t \in \mathbb{T} \\ & (7) \quad \sum_{t=t_s}^{t_s+\tau} DSM_{t}^{up} \cdot \eta = \sum_{t=t_s}^{t_s+\tau} DSM_{t}^{do, shift} \quad \forall t_s \in \{k \in \mathbb{T} \mid k \mod \tau = 0\} \\ & **The following parts of the objective function are created:** * Investment annuity: .. math:: invest \cdot costs_{invest} \\ * Variable costs: .. math:: DSM_{t}^{up} \cdot cost_{t}^{dsm, up} + DSM_{t}^{do, shift} \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} \\ See remarks in :class:`oemof.solph.custom.SinkDSMOemofBlock`. **Symbols and attribute names of variables and parameters** Please refer to :class:`oemof.solph.custom.SinkDSMOemofBlock`. The following variables and parameters are exclusively used for investment modeling: .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`invest` ",":attr:`~SinkDSM.invest` ","V", "DSM capacity invested in. Equals to the additionally installed capacity. The capacity share eligible for a shift is determined by flex share(s)." ":math:`invest_{min}` ", ":attr:`~SinkDSM.investment.minimum` ", "P", "minimum investment" ":math:`invest_{max}` ", ":attr:`~SinkDSM.investment.maximum` ", "P", "maximum investment" ":math:`E_{exist}` ",":attr:`~SinkDSM.investment.existing` ", "P", "existing DSM capacity" ":math:`s_{flex, up}` ",":attr:`~SinkDSM.flex_share_up` ", "P","Share of invested capacity that may be shift upwards at maximum" ":math:`s_{flex, do}` ",":attr:`~SinkDSM.flex_share_do` ", "P", "Share of invested capacity that may be shift downwards at maximum" ":math:`costs_{invest}` ",":attr:`~SinkDSM.investment.epcosts` ", "P", "specific investment annuity" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* # Set of DSM Components self.investdsm = Set(initialize=[n for n in group]) # ************* VARIABLES ***************************** # Define bounds for investments in demand response def _dsm_investvar_bound_rule(block, g): """Rule definition to bound the invested demand response capacity `invest`. """ return g.investment.minimum, g.investment.maximum # Investment in DR capacity self.invest = Var( self.investdsm, within=NonNegativeReals, bounds=_dsm_investvar_bound_rule, ) # Variable load shift down self.dsm_do_shift = Var( self.investdsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shedding self.dsm_do_shed = Var( self.investdsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up self.dsm_up = Var( self.investdsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: if not g.shift_eligibility: lhs = self.dsm_up[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) self.shift_shed_vars = Constraint(group, m.TIMESTEPS, noruleinit=True) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Demand Production Relation def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DSM. Generator Production == Demand_el +- DSM """ for t in m.TIMESTEPS: for g in group: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand + DSM_up - DSM_down rhs = ( g.demand[t] * (self.invest[g] + g.investment.existing) + self.dsm_up[g, t] - self.dsm_do_shift[g, t] - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Upper bounds relation def dsm_up_constraint_rule(block): """Realised upward load shift at time t has to be smaller than upward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM up lhs = self.dsm_up[g, t] # Capacity dsm_up rhs = ( g.capacity_up[t] * (self.invest[g] + g.investment.existing) * g.flex_share_up ) # add constraint block.dsm_up_constraint.add((g, t), (lhs <= rhs)) self.dsm_up_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_up_constraint_build = BuildAction(rule=dsm_up_constraint_rule) # Upper bounds relation def dsm_down_constraint_rule(block): """Realised downward load shift at time t has to be smaller than downward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM down lhs = self.dsm_do_shift[g, t] + self.dsm_do_shed[g, t] # Capacity dsm_down rhs = ( g.capacity_down[t] * (self.invest[g] + g.investment.existing) * g.flex_share_down ) # add constraint block.dsm_down_constraint.add((g, t), (lhs <= rhs)) self.dsm_down_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_down_constraint_build = BuildAction( rule=dsm_down_constraint_rule ) def dsm_sum_constraint_rule(block): """Relation to compensate the total amount of positive and negative DSM in between the shift_interval. This constraint is building balance in full intervals starting with index 0. The last interval might not be full. """ for g in group: intervals = range( m.TIMESTEPS[1], m.TIMESTEPS[-1], g.shift_interval ) for interval in intervals: if (interval + g.shift_interval - 1) > m.TIMESTEPS[-1]: timesteps = range(interval, m.TIMESTEPS[-1] + 1) else: timesteps = range( interval, interval + g.shift_interval ) # DSM up/down lhs = ( sum(self.dsm_up[g, tt] for tt in timesteps) * g.efficiency ) # value rhs = sum(self.dsm_do_shift[g, tt] for tt in timesteps) # add constraint block.dsm_sum_constraint.add((g, interval), (lhs == rhs)) self.dsm_sum_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_sum_constraint_build = BuildAction( rule=dsm_sum_constraint_rule ) def _objective_expression(self): r"""Objective expression with variable and investment costs for DSM""" m = self.parent_block() investment_costs = 0 variable_costs = 0 for g in self.investdsm: if g.investment.ep_costs is not None: investment_costs += self.invest[g] * g.investment.ep_costs else: raise ValueError("Missing value for investment costs!") for t in m.TIMESTEPS: variable_costs += ( self.dsm_up[g, t] * g.cost_dsm_up[t] * m.objective_weighting[t] ) variable_costs += ( self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=investment_costs + variable_costs) return self.cost
[docs]class SinkDSMDIWBlock(SimpleBlock): r"""Constraints for SinkDSM with "DIW" approach **The following constraints are created for approach = 'DIW':** .. _SinkDSMDIW equations: .. math:: & (1) \quad DSM_{t}^{up} = 0 \quad \forall t \quad if \space eligibility_{shift} = False \\ & (2) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \quad if \space eligibility_{shed} = False \\ & (3) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max} + DSM_{t}^{up} - \sum_{tt=t-L}^{t+L} DSM_{tt,t}^{do, shift} - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (4) \quad DSM_{t}^{up} \cdot \eta = \sum_{tt=t-L}^{t+L} DSM_{t,tt}^{do, shift} \quad \forall t \in \mathbb{T} \\ & (5) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot E_{up, max} \quad \forall t \in \mathbb{T} \\ & (6) \quad \sum_{t=tt-L}^{tt+L} DSM_{t,tt}^{do, shift} + DSM_{tt}^{do, shed} \leq E_{tt}^{do} \cdot E_{do, max} \quad \forall tt \in \mathbb{T} \\ & (7) \quad DSM_{tt}^{up} + \sum_{t=tt-L}^{tt+L} DSM_{t,tt}^{do, shift} + DSM_{tt}^{do, shed} \leq max \{ E_{tt}^{up} \cdot E_{up, max}, E_{tt}^{do} \cdot E_{do, max} \} \quad \forall tt \in \mathbb{T} \\ & (8) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{up} \leq E_{t}^{up} \cdot E_{up, max} \cdot L \cdot \Delta t \quad \forall t \in \mathbb{T} \\ & (9) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{do, shed} \leq E_{t}^{do} \cdot E_{do, max} \cdot t_{shed} \cdot \Delta t \quad \forall t \in \mathbb{T} \\ & *Note*: For the sake of readability, the handling of indices is not displayed here. E.g. evaluating a variable for t-L may lead to a negative and therefore infeasible index. This is addressed by limiting the sums to non-negative indices within the model index bounds. Please refer to the constraints implementation themselves. **The following parts of the objective function are created:** .. math:: DSM_{t}^{up} \cdot cost_{t}^{dsm, up} + \sum_{tt=0}^{|T|} DSM_{t, tt}^{do, shift} \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} \\ **Table: Symbols and attribute names of variables and parameters** .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`DSM_{t}^{up}` ",":attr:`~SinkDSM.dsm_up[g,t]`", "V", "DSM up shift (additional load) in hour t" ":math:`DSM_{t,tt}^{do, shift}` ", ":attr:`~SinkDSM.dsm_do_shift[g,t,tt]`", "V", "DSM down shift (less load) in hour tt to compensate for upwards shifts in hour t" ":math:`DSM_{t}^{do, shed}` ",":attr:`~SinkDSM.dsm_do_shed[g,t]` ", "V","DSM shedded (capacity shedded, i.e. not compensated for)" ":math:`\dot{E}_{t}` ",":attr:`flow[g,t]`","V","Energy flowing in from (electrical) inflow bus" ":math:`L`",":attr:`~SinkDSM.delay_time`","P", "Maximum delay time for load shift (time until the energy balance has to be levelled out again; roundtrip time of one load shifting cycle, i.e. time window for upshift and compensating downshift)" ":math:`t_{she}`",":attr:`~SinkDSM.shed_time`","P", "Maximum time for one load shedding process" ":math:`demand_{t}`",":attr:`~SinkDSM.demand[t]`","P", "(Electrical) demand series (normalized)" ":math:`demand_{max}`",":attr:`~SinkDSM.max_demand`","P", "Maximum demand value" ":math:`E_{t}^{do}`",":attr:`~SinkDSM.capacity_down[t]`","P", "Capacity allowed for a load adjustment downwards (normalized) (DSM down shift + DSM shedded)" ":math:`E_{t}^{up}`",":attr:`~SinkDSM.capacity_up[t]`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`E_{do, max}`",":attr:`~SinkDSM.max_capacity_down`","P", "Maximum capacity allowed for a load adjustment downwards (DSM down shift + DSM shedded)" ":math:`E_{up, max}`",":attr:`~SinkDSM.max_capacity_up`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`\eta`",":attr:`~SinkDSM.efficiency`","P", "Efficiency loss for load shifting processes" ":math:`\mathbb{T}` "," ","P", "Time steps" ":math:`eligibility_{shift}` ", ":attr:`~SinkDSM.shift_eligibility`","P", "Boolean parameter indicating if unit can be used for load shifting" ":math:`eligibility_{shed}` ", ":attr:`~SinkDSM.shed_eligibility`","P", "Boolean parameter indicating if unit can be used for load shedding" ":math:`cost_{t}^{dsm, up}` ", ":attr:`~SinkDSM.cost_dsm_up[t]`", "P", "Variable costs for an upwards shift" ":math:`cost_{t}^{dsm, do, shift}` ", ":attr:`~SinkDSM.cost_dsm_down_shift[t]`","P", "Variable costs for a downwards shift (load shifting)" ":math:`cost_{t}^{dsm, do, shed}` ", ":attr:`~SinkDSM.cost_dsm_down_shed[t]`","P", "Variable costs for shedding load" ":math:`\R`",":attr:`~SinkDSM.recovery_time_shift`","P", "Minimum time between the end of one load shifting process and the start of another" ":math:`\Delta t`",":attr:`~models.Model.timeincrement`","P", "The time increment of the model" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* # Set of DSM Components self.dsm = Set(initialize=[g for g in group]) # ************* VARIABLES ***************************** # Variable load shift down self.dsm_do_shift = Var( self.dsm, m.TIMESTEPS, m.TIMESTEPS, initialize=0, within=NonNegativeReals, ) # Variable load shedding self.dsm_do_shed = Var( self.dsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up self.dsm_up = Var( self.dsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: if not g.shift_eligibility: lhs = self.dsm_up[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) self.shift_shed_vars = Constraint(group, m.TIMESTEPS, noruleinit=True) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Demand Production Relation def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DSM. Sink Inflow == Demand +- DSM """ for t in m.TIMESTEPS: for g in group: # first time steps: 0 + delay time if t <= g.delay_time: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * g.max_demand + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range(t + g.delay_time + 1) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) # main use case elif g.delay_time < t <= m.TIMESTEPS[-1] - g.delay_time: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * g.max_demand + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range( t - g.delay_time, t + g.delay_time + 1 ) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) # last time steps: end - delay time else: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * g.max_demand + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range( t - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Equation 7 (resp. 7') def dsm_up_down_constraint_rule(block): """Equation 7 (resp. 7') by Zerrahn & Schill: Every upward load shift has to be compensated by downward load shifts in a defined time frame. Slightly modified equations for the first and last time steps due to variable initialization. Efficiency value depicts possible energy losses through load shifting (Equation 7'). """ for t in m.TIMESTEPS: for g in group: # first time steps: 0 + delay time if t <= g.delay_time: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range(t + g.delay_time + 1) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) # main use case elif g.delay_time < t <= m.TIMESTEPS[-1] - g.delay_time: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range( t - g.delay_time, t + g.delay_time + 1 ) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) # last time steps: end - delay time else: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range( t - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) self.dsm_updo_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_updo_constraint_build = BuildAction( rule=dsm_up_down_constraint_rule ) # Equation 8 def dsm_up_constraint_rule(block): """Equation 8 by Zerrahn & Schill: Realised upward load shift at time t has to be smaller than upward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM up lhs = self.dsm_up[g, t] # Capacity dsm_up rhs = g.capacity_up[t] * g.max_capacity_up # add constraint block.dsm_up_constraint.add((g, t), (lhs <= rhs)) self.dsm_up_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_up_constraint_build = BuildAction(rule=dsm_up_constraint_rule) # Equation 9 (modified) def dsm_do_constraint_rule(block): """Equation 9 by Zerrahn & Schill: Realised downward load shift at time t has to be smaller than downward DSM capacity at time t. """ for tt in m.TIMESTEPS: for g in group: # first times steps: 0 + delay if tt <= g.delay_time: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range(tt + g.delay_time + 1) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = g.capacity_down[tt] * g.max_capacity_down # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) # main use case elif g.delay_time < tt <= m.TIMESTEPS[-1] - g.delay_time: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, tt + g.delay_time + 1 ) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = g.capacity_down[tt] * g.max_capacity_down # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) # last time steps: end - delay time else: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = g.capacity_down[tt] * g.max_capacity_down # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) self.dsm_do_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_do_constraint_build = BuildAction(rule=dsm_do_constraint_rule) # Equation 10 def c2_constraint_rule(block): """Equation 10 by Zerrahn & Schill: The realised DSM up or down at time T has to be smaller than the maximum downward or upward capacity at time T. Therefore, in total each individual DSM unit within the modeled portfolio can only be shifted up OR down at a given time. """ for tt in m.TIMESTEPS: for g in group: # first times steps: 0 + delay time if tt <= g.delay_time: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range(tt + g.delay_time + 1) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.max_capacity_up, g.capacity_down[tt] * g.max_capacity_down, ) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) elif g.delay_time < tt <= m.TIMESTEPS[-1] - g.delay_time: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, tt + g.delay_time + 1 ) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.max_capacity_up, g.capacity_down[tt] * g.max_capacity_down, ) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) else: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.max_capacity_up, g.capacity_down[tt] * g.max_capacity_down, ) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) self.C2_constraint = Constraint(group, m.TIMESTEPS, noruleinit=True) self.C2_constraint_build = BuildAction(rule=c2_constraint_rule) def recovery_constraint_rule(block): """Equation 11 by Zerrahn & Schill: A recovery time is introduced to account for the fact that there may be some restrictions before the next load shift may take place. Rule is only applicable if a recovery time is defined. """ for t in m.TIMESTEPS: for g in group: # No need to build constraint if no recovery # time is defined. if g.recovery_time_shift not in [None, 0]: # main use case if t <= m.TIMESTEPS[-1] - g.recovery_time_shift: # DSM up lhs = sum( self.dsm_up[g, tt] for tt in range(t, t + g.recovery_time_shift) ) # max energy shift for shifting process rhs = ( g.capacity_up[t] * g.max_capacity_up * g.delay_time * m.timeincrement[t] ) # add constraint block.recovery_constraint.add((g, t), (lhs <= rhs)) # last time steps: end - recovery time else: # DSM up lhs = sum( self.dsm_up[g, tt] for tt in range(t, m.TIMESTEPS[-1] + 1) ) # max energy shift for shifting process rhs = ( g.capacity_up[t] * g.max_capacity_up * g.delay_time * m.timeincrement[t] ) # add constraint block.recovery_constraint.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) self.recovery_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.recovery_constraint_build = BuildAction( rule=recovery_constraint_rule ) # Equation 9a from Zerrahn and Schill (2015b) def shed_limit_constraint_rule(block): """The following constraint is highly similar to equation 9a from Zerrahn and Schill (2015b): A recovery time for load shedding is introduced in order to limit the overall amount of shedded energy. """ for t in m.TIMESTEPS: for g in group: # Only applicable for load shedding if g.shed_eligibility: # main use case if t <= m.TIMESTEPS[-1] - g.recovery_time_shed: # DSM up lhs = sum( self.dsm_do_shed[g, tt] for tt in range(t, t + g.recovery_time_shed) ) # max energy shift for shifting process rhs = ( g.capacity_down[t] * g.max_capacity_down * g.shed_time * m.timeincrement[t] ) # add constraint block.shed_limit_constraint.add( (g, t), (lhs <= rhs) ) # last time steps: end - recovery time else: # DSM up lhs = sum( self.dsm_do_shed[g, tt] for tt in range(t, m.TIMESTEPS[-1] + 1) ) # max energy shift for shifting process rhs = ( g.capacity_down[t] * g.max_capacity_down * g.shed_time * m.timeincrement[t] ) # add constraint block.shed_limit_constraint.add( (g, t), (lhs <= rhs) ) else: pass # return(Constraint.Skip) self.shed_limit_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.shed_limit_constraint_build = BuildAction( rule=shed_limit_constraint_rule ) def _objective_expression(self): r"""Objective expression with variable costs for DSM activity""" m = self.parent_block() dsm_cost = 0 for t in m.TIMESTEPS: for g in self.dsm: dsm_cost += ( self.dsm_up[g, t] * g.cost_dsm_up[t] * m.objective_weighting[t] ) dsm_cost += ( sum(self.dsm_do_shift[g, tt, t] for tt in m.TIMESTEPS) * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=dsm_cost) return self.cost
[docs]class SinkDSMDIWInvestmentBlock(SimpleBlock): r"""Constraints for SinkDSM with "DIW" approach and :attr:`investment` **The following constraints are created for approach = 'DIW' with an investment object defined:** .. _SinkDSMDIW equations: .. math:: & (1) \quad invest_{min} \leq invest \leq invest_{max} \\ & (2) \quad DSM_{t}^{up} = 0 \quad \forall t \quad if \space eligibility_{shift} = False \\ & (3) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \quad if \space eligibility_{shed} = False \\ & (4) \quad \dot{E}_{t} = demand_{t} \cdot (invest + E_{exist}) + DSM_{t}^{up} - \sum_{tt=t-L}^{t+L} DSM_{tt,t}^{do, shift} - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (5) \quad DSM_{t}^{up} \cdot \eta = \sum_{tt=t-L}^{t+L} DSM_{t,tt}^{do, shift} \quad \forall t \in \mathbb{T} \\ & (6) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot (invest + E_{exist}) \ s_{flex, up} \quad \forall t \in \mathbb{T} \\ & (7) \quad \sum_{t=tt-L}^{tt+L} DSM_{t,tt}^{do, shift} + DSM_{tt}^{do, shed} \leq E_{tt}^{do} \cdot (invest + E_{exist}) \cdot s_{flex, do} \quad \forall tt \in \mathbb{T} \\ & (8) \quad DSM_{tt}^{up} + \sum_{t=tt-L}^{tt+L} DSM_{t,tt}^{do, shift} + DSM_{tt}^{do, shed} \leq max \{ E_{tt}^{up} \cdot s_{flex, up}, E_{tt}^{do} \cdot s_{flex, do} \} \cdot (invest + E_{exist}) \quad \forall tt \in \mathbb{T} \\ & (9) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{up} \leq E_{t}^{up} \cdot (invest + E_{exist}) \cdot s_{flex, up} \cdot L \cdot \Delta t \quad \forall t \in \mathbb{T} \\ & (10) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{do, shed} \leq E_{t}^{do} \cdot (invest + E_{exist}) \cdot s_{flex, do} \cdot t_{shed} \cdot \Delta t \quad \forall t \in \mathbb{T} \\ *Note*: For the sake of readability, the handling of indices is not displayed here. E.g. evaluating a variable for t-L may lead to a negative and therefore infeasible index. This is addressed by limiting the sums to non-negative indices within the model index bounds. Please refer to the constraints implementation themselves. **The following parts of the objective function are created:** * Investment annuity: .. math:: invest \cdot costs_{invest} \\ * Variable costs: .. math:: DSM_{t}^{up} \cdot cost_{t}^{dsm, up} + \sum_{tt=0}^{T} DSM_{t, tt}^{do, shift} \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} **Table: Symbols and attribute names of variables and parameters** Please refer to :class:`oemof.solph.custom.SinkDSMDIWBlock`. The following variables and parameters are exclusively used for investment modeling: .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`invest` ",":attr:`~SinkDSM.invest` ","V", "DSM capacity invested in. Equals to the additionally installed capacity. The capacity share eligible for a shift is determined by flex share(s)." ":math:`invest_{min}` ", ":attr:`~SinkDSM.investment.minimum` ", "P", "minimum investment" ":math:`invest_{max}` ", ":attr:`~SinkDSM.investment.maximum` ", "P", "maximum investment" ":math:`E_{exist}` ",":attr:`~SinkDSM.investment.existing` ", "P", "existing DSM capacity" ":math:`s_{flex, up}` ",":attr:`~SinkDSM.flex_share_up` ", "P","Share of invested capacity that may be shift upwards at maximum" ":math:`s_{flex, do}` ",":attr:`~SinkDSM.flex_share_do` ", "P", "Share of invested capacity that may be shift downwards at maximum" ":math:`costs_{invest}` ",":attr:`~SinkDSM.investment.ep_costs` ", "P", "specific investment annuity" ":math:`T` "," ","P", "Overall amount of time steps (cardinality)" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* # Set of DSM Components self.investdsm = Set(initialize=[g for g in group]) # ************* VARIABLES ***************************** # Define bounds for investments in demand response def _dsm_investvar_bound_rule(block, g): """Rule definition to bound the demand response capacity invested in (`invest`). """ return g.investment.minimum, g.investment.maximum # Investment in DR capacity self.invest = Var( self.investdsm, within=NonNegativeReals, bounds=_dsm_investvar_bound_rule, ) # Variable load shift down self.dsm_do_shift = Var( self.investdsm, m.TIMESTEPS, m.TIMESTEPS, initialize=0, within=NonNegativeReals, ) # Variable load shedding self.dsm_do_shed = Var( self.investdsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up self.dsm_up = Var( self.investdsm, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: if not g.shift_eligibility: lhs = self.dsm_up[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, t), (lhs == rhs)) self.shift_shed_vars = Constraint(group, m.TIMESTEPS, noruleinit=True) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Demand Production Relation def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DSM. Sink Inflow == Demand +- DSM """ for t in m.TIMESTEPS: for g in group: # first time steps: 0 + delay time if t <= g.delay_time: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * (self.invest[g] + g.investment.existing) + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range(t + g.delay_time + 1) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) # main use case elif g.delay_time < t <= m.TIMESTEPS[-1] - g.delay_time: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * (self.invest[g] + g.investment.existing) + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range( t - g.delay_time, t + g.delay_time + 1 ) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) # last time steps: end - delay time else: # Inflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DSM rhs = ( g.demand[t] * (self.invest[g] + g.investment.existing) + self.dsm_up[g, t] - sum( self.dsm_do_shift[g, tt, t] for tt in range( t - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Equation 7 (resp. 7') def dsm_up_down_constraint_rule(block): """Equation 7 (resp. 7') by Zerrahn & Schill: Every upward load shift has to be compensated by downward load shifts in a defined time frame. Slightly modified equations for the first and last time steps due to variable initialization. Efficiency value depicts possible energy losses through load shifting (Equation 7'). """ for t in m.TIMESTEPS: for g in group: # first time steps: 0 + delay time if t <= g.delay_time: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range(t + g.delay_time + 1) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) # main use case elif g.delay_time < t <= m.TIMESTEPS[-1] - g.delay_time: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range( t - g.delay_time, t + g.delay_time + 1 ) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) # last time steps: end - delay time else: # DSM up lhs = self.dsm_up[g, t] * g.efficiency # DSM down rhs = sum( self.dsm_do_shift[g, t, tt] for tt in range( t - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) # add constraint block.dsm_updo_constraint.add((g, t), (lhs == rhs)) self.dsm_updo_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_updo_constraint_build = BuildAction( rule=dsm_up_down_constraint_rule ) # Equation 8 def dsm_up_constraint_rule(block): """Equation 8 by Zerrahn & Schill: Realised upward load shift at time t has to be smaller than upward DSM capacity at time t. """ for t in m.TIMESTEPS: for g in group: # DSM up lhs = self.dsm_up[g, t] # Capacity dsm_up rhs = ( g.capacity_up[t] * (self.invest[g] + g.investment.existing) * g.flex_share_up ) # add constraint block.dsm_up_constraint.add((g, t), (lhs <= rhs)) self.dsm_up_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_up_constraint_build = BuildAction(rule=dsm_up_constraint_rule) # Equation 9 (modified) def dsm_do_constraint_rule(block): """Equation 9 by Zerrahn & Schill: Realised downward load shift at time t has to be smaller than downward DSM capacity at time t. """ for tt in m.TIMESTEPS: for g in group: # first times steps: 0 + delay if tt <= g.delay_time: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range(tt + g.delay_time + 1) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = ( g.capacity_down[tt] * (self.invest[g] + g.investment.existing) * g.flex_share_down ) # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) # main use case elif g.delay_time < tt <= m.TIMESTEPS[-1] - g.delay_time: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, tt + g.delay_time + 1 ) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = ( g.capacity_down[tt] * (self.invest[g] + g.investment.existing) * g.flex_share_down ) # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) # last time steps: end - delay time else: # DSM down lhs = ( sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) + self.dsm_do_shed[g, tt] ) # Capacity DSM down rhs = ( g.capacity_down[tt] * (self.invest[g] + g.investment.existing) * g.flex_share_down ) # add constraint block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) self.dsm_do_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dsm_do_constraint_build = BuildAction(rule=dsm_do_constraint_rule) # Equation 10 def c2_constraint_rule(block): """Equation 10 by Zerrahn & Schill: The realised DSM up or down at time T has to be smaller than the maximum downward or upward capacity at time T. Therefore, in total each individual DSM unit within the modeled portfolio can only be shifted up OR down at a given time. """ for tt in m.TIMESTEPS: for g in group: # first times steps: 0 + delay time if tt <= g.delay_time: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range(tt + g.delay_time + 1) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.flex_share_up, g.capacity_down[tt] * g.flex_share_down, ) * (self.invest[g] + g.investment.existing) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) elif g.delay_time < tt <= m.TIMESTEPS[-1] - g.delay_time: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, tt + g.delay_time + 1 ) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.flex_share_up, g.capacity_down[tt] * g.flex_share_down, ) * (self.invest[g] + g.investment.existing) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) else: # DSM up/down lhs = ( self.dsm_up[g, tt] + sum( self.dsm_do_shift[g, t, tt] for t in range( tt - g.delay_time, m.TIMESTEPS[-1] + 1 ) ) + self.dsm_do_shed[g, tt] ) # max capacity at tt rhs = max( g.capacity_up[tt] * g.flex_share_up, g.capacity_down[tt] * g.flex_share_down, ) * (self.invest[g] + g.investment.existing) # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) self.C2_constraint = Constraint(group, m.TIMESTEPS, noruleinit=True) self.C2_constraint_build = BuildAction(rule=c2_constraint_rule) def recovery_constraint_rule(block): """Equation 11 by Zerrahn & Schill: A recovery time is introduced to account for the fact that there may be some restrictions before the next load shift may take place. Rule is only applicable if a recovery time is defined. """ for t in m.TIMESTEPS: for g in group: # No need to build constraint if no recovery # time is defined. if g.recovery_time_shift not in [None, 0]: # main use case if t <= m.TIMESTEPS[-1] - g.recovery_time_shift: # DSM up lhs = sum( self.dsm_up[g, tt] for tt in range(t, t + g.recovery_time_shift) ) # max energy shift for shifting process rhs = ( g.capacity_up[t] * (self.invest[g] + g.investment.existing) * g.flex_share_up * g.delay_time * m.timeincrement[t] ) # add constraint block.recovery_constraint.add((g, t), (lhs <= rhs)) # last time steps: end - recovery time else: # DSM up lhs = sum( self.dsm_up[g, tt] for tt in range(t, m.TIMESTEPS[-1] + 1) ) # max energy shift for shifting process rhs = ( g.capacity_up[t] * (self.invest[g] + g.investment.existing) * g.flex_share_up * g.delay_time * m.timeincrement[t] ) # add constraint block.recovery_constraint.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) self.recovery_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.recovery_constraint_build = BuildAction( rule=recovery_constraint_rule ) # Equation 9a from Zerrahn and Schill (2015b) def shed_limit_constraint_rule(block): """The following constraint is highly similar to equation 9a from Zerrahn and Schill (2015b): A recovery time for load shedding is introduced in order to limit the overall amount of shedded energy. """ for t in m.TIMESTEPS: for g in group: # Only applicable for load shedding if g.shed_eligibility: # main use case if t <= m.TIMESTEPS[-1] - g.recovery_time_shed: # DSM up lhs = sum( self.dsm_do_shed[g, tt] for tt in range(t, t + g.recovery_time_shed) ) # max energy shift for shifting process rhs = ( g.capacity_down[t] * (self.invest[g] + g.investment.existing) * g.flex_share_down * g.shed_time * m.timeincrement[t] ) # add constraint block.shed_limit_constraint.add( (g, t), (lhs <= rhs) ) # last time steps: end - recovery time else: # DSM up lhs = sum( self.dsm_do_shed[g, tt] for tt in range(t, m.TIMESTEPS[-1] + 1) ) # max energy shift for shifting process rhs = ( g.capacity_down[t] * (self.invest[g] + g.investment.existing) * g.flex_share_down * g.shed_time * m.timeincrement[t] ) # add constraint block.shed_limit_constraint.add( (g, t), (lhs <= rhs) ) else: pass # return(Constraint.Skip) self.shed_limit_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.shed_limit_constraint_build = BuildAction( rule=shed_limit_constraint_rule ) def _objective_expression(self): r"""Objective expression with variable and investment costs for DSM""" m = self.parent_block() investment_costs = 0 variable_costs = 0 for g in self.investdsm: if g.investment.ep_costs is not None: investment_costs += self.invest[g] * g.investment.ep_costs else: raise ValueError("Missing value for investment costs!") for t in m.TIMESTEPS: variable_costs += ( self.dsm_up[g, t] * g.cost_dsm_up[t] * m.objective_weighting[t] ) variable_costs += ( sum(self.dsm_do_shift[g, tt, t] for tt in m.TIMESTEPS) * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=investment_costs + variable_costs) return self.cost
[docs]class SinkDSMDLRBlock(SimpleBlock): r"""Constraints for SinkDSM with "DLR" approach **The following constraints are created for approach = 'DLR':** .. _SinkDSMDLR equations: .. math:: & (1) \quad DSM_{h, t}^{up} = 0 \quad \forall h \in H_{DR} \forall t \in \mathbb{T} \quad if \space eligibility_{shift} = False \\ & (2) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \in \mathbb{T} \quad if \space eligibility_{shed} = False \\ & (3) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max} + \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo} - DSM_{h, t}^{do, shift} - DSM_{h, t}^{balanceUp}) - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (4) \quad DSM_{h, t}^{balanceDo} = \frac{DSM_{h, t - h}^{do, shift}}{\eta} \quad \forall h \in H_{DR} \forall t \in [h..T] \\ & (5) \quad DSM_{h, t}^{balanceUp} = DSM_{h, t-h}^{up} \cdot \eta \quad \forall h \in H_{DR} \forall t \in [h..T] \\ & (6) \quad DSM_{h, t}^{do, shift} = 0 \quad \forall h \in H_{DR} \forall t \in [T - h..T] \\ & (7) \quad DSM_{h, t}^{up} = 0 \quad \forall h \in H_{DR} \forall t \in [T - h..T] \\ & (8) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) + DSM_{t}^{do, shed} \leq E_{t}^{do} \cdot E_{max, do} \quad \forall t \in \mathbb{T} \\ & (9) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo}) \leq E_{t}^{up} \cdot E_{max, up} \quad \forall t \in \mathbb{T} \\ & (10) \quad \Delta t \cdot \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} - DSM_{h, t}^{balanceDo} \cdot \eta) = W_{t}^{levelDo} - W_{t-1}^{levelDo} \quad \forall t \in [1..T] \\ & (11) \quad \Delta t \cdot \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} \cdot \eta - DSM_{h, t}^{balanceUp}) = W_{t}^{levelUp} - W_{t-1}^{levelUp} \quad \forall t \in [1..T] \\ & (12) \quad W_{t}^{levelDo} \leq \overline{E}_{t}^{do} \cdot E_{max, do} \cdot t_{shift} \quad \forall t \in \mathbb{T} \\ & (13) \quad W_{t}^{levelUp} \leq \overline{E}_{t}^{up} \cdot E_{max, up} \cdot t_{shift} \quad \forall t \in \mathbb{T} \\ & (14) \quad \displaystyle\sum_{t=0}^{T} DSM_{t}^{do, shed} \leq E_{max, do} \cdot \overline{E}_{t}^{do} \cdot t_{shed} \cdot n^{yearLimitShed} \\ & (15) \quad \displaystyle\sum_{t=0}^{T} \sum_{h=1}^{H_{DR}} DSM_{h, t}^{do, shift} \leq E_{max, do} \cdot \overline{E}_{t}^{do} \cdot t_{shift} \cdot n^{yearLimitShift} \\ (optional \space constraint) \\ & (16) \quad \displaystyle\sum_{t=0}^{T} \sum_{h=1}^{H_{DR}} DSM_{h, t}^{up} \leq E_{max, up} \cdot \overline{E}_{t}^{up} \cdot t_{shift} \cdot n^{yearLimitShift} \\ (optional \space constraint) \\ & (17) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{do, shift} \leq E_{max, do} \cdot \overline{E}_{t}^{do} \cdot t_{shift} - \displaystyle\sum_{t'=1}^{t_{dayLimit}} \sum_{h=1}^{H_{DR}} DSM_{h, t - t'}^{do, shift} \quad \forall t \in [t-t_{dayLimit}..T] \\ (optional \space constraint) \\ & (18) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{up} \leq E_{max, up} \cdot \overline{E}_{t}^{up} \cdot t_{shift} - \displaystyle\sum_{t'=1}^{t_{dayLimit}} \sum_{h=1}^{H_{DR}} DSM_{h, t - t'}^{up} \quad \forall t \in [t-t_{dayLimit}..T] \\ (optional \space constraint) \\ & (19) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo} + DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) + DSM_{t}^{do, shed} \leq \max \{E_{t}^{up} \cdot E_{max, up}, E_{t}^{do} \cdot E_{max, do} \} \quad \forall t \in \mathbb{T} \\ (optional \space constraint) \\ & *Note*: For the sake of readability, the handling of indices is not displayed here. E.g. evaluating a variable for t-L may lead to a negative and therefore infeasible index. This is addressed by limiting the sums to non-negative indices within the model index bounds. Please refer to the constraints implementation themselves. **The following parts of the objective function are created:** .. math:: \sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo}) \cdot cost_{t}^{dsm, up} + \sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} \\ **Table: Symbols and attribute names of variables and parameters** .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`DSM_{h, t}^{up}` ",":attr:`~SinkDSM.dsm_up[g,h,t]`", "V", "DSM up shift (additional load) in hour t with delay time h" ":math:`DSM_{h, t}^{do, shift}` ", ":attr:`~SinkDSM.dsm_do_shift[g,h, t]`", "V", "DSM down shift (less load) in hour t with delay time h" ":math:`DSM_{h, t}^{balanceUp}` ", ":attr:`~SinkDSM.balance_dsm_up[g,h,t]`", "V", "DSM down shift (less load) in hour t with delay time h to balance previous upshift" ":math:`DSM_{h, t}^{balanceDo}` ", ":attr:`~SinkDSM.balance_dsm_do[g,h,t]`", "V", "DSM up shift (additional load) in hour t with delay time h to balance previous downshift" ":math:`DSM_{t}^{do, shed}` ", ":attr:`~SinkDSM.dsm_do_shed[g, t]` ", "V","DSM shedded (capacity shedded, i.e. not compensated for)" ":math:`\dot{E}_{t}` ",":attr:`flow[g,t]`","V","Energy flowing in from (electrical) inflow bus" ":math:`h`","element of :attr:`~SinkDSM.delay_time`","P", "delay time for load shift (integer value from set of feasible delay times per DSM portfolio) (time until the energy balance has to be levelled out again; roundtrip time of one load shifting cycle, i.e. time window for upshift and compensating downshift)" ":math:`H_{DR}`", "`range(length(:attr:`~SinkDSM.delay_time`) + 1)`", "P", "Set of feasible delay times for load shift of a certain DSM portfolio (time until the energy balance has to be levelled out again; roundtrip time of one load shifting cycle, i.e. time window for upshift and compensating downshift)" ":math:`t_{shift}`",":attr:`~SinkDSM.shift_time`","P", "Maximum time for a shift in one direction, i. e. maximum time for an upshift or a downshift in a load shifting cycle" ":math:`t_{she}`",":attr:`~SinkDSM.shed_time`","P", "Maximum time for one load shedding process" ":math:`demand_{t}`",":attr:`~SinkDSM.demand[t]`","P", "(Electrical) demand series (normalized)" ":math:`demand_{max}`",":attr:`~SinkDSM.max_demand`","P", "Maximum demand value" ":math:`E_{t}^{do}`",":attr:`~SinkDSM.capacity_down[t]`","P", "Capacity allowed for a load adjustment downwards (normalized) (DSM down shift + DSM shedded)" ":math:`E_{t}^{up}`",":attr:`~SinkDSM.capacity_up[t]`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`E_{do, max}`",":attr:`~SinkDSM.max_capacity_down`","P", "Maximum capacity allowed for a load adjustment downwards (DSM down shift + DSM shedded)" ":math:`E_{up, max}`",":attr:`~SinkDSM.max_capacity_up`","P", "Capacity allowed for a shift upwards (normalized) (DSM up shift)" ":math:`\eta`",":attr:`~SinkDSM.efficiency`","P", "Efficiency loss for load shifting processes" ":math:`\mathbb{T}` "," ","P", "Set of time steps" ":math:`T` "," ","P", "Overall amount of time steps (cardinality)" ":math:`eligibility_{shift}` ", ":attr:`~SinkDSM.shift_eligibility`","P", "Boolean parameter indicating if unit can be used for load shifting" ":math:`eligibility_{shed}` ", ":attr:`~SinkDSM.shed_eligibility`","P", "Boolean parameter indicating if unit can be used for load shedding" ":math:`cost_{t}^{dsm, up}` ", ":attr:`~SinkDSM.cost_dsm_up[t]`", "P", "Variable costs for an upwards shift" ":math:`cost_{t}^{dsm, do, shift}` ", ":attr:`~SinkDSM.cost_dsm_down_shift[t]`","P", "Variable costs for a downwards shift (load shifting)" ":math:`cost_{t}^{dsm, do, shed}` ", ":attr:`~SinkDSM.cost_dsm_down_shed[t]`","P", "Variable costs for shedding load" ":math:`\Delta t`",":attr:`~models.Model.timeincrement`","P", "The time increment of the model" ":math:`n_{yearLimitshift}`",":attr:`~SinkDSM.n_yearLimitShift`", "P", "Maximum allowed number of load shifts (at full capacity) in the optimization timeframe" ":math:`n_{yearLimitshed}`",":attr:`~SinkDSM.n_yearLimitShed`", "P", "Maximum allowed number of load sheds (at full capacity) in the optimization timeframe" ":math:`t_{dayLimit}`",":attr:`~SinkDSM.t_dayLimit`", "P", "Maximum duration of load shifts at full capacity per day resp. in the last hours before the current" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* # Set of DR Components self.DR = Set(initialize=[n for n in group]) # Depict different delay_times per unit via a mapping map_DR_H = { k: v for k, v in zip([n for n in group], [n.delay_time for n in group]) } unique_H = list(set(itertools.chain.from_iterable(map_DR_H.values()))) self.H = Set(initialize=unique_H) self.DR_H = Set( within=self.DR * self.H, initialize=[(dr, h) for dr in map_DR_H for h in map_DR_H[dr]], ) # ************* VARIABLES ***************************** # Variable load shift down (capacity) self.dsm_do_shift = Var( self.DR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable for load shedding (capacity) self.dsm_do_shed = Var( self.DR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up (capacity) self.dsm_up = Var( self.DR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable balance load shift down through upwards shift (capacity) self.balance_dsm_do = Var( self.DR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable balance load shift up through downwards shift (capacity) self.balance_dsm_up = Var( self.DR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable fictious DR storage level for downwards load shifts (energy) self.dsm_do_level = Var( self.DR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable fictious DR storage level for upwards load shifts (energy) self.dsm_up_level = Var( self.DR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if not g.shift_eligibility: lhs = self.dsm_up[g, h, t] rhs = 0 block.shift_shed_vars.add((g, h, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, h, t), (lhs == rhs)) self.shift_shed_vars = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Relation between inflow and effective Sink consumption def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DR. Bus outflow == Demand +- DR (i.e. effective Sink consumption) """ for t in m.TIMESTEPS: for g in group: # outflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DR rhs = ( g.demand[t] * g.max_demand + sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] - self.dsm_do_shift[g, h, t] - self.balance_dsm_up[g, h, t] for h in g.delay_time ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Equation 4.8 def capacity_balance_red_rule(block): """Load reduction must be balanced by load increase within delay_time """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if g.shift_eligibility: # main use case if t >= h: # balance load reduction lhs = self.balance_dsm_do[g, h, t] # load reduction (efficiency considered) rhs = ( self.dsm_do_shift[g, h, t - h] / g.efficiency ) # add constraint block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) # no balancing for the first timestep elif t == m.TIMESTEPS[1]: lhs = self.balance_dsm_do[g, h, t] rhs = 0 block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) else: pass # return(Constraint.Skip) # if only shedding is possible, balancing variable is 0 else: lhs = self.balance_dsm_do[g, h, t] rhs = 0 block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) self.capacity_balance_red = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.capacity_balance_red_build = BuildAction( rule=capacity_balance_red_rule ) # Equation 4.9 def capacity_balance_inc_rule(block): """Load increased must be balanced by load reduction within delay_time """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if g.shift_eligibility: # main use case if t >= h: # balance load increase lhs = self.balance_dsm_up[g, h, t] # load increase (efficiency considered) rhs = self.dsm_up[g, h, t - h] * g.efficiency # add constraint block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) # no balancing for the first timestep elif t == m.TIMESTEPS[1]: lhs = self.balance_dsm_up[g, h, t] rhs = 0 block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) else: pass # return(Constraint.Skip) # if only shedding is possible, balancing variable is 0 else: lhs = self.balance_dsm_up[g, h, t] rhs = 0 block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) self.capacity_balance_inc = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.capacity_balance_inc_build = BuildAction( rule=capacity_balance_inc_rule ) # Fix: prevent shifts which cannot be compensated def no_comp_red_rule(block): """Prevent downwards shifts that cannot be balanced anymore within the optimization timeframe """ for t in m.TIMESTEPS: for g in group: if g.fixes: for h in g.delay_time: if t > m.TIMESTEPS[-1] - h: # no load reduction anymore (dsm_do_shift = 0) lhs = self.dsm_do_shift[g, h, t] rhs = 0 block.no_comp_red.add((g, h, t), (lhs == rhs)) else: pass # return(Constraint.Skip) self.no_comp_red = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.no_comp_red_build = BuildAction(rule=no_comp_red_rule) # Fix: prevent shifts which cannot be compensated def no_comp_inc_rule(block): """Prevent upwards shifts that cannot be balanced anymore within the optimization timeframe """ for t in m.TIMESTEPS: for g in group: if g.fixes: for h in g.delay_time: if t > m.TIMESTEPS[-1] - h: # no load increase anymore (dsm_up = 0) lhs = self.dsm_up[g, h, t] rhs = 0 block.no_comp_inc.add((g, h, t), (lhs == rhs)) else: pass # return(Constraint.Skip) self.no_comp_inc = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.no_comp_inc_build = BuildAction(rule=no_comp_inc_rule) # Equation 4.11 def availability_red_rule(block): """Load reduction must be smaller than or equal to the (time-dependent) capacity limit """ for t in m.TIMESTEPS: for g in group: # load reduction lhs = ( sum( self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) + self.dsm_do_shed[g, t] ) # upper bound rhs = g.capacity_down[t] * g.max_capacity_down # add constraint block.availability_red.add((g, t), (lhs <= rhs)) self.availability_red = Constraint(group, m.TIMESTEPS, noruleinit=True) self.availability_red_build = BuildAction(rule=availability_red_rule) # Equation 4.12 def availability_inc_rule(block): """Load increase must be smaller than or equal to the (time-dependent) capacity limit """ for t in m.TIMESTEPS: for g in group: # load increase lhs = sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] for h in g.delay_time ) # upper bound rhs = g.capacity_up[t] * g.max_capacity_up # add constraint block.availability_inc.add((g, t), (lhs <= rhs)) self.availability_inc = Constraint(group, m.TIMESTEPS, noruleinit=True) self.availability_inc_build = BuildAction(rule=availability_inc_rule) # Equation 4.13 def dr_storage_red_rule(block): """Fictious demand response storage level for load reductions transition equation """ for t in m.TIMESTEPS: for g in group: # avoid timesteps prior to t = 0 if t > 0: # reduction minus balancing of reductions lhs = m.timeincrement[t] * sum( ( self.dsm_do_shift[g, h, t] - self.balance_dsm_do[g, h, t] * g.efficiency ) for h in g.delay_time ) # load reduction storage level transition rhs = ( self.dsm_do_level[g, t] - self.dsm_do_level[g, t - 1] ) # add constraint block.dr_storage_red.add((g, t), (lhs == rhs)) else: lhs = self.dsm_do_level[g, t] rhs = m.timeincrement[t] * sum( self.dsm_do_shift[g, h, t] for h in g.delay_time ) block.dr_storage_red.add((g, t), (lhs == rhs)) self.dr_storage_red = Constraint(group, m.TIMESTEPS, noruleinit=True) self.dr_storage_red_build = BuildAction(rule=dr_storage_red_rule) # Equation 4.14 def dr_storage_inc_rule(block): """Fictious demand response storage level for load increase transition equation """ for t in m.TIMESTEPS: for g in group: # avoid timesteps prior to t = 0 if t > 0: # increases minus balancing of reductions lhs = m.timeincrement[t] * sum( ( self.dsm_up[g, h, t] * g.efficiency - self.balance_dsm_up[g, h, t] ) for h in g.delay_time ) # load increase storage level transition rhs = ( self.dsm_up_level[g, t] - self.dsm_up_level[g, t - 1] ) # add constraint block.dr_storage_inc.add((g, t), (lhs == rhs)) else: # pass # return(Constraint.Skip) lhs = self.dsm_up_level[g, t] rhs = m.timeincrement[t] * sum( self.dsm_up[g, h, t] for h in g.delay_time ) block.dr_storage_inc.add((g, t), (lhs == rhs)) self.dr_storage_inc = Constraint(group, m.TIMESTEPS, noruleinit=True) self.dr_storage_inc_build = BuildAction(rule=dr_storage_inc_rule) # Equation 4.15 def dr_storage_limit_red_rule(block): """ Fictious demand response storage level for load reduction limit """ for t in m.TIMESTEPS: for g in group: if g.shift_eligibility: # fictious demand response load reduction storage level lhs = self.dsm_do_level[g, t] # maximum (time-dependent) available shifting capacity rhs = ( g.capacity_down_mean * g.max_capacity_down * g.shift_time ) # add constraint block.dr_storage_limit_red.add((g, t), (lhs <= rhs)) else: lhs = self.dsm_do_level[g, t] # Force storage level and thus dsm_do_shift to 0 rhs = 0 # add constraint block.dr_storage_limit_red.add((g, t), (lhs <= rhs)) self.dr_storage_limit_red = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_storage_level_red_build = BuildAction( rule=dr_storage_limit_red_rule ) # Equation 4.16 def dr_storage_limit_inc_rule(block): """ Fictious demand response storage level for load increase limit """ for t in m.TIMESTEPS: for g in group: # fictious demand response load reduction storage level lhs = self.dsm_up_level[g, t] # maximum (time-dependent) available shifting capacity rhs = g.capacity_up_mean * g.max_capacity_up * g.shift_time # add constraint block.dr_storage_limit_inc.add((g, t), (lhs <= rhs)) self.dr_storage_limit_inc = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_storage_level_inc_build = BuildAction( rule=dr_storage_limit_inc_rule ) # Equation 4.17' -> load shedding def dr_yearly_limit_shed_rule(block): """Introduce overall annual (energy) limit for load shedding resp. overall limit for optimization timeframe considered A year limit in contrast to Gils (2015) is defined a mandatory parameter here in order to achieve an approach comparable to the others. """ for g in group: if g.shed_eligibility: # sum of all load reductions lhs = sum(self.dsm_do_shed[g, t] for t in m.TIMESTEPS) # year limit rhs = ( g.capacity_down_mean * g.max_capacity_down * g.shed_time * g.n_yearLimit_shed ) # add constraint block.dr_yearly_limit_shed.add(g, (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_yearly_limit_shed = Constraint(group, noruleinit=True) self.dr_yearly_limit_shed_build = BuildAction( rule=dr_yearly_limit_shed_rule ) # ************* Optional Constraints ***************************** # Equation 4.17 def dr_yearly_limit_red_rule(block): """Introduce overall annual (energy) limit for load reductions resp. overall limit for optimization timeframe considered """ for g in group: if g.ActivateYearLimit: # sum of all load reductions lhs = sum( sum(self.dsm_do_shift[g, h, t] for h in g.delay_time) for t in m.TIMESTEPS ) # year limit rhs = ( g.capacity_down_mean * g.max_capacity_down * g.shift_time * g.n_yearLimit_shift ) # add constraint block.dr_yearly_limit_red.add(g, (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_yearly_limit_red = Constraint(group, noruleinit=True) self.dr_yearly_limit_red_build = BuildAction( rule=dr_yearly_limit_red_rule ) # Equation 4.18 def dr_yearly_limit_inc_rule(block): """Introduce overall annual (energy) limit for load increases resp. overall limit for optimization timeframe considered """ for g in group: if g.ActivateYearLimit: # sum of all load increases lhs = sum( sum(self.dsm_up[g, h, t] for h in g.delay_time) for t in m.TIMESTEPS ) # year limit rhs = ( g.capacity_up_mean * g.max_capacity_up * g.shift_time * g.n_yearLimit_shift ) # add constraint block.dr_yearly_limit_inc.add(g, (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_yearly_limit_inc = Constraint(group, noruleinit=True) self.dr_yearly_limit_inc_build = BuildAction( rule=dr_yearly_limit_inc_rule ) # Equation 4.19 def dr_daily_limit_red_rule(block): """ "Introduce rolling (energy) limit for load reductions This effectively limits DR utilization dependent on activations within previous hours. """ for t in m.TIMESTEPS: for g in group: if g.ActivateDayLimit: # main use case if t >= g.t_dayLimit: # load reduction lhs = sum( self.dsm_do_shift[g, h, t] for h in g.delay_time ) # daily limit rhs = ( g.capacity_down_mean * g.max_capacity_down * g.shift_time - sum( sum( self.dsm_do_shift[g, h, t - t_dash] for h in g.delay_time ) for t_dash in range( 1, int(g.t_dayLimit) + 1 ) ) ) # add constraint block.dr_daily_limit_red.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) else: pass # return(Constraint.Skip) self.dr_daily_limit_red = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_daily_limit_red_build = BuildAction( rule=dr_daily_limit_red_rule ) # Equation 4.20 def dr_daily_limit_inc_rule(block): """Introduce rolling (energy) limit for load increases This effectively limits DR utilization dependent on activations within previous hours. """ for t in m.TIMESTEPS: for g in group: if g.ActivateDayLimit: # main use case if t >= g.t_dayLimit: # load increase lhs = sum( self.dsm_up[g, h, t] for h in g.delay_time ) # daily limit rhs = ( g.capacity_up_mean * g.max_capacity_up * g.shift_time - sum( sum( self.dsm_up[g, h, t - t_dash] for h in g.delay_time ) for t_dash in range( 1, int(g.t_dayLimit) + 1 ) ) ) # add constraint block.dr_daily_limit_inc.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) else: pass # return(Constraint.Skip) self.dr_daily_limit_inc = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_daily_limit_inc_build = BuildAction( rule=dr_daily_limit_inc_rule ) # Addition: avoid simultaneous activations def dr_logical_constraint_rule(block): """Similar to equation 10 from Zerrahn and Schill (2015): The sum of upwards and downwards shifts may not be greater than the (bigger) capacity limit. """ for t in m.TIMESTEPS: for g in group: if g.addition: # sum of load increases and reductions lhs = ( sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] + self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) + self.dsm_do_shed[g, t] ) # maximum capacity eligibly for load shifting rhs = max( g.capacity_down[t] * g.max_capacity_down, g.capacity_up[t] * g.max_capacity_up, ) # add constraint block.dr_logical_constraint.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_logical_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_logical_constraint_build = BuildAction( rule=dr_logical_constraint_rule ) # Equation 4.23 def _objective_expression(self): r"""Objective expression with variable costs for DSM activity; Equation 4.23 from Gils (2015) """ m = self.parent_block() dr_cost = 0 for t in m.TIMESTEPS: for g in self.DR: dr_cost += ( sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] for h in g.delay_time ) * g.cost_dsm_up[t] * m.objective_weighting[t] ) dr_cost += ( sum( self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=dr_cost) return self.cost
[docs]class SinkDSMDLRInvestmentBlock(SinkDSMDLRBlock): r"""Constraints for SinkDSM with "DLR" approach and :attr:`investment` **The following constraints are created for approach = 'DLR' with an investment object defined:** .. _SinkDSMDLR equations: .. math:: & (1) \quad invest_{min} \leq invest \leq invest_{max} \\ & (2) \quad DSM_{h, t}^{up} = 0 \quad \forall h \in H_{DR} \forall t \in \mathbb{T} \quad if \space eligibility_{shift} = False \\ & (3) \quad DSM_{t}^{do, shed} = 0 \quad \forall t \in \mathbb{T} \quad if \space eligibility_{shed} = False \\ & (4) \quad \dot{E}_{t} = demand_{t} \cdot (invest + E_{exist}) + \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo} - DSM_{h, t}^{do, shift} - DSM_{h, t}^{balanceUp}) - DSM_{t}^{do, shed} \quad \forall t \in \mathbb{T} \\ & (5) \quad DSM_{h, t}^{balanceDo} = \frac{DSM_{h, t - h}^{do, shift}}{\eta} \quad \forall h \in H_{DR} \forall t \in [h..T] \\ & (6) \quad DSM_{h, t}^{balanceUp} = DSM_{h, t-h}^{up} \cdot \eta \quad \forall h \in H_{DR} \forall t \in [h..T] \\ & (7) \quad DSM_{h, t}^{do, shift} = 0 \quad \forall h \in H_{DR} \forall t \in [T - h..T] \\ & (8) \quad DSM_{h, t}^{up} = 0 \quad \forall h \in H_{DR} \forall t \in [T - h..T] \\ & (9) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) + DSM_{t}^{do, shed} \leq E_{t}^{do} \cdot (invest + E_{exist}) \cdot s_{flex, do} \quad \forall t \in \mathbb{T} \\ & (10) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo}) \leq E_{t}^{up} \cdot (invest + E_{exist}) \cdot s_{flex, up} \quad \forall t \in \mathbb{T} \\ & (11) \quad \Delta t \cdot \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} - DSM_{h, t}^{balanceDo} \cdot \eta) = W_{t}^{levelDo} - W_{t-1}^{levelDo} \quad \forall t \in [1..T] \\ & (12) \quad \Delta t \cdot \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} \cdot \eta - DSM_{h, t}^{balanceUp}) = W_{t}^{levelUp} - W_{t-1}^{levelUp} \quad \forall t \in [1..T] \\ & (13) \quad W_{t}^{levelDo} \leq \overline{E}_{t}^{do} \cdot (invest + E_{exist}) \cdot s_{flex, do} \cdot t_{shift} \quad \forall t \in \mathbb{T} \\ & (14) \quad W_{t}^{levelUp} \leq \overline{E}_{t}^{up} \cdot (invest + E_{exist}) \cdot s_{flex, up} \cdot t_{shift} \quad \forall t \in \mathbb{T} \\ & (15) \quad \displaystyle\sum_{t=0}^{T} DSM_{t}^{do, shed} \leq (invest + E_{exist}) \cdot s_{flex, do} \cdot \overline{E}_{t}^{do} \cdot t_{shed} \cdot n^{yearLimitShed} \\ & (16) \quad \displaystyle\sum_{t=0}^{T} \sum_{h=1}^{H_{DR}} DSM_{h, t}^{do, shift} \leq (invest + E_{exist}) \cdot s_{flex, do} \cdot \overline{E}_{t}^{do} \cdot t_{shift} \cdot n^{yearLimitShift} \\ (optional \space constraint) \\ & (17) \quad \displaystyle\sum_{t=0}^{T} \sum_{h=1}^{H_{DR}} DSM_{h, t}^{up} \leq (invest + E_{exist}) \cdot s_{flex, up} \cdot \overline{E}_{t}^{up} \cdot t_{shift} \cdot n^{yearLimitShift} \\ (optional \space constraint) \\ & (18) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{do, shift} \leq (invest + E_{exist}) \cdot s_{flex, do} \cdot \overline{E}_{t}^{do} \cdot t_{shift} - \displaystyle\sum_{t'=1}^{t_{dayLimit}} \sum_{h=1}^{H_{DR}} DSM_{h, t - t'}^{do, shift} \quad \forall t \in [t-t_{dayLimit}..T] \\ (optional \space constraint) \\ & (19) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{up} \leq (invest + E_{exist}) \cdot s_{flex, up} \cdot \overline{E}_{t}^{up} \cdot t_{shift} - \displaystyle\sum_{t'=1}^{t_{dayLimit}} \sum_{h=1}^{H_{DR}} DSM_{h, t - t'}^{up} \quad \forall t \in [t-t_{dayLimit}..T] \\ (optional \space constraint) \\ & (20) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo} + DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) + DSM_{t}^{shed} \leq \max \{E_{t}^{up} \cdot s_{flex, up}, E_{t}^{do} \cdot s_{flex, do} \} \cdot (invest + E_{exist}) \quad \forall t \in \mathbb{T} \\ (optional \space constraint) \\ & *Note*: For the sake of readability, the handling of indices is not displayed here. E.g. evaluating a variable for t-L may lead to a negative and therefore infeasible index. This is addressed by limiting the sums to non-negative indices within the model index bounds. Please refer to the constraints implementation themselves. **The following parts of the objective function are created:** * Investment annuity: .. math:: invest \cdot costs_{invest} \\ * Variable costs: .. math:: \sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up} + DSM_{h, t}^{balanceDo}) \cdot cost_{t}^{dsm, up} + \sum_{h=1}^{H_{DR}} (DSM_{h, t}^{do, shift} + DSM_{h, t}^{balanceUp}) \cdot cost_{t}^{dsm, do, shift} + DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed} \quad \forall t \in \mathbb{T} \\ **Table: Symbols and attribute names of variables and parameters** Please refer to :class:`oemof.solph.custom.SinkDSMDLRBlock`. The following variables and parameters are exclusively used for investment modeling: .. csv-table:: Variables (V) and Parameters (P) :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 ":math:`invest` ",":attr:`~SinkDSM.invest` ","V", "DSM capacity invested in. Equals to the additionally installed capacity. The capacity share eligible for a shift is determined by flex share(s)." ":math:`invest_{min}` ", ":attr:`~SinkDSM.investment.minimum` ", "P", "minimum investment" ":math:`invest_{max}` ", ":attr:`~SinkDSM.investment.maximum` ", "P", "maximum investment" ":math:`E_{exist}` ",":attr:`~SinkDSM.investment.existing` ", "P", "existing DSM capacity" ":math:`s_{flex, up}` ",":attr:`~SinkDSM.flex_share_up` ", "P","Share of invested capacity that may be shift upwards at maximum" ":math:`s_{flex, do}` ",":attr:`~SinkDSM.flex_share_do` ", "P", "Share of invested capacity that may be shift downwards at maximum" ":math:`costs_{invest}` ",":attr:`~SinkDSM.investment.ep_costs` ", "P", "specific investment annuity" """ CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _create(self, group=None): if group is None: return None m = self.parent_block() # for all DSM components get inflow from a bus for n in group: n.inflow = list(n.inputs)[0] # ************* SETS ********************************* self.INVESTDR = Set(initialize=[n for n in group]) # Depict different delay_times per unit via a mapping map_INVESTDR_H = { k: v for k, v in zip([n for n in group], [n.delay_time for n in group]) } unique_H = list( set(itertools.chain.from_iterable(map_INVESTDR_H.values())) ) self.H = Set(initialize=unique_H) self.INVESTDR_H = Set( within=self.INVESTDR * self.H, initialize=[ (dr, h) for dr in map_INVESTDR_H for h in map_INVESTDR_H[dr] ], ) # ************* VARIABLES ***************************** # Define bounds for investments in demand response def _dr_investvar_bound_rule(block, g): """Rule definition to bound the invested demand response capacity `invest`. """ return g.investment.minimum, g.investment.maximum # Investment in DR capacity self.invest = Var( self.INVESTDR, within=NonNegativeReals, bounds=_dr_investvar_bound_rule, ) # Variable load shift down (capacity) self.dsm_do_shift = Var( self.INVESTDR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable for load shedding (capacity) self.dsm_do_shed = Var( self.INVESTDR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable load shift up (capacity) self.dsm_up = Var( self.INVESTDR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable balance load shift down through upwards shift (capacity) self.balance_dsm_do = Var( self.INVESTDR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable balance load shift up through downwards shift (capacity) self.balance_dsm_up = Var( self.INVESTDR_H, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable fictious DR storage level for downwards load shifts (energy) self.dsm_do_level = Var( self.INVESTDR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # Variable fictious DR storage level for upwards load shifts (energy) self.dsm_up_level = Var( self.INVESTDR, m.TIMESTEPS, initialize=0, within=NonNegativeReals ) # ************* CONSTRAINTS ***************************** def _shift_shed_vars_rule(block): """Force shifting resp. shedding variables to zero dependent on how boolean parameters for shift resp. shed eligibility are set. """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if not g.shift_eligibility: lhs = self.dsm_up[g, h, t] rhs = 0 block.shift_shed_vars.add((g, h, t), (lhs == rhs)) if not g.shed_eligibility: lhs = self.dsm_do_shed[g, t] rhs = 0 block.shift_shed_vars.add((g, h, t), (lhs == rhs)) self.shift_shed_vars = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.shift_shed_vars_build = BuildAction(rule=_shift_shed_vars_rule) # Relation between inflow and effective Sink consumption def _input_output_relation_rule(block): """Relation between input data and pyomo variables. The actual demand after DR. Bus outflow == Demand +- DR (i.e. effective Sink consumption) """ for t in m.TIMESTEPS: for g in group: # outflow from bus lhs = m.flow[g.inflow, g, t] # Demand +- DR rhs = ( g.demand[t] * (self.invest[g] + g.investment.existing) + sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] - self.dsm_do_shift[g, h, t] - self.balance_dsm_up[g, h, t] for h in g.delay_time ) - self.dsm_do_shed[g, t] ) # add constraint block.input_output_relation.add((g, t), (lhs == rhs)) self.input_output_relation = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.input_output_relation_build = BuildAction( rule=_input_output_relation_rule ) # Equation 4.8 def capacity_balance_red_rule(block): """Load reduction must be balanced by load increase within delay_time """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if g.shift_eligibility: # main use case if t >= h: # balance load reduction lhs = self.balance_dsm_do[g, h, t] # load reduction (efficiency considered) rhs = ( self.dsm_do_shift[g, h, t - h] / g.efficiency ) # add constraint block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) # no balancing for the first timestep elif t == m.TIMESTEPS[1]: lhs = self.balance_dsm_do[g, h, t] rhs = 0 block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) else: pass # return(Constraint.Skip) # if only shedding is possible, balancing variable is 0 else: lhs = self.balance_dsm_do[g, h, t] rhs = 0 block.capacity_balance_red.add( (g, h, t), (lhs == rhs) ) self.capacity_balance_red = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.capacity_balance_red_build = BuildAction( rule=capacity_balance_red_rule ) # Equation 4.9 def capacity_balance_inc_rule(block): """Load increased must be balanced by load reduction within delay_time """ for t in m.TIMESTEPS: for g in group: for h in g.delay_time: if g.shift_eligibility: # main use case if t >= h: # balance load increase lhs = self.balance_dsm_up[g, h, t] # load increase (efficiency considered) rhs = self.dsm_up[g, h, t - h] * g.efficiency # add constraint block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) # no balancing for the first timestep elif t == m.TIMESTEPS[1]: lhs = self.balance_dsm_up[g, h, t] rhs = 0 block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) else: pass # return(Constraint.Skip) # if only shedding is possible, balancing variable is 0 else: lhs = self.balance_dsm_up[g, h, t] rhs = 0 block.capacity_balance_inc.add( (g, h, t), (lhs == rhs) ) self.capacity_balance_inc = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.capacity_balance_inc_build = BuildAction( rule=capacity_balance_inc_rule ) # Own addition: prevent shifts which cannot be compensated def no_comp_red_rule(block): """Prevent downwards shifts that cannot be balanced anymore within the optimization timeframe """ for t in m.TIMESTEPS: for g in group: if g.fixes: for h in g.delay_time: if t > m.TIMESTEPS[-1] - h: # no load reduction anymore (dsm_do_shift = 0) lhs = self.dsm_do_shift[g, h, t] rhs = 0 block.no_comp_red.add((g, h, t), (lhs == rhs)) else: pass # return(Constraint.Skip) self.no_comp_red = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.no_comp_red_build = BuildAction(rule=no_comp_red_rule) # Own addition: prevent shifts which cannot be compensated def no_comp_inc_rule(block): """Prevent upwards shifts that cannot be balanced anymore within the optimization timeframe """ for t in m.TIMESTEPS: for g in group: if g.fixes: for h in g.delay_time: if t > m.TIMESTEPS[-1] - h: # no load increase anymore (dsm_up = 0) lhs = self.dsm_up[g, h, t] rhs = 0 block.no_comp_inc.add((g, h, t), (lhs == rhs)) else: pass # return(Constraint.Skip) self.no_comp_inc = Constraint( group, self.H, m.TIMESTEPS, noruleinit=True ) self.no_comp_inc_build = BuildAction(rule=no_comp_inc_rule) # Equation 4.11 def availability_red_rule(block): """Load reduction must be smaller than or equal to the (time-dependent) capacity limit """ for t in m.TIMESTEPS: for g in group: # load reduction lhs = ( sum( self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) + self.dsm_do_shed[g, t] ) # upper bound rhs = ( g.capacity_down[t] * (self.invest[g] + g.investment.existing) * g.flex_share_down ) # add constraint block.availability_red.add((g, t), (lhs <= rhs)) self.availability_red = Constraint(group, m.TIMESTEPS, noruleinit=True) self.availability_red_build = BuildAction(rule=availability_red_rule) # Equation 4.12 def availability_inc_rule(block): """Load increase must be smaller than or equal to the (time-dependent) capacity limit """ for t in m.TIMESTEPS: for g in group: # load increase lhs = sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] for h in g.delay_time ) # upper bound rhs = ( g.capacity_up[t] * (self.invest[g] + g.investment.existing) * g.flex_share_up ) # add constraint block.availability_inc.add((g, t), (lhs <= rhs)) self.availability_inc = Constraint(group, m.TIMESTEPS, noruleinit=True) self.availability_inc_build = BuildAction(rule=availability_inc_rule) # Equation 4.13 def dr_storage_red_rule(block): """Fictious demand response storage level for load reductions transition equation """ for t in m.TIMESTEPS: for g in group: # avoid timesteps prior to t = 0 if t > 0: # reduction minus balancing of reductions lhs = m.timeincrement[t] * sum( ( self.dsm_do_shift[g, h, t] - self.balance_dsm_do[g, h, t] * g.efficiency ) for h in g.delay_time ) # load reduction storage level transition rhs = ( self.dsm_do_level[g, t] - self.dsm_do_level[g, t - 1] ) # add constraint block.dr_storage_red.add((g, t), (lhs == rhs)) else: # pass # return(Constraint.Skip) lhs = self.dsm_do_level[g, t] rhs = m.timeincrement[t] * sum( self.dsm_do_shift[g, h, t] for h in g.delay_time ) block.dr_storage_red.add((g, t), (lhs == rhs)) self.dr_storage_red = Constraint(group, m.TIMESTEPS, noruleinit=True) self.dr_storage_red_build = BuildAction(rule=dr_storage_red_rule) # Equation 4.14 def dr_storage_inc_rule(block): """Fictious demand response storage level for load increase transition equation """ for t in m.TIMESTEPS: for g in group: # avoid timesteps prior to t = 0 if t > 0: # increases minus balancing of reductions lhs = m.timeincrement[t] * sum( ( self.dsm_up[g, h, t] * g.efficiency - self.balance_dsm_up[g, h, t] ) for h in g.delay_time ) # load increase storage level transition rhs = ( self.dsm_up_level[g, t] - self.dsm_up_level[g, t - 1] ) # add constraint block.dr_storage_inc.add((g, t), (lhs == rhs)) else: # pass # return(Constraint.Skip) lhs = self.dsm_up_level[g, t] rhs = m.timeincrement[t] * sum( self.dsm_up[g, h, t] for h in g.delay_time ) block.dr_storage_inc.add((g, t), (lhs == rhs)) self.dr_storage_inc = Constraint(group, m.TIMESTEPS, noruleinit=True) self.dr_storage_inc_build = BuildAction(rule=dr_storage_inc_rule) # Equation 4.15 def dr_storage_limit_red_rule(block): """ Fictious demand response storage level for load reduction limit """ for t in m.TIMESTEPS: for g in group: if g.shift_eligibility: # fictious demand response load reduction storage level lhs = self.dsm_do_level[g, t] # maximum (time-dependent) available shifting capacity rhs = ( g.capacity_down_mean * (self.invest[g] + g.investment.existing) * g.flex_share_down * g.shift_time ) # add constraint block.dr_storage_limit_red.add((g, t), (lhs <= rhs)) else: lhs = self.dsm_do_level[g, t] # Force storage level and thus dsm_do_shift to 0 rhs = 0 # add constraint block.dr_storage_limit_red.add((g, t), (lhs <= rhs)) self.dr_storage_limit_red = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_storage_level_red_build = BuildAction( rule=dr_storage_limit_red_rule ) # Equation 4.16 def dr_storage_limit_inc_rule(block): """ Fictious demand response storage level for load increase limit """ for t in m.TIMESTEPS: for g in group: # fictious demand response load reduction storage level lhs = self.dsm_up_level[g, t] # maximum (time-dependent) available shifting capacity rhs = ( g.capacity_up_mean * (self.invest[g] + g.investment.existing) * g.flex_share_up * g.shift_time ) # add constraint block.dr_storage_limit_inc.add((g, t), (lhs <= rhs)) self.dr_storage_limit_inc = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_storage_level_inc_build = BuildAction( rule=dr_storage_limit_inc_rule ) # Equation 4.17' -> load shedding def dr_yearly_limit_shed_rule(block): """Introduce overall annual (energy) limit for load shedding resp. overall limit for optimization timeframe considered A year limit in contrast to Gils (2015) is defined a mandatory parameter here in order to achieve an approach comparable to the others. """ for g in group: if g.shed_eligibility: # sum of all load reductions lhs = sum(self.dsm_do_shed[g, t] for t in m.TIMESTEPS) # year limit rhs = ( g.capacity_down_mean * (self.invest[g] + g.investment.existing) * g.flex_share_down * g.shed_time * g.n_yearLimit_shed ) # add constraint block.dr_yearly_limit_shed.add(g, (lhs <= rhs)) self.dr_yearly_limit_shed = Constraint(group, noruleinit=True) self.dr_yearly_limit_shed_build = BuildAction( rule=dr_yearly_limit_shed_rule ) # ************* Optional Constraints ***************************** # Equation 4.17 def dr_yearly_limit_red_rule(block): """Introduce overall annual (energy) limit for load reductions resp. overall limit for optimization timeframe considered """ for g in group: if g.ActivateYearLimit: # sum of all load reductions lhs = sum( sum(self.dsm_do_shift[g, h, t] for h in g.delay_time) for t in m.TIMESTEPS ) # year limit rhs = ( g.capacity_down_mean * (self.invest[g] + g.investment.existing) * g.flex_share_down * g.shift_time * g.n_yearLimit_shift ) # add constraint block.dr_yearly_limit_red.add(g, (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_yearly_limit_red = Constraint(group, noruleinit=True) self.dr_yearly_limit_red_build = BuildAction( rule=dr_yearly_limit_red_rule ) # Equation 4.18 def dr_yearly_limit_inc_rule(block): """Introduce overall annual (energy) limit for load increases resp. overall limit for optimization timeframe considered """ for g in group: if g.ActivateYearLimit: # sum of all load increases lhs = sum( sum(self.dsm_up[g, h, t] for h in g.delay_time) for t in m.TIMESTEPS ) # year limit rhs = ( g.capacity_up_mean * (self.invest[g] + g.investment.existing) * g.flex_share_up * g.shift_time * g.n_yearLimit_shift ) # add constraint block.dr_yearly_limit_inc.add(g, (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_yearly_limit_inc = Constraint(group, noruleinit=True) self.dr_yearly_limit_inc_build = BuildAction( rule=dr_yearly_limit_inc_rule ) # Equation 4.19 def dr_daily_limit_red_rule(block): """Introduce rolling (energy) limit for load reductions This effectively limits DR utilization dependent on activations within previous hours. """ for t in m.TIMESTEPS: for g in group: if g.ActivateDayLimit: # main use case if t >= g.t_dayLimit: # load reduction lhs = sum( self.dsm_do_shift[g, h, t] for h in g.delay_time ) # daily limit rhs = g.capacity_down_mean * ( self.invest[g] + g.investment.existing ) * g.flex_share_down * g.shift_time - sum( sum( self.dsm_do_shift[g, h, t - t_dash] for h in g.delay_time ) for t_dash in range(1, int(g.t_dayLimit) + 1) ) # add constraint block.dr_daily_limit_red.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) else: pass # return(Constraint.Skip) self.dr_daily_limit_red = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_daily_limit_red_build = BuildAction( rule=dr_daily_limit_red_rule ) # Equation 4.20 def dr_daily_limit_inc_rule(block): """Introduce rolling (energy) limit for load increases This effectively limits DR utilization dependent on activations within previous hours. """ for t in m.TIMESTEPS: for g in group: if g.ActivateDayLimit: # main use case if t >= g.t_dayLimit: # load increase lhs = sum( self.dsm_up[g, h, t] for h in g.delay_time ) # daily limit rhs = g.capacity_up_mean * ( self.invest[g] + g.investment.existing ) * g.flex_share_up * g.shift_time - sum( sum( self.dsm_up[g, h, t - t_dash] for h in g.delay_time ) for t_dash in range(1, int(g.t_dayLimit) + 1) ) # add constraint block.dr_daily_limit_inc.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) else: pass # return(Constraint.Skip) self.dr_daily_limit_inc = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_daily_limit_inc_build = BuildAction( rule=dr_daily_limit_inc_rule ) # Addition: avoid simultaneous activations def dr_logical_constraint_rule(block): """Similar to equation 10 from Zerrahn and Schill (2015): The sum of upwards and downwards shifts may not be greater than the (bigger) capacity limit. """ for t in m.TIMESTEPS: for g in group: if g.addition: # sum of load increases and reductions lhs = ( sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] + self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) + self.dsm_do_shed[g, t] ) # maximum capacity eligibly for load shifting rhs = max( g.capacity_down[t] * g.flex_share_down, g.capacity_up[t] * g.flex_share_up, ) * (self.invest[g] + g.investment.existing) # add constraint block.dr_logical_constraint.add((g, t), (lhs <= rhs)) else: pass # return(Constraint.Skip) self.dr_logical_constraint = Constraint( group, m.TIMESTEPS, noruleinit=True ) self.dr_logical_constraint_build = BuildAction( rule=dr_logical_constraint_rule ) def _objective_expression(self): r"""Objective expression with variable and investment costs for DSM; Equation 4.23 from Gils (2015) """ m = self.parent_block() investment_costs = 0 variable_costs = 0 for g in self.INVESTDR: if g.investment.ep_costs is not None: investment_costs += self.invest[g] * g.investment.ep_costs else: raise ValueError("Missing value for investment costs!") for t in m.TIMESTEPS: variable_costs += ( sum( self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] for h in g.delay_time ) * g.cost_dsm_up[t] * m.objective_weighting[t] ) variable_costs += ( sum( self.dsm_do_shift[g, h, t] + self.balance_dsm_up[g, h, t] for h in g.delay_time ) * g.cost_dsm_down_shift[t] + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] ) * m.objective_weighting[t] self.cost = Expression(expr=investment_costs + variable_costs) return self.cost