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

Features/add min max runtimes #446

Merged
merged 29 commits into from
Mar 7, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6c2d0de
Add sketch with fix uptimes
ckaldemeyer Feb 7, 2018
ed6164a
Introduce sets for up and downtimes
ckaldemeyer Feb 7, 2018
c31b865
Sketch uptime with node class params
ckaldemeyer Feb 7, 2018
eb9065e
Include border regions
ckaldemeyer Feb 7, 2018
262ca73
Amend docstring
ckaldemeyer Feb 7, 2018
1c8e2e0
Finish min downtime
ckaldemeyer Feb 7, 2018
43cf98f
Set border regions to max of up/downtime with initial status
ckaldemeyer Feb 7, 2018
56e7be7
Align code style
ckaldemeyer Feb 7, 2018
12cbe06
Fix error with min downtime
ckaldemeyer Feb 7, 2018
4b0f1ac
Adapt docstring
ckaldemeyer Feb 7, 2018
f3ab8a2
Substitute constraint if statement through property
ckaldemeyer Feb 8, 2018
f2c8107
Remove whitespace
ckaldemeyer Feb 8, 2018
fe03245
PEP8tify if statements
ckaldemeyer Feb 8, 2018
c175903
Add min-uptime inequality
ckaldemeyer Feb 8, 2018
5c68632
Allow for time-dependent startup/shutdown costs
ckaldemeyer Feb 9, 2018
ffb9d3d
Adapt time-dependent objective expression
ckaldemeyer Feb 9, 2018
527f963
Fix objective expr and error with wrongly initialized sets
ckaldemeyer Feb 9, 2018
f54078c
Adapt WHATSNEW
ckaldemeyer Feb 9, 2018
9ad8503
Add equations to docstrings
ckaldemeyer Feb 9, 2018
179bb7a
add constraint test
uvchik Feb 12, 2018
41162bd
Add docstring for constraint test method
ckaldemeyer Feb 12, 2018
dddfa5b
Resolve merge conflict
ckaldemeyer Feb 12, 2018
1795b1d
Tweak formulation
ckaldemeyer Feb 12, 2018
f2dfb12
Update v0-2-1.rst
ckaldemeyer Mar 5, 2018
8884812
Update v0-2-1.rst
ckaldemeyer Mar 5, 2018
0632561
Update docstring of NonConvex Block
simnh Mar 6, 2018
83ef7ff
Merge branch 'features/add-min-max-runtimes' of https://github.com/oe…
simnh Mar 6, 2018
3f307e7
limit pyomo version
uvchik Mar 6, 2018
d4c4e34
Allow for higher Pyomo versions
ckaldemeyer Mar 7, 2018
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
6 changes: 5 additions & 1 deletion doc/whatsnew/v0-2-1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ API changes
New features
############

* It is now possible determine minimum up and downtimes for nonconvex flows.
Check the `oemof_examples <https://github.com/oemof/oemof_examples>`_
repository for an exemplary usage.


New components
Expand All @@ -32,7 +35,8 @@ Known issues
Bug fixes
#########


* Shutdown costs for nonconvex flows are now accounted within the objective
which was not the case before due to a naming error

Testing
#######
Expand Down
113 changes: 97 additions & 16 deletions oemof/solph/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,38 @@ class NonConvexFlow(SimpleBlock):
\forall t \in \textrm{TIMESTEPS}, \\
\forall (i, o) \in \textrm{SHUTDOWN\_FLOWS}.

Minimum uptime constraint :attr:`om.NonConvexFlow.uptime_constr[i,o,t]`
.. math::
(status(i, o, t)-status(i, o, t-1)) \cdot minimum\_uptime(i, o) \\
\leq \sum_{n=0}^{minimum\_uptime-1} status(i,o,t+n) \\
\forall t \in \textrm{TIMESTEPS} | \\
t \neq \{0..minimum\_uptime\} \cup \
\{t\_max-minimum\_uptime..t\_max\} , \\
\forall (i,o) \in \textrm{MINUPTIME\_FLOWS}.
\\ \\
status(i, o, t) = initial\_status(i, o) \\
\forall t \in \textrm{TIMESTEPS} | \\
t = \{0..minimum\_uptime\} \cup \
\{t\_max-minimum\_uptime..t\_max\} , \\
\forall (i,o) \in \textrm{MINUPTIME\_FLOWS}.

