Skip to content

Commit

Permalink
Merge branch 'dev' into features/fix-investment-results-in-repeated-s…
Browse files Browse the repository at this point in the history
…olving
  • Loading branch information
jokochems authored Dec 8, 2023
2 parents 944a049 + b6a64b9 commit 21e766f
Show file tree
Hide file tree
Showing 37 changed files with 576 additions and 620 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools setuptools_scm twine wheel
python -m pip install --upgrade pip setuptools setuptools_scm twine wheel build
- name: Create packages
run: python setup.py sdist bdist_wheel
run: python -m build .
- name: Run twine check
run: twine check dist/*
- uses: actions/upload-artifact@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tox_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools wheel
python -m pip install -U setuptools wheel build
python -m pip install -U tox
- name: Run ${{ matrix.toxenv }}
Expand Down
4 changes: 4 additions & 0 deletions docs/whatsnew/v0-5-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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.

Expand All @@ -25,6 +26,9 @@ Bug fixes
Limit to costs that occur within the optimization horizon to prevent a
bias towards investments happening earlier in the optimization horizon.
* Fix bugs in multi-period documentation.
* Fix y intersect of OffsetConverter
* Fix minimum uptime being relevant for initial downtime (and vice versa).
* Fix duplicated discounting of fixed costs for multi-period investment

Testing
#######
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def main():
invest_relation_output_capacity=1 / 6,
inflow_conversion_factor=1,
outflow_conversion_factor=0.8,
nominal_value=solph.Investment(ep_costs=epc_storage),
nominal_storage_capacity=solph.Investment(ep_costs=epc_storage),
)

energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def main():
invest_relation_output_capacity=1 / 6,
inflow_conversion_factor=1,
outflow_conversion_factor=0.8,
nominal_value=solph.Investment(ep_costs=epc_storage),
nominal_storage_capacity=solph.Investment(ep_costs=epc_storage),
)

energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def main():
invest_relation_output_capacity=1 / 6,
inflow_conversion_factor=1,
outflow_conversion_factor=0.8,
nominal_value=solph.Investment(ep_costs=epc_storage),
nominal_storage_capacity=solph.Investment(ep_costs=epc_storage),
)

energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def main():
invest_relation_output_capacity=1 / 6,
inflow_conversion_factor=1,
outflow_conversion_factor=0.8,
investment=solph.Investment(ep_costs=epc_storage),
nominal_storage_capacity=solph.Investment(ep_costs=epc_storage),
)

energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage)
Expand Down
5 changes: 2 additions & 3 deletions src/oemof/solph/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,12 +403,11 @@ def _add_parent_block_sets(self):
"""Add all basic sets to the model, i.e. NODES, TIMESTEPS and FLOWS.
Also create sets PERIODS and TIMEINDEX used for multi-period models.
"""
self.nodes = self.es.nodes
self.nodes = list(self.es.nodes)
if self.is_cellular:
# collect all nodes from the child cells
for cell in self.ec:
for node in cell.nodes:
self.nodes.append(node)
self.nodes.extend(cell.nodes)
# create set with all nodes
self.NODES = po.Set(initialize=[n for n in self.nodes])

Expand Down
33 changes: 14 additions & 19 deletions src/oemof/solph/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
SPDX-License-Identifier: MIT
"""

from oemof.solph._plumbing import sequence


Expand Down Expand Up @@ -184,11 +183,11 @@ class NonConvex:
from the actual output.
inactivity_costs : numeric (iterable or scalar)
Costs associated with not operating the flow.
minimum_uptime : numeric (1 or positive integer)
minimum_uptime : numeric or list of numeric (1 or positive integer)
Minimum number of time steps that a flow must be greater then its
minimum flow after startup. Be aware that minimum up and downtimes
can contradict each other and may lead to infeasible problems.
minimum_downtime : numeric (1 or positive integer)
minimum_downtime : numeric or list of numeric (1 or positive integer)
Minimum number of time steps a flow is forced to zero after
shutting down. Be aware that minimum up and downtimes can
contradict each other and may to infeasible problems.
Expand All @@ -199,12 +198,12 @@ class NonConvex:
initial_status : numeric (0 or 1)
Integer value indicating the status of the flow in the first time step
(0 = off, 1 = on). For minimum up and downtimes, the initial status
is set for the respective values in the edge regions e.g. if a
minimum uptime of four timesteps is defined, the initial status is
fixed for the four first and last timesteps of the optimization period.
If both, up and downtimes are defined, the initial status is set for
the maximum of both e.g. for six timesteps if a minimum downtime of
six timesteps is defined besides a four timestep minimum uptime.
is set for the respective values in the beginning e.g. if a
minimum uptime of four timesteps is defined and the initial status is
set to one, the initial status is fixed for the four first timesteps
of the optimization period. Otherwise if the initial status is set to
zero and the first timesteps are fixed for the number of minimum
downtime steps.
negative_gradient_limit : numeric (iterable, scalar or None)
the normed *upper bound* on the positive difference
(`flow[t-1] < flow[t]`) of two consecutive flow values.
Expand Down Expand Up @@ -232,8 +231,8 @@ def __init__(
custom_attributes = {}

self.initial_status = initial_status
self.minimum_uptime = minimum_uptime
self.minimum_downtime = minimum_downtime
self.minimum_uptime = sequence(minimum_uptime)
self.minimum_downtime = sequence(minimum_downtime)
self.maximum_startups = maximum_startups
self.maximum_shutdowns = maximum_shutdowns

Expand All @@ -247,11 +246,7 @@ def __init__(
for attribute, value in custom_attributes.items():
setattr(self, attribute, value)

@property
def max_up_down(self):
"""Return maximum of minimum_uptime and minimum_downtime.
The maximum of both is used to set the initial status for this
number of time steps within the edge regions.
"""
return max(self.minimum_uptime, self.minimum_downtime)
if initial_status == 0:
self.first_flexible_timestep = self.minimum_downtime[0]
else:
self.first_flexible_timestep = self.minimum_uptime[0]
4 changes: 2 additions & 2 deletions src/oemof/solph/_plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class _Sequence(UserList):
--------
>>> s = _Sequence(default=42)
>>> len(s)
0
1
>>> s[1]
42
>>> s[2]
Expand All @@ -79,7 +79,7 @@ class _Sequence(UserList):
def __init__(self, *args, **kwargs):
self.default = kwargs["default"]
self.default_changed = False
self.highest_index = -1
self.highest_index = 0
super().__init__(*args)

def __getitem__(self, key):
Expand Down
2 changes: 1 addition & 1 deletion src/oemof/solph/components/_generic_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,7 @@ def _objective_expression(self):
m.es.periods_years[p],
range_limit,
)
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
)

for n in self.EXISTING_INVESTSTORAGES:
if n.investment.fixed_costs[0] is not None:
Expand Down
8 changes: 2 additions & 6 deletions src/oemof/solph/components/_offset_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,7 @@ def _relation_rule(block):
# `NonConvexFlow`.
try:
expr += (
m.InvestNonConvexFlowBlock.status_nominal[
n, o, t
]
m.InvestNonConvexFlowBlock.status[n, o, t]
* n.coefficients[0][t]
)
# `KeyError` occurs when more than one
Expand All @@ -262,9 +260,7 @@ def _relation_rule(block):
# (inside the `try` block) does not exist.
except (KeyError, AttributeError):
expr += (
m.NonConvexFlowBlock.status_nominal[
n, o, t
]
m.NonConvexFlowBlock.status[n, o, t]
* n.coefficients[0][t]
)
block.relation.add((n, i, o, p, t), (expr == 0))
Expand Down
6 changes: 3 additions & 3 deletions src/oemof/solph/components/experimental/_sink_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,7 @@ def _objective_expression(self):
m.es.periods_years[p],
range_limit,
)
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
)

for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
Expand Down Expand Up @@ -3313,7 +3313,7 @@ def _objective_expression(self):
m.es.periods_years[p],
range_limit,
)
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
)

for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
Expand Down Expand Up @@ -5824,7 +5824,7 @@ def _objective_expression(self):
m.es.periods_years[p],
range_limit,
)
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
)

for g in self.EXISTING_INVESTDSM:
if g.investment.fixed_costs[0] is not None:
Expand Down
7 changes: 2 additions & 5 deletions src/oemof/solph/flows/_investment_flow_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,11 +1022,8 @@ def _objective_expression(self):
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,
)
) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
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:
Expand Down
74 changes: 47 additions & 27 deletions src/oemof/solph/flows/_non_convex_flow_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ def _create_variables(self):
"""
m = self.parent_block()
self.status = Var(self.NONCONVEX_FLOWS, m.TIMESTEPS, within=Binary)
for o, i in self.NONCONVEX_FLOWS:
if m.flows[o, i].nonconvex.initial_status is not None:
for t in range(
0, m.flows[o, i].nonconvex.first_flexible_timestep
):
self.status[o, i, t] = m.flows[
o, i
].nonconvex.initial_status
self.status[o, i, t].fix()

# `status_nominal` is a parameter which represents the
# multiplication of a binary variable (`status`)
Expand Down Expand Up @@ -173,10 +182,10 @@ def _sets_for_non_convex_flows(self, group):
`maximum_shutdowns` being not None.
MINUPTIMEFLOWS
A subset of set NONCONVEX_FLOWS with the attribute
`minimum_uptime` being not None.
`minimum_uptime` being > 0.
MINDOWNTIMEFLOWS
A subset of set NONCONVEX_FLOWS with the attribute
`minimum_downtime` being not None.
`minimum_downtime` being > 0.
POSITIVE_GRADIENT_FLOWS
A subset of set NONCONVEX_FLOWS with the attribute
`positive_gradient` being not None.
Expand Down Expand Up @@ -221,14 +230,14 @@ def _sets_for_non_convex_flows(self, group):
initialize=[
(g[0], g[1])
for g in group
if g[2].nonconvex.minimum_uptime > 0
if max(g[2].nonconvex.minimum_uptime) > 0
]
)
self.MINDOWNTIMEFLOWS = Set(
initialize=[
(g[0], g[1])
for g in group
if g[2].nonconvex.minimum_downtime > 0
if max(g[2].nonconvex.minimum_downtime) > 0
]
)
self.NEGATIVE_GRADIENT_FLOWS = Set(
Expand Down Expand Up @@ -441,10 +450,6 @@ def _inactivity_costs(self):

return inactivity_costs

@staticmethod
def _time_step_allows_flexibility(t, max_up_down, last_step):
return max_up_down <= t <= last_step - max_up_down

def _min_downtime_constraint(self):
r"""
.. math::
Expand All @@ -469,24 +474,32 @@ def min_downtime_rule(_, i, o, t):
"""
Rule definition for min-downtime constraints of non-convex flows.
"""
if self._time_step_allows_flexibility(
t, m.flows[i, o].nonconvex.max_up_down, m.TIMESTEPS.at(-1)
if (
m.flows[i, o].nonconvex.first_flexible_timestep
< t
< m.TIMESTEPS.at(-1)
):
# We have a 2D matrix of constraints,
# so testing is easier then just calling the rule for valid t.

expr = 0
expr += (
self.status[i, o, t - 1] - self.status[i, o, t]
) * m.flows[i, o].nonconvex.minimum_downtime
expr += -m.flows[i, o].nonconvex.minimum_downtime
) * m.flows[i, o].nonconvex.minimum_downtime[t]
expr += -m.flows[i, o].nonconvex.minimum_downtime[t]
expr += sum(
self.status[i, o, t + d]
for d in range(0, m.flows[i, o].nonconvex.minimum_downtime)
self.status[i, o, d]
for d in range(
t,
min(
t + m.flows[i, o].nonconvex.minimum_downtime[t],
len(m.TIMESTEPS),
),
)
)
return expr <= 0
else:
expr = 0
expr += self.status[i, o, t]
expr += -m.flows[i, o].nonconvex.initial_status
return expr == 0
return Constraint.Skip

return Constraint(
self.MINDOWNTIMEFLOWS, m.TIMESTEPS, rule=min_downtime_rule
Expand Down Expand Up @@ -514,23 +527,30 @@ def _min_uptime_rule(_, i, o, t):
"""
Rule definition for min-uptime constraints of non-convex flows.
"""
if self._time_step_allows_flexibility(
t, m.flows[i, o].nonconvex.max_up_down, m.TIMESTEPS.at(-1)
if (
m.flows[i, o].nonconvex.first_flexible_timestep
< t
< m.TIMESTEPS.at(-1)
):
# We have a 2D matrix of constraints,
# so testing is easier then just calling the rule for valid t.
expr = 0
expr += (
self.status[i, o, t] - self.status[i, o, t - 1]
) * m.flows[i, o].nonconvex.minimum_uptime
) * m.flows[i, o].nonconvex.minimum_uptime[t]
expr += -sum(
self.status[i, o, t + u]
for u in range(0, m.flows[i, o].nonconvex.minimum_uptime)
self.status[i, o, u]
for u in range(
t,
min(
t + m.flows[i, o].nonconvex.minimum_uptime[t],
len(m.TIMESTEPS),
),
)
)
return expr <= 0
else:
expr = 0
expr += self.status[i, o, t]
expr += -m.flows[i, o].nonconvex.initial_status
return expr == 0
return Constraint.Skip

return Constraint(
self.MINUPTIMEFLOWS, m.TIMESTEPS, rule=_min_uptime_rule
Expand Down
Loading

0 comments on commit 21e766f

Please sign in to comment.