Skip to content

Commit

Permalink
Merge pull request #997 from oemof/feature/remaining-value
Browse files Browse the repository at this point in the history
Compare differences in remaining value vs. actual value
  • Loading branch information
jokochems committed Oct 13, 2023
2 parents 2b7ab2c + 22b4519 commit 088a35f
Show file tree
Hide file tree
Showing 12 changed files with 3,121 additions and 15 deletions.
5 changes: 5 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,11 @@ Besides the `invest` variable, new variables are introduced as well. These are:
has not yet been tested.
* For now, both, the `timeindex` as well as the `timeincrement` of an energy system have to be defined since they
have to be of the same length for a multi-period model.
* You can choose whether or not to re-evaluate assets at the end of the optimization horizon. If you set attribute
`use_remaining_value` of the energy system to True (defaults to False), this leads to the model evaluating the
different in value at the end of the optimization horizon vs. at the time the investment was made. The difference
in value is added to or subtracted from the respective investment costs increment, assuming assets are to be
liquidated / re-evaluated at the end of the optimization horizon.
* Also please be aware, that periods correspond to years by default. You could also choose
monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also,
this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months.
Expand Down
5 changes: 5 additions & 0 deletions docs/whatsnew/v0-5-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ v0.5.2 (????)
API changes
###########

* New bool attribute `use_remaining_value` of `oemof.solph.EnergySystem`

New features
############

* Allow for evaluating differences in the remaining vs. the original value
for multi-period investments.

Documentation
#############

Expand Down
11 changes: 8 additions & 3 deletions src/oemof/solph/_energy_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class EnergySystem(es.EnergySystem):
For a standard model, periods are not (to be) declared, i.e. None.
A list with one entry is derived, i.e. [0].
use_remaining_value : bool
If True, compare the remaining value of an investment to the
original value (only applicable for multi-period models)
kwargs
"""

Expand All @@ -71,6 +75,7 @@ def __init__(
timeincrement=None,
infer_last_interval=None,
periods=None,
use_remaining_value=False,
**kwargs,
):
# Doing imports at runtime is generally frowned upon, but should work
Expand Down Expand Up @@ -160,7 +165,8 @@ def __init__(
timeindex=timeindex, timeincrement=timeincrement, **kwargs
)

if periods is not None:
self.periods = periods
if self.periods is not None:
msg = (
"CAUTION! You specified the 'periods' attribute for your "
"energy system.\n This will lead to creating "
Expand All @@ -171,11 +177,10 @@ def __init__(
"please report them."
)
warnings.warn(msg, debugging.SuspiciousUsageWarning)
self.periods = periods
if self.periods is not None:
self._extract_periods_years()
self._extract_periods_matrix()
self._extract_end_year_of_optimization()
self.use_remaining_value = use_remaining_value

def _extract_periods_years(self):
"""Map years in optimization to respective period based on time indices
Expand Down
142 changes: 139 additions & 3 deletions src/oemof/solph/components/_generic_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,21 @@ class GenericInvestmentStorageBlock(ScalarBlock):
&
\forall p \in \textrm{PERIODS}
In case, the remaining lifetime of a storage 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::
&
E_{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::
Expand All @@ -913,6 +928,24 @@ class GenericInvestmentStorageBlock(ScalarBlock):
&
\forall p \in \textrm{PERIODS}
In case, the remaining lifetime of a storage 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::
&
(E_{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::
Expand All @@ -929,12 +962,14 @@ class GenericInvestmentStorageBlock(ScalarBlock):
\sum_{pp=0}^{limit_{exo}} E_{exist} \cdot c_{fixed}(pp)
\cdot DF^{-pp}
whereby:
* :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
Expand Down Expand Up @@ -1765,7 +1800,19 @@ def _objective_expression(self):
investment_costs_increment = (
self.invest[n, p] * annuity * present_value_factor
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
investment_costs += investment_costs_increment
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
n,
m.es.end_year_of_optimization,
lifetime,
interest,
)
)
investment_costs += (
investment_costs_increment + remaining_value_difference
)
period_investment_costs[p] += investment_costs_increment

for n in self.NON_CONVEX_INVESTSTORAGES:
Expand Down Expand Up @@ -1794,7 +1841,20 @@ def _objective_expression(self):
self.invest[n, p] * annuity * present_value_factor
+ self.invest_status[n, p] * n.investment.offset[p]
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
investment_costs += investment_costs_increment
remaining_value_difference = (
self._evaluate_remaining_value_difference(
m,
p,
n,
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 n in self.INVESTSTORAGES:
Expand Down Expand Up @@ -1835,3 +1895,79 @@ def _objective_expression(self):
self.costs = Expression(expr=investment_costs + fixed_costs)

return self.costs

def _evaluate_remaining_value_difference(
self,
m,
p,
n,
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
n : oemof.solph.components.GenericStorage
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
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=n.investment.ep_costs[-1],
n=remaining_lifetime,
wacc=interest,
)
original_annuity = economics.annuity(
capex=n.investment.ep_costs[p],
n=remaining_lifetime,
wacc=interest,
)
present_value_factor_remaining = 1 / economics.annuity(
capex=1, n=remaining_lifetime, wacc=interest
)
if nonconvex:
return (
self.invest[n, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
+ self.invest_status[n, p]
* (n.investment.offset[-1] - n.investment.offset[p])
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
else:
return (
self.invest[n, p]
* (remaining_annuity - original_annuity)
* present_value_factor_remaining
) * (1 + m.discount_rate) ** (-end_year_of_optimization)
else:
return 0
else:
return 0

0 comments on commit 088a35f

Please sign in to comment.