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

Fix minimum up-/ downtime #1021

Merged
merged 30 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ac8b702
Draft simplified min_downtime_rule
p-snft Nov 22, 2023
2f09a60
limit t for constraint and update uptime_rule
AntonellaGia Nov 24, 2023
bb1ee0c
constraint-test
AntonellaGia Nov 24, 2023
c987b98
Merge pull request #1024 from AntonellaConsolinno/fix/minimum-uptime-…
p-snft Nov 24, 2023
c2175a9
Adhere to Black
p-snft Nov 24, 2023
b12a24f
Remove unused code
p-snft Nov 24, 2023
b9aa489
Fix activity status in initial steps
p-snft Nov 27, 2023
da8f826
Adhere to Black
p-snft Nov 27, 2023
70c3b61
Set first flexible time step of NonConvexFlow based on initial status
p-snft Nov 28, 2023
b616c52
Deprecate NonConvex.max_up_down
p-snft Nov 29, 2023
00e9825
Fix invalid initial status in constraint_test
p-snft Nov 29, 2023
111f1e1
Explain need for braching in _min_uptime_rule
p-snft Nov 29, 2023
fe05c56
Add NonConvex up-/ downtime fix to WhatsNew
p-snft Nov 29, 2023
7f21174
Let minimum_uptime and minimum_downtime be series
p-snft Nov 30, 2023
d72a590
Remove unused import
p-snft Nov 30, 2023
9312c7c
Let len(sequence(0)) == 1
p-snft Nov 30, 2023
1a93871
Adhere to Black
p-snft Nov 30, 2023
2b30979
Update documenation
AntonellaGia Dec 1, 2023
951ebbb
Remove trailing whitespace
p-snft Dec 1, 2023
42a2b80
Workaround useless statement message
p-snft Dec 1, 2023
67511b1
Fix storage investment test
p-snft Dec 1, 2023
64f67e7
Adjust min_max_runtime test lp files
p-snft Dec 1, 2023
22abd38
Replace "0.0" by "0" in dsm test lp file
p-snft Dec 1, 2023
aedfcbf
Merge branch 'dev' into fix/minimum-uptime-downtime
p-snft Dec 1, 2023
76989a0
iterate one more step uptime last timestep
AntonellaGia Dec 4, 2023
44639b6
iterate one more step downtime last timestep
AntonellaGia Dec 4, 2023
8d20184
Merge branch 'dev' into fix/minimum-uptime-downtime
p-snft Dec 6, 2023
d5fff12
Add last time step to Multi-period min/max constraint test
p-snft Dec 6, 2023
85f58f9
Merge branch 'fix/minimum-uptime-downtime' of github.com:oemof/oemof-…
p-snft Dec 6, 2023
88ccea7
Add sequence for up/downtime to Whatsnew
p-snft Dec 7, 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
1 change: 1 addition & 0 deletions docs/whatsnew/v0-5-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Bug fixes
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).

Testing
#######
Expand Down
17 changes: 6 additions & 11 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 @@ -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
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],
m.TIMESTEPS.at(-1),
),
)
)
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],
m.TIMESTEPS.at(-1),
),
)
)
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
2 changes: 1 addition & 1 deletion tests/constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,7 @@ def test_min_max_runtime(self):
nonconvex=solph.NonConvex(
minimum_downtime=4,
minimum_uptime=2,
initial_status=2,
initial_status=1,
startup_costs=5,
shutdown_costs=7,
),
Expand Down
49 changes: 18 additions & 31 deletions tests/lp_files/min_max_runtime.lp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ c_e_BusBlock_balance(Bus_T_0_2)_:

c_e_NonConvexFlowBlock_status_nominal_constraint(cheap_plant_min_down_constraints_Bus_T_0)_:
+1 NonConvexFlowBlock_status_nominal(cheap_plant_min_down_constraints_Bus_T_0)
-10 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
= 0
= 20

c_e_NonConvexFlowBlock_status_nominal_constraint(cheap_plant_min_down_constraints_Bus_T_1)_:
+1 NonConvexFlowBlock_status_nominal(cheap_plant_min_down_constraints_Bus_T_1)
Expand Down Expand Up @@ -73,14 +72,12 @@ c_u_NonConvexFlowBlock_max(cheap_plant_min_down_constraints_Bus_T_0_2)_:

c_u_NonConvexFlowBlock_startup_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
-1 NonConvexFlowBlock_startup(cheap_plant_min_down_constraints_Bus_T_0)
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
<= 2
<= 0

c_u_NonConvexFlowBlock_startup_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
-1 NonConvexFlowBlock_startup(cheap_plant_min_down_constraints_Bus_T_1)
-1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
<= 0
<= 2

c_u_NonConvexFlowBlock_startup_constr(cheap_plant_min_down_constraints_Bus_T_2)_:
-1 NonConvexFlowBlock_startup(cheap_plant_min_down_constraints_Bus_T_2)
Expand All @@ -90,44 +87,36 @@ c_u_NonConvexFlowBlock_startup_constr(cheap_plant_min_down_constraints_Bus_T_2)_

c_u_NonConvexFlowBlock_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
-1 NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_0)
-1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
<= -2
<= 0

c_u_NonConvexFlowBlock_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
-1 NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_1)
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
-1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
<= 0
<= -2

c_u_NonConvexFlowBlock_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_2)_:
-1 NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_2)
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
-1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
<= 0

c_e_NonConvexFlowBlock_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
= 2

c_e_NonConvexFlowBlock_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
c_u_NonConvexFlowBlock_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
= 2

c_e_NonConvexFlowBlock_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_2)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
= 2
<= 6

c_e_NonConvexFlowBlock_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
= 2
c_u_NonConvexFlowBlock_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
-3 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
+2 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
<= 0

c_e_NonConvexFlowBlock_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
= 2
c_u_NonConvexFlowBlock_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_0)_:
-3 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
<= -6

c_e_NonConvexFlowBlock_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_2)_:
+1 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
= 2
c_u_NonConvexFlowBlock_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_1)_:
+5 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
-4 NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
<= 4

bounds
0 <= flow(cheap_plant_min_down_constraints_Bus_T_0_0) <= 10.0
Expand All @@ -140,7 +129,6 @@ bounds
0 <= NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_1) <= 1
0 <= NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_2) <= 1
0 <= NonConvexFlowBlock_status_nominal(cheap_plant_min_down_constraints_Bus_T_0) <= +inf
0 <= NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0) <= 1
0 <= NonConvexFlowBlock_status_nominal(cheap_plant_min_down_constraints_Bus_T_1) <= +inf
0 <= NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1) <= 1
0 <= NonConvexFlowBlock_status_nominal(cheap_plant_min_down_constraints_Bus_T_2) <= +inf
Expand All @@ -152,7 +140,6 @@ binary
NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_0)
NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_1)
NonConvexFlowBlock_shutdown(cheap_plant_min_down_constraints_Bus_T_2)
NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_0)
NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_1)
NonConvexFlowBlock_status(cheap_plant_min_down_constraints_Bus_T_2)
end
39 changes: 39 additions & 0 deletions tests/test_plumbing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -

"""Testing the class NonConvex.

SPDX-FileCopyrightText: Patrik Schönfeldt

SPDX-License-Identifier: MIT
"""
import pytest

from oemof.solph._plumbing import sequence


def test_sequence():
seq0 = sequence(0)
assert seq0[0] == 0
assert len(seq0) == 1

assert seq0[10] == 0
assert len(seq0) == 11

assert max(seq0) == 0

seq10 = sequence(10)
assert max(seq10) == 10

assert seq10[0] == 10
assert len(seq10) == 1

assert seq10[10] == 10
assert len(seq10) == 11

seq12 = sequence([1, 3])
assert max(seq12) == 3
assert seq12[0] == 1
assert seq12[1] == 3

with pytest.raises(IndexError):
seq12[2]
21 changes: 0 additions & 21 deletions tests/test_scripts/test_solph/test_options/test_non_convex.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,3 @@ def test_custom_attribute():

assert non_convex_object.first_attribute is True
assert non_convex_object.second_attribute == 5


def test_max_up_down():
non_convex_object1 = NonConvex(
minimum_uptime=5,
minimum_downtime=4,
)
assert non_convex_object1.max_up_down == 5

non_convex_object1 = NonConvex(
minimum_downtime=4,
)
assert non_convex_object1.max_up_down == 4

non_convex_object1 = NonConvex(
minimum_uptime=5,
)
assert non_convex_object1.max_up_down == 5

non_convex_object1 = NonConvex()
assert non_convex_object1.max_up_down == 0