# -*- coding: utf-8 -*-
"""Creating sets, variables, constraints and parts of the objective function
for Flow objects with investment but without nonconvex option.
SPDX-FileCopyrightText: Uwe Krien <krien@uni-bremen.de>
SPDX-FileCopyrightText: Simon Hilpert
SPDX-FileCopyrightText: Cord Kaldemeyer
SPDX-FileCopyrightText: Patrik Schönfeldt
SPDX-FileCopyrightText: Birgit Schachler
SPDX-FileCopyrightText: jnnr
SPDX-FileCopyrightText: jmloenneberga
SPDX-FileCopyrightText: Johannes Kochems
SPDX-License-Identifier: MIT
"""
from warnings import warn
import numpy as np
from oemof.tools import debugging
from oemof.tools import economics
from pyomo.core import Binary
from pyomo.core import BuildAction
from pyomo.core import Constraint
from pyomo.core import Expression
from pyomo.core import NonNegativeReals
from pyomo.core import Set
from pyomo.core import Var
from pyomo.core.base.block import ScalarBlock
[docs]class InvestmentFlowBlock(ScalarBlock):
r"""Block for all flows with :attr:`Investment` being not None.
.. automethod:: _create_constraints
.. automethod:: _create_variables
.. automethod:: _create_sets
.. automethod:: _objective_expression
See :class:`oemof.solph.options.Investment` for all parameters of the
*Investment* class.
See :class:`oemof.solph.flows._simple_flow_block.SimpleFlowBlock`
for all parameters of the *SimpleFlowBlock* class.
The overall summed cost expressions for all *InvestmentFlowBlock* objects
can be accessed by
* :attr:`om.InvestmentFlowBlock.investment_costs`,
* :attr:`om.InvestmentFlowBlock.fixed_costs` and
* :attr:`om.InvestmentFlowBlock.costs`.
Their values after optimization can be retrieved by
* :meth:`om.InvestmentFlowBlock.investment_costs`,
* :attr:`om.InvestmentFlowBlock.period_investment_costs` (yielding a dict
keyed by periods); note: this is not a Pyomo expression, but calculated,
* :meth:`om.InvestmentFlowBlock.fixed_costs` and
* :meth:`om.InvestmentFlowBlock.costs`.
Note
----
In case of a nonconvex investment flow (:attr:`nonconvex=True`),
the existing flow capacity :math:`P_{exist}` needs to be zero.
Note
----
See also :class:`~oemof.solph.flows._flow.Flow`,
:class:`~oemof.solph.flows._simple_flow_block.SimpleFlowBlock` and
:class:`~oemof.solph._options.Investment`
""" # noqa: E501
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def _create(self, group=None):
r"""Creates sets, variables and constraints for SimpleFlowBlock
with investment attribute of type class:`.Investment`.
Parameters
----------
group : list
List containing tuples containing flow (f) objects that have an
attribute investment and the associated source (s) and target (t)
of flow e.g. groups=[(s1, t1, f1), (s2, t2, f2),..]
"""
if group is None:
return None
self._create_sets(group)
self._create_variables(group)
self._create_constraints()
[docs] def _create_sets(self, group):
"""
Creates all sets for investment flows.
"""
self.INVESTFLOWS = Set(initialize=[(g[0], g[1]) for g in group])
self.CONVEX_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].investment.nonconvex is False
]
)
self.NON_CONVEX_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].investment.nonconvex is True
]
)
self.FIXED_INVESTFLOWS = Set(
initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is not None]
)
self.NON_FIXED_INVESTFLOWS = Set(
initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is None]
)
self.FULL_LOAD_TIME_MAX_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].full_load_time_max is not None
]
)
self.FULL_LOAD_TIME_MIN_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].full_load_time_min is not None
]
)
self.MIN_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if (g[2].min[0] != 0 or len(g[2].min) > 1)
]
)
self.EXISTING_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].investment.existing is not None
]
)
self.OVERALL_MAXIMUM_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].investment.overall_maximum is not None
]
)
self.OVERALL_MINIMUM_INVESTFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].investment.overall_minimum is not None
]
)
[docs] def _create_variables(self, _):
r"""Creates all variables for investment flows.
All *InvestmentFlowBlock* objects are indexed by a starting and
ending node :math:`(i, o)`, which is omitted in the following
for the sake of convenience. The following variables are created:
* :math:`P(p, t)`
Actual flow value
(created in :class:`oemof.solph.models.BaseModel`),
indexed by tuple of periods p and timestep t
* :math:`P_{invest}(p)`
Value of the investment variable in period p,
equal to what is being invested and equivalent resp. similar to
the nominal value of the flows after optimization.
* :math:`P_{total}(p)`
Total installed capacity / energy in period p,
equivalent to the nominal value of the flows after optimization.
* :math:`P_{old}(p)`
Old capacity / energy to be decommissioned in period p
due to reaching its lifetime; applicable only
for multi-period models.
* :math:`P_{old,exo}(p)`
Old exogenous capacity / energy to be decommissioned in period p
due to reaching its lifetime, i.e. the amount that has
been specified by :attr:`existing` when it is decommisioned;
applicable only for multi-period models.
* :math:`P_{old,end}(p)`
Old endogenous capacity / energy to be decommissioned in period p
due to reaching its lifetime, i.e. the amount that has been
invested in by the model itself that is decommissioned in
a later period because of reaching its lifetime;
applicable only for multi-period models.
* :math:`Y_{invest}(p)`
Binary variable for the status of the investment, if
:attr:`nonconvex` is `True`.
"""
m = self.parent_block()
def _investvar_bound_rule(block, i, o, p):
"""Rule definition for bounds of invest variable."""
if (i, o) in self.CONVEX_INVESTFLOWS:
return (
m.flows[i, o].investment.minimum[p],
m.flows[i, o].investment.maximum[p],
)
elif (i, o) in self.NON_CONVEX_INVESTFLOWS:
return 0, m.flows[i, o].investment.maximum[p]
# create invest variable for an investment flow
self.invest = Var(
self.INVESTFLOWS,
m.PERIODS,
within=NonNegativeReals,
bounds=_investvar_bound_rule,
)
# Total capacity
self.total = Var(self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals)
if m.es.periods is not None:
self.old = Var(
self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
)
# Old endogenous capacity to be decommissioned (due to lifetime)
self.old_end = Var(
self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
)
# Old exogenous capacity to be decommissioned (due to lifetime)
self.old_exo = Var(
self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
)
# create status variable for a non-convex investment flow
self.invest_status = Var(
self.NON_CONVEX_INVESTFLOWS, m.PERIODS, within=Binary
)
[docs] def _create_constraints(self):
r"""Creates all constraints for standard flows.
Depending on the attributes of the *InvestmentFlowBlock*
and *SimpleFlowBlock*, different constraints are created.
The following constraints are created
for all *InvestmentFlowBlock* objects:\
Total capacity / energy
.. math::
&
if \quad p=0:\\
&
P_{total}(p) = P_{invest}(p) + P_{exist}(p) \\
&\\
&
else:\\
&
P_{total}(p) = P_{total}(p-1) + P_{invest}(p) - P_{old}(p) \\
&\\
&
\forall p \in \textrm{PERIODS}
Upper bound for the flow value
.. math::
&
P(p, t) \le ( P_{total}(p) ) \cdot f_{max}(t) \\
&
\forall p, t \in \textrm{TIMEINDEX}
For a multi-period model, the old capacity is defined as follows:
.. math::
&
P_{old}(p) = P_{old,exo}(p) + P_{old,end}(p)\\
&\\
&
if \quad p=0:\\
&
P_{old,end}(p) = 0\\
&\\
&
else \quad if \quad l \leq year(p):\\
&
P_{old,end}(p) = P_{invest}(p_{comm})\\
&\\
&
else:\\
&
P_{old,end}(p) = 0\\
&\\
&
if \quad p=0:\\
&
P_{old,exo}(p) = 0\\
&\\
&
else \quad if \quad l - a \leq year(p):\\
&
P_{old,exo}(p) = P_{exist} (*)\\
&\\
&
else:\\
&
P_{old,exo}(p) = 0\\
&\\
&
\forall p \in \textrm{PERIODS}
where:
* (*) is only performed for the first period the condition
is True. A decommissioning flag is then set to True
to prevent having falsely added old capacity in future periods.
* :math:`year(p)` is the year corresponding to period p
* :math:`p_{comm}` is the commissioning period of the flow
(which is determined by the model itself)
Depending on the attribute :attr:`nonconvex`, the constraints for the
bounds of the decision variable :math:`P_{invest}(p)` are different:\
* :attr:`nonconvex = False`
.. math::
&
P_{invest, min}(p) \le P_{invest}(p) \le P_{invest, max}(p) \\
&
\forall p \in \textrm{PERIODS}
* :attr:`nonconvex = True`
.. math::
&
P_{invest, min}(p) \cdot Y_{invest}(p) \le P_{invest}(p)\\
&
P_{invest}(p) \le P_{invest, max}(p) \cdot Y_{invest}(p)\\
&\\
&
\forall p \in \textrm{PERIODS}
For all *InvestmentFlowBlock* objects
(independent of the attribute :attr:`nonconvex`),
the following additional constraints are created, if the appropriate
attribute of the *SimpleFlowBlock*
(see :class:`oemof.solph.flows._simple_flow_block.SimpleFlowBlock`)
is set:
* :attr:`fix` is not None
Actual value constraint for investments with fixed flow values
.. math::
&
P(p, t) = P_{total}(p) \cdot f_{fix}(t) \\
&\\
&
\forall p, t \in \textrm{TIMEINDEX}
* :attr:`min != 0`
Lower bound for the flow values
.. math::
&
P(p, t) \geq P_{total}(p) \cdot f_{min}(t) \\
&\\
&
\forall p, t \in \textrm{TIMEINDEX}
* :attr:`full_load_time_max is not None`
Upper bound for the sum of all flow values
(e.g. maximum full load hours)
.. math::
\sum_{p, t} P(p, t) \cdot \tau(t) \leq P_{total}(p)
\cdot t_{full\_load, min}
* :attr:`full_load_time_min is not None`
Lower bound for the sum of all flow values
(e.g. minimum full load hours)
.. math::
\sum_{p, t} P(t) \cdot \tau(t) \geq P_{total}
\cdot t_{full\_load, min}
* :attr:`overall_maximum` is not None
(for multi-period model only)
Overall maximum of total installed capacity / energy for flow
.. math::
&
P_{total}(p) \leq P_{overall,max} \\
&\\
&
\forall p \in \textrm{PERIODS}
* :attr:`overall_minimum` is not None
(for multi-period model only)
Overall minimum of total installed capacity / energy for flow;
applicable only in last period
.. math::
P_{total}(p_{last}) \geq P_{overall,min}
"""
m = self.parent_block()
self.minimum_rule = self._minimum_investment_constraint()
self.maximum_rule = self._maximum_investment_constraint()
# Handle unit lifetimes
def _total_capacity_rule(block):
"""Rule definition for determining total installed
capacity (taking decommissioning into account)
"""
for i, o in self.INVESTFLOWS:
for p in m.PERIODS:
if p == 0:
expr = (
self.total[i, o, p]
== self.invest[i, o, p]
+ m.flows[i, o].investment.existing
)
self.total_rule.add((i, o, p), expr)
# applicable for multi-period model only
else:
expr = (
self.total[i, o, p]
== self.invest[i, o, p]
+ self.total[i, o, p - 1]
- self.old[i, o, p]
)
self.total_rule.add((i, o, p), expr)
self.total_rule = Constraint(
self.INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.total_rule_build = BuildAction(rule=_total_capacity_rule)
if m.es.periods is not None:
def _old_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 i, o in self.INVESTFLOWS:
lifetime = m.flows[i, o].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 {(i, o)} "
"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[i, o, 0] == 0
self.old_rule_end.add((i, o, 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[i, o, p] == 0
self.old_rule_end.add((i, o, p), expr)
# multiple invests can be decommissioned in the same period
# but only sequential ones, thus a bookkeeping is
# introduced and constraints 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[i, o, last_decomm_p] == expr
self.old_rule_end.add((i, o, 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[i, o, 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[i, o, 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[i, o, last_decomm_p] == expr
self.old_rule_end.add((i, o, last_decomm_p), expr)
self.old_rule_end = Constraint(
self.INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.old_rule_end_build = BuildAction(rule=_old_capacity_rule_end)
def _old_capacity_rule_exo(block):
"""Rule definition for determining old exogenously given
capacity to be decommissioned due to reaching its lifetime
"""
for i, o in self.INVESTFLOWS:
age = m.flows[i, o].investment.age
lifetime = m.flows[i, o].investment.lifetime
is_decommissioned = False
for p in m.PERIODS:
# No shutdown in first period
if p == 0:
expr = self.old_exo[i, o, p] == 0
self.old_rule_exo.add((i, o, p), expr)
elif lifetime - age <= m.es.periods_years[p]:
# Track decommissioning status
if not is_decommissioned:
expr = (
self.old_exo[i, o, p]
== m.flows[i, o].investment.existing
)
is_decommissioned = True
else:
expr = self.old_exo[i, o, p] == 0
self.old_rule_exo.add((i, o, p), expr)
else:
expr = self.old_exo[i, o, p] == 0
self.old_rule_exo.add((i, o, p), expr)
self.old_rule_exo = Constraint(
self.INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.old_rule_exo_build = BuildAction(rule=_old_capacity_rule_exo)
def _old_capacity_rule(block):
"""Rule definition for determining (overall) old capacity
to be decommissioned due to reaching its lifetime
"""
for i, o in self.INVESTFLOWS:
for p in m.PERIODS:
expr = (
self.old[i, o, p]
== self.old_end[i, o, p] + self.old_exo[i, o, p]
)
self.old_rule.add((i, o, p), expr)
self.old_rule = Constraint(
self.INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.old_rule_build = BuildAction(rule=_old_capacity_rule)
def _investflow_fixed_rule(block):
"""Rule definition of constraint to fix flow variable
of investment flow to (normed) actual value
"""
for i, o in self.FIXED_INVESTFLOWS:
for p, t in m.TIMEINDEX:
expr = (
m.flow[i, o, p, t]
== self.total[i, o, p] * m.flows[i, o].fix[t]
)
self.fixed.add((i, o, p, t), expr)
self.fixed = Constraint(
self.FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
)
self.fixed_build = BuildAction(rule=_investflow_fixed_rule)
def _max_investflow_rule(block):
"""Rule definition of constraint setting an upper bound of flow
variable in investment case.
"""
for i, o in self.NON_FIXED_INVESTFLOWS:
for p, t in m.TIMEINDEX:
expr = (
m.flow[i, o, p, t]
<= self.total[i, o, p] * m.flows[i, o].max[t]
)
self.max.add((i, o, p, t), expr)
self.max = Constraint(
self.NON_FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
)
self.max_build = BuildAction(rule=_max_investflow_rule)
def _min_investflow_rule(block):
"""Rule definition of constraint setting a lower bound on flow
variable in investment case.
"""
for i, o in self.MIN_INVESTFLOWS:
for p, t in m.TIMEINDEX:
expr = (
m.flow[i, o, p, t]
>= self.total[i, o, p] * m.flows[i, o].min[t]
)
self.min.add((i, o, p, t), expr)
self.min = Constraint(
self.MIN_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
)
self.min_build = BuildAction(rule=_min_investflow_rule)
def _full_load_time_max_investflow_rule(_, i, o):
"""Rule definition for build action of max. sum flow constraint
in investment case.
"""
expr = sum(
m.flow[i, o, p, t] * m.timeincrement[t] for p, t in m.TIMEINDEX
) <= (
m.flows[i, o].full_load_time_max
* sum(self.total[i, o, p] for p in m.PERIODS)
)
return expr
self.full_load_time_max = Constraint(
self.FULL_LOAD_TIME_MAX_INVESTFLOWS,
rule=_full_load_time_max_investflow_rule,
)
def _full_load_time_min_investflow_rule(_, i, o):
"""Rule definition for build action of min. sum flow constraint
in investment case.
"""
expr = sum(
m.flow[i, o, p, t] * m.timeincrement[t] for p, t in m.TIMEINDEX
) >= (
sum(self.total[i, o, p] for p in m.PERIODS)
* m.flows[i, o].full_load_time_min
)
return expr
self.full_load_time_min = Constraint(
self.FULL_LOAD_TIME_MIN_INVESTFLOWS,
rule=_full_load_time_min_investflow_rule,
)
if m.es.periods is not None:
def _overall_maximum_investflow_rule(block):
"""Rule definition for maximum overall investment
in investment case.
"""
for i, o in self.OVERALL_MAXIMUM_INVESTFLOWS:
for p in m.PERIODS:
expr = (
self.total[i, o, p]
<= m.flows[i, o].investment.overall_maximum
)
self.overall_maximum.add((i, o, p), expr)
self.overall_maximum = Constraint(
self.OVERALL_MAXIMUM_INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.overall_maximum_build = BuildAction(
rule=_overall_maximum_investflow_rule
)
def _overall_minimum_investflow_rule(block, i, o):
"""Rule definition for minimum overall investment
in investment case.
Note: This is only applicable for the last period
"""
expr = (
m.flows[i, o].investment.overall_minimum
<= self.total[i, o, m.PERIODS[-1]]
)
return expr
self.overall_minimum = Constraint(
self.OVERALL_MINIMUM_INVESTFLOWS,
rule=_overall_minimum_investflow_rule,
)
[docs] def _objective_expression(self):
r"""Objective expression for flows with investment attribute of type
class:`.Investment`. The returned costs are fixed and
investment costs. Variable costs are added from the standard flow
objective expression.
Objective terms for a standard model and a multi-period model differ
quite strongly. Besides, the part of the objective function added by
the *InvestmentFlowBlock* also depends on whether a convex
or nonconvex *InvestmentFlowBlock* is selected.
The following parts of the objective function are created:
*Standard model*
* :attr:`nonconvex = False`
.. math::
P_{invest}(0) \cdot c_{invest,var}(0)
* :attr:`nonconvex = True`
.. math::
P_{invest}(0) \cdot c_{invest,var}(0)
+ c_{invest,fix}(0) \cdot Y_{invest}(0) \\
Where 0 denotes the 0th (investment) period since
in a standard model, there is only this one period.
*Multi-period model*
* :attr:`nonconvex = False`
.. math::
&
P_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
\cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p}\\
&\\
&
\forall p \in \textrm{PERIODS}
In case, the remaining lifetime of an asset 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:`nonconvex = True`
.. math::
&
(P_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
\cdot \frac {1}{ANF(d, ir)}\\
&
+ c_{invest,fix}(p) \cdot b_{invest}(p)) \cdot DF^{-p}\\
&\\
&
\forall p \in \textrm{PERIODS}
In case, the remaining lifetime of an asset 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|}\\
&
+ (c_{invest,fix}(p) - c_{invest,fix}(|P|))
\cdot b_{invest}(p)) \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 \textrm{PERIODS}
* :attr:`fixed_costs` not None for existing capacity
.. math::
\sum_{pp=0}^{limit_{exo}} P_{exist} \cdot c_{fixed}(pp)
\cdot DF^{-pp}
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:`l_{r}` is the remaining lifetime at the end of the
optimization horizon (in case it is greater than 0 and
smaller than the actual lifetime).
* :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:`limit_{exo}=min\{year_{max}, l - a\}` is used as an
upper bound to ensure fixed costs for existing capacities to occur
within the optimization horizon. :math:`a` is the initial age
of an asset.
* :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+ir)^d \cdot ir} {(1+ir)^d - 1}
They are derived using the reciprocal of the oemof.tools.economics
annuity function with a capex of 1.
The interest rate :math:`ir` for the annuity is defined as weighted
average costs of capital (wacc) and assumed constant over time.
"""
if not hasattr(self, "INVESTFLOWS"):
return 0
m = self.parent_block()
investment_costs = 0
period_investment_costs = {p: 0 for p in m.PERIODS}
fixed_costs = 0
if m.es.periods is None:
for i, o in self.CONVEX_INVESTFLOWS:
for p in m.PERIODS:
investment_costs += (
self.invest[i, o, p]
* m.flows[i, o].investment.ep_costs[p]
)
for i, o in self.NON_CONVEX_INVESTFLOWS:
for p in m.PERIODS:
investment_costs += (
self.invest[i, o, p]
* m.flows[i, o].investment.ep_costs[p]
+ self.invest_status[i, o, p]
* m.flows[i, o].investment.offset[p]
)
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 i, o in self.CONVEX_INVESTFLOWS:
lifetime = m.flows[i, o].investment.lifetime
interest = m.flows[i, o].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=m.flows[i, o].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_remaining = 1 / economics.annuity(
capex=1, n=duration, wacc=interest
)
investment_costs_increment = (
self.invest[i, o, p]
* annuity
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
i,
o,
m.es.end_year_of_optimization,
lifetime,
interest,
)
)
investment_costs += (
investment_costs_increment + remaining_value_difference
)
period_investment_costs[p] += investment_costs_increment
for i, o in self.NON_CONVEX_INVESTFLOWS:
lifetime = m.flows[i, o].investment.lifetime
interest = m.flows[i, o].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=m.flows[i, o].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_remaining = 1 / economics.annuity(
capex=1, n=duration, wacc=interest
)
investment_costs_increment = (
self.invest[i, o, p]
* annuity
* present_value_factor_remaining
+ self.invest_status[i, o, p]
* m.flows[i, o].investment.offset[p]
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
i,
o,
m.es.end_year_of_optimization,
lifetime,
interest,
nonconvex=True,
)
)
investment_costs += (
investment_costs_increment + remaining_value_difference
)
period_investment_costs[p] += investment_costs_increment
for i, o in self.INVESTFLOWS:
if m.flows[i, o].investment.fixed_costs[0] is not None:
lifetime = m.flows[i, o].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[i, o, p]
* m.flows[i, o].investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(m.es.periods_years[p], range_limit)
)
for i, o in self.EXISTING_INVESTFLOWS:
if m.flows[i, o].investment.fixed_costs[0] is not None:
lifetime = m.flows[i, o].investment.lifetime
age = m.flows[i, o].investment.age
range_limit = min(
m.es.end_year_of_optimization, lifetime - age
)
fixed_costs += sum(
m.flows[i, o].investment.existing
* m.flows[i, o].investment.fixed_costs[pp]
* (1 + m.discount_rate) ** (-pp)
for pp in range(range_limit)
)
self.investment_costs = Expression(expr=investment_costs)
self.period_investment_costs = period_investment_costs
self.fixed_costs = Expression(expr=fixed_costs)
self.costs = Expression(expr=investment_costs + fixed_costs)
return self.costs
def _evaluate_remaining_value_difference(
self,
m,
p,
i,
o,
end_year_of_optimization,
lifetime,
interest,
nonconvex=False,
):
"""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
i : any instance of oemof.solph.components
start node of flow
o : any instance of oemof.solph.components
end node of flow
end_year_of_optimization : int
Last year of the optimization horizon
lifetime : int
lifetime of investment considered
interest : float
Demanded interest rate for investment
nonconvex : bool
Indicating whether considered flow is nonconvex.
"""
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=m.flows[i, o].investment.ep_costs[-1],
n=remaining_lifetime,
wacc=interest,
)
original_annuity = economics.annuity(
capex=m.flows[i, o].investment.ep_costs[p],
n=remaining_lifetime,
wacc=interest,
)
present_value_factor_remaining = 1 / economics.annuity(
capex=1, n=remaining_lifetime, wacc=interest
)
convex_investment_costs = (
self.invest[i, o, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
if nonconvex:
return convex_investment_costs + self.invest_status[
i, o, p
] * (
m.flows[i, o].investment.offset[-1]
- m.flows[i, o].investment.offset[p]
) * (
1 + m.discount_rate
) ** (
-end_year_of_optimization
)
else:
return convex_investment_costs
else:
return 0
else:
return 0
def _minimum_investment_constraint(self):
"""Constraint factory for a minimum investment"""
m = self.parent_block()
def _min_invest_rule(_):
"""Rule definition for applying a minimum investment"""
for i, o in self.NON_CONVEX_INVESTFLOWS:
for p in m.PERIODS:
expr = (
m.flows[i, o].investment.minimum[p]
* self.invest_status[i, o, p]
<= self.invest[i, o, p]
)
self.minimum_rule.add((i, o, p), expr)
self.minimum_rule = Constraint(
self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.minimum_rule_build = BuildAction(rule=_min_invest_rule)
return self.minimum_rule
def _maximum_investment_constraint(self):
"""Constraint factory for a maximum investment"""
m = self.parent_block()
def _max_invest_rule(_):
"""Rule definition for applying a minimum investment"""
for i, o in self.NON_CONVEX_INVESTFLOWS:
for p in m.PERIODS:
expr = self.invest[i, o, p] <= (
m.flows[i, o].investment.maximum[p]
* self.invest_status[i, o, p]
)
self.maximum_rule.add((i, o, p), expr)
self.maximum_rule = Constraint(
self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
)
self.maximum_rule_build = BuildAction(rule=_max_invest_rule)
return self.maximum_rule