# -*- coding: utf-8 -*-
"""
Implementation of demand-side management (demand response) which allows for
* modeling load shifting and/or shedding of a given baseline demand
for a demand response portfolio,
* assessing both, a pure dispatch and an investment model and
* choosing among different (storage-alike) implementations.
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: Julian Endres
SPDX-FileCopyrightText: Johannes Kochems
SPDX-License-Identifier: MIT
"""
import itertools
from warnings import warn
import numpy as np
from oemof.tools import debugging
from oemof.tools import economics
from pyomo.core.base.block import ScalarBlock
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._options import Investment
from oemof.solph._plumbing import sequence
from oemof.solph.components._sink import Sink
[docs]class SinkDSM(Sink):
r"""
Demand Side Management implemented as a Sink with flexibility potential
to deviate from the baseline demand in upwards or downwards direction.
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, in: Energy (84), pp. 840-845,
`10.1016/j.energy.2015.03.037
<https://doi.org/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 included. 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. `fixes` and
`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 `capacity_up` and `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. Else, underinvestments may occur.
capacity_up: int or iterable
maximum DSM capacity that may be increased (normalized)
capacity_down: int or iterable
maximum DSM capacity that may be reduced (normalized)
approach: str, one of '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 (`shift_interval` is mandatory).
Within time windows of the length `shift_interval` DSM
up and down shifts are balanced. For details see
:class:`~SinkDSMOemofBlock` resp.
:class:`~SinkDSMOemofInvestmentBlock`.
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 (`delay_time` is
mandatory).
For details see
:class:`~SinkDSMDIWBlock` resp.
:class:`~SinkDSMDIWInvestmentBlock`.
DLR :
Sophisticated model based on the formulation by
Gils (2015). The load shift of the component must be
compensated in a predefined delay time (`delay_time` is
mandatory).
For details see
:class:`~SinkDSMDLRBlock` resp.
:class:`~SinkDSMDLRInvestmentBlock`.
shift_interval: int
Only used when `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 or iterable
Only used when :attr:`~approach` is set to "DIW" or "DLR". Otherwise,
can be None. Iterable only allowed in case approach "DLR" is used.
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', if an integer is passed,
an iterable is constructed in order to model flexible delay times.
In case an iterable is passed, this will be used directly.
shift_time: int
Only used when `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 `shed_eligibility` is set to True.
Maximum length of a load shedding process at full capacity
(used within energy limit constraint)
max_demand: numeric or iterable
Maximum demand prior to demand response (per period)
max_capacity_down: numeric
Maximum capacity eligible for downshifts
prior to demand response (used only for dispatch mode)
max_capacity_up: numeric
Maximum capacity eligible for upshifts
prior to demand response (used only for dispatch mode)
cost_dsm_up : float
Cost per unit of DSM activity that increases the demand
cost_dsm_down_shift : float
Cost per unit of DSM activity that decreases the demand
for load shifting
cost_dsm_down_shed : float
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 `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
Minimum time between the end of one load shifting process
and the start of another for load shedding processes
ActivateYearLimit : boolean
Only used when `approach` is set to "DLR".
Control parameter; activates constraints for year limit if set to True
ActivateDayLimit : boolean
Only used when `approach` is set to "DLR".
Control parameter; activates constraints for day limit if set to True
n_yearLimit_shift : int
Only used when `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 `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 `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 `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 `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
fixed_costs : numeric
Nominal value of fixed costs (per year)
Note
----
* `method` has been renamed to `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 `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 `cost_dsm_up`
or `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,
label=None,
inputs=None,
shift_interval=None,
delay_time=None,
shift_time=None,
shed_time=None,
max_demand=None,
max_capacity_down=None,
max_capacity_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,
fixed_costs=0,
investment=None,
custom_attributes=None,
):
if custom_attributes is None:
custom_attributes = {}
super().__init__(
label=label, inputs=inputs, custom_attributes=custom_attributes
)
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":
if approach == "DIW":
if not isinstance(delay_time, int):
raise ValueError(
"If approach 'DIW' is used, "
"delay time has to be of type int."
)
self.delay_time = delay_time
else:
if isinstance(delay_time, int):
self.delay_time = [el for el in range(1, delay_time + 1)]
else:
self.delay_time = delay_time
self.shift_time = shift_time
self.shed_time = shed_time
self.max_capacity_down = max_capacity_down
self.max_capacity_up = max_capacity_up
self.max_demand = sequence(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 = np.mean(capacity_down)
self.capacity_up_mean = np.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
self.fixed_costs = sequence(fixed_costs)
# Check whether investment mode is active or not
self.investment = investment
self._invest_group = isinstance(self.investment, Investment)
if (
self.max_capacity_up is None or self.max_capacity_down is None
) and not self._invest_group:
e1 = (
"If you are using the dispatch mode, "
"you have to specify `max_capacity_up` "
"and `max_capacity_down`."
)
raise AttributeError(e1)
if self._invest_group:
self._check_invest_attributes()
def _check_invest_attributes(self):
if (
self.max_capacity_down or self.max_capacity_up
) and self.investment is not None:
e2 = (
"If you are using the investment mode, the invest variable "
"replaces the `max_capacity_down` "
"as well as the `max_capacity_up` values.\n"
"Therefore, `max_capacity_up` and `max_capacity_down` "
"values should be None (which is their default value)."
)
raise AttributeError(e2)
[docs] def constraint_group(self):
possible_approaches = ["DIW", "DLR", "oemof"]
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 in [possible_approaches[0], possible_approaches[1]]:
if self.delay_time is None:
raise ValueError(
f"Please define: delay_time.\n"
f"It is a mandatory parameter when using"
f" approach {self.approach}."
)
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(
f"Please define: shift_interval.\n"
f"It is a mandatory parameter when using"
f" approach {self.approach}."
)
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(ScalarBlock):
r"""Constraints for SinkDSM with "oemof" approach
**The following constraints are created for approach = "oemof":**
.. _SinkDSMOemofBlock equations:
.. math::
&
(1) \quad DSM_{t}^{up} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
& \\
&
(2) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{False} \\
& \\
&
(3) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max} + DSM_{t}^{up}
- DSM_{t}^{do, shift} - DSM_{t}^{do, shed} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(4) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot E_{up, max} \\
& \quad \quad \quad \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 \quad \quad \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 \quad \quad \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})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
* :attr:`fixed_costs` not None
.. math::
\displaystyle \sum_{pp=0}^{year_{max}} max\{E_{up, max}, E_{do, max}\}
\cdot c_{fixed}(pp) \cdot DF^{-pp}
**Table: Symbols and attribute names of variables and parameters**
.. table:: Variables (V), Parameters (P) and Sets (S)
:widths: 25, 25, 10, 40
================================= ======================== ==== =======================================
symbol attribute type explanation
================================= ======================== ==== =======================================
:math:`DSM_{t}^{up}` `dsm_up[g, t]` V DSM up shift (capacity shifted upwards)
:math:`DSM_{t}^{do, shift}` `dsm_do_shift[g, t]` V DSM down shift (capacity shifted downwards)
:math:`DSM_{t}^{do, shed}` `dsm_do_shed[g, t]` V DSM shedded (capacity shedded, i.e. not compensated for)
:math:`\dot{E}_{t}` `SinkDSM.inputs` V Energy flowing in from (electrical) inflow bus
:math:`demand_{t}` `demand[t]` P (Electrical) demand series before shifting (normalized)
:math:`demand_{max}(p)` `max_demand` P Maximum demand value in period p
:math:`E_{t}^{do}` `capacity_down[t]` P | Capacity allowed for a load adjustment downwards
| (normalized; shifting + shedding)
:math:`E_{t}^{up}` `capacity_up[t]` P Capacity allowed for a shift upwards (normalized)
:math:`E_{do, max}` `max_capacity_down` P | Maximum capacity allowed for a load adjustment downwards
| (shifting + shedding)
:math:`E_{up, max}` `max_capacity_up` P Maximum capacity allowed for a shift upwards
:math:`\tau` `shift_interval` P | interval (time within which the
| energy balance must be levelled out)
:math:`\eta` `efficiency` P Efficiency for load shifting processes
:math:`\mathbb{T}` S Time steps of the model
:math:`e_{shift}` `shift_eligibility` P | Boolean parameter indicating if unit can be used
| for load shifting
:math:`e_{shed}` `shed_eligibility` P | Boolean parameter indicating if unit can be used
| for load shedding
:math:`cost_{t}^{dsm, up}` `cost_dsm_up[t]` P Variable costs for an upwards shift
:math:`cost_{t}^{dsm, do, shift}` `cost_dsm_down_shift[t]` P Variable costs for a downwards shift (load shifting)
:math:`cost_{t}^{dsm, do, shed}` `cost_dsm_down_shift[t]` P Variable costs for shedding load
:math:`\omega_{t}` P Objective weighting of the model for timestep t
:math:`year_{max}` P Last year of the optimization horizon
================================= ======================== ==== =======================================
""" # noqa: E501
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 p, t in m.TIMEINDEX:
for g in group:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand + DSM_up - DSM_down
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs))
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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.at(1), m.TIMESTEPS.at(-1), g.shift_interval
)
for interval in intervals:
if (interval + g.shift_interval - 1) > m.TIMESTEPS.at(-1):
timesteps = range(interval, m.TIMESTEPS.at(-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()
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for t in m.TIMESTEPS:
for g in self.dsm:
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]
else:
for g in self.dsm:
for p, t in m.TIMEINDEX:
variable_costs += (
self.dsm_up[g, t]
* m.objective_weighting[t]
* g.cost_dsm_up[t]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.fixed_costs[0] is not None:
fixed_costs += sum(
max(g.max_capacity_up, g.max_capacity_down)
* g.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(m.es.end_year_of_optimization)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.costs = Expression(expr=variable_costs + fixed_costs)
return self.costs
[docs]class SinkDSMOemofInvestmentBlock(ScalarBlock):
r"""Constraints for SinkDSM with "oemof" approach and `investment`
defined
**The following constraints are created for approach = "oemof"
with an investment object defined:**
.. _SinkDSMOemofInvestmentBlock equations:
.. math::
&
(1) \quad invest_{min}(p) \leq invest(p) \leq invest_{max}(p) \\
& \quad \quad \quad \quad \forall p \in \mathbb{P}
& \\
&
(2) \quad DSM_{t}^{up} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
& \\
&
(3) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{False} \\
& \\
&
(4) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max}(p)
+ DSM_{t}^{up}
- DSM_{t}^{do, shift} - DSM_{t}^{do, shed} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(5) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(6) \quad DSM_{t}^{do, shift} + DSM_{t}^{do, shed} \leq
E_{t}^{do} \cdot P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(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 \quad \quad \quad \forall t_s \in
\{k \in \mathbb{T} \mid k \mod \tau = 0\} \\
**The following parts of the objective function are created:**
*Standard model*
* Investment annuity:
.. math::
P_{invest}(0) \cdot c_{invest}(0)
* 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})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
*Multi-period model*
* Investment annuity:
.. math::
&
P_{invest}(p) \cdot A(c_{invest}(p), l, ir)
\cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p} \\
&\\
&
\forall p \in \mathbb{P}
In case, the remaining lifetime of a DSM unit is greater than 0 and
attribute `use_remaining_value` of the energy system is True,
the difference in value for the investment period compared to the
last period of the optimization horizon is accounted for
as an adder to the investment costs:
.. math::
&
P_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
A(c_{invest,var}(|P|), l_{r}, ir)\\
& \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
&\\
&
\forall p \in \textrm{PERIODS}
* :attr:`fixed_costs` not None for investments
.. math::
&
(\sum_{pp=year(p)}^{limit_{end}}
P_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
\cdot DF^{-p} \\
&\\
&
\forall p \in \mathbb{P}
* 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})
\cdot \omega_{t}
\cdot DF^{-p} \\
& \quad \quad \quad \quad
\forall p, t \in \textrm{TIMEINDEX} \\
where:
* :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
and interest rate :math:`ir`.
* :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
and interest rate :math:`ir`.
* :math:`d=min\{year_{max} - year(p), l\}` defines the
number of years within the optimization horizon that investment
annuities are accounted for.
* :math:`year(p)` denotes the start year of period :math:`p`.
* :math:`year_{max}` denotes the last year of the optimization
horizon, i.e. at the end of the last period.
* :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
upper bound to ensure fixed costs for endogenous investments
to occur within the optimization horizon.
* :math:`DF=(1+dr)` is the discount factor.
The annuity / annuity factor hereby is:
.. math::
&
A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
\frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
&\\
&
ANF(d, ir)=\frac {(1+dr)^d \cdot dr} {(1+dr)^d - 1}
They are retrieved, using oemof.tools.economics annuity function. The
interest rate :math:`ir` for the annuity is defined as weighted
average costs of capital (wacc) and assumed constant over time.
See remarks in
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMOemofBlock`.
**Symbols and attribute names of variables and parameters**
* Please refer to
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMOemofBlock`.
for a variables and parameter description.
* The following variables and parameters are exclusively used for
investment modeling:
.. table:: Variables (V), Parameters (P) and Sets (S)
:widths: 25, 25, 10, 40
================================= ======================== ==== =======================================
symbol attribute type explanation
================================= ======================== ==== =======================================
:math:`P_{invest}(p)` `invest[p]` V | DSM capacity invested into in period p.
| Equals to the additionally shiftable resp. sheddable capacity.
:math:`invest_{min}(p)` `investment.minimum[p]` P minimum investment in period p
:math:`invest_{max}(p)` `investment.maximum[p]` P maximum investment in period p
:math:`P_{total}` `investment.total[p]` P total DSM capacity
:math:`costs_{invest}(p)` `investment.ep_costs[p]` P | specific investment annuity (standard model) resp.
| specific investment expenses (multi-period model)
:math:`\mathbb{P}` S Periods of the model
:math:`\textrm{TIMEINDEX}` S Timeindex set of the model (periods, timesteps)
================================= ======================== ==== =======================================
""" # noqa: E501
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])
self.OVERALL_MAXIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_maximum is not None
]
)
self.OVERALL_MINIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_minimum is not None
]
)
self.EXISTING_INVESTDSM = Set(
initialize=[g for g in group if g.investment.existing is not None]
)
# ************* VARIABLES *****************************
# Define bounds for investments in demand response
def _dsm_investvar_bound_rule(block, g, p):
"""Rule definition to bound the
invested demand response capacity `invest`.
"""
return g.investment.minimum[p], g.investment.maximum[p]
# Investment in DR capacity
self.invest = Var(
self.investdsm,
m.PERIODS,
within=NonNegativeReals,
bounds=_dsm_investvar_bound_rule,
)
# Total capacity
self.total = Var(self.investdsm, m.PERIODS, within=NonNegativeReals)
if m.es.periods is not None:
# Old capacity to be decommissioned (due to lifetime)
self.old = Var(self.investdsm, m.PERIODS, within=NonNegativeReals)
# Old endogenous capacity to be decommissioned (due to lifetime)
self.old_end = Var(
self.investdsm, m.PERIODS, within=NonNegativeReals
)
# Old exogenous capacity to be decommissioned (due to lifetime)
self.old_exo = Var(
self.investdsm, m.PERIODS, within=NonNegativeReals
)
# 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 *****************************
# Handle unit lifetimes
def _total_dsm_capacity_rule(block):
"""Rule definition for determining total installed
capacity (taking decommissioning into account)
"""
for g in group:
for p in m.PERIODS:
if p == 0:
expr = (
self.total[g, p]
== self.invest[g, p] + g.investment.existing
)
self.total_dsm_rule.add((g, p), expr)
else:
expr = (
self.total[g, p]
== self.invest[g, p]
+ self.total[g, p - 1]
- self.old[g, p]
)
self.total_dsm_rule.add((g, p), expr)
self.total_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.total_dsm_rule_build = BuildAction(rule=_total_dsm_capacity_rule)
if m.es.periods is not None:
def _old_dsm_capacity_rule_end(block):
"""Rule definition for determining old endogenously installed
capacity to be decommissioned due to reaching its lifetime.
Investment and decommissioning periods are linked within
the constraint. The respective decommissioning period is
determined for every investment period based on the components
lifetime and a matrix describing its age of each endogenous
investment. Decommissioning can only occur at the beginning of
each period.
Note
----
For further information on the implementation check
PR#957 https://github.com/oemof/oemof-solph/pull/957
"""
for g in group:
lifetime = g.investment.lifetime
if lifetime is None:
msg = (
"You have to specify a lifetime "
"for a Flow with an associated "
"investment object in "
f"a multi-period model! Value for {(g)} "
"is missing."
)
raise ValueError(msg)
# get the period matrix describing the temporal distance
# between all period combinations.
periods_matrix = m.es.periods_matrix
# get the index of the minimum value in each row greater
# equal than the lifetime. This value equals the
# decommissioning period if not zero. The index of this
# value represents the investment period. If np.where
# condition is not met in any row, min value will be zero
decomm_periods = np.argmin(
np.where(
(periods_matrix >= lifetime),
periods_matrix,
np.inf,
),
axis=1,
)
# no decommissioning in first period
expr = self.old_end[g, 0] == 0
self.old_dsm_rule_end.add((g, 0), expr)
# all periods not in decomm_periods have no decommissioning
# zero is excluded
for p in m.PERIODS:
if p not in decomm_periods and p != 0:
expr = self.old_end[g, p] == 0
self.old_dsm_rule_end.add((g, p), expr)
# multiple invests can be decommissioned in the same period
# but only sequential ones, thus a bookkeeping is
# introduced andconstraints are added to equation one
# iteration later.
last_decomm_p = np.nan
# loop over invest periods (values are decomm_periods)
for invest_p, decomm_p in enumerate(decomm_periods):
# Add constraint of iteration before
# (skipped in first iteration by last_decomm_p = nan)
if (decomm_p != last_decomm_p) and (
last_decomm_p is not np.nan
):
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
# no decommissioning if decomm_p is zero
if decomm_p == 0:
# overwrite decomm_p with zero to avoid
# chaining invest periods in next iteration
last_decomm_p = 0
# if decomm_p is the same as the last one chain invest
# period
elif decomm_p == last_decomm_p:
expr += self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# if decomm_p is not zero, not the same as the last one
# and it's not the first period
else:
expr = self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# Add constraint of very last iteration
if last_decomm_p != 0:
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
self.old_dsm_rule_end = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_end_build = BuildAction(
rule=_old_dsm_capacity_rule_end
)
def _old_dsm_capacity_rule_exo(block):
"""Rule definition for determining old exogenously given
capacity to be decommissioned due to reaching its lifetime
"""
for g in group:
age = g.investment.age
lifetime = g.investment.lifetime
is_decommissioned = False
for p in m.PERIODS:
# No shutdown in first period
if p == 0:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
elif lifetime - age <= m.es.periods_years[p]:
# Track decommissioning status
if not is_decommissioned:
expr = (
self.old_exo[g, p] == g.investment.existing
)
is_decommissioned = True
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
self.old_dsm_rule_exo = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_exo_build = BuildAction(
rule=_old_dsm_capacity_rule_exo
)
def _old_dsm_capacity_rule(block):
"""Rule definition for determining (overall) old capacity
to be decommissioned due to reaching its lifetime
"""
for g in group:
for p in m.PERIODS:
expr = (
self.old[g, p]
== self.old_end[g, p] + self.old_exo[g, p]
)
self.old_dsm_rule.add((g, p), expr)
self.old_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.old_dsm_rule_build = BuildAction(rule=_old_dsm_capacity_rule)
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 p, t in m.TIMEINDEX:
for g in group:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand + DSM_up - DSM_down
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs))
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
for g in group:
# DSM up
lhs = self.dsm_up[g, t]
# Capacity dsm_up
rhs = g.capacity_up[t] * self.total[g, p]
# add constraint
block.dsm_up_constraint.add((g, p, t), (lhs <= rhs))
self.dsm_up_constraint = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.total[g, p]
# add constraint
block.dsm_down_constraint.add((g, p, t), (lhs <= rhs))
self.dsm_down_constraint = Constraint(
group, m.TIMEINDEX, 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.at(1), m.TIMESTEPS.at(-1), g.shift_interval
)
for interval in intervals:
if (interval + g.shift_interval - 1) > m.TIMESTEPS.at(-1):
timesteps = range(interval, m.TIMESTEPS.at(-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
)
if m.es.periods is not None:
def _overall_dsm_maximum_investflow_rule(block):
"""Rule definition for maximum overall investment
in investment case.
"""
for g in self.OVERALL_MAXIMUM_INVESTDSM:
for p in m.PERIODS:
expr = self.total[g, p] <= g.investment.overall_maximum
self.overall_dsm_maximum.add((g, p), expr)
self.overall_dsm_maximum = Constraint(
self.OVERALL_MAXIMUM_INVESTDSM, m.PERIODS, noruleinit=True
)
self.overall_maximum_build = BuildAction(
rule=_overall_dsm_maximum_investflow_rule
)
def _overall_minimum_dsm_investflow_rule(block):
"""Rule definition for minimum overall investment
in investment case.
Note: This is only applicable for the last period
"""
for g in self.OVERALL_MINIMUM_INVESTDSM:
expr = (
g.investment.overall_minimum
<= self.total[g, m.PERIODS.at(-1)]
)
self.overall_minimum.add(g, expr)
self.overall_minimum = Constraint(
self.OVERALL_MINIMUM_INVESTDSM, noruleinit=True
)
self.overall_minimum_build = BuildAction(
rule=_overall_minimum_dsm_investflow_rule
)
def _objective_expression(self):
r"""Objective expression with variable and investment costs for DSM"""
m = self.parent_block()
investment_costs = 0
period_investment_costs = {p: 0 for p in m.PERIODS}
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for g in self.investdsm:
for p in m.PERIODS:
if g.investment.ep_costs is not None:
investment_costs += (
self.invest[g, p] * g.investment.ep_costs[p]
)
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]
else:
msg = (
"You did not specify an interest rate.\n"
"It will be set equal to the discount_rate of {} "
"of the model as a default.\nThis corresponds to a "
"social planner point of view and does not reflect "
"microeconomic interest requirements."
)
for g in self.investdsm:
if g.investment.ep_costs is not None:
lifetime = g.investment.lifetime
interest = g.investment.interest_rate
if interest == 0:
warn(
msg.format(m.discount_rate),
debugging.SuspiciousUsageWarning,
)
interest = m.discount_rate
for p in m.PERIODS:
annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=lifetime,
wacc=interest,
)
duration = min(
m.es.end_year_of_optimization
- m.es.periods_years[p],
lifetime,
)
present_value_factor = 1 / economics.annuity(
capex=1, n=duration, wacc=interest
)
investment_costs_increment = (
self.invest[g, p] * annuity * present_value_factor
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
g,
m.es.end_year_of_optimization,
lifetime,
interest,
)
)
investment_costs += (
investment_costs_increment
+ remaining_value_difference
)
period_investment_costs[
p
] += investment_costs_increment
else:
raise ValueError("Missing value for investment costs!")
for p, t in m.TIMEINDEX:
variable_costs += (
self.dsm_up[g, t]
* m.objective_weighting[t]
* g.cost_dsm_up[t]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
for p in m.PERIODS:
range_limit = min(
m.es.end_year_of_optimization,
m.es.periods_years[p] + lifetime,
)
fixed_costs += sum(
self.invest[g, p]
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(
m.es.periods_years[p],
range_limit,
)
)
for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
age = g.investment.age
range_limit = min(
m.es.end_year_of_optimization, lifetime - age
)
fixed_costs += sum(
g.investment.existing
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(range_limit)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.investment_costs = Expression(expr=investment_costs)
self.period_investment_costs = period_investment_costs
self.costs = Expression(
expr=investment_costs + variable_costs + fixed_costs
)
return self.costs
def _evaluate_remaining_value_difference(
self,
m,
p,
g,
end_year_of_optimization,
lifetime,
interest,
):
"""Evaluate and return the remaining value difference of an investment
The remaining value difference in the net present values if the asset
was to be liquidated at the end of the optimization horizon and the
net present value using the original investment expenses.
Parameters
----------
m : oemof.solph.models.Model
Optimization model
p : int
Period in which investment occurs
g : oemof.solph.components.experimental.SinkDSM
storage unit
end_year_of_optimization : int
Last year of the optimization horizon
lifetime : int
lifetime of investment considered
interest : float
Demanded interest rate for investment
"""
if m.es.use_remaining_value:
if end_year_of_optimization - m.es.periods_years[p] < lifetime:
remaining_lifetime = lifetime - (
end_year_of_optimization - m.es.periods_years[p]
)
remaining_annuity = economics.annuity(
capex=g.investment.ep_costs[-1],
n=remaining_lifetime,
wacc=interest,
)
original_annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=remaining_lifetime,
wacc=interest,
)
present_value_factor_remaining = 1 / economics.annuity(
capex=1, n=remaining_lifetime, wacc=interest
)
return (
self.invest[g, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
else:
return 0
else:
return 0
[docs]class SinkDSMDIWBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DIW" approach
**The following constraints are created for approach = "DIW":**
.. _SinkDSMDIWBlock equations:
.. math::
&
(1) \quad DSM_{t}^{up} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
& \\
&
(2) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{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 \quad \quad \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 \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(5) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot E_{up, max} \\
& \quad \quad \quad \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 \quad \quad \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 \quad \quad \quad \forall tt \in \mathbb{T} \\
& \\
&
(8) \quad \sum_{tt=t}^{t+R_{shi}-1} DSM_{tt}^{up}
\leq E_{t}^{up} \cdot E_{up, max} \cdot L \cdot \Delta t \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(9) \quad \sum_{tt=t}^{t+R_{she}-1} DSM_{tt}^{do, shed}
\leq E_{t}^{do} \cdot E_{do, max} \cdot t_{shed} \cdot \Delta t \\
& \quad \quad \quad \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_{tt, t}^{do, shift} \cdot
cost_{t}^{dsm, do, shift}
+ DSM_{t}^{do, shed} \cdot cost_{t}^{dsm, do, shed})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
* :attr:`fixed_costs` not None
.. math::
\displaystyle \sum_{pp=0}^{year_{max}} max\{E_{up, max}, E_{do, max}\}
\cdot c_{fixed}(pp) \cdot DF^{-pp}
**Table: Symbols and attribute names of variables and parameters**
.. table:: Variables (V), Parameters (P) and Sets (S)
:widths: 25, 25, 10, 40
================================= ======================== ==== =======================================
symbol attribute type explanation
================================= ======================== ==== =======================================
:math:`DSM_{t}^{up}` `dsm_up[g, t]` V DSM up shift (additional load) in hour t
:math:`DSM_{t, tt}^{do, shift}` `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}` `dsm_do_shed[g, t]` V DSM shedded (capacity shedded, i.e. not compensated for)
:math:`\dot{E}_{t}` `SinkDSM.inputs` V Energy flowing in from (electrical) inflow bus
:math:`L` `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}` `shed_time` P Maximum time for one load shedding process
:math:`demand_{t}` `demand[t]` P (Electrical) demand series (normalized)
:math:`demand_{max}` `max_demand` P Maximum demand value
:math:`E_{t}^{do}` `capacity_down[t]` P | Capacity allowed for a load adjustment downwards
| (normalized; shifting + shedding)
:math:`E_{t}^{up}` `capacity_up[t]` P Capacity allowed for a shift upwards (normalized)
:math:`E_{do, max}` `max_capacity_down` P | Maximum capacity allowed for a load adjustment downwards
| (shifting + shedding)
:math:`E_{up, max}` `max_capacity_up` P Maximum capacity allowed for a shift upwards
:math:`\eta` `efficiency` P Efficiency for load shifting processes
:math:`\mathbb{T}` S Time steps of the model
:math:`e_{shift}` `shift_eligibility` P | Boolean parameter indicating if unit can be used
| for load shifting
:math:`e_{shed}` `shed_eligibility` P | Boolean parameter indicating if unit can be used
| for load shedding
:math:`cost_{t}^{dsm, up}` `cost_dsm_up[t]` P Variable costs for an upwards shift
:math:`cost_{t}^{dsm, do, shift}` `cost_dsm_down_shift[t]` P Variable costs for a downwards shift (load shifting)
:math:`cost_{t}^{dsm, do, shed}` `cost_dsm_down_shift[t]` P Variable costs for shedding load
:math:`\omega_{t}` P Objective weighting of the model for timestep t
:math:`R_{shi}` `recovery_time_shift` P | Minimum time between the end of one load shifting process
| and the start of another
:math:`R_{she}` `recovery_time_shed` P | Minimum time between the end of one load shedding process
| and the start of another
:math:`\Delta t` P The time increment of the model
:math:`\omega_{t}` P Objective weighting of the model for timestep t
:math:`year_{max}` P Last year of the optimization horizon
================================= ======================== ==== =======================================
""" # noqa E:501
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 p, t in m.TIMEINDEX:
for g in group:
# first time steps: 0 + delay time
if t <= g.delay_time:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs)
)
# main use case
elif g.delay_time < t <= m.TIMESTEPS.at(-1) - g.delay_time:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs)
)
# last time steps: end - delay time
else:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ self.dsm_up[g, t]
- sum(
self.dsm_do_shift[g, tt, t]
for tt in range(
t - g.delay_time, m.TIMESTEPS.at(-1) + 1
)
)
- self.dsm_do_shed[g, t]
)
# add constraint
block.input_output_relation.add(
(g, p, t), (lhs == rhs)
)
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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.at(-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.at(-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.at(-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.at(-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.at(-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.at(-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.at(-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.at(-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.at(-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.at(-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()
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for t in m.TIMESTEPS:
for g in self.dsm:
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]
else:
for g in self.dsm:
for p, t in m.TIMEINDEX:
variable_costs += (
self.dsm_up[g, t]
* m.objective_weighting[t]
* g.cost_dsm_up[t]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.fixed_costs[0] is not None:
fixed_costs += sum(
max(g.max_capacity_up, g.max_capacity_down)
* g.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(m.es.end_year_of_optimization)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.costs = Expression(expr=variable_costs + fixed_costs)
return self.costs
[docs]class SinkDSMDIWInvestmentBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DIW" approach and `investment` defined
**The following constraints are created for approach = "DIW" with an
investment object defined:**
.. _SinkDSMDIWInvestmentBlock equations:
.. math::
&
(1) \quad invest_{min}(p) \leq invest(p) \leq invest_{max}(p) \\
& \quad \quad \quad \quad \forall p \in \mathbb{P}
& \\
&
(2) \quad DSM_{t}^{up} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
& \\
&
(3) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{False} \\
& \\
&
(4) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max}(p)
+ DSM_{t}^{up} -
\sum_{tt=t-L}^{t+L} DSM_{tt,t}^{do, shift} - DSM_{t}^{do, shed} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(5) \quad DSM_{t}^{up} \cdot \eta =
\sum_{tt=t-L}^{t+L} DSM_{t,tt}^{do, shift} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(6) \quad DSM_{t}^{up} \leq E_{t}^{up} \cdot P_{total}(p) \\
& \quad \quad \quad \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 P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(8) \quad DSM_{tt}^{up} + \sum_{t=tt-L}^{tt+L} DSM_{t,tt}^{do, shift}
+ DSM_{tt}^{do, shed} \\
& \quad \quad \leq max \{ E_{tt}^{up}, E_{tt}^{do} \}
\cdot P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(9) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{up}
\leq E_{t}^{up} \cdot P_{total}(p)
\cdot L \cdot \Delta t \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(10) \quad \sum_{tt=t}^{t+R-1} DSM_{tt}^{do, shed}
\leq E_{t}^{do} \cdot P_{total}(p)
\cdot t_{shed}
\cdot \Delta t \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
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:**
*Standard model*
* Investment annuity:
.. math::
P_{invest}(0) \cdot c_{invest}(0)
* 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})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
*Multi-period model*
* Investment annuity:
.. math::
&
P_{invest}(p) \cdot A(c_{invest}(p), l, ir)
\frac {1}{ANF(d, ir)} \cdot DF^{-p} \\
&\\
& \quad \quad \quad \quad \forall p \in \mathbb{P}
In case, the remaining lifetime of a DSM unit is greater than 0 and
attribute `use_remaining_value` of the energy system is True,
the difference in value for the investment period compared to the
last period of the optimization horizon is accounted for
as an adder to the investment costs:
.. math::
&
P_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
A(c_{invest,var}(|P|), l_{r}, ir)\\
& \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
&\\
&
\forall p \in \textrm{PERIODS}
* :attr:`fixed_costs` not None for investments
.. math::
&
(\sum_{pp=year(p)}^{limit_{end}}
P_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
\cdot DF^{-p} \\
&\\
& \quad \quad \quad \quad \forall p \in \mathbb{P}
* 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})
\cdot \omega_{t}
\cdot DF^{-p} \\
& \quad \quad \quad \quad
\forall p, t \in \textrm{TIMEINDEX} \\
where:
* :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
and interest rate :math:`ir`.
* :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
and interest rate :math:`ir`.
* :math:`d=min\{year_{max} - year(p), l\}` defines the
number of years within the optimization horizon that investment
annuities are accounted for.
* :math:`year(p)` denotes the start year of period :math:`p`.
* :math:`year_{max}` denotes the last year of the optimization
horizon, i.e. at the end of the last period.
* :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
upper bound to ensure fixed costs for endogenous investments
to occur within the optimization horizon.
* :math:`DF=(1+dr)` is the discount factor.
The annuity / annuity factor hereby is:
.. math::
&
A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
\frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
&\\
&
ANF(d, ir)=\frac {(1+dr)^d \cdot dr} {(1+dr)^d - 1}
They are retrieved, using oemof.tools.economics annuity function. The
interest rate :math:`ir` for the annuity is defined as weighted
average costs of capital (wacc) and assumed constant over time.
See remarks in
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMOemofBlock`.
**Table: Symbols and attribute names of variables and parameters**
* Please refer to
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMDIWBlock`
for a variables and parameter description.
* The following variables and parameters are exclusively used for
investment modeling:
.. table:: Variables (V), Parameters (P) and Sets (S)
:widths: 25, 25, 10, 40
================================= ======================== ==== =======================================
symbol attribute type explanation
================================= ======================== ==== =======================================
:math:`P_{invest}(p)` `invest[p]` V | DSM capacity invested into in period p.
| Equals to the additionally shiftable resp. sheddable capacity.
:math:`invest_{min}(p)` `investment.minimum[p]` P minimum investment in period p
:math:`invest_{max}(p)` `investment.maximum[p]` P maximum investment in period p
:math:`P_{total}` `investment.total[p]` P total DSM capacity
:math:`costs_{invest}(p)` `investment.ep_costs[p]` P | specific investment annuity (standard model) resp.
| specific investment expenses (multi-period model)
:math:`\mathbb{P}` S Periods of the model
:math:`\textrm{TIMEINDEX}` S Timeindex set of the model (periods, timesteps)
================================= ======================== ==== =======================================
""" # noqa: E501
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])
self.OVERALL_MAXIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_maximum is not None
]
)
self.OVERALL_MINIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_minimum is not None
]
)
self.EXISTING_INVESTDSM = Set(
initialize=[g for g in group if g.investment.existing is not None]
)
# ************* VARIABLES *****************************
# Define bounds for investments in demand response
def _dsm_investvar_bound_rule(block, g, p):
"""Rule definition to bound the
demand response capacity invested in (`invest`).
"""
return g.investment.minimum[p], g.investment.maximum[p]
# Investment in DR capacity
self.invest = Var(
self.investdsm,
m.PERIODS,
within=NonNegativeReals,
bounds=_dsm_investvar_bound_rule,
)
# Total capacity
self.total = Var(self.investdsm, m.PERIODS, within=NonNegativeReals)
if m.es.periods is not None:
# Old capacity to be decommissioned (due to lifetime)
# Old capacity built out of old exogenous and endogenous capacities
self.old = Var(self.investdsm, m.PERIODS, within=NonNegativeReals)
# Old endogenous capacity to be decommissioned (due to lifetime)
self.old_end = Var(
self.investdsm, m.PERIODS, within=NonNegativeReals
)
# Old exogenous capacity to be decommissioned (due to lifetime)
self.old_exo = Var(
self.investdsm, m.PERIODS, within=NonNegativeReals
)
# 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 *****************************
# Handle unit lifetimes
def _total_dsm_capacity_rule(block):
"""Rule definition for determining total installed
capacity (taking decommissioning into account)
"""
for g in group:
for p in m.PERIODS:
if p == 0:
expr = (
self.total[g, p]
== self.invest[g, p] + g.investment.existing
)
self.total_dsm_rule.add((g, p), expr)
else:
expr = (
self.total[g, p]
== self.invest[g, p]
+ self.total[g, p - 1]
- self.old[g, p]
)
self.total_dsm_rule.add((g, p), expr)
self.total_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.total_dsm_rule_build = BuildAction(rule=_total_dsm_capacity_rule)
if m.es.periods is not None:
def _old_dsm_capacity_rule_end(block):
"""Rule definition for determining old endogenously installed
capacity to be decommissioned due to reaching its lifetime.
Investment and decommissioning periods are linked within
the constraint. The respective decommissioning period is
determined for every investment period based on the components
lifetime and a matrix describing its age of each endogenous
investment. Decommissioning can only occur at the beginning of
each period.
Note
----
For further information on the implementation check
PR#957 https://github.com/oemof/oemof-solph/pull/957
"""
for g in group:
lifetime = g.investment.lifetime
if lifetime is None:
msg = (
"You have to specify a lifetime "
"for a Flow with an associated "
"investment object in "
f"a multi-period model! Value for {g} "
"is missing."
)
raise ValueError(msg)
# get the period matrix describing the temporal distance
# between all period combinations.
periods_matrix = m.es.periods_matrix
# get the index of the minimum value in each row greater
# equal than the lifetime. This value equals the
# decommissioning period if not zero. The index of this
# value represents the investment period. If np.where
# condition is not met in any row, min value will be zero
decomm_periods = np.argmin(
np.where(
(periods_matrix >= lifetime),
periods_matrix,
np.inf,
),
axis=1,
)
# no decommissioning in first period
expr = self.old_end[g, 0] == 0
self.old_dsm_rule_end.add((g, 0), expr)
# all periods not in decomm_periods have no decommissioning
# zero is excluded
for p in m.PERIODS:
if p not in decomm_periods and p != 0:
expr = self.old_end[g, p] == 0
self.old_dsm_rule_end.add((g, p), expr)
# multiple invests can be decommissioned in the same period
# but only sequential ones, thus a bookkeeping is
# introduced andconstraints are added to equation one
# iteration later.
last_decomm_p = np.nan
# loop over invest periods (values are decomm_periods)
for invest_p, decomm_p in enumerate(decomm_periods):
# Add constraint of iteration before
# (skipped in first iteration by last_decomm_p = nan)
if (decomm_p != last_decomm_p) and (
last_decomm_p is not np.nan
):
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
# no decommissioning if decomm_p is zero
if decomm_p == 0:
# overwrite decomm_p with zero to avoid
# chaining invest periods in next iteration
last_decomm_p = 0
# if decomm_p is the same as the last one chain invest
# period
elif decomm_p == last_decomm_p:
expr += self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# if decomm_p is not zero, not the same as the last one
# and it's not the first period
else:
expr = self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# Add constraint of very last iteration
if last_decomm_p != 0:
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
self.old_dsm_rule_end = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_end_build = BuildAction(
rule=_old_dsm_capacity_rule_end
)
def _old_dsm_capacity_rule_exo(block):
"""Rule definition for determining old exogenously given
capacity to be decommissioned due to reaching its lifetime
"""
for g in group:
age = g.investment.age
lifetime = g.investment.lifetime
is_decommissioned = False
for p in m.PERIODS:
# No shutdown in first period
if p == 0:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
elif lifetime - age <= m.es.periods_years[p]:
# Track decommissioning status
if not is_decommissioned:
expr = (
self.old_exo[g, p] == g.investment.existing
)
is_decommissioned = True
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
self.old_dsm_rule_exo = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_exo_build = BuildAction(
rule=_old_dsm_capacity_rule_exo
)
def _old_dsm_capacity_rule(block):
"""Rule definition for determining (overall) old capacity
to be decommissioned due to reaching its lifetime
"""
for g in group:
for p in m.PERIODS:
expr = (
self.old[g, p]
== self.old_end[g, p] + self.old_exo[g, p]
)
self.old_dsm_rule.add((g, p), expr)
self.old_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.old_dsm_rule_build = BuildAction(rule=_old_dsm_capacity_rule)
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 p, t in m.TIMEINDEX:
for g in group:
# first time steps: 0 + delay time
if t <= g.delay_time:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs)
)
# main use case
elif g.delay_time < t <= m.TIMESTEPS.at(-1) - g.delay_time:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs)
)
# last time steps: end - delay time
else:
# Inflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DSM
rhs = (
g.demand[t] * g.max_demand[p]
+ self.dsm_up[g, t]
- sum(
self.dsm_do_shift[g, tt, t]
for tt in range(
t - g.delay_time, m.TIMESTEPS.at(-1) + 1
)
)
- self.dsm_do_shed[g, t]
)
# add constraint
block.input_output_relation.add(
(g, p, t), (lhs == rhs)
)
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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.at(-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.at(-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 p, t in m.TIMEINDEX:
for g in group:
# DSM up
lhs = self.dsm_up[g, t]
# Capacity dsm_up
rhs = g.capacity_up[t] * self.total[g, p]
# add constraint
block.dsm_up_constraint.add((g, p, t), (lhs <= rhs))
self.dsm_up_constraint = Constraint(
group, m.TIMEINDEX, 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 p, tt in m.TIMEINDEX:
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.total[g, p]
# add constraint
block.dsm_do_constraint.add((g, p, tt), (lhs <= rhs))
# main use case
elif (
g.delay_time < tt <= m.TIMESTEPS.at(-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.total[g, p]
# add constraint
block.dsm_do_constraint.add((g, p, 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.at(-1) + 1
)
)
+ self.dsm_do_shed[g, tt]
)
# Capacity DSM down
rhs = g.capacity_down[tt] * self.total[g, p]
# add constraint
block.dsm_do_constraint.add((g, p, tt), (lhs <= rhs))
self.dsm_do_constraint = Constraint(
group, m.TIMEINDEX, 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 p, tt in m.TIMEINDEX:
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.capacity_down[tt],
)
* self.total[g, p]
)
# add constraint
block.C2_constraint.add((g, p, tt), (lhs <= rhs))
elif (
g.delay_time < tt <= m.TIMESTEPS.at(-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.capacity_down[tt],
)
* self.total[g, p]
)
# add constraint
block.C2_constraint.add((g, p, 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.at(-1) + 1
)
)
+ self.dsm_do_shed[g, tt]
)
# max capacity at tt
rhs = (
max(
g.capacity_up[tt],
g.capacity_down[tt],
)
* self.total[g, p]
)
# add constraint
block.C2_constraint.add((g, p, tt), (lhs <= rhs))
self.C2_constraint = Constraint(group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.at(-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.total[g, p]
* g.delay_time
* m.timeincrement[t]
)
# add constraint
block.recovery_constraint.add(
(g, p, 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.at(-1) + 1)
)
# max energy shift for shifting process
rhs = (
g.capacity_up[t]
* self.total[g, p]
* g.delay_time
* m.timeincrement[t]
)
# add constraint
block.recovery_constraint.add(
(g, p, t), (lhs <= rhs)
)
else:
pass # return(Constraint.Skip)
self.recovery_constraint = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
for g in group:
# Only applicable for load shedding
if g.shed_eligibility:
# main use case
if t <= m.TIMESTEPS.at(-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.total[g, p]
* g.shed_time
* m.timeincrement[t]
)
# add constraint
block.shed_limit_constraint.add(
(g, p, 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.at(-1) + 1)
)
# max energy shift for shifting process
rhs = (
g.capacity_down[t]
* self.total[g, p]
* g.shed_time
* m.timeincrement[t]
)
# add constraint
block.shed_limit_constraint.add(
(g, p, t), (lhs <= rhs)
)
else:
pass # return(Constraint.Skip)
self.shed_limit_constraint = Constraint(
group, m.TIMEINDEX, noruleinit=True
)
self.shed_limit_constraint_build = BuildAction(
rule=shed_limit_constraint_rule
)
if m.es.periods is not None:
def _overall_dsm_maximum_investflow_rule(block):
"""Rule definition for maximum overall investment
in investment case.
"""
for g in self.OVERALL_MAXIMUM_INVESTDSM:
for p in m.PERIODS:
expr = self.total[g, p] <= g.investment.overall_maximum
self.overall_dsm_maximum.add((g, p), expr)
self.overall_dsm_maximum = Constraint(
self.OVERALL_MAXIMUM_INVESTDSM, m.PERIODS, noruleinit=True
)
self.overall_maximum_build = BuildAction(
rule=_overall_dsm_maximum_investflow_rule
)
def _overall_minimum_dsm_investflow_rule(block):
"""Rule definition for minimum overall investment
in investment case.
Note: This is only applicable for the last period
"""
for g in self.OVERALL_MINIMUM_INVESTDSM:
expr = (
g.investment.overall_minimum
<= self.total[g, m.PERIODS.at(-1)]
)
self.overall_minimum.add(g, expr)
self.overall_minimum = Constraint(
self.OVERALL_MINIMUM_INVESTDSM, noruleinit=True
)
self.overall_minimum_build = BuildAction(
rule=_overall_minimum_dsm_investflow_rule
)
def _objective_expression(self):
r"""Objective expression with variable and investment costs for DSM"""
m = self.parent_block()
investment_costs = 0
period_investment_costs = {p: 0 for p in m.PERIODS}
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for g in self.investdsm:
for p in m.PERIODS:
if g.investment.ep_costs is not None:
investment_costs += (
self.invest[g, p] * g.investment.ep_costs[p]
)
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]
else:
msg = (
"You did not specify an interest rate.\n"
"It will be set equal to the discount_rate of {} "
"of the model as a default.\nThis corresponds to a "
"social planner point of view and does not reflect "
"microeconomic interest requirements."
)
for g in self.investdsm:
if g.investment.ep_costs is not None:
lifetime = g.investment.lifetime
interest = g.investment.interest_rate
if interest == 0:
warn(
msg.format(m.discount_rate),
debugging.SuspiciousUsageWarning,
)
interest = m.discount_rate
for p in m.PERIODS:
annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=lifetime,
wacc=interest,
)
duration = min(
m.es.end_year_of_optimization
- m.es.periods_years[p],
lifetime,
)
present_value_factor = 1 / economics.annuity(
capex=1, n=duration, wacc=interest
)
investment_costs_increment = (
self.invest[g, p] * annuity * present_value_factor
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
g,
m.es.end_year_of_optimization,
lifetime,
interest,
)
)
investment_costs += (
investment_costs_increment
+ remaining_value_difference
)
period_investment_costs[
p
] += investment_costs_increment
else:
raise ValueError("Missing value for investment costs!")
for p, t in m.TIMEINDEX:
variable_costs += (
self.dsm_up[g, t]
* m.objective_weighting[t]
* g.cost_dsm_up[t]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
for p in m.PERIODS:
range_limit = min(
m.es.end_year_of_optimization,
m.es.periods_years[p] + lifetime,
)
fixed_costs += sum(
self.invest[g, p]
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(
m.es.periods_years[p],
range_limit,
)
)
for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
age = g.investment.age
range_limit = min(
m.es.end_year_of_optimization, lifetime - age
)
fixed_costs += sum(
g.investment.existing
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(range_limit)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.investment_costs = Expression(expr=investment_costs)
self.period_investment_costs = period_investment_costs
self.costs = Expression(
expr=investment_costs + variable_costs + fixed_costs
)
return self.costs
def _evaluate_remaining_value_difference(
self,
m,
p,
g,
end_year_of_optimization,
lifetime,
interest,
):
"""Evaluate and return the remaining value difference of an investment
The remaining value difference in the net present values if the asset
was to be liquidated at the end of the optimization horizon and the
net present value using the original investment expenses.
Parameters
----------
m : oemof.solph.models.Model
Optimization model
p : int
Period in which investment occurs
g : oemof.solph.components.experimental.SinkDSM
storage unit
end_year_of_optimization : int
Last year of the optimization horizon
lifetime : int
lifetime of investment considered
interest : float
Demanded interest rate for investment
"""
if m.es.use_remaining_value:
if end_year_of_optimization - m.es.periods_years[p] < lifetime:
remaining_lifetime = lifetime - (
end_year_of_optimization - m.es.periods_years[p]
)
remaining_annuity = economics.annuity(
capex=g.investment.ep_costs[-1],
n=remaining_lifetime,
wacc=interest,
)
original_annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=remaining_lifetime,
wacc=interest,
)
present_value_factor_remaining = 1 / economics.annuity(
capex=1, n=remaining_lifetime, wacc=interest
)
return (
self.invest[g, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
else:
return 0
else:
return 0
[docs]class SinkDSMDLRBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DLR" approach
**The following constraints are created for approach = "DLR":**
.. _SinkDSMDLRBlock equations:
.. math::
&
(1) \quad DSM_{h, t}^{up} = 0 \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
& \\
&
(2) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{False} \\
& \\
&
(3) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max} \\
& \quad \quad \quad \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} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(4) \quad DSM_{h, t}^{balanceDo} =
\frac{DSM_{h, t - h}^{do, shift}}{\eta} \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in [h..T] \\
& \\
&
(5) \quad DSM_{h, t}^{balanceUp} =
DSM_{h, t-h}^{up} \cdot \eta \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in [h..T] \\
& \\
&
(6) \quad DSM_{h, t}^{do, shift} = 0
\quad \forall h \in H_{DR} \\
& \quad \quad \quad \quad \forall t \in [T - h..T] \\
& \\
&
(7) \quad DSM_{h, t}^{up} = 0
\quad \forall h \in H_{DR} \\
& \quad \quad \quad \quad \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 \quad \quad \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 \quad \quad \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 \quad \quad \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 \quad \quad \quad \forall t \in [1..T] \\
& \\
&
(12) \quad W_{t}^{levelDo} \leq \overline{E}_{t}^{do}
\cdot E_{max, do} \cdot t_{shift} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \\
&
(13) \quad W_{t}^{levelUp} \leq \overline{E}_{t}^{up}
\cdot E_{max, up} \cdot t_{shift} \\
& \quad \quad \quad \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} \\
& \quad \quad \textrm{(optional 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} \\
& \quad \quad \textrm{(optional 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 \quad \quad \quad \forall t \in [t-t_{dayLimit}..T] \\
& \quad \quad \textrm{(optional 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 \quad \quad \quad \forall t \in [t-t_{dayLimit}..T] \\
& \quad \quad \textrm{(optional 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} \\
& \quad \quad \leq \max \{E_{t}^{up} \cdot E_{max, up},
E_{t}^{do} \cdot E_{max, do} \} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
& \quad \quad \textrm{(optional 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})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
* :attr:`fixed_costs` not None
.. math::
\displaystyle \sum_{pp=0}^{year_{max}} max\{E_{up, max}, E_{do, max}\}
\cdot c_{fixed}(pp) \cdot DF^{-pp}
**Table: Symbols and attribute names of variables and parameters**
.. table:: Variables (V), Parameters (P) and (additional) Sets (S)
:widths: 25, 25, 10, 40
=========================================== ================================= ==== =======================================
symbol attribute type explanation
=========================================== ================================= ==== =======================================
:math:`DSM_{h, t}^{up}` `dsm_up[g, h, t]` V DSM up shift (additional load) in hour t with delay time h
:math:`DSM_{h, t}^{do, shift}` `dsm_do_shift[g, h, t]` V DSM down shift (less load) in hour t with delay time h
:math:`DSM_{h, t}^{balanceUp}` `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}` `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}` `dsm_do_shed[g, t]` V DSM shedded (capacity shedded, i.e. not compensated for)
:math:`\dot{E}_{t}` `SinkDSM.inputs` V Energy flowing in from (electrical) inflow bus
:math:`h` `delay_time` P | Maximum 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(len(delay_time))` S | Set of feasible delay times for load shift
| of a certain DSM portfolio
:math:`t_{shift}` `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}` `shed_time` P Maximum time for one load shedding process
:math:`demand_{t}` `demand[t]` P (Electrical) demand series (normalized)
:math:`demand_{max}` `max_demand` P Maximum demand value
:math:`E_{t}^{do}` `capacity_down[t]` P | Capacity allowed for a load adjustment downwards
| (normalized; shifting + shedding)
:math:`E_{t}^{up}` `capacity_up[t]` P Capacity allowed for a shift upwards (normalized)
:math:`E_{do, max}` `max_capacity_down` P | Maximum capacity allowed for a load adjustment downwards
| (shifting + shedding)
:math:`E_{up, max}` `max_capacity_up` P Maximum capacity allowed for a shift upwards
:math:`\eta` `efficiency` P Efficiency for load shifting processes
:math:`\mathbb{T}` S Time steps of the model
:math:`e_{shift}` `shift_eligibility` P | Boolean parameter indicating if unit can be used
| for load shifting
:math:`e_{shed}` `shed_eligibility` P | Boolean parameter indicating if unit can be used
| for load shedding
:math:`cost_{t}^{dsm, up}` `cost_dsm_up[t]` P Variable costs for an upwards shift
:math:`cost_{t}^{dsm, do, shift}` `cost_dsm_down_shift[t]` P Variable costs for a downwards shift (load shifting)
:math:`cost_{t}^{dsm, do, shed}` `cost_dsm_down_shift[t]` P Variable costs for shedding load
:math:`\omega_{t}` P Objective weighting of the model for timestep t
:math:`R_{shi}` `recovery_time_shift` P | Minimum time between the end of one load shifting process
| and the start of another
:math:`R_{she}` `recovery_time_shed` P | Minimum time between the end of one load shedding process
| and the start of another
:math:`\Delta t` P The time increment of the model
:math:`\omega_{t}` P Objective weighting of the model for timestep t
:math:`n_{yearLimitShift}` `n_yeaLimitShift` P | Maximum allowed number of load shifts (at full capacity)
| in the optimization timeframe
:math:`n_{yearLimitShed}` `n_yeaLimitShed` P | Maximum allowed number of load sheds (at full capacity)
| in the optimization timeframe
:math:`t_{dayLimit}` `t_dayLimit` P | Maximum duration of load shifts at full capacity per day
| resp. in the last hours before the current"
:math:`year_{max}` P Last year of the optimization horizon
=========================================== ================================= ==== =======================================
""" # noqa: E501
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.
BusBlock outflow == Demand +- DR (i.e. effective Sink consumption)
"""
for p, t in m.TIMEINDEX:
for g in group:
# outflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DR
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs))
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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.at(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.at(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.at(-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.at(-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:
for p in m.PERIODS:
# sum of all load reductions
lhs = sum(
self.dsm_do_shed[g, t]
for pp, t in m.TIMEINDEX
if pp == p
)
# 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, p), (lhs <= rhs))
else:
pass # return(Constraint.Skip)
self.dr_yearly_limit_shed = Constraint(
group, m.PERIODS, 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:
for p in m.PERIODS:
# sum of all load reductions
lhs = sum(
sum(
self.dsm_do_shift[g, h, t]
for h in g.delay_time
)
for pp, t in m.TIMEINDEX
if pp == p
)
# 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, p), (lhs <= rhs))
else:
pass # return(Constraint.Skip)
self.dr_yearly_limit_red = Constraint(
group, m.PERIODS, 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:
for p in m.PERIODS:
# sum of all load increases
lhs = sum(
sum(self.dsm_up[g, h, t] for h in g.delay_time)
for pp, t in m.TIMEINDEX
if pp == p
)
# 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, p), (lhs <= rhs))
else:
pass # return(Constraint.Skip)
self.dr_yearly_limit_inc = Constraint(
group, m.PERIODS, 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()
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for t in m.TIMESTEPS:
for g in self.DR:
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]
else:
for g in self.DR:
for p, t in m.TIMEINDEX:
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.fixed_costs[0] is not None:
fixed_costs += sum(
max(g.max_capacity_up, g.max_capacity_down)
* g.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(m.es.end_year_of_optimization)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.costs = Expression(expr=variable_costs + fixed_costs)
return self.costs
[docs]class SinkDSMDLRInvestmentBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DLR" approach and `investment` defined
**The following constraints are created for approach = "DLR" with an
investment object defined:**
.. _SinkDSMDLRInvestmentBlock equations:
.. math::
&
(1) \quad invest_{min}(p) \leq invest(p) \leq invest_{max}(p) \\
& \quad \quad \quad \quad \forall p \in \mathbb{P}
& \\
&
(2) \quad DSM_{h, t}^{up} = 0 \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in \mathbb{T}
\quad \textrm{if} \quad e_{shift} = \textrm{False} \\
&
(3) \quad DSM_{t}^{do, shed} = 0 \\
& \quad \quad \quad \quad \forall t \in \mathbb{T}
\quad \textrm{if} \quad e_{shed} = \textrm{False} \\
& \\
&
(4) \quad \dot{E}_{t} = demand_{t} \cdot demand_{max}(p) \\
& \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 \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(5) \quad DSM_{h, t}^{balanceDo} =
\frac{DSM_{h, t - h}^{do, shift}}{\eta} \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in [h..T] \\
& \\
&
(6) \quad DSM_{h, t}^{balanceUp} =
DSM_{h, t-h}^{up} \cdot \eta \\
& \quad \quad \quad \quad \forall h \in H_{DR}, t \in [h..T] \\
& \\
&
(7) \quad DSM_{h, t}^{do, shift} = 0
\quad \forall h \in H_{DR} \\
& \quad \quad \quad \quad \forall t \in [T - h..T] \\
& \\
&
(8) \quad DSM_{h, t}^{up} = 0 \\
& \quad \quad \quad \quad \forall h \in H_{DR}, 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 P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(10) \quad \displaystyle\sum_{h=1}^{H_{DR}} (DSM_{h, t}^{up}
+ DSM_{h, t}^{balanceDo})
\leq E_{t}^{up} \cdot P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(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 \quad \quad \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 \quad \quad \quad \forall t \in [1..T] \\
& \\
&
(13) \quad W_{t}^{levelDo} \leq \overline{E}_{t}^{do}
\cdot P_{total}(p) \cdot t_{shift} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(14) \quad W_{t}^{levelUp} \leq \overline{E}_{t}^{up}
\cdot P_{total}(p) \cdot t_{shift} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \\
&
(15) \quad \displaystyle\sum_{t=0}^{T} DSM_{t}^{do, shed}
\leq P_{total}(p) \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 P_{total}(p)
\cdot \overline{E}_{t}^{do}
\cdot t_{shift}
\cdot n^{yearLimitShift} \\
& \quad \quad \textrm{(optional constraint)} \\
& \\
&
(17) \quad \displaystyle\sum_{t=0}^{T} \sum_{h=1}^{H_{DR}}
DSM_{h, t}^{up}
\leq P_{total}(p)
\cdot \overline{E}_{t}^{up}
\cdot t_{shift}
\cdot n^{yearLimitShift} \\
& \quad \quad \textrm{(optional constraint)} \\
&
(18) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{do, shift}
\leq P_{total}(p)
\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 \quad \quad \quad \forall t \in [t-t_{dayLimit}..T] \\
& \quad \quad \textrm{(optional constraint)} \\
& \\
&
(19) \quad \displaystyle\sum_{h=1}^{H_{DR}} DSM_{h, t}^{up}
\leq (invest + E_{exist})
\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 \quad \quad \quad \forall t \in [t-t_{dayLimit}..T] \\
& \quad \quad \textrm{(optional 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}, E_{t}^{do} \} \cdot P_{total}(p) \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
& \quad \quad \textrm{(optional 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:**
*Standard model*
* Investment annuity:
.. math::
P_{invest}(0) \cdot c_{invest}(0)
* 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})
\cdot \omega_{t} \\
& \quad \quad \quad \quad \forall t \in \mathbb{T} \\
*Multi-period model*
* Investment annuity:
.. math::
&
P_{invest}(p) \cdot A(c_{invest}(p), l, ir)
\cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p} \\
&\\
&
\forall p \in \mathbb{P}
In case, the remaining lifetime of a DSM unit is greater than 0 and
attribute `use_remaining_value` of the energy system is True,
the difference in value for the investment period compared to the
last period of the optimization horizon is accounted for
as an adder to the investment costs:
.. math::
&
P_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
A(c_{invest,var}(|P|), l_{r}, ir)\\
& \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
&\\
&
\forall p \in \textrm{PERIODS}
* :attr:`fixed_costs` not None for investments
.. math::
&
(\sum_{pp=year(p)}^{limit_{end}}
P_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
\cdot DF^{-p} \\
&\\
&
\forall p \in \mathbb{P}
* 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})
\cdot \omega_{t} \cdot DF^{-p} \\
& \quad \quad \quad \quad \forall p, t \in \textrm{TIMEINDEX} \\
where:
* :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
and interest rate :math:`ir`.
* :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
and interest rate :math:`ir`.
* :math:`d=min\{year_{max} - year(p), l\}` defines the
number of years within the optimization horizon that investment
annuities are accounted for.
* :math:`year(p)` denotes the start year of period :math:`p`.
* :math:`year_{max}` denotes the last year of the optimization
horizon, i.e. at the end of the last period.
* :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
upper bound to ensure fixed costs for endogenous investments
to occur within the optimization horizon.
* :math:`DF=(1+dr)` is the discount factor.
The annuity / annuity factor hereby is:
.. math::
&
A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
\frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
&\\
&
ANF(d, ir)=\frac {(1+dr)^d \cdot dr} {(1+dr)^d - 1}
They are retrieved, using oemof.tools.economics annuity function. The
interest rate :math:`ir` for the annuity is defined as weighted
average costs of capital (wacc) and assumed constant over time.
See remarks in
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMOemofBlock`.
**Table: Symbols and attribute names of variables and parameters**
* Please refer to
:class:`oemof.solph.components.experimental._sink_dsm.SinkDSMDLRBlock`.
* The following variables and parameters are exclusively used for
investment modeling:
.. table:: Variables (V), Parameters (P) and Sets (S)
:widths: 25, 25, 10, 40
================================= ======================== ==== =======================================
symbol attribute type explanation
================================= ======================== ==== =======================================
:math:`P_{invest}(p)` `invest[p]` V | DSM capacity invested into in period p.
| Equals to the additionally shiftable resp. sheddable capacity.
:math:`invest_{min}(p)` `investment.minimum[p]` P minimum investment in period p
:math:`invest_{max}(p)` `investment.maximum[p]` P maximum investment in period p
:math:`P_{total}` `investment.total[p]` P total DSM capacity
:math:`costs_{invest}(p)` `investment.ep_costs[p]` P | specific investment annuity (standard model) resp.
| specific investment expenses (multi-period model)
:math:`\mathbb{P}` S Periods of the model
:math:`\textrm{TIMEINDEX}` S Timeindex set of the model (periods, timesteps)
================================= ======================== ==== =======================================
""" # noqa: E501
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]
],
)
self.OVERALL_MAXIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_maximum is not None
]
)
self.OVERALL_MINIMUM_INVESTDSM = Set(
initialize=[
g for g in group if g.investment.overall_minimum is not None
]
)
self.EXISTING_INVESTDSM = Set(
initialize=[g for g in group if g.investment.existing is not None]
)
# ************* VARIABLES *****************************
# Define bounds for investments in demand response
def _dr_investvar_bound_rule(block, g, p):
"""Rule definition to bound the
invested demand response capacity `invest`.
"""
return g.investment.minimum[p], g.investment.maximum[p]
# Investment in DR capacity
self.invest = Var(
self.INVESTDR,
m.PERIODS,
within=NonNegativeReals,
bounds=_dr_investvar_bound_rule,
)
# Total capacity
self.total = Var(self.INVESTDR, m.PERIODS, within=NonNegativeReals)
if m.es.periods is not None:
# Old capacity to be decommissioned (due to lifetime)
self.old = Var(self.INVESTDR, m.PERIODS, within=NonNegativeReals)
# Old endogenous capacity to be decommissioned (due to lifetime)
self.old_end = Var(
self.INVESTDR, m.PERIODS, within=NonNegativeReals
)
# Old exogenous capacity to be decommissioned (due to lifetime)
self.old_exo = Var(
self.INVESTDR, m.PERIODS, within=NonNegativeReals
)
# 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 *****************************
# Handle unit lifetimes
def _total_capacity_rule(block):
"""Rule definition for determining total installed
capacity (taking decommissioning into account)
"""
for g in group:
for p in m.PERIODS:
if p == 0:
expr = (
self.total[g, p]
== self.invest[g, p] + g.investment.existing
)
self.total_dsm_rule.add((g, p), expr)
else:
expr = (
self.total[g, p]
== self.invest[g, p]
+ self.total[g, p - 1]
- self.old[g, p]
)
self.total_dsm_rule.add((g, p), expr)
self.total_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.total_dsm_rule_build = BuildAction(rule=_total_capacity_rule)
if m.es.periods is not None:
def _old_dsm_capacity_rule_end(block):
"""Rule definition for determining old endogenously installed
capacity to be decommissioned due to reaching its lifetime.
Investment and decommissioning periods are linked within
the constraint. The respective decommissioning period is
determined for every investment period based on the components
lifetime and a matrix describing its age of each endogenous
investment. Decommissioning can only occur at the beginning of
each period.
Note
----
For further information on the implementation check
PR#957 https://github.com/oemof/oemof-solph/pull/957
"""
for g in group:
lifetime = g.investment.lifetime
if lifetime is None:
msg = (
"You have to specify a lifetime "
"for a Flow with an associated "
"investment object in "
f"a multi-period model! Value for {(g)} "
"is missing."
)
raise ValueError(msg)
# get the period matrix describing the temporal distance
# between all period combinations.
periods_matrix = m.es.periods_matrix
# get the index of the minimum value in each row greater
# equal than the lifetime. This value equals the
# decommissioning period if not zero. The index of this
# value represents the investment period. If np.where
# condition is not met in any row, min value will be zero
decomm_periods = np.argmin(
np.where(
(periods_matrix >= lifetime),
periods_matrix,
np.inf,
),
axis=1,
)
# no decommissioning in first period
expr = self.old_end[g, 0] == 0
self.old_dsm_rule_end.add((g, 0), expr)
# all periods not in decomm_periods have no decommissioning
# zero is excluded
for p in m.PERIODS:
if p not in decomm_periods and p != 0:
expr = self.old_end[g, p] == 0
self.old_dsm_rule_end.add((g, p), expr)
# multiple invests can be decommissioned in the same period
# but only sequential ones, thus a bookkeeping is
# introduced andconstraints are added to equation one
# iteration later.
last_decomm_p = np.nan
# loop over invest periods (values are decomm_periods)
for invest_p, decomm_p in enumerate(decomm_periods):
# Add constraint of iteration before
# (skipped in first iteration by last_decomm_p = nan)
if (decomm_p != last_decomm_p) and (
last_decomm_p is not np.nan
):
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
# no decommissioning if decomm_p is zero
if decomm_p == 0:
# overwrite decomm_p with zero to avoid
# chaining invest periods in next iteration
last_decomm_p = 0
# if decomm_p is the same as the last one chain invest
# period
elif decomm_p == last_decomm_p:
expr += self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# if decomm_p is not zero, not the same as the last one
# and it's not the first period
else:
expr = self.invest[g, invest_p]
# overwrite decomm_p
last_decomm_p = decomm_p
# Add constraint of very last iteration
if last_decomm_p != 0:
expr = self.old_end[g, last_decomm_p] == expr
self.old_dsm_rule_end.add((g, last_decomm_p), expr)
self.old_dsm_rule_end = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_end_build = BuildAction(
rule=_old_dsm_capacity_rule_end
)
def _old_dsm_capacity_rule_exo(block):
"""Rule definition for determining old exogenously given
capacity to be decommissioned due to reaching its lifetime
"""
for g in group:
age = g.investment.age
lifetime = g.investment.lifetime
is_decommissioned = False
for p in m.PERIODS:
# No shutdown in first period
if p == 0:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
elif lifetime - age <= m.es.periods_years[p]:
# Track decommissioning status
if not is_decommissioned:
expr = (
self.old_exo[g, p] == g.investment.existing
)
is_decommissioned = True
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
else:
expr = self.old_exo[g, p] == 0
self.old_dsm_rule_exo.add((g, p), expr)
self.old_dsm_rule_exo = Constraint(
group, m.PERIODS, noruleinit=True
)
self.old_dsm_rule_exo_build = BuildAction(
rule=_old_dsm_capacity_rule_exo
)
def _old_dsm_capacity_rule(block):
"""Rule definition for determining (overall) old capacity
to be decommissioned due to reaching its lifetime
"""
for g in group:
for p in m.PERIODS:
expr = (
self.old[g, p]
== self.old_end[g, p] + self.old_exo[g, p]
)
self.old_dsm_rule.add((g, p), expr)
self.old_dsm_rule = Constraint(group, m.PERIODS, noruleinit=True)
self.old_dsm_rule_build = BuildAction(rule=_old_dsm_capacity_rule)
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.
BusBlock outflow == Demand +- DR (i.e. effective Sink consumption)
"""
for p, t in m.TIMEINDEX:
for g in group:
# outflow from bus
lhs = m.flow[g.inflow, g, p, t]
# Demand +- DR
rhs = (
g.demand[t] * g.max_demand[p]
+ 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, p, t), (lhs == rhs))
self.input_output_relation = Constraint(
group, m.TIMEINDEX, 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.at(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.at(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.at(-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.at(-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 p, t in m.TIMEINDEX:
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.total[g, p]
# add constraint
block.availability_red.add((g, p, t), (lhs <= rhs))
self.availability_red = Constraint(group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.total[g, p]
# add constraint
block.availability_inc.add((g, p, t), (lhs <= rhs))
self.availability_inc = Constraint(group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.total[g, p]
* g.shift_time
)
# add constraint
block.dr_storage_limit_red.add((g, p, 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, p, t), (lhs <= rhs))
self.dr_storage_limit_red = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.total[g, p] * g.shift_time
# add constraint
block.dr_storage_limit_inc.add((g, p, t), (lhs <= rhs))
self.dr_storage_limit_inc = Constraint(
group, m.TIMEINDEX, 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:
for p in m.PERIODS:
if g.shed_eligibility:
# sum of all load reductions
lhs = sum(
self.dsm_do_shed[g, t]
for pp, t in m.TIMEINDEX
if pp == p
)
# year limit
rhs = (
g.capacity_down_mean
* self.total[g, p]
* g.shed_time
* g.n_yearLimit_shed
)
# add constraint
block.dr_yearly_limit_shed.add((g, p), (lhs <= rhs))
self.dr_yearly_limit_shed = Constraint(
group, m.PERIODS, 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:
for p in m.PERIODS:
# sum of all load reductions
lhs = sum(
sum(
self.dsm_do_shift[g, h, t]
for h in g.delay_time
)
for pp, t in m.TIMEINDEX
if pp == p
)
# year limit
rhs = (
g.capacity_down_mean
* self.total[g, p]
* g.shift_time
* g.n_yearLimit_shift
)
# add constraint
block.dr_yearly_limit_red.add((g, p), (lhs <= rhs))
else:
pass # return(Constraint.Skip)
self.dr_yearly_limit_red = Constraint(
group, m.PERIODS, 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:
for p in m.PERIODS:
# sum of all load increases
lhs = sum(
sum(self.dsm_up[g, h, t] for h in g.delay_time)
for pp, t in m.TIMEINDEX
if pp == p
)
# year limit
rhs = (
g.capacity_up_mean
* self.total[g, p]
* g.shift_time
* g.n_yearLimit_shift
)
# add constraint
block.dr_yearly_limit_inc.add((g, p), (lhs <= rhs))
else:
pass # return(Constraint.Skip)
self.dr_yearly_limit_inc = Constraint(
group, m.PERIODS, 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 p, t in m.TIMEINDEX:
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.total[
g, p
] * 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, p, t), (lhs <= rhs)
)
else:
pass # return(Constraint.Skip)
else:
pass # return(Constraint.Skip)
self.dr_daily_limit_red = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.total[
g, p
] * 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, p, t), (lhs <= rhs)
)
else:
pass # return(Constraint.Skip)
else:
pass # return(Constraint.Skip)
self.dr_daily_limit_inc = Constraint(
group, m.TIMEINDEX, 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 p, t in m.TIMEINDEX:
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.capacity_up[t],
)
* self.total[g, p]
)
# add constraint
block.dr_logical_constraint.add(
(g, p, t), (lhs <= rhs)
)
else:
pass # return(Constraint.Skip)
self.dr_logical_constraint = Constraint(
group, m.TIMEINDEX, noruleinit=True
)
self.dr_logical_constraint_build = BuildAction(
rule=dr_logical_constraint_rule
)
if m.es.periods is not None:
def _overall_dsm_maximum_investflow_rule(block):
"""Rule definition for maximum overall investment
in investment case.
"""
for g in self.OVERALL_MAXIMUM_INVESTDSM:
for p in m.PERIODS:
expr = self.total[g, p] <= g.investment.overall_maximum
self.overall_dsm_maximum.add((g, p), expr)
self.overall_dsm_maximum = Constraint(
self.OVERALL_MAXIMUM_INVESTDSM, m.PERIODS, noruleinit=True
)
self.overall_maximum_build = BuildAction(
rule=_overall_dsm_maximum_investflow_rule
)
def _overall_minimum_dsm_investflow_rule(block):
"""Rule definition for minimum overall investment
in investment case.
Note: This is only applicable for the last period
"""
for g in self.OVERALL_MINIMUM_INVESTDSM:
expr = (
g.investment.overall_minimum
<= self.total[g, m.PERIODS.at(-1)]
)
self.overall_minimum.add(g, expr)
self.overall_minimum = Constraint(
self.OVERALL_MINIMUM_INVESTDSM, noruleinit=True
)
self.overall_minimum_build = BuildAction(
rule=_overall_minimum_dsm_investflow_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
period_investment_costs = {p: 0 for p in m.PERIODS}
variable_costs = 0
fixed_costs = 0
if m.es.periods is None:
for g in self.INVESTDR:
for p in m.PERIODS:
if g.investment.ep_costs is not None:
investment_costs += (
self.invest[g, p] * g.investment.ep_costs[p]
)
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]
else:
msg = (
"You did not specify an interest rate.\n"
"It will be set equal to the discount_rate of {} "
"of the model as a default.\nThis corresponds to a "
"social planner point of view and does not reflect "
"microeconomic interest requirements."
)
for g in self.INVESTDR:
if g.investment.ep_costs is not None:
lifetime = g.investment.lifetime
interest = g.investment.interest_rate
if interest == 0:
warn(
msg.format(m.discount_rate),
debugging.SuspiciousUsageWarning,
)
interest = m.discount_rate
for p in m.PERIODS:
annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=lifetime,
wacc=interest,
)
duration = min(
m.es.end_year_of_optimization
- m.es.periods_years[p],
lifetime,
)
present_value_factor = 1 / economics.annuity(
capex=1, n=duration, wacc=interest
)
investment_costs_increment = (
self.invest[g, p] * annuity * present_value_factor
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
g,
m.es.end_year_of_optimization,
lifetime,
interest,
)
)
investment_costs += (
investment_costs_increment
+ remaining_value_difference
)
period_investment_costs[
p
] += investment_costs_increment
else:
raise ValueError("Missing value for investment costs!")
for p, t in m.TIMEINDEX:
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
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]
* (1 + m.discount_rate) ** (-m.es.periods_years[p])
)
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
for p in m.PERIODS:
range_limit = min(
m.es.end_year_of_optimization,
m.es.periods_years[p] + lifetime,
)
fixed_costs += sum(
self.invest[g, p]
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(
m.es.periods_years[p],
range_limit,
)
)
for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
lifetime = g.investment.lifetime
age = g.investment.age
range_limit = min(
m.es.end_year_of_optimization, lifetime - age
)
fixed_costs += sum(
g.investment.existing
* g.investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(range_limit)
)
self.variable_costs = Expression(expr=variable_costs)
self.fixed_costs = Expression(expr=fixed_costs)
self.investment_costs = Expression(expr=investment_costs)
self.period_investment_costs = period_investment_costs
self.costs = Expression(
expr=investment_costs + variable_costs + fixed_costs
)
return self.costs
def _evaluate_remaining_value_difference(
self,
m,
p,
g,
end_year_of_optimization,
lifetime,
interest,
):
"""Evaluate and return the remaining value difference of an investment
The remaining value difference in the net present values if the asset
was to be liquidated at the end of the optimization horizon and the
net present value using the original investment expenses.
Parameters
----------
m : oemof.solph.models.Model
Optimization model
p : int
Period in which investment occurs
g : oemof.solph.components.experimental.SinkDSM
storage unit
end_year_of_optimization : int
Last year of the optimization horizon
lifetime : int
lifetime of investment considered
interest : float
Demanded interest rate for investment
"""
if m.es.use_remaining_value:
if end_year_of_optimization - m.es.periods_years[p] < lifetime:
remaining_lifetime = lifetime - (
end_year_of_optimization - m.es.periods_years[p]
)
remaining_annuity = economics.annuity(
capex=g.investment.ep_costs[-1],
n=remaining_lifetime,
wacc=interest,
)
original_annuity = economics.annuity(
capex=g.investment.ep_costs[p],
n=remaining_lifetime,
wacc=interest,
)
present_value_factor_remaining = 1 / economics.annuity(
capex=1, n=remaining_lifetime, wacc=interest
)
return (
self.invest[g, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
else:
return 0
else:
return 0