diff --git a/doc/whatsnew/v0-2-1.rst b/doc/whatsnew/v0-2-1.rst index e9e168455..4852c55f3 100644 --- a/doc/whatsnew/v0-2-1.rst +++ b/doc/whatsnew/v0-2-1.rst @@ -10,7 +10,10 @@ API changes New features ############ - +* It is now possible determine minimum up and downtimes for nonconvex flows. + Check the `oemof_examples `_ + repository for an exemplary usage. +* Startup and shutdown costs can now be defined time-dependent New components ############## @@ -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 ####### diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 74e660ec3..787e821fa 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -572,6 +572,14 @@ class NonConvexFlow(SimpleBlock): A subset of set NONCONVEX_FLOWS with the attribute :attr:`shutdown_costs` being not None. + MINUPTIMEFLOWS + A subset of set NONCONVEX_FLOWS with the attribute + :attr:`minimum_uptime` being not None. + + MINDOWNTIMEFLOWS + A subset of set NONCONVEX_FLOWS with the attribute + :attr:`minimum_downtime` being not None. + **The following variables are created:** Status variable (binary) :attr:`om.NonConvexFlow.status`: @@ -615,6 +623,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: @@ -652,20 +692,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. @@ -714,8 +762,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. @@ -729,17 +815,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 diff --git a/oemof/solph/components.py b/oemof/solph/components.py index 91ec83440..0619f2b93 100644 --- a/oemof/solph/components.py +++ b/oemof/solph/components.py @@ -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 diff --git a/oemof/solph/options.py b/oemof/solph/options.py index db175f1f0..e08d9bb93 100644 --- a/oemof/solph/options.py +++ b/oemof/solph/options.py @@ -8,6 +8,8 @@ SPDX-License-Identifier: GPL-3.0-or-later """ +from oemof.solph.plumbing import sequence + class Investment: """ @@ -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 diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index a9abc9fcb..61c776287 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -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: @@ -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', @@ -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( @@ -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', @@ -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) \ No newline at end of file + 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') diff --git a/tests/lp_files/min_max_runtime.lp b/tests/lp_files/min_max_runtime.lp new file mode 100644 index 000000000..9c36a2dab --- /dev/null +++ b/tests/lp_files/min_max_runtime.lp @@ -0,0 +1,143 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++7 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_0) ++7 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_1) ++7 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_2) ++5 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_0) ++5 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_1) ++5 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_2) ++10 flow(cheap_plant_min_down_constraints_Bus_T_0) ++10 flow(cheap_plant_min_down_constraints_Bus_T_1) ++10 flow(cheap_plant_min_down_constraints_Bus_T_2) + +s.t. + +c_e_Bus_balance(Bus_T_0)_: ++1 flow(cheap_plant_min_down_constraints_Bus_T_0) += 0 + +c_e_Bus_balance(Bus_T_1)_: ++1 flow(cheap_plant_min_down_constraints_Bus_T_1) += 0 + +c_e_Bus_balance(Bus_T_2)_: ++1 flow(cheap_plant_min_down_constraints_Bus_T_2) += 0 + +c_u_NonConvexFlow_min(cheap_plant_min_down_constraints_Bus_T_0)_: ++5 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) +-1 flow(cheap_plant_min_down_constraints_Bus_T_0) +<= 0 + +c_u_NonConvexFlow_min(cheap_plant_min_down_constraints_Bus_T_1)_: ++5 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) +-1 flow(cheap_plant_min_down_constraints_Bus_T_1) +<= 0 + +c_u_NonConvexFlow_min(cheap_plant_min_down_constraints_Bus_T_2)_: ++5 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) +-1 flow(cheap_plant_min_down_constraints_Bus_T_2) +<= 0 + +c_u_NonConvexFlow_max(cheap_plant_min_down_constraints_Bus_T_0)_: +-10 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) ++1 flow(cheap_plant_min_down_constraints_Bus_T_0) +<= 0 + +c_u_NonConvexFlow_max(cheap_plant_min_down_constraints_Bus_T_1)_: +-10 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) ++1 flow(cheap_plant_min_down_constraints_Bus_T_1) +<= 0 + +c_u_NonConvexFlow_max(cheap_plant_min_down_constraints_Bus_T_2)_: +-10 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) ++1 flow(cheap_plant_min_down_constraints_Bus_T_2) +<= 0 + +c_u_NonConvexFlow_startup_constr(cheap_plant_min_down_constraints_Bus_T_0)_: +-1 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_0) ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) +<= 2 + +c_u_NonConvexFlow_startup_constr(cheap_plant_min_down_constraints_Bus_T_1)_: +-1 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_1) +-1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) +<= 0 + +c_u_NonConvexFlow_startup_constr(cheap_plant_min_down_constraints_Bus_T_2)_: +-1 NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_2) +-1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) +<= 0 + +c_u_NonConvexFlow_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_0)_: +-1 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_0) +-1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) +<= -2 + +c_u_NonConvexFlow_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_1)_: +-1 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_1) ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) +-1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) +<= 0 + +c_u_NonConvexFlow_shutdown_constr(cheap_plant_min_down_constraints_Bus_T_2)_: +-1 NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_2) ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) +-1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) +<= 0 + +c_e_NonConvexFlow_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_0)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) += 2 + +c_e_NonConvexFlow_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_1)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) += 2 + +c_e_NonConvexFlow_min_uptime_constr(cheap_plant_min_down_constraints_Bus_T_2)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) += 2 + +c_e_NonConvexFlow_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_0)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) += 2 + +c_e_NonConvexFlow_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_1)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) += 2 + +c_e_NonConvexFlow_min_downtime_constr(cheap_plant_min_down_constraints_Bus_T_2)_: ++1 NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) += 2 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(cheap_plant_min_down_constraints_Bus_T_0) <= 10 + 0 <= flow(cheap_plant_min_down_constraints_Bus_T_1) <= 10 + 0 <= flow(cheap_plant_min_down_constraints_Bus_T_2) <= 10 + 0 <= NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) <= 1 + 0 <= NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) <= 1 + 0 <= NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) <= 1 + 0 <= NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_0) <= 1 + 0 <= NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_1) <= 1 + 0 <= NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_2) <= 1 + 0 <= NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_0) <= 1 + 0 <= NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_1) <= 1 + 0 <= NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_2) <= 1 +binary + NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_0) + NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_1) + NonConvexFlow_status(cheap_plant_min_down_constraints_Bus_T_2) + NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_0) + NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_1) + NonConvexFlow_startup(cheap_plant_min_down_constraints_Bus_T_2) + NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_0) + NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_1) + NonConvexFlow_shutdown(cheap_plant_min_down_constraints_Bus_T_2) +end