Minimum downtime constraint :attr:`om.NonConvexFlow.downtime_constr[i,o,t]`
.. math::
(status(i, o, t-1)-status(i, o, t)) \
\cdot minimum\_downtime(i, o) \\
\leq minimum\_downtime(i, o) \
- \sum_{n=0}^{minimum\_downtime-1} status(i,o,t+n) \\
\forall t \in \textrm{TIMESTEPS} | \\
t \neq \{0..minimum\_downtime\} \cup \
\{t\_max-minimum\_downtime..t\_max\} , \\
\forall (i,o) \in \textrm{MINDOWNTIME\_FLOWS}.
\\ \\
status(i, o, t) = initial\_status(i, o) \\
\forall t \in \textrm{TIMESTEPS} | \\
t = \{0..minimum\_downtime\} \cup \
\{t\_max-minimum\_downtime..t\_max\} , \\
\forall (i,o) \in \textrm{MINDOWNTIME\_FLOWS}.

**The following parts of the objective function are created:**

If :attr:`nonconvex.startup_costs` is set by the user:
Expand Down Expand Up @@ -652,20 +684,28 @@ def _create(self, group=None):
if g[2].min[0] is not None])

self.STARTUPFLOWS = Set(initialize=[(g[0], g[1]) for g in group
if g[2].nonconvex.startup_costs is not None])
if g[2].nonconvex.startup_costs[0]
is not None])

self.SHUTDOWNFLOWS = Set(initialize=[(g[0], g[1]) for g in group
if g[2].nonconvex.shutdown_costs is not None])
if g[2].nonconvex.shutdown_costs[0]
is not None])

self.MINUPTIMEFLOWS = Set(initialize=[(g[0], g[1]) for g in group
if g[2].nonconvex.minimum_uptime
is not None])

self.MINDOWNTIMEFLOWS = Set(initialize=[(g[0], g[1]) for g in group
if g[2].nonconvex.minimum_downtime
is not None])

# ################### VARIABLES AND CONSTRAINTS #######################
self.status = Var(self.NONCONVEX_FLOWS, m.TIMESTEPS, within=Binary)

if self.STARTUPFLOWS:
self.startup = Var(self.STARTUPFLOWS, m.TIMESTEPS,
within=Binary)
self.startup = Var(self.STARTUPFLOWS, m.TIMESTEPS, within=Binary)
if self.SHUTDOWNFLOWS:
self.shutdown = Var(self.SHUTDOWNFLOWS, m.TIMESTEPS,
within=Binary)
self.shutdown = Var(self.SHUTDOWNFLOWS, m.TIMESTEPS, within=Binary)

