Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for fixing investments #1007

Open
wants to merge 47 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b001fe6
Introduce draft for fixing investments (yet untested)
jokochems Oct 31, 2023
63e449f
Reformat using black
jokochems Oct 31, 2023
ebe0fa2
Revise architecture and fix erroneous indexing
jokochems Nov 1, 2023
9ae659f
Add / refactor test(s)
jokochems Nov 1, 2023
670a534
Add fixes
jokochems Nov 1, 2023
d66e556
Fix black issues
jokochems Nov 1, 2023
11a7fdb
Add further tests
jokochems Nov 1, 2023
a0a8ea3
Add lp files
jokochems Nov 1, 2023
bdc49ae
Add minor black fix
jokochems Nov 1, 2023
d67d4cf
Fix import
jokochems Nov 1, 2023
2bfa151
Add test for newly introduced error message
jokochems Nov 1, 2023
93752f4
Rename to address line length issue
jokochems Nov 1, 2023
74ffe1c
Satisfy our picky CI
jokochems Nov 1, 2023
3dfeb76
Alter import order
jokochems Nov 1, 2023
427d461
Fix imports once more
jokochems Nov 1, 2023
ce68649
Allow for fixing investments in GenericStorage and SinkDSM
jokochems Nov 1, 2023
fac2b97
Extend test to all DSM modelling approaches
jokochems Nov 1, 2023
2c9766a
Extend docs and changelog
jokochems Nov 1, 2023
536dd91
Add constraint test for fixed storage investment
jokochems Nov 1, 2023
95f53c4
Add fix
jokochems Nov 1, 2023
944a049
Add tests for dsm units
jokochems Nov 1, 2023
21e766f
Merge branch 'dev' into features/fix-investment-results-in-repeated-s…
jokochems Dec 8, 2023
1c5b64a
Introduce draft for fixing investments (yet untested)
jokochems Oct 31, 2023
9953714
Reformat using black
jokochems Oct 31, 2023
3d9d334
Revise architecture and fix erroneous indexing
jokochems Nov 1, 2023
011471b
Add / refactor test(s)
jokochems Nov 1, 2023
8893b11
Add fixes
jokochems Nov 1, 2023
d3c78aa
Fix black issues
jokochems Nov 1, 2023
4d8a002
Add further tests
jokochems Nov 1, 2023
4401b9a
Add lp files
jokochems Nov 1, 2023
cb6b9f8
Add minor black fix
jokochems Nov 1, 2023
817d092
Fix import
jokochems Nov 1, 2023
b8b1f2e
Add test for newly introduced error message
jokochems Nov 1, 2023
296934d
Rename to address line length issue
jokochems Nov 1, 2023
71808ce
Satisfy our picky CI
jokochems Nov 1, 2023
4a6a5cf
Alter import order
jokochems Nov 1, 2023
19a98b2
Fix imports once more
jokochems Nov 1, 2023
a1f9692
Allow for fixing investments in GenericStorage and SinkDSM
jokochems Nov 1, 2023
d412725
Extend test to all DSM modelling approaches
jokochems Nov 1, 2023
a2add52
Extend docs and changelog
jokochems Nov 1, 2023
f611d86
Add constraint test for fixed storage investment
jokochems Nov 1, 2023
759a219
Add fix
jokochems Nov 1, 2023
72980a9
Add tests for dsm units
jokochems Nov 1, 2023
10a6f70
Merge remote-tracking branch 'upstream/features/fix-investment-result…
jokochems Dec 8, 2023
5b535fd
Allow for rounding in repeated solving
jokochems Dec 8, 2023
ba35e54
Add / fix tests
jokochems Dec 8, 2023
91669ff
Alter import order
jokochems Dec 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,33 @@ Besides the `invest` variable, new variables are introduced as well. These are:
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.

Repeated solving with fixed investment results
----------------------------------------------

You can rerun a given optimization model with fixed investments.
Let's assume you have set up a model instance called ``model``
containing investments and solved it by calling:

.. code-block:: python

model = solph.Model(energysystem)
model.solve()

You can now fix the investment results from the previous solving and
only solve the dispatch-related part via a call of the method
``fix_investments`` as well as a second solving of your model:

.. code-block:: python

model.fix_investments()
model.solve()

This will now take the investment variables as parameters. Thus, their
values from the previous solving are parameters in the second solving. Only
the dispatch-related variables will be optimized. This two-stage approach
for instance may allow you to interpret dual values of balancing constraints
which you cannot sensibly interpret in the presence of investment variables.

Modelling cellular energy systems and modularizing energy system models
-----------------------------------------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions docs/whatsnew/v0-5-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ API changes
###########

* New bool attribute `use_remaining_value` of `oemof.solph.EnergySystem`
* New method `fix_investments` of `oemof.solph.Model`

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