def _minimum_flow_rule(block, i, o, t):
"""Rule definition for MILP minimum flow constraints.
Expand Down Expand Up @@ -714,8 +754,46 @@ def _shutdown_rule(block, i, o, t):
self.shutdown_constr = Constraint(self.SHUTDOWNFLOWS, m.TIMESTEPS,
rule=_shutdown_rule)

def _min_uptime_rule(block, i, o, t):
"""Rule definition for min-uptime constraints of nonconvex flows.
"""
if (t >= m.flows[i, o].nonconvex.max_up_down and
t <= m.TIMESTEPS[-1]-m.flows[i, o].nonconvex.max_up_down):
expr = 0
expr += ((self.status[i, o, t]-self.status[i, o, t-1]) *
m.flows[i, o].nonconvex.minimum_uptime)
expr += -sum(self.status[i, o, t+u] for u in range(0,
m.flows[i, o].nonconvex.minimum_uptime))
return expr <= 0
else:
expr = 0
expr += self.status[i, o, t]
expr += -m.flows[i, o].nonconvex.initial_status
return expr == 0
self.min_uptime_constr = Constraint(
self.MINUPTIMEFLOWS, m.TIMESTEPS, rule=_min_uptime_rule)

def _min_downtime_rule(block, i, o, t):
"""Rule definition for min-downtime constraints of nonconvex flows.
"""
if (t >= m.flows[i, o].nonconvex.max_up_down and
t <= m.TIMESTEPS[-1]-m.flows[i, o].nonconvex.max_up_down):
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
expr += sum(self.status[i, o, t+d] for d in range(0,
m.flows[i, o].nonconvex.minimum_downtime))
return expr <= 0
else:
expr = 0
expr += self.status[i, o, t]
expr += -m.flows[i, o].nonconvex.initial_status
return expr == 0
self.min_downtime_constr = Constraint(
self.MINDOWNTIMEFLOWS, m.TIMESTEPS, rule=_min_downtime_rule)

# TODO: Add gradient constraints for nonconvex block / flows
# TODO: Add min-up/min-downtime constraints

def _objective_expression(self):
r"""Objective expression for nonconvex flows.
Expand All @@ -729,17 +807,20 @@ def _objective_expression(self):
shutdowncosts = 0

if self.STARTUPFLOWS:
startcosts += sum(self.startup[i, o, t] *
m.flows[i, o].nonconvex.startup_costs
for i, o in self.STARTUPFLOWS
for t in m.TIMESTEPS)
for i, o in self.STARTUPFLOWS:
if (m.flows[i, o].nonconvex.startup_costs[0] is not None):
startcosts += sum(self.startup[i, o, t] *
m.flows[i, o].nonconvex.startup_costs[t]
for t in m.TIMESTEPS)
self.startcosts = Expression(expr=startcosts)

if self.SHUTDOWNFLOWS:
shutdowncosts += sum(self.shutdown[i, o, t] *
m.flows[i, o].nonconvex.shutdown_costs
for i, o in self.SHUTDOWNFLOWS
for t in m.TIMESTEPS)
self.shudowcosts = Expression(expr=shutdowncosts)
for i, o in self.SHUTDOWNFLOWS:
if (m.flows[i, o].nonconvex.shutdown_costs[0] is not None):
shutdowncosts += sum(
self.shutdown[i, o, t] *
m.flows[i, o].nonconvex.shutdown_costs[t]
for t in m.TIMESTEPS)
self.shutdowncosts = Expression(expr=shutdowncosts)

return startcosts + shutdowncosts
1 change: 0 additions & 1 deletion oemof/solph/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,6 @@ def _calculate_alphas(self):
length = [len(a) for a in attrs if not isinstance(a, (int, float))]
max_length = max(length)


if all(len(a) == max_length for a in attrs):
if max_length == 0:
max_length += 1 # increment dimension for scalars from 0 to 1
Expand Down
62 changes: 51 additions & 11 deletions oemof/solph/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
SPDX-License-Identifier: GPL-3.0-or-later
"""

from oemof.solph.plumbing import sequence


class Investment:
"""
Expand Down Expand Up @@ -35,20 +37,58 @@ class NonConvex:
startup_costs : numeric
Costs associated with a start of the flow (representing a unit).
shutdown_costs : numeric
Costs associated with the shutdown of the flow (representing a until).
minimum_uptime : numeric
Costs associated with the shutdown of the flow (representing a unit).
minimum_uptime : numeric (1 or positive integer)
Minimum time that a flow must be greater then its minimum flow after
startup.
minimum_downtime : numeric
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 time 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.
initial_status : numeric (0 or 1)
Integer value indicating the status of the flow in the first time step
(0 = off, 1 = on).
(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 in addition to a four timestep minimum uptime.
"""
def __init__(self, **kwargs):
# super().__init__(self, **kwargs)
self.startup_costs = kwargs.get('startup_costs')
self.shutdown_costs = kwargs.get('shutdown_costs')
self.minimum_uptime = kwargs.get('minimum_uptime')
self.minimum_downtime = kwargs.get('minimum_downtime')
self.initial_status = kwargs.get('initial_status', 0)
scalars = ['minimum_uptime', 'minimum_downtime', 'initial_status']
sequences = ['startup_costs', 'shutdown_costs']
defaults = {'initial_status': 0}

for attribute in set(scalars + sequences + list(kwargs)):
value = kwargs.get(attribute, defaults.get(attribute))
setattr(self, attribute,
sequence(value) if attribute in sequences else value)

self._max_up_down = None

def _calculate_max_up_down(self):
"""
Calculate maximum of up and downtime for direct usage in constraints.

The maximum of both is used to set the initial status for this
number of timesteps within the edge regions.
"""
if (self.minimum_uptime is not None and self.minimum_downtime is None):
max_up_down = self.minimum_uptime
elif (self.minimum_uptime is None and
self.minimum_downtime is not None):
max_up_down = self.minimum_downtime
else:
max_up_down = max(self.minimum_uptime, self.minimum_downtime)

self._max_up_down = max_up_down

@property
def max_up_down(self):
"""Compute or return the _max_up_down attribute."""
if self._max_up_down is None:
self._calculate_max_up_down()

return self._max_up_down
31 changes: 24 additions & 7 deletions tests/constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def get_om(self):
timeindex=self.energysystem.timeindex)

def compare_lp_files(self, filename, ignored=None, my_om=None):
r"""Compare lp-files to check constraints generated within solph.

An lp-file is being generated automatically when the tests are
executed. Make sure that you create an empty file first and
transfer the content from the one that has been created automatically
into this one afterwards. Please ensure that the content is being
checked carefully. Otherwise, errors are included within the code base.
"""
if my_om is None:
om = self.get_om()
else:
Expand Down Expand Up @@ -378,8 +386,7 @@ def test_flow_without_emission_for_emission_constraint_no_error(self):
solph.constraints.emission_limit(om, limit=777)

def test_equate_variables_constraint(self):
"""Testing the equate_variables function in the constraint module.
"""
"""Testing the equate_variables function in the constraint module."""
bus1 = solph.Bus(label='Bus1')
storage = solph.components.GenericStorage(
label='storage',
Expand All @@ -403,8 +410,7 @@ def test_equate_variables_constraint(self):
self.compare_lp_files('connect_investment.lp', my_om=om)

def test_gradient(self):
"""
"""
"""Testing min and max runtimes for nonconvex flows."""
bel = solph.Bus(label='electricityBus')

solph.Source(label='powerplant', outputs={bel: solph.Flow(
Expand All @@ -415,8 +421,7 @@ def test_gradient(self):
self.compare_lp_files('source_with_gradient.lp')

def test_investment_limit(self):
"""Testing the investment_limit function in the constraint module.
"""
"""Testing the investment_limit function in the constraint module."""
bus1 = solph.Bus(label='Bus1')
solph.components.GenericStorage(
label='storage',
Expand All @@ -430,4 +435,16 @@ def test_investment_limit(self):
om = self.get_om()
solph.constraints.investment_limit(om, limit=900)

self.compare_lp_files('investment_limit.lp', my_om=om)
self.compare_lp_files('investment_limit.lp', my_om=om)

def test_min_max_runtime(self):
"""Testing min and max runtimes for nonconvex flows."""
bus_t = solph.Bus(label='Bus_T')
solph.Source(
label='cheap_plant_min_down_constraints',
outputs={bus_t: solph.Flow(
nominal_value=10, min=0.5, max=1.0, variable_costs=10,
nonconvex=solph.NonConvex(
minimum_downtime=4, minimum_uptime=2, initial_status=2,
startup_costs=5, shutdown_costs=7))})
self.compare_lp_files('min_max_runtime.lp')
Loading