* Allow for evaluating differences in the remaining vs. the original value
for multi-period investments.
* Allow to define minimum up- and down-time per time step
* Allow for fixing investment variables to the values of a previous
optimization run, enabling a repeated solve with fixed investments.

Documentation
#############
Expand Down
33 changes: 33 additions & 0 deletions src/oemof/solph/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ class Model(BaseModel):
discount_rate : float or None
The rate used for discounting in a multi-period model.
A 2% discount rate needs to be defined as 0.02.
rounding_precision : int or None
Number of decimal digits to apply for rounding when fixing variables
in a repeated model solve.

Note
----
Expand Down Expand Up @@ -383,6 +386,8 @@ def __init__(self, energysystem, discount_rate=None, **kwargs):
self._set_discount_rate_with_warning()
else:
pass
self._fix_investments = False
self.rounding_precision = None
super().__init__(energysystem, **kwargs)

def _set_discount_rate_with_warning(self):
Expand Down Expand Up @@ -520,3 +525,31 @@ def _add_parent_block_variables(self):
if (o, i) in self.UNIDIRECTIONAL_FLOWS:
for p, t in self.TIMEINDEX:
self.flow[o, i, p, t].setlb(0)

def fix_investments(self, rounding_precision=None):
"""Fix investment results of an already solved model

Parameters
----------
rounding_precision : int or None
If not None, round investments to given number of decimal digits
"""
if self.solver_results is None:
msg = (
"Cannot fix investments as model has not yet been solved!\n"
"You have to first solve your model and then call method "
"`fix_investments()` on your model instance."
)
raise ValueError(msg)
self._fix_investments = True
self.rounding_precision = rounding_precision
if hasattr(self, "InvestmentFlowBlock"):
self.InvestmentFlowBlock.fix_investments_results()
if hasattr(self, "GenericInvestmentStorageBlock"):
self.GenericInvestmentStorageBlock.fix_investments_results()
if hasattr(self, "SinkDSMOemofInvestmentBlock"):
self.SinkDSMOemofInvestmentBlock.fix_investments_results()
if hasattr(self, "SinkDSMDIWInvestmentBlock"):
self.SinkDSMDIWInvestmentBlock.fix_investments_results()
if hasattr(self, "SinkDSMDLRInvestmentBlock"):
self.SinkDSMDLRInvestmentBlock.fix_investments_results()
64 changes: 64 additions & 0 deletions src/oemof/solph/components/_generic_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from pyomo.environ import NonNegativeReals
from pyomo.environ import Set
from pyomo.environ import Var
from pyomo.environ import value

from oemof.solph._helpers import check_node_object_for_missing_attribute
from oemof.solph._options import Investment
Expand Down Expand Up @@ -1968,3 +1969,66 @@ def _evaluate_remaining_value_difference(
return 0
else:
return 0

def fix_investments_results(self):
"""Fix investments if `_fix_investments` is set to True for model"""
m = self.parent_block()
for n in self.INVESTSTORAGES:
for p in m.PERIODS:
if m.rounding_precision is not None:
self.invest[n, p] = self._round_and_ensure_bounds(
self.invest[n, p],
lb=n.investment.minimum[p],
ub=n.investment.maximum[p],
)
ub_total = (
n.investment.overall_maximum
if n.investment.overall_maximum is not None
else float("+inf")
)
self.total[n, p] = self._round_and_ensure_bounds(
self.total[n, p],
ub=ub_total,
)
self.invest[n, p].fix()
self.total[n, p].fix()
if m.es.periods is not None:
if m.rounding_precision is not None:
self.old[n, p] = self._round_and_ensure_bounds(
self.old[n, p],
)
self.old_end[n, p] = self._round_and_ensure_bounds(
self.old_end[n, p],
)
self.old_exo[n, p] = self._round_and_ensure_bounds(
self.old_exo[n, p],
)
self.old[n, p].fix()
self.old_end[n, p].fix()
self.old_exo[n, p].fix()

def _round_and_ensure_bounds(self, var, lb=0, ub=float("+inf")):
"""Round given investments and ensure within bounds

Parameters
----------
var : pyomo.core.IndexedVar
variable to round

lb : float
Lower bound to be ensured

ub : float
Upper bound to be ensured
"""
m = self.parent_block()
return min(
ub,
max(
lb,
round(
value(var),
m.rounding_precision,
),
),
)
190 changes: 190 additions & 0 deletions src/oemof/solph/components/experimental/_sink_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from pyomo.environ import NonNegativeReals
from pyomo.environ import Set
from pyomo.environ import Var
from pyomo.environ import value

from oemof.solph._options import Investment
from oemof.solph._plumbing import sequence
Expand Down Expand Up @@ -1536,6 +1537,69 @@ def _evaluate_remaining_value_difference(
else:
return 0

def fix_investments_results(self):
"""Fix investments if `_fix_investments` is set to True for model"""
m = self.parent_block()
for g in self.investdsm:
for p in m.PERIODS:
if m.rounding_precision is not None:
self.invest[g, p] = self._round_and_ensure_bounds(
self.invest[g, p],
lb=g.investment.minimum[p],
ub=g.investment.maximum[p],
)
ub_total = (
g.investment.overall_maximum
if g.investment.overall_maximum is not None
else float("+inf")
)
self.total[g, p] = self._round_and_ensure_bounds(
self.total[g, p],
ub=ub_total,
)
self.invest[g, p].fix()
self.total[g, p].fix()
if m.es.periods is not None:
if m.rounding_precision is not None:
self.old[g, p] = self._round_and_ensure_bounds(
self.old[g, p],
)
self.old_end[g, p] = self._round_and_ensure_bounds(
self.old_end[g, p],
)
self.old_exo[g, p] = self._round_and_ensure_bounds(
self.old_exo[g, p],
)
self.old[g, p].fix()
self.old_end[g, p].fix()
self.old_exo[g, p].fix()

def _round_and_ensure_bounds(self, var, lb=0, ub=float("+inf")):
"""Round given investments and ensure within bounds

Parameters
----------
var : pyomo.core.IndexedVar
variable to round

lb : float
Lower bound to be ensured

ub : float
Upper bound to be ensured
"""
m = self.parent_block()
return min(
ub,
max(
lb,
round(
value(var),
m.rounding_precision,
),
),
)


class SinkDSMDIWBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DIW" approach
Expand Down Expand Up @@ -3390,6 +3454,69 @@ def _evaluate_remaining_value_difference(
else:
return 0

def fix_investments_results(self):
"""Fix investments if `_fix_investments` is set to True for model"""
m = self.parent_block()
for g in self.investdsm:
for p in m.PERIODS:
if m.rounding_precision is not None:
self.invest[g, p] = self._round_and_ensure_bounds(
self.invest[g, p],
lb=g.investment.minimum[p],
ub=g.investment.maximum[p],
)
ub_total = (
g.investment.overall_maximum
if g.investment.overall_maximum is not None
else float("+inf")
)
self.total[g, p] = self._round_and_ensure_bounds(
self.total[g, p],
ub=ub_total,
)
self.invest[g, p].fix()
self.total[g, p].fix()
if m.es.periods is not None:
if m.rounding_precision is not None:
self.old[g, p] = self._round_and_ensure_bounds(
self.old[g, p],
)
self.old_end[g, p] = self._round_and_ensure_bounds(
self.old_end[g, p],
)
self.old_exo[g, p] = self._round_and_ensure_bounds(
self.old_exo[g, p],
)
self.old[g, p].fix()
self.old_end[g, p].fix()
self.old_exo[g, p].fix()

def _round_and_ensure_bounds(self, var, lb=0, ub=float("+inf")):
"""Round given investments and ensure within bounds

Parameters
----------
var : pyomo.core.IndexedVar
variable to round

lb : float
Lower bound to be ensured

ub : float
Upper bound to be ensured
"""
m = self.parent_block()
return min(
ub,
max(
lb,
round(
value(var),
m.rounding_precision,
),
),
)


class SinkDSMDLRBlock(ScalarBlock):
r"""Constraints for SinkDSM with "DLR" approach
Expand Down Expand Up @@ -5888,3 +6015,66 @@ def _evaluate_remaining_value_difference(
return 0
else:
return 0

def fix_investments_results(self):
"""Fix investments if `_fix_investments` is set to True for model"""
m = self.parent_block()
for g in self.INVESTDR:
for p in m.PERIODS:
if m.rounding_precision is not None:
self.invest[g, p] = self._round_and_ensure_bounds(
self.invest[g, p],
lb=g.investment.minimum[p],
ub=g.investment.maximum[p],
)
ub_total = (
g.investment.overall_maximum
if g.investment.overall_maximum is not None
else float("+inf")
)
self.total[g, p] = self._round_and_ensure_bounds(
self.total[g, p],
ub=ub_total,
)
self.invest[g, p].fix()
self.total[g, p].fix()
if m.es.periods is not None:
if m.rounding_precision is not None:
self.old[g, p] = self._round_and_ensure_bounds(
self.old[g, p],
)
self.old_end[g, p] = self._round_and_ensure_bounds(
self.old_end[g, p],
)
self.old_exo[g, p] = self._round_and_ensure_bounds(
self.old_exo[g, p],
)
self.old[g, p].fix()
self.old_end[g, p].fix()
self.old_exo[g, p].fix()

def _round_and_ensure_bounds(self, var, lb=0, ub=float("+inf")):
"""Round given investments and ensure within bounds

Parameters
----------
var : pyomo.core.IndexedVar
variable to round

lb : float
Lower bound to be ensured

ub : float
Upper bound to be ensured
"""
m = self.parent_block()
return min(
ub,
max(
lb,
round(
value(var),
m.rounding_precision,
),
),
)
Loading
Loading