From c34d5e7d0de79279c9b5446a86e85e6c69c00511 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 9 Dec 2019 17:53:55 +0100 Subject: [PATCH 01/45] Add schedule for flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It can be important to determine the output of e.g. a CHP by a given schedule. In practice it´s used to fulfill contracts. --- oemof/solph/blocks.py | 37 ++++++++++++++++++++++++++++++++++++- oemof/solph/network.py | 21 +++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 7ad4badd3..7a863257a 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -44,6 +44,9 @@ class Flow(SimpleBlock): INTEGER_FLOWS A set of flows wher the attribute :attr:`integer` is True (forces flow to only take integer values) + PENALTY_FLOWS + A set of flows with :attr:`schedule` not None (forces flow to this + schedule) **The following constraints are build:** @@ -120,6 +123,10 @@ def _create(self, group=None): self.INTEGER_FLOWS = Set( initialize=[(g[0], g[1]) for g in group if g[2].integer]) + + self.PENALTY_FLOWS = Set( + initialize=[(g[0], g[1]) for g in group if g[2].schedule[0] is not None] + ) # ######################### Variables ################################ self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, @@ -130,6 +137,13 @@ def _create(self, group=None): self.integer_flow = Var(self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers) + + self.slack_pos = Var(self.PENALTY_FLOWS, + m.TIMESTEPS, within=NonNegativeReals) + + self.slack_neg = Var(self.PENALTY_FLOWS, + m.TIMESTEPS, within=NonNegativeReals) + # set upper bound of gradient variable for i, o, f in group: if m.flows[i, o].positive_gradient['ub'][0] is not None: @@ -209,6 +223,20 @@ def _integer_flow_rule(block, i, o, t): self.integer_flow_constr = Constraint(self.INTEGER_FLOWS, m.TIMESTEPS, rule=_integer_flow_rule) + def _schedule_rule(model): + for inp, out in self.PENALTY_FLOWS: + for ts in m.TIMESTEPS: + if m.flows[inp, out].schedule[ts] is not None: + lhs = (m.flow[inp, out, ts] + self.slack_pos[inp, out, ts] - + self.slack_neg[inp, out, ts]) + rhs = m.flows[inp, out].schedule[ts] + self.schedule_constr.add((inp, out, ts), + lhs == rhs) + self.schedule_constr = Constraint( + self.PENALTY_FLOWS, m.TIMESTEPS, noruleinit=True) + self.schedule_build = BuildAction( + rule=_schedule_rule) + def _objective_expression(self): r""" Objective expression for all standard flows with fixed costs and variable costs. @@ -217,6 +245,7 @@ def _objective_expression(self): variable_costs = 0 gradient_costs = 0 + penalty_costs = 0 for i, o in m.FLOWS: if m.flows[i, o].variable_costs[0] is not None: @@ -236,7 +265,13 @@ def _objective_expression(self): m.flows[i, o].negative_gradient[ 'costs']) - return variable_costs + gradient_costs + if m.flows[i, o].schedule[0] is not None: + for t in m.TIMESTEPS: + penalty_costs += (self.slack_pos[i, o, t] * + m.flows[i, o].penalty_pos[t]) + penalty_costs += (self.slack_neg[i, o, t] * + m.flows[i, o].penalty_neg[t]) + return variable_costs + gradient_costs + penalty_costs class InvestmentFlow(SimpleBlock): diff --git a/oemof/solph/network.py b/oemof/solph/network.py index aaf956644..2c3615026 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -117,7 +117,17 @@ class Flow(on.Edge): :class:`Flow `. Note: at the moment this does not work if the investment attribute is set . - + schedule : numeric (sequence or scalar) + Schedule for the flow. Flow has to follow the schedule, otherwise a + penalty term will be activated. If array-like, values can be None + for flexible, non-fixed flow during the certain timestep. Used in + combination with the :attr:`penalty`. + penalty : numeric + The penalty parameter of the penalty term describes the costs + associated with one unit of the flow when not following the schedule. + If this is set the costs will be added to the objective expression + of the optimization problem. Used in combination with the + :attr:`schedule`. Notes ----- The following sets, variables, constraints and objective parts are created @@ -157,11 +167,13 @@ def __init__(self, **kwargs): scalars = ['nominal_value', 'summed_max', 'summed_min', 'investment', 'nonconvex', 'integer', 'fixed'] - sequences = ['actual_value', 'variable_costs', 'min', 'max'] + sequences = ['actual_value', 'variable_costs', 'min', 'max', 'schedule', + 'penalty_neg', 'penalty_pos'] dictionaries = ['positive_gradient', 'negative_gradient'] defaults = {'fixed': False, 'min': 0, 'max': 1, 'variable_costs': 0, 'positive_gradient': {'ub': None, 'costs': 0}, 'negative_gradient': {'ub': None, 'costs': 0}, + 'penalty_neg': 0, 'penalty_pos': 0, 'schedule': None } keys = [k for k in kwargs if k != 'label'] @@ -188,6 +200,11 @@ def __init__(self, **kwargs): if self.investment and self.nonconvex: raise ValueError("Investment flows cannot be combined with " + "nonconvex flows!") + if (len(self.schedule) != 0) and not \ + (self.penalty_pos[0] and self.penalty_neg[0]): + raise ValueError("The penalty and schedule attribute need " + "to be used in combination. \n Please set " + "the schedule attribute of the flow.") class Bus(on.Bus): From c49a010629c29d84ab8e01b6ad8d37e1676774cb Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 9 Dec 2019 18:37:30 +0100 Subject: [PATCH 02/45] Adapted existing test files New parameters for class `Flow` has been added. They had to be added to the existing test files too. --- tests/test_processing.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index 00c402c18..24a1ad628 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -100,13 +100,17 @@ def test_flows_with_none_exclusion(self): 'positive_gradient_costs': 0, 'variable_costs': 0, 'label': str(b_el2.outputs[demand].label), + 'penalty_pos': 0, + 'penalty_neg': 0, } ).sort_index() ) + print(param_results[(b_el2, demand)]['sequences']) assert_frame_equal( param_results[(b_el2, demand)]['sequences'], pandas.DataFrame( - {'actual_value': self.demand_values} + {'actual_value': self.demand_values, + } ), check_like=True ) @@ -133,6 +137,9 @@ def test_flows_without_none_exclusion(self): 'flow': None, 'values': None, 'label': str(b_el2.outputs[demand].label), + 'penalty_pos': 0, + 'penalty_neg': 0, + 'schedule': None, } assert_series_equal( param_results[(b_el2, demand)]['scalars'].sort_index(), @@ -142,7 +149,7 @@ def test_flows_without_none_exclusion(self): 'actual_value': self.demand_values, } default_sequences = [ - 'actual_value' + 'actual_value', ] for attr in default_sequences: if attr not in sequences_attributes: From 088764550adf4eb15ee27de62408b2a805b69c59 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 9 Dec 2019 19:27:10 +0100 Subject: [PATCH 03/45] Adapted existing tests for scheduled flows --- tests/test_processing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index 24a1ad628..2fdb65866 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -105,7 +105,6 @@ def test_flows_with_none_exclusion(self): } ).sort_index() ) - print(param_results[(b_el2, demand)]['sequences']) assert_frame_equal( param_results[(b_el2, demand)]['sequences'], pandas.DataFrame( From b76039645bc9dbf3376d2d1cf28c026522e07fb6 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 12:31:42 +0100 Subject: [PATCH 04/45] added constraint and variables to docstring --- oemof/solph/blocks.py | 38 +++++++++++++++++++++++++++++--------- oemof/solph/network.py | 17 ++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 7a863257a..90aeffa89 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -28,6 +28,14 @@ class Flow(SimpleBlock): Difference of a flow in consecutive timesteps if flow is increased indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS. + slack_pos : + Difference of a flow in consecutive timesteps if flow exceeds + schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. + + slack_neg : + Deficit of flow compared to schedule. Indexed by SCHEDULE_FLOWS, + TIMESTEPS. + **The following sets are created:** (-> see basic sets at :class:`.Model` ) @@ -42,10 +50,10 @@ class Flow(SimpleBlock): A set of flows with the attribute :attr:`positive_gradient` being not None INTEGER_FLOWS - A set of flows wher the attribute :attr:`integer` is True (forces flow + A set of flows where the attribute :attr:`integer` is True (forces flow to only take integer values) - PENALTY_FLOWS - A set of flows with :attr:`schedule` not None (forces flow to this + SCHEDULE_FLOWS + A set of flows with :attr:`schedule` not None (forces flow to follow schedule) **The following constraints are build:** @@ -74,16 +82,28 @@ class Flow(SimpleBlock): \forall (i, o) \in \textrm{POSITIVE\_GRADIENT\_FLOWS}, \\ \forall t \in \textrm{TIMESTEPS}. + Schedule constraint :attr:`om.Flow.positive_gradient_constr[i, o]`: + .. math:: flow(i, o, t) + slack_pos(i, o, t) - \ + slack_neg(i, o, t) = schedule(i, o, t) \\ + \forall (i, o) \in \textrm{SCHEDULE\_FLOWS}, \\ + \forall t \in \textrm{TIMESTEPS}. + **The following parts of the objective function are created:** If :attr:`variable_costs` are set by the user: .. math:: - \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t) + \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t) The expression can be accessed by :attr:`om.Flow.variable_costs` and their value after optimization by :meth:`om.Flow.variable_costs()` . + If :attr:`schedule`, :attr:`penalty_pos` and :attr:`penalty_neg` are + set by the user: + .. math:: \sum_{(i,o)} \sum_t penalty_pos(i, o, t) \cdot \ + slack_pos(i, o, t) + penalty_neg(i, o, t) \cdot \ + slack_neg(i, o, t) """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -124,7 +144,7 @@ def _create(self, group=None): initialize=[(g[0], g[1]) for g in group if g[2].integer]) - self.PENALTY_FLOWS = Set( + self.SCHEDULE_FLOWS = Set( initialize=[(g[0], g[1]) for g in group if g[2].schedule[0] is not None] ) # ######################### Variables ################################ @@ -138,10 +158,10 @@ def _create(self, group=None): self.integer_flow = Var(self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers) - self.slack_pos = Var(self.PENALTY_FLOWS, + self.slack_pos = Var(self.SCHEDULE_FLOWS, m.TIMESTEPS, within=NonNegativeReals) - self.slack_neg = Var(self.PENALTY_FLOWS, + self.slack_neg = Var(self.SCHEDULE_FLOWS, m.TIMESTEPS, within=NonNegativeReals) # set upper bound of gradient variable @@ -224,7 +244,7 @@ def _integer_flow_rule(block, i, o, t): rule=_integer_flow_rule) def _schedule_rule(model): - for inp, out in self.PENALTY_FLOWS: + for inp, out in self.SCHEDULE_FLOWS: for ts in m.TIMESTEPS: if m.flows[inp, out].schedule[ts] is not None: lhs = (m.flow[inp, out, ts] + self.slack_pos[inp, out, ts] - @@ -233,7 +253,7 @@ def _schedule_rule(model): self.schedule_constr.add((inp, out, ts), lhs == rhs) self.schedule_constr = Constraint( - self.PENALTY_FLOWS, m.TIMESTEPS, noruleinit=True) + self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True) self.schedule_build = BuildAction( rule=_schedule_rule) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 2c3615026..130510f23 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -121,13 +121,20 @@ class Flow(on.Edge): Schedule for the flow. Flow has to follow the schedule, otherwise a penalty term will be activated. If array-like, values can be None for flexible, non-fixed flow during the certain timestep. Used in - combination with the :attr:`penalty`. - penalty : numeric - The penalty parameter of the penalty term describes the costs - associated with one unit of the flow when not following the schedule. + combination with :attr:`penalty_pos` and :attr:`penalty_neg`. + penalty_pos : numeric (sequence or scalar) + A penalty parameter of the penalty term which describes the costs + associated with one unit of the flow when flow exceeds the schedule. If this is set the costs will be added to the objective expression of the optimization problem. Used in combination with the - :attr:`schedule`. + :attr:`schedule` and :attr:`penalty_neg` + penalty_neg: numeric (sequence or scalar) + A penalty parameter of the penalty term which describes the costs + associated with one unit of the flow when flow has a deficit compared + to the schedule. If this is set the costs will be added to the + objective expression of the optimization problem. Used in combination + with the :attr:`schedule` and :attr:`penalty_pos` + Notes ----- The following sets, variables, constraints and objective parts are created From ea2e2ff8b9fdff6c7b436d1d54bfcde215280336 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 13:48:33 +0100 Subject: [PATCH 05/45] Added flexible schedule Now it`s possible to fix the flow to a schedule with possible None values. If schedule is None for the certain timestep, flow will freely be calculated without any penalty constraints. --- oemof/solph/blocks.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 90aeffa89..7475cbb22 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -145,7 +145,11 @@ def _create(self, group=None): if g[2].integer]) self.SCHEDULE_FLOWS = Set( - initialize=[(g[0], g[1]) for g in group if g[2].schedule[0] is not None] + initialize=[(g[0], g[1]) for g in group if ( + len(g[2].schedule) != 0 or + (len(g[2].schedule) == 0 and + g[2].schedule[0] is not None) + )] ) # ######################### Variables ################################ @@ -285,7 +289,12 @@ def _objective_expression(self): m.flows[i, o].negative_gradient[ 'costs']) - if m.flows[i, o].schedule[0] is not None: + schedule = m.flows[i, o].schedule + if ( + len(schedule) > 1 or + (len(schedule) == 0 and + schedule[0] is not None) + ): for t in m.TIMESTEPS: penalty_costs += (self.slack_pos[i, o, t] * m.flows[i, o].penalty_pos[t]) From 78fceff1b9dbdd48a875ab04f790c1ec6d82b2ae Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 14:14:45 +0100 Subject: [PATCH 06/45] Added new `whatsnew` version file The implementation for penalty terms for scheduled flows is finished. * Maybe set a reasonable default value for penalty terms (penalty_pos and penalty_neg), but this acquires mathematical research. --- doc/whatsnew/v0-3-3.rst | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 doc/whatsnew/v0-3-3.rst diff --git a/doc/whatsnew/v0-3-3.rst b/doc/whatsnew/v0-3-3.rst new file mode 100644 index 000000000..9b80c537e --- /dev/null +++ b/doc/whatsnew/v0-3-3.rst @@ -0,0 +1,49 @@ +v0.3.3 (??, ??) ++++++++++++++++++++++++++++ + + +API changes +########### + +* something + +New features +############ + + * It is now possible to determine a schedule for a flow. Flow will be pushed + to the schedule, if possible. + +New components +############## + +* something + +Documentation +############# + +* something + +Known issues +############ + +* something + +Bug fixes +######### + +* something + +Testing +####### + +* something + +Other changes +############# + +* something + +Contributors +############ + +* Caterina Köhl \ No newline at end of file From 8bd7dda7f744674da00d8cdc245d22bf6f3fcf33 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 14:42:01 +0100 Subject: [PATCH 07/45] Fixed if statement according to Style Guide --- oemof/solph/network.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 130510f23..e204826b0 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -207,8 +207,11 @@ def __init__(self, **kwargs): if self.investment and self.nonconvex: raise ValueError("Investment flows cannot be combined with " + "nonconvex flows!") - if (len(self.schedule) != 0) and not \ - (self.penalty_pos[0] and self.penalty_neg[0]): + if ( + len(self.schedule) != 0 and + ((len(self.penalty_pos) == 0 and self.penalty_pos[0]) or + (len(self.penalty_neg) == 0 and self.penalty_neg[0])) + ): raise ValueError("The penalty and schedule attribute need " "to be used in combination. \n Please set " "the schedule attribute of the flow.") From 650f9ece63236de0f83f7f5a8a8c3744ba45625e Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 14:47:03 +0100 Subject: [PATCH 08/45] Fixed boolean logic mistake --- oemof/solph/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index e204826b0..a1c8351aa 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -209,8 +209,8 @@ def __init__(self, **kwargs): "nonconvex flows!") if ( len(self.schedule) != 0 and - ((len(self.penalty_pos) == 0 and self.penalty_pos[0]) or - (len(self.penalty_neg) == 0 and self.penalty_neg[0])) + ((len(self.penalty_pos) == 0 and not self.penalty_pos[0]) or + (len(self.penalty_neg) == 0 and not self.penalty_neg[0])) ): raise ValueError("The penalty and schedule attribute need " "to be used in combination. \n Please set " From e264e5a3ef9e57089390d3e0fe895ce380ee1d0f Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 15:06:05 +0100 Subject: [PATCH 09/45] Clearified docstring for penalty parameters --- oemof/solph/network.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index a1c8351aa..c68253fd3 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -124,16 +124,16 @@ class Flow(on.Edge): combination with :attr:`penalty_pos` and :attr:`penalty_neg`. penalty_pos : numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs - associated with one unit of the flow when flow exceeds the schedule. - If this is set the costs will be added to the objective expression - of the optimization problem. Used in combination with the + associated with one unit of the exceeded flow when flow exceeds the + schedule. If this is set the costs will be added to the objective + expression of the optimization problem. Used in combination with the :attr:`schedule` and :attr:`penalty_neg` penalty_neg: numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs - associated with one unit of the flow when flow has a deficit compared - to the schedule. If this is set the costs will be added to the - objective expression of the optimization problem. Used in combination - with the :attr:`schedule` and :attr:`penalty_pos` + associated with one unit of deficit of the flow compared to the + schedule. If this is set the costs will be added to the objective + expression of the optimization problem. Used in combination with + the :attr:`schedule` and :attr:`penalty_pos` Notes ----- From a9ca25a5d13e6b28c903a7eca516b01b54408ed2 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 15:35:02 +0100 Subject: [PATCH 10/45] Clarified doc for _pos _neg parameters/variables --- oemof/solph/blocks.py | 6 +++--- oemof/solph/network.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 7475cbb22..9ec996626 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -29,11 +29,11 @@ class Flow(SimpleBlock): indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS. slack_pos : - Difference of a flow in consecutive timesteps if flow exceeds - schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. + Difference of a flow to schedule in consecutive timesteps if flow + has deficit to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. slack_neg : - Deficit of flow compared to schedule. Indexed by SCHEDULE_FLOWS, + Excees of flow compared to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. **The following sets are created:** (-> see basic sets at diff --git a/oemof/solph/network.py b/oemof/solph/network.py index c68253fd3..224a35342 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -124,13 +124,13 @@ class Flow(on.Edge): combination with :attr:`penalty_pos` and :attr:`penalty_neg`. penalty_pos : numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs - associated with one unit of the exceeded flow when flow exceeds the + associated with one unit of deficit of the flow compared to the schedule. If this is set the costs will be added to the objective expression of the optimization problem. Used in combination with the :attr:`schedule` and :attr:`penalty_neg` penalty_neg: numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs - associated with one unit of deficit of the flow compared to the + associated with one unit of the exceeded flow when flow exceeds the schedule. If this is set the costs will be added to the objective expression of the optimization problem. Used in combination with the :attr:`schedule` and :attr:`penalty_pos` From 46cc20193ab49344aadde0dd9309699627fcc8fb Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 16:45:57 +0100 Subject: [PATCH 11/45] Improved code to pep8 standards --- oemof/solph/blocks.py | 34 +++++++++++++++------------------- oemof/solph/models.py | 2 +- oemof/solph/network.py | 9 ++++----- tests/test_processing.py | 3 +-- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 9ec996626..1340878c6 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -29,7 +29,7 @@ class Flow(SimpleBlock): indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS. slack_pos : - Difference of a flow to schedule in consecutive timesteps if flow + Difference of a flow to schedule in consecutive timesteps if flow has deficit to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. slack_neg : @@ -92,7 +92,7 @@ class Flow(SimpleBlock): If :attr:`variable_costs` are set by the user: .. math:: - \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t) + \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t) The expression can be accessed by :attr:`om.Flow.variable_costs` and their value after optimization by :meth:`om.Flow.variable_costs()` . @@ -146,11 +146,9 @@ def _create(self, group=None): self.SCHEDULE_FLOWS = Set( initialize=[(g[0], g[1]) for g in group if ( - len(g[2].schedule) != 0 or - (len(g[2].schedule) == 0 and - g[2].schedule[0] is not None) - )] - ) + len(g[2].schedule) != 0 or + (len(g[2].schedule) == 0 and + g[2].schedule[0] is not None))]) # ######################### Variables ################################ self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, @@ -161,12 +159,12 @@ def _create(self, group=None): self.integer_flow = Var(self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers) - + self.slack_pos = Var(self.SCHEDULE_FLOWS, - m.TIMESTEPS, within=NonNegativeReals) + m.TIMESTEPS, within=NonNegativeReals) self.slack_neg = Var(self.SCHEDULE_FLOWS, - m.TIMESTEPS, within=NonNegativeReals) + m.TIMESTEPS, within=NonNegativeReals) # set upper bound of gradient variable for i, o, f in group: @@ -252,10 +250,10 @@ def _schedule_rule(model): for ts in m.TIMESTEPS: if m.flows[inp, out].schedule[ts] is not None: lhs = (m.flow[inp, out, ts] + self.slack_pos[inp, out, ts] - - self.slack_neg[inp, out, ts]) + self.slack_neg[inp, out, ts]) rhs = m.flows[inp, out].schedule[ts] self.schedule_constr.add((inp, out, ts), - lhs == rhs) + lhs == rhs) self.schedule_constr = Constraint( self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True) self.schedule_build = BuildAction( @@ -290,15 +288,13 @@ def _objective_expression(self): 'costs']) schedule = m.flows[i, o].schedule - if ( - len(schedule) > 1 or - (len(schedule) == 0 and - schedule[0] is not None) - ): + if (len(schedule) > 1 or + (len(schedule) == 0 and + schedule[0] is not None)): for t in m.TIMESTEPS: - penalty_costs += (self.slack_pos[i, o, t] * + penalty_costs += (self.slack_pos[i, o, t] * m.flows[i, o].penalty_pos[t]) - penalty_costs += (self.slack_neg[i, o, t] * + penalty_costs += (self.slack_neg[i, o, t] * m.flows[i, o].penalty_neg[t]) return variable_costs + gradient_costs + penalty_costs diff --git a/oemof/solph/models.py b/oemof/solph/models.py index 9b53f260d..637d8de1c 100644 --- a/oemof/solph/models.py +++ b/oemof/solph/models.py @@ -31,7 +31,7 @@ class BaseModel(po.ConcreteModel): Defaults to :const:`Model.CONSTRAINTS` objective_weighting : array like (optional) Weights used for temporal objective function - expressions. If nothing is passed `timeincrement` will be used which + expressions. If nothing is passed `timeincrement` will be used which is calculated from the freq length of the energy system timeindex . auto_construct : boolean If this value is true, the set, variables, constraints, etc. are added, diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 224a35342..0fd34cc9e 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -174,8 +174,8 @@ def __init__(self, **kwargs): scalars = ['nominal_value', 'summed_max', 'summed_min', 'investment', 'nonconvex', 'integer', 'fixed'] - sequences = ['actual_value', 'variable_costs', 'min', 'max', 'schedule', - 'penalty_neg', 'penalty_pos'] + sequences = ['actual_value', 'variable_costs', 'min', 'max', + 'schedule', 'penalty_neg', 'penalty_pos'] dictionaries = ['positive_gradient', 'negative_gradient'] defaults = {'fixed': False, 'min': 0, 'max': 1, 'variable_costs': 0, 'positive_gradient': {'ub': None, 'costs': 0}, @@ -208,10 +208,9 @@ def __init__(self, **kwargs): raise ValueError("Investment flows cannot be combined with " + "nonconvex flows!") if ( - len(self.schedule) != 0 and + len(self.schedule) != 0 and ((len(self.penalty_pos) == 0 and not self.penalty_pos[0]) or - (len(self.penalty_neg) == 0 and not self.penalty_neg[0])) - ): + (len(self.penalty_neg) == 0 and not self.penalty_neg[0]))): raise ValueError("The penalty and schedule attribute need " "to be used in combination. \n Please set " "the schedule attribute of the flow.") diff --git a/tests/test_processing.py b/tests/test_processing.py index 2fdb65866..6f072dce9 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -108,8 +108,7 @@ def test_flows_with_none_exclusion(self): assert_frame_equal( param_results[(b_el2, demand)]['sequences'], pandas.DataFrame( - {'actual_value': self.demand_values, - } + {'actual_value': self.demand_values} ), check_like=True ) From c7882a97d7d2e6c888b686a34c9a3ca95e3c04dd Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 16:47:33 +0100 Subject: [PATCH 12/45] Improved code to pep8 standards --- oemof/solph/blocks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 1340878c6..7f170070c 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -249,7 +249,8 @@ def _schedule_rule(model): for inp, out in self.SCHEDULE_FLOWS: for ts in m.TIMESTEPS: if m.flows[inp, out].schedule[ts] is not None: - lhs = (m.flow[inp, out, ts] + self.slack_pos[inp, out, ts] - + lhs = (m.flow[inp, out, ts] + + self.slack_pos[inp, out, ts] - self.slack_neg[inp, out, ts]) rhs = m.flows[inp, out].schedule[ts] self.schedule_constr.add((inp, out, ts), From d5e5f6cf8140c159503a1fab69e70746f0370cdb Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 18:01:10 +0100 Subject: [PATCH 13/45] Added constraint test for scheduled flows --- tests/constraint_tests.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index 6546997dd..c2debee8a 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -676,4 +676,22 @@ def test_dsm_module_interval(self): method='interval', shift_interval=2 ) - self.compare_lp_files('dsm_module_interval.lp') \ No newline at end of file + self.compare_lp_files('dsm_module_interval.lp') + + def test_flow_schedule(self): + """Contraint test of scheduled flows + """ + b_gas = solph.Bus(label='bus_gas') + b_th = solph.Bus(label='bus_th_penalty') + + schedule = [None, 300, 50] + solph.Transformer( + label="boiler_penalty", + inputs={b_gas: solph.Flow()}, + outputs={b_th: solph.Flow(nominal_value=200, variable_costs=0, + penalty_pos = [0,999,999], + penalty_neg = 999, + schedule=schedule)}, + conversion_factors={b_th: 1} + ) + self.compare_lp_files('flow_schedule.lp') \ No newline at end of file From 2d897750959a4bc6c17d6a6484cfd1c2ad2ffd68 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 18:02:07 +0100 Subject: [PATCH 14/45] Added flow_scheduled.lp file for testing --- tests/lp_files/flow_schedule.lp | 79 +++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/lp_files/flow_schedule.lp diff --git a/tests/lp_files/flow_schedule.lp b/tests/lp_files/flow_schedule.lp new file mode 100644 index 000000000..dd2da45d5 --- /dev/null +++ b/tests/lp_files/flow_schedule.lp @@ -0,0 +1,79 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++999 Flow_slack_neg(boiler_penalty_bus_th_penalty_0) ++999 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) ++999 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) ++999 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) ++999 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) + +s.t. + +c_e_Bus_balance(bus_gas_0)_: ++1 flow(bus_gas_boiler_penalty_0) += 0 + +c_e_Bus_balance(bus_gas_1)_: ++1 flow(bus_gas_boiler_penalty_1) += 0 + +c_e_Bus_balance(bus_gas_2)_: ++1 flow(bus_gas_boiler_penalty_2) += 0 + +c_e_Bus_balance(bus_th_penalty_0)_: ++1 flow(boiler_penalty_bus_th_penalty_0) += 0 + +c_e_Bus_balance(bus_th_penalty_1)_: ++1 flow(boiler_penalty_bus_th_penalty_1) += 0 + +c_e_Bus_balance(bus_th_penalty_2)_: ++1 flow(boiler_penalty_bus_th_penalty_2) += 0 + +c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_0)_: +-1 flow(boiler_penalty_bus_th_penalty_0) ++1 flow(bus_gas_boiler_penalty_0) += 0 + +c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_1)_: +-1 flow(boiler_penalty_bus_th_penalty_1) ++1 flow(bus_gas_boiler_penalty_1) += 0 + +c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_2)_: +-1 flow(boiler_penalty_bus_th_penalty_2) ++1 flow(bus_gas_boiler_penalty_2) += 0 + +c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_1)_: +-1 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) ++1 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) ++1 flow(boiler_penalty_bus_th_penalty_1) += 300 + +c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_2)_: +-1 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) ++1 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) ++1 flow(boiler_penalty_bus_th_penalty_2) += 50 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(boiler_penalty_bus_th_penalty_0) <= 200 + 0 <= flow(boiler_penalty_bus_th_penalty_1) <= 200 + 0 <= flow(boiler_penalty_bus_th_penalty_2) <= 200 + 0 <= flow(bus_gas_boiler_penalty_0) <= +inf + 0 <= flow(bus_gas_boiler_penalty_1) <= +inf + 0 <= flow(bus_gas_boiler_penalty_2) <= +inf + 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf + 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf + 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf +end \ No newline at end of file From 151576da9de5b79611c751a736ec463054bb9b84 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Tue, 10 Dec 2019 18:16:10 +0100 Subject: [PATCH 15/45] Improve test for flow schedule --- tests/constraint_tests.py | 14 +++++++------- tests/lp_files/flow_schedule.lp | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index c2debee8a..06b75c1c0 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -340,7 +340,7 @@ def test_storage_minimum_invest(self): ep_costs=145, minimum=100, maximum=200)) self.compare_lp_files('storage_invest_minimum.lp') - + def test_storage_unbalanced(self): """Testing a unbalanced storage (e.g. battery).""" bel = solph.Bus(label='electricityBus') @@ -353,9 +353,9 @@ def test_storage_unbalanced(self): initial_storage_level=None, balanced=False, invest_relation_input_capacity=1, - invest_relation_output_capacity=1) + invest_relation_output_capacity=1) self.compare_lp_files('storage_unbalanced.lp') - + def test_storage_invest_unbalanced(self): """Testing a unbalanced storage (e.g. battery).""" bel = solph.Bus(label='electricityBus') @@ -689,9 +689,9 @@ def test_flow_schedule(self): label="boiler_penalty", inputs={b_gas: solph.Flow()}, outputs={b_th: solph.Flow(nominal_value=200, variable_costs=0, - penalty_pos = [0,999,999], - penalty_neg = 999, - schedule=schedule)}, + penalty_pos=[0, 800, 900], + penalty_neg=999, + schedule=schedule)}, conversion_factors={b_th: 1} ) - self.compare_lp_files('flow_schedule.lp') \ No newline at end of file + self.compare_lp_files('flow_schedule.lp') diff --git a/tests/lp_files/flow_schedule.lp b/tests/lp_files/flow_schedule.lp index dd2da45d5..e033ce63d 100644 --- a/tests/lp_files/flow_schedule.lp +++ b/tests/lp_files/flow_schedule.lp @@ -5,8 +5,8 @@ objective: +999 Flow_slack_neg(boiler_penalty_bus_th_penalty_0) +999 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) +999 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) -+999 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) -+999 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) ++800 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) ++900 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) s.t. From 31397a1fd5a51393168c20d64dc4f26e73758766 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 13 Jan 2020 12:05:35 +0100 Subject: [PATCH 16/45] Rename slack_neg and slack_pos to schedule_slack For clarifying, variable names have been changed. --- oemof/solph/blocks.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 16f7b3a5f..8f2412e2c 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -28,11 +28,11 @@ class Flow(SimpleBlock): Difference of a flow in consecutive timesteps if flow is increased indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS. - slack_pos : + schedule_slack_pos : Difference of a flow to schedule in consecutive timesteps if flow has deficit to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. - slack_neg : + schedule_slack_neg : Excees of flow compared to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS. @@ -83,8 +83,8 @@ class Flow(SimpleBlock): \forall t \in \textrm{TIMESTEPS}. Schedule constraint :attr:`om.Flow.positive_gradient_constr[i, o]`: - .. math:: flow(i, o, t) + slack_pos(i, o, t) - \ - slack_neg(i, o, t) = schedule(i, o, t) \\ + .. math:: flow(i, o, t) + schedule_slack_pos(i, o, t) - \ + schedule_slack_neg(i, o, t) = schedule(i, o, t) \\ \forall (i, o) \in \textrm{SCHEDULE\_FLOWS}, \\ \forall t \in \textrm{TIMESTEPS}. @@ -100,8 +100,8 @@ class Flow(SimpleBlock): If :attr:`schedule`, :attr:`penalty_pos` and :attr:`penalty_neg` are set by the user: .. math:: \sum_{(i,o)} \sum_t penalty_pos(i, o, t) \cdot \ - slack_pos(i, o, t) + penalty_neg(i, o, t) \cdot \ - slack_neg(i, o, t) + schedule_slack_pos(i, o, t) + penalty_neg(i, o, t) \cdot \ + schedule_slack_neg(i, o, t) """ def __init__(self, *args, **kwargs): @@ -160,10 +160,10 @@ def _create(self, group=None): self.integer_flow = Var(self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers) - self.slack_pos = Var(self.SCHEDULE_FLOWS, + self.schedule_slack_pos = Var(self.SCHEDULE_FLOWS, m.TIMESTEPS, within=NonNegativeReals) - self.slack_neg = Var(self.SCHEDULE_FLOWS, + self.schedule_slack_neg = Var(self.SCHEDULE_FLOWS, m.TIMESTEPS, within=NonNegativeReals) # set upper bound of gradient variable @@ -250,11 +250,13 @@ def _schedule_rule(model): for ts in m.TIMESTEPS: if m.flows[inp, out].schedule[ts] is not None: lhs = (m.flow[inp, out, ts] + - self.slack_pos[inp, out, ts] - - self.slack_neg[inp, out, ts]) + self.schedule_slack_pos[inp, out, ts] - + self.schedule_slack_neg[inp, out, ts]) rhs = m.flows[inp, out].schedule[ts] self.schedule_constr.add((inp, out, ts), lhs == rhs) + else: + print("UHU") self.schedule_constr = Constraint( self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True) self.schedule_build = BuildAction( @@ -293,9 +295,9 @@ def _objective_expression(self): (len(schedule) == 0 and schedule[0] is not None)): for t in m.TIMESTEPS: - penalty_costs += (self.slack_pos[i, o, t] * + penalty_costs += (self.schedule_slack_pos[i, o, t] * m.flows[i, o].penalty_pos[t]) - penalty_costs += (self.slack_neg[i, o, t] * + penalty_costs += (self.schedule_slack_neg[i, o, t] * m.flows[i, o].penalty_neg[t]) return variable_costs + gradient_costs + penalty_costs From bd43f3e6075e6adaf1b12018b061a2495de15ff4 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 13 Jan 2020 14:29:12 +0100 Subject: [PATCH 17/45] Deleted unnecessary print statement --- oemof/solph/blocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 8f2412e2c..297f874e7 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -256,7 +256,6 @@ def _schedule_rule(model): self.schedule_constr.add((inp, out, ts), lhs == rhs) else: - print("UHU") self.schedule_constr = Constraint( self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True) self.schedule_build = BuildAction( From a7709afa290b9582d9e7abbd9d436caa1bdf91c9 Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 13 Jan 2020 14:43:53 +0100 Subject: [PATCH 18/45] Deleted unnecessary else statement --- oemof/solph/blocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index 297f874e7..bc88f03e5 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -255,7 +255,6 @@ def _schedule_rule(model): rhs = m.flows[inp, out].schedule[ts] self.schedule_constr.add((inp, out, ts), lhs == rhs) - else: self.schedule_constr = Constraint( self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True) self.schedule_build = BuildAction( From cbabaa757d2ca4b321370832d939ddbcb4cc2d5a Mon Sep 17 00:00:00 2001 From: Consolinno Date: Mon, 13 Jan 2020 14:53:04 +0100 Subject: [PATCH 19/45] Replaced lp file due to new variable names --- tests/lp_files/flow_schedule.lp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/lp_files/flow_schedule.lp b/tests/lp_files/flow_schedule.lp index e033ce63d..f748fa345 100644 --- a/tests/lp_files/flow_schedule.lp +++ b/tests/lp_files/flow_schedule.lp @@ -2,11 +2,11 @@ min objective: -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_0) -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) -+800 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) -+900 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) ++800 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) ++900 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) s.t. @@ -50,14 +50,14 @@ c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_2)_: = 0 c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_1)_: --1 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) -+1 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) +-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) ++1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) +1 flow(boiler_penalty_bus_th_penalty_1) = 300 c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_2)_: --1 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) -+1 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) +-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) ++1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) +1 flow(boiler_penalty_bus_th_penalty_2) = 50 @@ -71,9 +71,9 @@ bounds 0 <= flow(bus_gas_boiler_penalty_0) <= +inf 0 <= flow(bus_gas_boiler_penalty_1) <= +inf 0 <= flow(bus_gas_boiler_penalty_2) <= +inf - 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf - 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf + 0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf end \ No newline at end of file From f71ae4a9a657901987269300a4fecb25f63a6ba3 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Mon, 13 Jan 2020 14:53:04 +0100 Subject: [PATCH 20/45] Replaced lp file due to new variable names New lp file was created due to change in variable names. Also author and email adress were updated. --- tests/lp_files/flow_schedule.lp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/lp_files/flow_schedule.lp b/tests/lp_files/flow_schedule.lp index e033ce63d..f748fa345 100644 --- a/tests/lp_files/flow_schedule.lp +++ b/tests/lp_files/flow_schedule.lp @@ -2,11 +2,11 @@ min objective: -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_0) -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) -+999 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) -+800 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) -+900 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) ++999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) ++800 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) ++900 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) s.t. @@ -50,14 +50,14 @@ c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_2)_: = 0 c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_1)_: --1 Flow_slack_neg(boiler_penalty_bus_th_penalty_1) -+1 Flow_slack_pos(boiler_penalty_bus_th_penalty_1) +-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) ++1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) +1 flow(boiler_penalty_bus_th_penalty_1) = 300 c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_2)_: --1 Flow_slack_neg(boiler_penalty_bus_th_penalty_2) -+1 Flow_slack_pos(boiler_penalty_bus_th_penalty_2) +-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) ++1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) +1 flow(boiler_penalty_bus_th_penalty_2) = 50 @@ -71,9 +71,9 @@ bounds 0 <= flow(bus_gas_boiler_penalty_0) <= +inf 0 <= flow(bus_gas_boiler_penalty_1) <= +inf 0 <= flow(bus_gas_boiler_penalty_2) <= +inf - 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf - 0 <= Flow_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf - 0 <= Flow_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf + 0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf + 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf end \ No newline at end of file From 4a7106f1e9aa3749bb21add685fdee8dd297cdb1 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Mon, 10 Feb 2020 11:49:07 +0100 Subject: [PATCH 21/45] Renamed added parameters Deleted unnecessary parameters in default list for flows and renamed parameters for schedule to more ambigous names: penalty_neg/pos -> schedule_cost_neg/pos --- oemof/solph/blocks.py | 14 +++++++------- oemof/solph/network.py | 21 ++++++++++++--------- tests/constraint_tests.py | 4 ++-- tests/test_processing.py | 8 ++++---- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index bc88f03e5..c9da5417f 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -97,10 +97,10 @@ class Flow(SimpleBlock): The expression can be accessed by :attr:`om.Flow.variable_costs` and their value after optimization by :meth:`om.Flow.variable_costs()` . - If :attr:`schedule`, :attr:`penalty_pos` and :attr:`penalty_neg` are + If :attr:`schedule`, :attr:`schedule_cost_pos` and :attr:`schedule_cost_neg` are set by the user: - .. math:: \sum_{(i,o)} \sum_t penalty_pos(i, o, t) \cdot \ - schedule_slack_pos(i, o, t) + penalty_neg(i, o, t) \cdot \ + .. math:: \sum_{(i,o)} \sum_t schedule_cost_pos(i, o, t) \cdot \ + schedule_slack_pos(i, o, t) + schedule_cost_neg(i, o, t) \cdot \ schedule_slack_neg(i, o, t) """ @@ -161,10 +161,10 @@ def _create(self, group=None): m.TIMESTEPS, within=NonNegativeIntegers) self.schedule_slack_pos = Var(self.SCHEDULE_FLOWS, - m.TIMESTEPS, within=NonNegativeReals) + m.TIMESTEPS, within=NonNegativeReals) self.schedule_slack_neg = Var(self.SCHEDULE_FLOWS, - m.TIMESTEPS, within=NonNegativeReals) + m.TIMESTEPS, within=NonNegativeReals) # set upper bound of gradient variable for i, o, f in group: @@ -294,9 +294,9 @@ def _objective_expression(self): schedule[0] is not None)): for t in m.TIMESTEPS: penalty_costs += (self.schedule_slack_pos[i, o, t] * - m.flows[i, o].penalty_pos[t]) + m.flows[i, o].schedule_cost_pos[t]) penalty_costs += (self.schedule_slack_neg[i, o, t] * - m.flows[i, o].penalty_neg[t]) + m.flows[i, o].schedule_cost_neg[t]) return variable_costs + gradient_costs + penalty_costs diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 691aebe44..1905fe9a4 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -121,19 +121,20 @@ class Flow(on.Edge): Schedule for the flow. Flow has to follow the schedule, otherwise a penalty term will be activated. If array-like, values can be None for flexible, non-fixed flow during the certain timestep. Used in - combination with :attr:`penalty_pos` and :attr:`penalty_neg`. - penalty_pos : numeric (sequence or scalar) + combination with :attr:`schedule_cost_pos` and + :attr:`schedule_cost_neg`. + schedule_cost_pos : numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs associated with one unit of deficit of the flow compared to the schedule. If this is set the costs will be added to the objective expression of the optimization problem. Used in combination with the - :attr:`schedule` and :attr:`penalty_neg` - penalty_neg: numeric (sequence or scalar) + :attr:`schedule` and :attr:`schedule_cost_neg` + schedule_cost_neg: numeric (sequence or scalar) A penalty parameter of the penalty term which describes the costs associated with one unit of the exceeded flow when flow exceeds the schedule. If this is set the costs will be added to the objective expression of the optimization problem. Used in combination with - the :attr:`schedule` and :attr:`penalty_pos` + the :attr:`schedule` and :attr:`schedule_cost_pos` Notes ----- @@ -175,12 +176,12 @@ def __init__(self, **kwargs): scalars = ['nominal_value', 'summed_max', 'summed_min', 'investment', 'nonconvex', 'integer', 'fixed'] sequences = ['actual_value', 'variable_costs', 'min', 'max', - 'schedule', 'penalty_neg', 'penalty_pos'] + 'schedule', 'schedule_cost_neg', 'schedule_cost_pos'] dictionaries = ['positive_gradient', 'negative_gradient'] defaults = {'fixed': False, 'min': 0, 'max': 1, 'variable_costs': 0, 'positive_gradient': {'ub': None, 'costs': 0}, 'negative_gradient': {'ub': None, 'costs': 0}, - 'penalty_neg': 0, 'penalty_pos': 0, 'schedule': None + 'schedule_cost_neg': 0, 'schedule_cost_pos': 0, } keys = [k for k in kwargs if k != 'label'] @@ -209,8 +210,10 @@ def __init__(self, **kwargs): "nonconvex flows!") if ( len(self.schedule) != 0 and - ((len(self.penalty_pos) == 0 and not self.penalty_pos[0]) or - (len(self.penalty_neg) == 0 and not self.penalty_neg[0]))): + ((len(self.schedule_cost_pos) == 0 and + not self.schedule_cost_pos[0]) or + (len(self.schedule_cost_neg) == 0 and + not self.schedule_cost_neg[0]))): raise ValueError("The penalty and schedule attribute need " "to be used in combination. \n Please set " "the schedule attribute of the flow.") diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index a74fbefc2..b281848d7 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -692,8 +692,8 @@ def test_flow_schedule(self): label="boiler_penalty", inputs={b_gas: solph.Flow()}, outputs={b_th: solph.Flow(nominal_value=200, variable_costs=0, - penalty_pos=[0, 800, 900], - penalty_neg=999, + schedule_cost_pos=[0, 800, 900], + schedule_cost_neg=999, schedule=schedule)}, conversion_factors={b_th: 1} ) diff --git a/tests/test_processing.py b/tests/test_processing.py index d2e6bac54..fa6c1683d 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -99,8 +99,8 @@ def test_flows_with_none_exclusion(self): 'positive_gradient_costs': 0, 'variable_costs': 0, 'label': str(b_el2.outputs[demand].label), - 'penalty_pos': 0, - 'penalty_neg': 0, + 'schedule_cost_pos': 0, + 'schedule_cost_neg': 0, } ).sort_index() ) @@ -134,8 +134,8 @@ def test_flows_without_none_exclusion(self): 'flow': None, 'values': None, 'label': str(b_el2.outputs[demand].label), - 'penalty_pos': 0, - 'penalty_neg': 0, + 'schedule_cost_pos': 0, + 'schedule_cost_neg': 0, 'schedule': None, } assert_series_equal( From c65349c00031c581230e69c22181b65f45c1a975 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Wed, 12 Feb 2020 12:58:46 +0100 Subject: [PATCH 22/45] add csv --- test2_generic_chp.csv | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test2_generic_chp.csv diff --git a/test2_generic_chp.csv b/test2_generic_chp.csv new file mode 100644 index 000000000..424fee774 --- /dev/null +++ b/test2_generic_chp.csv @@ -0,0 +1,8 @@ +timestep,demand_th,price_el +1,0.88,-50 +2,0.74,100 +3,0.13,50 +4,0.99,50 +5,0.85,-50 +6,0.996,50 +7,0.47,50 \ No newline at end of file From fab3209418f32328f182324566f846b5ead3a8b6 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Wed, 12 Feb 2020 12:59:10 +0100 Subject: [PATCH 23/45] renamed csv file --- test_schedule_flow.csv | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test_schedule_flow.csv diff --git a/test_schedule_flow.csv b/test_schedule_flow.csv new file mode 100644 index 000000000..424fee774 --- /dev/null +++ b/test_schedule_flow.csv @@ -0,0 +1,8 @@ +timestep,demand_th,price_el +1,0.88,-50 +2,0.74,100 +3,0.13,50 +4,0.99,50 +5,0.85,-50 +6,0.996,50 +7,0.47,50 \ No newline at end of file From 810ea702c4fb12172f5b8d27ad0389aa93aa8cb8 Mon Sep 17 00:00:00 2001 From: uvchik Date: Tue, 7 Apr 2020 13:31:03 +0200 Subject: [PATCH 24/45] Merge upstream dev into schedule_for_flows --- .appveyor.yml | 32 + .bumpversion.cfg | 20 + .cookiecutterrc | 69 + .coveragerc | 17 +- .editorconfig | 13 + .gitignore | 86 +- .readthedocs.yml | 10 + .travis.yml | 75 +- AUTHORS.rst | 5 + CHANGELOG.rst | 8 + CONTRIBUTING.md | 1 - CONTRIBUTING.rst | 90 + LICENSE | 20 +- MANIFEST | 2 - MANIFEST.in | 22 + README.rst | 180 +- VERSION | 3 +- ci/appveyor-with-compiler.cmd | 23 + ci/bootstrap.py | 104 ++ ci/requirements.txt | 4 + ci/templates/.appveyor.yml | 52 + ci/templates/.travis.yml | 58 + ci/templates/tox.ini | 112 ++ .../ExtractionTurbine_range_of_operation.svg | 436 ----- doc/about_oemof.rst | 104 -- doc/api.rst | 10 - doc/api/modules.rst | 7 - doc/api/oemof.outputlib.rst | 30 - doc/api/oemof.rst | 55 - doc/api/oemof.tools.rst | 46 - doc/conf.py | 355 ---- doc/developing_oemof.rst | 225 --- doc/index.rst | 33 - doc/oemof_network.rst | 25 - doc/oemof_tools.rst | 50 - doc/using_oemof.rst | 93 - doc/whatsnew/v0-3-3.rst | 49 - doc/whatsnew/v0-4-0.rst | 48 - .../ExtractionTurbine_range_of_operation.svg | 548 ++++++ {doc => docs}/_files/GenericCHP.svg | 0 .../_files/OffsetTransformer_efficiency.svg | 0 .../OffsetTransformer_power_relation.svg | 0 .../_files/Plot_delay_2013-01-01.svg | 0 {doc => docs}/_files/example_figures.png | Bin {doc => docs}/_files/example_network.svg | 0 {doc => docs}/_files/example_variable_chp.svg | 0 {doc => docs}/_files/framework_concept.svg | 0 .../nonconvex_invest_investcosts_power.svg | 1604 +++++++++++++++++ .../nonconvex_invest_specific_costs.svg | 1205 +++++++++++++ {doc => docs}/_files/oemof_solph_example.svg | 0 {doc => docs}/_files/pahesmf_init.py | 0 {doc => docs}/_files/variable_chp_plot.svg | 0 docs/authors.rst | 1 + doc/whats_new.rst => docs/changelog.rst | 5 +- docs/conf.py | 47 + docs/contributing.rst | 1 + {doc => docs}/docutils.conf | 0 {doc => docs}/getting_started.rst | 18 +- docs/index.rst | 25 + {doc => docs}/installation_and_setup.rst | 74 +- docs/old_usage.rst | 67 + docs/readme.rst | 1 + docs/reference/index.rst | 7 + {doc/api => docs/reference}/oemof.solph.rst | 29 +- docs/requirements.txt | 2 + doc/oemof_outputlib.rst => docs/results.rst | 18 +- docs/spelling_wordlist.txt | 11 + doc/oemof_solph.rst => docs/usage.rst | 187 +- {doc => docs}/whatsnew/v0-0-1.rst | 0 {doc => docs}/whatsnew/v0-0-2.rst | 18 +- {doc => docs}/whatsnew/v0-0-3.rst | 20 +- {doc => docs}/whatsnew/v0-0-4.rst | 6 +- {doc => docs}/whatsnew/v0-0-5.rst | 10 +- {doc => docs}/whatsnew/v0-0-6.rst | 8 +- {doc => docs}/whatsnew/v0-0-7.rst | 0 {doc => docs}/whatsnew/v0-1-0.rst | 28 +- {doc => docs}/whatsnew/v0-1-1.rst | 8 +- {doc => docs}/whatsnew/v0-1-2.rst | 6 +- {doc => docs}/whatsnew/v0-1-4.rst | 4 +- {doc => docs}/whatsnew/v0-2-0.rst | 26 +- {doc => docs}/whatsnew/v0-2-1.rst | 30 +- {doc => docs}/whatsnew/v0-2-2.rst | 16 +- {doc => docs}/whatsnew/v0-2-3.rst | 2 +- {doc => docs}/whatsnew/v0-3-0.rst | 16 +- {doc => docs}/whatsnew/v0-3-1.rst | 0 {doc => docs}/whatsnew/v0-3-2.rst | 2 +- docs/whatsnew/v0-3-3.rst | 51 + docs/whatsnew/v0-4-0.rst | 65 + nose.cfg | 3 - oemof/__init__.py | 4 - oemof/energy_system.py | 231 --- oemof/graph.py | 130 -- oemof/groupings.py | 307 ---- oemof/network.py | 441 ----- oemof/outputlib/__init__.py | 2 - oemof/solph/__init__.py | 9 - oemof/tools/__init__.py | 3 - oemof/tools/economics.py | 68 - oemof/tools/logger.py | 200 -- setup.cfg | 103 ++ setup.py | 126 +- src/oemof/solph/__init__.py | 21 + {oemof => src/oemof}/solph/blocks.py | 335 +++- {oemof => src/oemof}/solph/components.py | 472 +++-- .../oemof/solph}/console_scripts.py | 2 +- {oemof => src/oemof}/solph/constraints.py | 23 +- {oemof => src/oemof}/solph/custom.py | 166 +- {oemof => src/oemof}/solph/groupings.py | 2 +- {oemof/tools => src/oemof/solph}/helpers.py | 38 +- {oemof => src/oemof}/solph/models.py | 14 +- {oemof => src/oemof}/solph/network.py | 68 +- {oemof => src/oemof}/solph/options.py | 66 +- {oemof => src/oemof}/solph/plumbing.py | 19 +- .../oemof/solph}/processing.py | 7 +- {oemof/outputlib => src/oemof/solph}/views.py | 54 +- tests/basic_tests.py | 241 --- tests/constraint_tests.py | 155 +- tests/lp_files/flow_invest_with_offset.lp | 68 + .../flow_invest_with_offset_no_minimum.lp | 67 + tests/lp_files/flow_invest_without_offset.lp | 67 + tests/lp_files/storage_fixed_losses.lp | 66 + .../lp_files/storage_invest_1_fixed_losses.lp | 151 ++ .../lp_files/storage_invest_all_nonconvex.lp | 159 ++ tests/lp_files/storage_invest_with_offset.lp | 162 ++ .../lp_files/storage_invest_without_offset.lp | 161 ++ tests/regression_tests.py | 4 +- tests/run_nose.py | 2 +- tests/solph_tests.py | 20 +- tests/test_components.py | 224 ++- tests/test_console_scripts.py | 3 +- tests/test_constraints_module.py | 43 + tests/test_groupings.py | 6 +- tests/test_models.py | 57 +- tests/test_network_classes.py | 14 +- tests/test_outputlib/__init__.py | 10 +- tests/test_outputlib/test_views.py | 12 +- tests/test_processing.py | 30 +- .../test_connect_invest.py | 64 +- .../test_add_constraints.py | 15 +- .../test_generic_caes/test_generic_caes.py | 46 +- .../test_generic_chp/test_generic_chp.py | 10 +- .../test_solph/test_lopf/test_lopf.py | 39 +- .../test_simple_model/test_simple_dispatch.py | 15 +- .../test_simple_dispatch_one.py | 14 +- .../test_simple_model/test_simple_invest.py | 18 +- .../es_dump_test_2_1dev.oemof | Bin 72333 -> 0 bytes ...2_3dev.oemof => es_dump_test_3_2dev.oemof} | Bin 84189 -> 83718 bytes .../test_invest_storage_regression.py | 10 +- .../test_storage_investment.py | 46 +- .../test_storage_with_tuple_label.py | 12 +- .../test_variable_chp/test_variable_chp.py | 22 +- tests/test_solph_network_classes.py | 4 +- tests/test_warnings.py | 89 + tests/tool_tests.py | 51 - tox.ini | 136 +- 155 files changed, 7712 insertions(+), 4253 deletions(-) create mode 100644 .appveyor.yml create mode 100644 .bumpversion.cfg create mode 100644 .cookiecutterrc create mode 100644 .editorconfig create mode 100644 .readthedocs.yml create mode 100644 AUTHORS.rst create mode 100644 CHANGELOG.rst delete mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING.rst delete mode 100644 MANIFEST create mode 100644 MANIFEST.in create mode 100644 ci/appveyor-with-compiler.cmd create mode 100755 ci/bootstrap.py create mode 100644 ci/requirements.txt create mode 100644 ci/templates/.appveyor.yml create mode 100644 ci/templates/.travis.yml create mode 100644 ci/templates/tox.ini delete mode 100644 doc/_files/ExtractionTurbine_range_of_operation.svg delete mode 100644 doc/about_oemof.rst delete mode 100644 doc/api.rst delete mode 100644 doc/api/modules.rst delete mode 100644 doc/api/oemof.outputlib.rst delete mode 100644 doc/api/oemof.rst delete mode 100644 doc/api/oemof.tools.rst delete mode 100644 doc/conf.py delete mode 100644 doc/developing_oemof.rst delete mode 100644 doc/index.rst delete mode 100644 doc/oemof_network.rst delete mode 100644 doc/oemof_tools.rst delete mode 100644 doc/using_oemof.rst delete mode 100644 doc/whatsnew/v0-3-3.rst delete mode 100644 doc/whatsnew/v0-4-0.rst create mode 100644 docs/_files/ExtractionTurbine_range_of_operation.svg rename {doc => docs}/_files/GenericCHP.svg (100%) rename {doc => docs}/_files/OffsetTransformer_efficiency.svg (100%) rename {doc => docs}/_files/OffsetTransformer_power_relation.svg (100%) rename {doc => docs}/_files/Plot_delay_2013-01-01.svg (100%) rename {doc => docs}/_files/example_figures.png (100%) rename {doc => docs}/_files/example_network.svg (100%) rename {doc => docs}/_files/example_variable_chp.svg (100%) rename {doc => docs}/_files/framework_concept.svg (100%) create mode 100644 docs/_files/nonconvex_invest_investcosts_power.svg create mode 100644 docs/_files/nonconvex_invest_specific_costs.svg rename {doc => docs}/_files/oemof_solph_example.svg (100%) rename {doc => docs}/_files/pahesmf_init.py (100%) rename {doc => docs}/_files/variable_chp_plot.svg (100%) create mode 100644 docs/authors.rst rename doc/whats_new.rst => docs/changelog.rst (93%) create mode 100644 docs/conf.py create mode 100644 docs/contributing.rst rename {doc => docs}/docutils.conf (100%) rename {doc => docs}/getting_started.rst (90%) create mode 100644 docs/index.rst rename {doc => docs}/installation_and_setup.rst (77%) create mode 100644 docs/old_usage.rst create mode 100644 docs/readme.rst create mode 100644 docs/reference/index.rst rename {doc/api => docs/reference}/oemof.solph.rst (71%) create mode 100644 docs/requirements.txt rename doc/oemof_outputlib.rst => docs/results.rst (96%) create mode 100644 docs/spelling_wordlist.txt rename doc/oemof_solph.rst => docs/usage.rst (88%) rename {doc => docs}/whatsnew/v0-0-1.rst (100%) rename {doc => docs}/whatsnew/v0-0-2.rst (84%) rename {doc => docs}/whatsnew/v0-0-3.rst (72%) rename {doc => docs}/whatsnew/v0-0-4.rst (91%) rename {doc => docs}/whatsnew/v0-0-5.rst (79%) rename {doc => docs}/whatsnew/v0-0-6.rst (78%) rename {doc => docs}/whatsnew/v0-0-7.rst (100%) rename {doc => docs}/whatsnew/v0-1-0.rst (91%) rename {doc => docs}/whatsnew/v0-1-1.rst (68%) rename {doc => docs}/whatsnew/v0-1-2.rst (88%) rename {doc => docs}/whatsnew/v0-1-4.rst (69%) rename {doc => docs}/whatsnew/v0-2-0.rst (89%) rename {doc => docs}/whatsnew/v0-2-1.rst (74%) rename {doc => docs}/whatsnew/v0-2-2.rst (81%) rename {doc => docs}/whatsnew/v0-2-3.rst (92%) rename {doc => docs}/whatsnew/v0-3-0.rst (89%) rename {doc => docs}/whatsnew/v0-3-1.rst (100%) rename {doc => docs}/whatsnew/v0-3-2.rst (94%) create mode 100644 docs/whatsnew/v0-3-3.rst create mode 100644 docs/whatsnew/v0-4-0.rst delete mode 100644 nose.cfg delete mode 100644 oemof/__init__.py delete mode 100644 oemof/energy_system.py delete mode 100644 oemof/graph.py delete mode 100644 oemof/groupings.py delete mode 100644 oemof/network.py delete mode 100644 oemof/outputlib/__init__.py delete mode 100644 oemof/solph/__init__.py delete mode 100644 oemof/tools/__init__.py delete mode 100644 oemof/tools/economics.py delete mode 100644 oemof/tools/logger.py create mode 100644 src/oemof/solph/__init__.py rename {oemof => src/oemof}/solph/blocks.py (77%) rename {oemof => src/oemof}/solph/components.py (77%) rename {oemof/tools => src/oemof/solph}/console_scripts.py (100%) rename {oemof => src/oemof}/solph/constraints.py (86%) rename {oemof => src/oemof}/solph/custom.py (94%) rename {oemof => src/oemof}/solph/groupings.py (98%) rename {oemof/tools => src/oemof/solph}/helpers.py (56%) rename {oemof => src/oemof}/solph/models.py (99%) rename {oemof => src/oemof}/solph/network.py (83%) rename {oemof => src/oemof}/solph/options.py (58%) rename {oemof => src/oemof}/solph/plumbing.py (76%) rename {oemof/outputlib => src/oemof/solph}/processing.py (99%) rename {oemof/outputlib => src/oemof/solph}/views.py (92%) delete mode 100644 tests/basic_tests.py create mode 100644 tests/lp_files/flow_invest_with_offset.lp create mode 100644 tests/lp_files/flow_invest_with_offset_no_minimum.lp create mode 100644 tests/lp_files/flow_invest_without_offset.lp create mode 100644 tests/lp_files/storage_fixed_losses.lp create mode 100644 tests/lp_files/storage_invest_1_fixed_losses.lp create mode 100644 tests/lp_files/storage_invest_all_nonconvex.lp create mode 100644 tests/lp_files/storage_invest_with_offset.lp create mode 100644 tests/lp_files/storage_invest_without_offset.lp create mode 100644 tests/test_constraints_module.py delete mode 100644 tests/test_scripts/test_solph/test_storage_investment/es_dump_test_2_1dev.oemof rename tests/test_scripts/test_solph/test_storage_investment/{es_dump_test_2_3dev.oemof => es_dump_test_3_2dev.oemof} (54%) create mode 100644 tests/test_warnings.py delete mode 100644 tests/tool_tests.py diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..ae9669e87 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,32 @@ +version: '{branch}-{build}' +build: off +environment: + global: + COVERALLS_EXTRAS: '-v' + COVERALLS_REPO_TOKEN: COVERALLSTOKEN + matrix: + - TOXENV: check + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' + - ps: (new-object net.webclient).DownloadFile('https://osf.io/rmfb7/download', 'my_test_solver.exe') + +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 000000000..18794f99e --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,20 @@ +[bumpversion] +current_version = 0.4.0.dev0 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:README.rst] +search = v{current_version}. +replace = v{new_version}. + +[bumpversion:file:docs/conf.py] +search = version = release = '{current_version}' +replace = version = release = '{new_version}' + +[bumpversion:file:src/oemof/solph/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' diff --git a/.cookiecutterrc b/.cookiecutterrc new file mode 100644 index 000000000..fb28818a5 --- /dev/null +++ b/.cookiecutterrc @@ -0,0 +1,69 @@ +# This file exists so you can easily regenerate your project. +# +# `cookiepatcher` is a convenient shim around `cookiecutter` +# for regenerating projects (it will generate a .cookiecutterrc +# automatically for any template). To use it: +# +# pip install cookiepatcher +# cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path +# +# See: +# https://pypi.org/project/cookiepatcher +# +# Alternatively, you can run: +# +# cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary + +default_context: + + _extensions: ['jinja2_time.TimeExtension'] + _template: 'gh:ionelmc/cookiecutter-pylibrary' + allow_tests_inside_package: 'yes' + appveyor: 'yes' + c_extension_function: 'longest' + c_extension_module: '_oemof-solph' + c_extension_optional: 'no' + c_extension_support: 'no' + c_extension_test_pypi: 'no' + c_extension_test_pypi_username: 'oemof' + codacy: 'yes' + codacy_projectid: 'CODECYTOKEN' + codeclimate: 'no' + codecov: 'yes' + command_line_interface: 'no' + command_line_interface_bin_name: 'oemof-solph' + coveralls: 'yes' + coveralls_token: 'COVERALLSTOKEN' + distribution_name: 'oemof-solph' + email: 'contact@oemof.org' + full_name: 'oemof developer group' + landscape: 'no' + license: 'MIT license' + linter: 'flake8' + package_name: 'oemof-solph' + project_name: 'oemof-solph' + project_short_description: 'A model generator for energy system modelling and optimisation.' + pypi_badge: 'yes' + pypi_disable_upload: 'no' + release_date: '2020-04-04' + repo_hosting: 'github.com' + repo_hosting_domain: 'github.com' + repo_name: 'oemof-solph' + repo_username: 'oemof' + requiresio: 'yes' + scrutinizer: 'yes' + setup_py_uses_setuptools_scm: 'no' + setup_py_uses_test_runner: 'no' + sphinx_docs: 'yes' + sphinx_docs_hosting: 'https://oemof-solph.readthedocs.io/' + sphinx_doctest: 'no' + sphinx_theme: 'sphinx-rtd-theme' + test_matrix_configurator: 'yes' + test_matrix_separate_coverage: 'no' + test_runner: 'pytest' + travis: 'yes' + travis_osx: 'yes' + version: '0.4.0.dev0' + website: 'https://oemof.org' + year_from: '2014' + year_to: '2020' diff --git a/.coveragerc b/.coveragerc index 75bed5d38..e84deab19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,16 @@ +[paths] +source = src + [run] -omit = - *custom* \ No newline at end of file +branch = true +source = + src + tests +parallel = true +omit = *custom* + *test* + +[report] +show_missing = true +precision = 2 +omit = *migrations* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..6eb7567d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# see https://editorconfig.org/ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + +[*.{bat,cmd,ps1}] +end_of_line = crlf diff --git a/.gitignore b/.gitignore index cd577359e..dfe58380d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,71 @@ -/.spyderproject -/doc/Makefile *.py[cod] -*.py.orig -*.nja -*.odb -*.textile -*.log -*.svg -*.png -*.pdf -*.lp +__pycache__ + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +wheelhouse +develop-eggs +.installed.cfg +lib +lib64 +venv*/ +pyvenv*/ +pip-wheel-metadata/ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +.coverage.* +.pytest_cache/ +nosetests.xml +coverage.xml +htmlcov + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject .idea -!doc/_files/*.svg -!doc/_files/*.png -!tests/lp_files/*.lp -doc/_build/ -dist/* -oemof.egg-info/* -oemof_base.egg-info/* -build/* +*.iml +*.komodoproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build +.DS_Store +*~ +.*.sw[po] +.build +.ve +.env +.cache +.pytest +.benchmarks +.bootstrap +.appveyor.token +*.bak +# Mypy Cache +.mypy_cache/ diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..59ff5c04f --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,10 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 +sphinx: + configuration: docs/conf.py +formats: all +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.travis.yml b/.travis.yml index d5948e714..5abef8b10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,62 @@ language: python - -matrix: - include: - - python: 3.5 - - python: 3.6 - - python: 3.7 - dist: xenial +dist: xenial +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all addons: - apt: - packages: - - coinor-cbc + apt: + packages: + - coinor-cbc +matrix: + include: + - python: '3.6' + env: + - TOXENV=check + - python: '3.7' + env: + - TOXENV=docs + - env: + - TOXENV=py36,codecov,coveralls + python: '3.6' + - env: + - TOXENV=py37,codecov,coveralls + python: '3.7' +# - os: osx +# language: generic +# env: +# - TOXENV=py37 + - env: + - TOXENV=py38,codecov,coveralls + python: '3.8' + - env: + - TOXENV=py38-nocov + python: '3.8' +before_install: + - python --version + - uname -a + - lsb_release -a || true + - | + if [[ $TRAVIS_OS_NAME == 'osx' ]]; then + [[ $TOXENV =~ py3 ]] && brew upgrade python + [[ $TOXENV =~ py2 ]] && brew install python@2 + export PATH="/usr/local/opt/python/libexec/bin:${PATH}" + fi install: - - pip install . - - pip install coveralls - -# command to run tests + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version script: - - nosetests --with-doctest --with-coverage -c nose.cfg - -after_success: - - coveralls - + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 000000000..1d70e9b83 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,5 @@ + +Authors +======= + +* oemof developer group - https://oemof.org diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 000000000..33f5d9c9e --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,8 @@ + +Changelog +========= + +0.4.0.dev0 (2020-04-04) +----------------------- + +* First release on PyPI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 8c2ad9005..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -The developer rules can be found in the chapter [developing oemof](https://oemof.readthedocs.io/en/latest/developing_oemof.html) of the oemof.documentation. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..29139b9fb --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +oemof-solph could always use more documentation, whether as part of the +official oemof-solph docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/oemof/oemof-solph/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `oemof-solph` for local development: + +1. Fork `oemof-solph `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:oemof/oemof-solph.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run all the test environments in *parallel* (you need to ``pip install detox``):: + + detox diff --git a/LICENSE b/LICENSE index 224dd8c5c..29148faa4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,9 @@ MIT License -Copyright (c) 2019 oemof developer group +Copyright (c) 2014-2020, oemof developer group -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index d2385e49a..000000000 --- a/MANIFEST +++ /dev/null @@ -1,2 +0,0 @@ -# file GENERATED by distutils, do NOT edit -setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..cafe2d1b1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,22 @@ +graft docs +graft src +graft ci +graft tests + +include .bumpversion.cfg +include .coveragerc +include .cookiecutterrc +include .editorconfig + +include AUTHORS.rst +include CHANGELOG.rst +include CONTRIBUTING.rst +include LICENSE +include README.rst +include *.md +include VERSION +include pytest.ini + +include tox.ini .travis.yml .appveyor.yml .readthedocs.yml + +global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 2a244a230..d08a94b35 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,88 @@ -.. image:: https://coveralls.io/repos/github/oemof/oemof/badge.svg?branch=dev - :target: https://coveralls.io/github/oemof/oemof?branch=dev -.. image:: https://travis-ci.org/oemof/oemof.svg?branch=dev - :target: https://travis-ci.org/oemof/oemof -.. image:: https://readthedocs.org/projects/oemof/badge/?version=stable - :target: https://oemof.readthedocs.io/en/stable/ -.. image:: https://badge.fury.io/py/oemof.svg - :target: https://badge.fury.io/py/oemof -.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596235.svg - :target: https://doi.org/10.5281/zenodo.596235 -.. image:: https://img.shields.io/lgtm/alerts/g/oemof/oemof.svg - :target: https://lgtm.com/projects/g/oemof/oemof/alerts/ -.. image:: https://img.shields.io/lgtm/grade/python/g/oemof/oemof.svg - :target: https://lgtm.com/projects/g/oemof/oemof/context:python - -Oemof stands for "Open Energy System Modelling Framework" and provides a free, open source and clearly documented toolbox to analyse energy supply systems. It is developed in Python and designed as a framework with a modular structure containing several packages which communicate through well defined interfaces. - -With oemof we provide base packages for energy system modelling and optimisation. - -Everybody is welcome to use and/or develop oemof. Read our `'Why should I contribute' `_ section. +======== +Overview +======== + +.. start-badges + +.. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + | |coveralls| |codecov| + | |scrutinizer| |codacy| |codeclimate| + * - package + - | |version| |wheel| |supported-versions| |supported-implementations| + | |commits-since| +.. |docs| image:: https://readthedocs.org/projects/oemof-solph/badge/?style=flat + :target: https://readthedocs.org/projects/oemof-solph + :alt: Documentation Status + +.. |travis| image:: https://api.travis-ci.org/oemof/oemof-solph.svg?branch=dev + :alt: Travis-CI Build Status + :target: https://travis-ci.org/oemof/oemof-solph + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/oemof/oemof-solph?branch=dev&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/oemof-developer/oemof-solph + +.. |requires| image:: https://requires.io/github/oemof/oemof-solph/requirements.svg?branch=dev + :alt: Requirements Status + :target: https://requires.io/github/oemof/oemof-solph/requirements/?branch=dev + +.. |coveralls| image:: https://coveralls.io/repos/oemof/oemof-solph/badge.svg?branch=dev&service=github + :alt: Coverage Status + :target: https://coveralls.io/r/oemof/oemof-solph + +.. |codecov| image:: https://codecov.io/gh/oemof/oemof-solph/branch/dev/graphs/badge.svg?branch=dev + :alt: Coverage Status + :target: https://codecov.io/github/oemof/oemof-solph + +.. |codacy| image:: https://api.codacy.com/project/badge/Grade/a6e5cb2dd2694c73895e142e4cf680d5 + :target: https://www.codacy.com/gh/oemof/oemof-solph?utm_source=github.com&utm_medium=referral&utm_content=oemof/oemof-solph&utm_campaign=Badge_Grade + :alt: Codacy Code Quality Status + +.. |codeclimate| image:: https://codeclimate.com/github/oemof/oemof-solph/badges/gpa.svg + :target: https://codeclimate.com/github/oemof/oemof-solph + :alt: CodeClimate Quality Status + +.. |version| image:: https://img.shields.io/pypi/v/oemof.svg + :alt: PyPI Package latest release + +.. :target: https://pypi.org/project/oemof-solph + +.. |wheel| image:: https://img.shields.io/pypi/wheel/oemof.svg + :alt: PyPI Wheel + +.. :target: https://pypi.org/project/oemof + +.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/oemof.svg + :alt: Supported versions + +.. :target: https://pypi.org/project/oemof-solph + +.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/oemof.svg + :alt: Supported implementations + +.. :target: https://pypi.org/project/oemof + +.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/oemof-solph/v0.3.2/dev + :alt: Commits since latest release + :target: https://github.com/oemof/oemof-solph/compare/v0.3.2...dev + + +.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/oemof/oemof-solph/dev.svg + :alt: Scrutinizer Status + :target: https://scrutinizer-ci.com/g/oemof/oemof-solph/ + + +.. end-badges + +A model generator for energy system modelling and optimisation. + +Everybody is welcome to use and/or develop oemof.solph. Read our `'Why should I contribute' `_ section. Contribution is already possible on a low level by simply fixing typos in oemof's documentation or rephrasing sections which are unclear. If you want to support us that way please fork the oemof repository to your own github account and make changes as described in the github guidelines: https://guides.github.com/activities/hello-world/ @@ -26,45 +91,53 @@ Contribution is already possible on a low level by simply fixing typos in oemof' :local: :backlinks: top +Installation +============ -Documentation -============= +If you have a working Python3 environment, use pypi to install the latest oemof version. Python >= 3.5 is recommended. Lower versions may work but are not tested. -Full documentation can be found at `readthedocs `_. Use the `project site `_ of readthedocs to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. -To get the latest news visit and follow our `website `_. +:: -Installing oemof -================ + pip install https://github.com/oemof/oemof-solph/archive/master.zip" -If you have a working Python3 environment, use pypi to install the latest oemof version. Python >= 3.5 is recommended. Lower versions may work but are not tested. +You can also install the in-development version with:: + + pip install https://github.com/oemof/oemof-solph/archive/dev.zip -.. code:: bash +For more details have a look at the `'Installation and setup' `_ section. There is also a `YouTube tutorial `_ on how to install oemof under Windows. - pip install oemof +The packages **feedinlib**, **demandlib** and **oemof.db** have to be installed separately. See section `'Using oemof' `_ for more details about all oemof packages. -For more details have a look at the `'Installation and setup' `_ section. There is also a `YouTube tutorial `_ on how to install oemof under Windows. - -The packages **feedinlib**, **demandlib** and **oemof.db** have to be installed separately. See section `'Using oemof' `_ for more details about all oemof packages. +If you want to use the latest features, you might want to install the **developer version**. See section `'Developing oemof' `_ for more information. The developer version is not recommended for productive use. + + +Documentation +============= + + +https://oemof-solph.readthedocs.io/ + +Full documentation can be found at `readthedocs `_. Use the `project site `_ of readthedocs to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. + +To get the latest news visit and follow our `website `_. -If you want to use the latest features, you might want to install the **developer version**. See section `'Developing oemof' `_ for more information. The developer version is not recommended for productive use. - Structure of the oemof cosmos ============================= -Oemof packages are organised in different levels. The basic oemof interfaces are defined by the core libraries (network). The next level contains libraries that depend on the core libraries but do not provide interfaces to other oemof libraries (solph, outputlib). The third level are libraries that do not depend on any oemof interface and therefore can be used as stand-alone application (demandlib, feedinlib). Together with some other recommended projects (pvlib, windpowerlib) the oemof cosmos provides a wealth of tools to model energy systems. If you want to become part of it, feel free to join us. +Oemof packages are organised in different levels. The basic oemof interfaces are defined by the core libraries (network). The next level contains libraries that depend on the core libraries but do not provide interfaces to other oemof libraries (solph, outputlib). The third level are libraries that do not depend on any oemof interface and therefore can be used as stand-alone application (demandlib, feedinlib). Together with some other recommended projects (pvlib, windpowerlib) the oemof cosmos provides a wealth of tools to model energy systems. If you want to become part of it, feel free to join us. Examples ======== -The linkage of specific modules of the various packages is called an +The linkage of specific modules of the various packages is called an application (app) and depicts for example a concrete energy system model. You can find a large variety of helpful examples in `oemof's example repository `_ on github to download or clone. The examples show optimisations of different energy systems and are supposed to help new users to understand the framework's structure. There is some elaboration on the examples in the respective repository. You are welcome to contribute your own examples via a `pull request `_ or by sending us an e-mail (see `here `_ for contact information). -Got further questions on using oemof? +Got further questions on using oemof? ====================================== If you have questions regarding the use of oemof you can visit the forum at: `https://forum.openmod-initiative.org/tags/c/qa/oemof` and open a new thread if your questions hasn't been already answered. @@ -72,8 +145,8 @@ Join the developers! ==================== A warm welcome to all who want to join the developers and contribute to oemof. Information -on the details and how to approach us can be found -`in the documentation `_ . +on the details and how to approach us can be found +`in the documentation `_ . Keep in touch @@ -88,8 +161,8 @@ Citing oemof The core ideas of oemof are described in `DOI:10.1016/j.esr.2018.07.001 `_ (preprint at `arXiv:1808.0807 `_). To allow citing specific versions of oemof, we use the zenodo project to get a DOI for each version. `Select the version you want to cite `_. -License -======= +Free software: MIT license +========================== Copyright (c) 2019 oemof developer group @@ -111,3 +184,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Development +=========== + +To run the all tests run:: + + tox + +Note, to combine the coverage data from all the tox environments run: + +.. list-table:: + :widths: 10 90 + :stub-columns: 1 + + - - Windows + - :: + + set PYTEST_ADDOPTS=--cov-append + tox + + - - Other + - :: + + PYTEST_ADDOPTS=--cov-append tox + diff --git a/VERSION b/VERSION index 58ce7e727..52c69476c 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1 @@ -__version__ = "0.4.0dev" - +__version__ = "0.4.0.dev0" diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd new file mode 100644 index 000000000..289585fc1 --- /dev/null +++ b/ci/appveyor-with-compiler.cmd @@ -0,0 +1,23 @@ +:: Very simple setup: +:: - if WINDOWS_SDK_VERSION is set then activate the SDK. +:: - disable the WDK if it's around. + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" +ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% + +IF EXIST %WIN_WDK% ( + REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN %WIN_WDK% 0wdf +) +IF "%WINDOWS_SDK_VERSION%"=="" GOTO main + +SET DISTUTILS_USE_SDK=1 +SET MSSdk=1 +"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% +CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + +:main +ECHO Executing: %COMMAND_TO_RUN% +CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py new file mode 100755 index 000000000..fde474379 --- /dev/null +++ b/ci/bootstrap.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import os +import sys +import subprocess +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import join + +base_path = dirname(dirname(abspath(__file__))) + + +def check_call(args): + print("+", *args) + subprocess.check_call(args) + + +def exec_in_env(): + env_path = join(base_path, ".tox", "bootstrap") + if sys.platform == "win32": + bin_path = join(env_path, "Scripts") + else: + bin_path = join(env_path, "bin") + if not exists(env_path): + + print("Making bootstrap env in: {0} ...".format(env_path)) + try: + check_call([sys.executable, "-m", "venv", env_path]) + except subprocess.CalledProcessError: + try: + check_call([sys.executable, "-m", "virtualenv", env_path]) + except subprocess.CalledProcessError: + check_call(["virtualenv", env_path]) + print("Installing `jinja2` into bootstrap environment...") + check_call( + [join(bin_path, "pip"), "install", "jinja2", "tox", "matrix"] + ) + python_executable = join(bin_path, "python") + if not os.path.exists(python_executable): + python_executable += ".exe" + + print("Re-executing with: {0}".format(python_executable)) + print("+ exec", python_executable, __file__, "--no-env") + os.execv(python_executable, [python_executable, __file__, "--no-env"]) + + +def main(): + import jinja2 + import matrix + + + print("Project path: {0}".format(base_path)) + + jinja = jinja2.Environment( + loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + ) + + tox_environments = {} + for (alias, conf) in matrix.from_file( + join(base_path, "setup.cfg") + ).items(): + # python = conf["python_versions"] + deps = conf["dependencies"] + tox_environments[alias] = { + "deps": deps.split(), + } + if "coverage_flags" in conf: + cover = {"false": False, "true": True}[ + conf["coverage_flags"].lower() + ] + tox_environments[alias].update(cover=cover) + if "environment_variables" in conf: + env_vars = conf["environment_variables"] + tox_environments[alias].update(env_vars=env_vars.split()) + + for name in os.listdir(join("ci", "templates")): + with open(join(base_path, name), "w") as fh: + fh.write( + jinja.get_template(name).render( + tox_environments=tox_environments + ) + ) + print("Wrote {}".format(name)) + print("DONE.") + + +if __name__ == "__main__": + args = sys.argv[1:] + if args == ["--no-env"]: + main() + elif not args: + exec_in_env() + else: + print("Unexpected arguments {0}".format(args), file=sys.stderr) + sys.exit(1) + diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 000000000..b2a21e519 --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1,4 @@ +virtualenv>=16.6.0 +pip>=19.1.1 +setuptools>=18.0.1 +six>=1.12.0 diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml new file mode 100644 index 000000000..7128cc1b2 --- /dev/null +++ b/ci/templates/.appveyor.yml @@ -0,0 +1,52 @@ +version: '{branch}-{build}' +build: off +environment: + global: + COVERALLS_EXTRAS: '-v' + COVERALLS_REPO_TOKEN: COVERALLSTOKEN + matrix: + - TOXENV: check + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' +{% for env, config in tox_environments|dictsort %} +{% if env.startswith(('py2', 'py3')) %} + - TOXENV: {{ env }}{% if config.cover %},codecov,coveralls{% endif %}{{ "" }} + TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe + PYTHON_HOME: C:\Python{{ env[2:4] }} + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '32' +{% if 'nocov' in env %} + WHEEL_PATH: .tox/dist +{% endif %} + - TOXENV: {{ env }}{% if config.cover %},codecov,coveralls{% endif %}{{ "" }} + TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe + PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '64' +{% if 'nocov' in env %} + WHEEL_PATH: .tox/dist +{% endif %} +{% if env.startswith('py2') %} + WINDOWS_SDK_VERSION: v7.0 +{% endif %} +{% endif %}{% endfor %} +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml new file mode 100644 index 000000000..56da4d5f2 --- /dev/null +++ b/ci/templates/.travis.yml @@ -0,0 +1,58 @@ +language: python +dist: xenial +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all +matrix: + include: + - python: '3.6' + env: + - TOXENV=check + - python: '3.6' + env: + - TOXENV=docs +{%- for env, config in tox_environments|dictsort %}{{ '' }} +{%- if 'py37' in env or 'py27' in env %}{{ '' }} + - os: osx + language: generic + env: + - TOXENV={{ env }} +{%- endif %}{{ '' }} + - env: + - TOXENV={{ env }}{% if config.cover %},codecov,coveralls{% endif %} +{%- if env.startswith('pypy3') %}{{ '' }} + - TOXPYTHON=pypy3 + python: 'pypy3' +{%- elif env.startswith('pypy') %}{{ '' }} + python: 'pypy' +{%- else %}{{ '' }} + python: '{{ '{0[2]}.{0[3]}'.format(env) }}' +{%- endif %} +{%- endfor %}{{ '' }} +before_install: + - python --version + - uname -a + - lsb_release -a || true + - | + if [[ $TRAVIS_OS_NAME == 'osx' ]]; then + [[ $TOXENV =~ py3 ]] && brew upgrade python + [[ $TOXENV =~ py2 ]] && brew install python@2 + export PATH="/usr/local/opt/python/libexec/bin:${PATH}" + fi +install: + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/ci/templates/tox.ini b/ci/templates/tox.ini new file mode 100644 index 000000000..4eae8c248 --- /dev/null +++ b/ci/templates/tox.ini @@ -0,0 +1,112 @@ +[tox] +envlist = + clean, + check, + docs, +{% for env in tox_environments|sort %} + {{ env }}, +{% endfor %} + report + +[testenv] +basepython = + docs: {env:TOXPYTHON:python3.6} + {bootstrap,clean,check,report,codecov,coveralls}: {env:TOXPYTHON:python3} +setenv = + PYTHONPATH={toxinidir}/tests + PYTHONUNBUFFERED=yes +passenv = + * +deps = + pytest + pytest-travis-fold + pytest-cov +commands = + {posargs:pytest --cov --cov-report=term-missing -vv --ignore=src} + +[testenv:bootstrap] +deps = + jinja2 + matrix +skip_install = true +commands = + python ci/bootstrap.py --no-env + +[testenv:check] +deps = + docutils + check-manifest + flake8 + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py check --strict --metadata --restructuredtext + check-manifest {toxinidir} + flake8 src tests setup.py + isort --verbose --check-only --diff --recursive src tests setup.py + + +[testenv:docs] +usedevelop = true +deps = + -r{toxinidir}/docs/requirements.txt +commands = + sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs + +[testenv:coveralls] +deps = + coveralls +skip_install = true +commands = + coveralls [] + + + +[testenv:codecov] +deps = + codecov +skip_install = true +commands = + codecov [] + +[testenv:report] +deps = coverage +skip_install = true +commands = + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = coverage +{% for env, config in tox_environments|dictsort %} + +[testenv:{{ env }}] +basepython = {env:TOXPYTHON:{{ env.split("-")[0] if env.startswith("pypy") else "python{0[2]}.{0[3]}".format(env) }}} +{% if config.cover or config.env_vars %} +setenv = + {[testenv]setenv} +{% endif %} +{% for var in config.env_vars %} + {{ var }} +{% endfor %} +{% if config.cover %} +usedevelop = true +commands = + {posargs:pytest --cov --cov-report=term-missing -vv} +{% endif %} +{% if config.cover or config.deps %} +deps = + {[testenv]deps} +{% endif %} +{% if config.cover %} + pytest-cov +{% endif %} +{% for dep in config.deps %} + {{ dep }} +{% endfor -%} +{% endfor -%} diff --git a/doc/_files/ExtractionTurbine_range_of_operation.svg b/doc/_files/ExtractionTurbine_range_of_operation.svg deleted file mode 100644 index c6b23f5ec..000000000 --- a/doc/_files/ExtractionTurbine_range_of_operation.svg +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - P - - Q - . - - - - - outflow-relation-constraint: - - - input-outflow-constraint: - - - - Input flow - - ß defines the slope - limits the range of operation - - - - diff --git a/doc/about_oemof.rst b/doc/about_oemof.rst deleted file mode 100644 index 1230d120e..000000000 --- a/doc/about_oemof.rst +++ /dev/null @@ -1,104 +0,0 @@ -########################################## - About oemof -########################################## - -This overview has been developed to make oemof easy to use and develop. It describes general ideas behind and structures of oemof and its modules. - -.. contents:: - :depth: 1 - :local: - :backlinks: top - - -The idea of an open framework -============================== - -The Open Energy System Modelling Framework has been developed for the modelling and analysis of energy supply systems considering power and heat as well as prospectively mobility. - -Oemof has been implemented in Python and uses several Python packages for scientific applications (e.g. mathematical optimisation, network analysis, data analysis), optionally in combination with a PostgreSQL/PostGIS database. -It offers a toolbox of various features needed to build energy system models in high temporal and spatial resolution. -For instance, the wind energy feed-in in a model region can be modelled based on weather data, the CO2-minimal operation of biomass power plants can be calculated or the future energy supply of Europe can be simulated. - -The framework consists of different libraries. For the communication between these libraries different interfaces are provided. -The oemof libraries and their modules are used to build what we call an 'application' (app) which depicts a concrete energy system model or a subprocess of this model. -Generally, applications can be developed highly individually by the use of one or more libraries depending on the scope and purpose. -The following image illustrates the typical application building process. - -.. image:: _files/framework_concept.svg - :height: 475px - :width: 1052 px - :scale: 30 % - :alt: The idea of Open Energy System Modelling Framework (oemof) - :align: center - -It gets clear that applications can be build flexibly using different libraries. -Furthermore, single components of applications can be substituted easily if different functionalities are needed. -This allows for individual application development and provides all degrees of freedom to the developer -which is particularly relevant in environments such as scientific work groups that often work spatially distributed. - -Among other applications, the apps 'renpassG!S' and 'reegis' are currently developed based on the framework. -'renpassG!S' enables the simulation of a future European energy system with a high spatial and temporal resolution. -Different expansion pathways of conventional power plants, renewable energies and net infrastructure can be considered. -The app 'reegis' provides a simulation of a regional heat and power supply system. -Another application is 'HESYSOPT' which has been desined to simulate combined heat and power systems with MILP on the component level. -These three examples show that the modular approach of the framework allows applications with very different objectives. - -Application Examples -============================== - -Some applications are publicly available and continuously developed. -Examples and a screenshot gallery can be found on `oemof's official homepage `_. - - -Why are we developing oemof? -============================== - -Energy system models often do not have publicly accessible source code and freely available data and are poorly documented. -The missing transparency slows down the scientific discussion on model quality with regard to certain problems such as grid extension or cross-border interaction between national energy systems. -Besides, energy system models are often developed for a certain application and cannot (or only with great effort) be adjusted to other requirements. - -The Center for Sustainable Energy Systems (ZNES) Flensburg together with the Reiner Lemoine Institute (RLI) in Berlin and the Otto-von-Guericke-University of Magdeburg (OVGU) -are developing the Open Energy System Modelling Framework (oemof) to address these problems by offering a free, open and clearly documented framework for energy system modelling. -This transparent approach allows a sound scientific discourse on the underlying models and data. -In this way the assessment of quality and significance of undertaken analyses is improved. Moreover, the modular composition of the framework supports the adjustment to a large number of application purposes. - -The open source approach allows a collaborative development of the framework that offers several advantages: - -- **Synergies** - By developing collaboratively synergies between the participating institutes can be utilized. - -- **Debugging** - Through the input of a larger group of users and developers bugs are identified and fixed at an earlier stage. - -- **Advancement** - The oemof-based application profits from further development of the framework. - - -.. _why_contribute_label: - -Why should I contribute? -======================== - - * You do not want to start at the very beginning. - You are not the first one, who wants to set up a energy system model. So why not start with existing code? - * You want your code to be more stable. - If other people use your code, they may find bugs or will have ideas to improve it. - * Tired of 'write-only-code'. - Developing as part of a framework encourages you to document sufficiently, so that after years you may still understand your own code. - * You want to talk to other people when you are deadlocked. - People are even more willing to help, if they are interested in what you are doing because they can use it afterwards. - * You want your code to be seen and used. We try to make oemof more and more visible to the modelling community. Together it will be easier to increase the awareness of this framework and therefore for your contribution. - -We know, sometimes it is difficult to start on an existing concept. It will take some time to understand it and you will need extra time to document your own stuff. -But once you understand the libraries you will get lots of interesting features, always with the option to fit them to your own needs. - -If you first want to try out the collaborative process of software development you can start with a contribution on a low level. Fixing typos in the documentation or rephrasing sentences which are unclear would help us on the one hand and brings you nearer to the collaboration process on the other hand. - -For any kind of contribution, please fork the oemof repository to your own github account and make changes as described in the github guidelines: https://guides.github.com/activities/hello-world/ - -Just contact us if you have any questions! - - -Join oemof with your own approach or project -============================================ - -Oemof is designed as a framework and there is a lot of space for own ideas or own libraries. No matter if you want a heuristic solver library or different linear solver libraries. -You may want to add tools to analyse the results or something we never heard of. -You want to add a GUI or your application to be linked to. We think, that working together in one framework will increase the probability that somebody will use and test your code (see :ref:`why_contribute_label`). - -Interested? Together we can talk about how to transfer your ideas into oemof or even integrate your code. Maybe we just link to your project and try to adept the API for a better fit in the future. - -Also consider joining our developer meetings which take place every 6 months (usually May and December). Just contact us! diff --git a/doc/api.rst b/doc/api.rst deleted file mode 100644 index 521b7f2ab..000000000 --- a/doc/api.rst +++ /dev/null @@ -1,10 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~ -API -~~~~~~~~~~~~~~~~~~~~~~ - -.. toctree:: - :maxdepth: 1 - :glob: - - api/* - diff --git a/doc/api/modules.rst b/doc/api/modules.rst deleted file mode 100644 index 580a494c8..000000000 --- a/doc/api/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -oemof -===== - -.. toctree:: - :maxdepth: 4 - - oemof diff --git a/doc/api/oemof.outputlib.rst b/doc/api/oemof.outputlib.rst deleted file mode 100644 index 6122cdc74..000000000 --- a/doc/api/oemof.outputlib.rst +++ /dev/null @@ -1,30 +0,0 @@ -oemof.outputlib package -======================= - -Submodules ----------- - -oemof.outputlib.processing module ---------------------------------- - -.. automodule:: oemof.outputlib.processing - :members: - :undoc-members: - :show-inheritance: - -oemof.outputlib.views module ----------------------------- - -.. automodule:: oemof.outputlib.views - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: oemof.outputlib - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/oemof.rst b/doc/api/oemof.rst deleted file mode 100644 index 16558b8eb..000000000 --- a/doc/api/oemof.rst +++ /dev/null @@ -1,55 +0,0 @@ -oemof package -============= - -Subpackages ------------ - -.. toctree:: - - oemof.outputlib - oemof.solph - oemof.tools - -Submodules ----------- - -oemof.energy\_system module ---------------------------- - -.. automodule:: oemof.energy_system - :members: - :undoc-members: - :show-inheritance: - -oemof.graph module ------------------- - -.. automodule:: oemof.graph - :members: - :undoc-members: - :show-inheritance: - -oemof.groupings module ----------------------- - -.. automodule:: oemof.groupings - :members: - :undoc-members: - :show-inheritance: - -oemof.network module --------------------- - -.. automodule:: oemof.network - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: oemof - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/oemof.tools.rst b/doc/api/oemof.tools.rst deleted file mode 100644 index 07c7e32d9..000000000 --- a/doc/api/oemof.tools.rst +++ /dev/null @@ -1,46 +0,0 @@ -oemof.tools package -=================== - -Submodules ----------- - -oemof.tools.console\_scripts module ------------------------------------ - -.. automodule:: oemof.tools.console_scripts - :members: - :undoc-members: - :show-inheritance: - -oemof.tools.economics module ----------------------------- - -.. automodule:: oemof.tools.economics - :members: - :undoc-members: - :show-inheritance: - -oemof.tools.helpers module --------------------------- - -.. automodule:: oemof.tools.helpers - :members: - :undoc-members: - :show-inheritance: - -oemof.tools.logger module -------------------------- - -.. automodule:: oemof.tools.logger - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: oemof.tools - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index 605aee5cc..000000000 --- a/doc/conf.py +++ /dev/null @@ -1,355 +0,0 @@ -# -*- coding: utf-8 -*- -# -# oemof documentation build configuration file, created by -# sphinx-quickstart on Thu Dec 18 16:57:35 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -rst_prolog =""" -.. role:: py(code) - :language: python - :class: highlight - -""" - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.viewcode', - 'sphinx.ext.imgmath', - 'sphinx.ext.napoleon' -] - -numpydoc_show_class_members = False - -# -autoclass_content = 'both' - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'oemof' -copyright = u'2014-2018, oemof-developer-group' -author = u'oemof-Team' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '' -# The full version, including alpha/beta/rc tags. -release = '' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build', 'whatsnew/*'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -#html_theme = 'bizstyle' -import sphinx_rtd_theme - -html_theme = "sphinx_rtd_theme" - -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = { -# "sidebarwidth": "25em", -# "documentwidth":"50em", -# "pagewidth": "75em", -# } - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'oemof_doc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'oemof.tex', u'oemof Documentation', - u'oemof-Team', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'oemof', u'oemof Documentation', - [u'oemof-Team'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'oemof', u'oemof Documentation', - u'Author', 'oemof', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'oemof' -epub_author = u'oemof-Team' -epub_publisher = u'oemof-Team' -epub_copyright = u'2014, oemof-Team' - -# The basename for the epub file. It defaults to the project name. -#epub_basename = u'pahesmf' - -# The HTML theme for the epub output. Since the default themes are not optimized -# for small screen space, using the same theme for HTML and epub output is -# usually not wise. This defaults to 'epub', a theme designed to save visual -# space. -#epub_theme = 'epub' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - -# Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' - -# Fix unsupported image types using the PIL. -#epub_fix_images = False - -# Scale large images. -#epub_max_image_width = 0 - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' - -# If false, no index is generated. -#epub_use_index = True diff --git a/doc/developing_oemof.rst b/doc/developing_oemof.rst deleted file mode 100644 index 8c53144dd..000000000 --- a/doc/developing_oemof.rst +++ /dev/null @@ -1,225 +0,0 @@ -.. _developing_oemof_label: - -Developing oemof -================ - -Oemof is developed collaboratively and therefore driven by its community. While advancing -as a user of oemof, you may find situations that demand solutions that are not readily -available. In this case, your solution may be of help to other users as well. Contributing -to the development of oemof is good for two reasons: Your code may help others and you -increase the quality of your code through the review of other developers. Read also these -arguments on -`why you should contribute `_. - -A first step to get involved with development can be contributing a component that is -not part of the current version that you defined for your energy system. We have a module -oemof.solph.custom that is dedicated to collect custom components created by users. Feel free -to start a pull request and contribute. - -Another way to join the developers and learn how to contribute is to help improve the documentation. -If you find that some part could be more clear or if you even find a mistake, please -consider fixing it and creating a pull request. - -New developments that provide new functionality may enter oemof at different locations. -Please feel free to discuss contributions by creating a pull request or an issue. - -In the following you find important notes for developing oemof and elements within -the framework. On whatever level you may want to start, we highly encourage you -to contribute to the further development of oemof. If you want to collaborate see -description below or contact us. - -.. contents:: - :depth: 1 - :local: - :backlinks: top - -Install the developer version ------------------------------ - -To avoid problems make sure you have fully uninstalled previous versions of oemof. It is highly recommended to use a virtual environment. See this `virtualenv tutorial -`_ for more help. Afterwards you have -to clone the repository. See the `github documentation `_ to learn how to clone a repository. -Now you can install the cloned repository using pip: - -.. code:: bash - - pip install -e /path/to/the/repository - - -Newly added required packages (via PyPi) can be installed by performing a manual - upgrade of oemof. In that case run: - -.. code:: bash - - pip install --upgrade -e /path/to/the/repository - -Contribute to the documentation -------------------------------- - -If you want to contribute by improving the documentation (typos, grammar, comprehensibility), please use the developer version of the dev branch at -`readthedocs.org `_. -Every fixed typo helps. - -Contribute to new components ----------------------------- - -You can develop a new component according to your needs. Therefore you can use the module oemof.solph.custom which collects custom components created by users and lowers the entry barrier for contributing. - -Your code should fit to the :ref:`style_guidlines_label` and the docstring should be complete and hold the equations used in the constraints. But there are several steps you do not necessarily need to fulfill when contributing to oemof.solph.custom: you do not need to meet the :ref:`naming_conventions_label`. Also compatiblity to the results-API must not be guaranteed. Further you do not need to test your components or adapt the documentation. These steps are all necessary once your custom component becomes a constant part of oemof (oemof.solph.components) and are described here: :ref:`coding_requirements_label`. But in the first step have a look at existing custom components created by other users in oemof.solph.custom and easily create your own if you need. - -Collaboration with pull requests --------------------------------- - -To collaborate use the pull request functionality of github as described here: https://guides.github.com/activities/hello-world/ - -How to create a pull request -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* Fork the oemof repository to your own github account. -* Change, add or remove code. -* Commit your changes. -* Create a pull request and describe what you will do and why. Please use the pull request template we offer. It will be shown to you when you click on "New pull request". -* Wait for approval. - -.. _coding_requirements_label: - -Generally the following steps are required when changing, adding or removing code -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* Read the :ref:`style_guidlines_label` and :ref:`naming_conventions_label` and follow them -* Add new tests according to what you have done -* Add/change the documentation (new feature, API changes ...) -* Add a whatsnew entry and your name to Contributors -* Check if all :ref:`tests_label` still work. - -.. _tests_label: - -Tests ------ - -.. role:: bash(code) - :language: bash - -Run the following test before pushing a successful merge. - -.. code:: bash - - nosetests -w "/path/to/oemof" --with-doctest - -.. _style_guidlines_label: - -Issue-Management ----------------- - -A good way for communication with the developer group are issues. If you -find a bug, want to contribute an enhancement or have a question on a specific problem -in development you want to discuss, please create an issue: - -* describing your point accurately -* using the list of category tags -* addressing other developers - -If you want to address other developers you can use @name-of-developer, or -use e.g. @oemof-solph to address a team. `Here `_ -you can find an overview over existing teams on different subjects and their members. - -Look at the existing issues to get an idea on the usage of issues. - -Style guidelines ----------------- - -We mostly follow standard guidelines instead of developing own rules. So if anything is not defined in this section, search for a `PEP rule `_ and follow it. - -Docstrings -^^^^^^^^^^ - -We decided to use the style of the numpydoc docstrings. See the following link for an -`example `_. - - -Code commenting -^^^^^^^^^^^^^^^^ - -Code comments are block and inline comments in the source code. They can help to understand the code and should be utilized "as much as necessary, as little as possible". When writing comments follow the PEP 0008 style guide: https://www.python.org/dev/peps/pep-0008/#comments. - - -PEP8 (Python Style Guide) -^^^^^^^^^^^^^^^^^^^^^^^^^ - -* We adhere to `PEP8 `_ for any code - produced in the framework. - -* We use pylint to check your code. Pylint is integrated in many IDEs and - Editors. `Check here `_ or ask the - maintainer of your IDE or Editor - -* Some IDEs have pep8 checkers, which are very helpful, especially for python - beginners. - -Quoted strings -^^^^^^^^^^^^^^ - -As there is no recommendation in the PEP rules we use double quotes for strings read by humans such as logging/error messages and single quotes for internal strings such as keys and column names. However one can deviate from this rules if the string contains a double or single quote to avoid escape characters. According to `PEP 257 `_ and numpydoc we use three double quotes for docstrings. - -.. code-block:: python - - logging.info("We use double quotes for messages") - - my_dictionary.get('key_string') - - logging.warning('Use three " to quote docstrings!' # exception to avoid escape characters - -.. _naming_conventions_label: - -Naming Conventions ------------------- - -* We use plural in the code for modules if there is possibly more than one child - class (e.g. import transformers AND NOT transformer). If there are arrays in - the code that contain multiple elements they have to be named in plural (e.g. - `transformers = [T1, T2,...]`). - -* Please, follow the naming conventions of - `pylint `_ - -* Use talking names - - * Variables/Objects: Name it after the data they describe - (power\_line, wind\_speed) - * Functions/Method: Name it after what they do: **use verbs** - (get\_wind\_speed, set\_parameter) - - -Using git ---------- - -Branching model -^^^^^^^^^^^^^^^ - -So far we adhere mostly to the git branching model by -`Vincent Driessen `_. - -Differences are: - -* instead of the name ``origin/develop`` we call the branch ``origin/dev``. -* feature branches are named like ``features/*`` -* release branches are named like ``releases/*`` - -Commit message -^^^^^^^^^^^^^^ - -Use this nice little `commit tutorial `_ to -learn how to write a nice commit message. - - -Documentation ----------------- - -The general implementation-independent documentation such as installation guide, flow charts, and mathematical models is done via ReStructuredText (rst). The files can be found in the folder */oemof/doc*. For further information on restructured text see: http://docutils.sourceforge.net/rst.html. - - -How to become a member of oemof -------------------------------- - -And last but not least, `here `_ -you will find all information about how to become a member of the oemof organisation and of developer teams. - diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 1d4960f99..000000000 --- a/doc/index.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. oemof documentation master file, created by - sphinx-quickstart on Thu Dec 18 16:57:35 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to oemof's documentation! -=================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - getting_started - about_oemof - installation_and_setup - using_oemof - developing_oemof - whats_new - oemof_network - oemof_solph - oemof_outputlib - oemof_tools - api - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/doc/oemof_network.rst b/doc/oemof_network.rst deleted file mode 100644 index d8f601fff..000000000 --- a/doc/oemof_network.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _oemof_network_label: - -~~~~~~~~~~~~~~~~~~~~~~ -oemof-network -~~~~~~~~~~~~~~~~~~~~~~ - -The modeling of energy supply systems and its variety of components has a clearly structured approach within the oemof framework. Thus, energy supply systems with different levels of complexity can be based on equal basic module blocks. Those form an universal basic structure. - -A *node* is either a *bus* or a *component*. A bus is always connected with one or several components. Likewise components are always connected with one or several buses. Based on their characteristics components are divided into several sub types. - -*Transformers* have any number of inputs and outputs, e.g. a CHP takes from a bus of type 'gas' and feeds into a bus of type 'electricity' and a bus of type 'heat'. With additional information like parameters and transfer functions input and output can be specified. Using the example of a gas turbine, the resource consumption (input) is related to the provided end energy (output) by means of an conversion factor. Components of type *transformer* can also be used to model transmission lines. - -A *sink* has only an input but no output. With *sink* consumers like households can be modeled. But also for modelling excess energy you would use a *sink*. - -A *source* has exactly one output but no input. Thus for example, wind energy and photovoltaic plants can be modeled. - -Components and buses can be combined to an energy system. Components and buses are nodes, connected among each other through edges which are the inputs and outputs of the components. Such a model can be interpreted mathematically as bipartite graph as buses are solely connected to components and vice versa. Thereby the in- and outputs of the components are the directed edges of the graph. The components and buses themselves are the nodes of the graph. - -**oemof-network is part of oemofs core and contains the base classes that are used in oemof-solph. You do not need to define your energy system on the network level as all components can be found in oemof-solph, too. You may want to inherit from oemof-network components if you want to create new components.** - -.. _oemof_graph_label: - -Graph ------ -In the graph module you will find a function to create a networkx graph from an energy system or solph model. The networkx package provides many features to analyse, draw and export graphs. See the `networkx documentation `_ for more details. See the API-doc of :py:mod:`~oemof.graph` for all details and an example. The graph module can be used with energy systems of solph as well. diff --git a/doc/oemof_tools.rst b/doc/oemof_tools.rst deleted file mode 100644 index 97e7571e6..000000000 --- a/doc/oemof_tools.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. _oemof_tools_label: - -~~~~~~~~~~~~~~~~~~~~~~ -oemof-tools -~~~~~~~~~~~~~~~~~~~~~~ - -The oemof tools package contains little helpers to create your own application. You can use a configuration file in the ini-format to define computer specific parameters such as paths, addresses etc.. Furthermore a logging module helps you creating log files for your application. - -.. contents:: List of oemof tools - :depth: 1 - :local: - :backlinks: top - -Economics ---------- - -Calculate the annuity. See the API-doc of :py:func:`~oemof.tools.economics.annuity` for all details. - - -Helpers -------- - -Excess oemof's default path. See the API-doc of :py:mod:`~oemof.tools.helpers` for all details. - - -Logger -------- - -The main purpose of this function is to provide a logger with well set default values but with the opportunity to change the most important parameters if you know what you want after a while. This is what most new users (or users who do not want to care about loggers) need. -If you are an advanced user with your own ideas it might be easier to copy the whole function to your application and adapt it to your own wishes. - -.. code-block:: python - - define_logging(logpath=None, logfile='oemof.log', file_format=None, - screen_format=None, file_datefmt=None, screen_datefmt=None, - screen_level=logging.INFO, file_level=logging.DEBUG, - log_version=True, log_path=True, timed_rotating=None): - -By default down to INFO all messages are written on the screen and down to DEBUG all messages are written in the file. The file is placed in $HOME/.oemof/log_files as oemof.log. But you can easily pass your own path and your own filename. You can also change the logging level (screen/file) by changing the screen_level or the file_level to logging.DEBUG, logging.INFO, logging.WARNING.... . You can stop the logger from logging the oemof version or commit with *log_version=False* and the path of the file with *log_path=False*. Furthermore, you can change the format on the screen and in the file according to the python logging documentation. You can also change the used time format according to this documentation. - -.. code-block:: python - - file_format = "%(asctime)s - %(levelname)s - %(module)s - %(message)s" - file_datefmt = "%x - %X" - screen_format = "%(asctime)s-%(levelname)s-%(message)s" - screen_datefmt = "%H:%M:%S" - -You can also change the behaviour of the file handling (TimedRotatingFileHandler) by passing a dictionary with your own options (timed_rotating). - -See the API-doc of :py:func:`~oemof.tools.logger.define_logging` for all details. diff --git a/doc/using_oemof.rst b/doc/using_oemof.rst deleted file mode 100644 index 8a8729e75..000000000 --- a/doc/using_oemof.rst +++ /dev/null @@ -1,93 +0,0 @@ -.. _using_oemof_label: - -##################### -Using oemof -##################### - -Oemof is a framework and even though it is in an early stage it already provides useful tools to model energy systems. To model an energy system you have to write your own application in which you combine the oemof libraries for you specific task. The `example section `_ shows how an oemof application may look like. - -.. contents:: `Current oemof libraries` - :depth: 1 - :local: - :backlinks: top - - -oemof-network -============= -The :ref:`oemof_network_label` library is part of the oemof installation. By now it can be used to define energy systems as a network with components and buses. Every component should be connected to one or more buses. After definition, a component has to explicitely be added to its energy system. Allowed components are sources, sinks and transformer. - -.. image:: _files/example_network.svg - :scale: 30 % - :alt: alternate text - :align: center - -The code of the example above: - -.. code-block:: python - - from oemof.network import * - from oemof.energy_system import * - - # create the energy system - es = EnergySystem() - - # create bus 1 - bus_1 = Bus(label="bus_1") - - # create bus 2 - bus_2 = Bus(label="bus_2") - - # add bus 1 and bus 2 to energy system - es.add(bus_1, bus_2) - - # create and add sink 1 to energy system - es.add(Sink(label='sink_1', inputs={bus_1: []})) - - # create and add sink 2 to energy system - es.add(Sink(label='sink_2', inputs={bus_2: []})) - - # create and add source to energy system - es.add(Source(label='source', outputs={bus_1: []})) - - # create and add transformer to energy system - es.add(Transformer(label='transformer', inputs={bus_1: []}, outputs={bus_2: []})) - -The network class is aimed to be very generic and might have some network analyse tools in the future. By now the network library is mainly used as the base for the solph library. - -oemof-solph -=========== -The :ref:`oemof_solph_label` library is part of the oemof installation. Solph is designed to create and solve linear or mixed-integer -linear optimization problems. It is based on optimization modelling language pyomo. - -To use solph at least one linear solver has to be installed on your system. See the `pyomo installation guide `_ to learn which solvers are supported. Solph is tested with the open source solver `cbc` and the `gurobi` solver (free for academic use). The open `glpk` solver recently showed some odd behaviour. - -The formulation of the energy system is based on the oemof-network library but contains additional components such as storages. Furthermore the network class are enhanced with additional parameters such as efficiencies, bounds, cost and more. See the API documentation for more details. Try the `examples `_ to learn how to build a linear energy system. - -oemof-outputlib -=============== -The :ref:`oemof_outputlib_label` library is part of the oemof installation. It collects the results of an optimisation in a dictionary holding scalar variables and `pandas DataFrame `_ for time dependend output. This makes it easy to process or plot the results using the capabilities of the pandas library. - -The following code collects the results in a pandas DataFrame and selects the data -for a specific component, in this case 'heat'. - -.. code-block:: python - - results = outputlib.processing.results(om) - heat = outputlib.views.node(results, 'heat') - -To visualize results, either use `pandas own visualization functionality `_, matplotlib or the plot library of your -choice. Some existing plot methods can be found in a separate repository -`oemof_visio `_ -which can be helpful when looking for a quick way to create a plot. - - -feedinlib -========= -The `feedinlib `_ library is not part of the oemof installation and has to be installed separately using pypi. It serves as an interface between Open Data weather data and libraries to calculate feedin timeseries for fluctuating renewable energy sources. - -It is currently under revision (see `here `_ for further information). To begin with it will provide an interface to the `pvlib `_ and `windpowerlib `_ and functions to download MERRA2 weather data and `open_FRED weather data `_. -See `documentation of the feedinlib `_ for a full description of the library. - -demandlib -========= -The `demandlib `_ library is not part of the oemof installation and has to be installed separately using pypi. At the current state the demandlib can be used to create load profiles for elctricity and heat knowing the annual demand. See the `documentation of the demandlib `_ for examples and a full description of the library. diff --git a/doc/whatsnew/v0-3-3.rst b/doc/whatsnew/v0-3-3.rst deleted file mode 100644 index 9b80c537e..000000000 --- a/doc/whatsnew/v0-3-3.rst +++ /dev/null @@ -1,49 +0,0 @@ -v0.3.3 (??, ??) -+++++++++++++++++++++++++++ - - -API changes -########### - -* something - -New features -############ - - * It is now possible to determine a schedule for a flow. Flow will be pushed - to the schedule, if possible. - -New components -############## - -* something - -Documentation -############# - -* something - -Known issues -############ - -* something - -Bug fixes -######### - -* something - -Testing -####### - -* something - -Other changes -############# - -* something - -Contributors -############ - -* Caterina Köhl \ No newline at end of file diff --git a/doc/whatsnew/v0-4-0.rst b/doc/whatsnew/v0-4-0.rst deleted file mode 100644 index 790db30f5..000000000 --- a/doc/whatsnew/v0-4-0.rst +++ /dev/null @@ -1,48 +0,0 @@ -v0.4.0 (January ??, 2020) -+++++++++++++++++++++++++++ - - -API changes -########### - -* something - -New features -############ - -* something - -New components -############## - -* something - -Documentation -############# - -* something - -Known issues -############ - -* something - -Bug fixes -######### - -* something - -Testing -####### - -* something - -Other changes -############# - -* something - -Contributors -############ - -* something \ No newline at end of file diff --git a/docs/_files/ExtractionTurbine_range_of_operation.svg b/docs/_files/ExtractionTurbine_range_of_operation.svg new file mode 100644 index 000000000..360997f92 --- /dev/null +++ b/docs/_files/ExtractionTurbine_range_of_operation.svg @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + P + + Q + . + + + + + + + + + + + + + + Operation range constrained by nominal value of input flow + Backpressure linedefined by (2)outflow-relation-constraint + Iso-input lines with slope βdefined by (1)input-outflow-constraint + + + diff --git a/doc/_files/GenericCHP.svg b/docs/_files/GenericCHP.svg similarity index 100% rename from doc/_files/GenericCHP.svg rename to docs/_files/GenericCHP.svg diff --git a/doc/_files/OffsetTransformer_efficiency.svg b/docs/_files/OffsetTransformer_efficiency.svg similarity index 100% rename from doc/_files/OffsetTransformer_efficiency.svg rename to docs/_files/OffsetTransformer_efficiency.svg diff --git a/doc/_files/OffsetTransformer_power_relation.svg b/docs/_files/OffsetTransformer_power_relation.svg similarity index 100% rename from doc/_files/OffsetTransformer_power_relation.svg rename to docs/_files/OffsetTransformer_power_relation.svg diff --git a/doc/_files/Plot_delay_2013-01-01.svg b/docs/_files/Plot_delay_2013-01-01.svg similarity index 100% rename from doc/_files/Plot_delay_2013-01-01.svg rename to docs/_files/Plot_delay_2013-01-01.svg diff --git a/doc/_files/example_figures.png b/docs/_files/example_figures.png similarity index 100% rename from doc/_files/example_figures.png rename to docs/_files/example_figures.png diff --git a/doc/_files/example_network.svg b/docs/_files/example_network.svg similarity index 100% rename from doc/_files/example_network.svg rename to docs/_files/example_network.svg diff --git a/doc/_files/example_variable_chp.svg b/docs/_files/example_variable_chp.svg similarity index 100% rename from doc/_files/example_variable_chp.svg rename to docs/_files/example_variable_chp.svg diff --git a/doc/_files/framework_concept.svg b/docs/_files/framework_concept.svg similarity index 100% rename from doc/_files/framework_concept.svg rename to docs/_files/framework_concept.svg diff --git a/docs/_files/nonconvex_invest_investcosts_power.svg b/docs/_files/nonconvex_invest_investcosts_power.svg new file mode 100644 index 000000000..db19a4891 --- /dev/null +++ b/docs/_files/nonconvex_invest_investcosts_power.svg @@ -0,0 +1,1604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_files/nonconvex_invest_specific_costs.svg b/docs/_files/nonconvex_invest_specific_costs.svg new file mode 100644 index 000000000..38937005f --- /dev/null +++ b/docs/_files/nonconvex_invest_specific_costs.svg @@ -0,0 +1,1205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_files/oemof_solph_example.svg b/docs/_files/oemof_solph_example.svg similarity index 100% rename from doc/_files/oemof_solph_example.svg rename to docs/_files/oemof_solph_example.svg diff --git a/doc/_files/pahesmf_init.py b/docs/_files/pahesmf_init.py similarity index 100% rename from doc/_files/pahesmf_init.py rename to docs/_files/pahesmf_init.py diff --git a/doc/_files/variable_chp_plot.svg b/docs/_files/variable_chp_plot.svg similarity index 100% rename from doc/_files/variable_chp_plot.svg rename to docs/_files/variable_chp_plot.svg diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 000000000..e122f914a --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/doc/whats_new.rst b/docs/changelog.rst similarity index 93% rename from doc/whats_new.rst rename to docs/changelog.rst index bea4d4e5d..c49ff23b2 100644 --- a/doc/whats_new.rst +++ b/docs/changelog.rst @@ -1,5 +1,5 @@ -What's New -~~~~~~~~~~ +Changelog +~~~~~~~~~ These are new features and improvements of note in each release @@ -9,6 +9,7 @@ These are new features and improvements of note in each release :backlinks: top .. include:: whatsnew/v0-4-0.rst +.. include:: whatsnew/v0-3-3.rst .. include:: whatsnew/v0-3-2.rst .. include:: whatsnew/v0-3-1.rst .. include:: whatsnew/v0-3-0.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..f99d713b1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.ifconfig", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", +] +source_suffix = '.rst' +master_doc = 'index' +project = 'oemof.solph' +year = '2014-2020' +author = 'oemof developer group' +copyright = '{0}, {1}'.format(year, author) +version = release = '0.4.0.dev0' + +pygments_style = "trac" +templates_path = ["."] +extlinks = { + 'issue': ('https://github.com/oemof/oemof-solph/pull/%s', '#'), + 'pr': ('https://github.com/oemof/oemof-solph/pull/%s', 'PR #'), +} +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get("READTHEDOCS", None) == "True" + +if not on_rtd: # only set the theme if we're building docs locally + html_theme = "sphinx_rtd_theme" + +html_use_smartypants = True +html_last_updated_fmt = "%b %d, %Y" +html_split_index = False +html_sidebars = { + "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"], +} +html_short_title = "%s-%s" % (project, version) + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 000000000..e582053ea --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/doc/docutils.conf b/docs/docutils.conf similarity index 100% rename from doc/docutils.conf rename to docs/docutils.conf diff --git a/doc/getting_started.rst b/docs/getting_started.rst similarity index 90% rename from doc/getting_started.rst rename to docs/getting_started.rst index 50fb435dd..a693d3151 100644 --- a/doc/getting_started.rst +++ b/docs/getting_started.rst @@ -6,7 +6,7 @@ Oemof stands for "Open Energy System Modelling Framework" and provides a free, o With oemof we provide base packages for energy system modelling and optimisation. -Everybody is welcome to use and/or develop oemof. Read our :ref:`why_contribute_label` section. +Everybody is welcome to use and/or develop oemof. Read more about the `benefits of contributing `_ . Contribution is already possible on a low level by simply fixing typos in oemof's documentation or rephrasing sections which are unclear. If you want to support us that way please fork the oemof repository to your own github account and make changes as described in the github guidelines: https://guides.github.com/activities/hello-world/ @@ -33,27 +33,27 @@ If you have a working Python3 environment, use pypi to install the latest oemof pip install oemof For more details have a look at :ref:`installation_and_setup_label`. There is also a `YouTube tutorial `_ on how to install oemof under Windows. - + The packages **feedinlib**, **demandlib** and **oemof.db** have to be installed separately. See section :ref:`using_oemof_label` for more details about all oemof packages. -If you want to use the latest features, you might want to install the **developer version**. See :ref:`developing_oemof_label` for more information. The developer version is not recommended for productive use. - +If you want to use the latest features, you might want to install the **developer version**. See `our developer section `_ for more information. The developer version is not recommended for productive use. + Structure of the oemof cosmos ============================= -Oemof packages are organised in different levels. The basic oemof interfaces are defined by the core libraries (network). The next level contains libraries that depend on the core libraries but do not provide interfaces to other oemof libraries (solph, outputlib). The third level are libraries that do not depend on any oemof interface and therefore can be used as stand-alone application (demandlib, feedinlib). Together with some other recommended projects (pvlib, windpowerlib) the oemof cosmos provides a wealth of tools to model energy systems. If you want to become part of it, feel free to join us. +Oemof packages are organised in different levels. The basic oemof interfaces are defined by the core libraries (network). The next level contains libraries that depend on the core libraries but do not provide interfaces to other oemof libraries (solph, outputlib). The third level are libraries that do not depend on any oemof interface and therefore can be used as stand-alone application (demandlib, feedinlib). Together with some other recommended projects (pvlib, windpowerlib) the oemof cosmos provides a wealth of tools to model energy systems. If you want to become part of it, feel free to join us. Examples ======== -The linkage of specific modules of the various packages is called an +The linkage of specific modules of the various packages is called an application (app) and depicts for example a concrete energy system model. -You can find a large variety of helpful examples in `oemof's example repository `_ on github to download or clone. The examples show optimisations of different energy systems and are supposed to help new users to understand the framework's structure. There is some elaboration on the examples in the respective repository. +You can find a large variety of helpful examples in `oemof's example repository `_ on github to download or clone. The examples show optimisations of different energy systems and are supposed to help new users to understand the framework's structure. There is some elaboration on the examples in the respective repository. You are welcome to contribute your own examples via a `pull request `_ or by sending us an e-mail (see `here `_ for contact information). -Got further questions on using oemof? +Got further questions on using oemof? ====================================== If you have questions regarding the use of oemof you can visit the forum at: https://forum.openmod-initiative.org/tags/c/qa/oemof and open a new thread if your questions hasn't been already answered. @@ -61,7 +61,7 @@ Join the developers! ==================== A warm welcome to all who want to join the developers and contribute to oemof. Information -on the details and how to approach us can be found +on the details and how to approach us can be found `in the documentation `_ . diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..2ddb9d553 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +======== +Contents +======== + +.. toctree:: + :maxdepth: 2 + + readme + getting_started + installation_and_setup + usage + results + old_usage + reference/index + contributing + authors + changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/installation_and_setup.rst b/docs/installation_and_setup.rst similarity index 77% rename from doc/installation_and_setup.rst rename to docs/installation_and_setup.rst index 824dfc22a..6959d03b8 100644 --- a/doc/installation_and_setup.rst +++ b/docs/installation_and_setup.rst @@ -22,20 +22,25 @@ As oemof is designed as a Python package it is mandatory to have Python 3 instal .. code:: console - pip install oemof + pip install oemof.solph To use pip you have to install the pypi package. Normally pypi is part of your virtual environment. +You can also install the in-development version with:: + + pip install https://github.com/oemof/oemof-solph/archive/dev.zip + + Using Linux repositories to install Python ------------------------------------------ -Most Linux distributions will have Python 3 in their repository. Use the specific software management to install it. -If you are using Ubuntu/Debian try executing the following code in your terminal: +Most Linux distributions will have Python 3 in their repository. Use the specific software management to install it. +If you are using Ubuntu/Debian try executing the following code in your terminal: .. code:: console sudo apt-get install python3 - + You can also download different versions of Python via https://www.python.org/downloads/. Using Virtualenv (community driven) @@ -43,7 +48,7 @@ Using Virtualenv (community driven) Skip the steps you have already done. Check your architecture first (32/64 bit). - 1. Install virtualenv using the package management of your Linux distribution, pip install or install it from source (`see virtualenv documentation `_) + 1. Install virtualenv using the package management of your Linux distribution, pip install or install it from source (`see virtualenv documentation `_) 2. Open terminal to create and activate a virtual environment by typing: .. code-block:: console @@ -61,7 +66,7 @@ Using Anaconda Skip the steps you have already done. Check your architecture first (32/64 bit). - 1. Download latest `Anaconda (Linux) `_ for Python 3.x (64 or 32 bit) + 1. Download latest `Anaconda (Linux) `_ for Python 3.x (64 or 32 bit) 2. Install Anaconda 3. Open terminal to create and activate a virtual environment by typing: @@ -73,13 +78,13 @@ Skip the steps you have already done. Check your architecture first (32/64 bit). 4. In terminal type: :code:`pip install oemof` 5. Install a :ref:`linux_solver_label` if you want to use solph and execute the solph examples (See :ref:`check_installation_label` ) to check if the installation of the solver and oemof was successful - + .. _linux_solver_label: Solver ------ -In order to use solph you need to install a solver. There are various commercial and open-source solvers that can be used with oemof. +In order to use solph you need to install a solver. There are various commercial and open-source solvers that can be used with oemof. There are two common OpenSource solvers available (CBC, GLPK), while oemof recommends CBC (Coin-or branch and cut). But sometimes its worth comparing the results of different solvers. @@ -87,7 +92,7 @@ To install the solvers have a look at the package repository of your Linux distr Check the solver installation by executing the test_installation example (see :ref:`check_installation_label` ). -Other commercial solvers like Gurobi or Cplex can be used as well. Have a look at the `pyomo documentation `_ to learn about which solvers are supported. +Other commercial solvers like Gurobi or Cplex can be used as well. Have a look at the `pyomo docs `_ to learn about which solvers are supported. Windows @@ -117,14 +122,14 @@ Skip the steps you have already done. Check your architecture first (32/64 bit) 2. Install WinPython 3. Open the 'WinPython Command Prompt' and type: :code:`pip install oemof` 4. Install a :ref:`windows_solver_label` if you want to use solph and execute the solph examples (See :ref:`check_installation_label` ) to check if the installation of the solver and oemof was successful - + Using Anaconda --------------------------------------- Skip the steps you have already done. Check your architecture first (32/64 bit) - 1. Download latest `Anaconda `_ for Python 3.x (64 or 32 bit) + 1. Download latest `Anaconda `_ for Python 3.x (64 or 32 bit) 2. Install Anaconda 3. Open 'Anaconda Prompt' to create and activate a virtual environment by typing: @@ -136,51 +141,62 @@ Skip the steps you have already done. Check your architecture first (32/64 bit) 4. In 'Anaconda Prompt' type: :code:`pip install oemof` 5. Install a :ref:`windows_solver_label` if you want to use solph and execute the solph examples (See :ref:`check_installation_label` ) to check if the installation of the solver and oemof was successful - -.. _windows_solver_label: + +.. _windows_solver_label: Windows Solver -------------- -In order to use solph you need to install a solver. There are various commercial and open-source solvers that can be used with oemof. +In order to use solph you need to install a solver. There are various commercial and open-source solvers that can be used with oemof. You do not have to install both solvers. Oemof recommends the CBC (Coin-or branch and cut) solver. But sometimes its worth comparing the results of different solvers (e.g. GLPK). - 1. Downloaded CBC from here (`64 `_ or `32 `_ bit) - 2. Download GLPK from `here (64/32 bit) `_ - 3. Unpacked CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) + 1. Download CBC (`64 `_ or `32 `_ bit) + 2. Download `GLPK (64/32 bit) `_ + 3. Unpack CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) 4. Add the path of the executable files of both solvers to the PATH variable using `this tutorial `_ 5. Restart Windows Check the solver installation by executing the test_installation example (see :ref:`check_installation_label` ). - + Other commercial solvers like Gurobi or Cplex can be used as well. Have a look at the `pyomo documentation `_ to learn about which solvers are supported. Mac OSX ======= -Installation guidelines for Mac OS are still incomplete and not tested. As we do not have Mac users we could not test the following approaches, but they should work. If you are a Mac user please help us to improve this installation guide. Have look at the installation guide of Linux or Windows to get an idea what to do. +Having Python 3 installed +------------------------------ +If you have Python3 already installed, you can follow the installation instructions for Linux to install oemof. + +Install Python 3 +------------------------------ +If you are using brew you can simply run + +.. code:: console -You can download python here: https://www.python.org/downloads/mac-osx/. For information on the installation process and on how to install python packages see here: https://docs.python.org/3/using/mac.html. + brew install python3 -Virtualenv: http://sourabhbajaj.com/mac-setup/Python/README.html +Otherwise please refer to https://www.python.org/downloads/mac-osx/ for installation instructions. -Anaconda: https://www.continuum.io/downloads#osx +Mac Solver +-------------- +So far only the CBC solver was tested on a Mac. If you are a Mac user and are using other Solvers successfully please help us to improve this installation guide. -You have to install a solver if you want to use solph and execute the solph examples (See :ref:`check_installation_label` ) to check if the installation of the solver and oemof was successful. +Please follow the installation instructions on the respective homepages for details. CBC-solver: https://projects.coin-or.org/Cbc GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ +If you install the CBC solver via brew (highly recommended), it should work without additional configuration. .. _check_installation_label: -Run the installation_test file +Run the installation_test file ====================================== - + Test the installation and the installed solver: To test the whether the installation was successful simply run @@ -188,9 +204,9 @@ To test the whether the installation was successful simply run .. code:: console oemof_installation_test - -in your virtual environment. -If the installation was successful, you will get: + +in your virtual environment. +If the installation was successful, you will get: .. code:: console @@ -205,4 +221,4 @@ If the installation was successful, you will get: as an output. - + diff --git a/docs/old_usage.rst b/docs/old_usage.rst new file mode 100644 index 000000000..c20470135 --- /dev/null +++ b/docs/old_usage.rst @@ -0,0 +1,67 @@ +This section might be deprecated! + +Use solph +========= +The :ref:`oemof_solph_label` library is part of the oemof installation. Solph is designed to create and solve linear or mixed-integer +linear optimization problems. It is based on optimization modelling language pyomo. + +To use solph at least one linear solver has to be installed on your system. See the `pyomo installation guide `_ to learn which solvers are supported. Solph is tested with the open source solver `cbc` and the `gurobi` solver (free for academic use). The open `glpk` solver recently showed some odd behaviour. + +The formulation of the energy system is based on the oemof-network library but contains additional components such as storages. Furthermore the network class are enhanced with additional parameters such as efficiencies, bounds, cost and more. See the API documentation for more details. Try the `examples `_ to learn how to build a linear energy system. + +Create a generic energy system +============================== +The `oemof_network_label` library is part of the oemof installation. By now it can be used to define energy systems as a network with components and buses. Every component should be connected to one or more buses. After definition, a component has to explicitely be added to its energy system. Allowed components are sources, sinks and transformer. + +.. image:: _files/example_network.svg + :scale: 30 % + :alt: alternate text + :align: center + +The code of the example above: + +.. code-block:: python + + from oemof.solph import network + from oemof.solph import energy_system + + # create the energy system + es = energy_system.EnergySystem() + + # create bus 1 + bus_1 = network.Bus(label="bus_1") + + # create bus 2 + bus_2 = network.Bus(label="bus_2") + + # add bus 1 and bus 2 to energy system + es.add(bus_1, bus_2) + + # create and add sink 1 to energy system + es.add(network.Sink(label='sink_1', inputs={bus_1: []})) + + # create and add sink 2 to energy system + es.add(network.Sink(label='sink_2', inputs={bus_2: []})) + + # create and add source to energy system + es.add(network.Source(label='source', outputs={bus_1: []})) + + # create and add transformer to energy system + es.add(network.Transformer(label='transformer', inputs={bus_1: []}, outputs={bus_2: []})) + +The network class is aimed to be very generic, see the component or custom (experimental!) module +for more specific components. + +Process Results +=============== + + +.. code-block:: python + + results = outputlib.processing.results(om) + heat = outputlib.views.node(results, 'heat') + +To visualize results, either use `pandas own visualization functionality `_, matplotlib or the plot library of your +choice. Some existing plot methods can be found in a separate repository +`oemof_visio `_ +which can be helpful when looking for a quick way to create a plot. diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 000000000..72a335581 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 000000000..d67fe92b3 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,7 @@ +Reference +========= + +.. toctree:: + :glob: + + oemof.solph* diff --git a/doc/api/oemof.solph.rst b/docs/reference/oemof.solph.rst similarity index 71% rename from doc/api/oemof.solph.rst rename to docs/reference/oemof.solph.rst index b67ae1db3..4f646fa51 100644 --- a/doc/api/oemof.solph.rst +++ b/docs/reference/oemof.solph.rst @@ -28,6 +28,14 @@ oemof.solph.constraints module :undoc-members: :show-inheritance: +oemof.solph.console\_scripts module +----------------------------------- + +.. automodule:: oemof.solph.console_scripts + :members: + :undoc-members: + :show-inheritance: + oemof.solph.custom module ------------------------- @@ -44,6 +52,14 @@ oemof.solph.groupings module :undoc-members: :show-inheritance: +oemof.solph.helpers module +-------------------------- + +.. automodule:: oemof.solph.helpers + :members: + :undoc-members: + :show-inheritance: + oemof.solph.models module ------------------------- @@ -76,11 +92,18 @@ oemof.solph.plumbing module :undoc-members: :show-inheritance: +oemof.solph.processing module +--------------------------------- + +.. automodule:: oemof.solph.processing + :members: + :undoc-members: + :show-inheritance: -Module contents ---------------- +oemof.solph.views module +--------------------------------- -.. automodule:: oemof.solph +.. automodule:: oemof.solph.views :members: :undoc-members: :show-inheritance: diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..37da9aeed --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx>=1.3 +sphinx-rtd-theme diff --git a/doc/oemof_outputlib.rst b/docs/results.rst similarity index 96% rename from doc/oemof_outputlib.rst rename to docs/results.rst index b5c2bdfad..fb1b65ad7 100644 --- a/doc/oemof_outputlib.rst +++ b/docs/results.rst @@ -9,7 +9,7 @@ results that were part of the outputlib in earlier versions are no longer part o as the requirements to plotting functions greatly depend on individial requirements. Basic functions for plotting of optimisation results are now found in -a separate repository `oemof_visio `_. +a separate repository `oemof_visio `_. The main purpose of the outputlib is to collect and organise results. It gives back the results as a python dictionary holding pandas Series for scalar values and pandas DataFrames for all nodes and flows between them. This way we can make use of the full power of the pandas package available to process the results. @@ -125,27 +125,27 @@ dictionary such that the keys are changed to strings given by the labels: views.convert_keys_to_strings(results) print(results[('wind', 'bus_electricity')]['sequences'] - -Another option is to access data belonging to a grouping by the name of the grouping -(`note also this section on groupings `_. -Given the label of an object, e.g. 'wind' you can access the grouping by its label + +Another option is to access data belonging to a grouping by the name of the grouping +(`note also this section on groupings `_. +Given the label of an object, e.g. 'wind' you can access the grouping by its label and use this to extract data from the results dictionary. .. code-block:: python node_wind = energysystem.groups['wind'] print(results[(node_wind, bus_electricity)]) - -However, in many situations it might be convenient to use the views module to + +However, in many situations it might be convenient to use the views module to collect information on a specific node. You can request all data related to a specific node by using either the node's variable name or its label: - + .. code-block:: python data_wind = outputlib.views.node(results, 'wind') - + A function for collecting and printing meta results, i.e. information on the objective function, the problem and the solver, is provided as well: diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 000000000..f95eb78d8 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,11 @@ +builtin +builtins +classmethod +staticmethod +classmethods +staticmethods +args +kwargs +callstack +Changelog +Indices diff --git a/doc/oemof_solph.rst b/docs/usage.rst similarity index 88% rename from doc/oemof_solph.rst rename to docs/usage.rst index d18f4701d..9283242cf 100644 --- a/doc/oemof_solph.rst +++ b/docs/usage.rst @@ -1,10 +1,12 @@ .. _oemof_solph_label: +.. _using_oemof_label: + ~~~~~~~~~~~ oemof-solph ~~~~~~~~~~~ -Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The packages is based on pyomo. To create an energy system model the :ref:`oemof_network_label` is used and extended by components such as storages. To get started with solph, checkout the examples in the :ref:`solph_examples_label` section. +Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The packages is based on pyomo. To create an energy system model generic and specific components are available. To get started with solph, checkout the examples in the :ref:`solph_examples_label` section. .. contents:: :depth: 2 @@ -15,11 +17,28 @@ Solph is an oemof-package, designed to create and solve linear or mixed-integer How can I use solph? -------------------- -To use solph you have to install oemof and at least one solver, which can be used together with pyomo. See `pyomo installation guide `_. +To use solph you have to install oemof and at least one solver, which can be used together with pyomo (e.g. CBC, GLPK, Gurobi, Cplex). See the `pyomo installation guide `_ for all supported solver. You can test it by executing one of the existing examples. Be aware that the examples require the CBC solver but you can change the solver name in the example files to your solver. Once the example work you are close to your first energy model. + +Handling of Warnings +^^^^^^^^^^^^^^^^^^^^ + +The solph library is designed to be as generic as possible to make it possible +to use it in different use cases. This concept makes it difficult to raise +Error or Warnings because sometimes untypical combinations of parameters are +allowed even though they might be wrong in over 99% of the use cases. + +Therefore, a SuspiciousUsageWarning was introduced. This warning will warn you +if you do something untypical. If you are sure that you know what you are doing +you can switch the warning off. + +See `the debugging module of oemof-tools `_ for more +information. + + Set up an energy system ^^^^^^^^^^^^^^^^^^^^^^^ @@ -52,7 +71,7 @@ Basically, there are two types of *nodes* - *components* and *buses*. Every Comp All solph *components* can be used to set up an energy system model but you should read the documentation of each *component* to learn about usage and restrictions. For example it is not possible to combine every *component* with every *flow*. Furthermore, you can add your own *components* in your application (see below) but we would be pleased to integrate them into solph if they are of general interest. To do so please use the module oemof.solph.custom as described here: http://oemof.readthedocs.io/en/latest/developing_oemof.html#contribute-to-new-components -An example of a simple energy system shows the usage of the nodes for +An example of a simple energy system shows the usage of the nodes for real world representations: .. image:: _files/oemof_solph_example.svg @@ -346,30 +365,32 @@ The :py:class:`~oemof.solph.components.ExtractionTurbineCHP` inherits from the the application example for the component is a flexible combined heat and power (chp) plant. Of course, an instance of this class can represent also another component with one input and two output flows and a flexible ratio between -these flows, leading to the following constraints: +these flows, with the following constraints: -.. include:: ../oemof/solph/components.py +.. include:: ../src/oemof/solph/components.py :start-after: _ETCHP-equations: :end-before: """ -These constraints are applied in addition those of a standard +These constraints are applied in addition to those of a standard :class:`~oemof.solph.network.Transformer`. The constraints limit the range of the possible operation points, like the following picture shows. For a certain flow of fuel, there is a line of operation points, whose slope is defined by -:math:`\beta`. The second constrain limits the decrease of electrical power. +the power loss factor :math:`\beta` (in some contexts also referred to as +:math:`C_v`). The second constraint limits the decrease of electrical power and +incorporates the backpressure coefficient :math:`C_b`. .. image:: _files/ExtractionTurbine_range_of_operation.svg :width: 70 % :alt: variable_chp_plot.svg :align: center - -For now :py:class:`~oemof.solph.components.ExtractionTurbineCHP` instances are -restricted to one input and two output flows. The class allows the definition -of a different efficiency for every time step but the corresponding series has -to be predefined as a parameter for the optimisation. In contrast to the + +For now, :py:class:`~oemof.solph.components.ExtractionTurbineCHP` instances must +have one input and two output flows. The class allows the definition +of a different efficiency for every time step that can be passed as a series +of parameters that are fixed before the optimisation. In contrast to the :class:`~oemof.solph.network.Transformer`, a main flow and a tapped flow is -defined. For the main flow you can define a conversion factor if the second -flow is zero (conversion_factor_single_flow). +defined. For the main flow you can define a separate conversion factor that +applies when the second flow is zero (*`conversion_factor_full_condensation`*). .. code-block:: python @@ -380,13 +401,13 @@ flow is zero (conversion_factor_single_flow). conversion_factors={b_el: 0.3, b_th: 0.5}, conversion_factor_full_condensation={b_el: 0.5}) -The key of the parameter *'conversion_factor_full_condensation'* will indicate the -main flow. In the example above, the flow to the Bus *'b_el'* is the main flow -and the flow to the Bus *'b_th'* is the tapped flow. The following plot shows -how the variable chp (right) schedules it's electrical and thermal power -production in contrast to a fixed chp (left). The plot is the output of an -example in the `oemof example repository -`_. +The key of the parameter *'conversion_factor_full_condensation'* defines which +of the two flows is the main flow. In the example above, the flow to the Bus +*'b_el'* is the main flow and the flow to the Bus *'b_th'* is the tapped flow. +The following plot shows how the variable chp (right) schedules it's electrical +and thermal power production in contrast to a fixed chp (left). The plot is the +output of an example in the `oemof example repository +`_. .. image:: _files/variable_chp_plot.svg :scale: 10 % @@ -398,21 +419,6 @@ example in the `oemof example repository .. _oemof_solph_components_generic_caes_label: -GenericCAES (custom) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Compressed Air Energy Storage (CAES). -The following constraints describe the CAES: - -.. include:: ../oemof/solph/custom.py - :start-after: _GenericCAES-equations: - :end-before: """ - -.. note:: See the :py:class:`~oemof.solph.components.GenericCAES` class for all parameters and the mathematical background. - -.. _oemof_solph_components_generic_chp_label: - - GenericCHP (component) ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -500,13 +506,13 @@ are active in all three cases. Constraint 10 depends on the attribute back_press an equality, if not it is a less or equal. Constraint 11 is only needed for modeling motoric CHP which is done by setting the attribute `H_L_FG_share_min`. -.. include:: ../oemof/solph/components.py +.. include:: ../src/oemof/solph/components.py :start-after: _GenericCHP-equations1-10: :end-before: **For the attribute** If :math:`\dot{H}_{L,FG,min}` is given, e.g. for a motoric CHP: -.. include:: ../oemof/solph/components.py +.. include:: ../src/oemof/solph/components.py :start-after: _GenericCHP-equations11: :end-before: """ @@ -555,7 +561,7 @@ The following code block shows an example of the storage parametrization for the initial_storage_level=0.5, balanced=True, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) -For more information see the definition of the :py:class:`~oemof.solph.components.GenericStorage` class or check the `example repository `_. +For more information see the definition of the :py:class:`~oemof.solph.components.GenericStorage` class or check the `example repository `_. Using an investment object with the GenericStorage component @@ -565,17 +571,17 @@ Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds tw * Invest into the flow parameters e.g. a turbine or a pump * Invest into capacity of the storage e.g. a basin or a battery cell - -Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. - -As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows -with the capacity of the storage. + +Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. + +As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows +with the capacity of the storage. Three parameters are responsible for connecting the flows and the capacity of the storage: * ' `invest_relation_input_capacity` ' fixes the input flow investment to the capacity investment. A ratio of ‘1’ means that the storage can be filled within one time-period. * ' `invest_relation_output_capacity` ' fixes the output flow investment to the capacity investment. A ratio of ‘1’ means that the storage can be emptied within one period. - * ' `invest_relation_input_output` ' fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. - + * ' `invest_relation_input_output` ' fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. + You should not set all 3 parameters at the same time, since it will lead to overdetermination. The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Both flows and the storage itself (representing: pump, turbine, basin) are free in their investment. You can set the parameters to `None` or delete them as `None` is the default value. @@ -659,7 +665,7 @@ linear equation of in- and outflow does not hit the origin, but is offset. By mu the Offset :math:`C_{0}` with the binary status variable of the nonconvex flow, the origin (0, 0) becomes part of the solution space and the boiler is allowed to switch off: -.. include:: ../oemof/solph/components.py +.. include:: ../src/oemof/solph/components.py :start-after: _OffsetTransformer-equations: :end-before: """ @@ -692,6 +698,20 @@ Electrical line. .. _oemof_solph_custom_link_label: +GenericCAES (custom) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed Air Energy Storage (CAES). +The following constraints describe the CAES: + +.. include:: ../src/oemof/solph/custom.py + :start-after: _GenericCAES-equations: + :end-before: """ + +.. note:: See the :py:class:`~oemof.solph.components.GenericCAES` class for all parameters and the mathematical background. + +.. _oemof_solph_components_generic_chp_label: + Link (custom) ^^^^^^^^^^^^^ @@ -779,7 +799,7 @@ Yielding the following results :align: center -.. note:: +.. note:: * This component is a candidate component. It's implemented as a custom component for users that like to use and test the component at early stage. Please report issues to improve the component. @@ -851,6 +871,63 @@ would look like this: from oemof.tools import economics epc = economics.annuity(1000, 20, 0.05) +So far, the investment costs and the installed capacity are mathematically a +line through origin. But what if there is a minimum threshold for doing an +investment, e.g. you cannot buy gas turbines lower than a certain +nominal power, or, the marginal costs of bigger plants +decrease. +Therefore, you can use the parameter *nonconvex* and *offset* of the +investment class. Both, work with investment in flows and storages. Here is an +example of an transformer: + +.. code-block:: python + + trafo = solph.Transformer( + label='transformer_nonconvex', + inputs={bus_0: solph.Flow()}, + outputs={bus_1: solph.Flow( + investment=solph.Investment( + ep_costs=4, + maximum=100, + minimum=20, + nonconvex=True, + offset=400))}, + conversion_factors={bus_1: 0.9}) + +In this examples, it is assumed, that independent of the size of the +transformer, there are always fix investment costs of 400 (€). +The minimum investment size is 20 (kW) +and the costs per installed unit are 4 (€/kW). With this +option, you could theoretically approximate every cost function you want. But +be aware that for every nonconvex investment flow or storage you are using, +an additional binary variable is created. This might boost your computing time +into the limitless. + +The following figures illustrates the use of the nonconvex investment flow. +Here, :math:`c_{invest,fix}` is the *offset* value and :math:`c_{invest,var}` is +the *ep_costs* value: + +.. image:: _files/nonconvex_invest_investcosts_power.svg + :width: 70 % + :alt: nonconvex_invest_investcosts_power.svg + :align: center + +In case of a convex investment (which is the default setting +`nonconvex=Flase`), the *minimum* attribute leads to a forced investment, +whereas in the nonconvex case, the investment can become zero as well. + +The calculation of the specific costs per kilowatt installed capacity results +in the following relation for convex and nonconvex investments: + +.. image:: _files/nonconvex_invest_specific_costs.svg + :width: 70 % + :alt: nonconvex_invest_specific_costs.svg + :align: center + +See :py:class:`~oemof.solph.blocks.InvestmentFlow` and +:py:class:`~oemof.solph.components.GenericInvestmentStorageBlock` for all the +mathematical background, like variables and constraints, which are used. + .. note:: At the moment the investment class is not compatible with the MIP classes :py:class:`~oemof.solph.options.NonConvex`. @@ -858,8 +935,8 @@ Mixed Integer (Linear) Problems ------------------------------- Solph also allows you to model components with respect to more technical details -such as a minimal power production. Therefore, the class -:py:class:`~oemof.solph.options.NonConvex` exists in the +such as a minimal power production. Therefore, the class +:py:class:`~oemof.solph.options.NonConvex` exists in the :py:mod:`~oemof.solph.options` module. Note that the usage of this class is currently not compatible with the :py:class:`~oemof.solph.options.Investment` class. @@ -882,12 +959,12 @@ you have to do is to invoke a class instance inside your Flow() - declaration: b_th: Flow(nominal_value=40)}, conversion_factors={b_el: 0.3, b_th: 0.4}) -The NonConvex() object of the electrical output of the created LinearTransformer will create +The NonConvex() object of the electrical output of the created LinearTransformer will create a 'status' variable for the flow. This will be used to model for example minimal/maximal power production constraints if the attributes `min`/`max` of the flow are set. It will also be used to include start up constraints and costs if corresponding attributes of the class are provided. For more -information see the API of the :py:class:`~oemof.solph.options.NonConvex` class and its corresponding +information see the API of the :py:class:`~oemof.solph.options.NonConvex` class and its corresponding block class :py:class:`~oemof.solph.blocks.NonConvex`. .. note:: The usage of this class can sometimes be tricky as there are many interdenpendencies. So @@ -899,7 +976,7 @@ Adding additional constraints ----------------------------- You can add additional constraints to your :py:class:`~oemof.solph.models.Model`. See `flexible_modelling in the example repository -`_ to learn how to do it. +`_ to learn how to do it. Some predefined additional constraints can be found in the :py:mod:`~oemof.solph.constraints` module. @@ -952,7 +1029,7 @@ The idea is to create different sheets within one spreadsheet file for different Once you have create your specific excel reader you can lower the entry barrier for other users. It is some sort of a GUI in form of platform independent spreadsheet software and to make data and models exchangeable in one archive. -See the `example repository `_ for an excel reader example. +See the `example repository `_ for an excel reader example. .. _solph_examples_label: @@ -960,4 +1037,4 @@ See the `example repository `_ for an e Solph Examples -------------- -See the `example repository `_ for various examples. The repository has sections for each major release. +See the `example repository `_ for various examples. The repository has sections for each major release. diff --git a/doc/whatsnew/v0-0-1.rst b/docs/whatsnew/v0-0-1.rst similarity index 100% rename from doc/whatsnew/v0-0-1.rst rename to docs/whatsnew/v0-0-1.rst diff --git a/doc/whatsnew/v0-0-2.rst b/docs/whatsnew/v0-0-2.rst similarity index 84% rename from doc/whatsnew/v0-0-2.rst rename to docs/whatsnew/v0-0-2.rst index cc374a552..9d8f235d8 100644 --- a/doc/whatsnew/v0-0-2.rst +++ b/docs/whatsnew/v0-0-2.rst @@ -4,21 +4,21 @@ v0.0.2 (December 22, 2015) New features ############ - * Adding a definition of a default oemof logger (`issue #28 `_) - * Revise the EnergySystem class according to the oemof developing meeting (`issue #25 `_) - * Add a dump and restore method to the EnergySystem class to dump/restore its attributes (`issue #31 `_) + * Adding a definition of a default oemof logger (`issue #28 `_) + * Revise the EnergySystem class according to the oemof developing meeting (`issue #25 `_) + * Add a dump and restore method to the EnergySystem class to dump/restore its attributes (`issue #31 `_) * Functionality for minimum up- and downtime constraints (oemof.solph.linear_mixed_integer_constraints module) * Add `relax` option to simulation class for calculation of linear relaxed mixed integer problems * Instances of :class:`EnergySystem ` now keep track of :class:`Entities ` via the :attr:`entities ` attribute. - (`issue #20 `_) + (`issue #20 `_) * There's now a standard way of working with the results obtained via a call to :meth:`OptimizationModel#results `. See its documentation, the documentation of :meth:`EnergySystem#optimize ` and finally the discussion - at `issue #33 `_ for more + at `issue #33 `_ for more information. * New class :class:`VariableEfficiencyCHP ` to model combined heat and power units with variable electrical efficiency. @@ -30,19 +30,19 @@ New features Documentation ############# - * missing docstrings of the core subpackage added (`issue #9 `_) + * missing docstrings of the core subpackage added (`issue #9 `_) * missing figures of the meta-documentation added - * missing content in developer notes (`issue #34 `_) + * missing content in developer notes (`issue #34 `_) Testing -####### +####### Bug fixes ######### * now the api-docs can be read on readthedocs.org - * a storage automically calculates its maximum output/input if the capacity and the c-rate is given (`issue #27 `_) + * a storage automically calculates its maximum output/input if the capacity and the c-rate is given (`issue #27 `_) * Fix error in accessing dual variables in oemof.solph.postprocessing Other changes diff --git a/doc/whatsnew/v0-0-3.rst b/docs/whatsnew/v0-0-3.rst similarity index 72% rename from doc/whatsnew/v0-0-3.rst rename to docs/whatsnew/v0-0-3.rst index 341087f3c..e6860ed90 100644 --- a/doc/whatsnew/v0-0-3.rst +++ b/docs/whatsnew/v0-0-3.rst @@ -4,30 +4,30 @@ v0.0.3 (January 29, 2016) New features ############ - * Added a class to convert the results dictionary to a multiindex DataFrame (`issue #36 `_) - * Added a basic plot library (`issue #36 `_) - * Add logging functionalities (`issue #28 `_) - * Add `entities_from_csv` functionality for creating of entities from csv-files - * Add a time-depended upper bound for the output of a component (`issue #65 `_) - * Add fast_build functionlity for pyomo models in solph module (`issue #68 `_) + * Added a class to convert the results dictionary to a multiindex DataFrame (`issue #36 `_) + * Added a basic plot library (`issue #36 `_) + * Add logging functionalities (`issue #28 `_) + * Add `entities_from_csv` functionality for creating of entities from csv-files + * Add a time-depended upper bound for the output of a component (`issue #65 `_) + * Add fast_build functionlity for pyomo models in solph module (`issue #68 `_) * The package is no longer named `oemof_base` but is now just called `oemof`. * The `results` dictionary stored in the energy system now contains an attribute for the objective function and for objects which have special result attributes, those are now accessible under the object keys, too. - (`issue #58 `_) + (`issue #58 `_) Documentation ############# * Added the Readme.rst as "Getting started" to the documentation. - * Fixed installation description (`issue #38 `_) + * Fixed installation description (`issue #38 `_) * Improved the developer notes. Testing ####### - * With this release we start implementing nosetests (`issue #47 `_ - * Tests added to test constraints and the registration process (`issue #73 `_). + * With this release we start implementing nosetests (`issue #47 `_ + * Tests added to test constraints and the registration process (`issue #73 `_). Bug fixes diff --git a/doc/whatsnew/v0-0-4.rst b/docs/whatsnew/v0-0-4.rst similarity index 91% rename from doc/whatsnew/v0-0-4.rst rename to docs/whatsnew/v0-0-4.rst index ecf05edda..40dc17bc6 100644 --- a/doc/whatsnew/v0-0-4.rst +++ b/docs/whatsnew/v0-0-4.rst @@ -4,8 +4,8 @@ v0.0.4 (March 03, 2016) New features ############ -* Revise the outputlib according to (`issue #54 `_) -* Add postheating device to transport energy between two buses with different temperature levels (`issue #97 `_) +* Revise the outputlib according to (`issue #54 `_) +* Add postheating device to transport energy between two buses with different temperature levels (`issue #97 `_) * Better integration with pandas Documentation @@ -14,7 +14,7 @@ Documentation * Update developer notes Testing -####### +####### * Described testing procedures in developer notes * New constraint tests for heating buses diff --git a/doc/whatsnew/v0-0-5.rst b/docs/whatsnew/v0-0-5.rst similarity index 79% rename from doc/whatsnew/v0-0-5.rst rename to docs/whatsnew/v0-0-5.rst index 32f48b096..4cc13ba1d 100644 --- a/doc/whatsnew/v0-0-5.rst +++ b/docs/whatsnew/v0-0-5.rst @@ -7,10 +7,10 @@ New features * There's now a :class:`flexible transformer ` with two inputs and one output. - (`Issue #116 `_) + (`Issue #116 `_) * You now have the option create special groups of entities in your energy system. The feature is not yet fully implemented, but simple use cases are - usable already. (`Issue #60 `_) + usable already. (`Issue #60 `_) Documentation ############# @@ -34,15 +34,15 @@ Bug fixes * Searching for oemof's configuration directory is now done in a platform independent manner. - (`Issue #122 `_) + (`Issue #122 `_) * Weeks no longer have more than seven days. - (`Issue #126 `_) + (`Issue #126 `_) Other changes ############# -* Oemof has a new dependency: `dill `_. It +* Oemof has a new dependency: `dill `_. It enables serialization of less common types and acts as a drop in replacement for `pickle `_. * Demandlib's API has been simplified. diff --git a/doc/whatsnew/v0-0-6.rst b/docs/whatsnew/v0-0-6.rst similarity index 78% rename from doc/whatsnew/v0-0-6.rst rename to docs/whatsnew/v0-0-6.rst index d585f238c..9f7794210 100644 --- a/doc/whatsnew/v0-0-6.rst +++ b/docs/whatsnew/v0-0-6.rst @@ -6,8 +6,8 @@ New features * It is now possible to choose whether or not the heat load profile generated with the BDEW heat load profile method should only include space heating or space heating and warm water combined. - (`Issue #130 `_) -* Add possibility to change the order of the columns of a DataFrame subset. This is useful to change the order of stacked plots. (`Issue #148 `_) + (`Issue #130 `_) +* Add possibility to change the order of the columns of a DataFrame subset. This is useful to change the order of stacked plots. (`Issue #148 `_) Documentation ############# @@ -15,12 +15,12 @@ Documentation Testing ####### -* Fix constraint tests (`Issue #137 `_) +* Fix constraint tests (`Issue #137 `_) Bug fixes ######### * Use of wrong columns in generation of SF vector in BDEW heat load profile - generation (`Issue #129 `_) + generation (`Issue #129 `_) * Use of wrong temperature vector in generation of h vector in BDEW heat load profile generation. diff --git a/doc/whatsnew/v0-0-7.rst b/docs/whatsnew/v0-0-7.rst similarity index 100% rename from doc/whatsnew/v0-0-7.rst rename to docs/whatsnew/v0-0-7.rst diff --git a/doc/whatsnew/v0-1-0.rst b/docs/whatsnew/v0-1-0.rst similarity index 91% rename from doc/whatsnew/v0-1-0.rst rename to docs/whatsnew/v0-1-0.rst index 795556ce0..740d6f6b1 100644 --- a/doc/whatsnew/v0-1-0.rst +++ b/docs/whatsnew/v0-1-0.rst @@ -1,23 +1,23 @@ v0.1.0 (November 1, 2016) ++++++++++++++++++++++++++ -The framework provides the basis for a great range of different energy -system model types, ranging from LP bottom-up (power and heat) economic dispatch -models with optional investment to MILP operational unit commitment models. - -With v0.1.0 we refactored oemof (not backward compatible!) to bring the -implementation in line with the general concept. Hence, the API of components -has changed significantly and we introduced the new 'Flow' component. Besides -an extensive grouping functionality for automatic creation of constraints based +The framework provides the basis for a great range of different energy +system model types, ranging from LP bottom-up (power and heat) economic dispatch +models with optional investment to MILP operational unit commitment models. + +With v0.1.0 we refactored oemof (not backward compatible!) to bring the +implementation in line with the general concept. Hence, the API of components +has changed significantly and we introduced the new 'Flow' component. Besides +an extensive grouping functionality for automatic creation of constraints based on component input data the documentation has been revised. -We provide examples to show the broad range of possible applications and the -frameworks flexibility. +We provide examples to show the broad range of possible applications and the +frameworks flexibility. API changes ########### - + * The demandlib is no longer part of the oemof package. It has its own package now: (`demandlib `_) @@ -61,12 +61,12 @@ Documentation Testing ####### - * Create a structure to use examples as system tests (`issue #160 `_) + * Create a structure to use examples as system tests (`issue #160 `_) Bug fixes ######### - * Fix relative path of logger (`issue #201 `_) + * Fix relative path of logger (`issue #201 `_) * More path fixes regarding installation via pip @@ -74,7 +74,7 @@ Other changes ############# * Travis CI will now check PR's automatically - * Examples executable from command-line (`issue #227 `_) + * Examples executable from command-line (`issue #227 `_) Contributors diff --git a/doc/whatsnew/v0-1-1.rst b/docs/whatsnew/v0-1-1.rst similarity index 68% rename from doc/whatsnew/v0-1-1.rst rename to docs/whatsnew/v0-1-1.rst index 6d31d3b3c..a8fe7208e 100644 --- a/doc/whatsnew/v0-1-1.rst +++ b/docs/whatsnew/v0-1-1.rst @@ -6,12 +6,12 @@ Hot fix release to make examples executable. Bug fixes ######### - * Fix copy of default logging.ini (`issue #235 `_) - * Add matplotlib to requirements to make examples executable after installation (`issue #236 `_) + * Fix copy of default logging.ini (`issue #235 `_) + * Add matplotlib to requirements to make examples executable after installation (`issue #236 `_) Contributors ############ - + * Guido Plessmann - * Uwe Krien + * Uwe Krien diff --git a/doc/whatsnew/v0-1-2.rst b/docs/whatsnew/v0-1-2.rst similarity index 88% rename from doc/whatsnew/v0-1-2.rst rename to docs/whatsnew/v0-1-2.rst index 49ef6e3af..5c746311e 100644 --- a/doc/whatsnew/v0-1-2.rst +++ b/docs/whatsnew/v0-1-2.rst @@ -4,9 +4,9 @@ v0.1.2 (March 27, 2017) New features ############ -* Revise examples - clearer naming, cleaner code, all examples work with cbc solver (`issue #238 `_, `issue #247 `_). -* Add option to choose solver when executing the examples (`issue #247 `_). -* Add new transformer class: VariableFractionTransformer (child class of LinearTransformer). This class represents transformers with a variable fraction between its output flows. In contrast to the LinearTransformer by now it is restricted to two output flows.(`issue #248 `_) +* Revise examples - clearer naming, cleaner code, all examples work with cbc solver (`issue #238 `_, `issue #247 `_). +* Add option to choose solver when executing the examples (`issue #247 `_). +* Add new transformer class: VariableFractionTransformer (child class of LinearTransformer). This class represents transformers with a variable fraction between its output flows. In contrast to the LinearTransformer by now it is restricted to two output flows.(`issue #248 `_) * Add new transformer class: N1Transformer (counterpart of LinearTransformer). This class allows to have multiple inputflows that are converted into one output flow e.g. heat pumps or mixing-components. * Allow to set addtional flow attributes inside NodesFromCSV in solph inputlib * Add economics module to calculate investment annuities (more to come in future versions) diff --git a/doc/whatsnew/v0-1-4.rst b/docs/whatsnew/v0-1-4.rst similarity index 69% rename from doc/whatsnew/v0-1-4.rst rename to docs/whatsnew/v0-1-4.rst index a218d58ea..e00c1a419 100644 --- a/doc/whatsnew/v0-1-4.rst +++ b/docs/whatsnew/v0-1-4.rst @@ -4,7 +4,7 @@ v0.1.4 (March 28, 2017) Bug fixes ######### -* fix examples (`issue #298 `_) +* fix examples (`issue #298 `_) Documentation ############# @@ -13,6 +13,6 @@ Documentation Contributors ############ - + * Uwe Krien * Stephan Günther diff --git a/doc/whatsnew/v0-2-0.rst b/docs/whatsnew/v0-2-0.rst similarity index 89% rename from doc/whatsnew/v0-2-0.rst rename to docs/whatsnew/v0-2-0.rst index f0534235e..20000c778 100644 --- a/doc/whatsnew/v0-2-0.rst +++ b/docs/whatsnew/v0-2-0.rst @@ -7,14 +7,14 @@ API changes * The `NodesFromCSV` has been removed from the code base. An alternative excel (spreadsheet) reader is provided in the newly created - `excel example in the oemof_examples `_ + `excel example in the oemof_examples `_ repository. * LinearTransformer and LinearN1Transformer classes are now combined within one Transformer class. The new class has n inputs and n outputs. Please note that the definition of the conversion factors (for N1) has changed. See the new docstring of :class:`~oemof.solph.network.Transformer` class to avoid errors - (`Issue #351 `_). -* The :class:`oemof.solph.network.Storage` class has been renamed and moved to + (`Issue #351 `_). +* The :class:`oemof.solph.network.Storage` class has been renamed and moved to :class:`oemof.solph.components.GenericStorage`. * As the example section has been moved to a new repository the `oemof_example` command was removed. Use `oemof_installation_test` to check your installation @@ -32,11 +32,11 @@ API changes `. This option has been made a bit more feature rich by the way, but see below for more on this. Also check the - `oemof_examples `_ repository + `oemof_examples `_ repository for more information on the usage. * The `fixed_costs` attribute of the :class:`nodes ` has been removed. See - (`Issue #407 `_) for more + (`Issue #407 `_) for more information. * The classes :class:`DataFramePlot ` and :class:`ResultsDataFrame ` have been removed @@ -45,14 +45,14 @@ API changes New features ############ -* A new `oemof_examples `_ repository +* A new `oemof_examples `_ repository with some new examples was created. * A new outputlib module has been created to provide a convenient data structure for optimization results and enable quick analyses. All decision variables of a Node are now collected automatically which enables an easier development of custom components. See the revised :ref:`oemof_outputlib_label` documentation for more details or have a look at - the `oemof_examples `_ repository + the `oemof_examples `_ repository for information on the usage. Keep your eyes open, some new functions will come soon that make the processing of the results easier. See the actual pull request or issues for detailed information. @@ -64,7 +64,7 @@ New features constraints or let us know what is needed in the future. * A module to create a networkx graph from your energy system or your optimisation model was added. You can use networkx to plot and analyse graphs. - See :ref:`oemof_graph_label` in the documentation for more information. + See the graph module in the documentation of oemof-network for more information. * It's now possible to modify a :class:`node's ` :attr:`inputs ` and :attr:`outputs ` by inserting and removing @@ -92,13 +92,13 @@ New components Please check your results. Feedback is welcome! * The custom component :class:`Link ` can now be used to model a bidirectional connection within one component. Check out the example in the - `oemof_examples `_ repository. + `oemof_examples `_ repository. * The component :class:`GenericCHP ` can be used to model different CHP types such as extraction or back-pressure turbines and motoric plants. The component uses a mixed-integer linear formulation and can be adapted to different technical layouts with a high level of detail. Check out the example in the - `oemof_examples `_ repository. + `oemof_examples `_ repository. * The component :class:`GenericCAES ` can be used to model different concepts of compressed air energy storage. Technical concepts such as diabatic or adiabatic layouts can be modelled at a high level @@ -122,7 +122,7 @@ Known issues ############ * It is not possible to model one time step with oemof.solph. You have to model at least two timesteps - (`Issue #306 `_). Please leave a + (`Issue #306 `_). Please leave a comment if this bug affects you. Bug fixes @@ -147,9 +147,9 @@ Other changes they are less a necessary part but more an optional tool . Basic plotting examples that show how to quickly create plots from optimization results can now be found in the - `oemof_examples `_ repository. + `oemof_examples `_ repository. You can still find the "old" standard plots within the - `oemof_visio `_ repository as they are + `oemof_visio `_ repository as they are now maintained separately. * A `user forum `_ has been created to answer use questions. diff --git a/doc/whatsnew/v0-2-1.rst b/docs/whatsnew/v0-2-1.rst similarity index 74% rename from doc/whatsnew/v0-2-1.rst rename to docs/whatsnew/v0-2-1.rst index 6aa698a83..3da9af5a7 100644 --- a/doc/whatsnew/v0-2-1.rst +++ b/docs/whatsnew/v0-2-1.rst @@ -8,20 +8,20 @@ API changes * The function create_nx_graph only takes an energysystem as argument, not a solph model. As it is not a major release you can still pass a Model but you should adapt your application as soon as possible. - (`Issue #439 `_) + (`Issue #439 `_) New features ############ * It is now possible determine minimum up and downtimes for nonconvex flows. - Check the `oemof_examples `_ + Check the `oemof_examples `_ repository for an exemplary usage. * Startup and shutdown costs can now be defined time-dependent. * The graph module has been revised. - (`Issue #439 `_) + (`Issue #439 `_) * You can now store a graph to disc as `.graphml` file to open it in yEd with labels. @@ -30,11 +30,11 @@ New features * Two functions `get_node_by_name` and `filter_nodes` have been added that allow to get specified nodes or nodes of one kind from the results - dictionary. (`Issue #426 `_) + dictionary. (`Issue #426 `_) * A new function `param_results()` collects all parameters of nodes and flows in a dictionary similar to the `results` dictionary. - (`Issue #445 `_) + (`Issue #445 `_) * In `outputlib.views.node()`, an option for multiindex dataframe has been added. @@ -50,7 +50,7 @@ Known issues * It is not possible to model one time step with oemof.solph. You have to model at least two timesteps - (`Issue #306 `_). Please leave a + (`Issue #306 `_). Please leave a comment if this bug affects you. @@ -60,7 +60,7 @@ Bug fixes * Shutdown costs for nonconvex flows are now accounted within the objective which was not the case before due to a naming error. * Console script `oemof_test_installation` has been fixed. - (`Issue #452 `_) + (`Issue #452 `_) * Adapt solph to API change in the Pyomo package. * Deserializing a :class:`Node ` leads to an object which was no longer serializable. This is fixed now. :class:`Node @@ -74,7 +74,7 @@ Testing ####### * New console script `test_oemof` has been added (experimental). - (`Issue #453 `_) + (`Issue #453 `_) Other changes @@ -82,17 +82,17 @@ Other changes * Internal change: Unnecessary list extensions while creating a model are avoided, which leads to a decrease in runtime. - (`Issue #438 `_) + (`Issue #438 `_) * The negative/positive gradient attributes are dictionaries. In the constructor they moved from sequences to a new `dictionaries` argument. - (`Issue #437 `_) + (`Issue #437 `_) * License agreement was adapted according to the reuse project - (`Issue #442 `_) + (`Issue #442 `_) * Code of conduct was added. - (`Issue #440 `_) - * Version of required packages is now limited to the most actual version - (`Issue #464 `_) - + (`Issue #440 `_) + * Version of required packages is now limited to the most actual version + (`Issue #464 `_) + Contributors ############ diff --git a/doc/whatsnew/v0-2-2.rst b/docs/whatsnew/v0-2-2.rst similarity index 81% rename from doc/whatsnew/v0-2-2.rst rename to docs/whatsnew/v0-2-2.rst index 772854434..63b3c12aa 100644 --- a/doc/whatsnew/v0-2-2.rst +++ b/docs/whatsnew/v0-2-2.rst @@ -7,7 +7,7 @@ API changes * The storage API has been revised, even though it is still possible to use the old API. In that case a warning is raised - (`Issue #491 `_). + (`Issue #491 `_). * The newly introduced parm_results are not results and therefore renamed to parameter_as_dict. The old name is still valid but raises a warning. @@ -19,7 +19,7 @@ New features It will now be possible to run investment optimization based on already installed capacity of a component. Take a look on :ref:`investment_mode_label` for usage of this option. - (`Issue #489 `_). + (`Issue #489 `_). * Investement variables for the capacity and the flows are now decoupled to enable more flexibility. It is possible to couple the flows to the capacity, @@ -31,10 +31,10 @@ New features `nominal_value` of the Flow classes. The attributes `nominal_input_capacity_ratio` and `nominal_input_capacity_ratio` will be removed in v0.3.0. Please adapt your application to avoid problems in the - future (`Issue #480 `_). + future (`Issue #480 `_). * We now have experimental support for deserializing an energy system from a - tabular `data package `_. Since + tabular `data package `_. Since we have to extend the datapackage format a bit, the specification is not yet finalized and documentation as well as tests range from sparse to nonexistent. But anyone who wishes to help with the code is welcome to check @@ -50,15 +50,15 @@ Documentation ############# * The documentation of the storage - `storage component `_ has been improved. + `storage component `_ has been improved. * The documentation of the - `Extraction Turbine `_ has been improved. + `Extraction Turbine `_ has been improved. Known issues ############ * It is not possible to model one time step with oemof.solph. You have to - model at least two timesteps (`Issue #306 `_). Please leave a comment if this bug affects you. + model at least two timesteps (`Issue #306 `_). Please leave a comment if this bug affects you. Bug fixes ######### @@ -70,7 +70,7 @@ Bug fixes * In the solph constraints module the emission constraint did not include the timeincrement from the model which has now be fixed. * The parameter_as_dict (former: param_results) do work with the views - functions now (`Issue #494 `_). + functions now (`Issue #494 `_). Testing ####### diff --git a/doc/whatsnew/v0-2-3.rst b/docs/whatsnew/v0-2-3.rst similarity index 92% rename from doc/whatsnew/v0-2-3.rst rename to docs/whatsnew/v0-2-3.rst index 69d66dc2d..652df40dd 100644 --- a/doc/whatsnew/v0-2-3.rst +++ b/docs/whatsnew/v0-2-3.rst @@ -4,7 +4,7 @@ v0.2.3 (November 21, 2018) Bug fixes ######### -* Some functions did not work with tuples as labels. It has been fixed for the ExtractionTurbineCHP, the graph module and the parameter_as_dict function. (`Issue #507 `_) +* Some functions did not work with tuples as labels. It has been fixed for the ExtractionTurbineCHP, the graph module and the parameter_as_dict function. (`Issue #507 `_) Contributors ############ diff --git a/doc/whatsnew/v0-3-0.rst b/docs/whatsnew/v0-3-0.rst similarity index 89% rename from doc/whatsnew/v0-3-0.rst rename to docs/whatsnew/v0-3-0.rst index eb7193392..cdc3abb47 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/docs/whatsnew/v0-3-0.rst @@ -6,8 +6,8 @@ API changes ########### * The ``param_results`` function does not exist anymore. It has been renamed to - ``parameter_as_dict`` (`Issue #537 `_). + ``parameter_as_dict`` (`Issue #537 `_). * The storage API has been revised. Please check the `API documentation `_). + `Issue #522 `_). New features ############ * Now it is possible to model just one time step. This is important for time step based models and all other models with an outer loop - (`Issue #519 `_). + (`Issue #519 `_). * The storage can be used unbalanced now, which means that the level at the end could be different to the level at the beginning of the modeled time period. - See the `storage documentation `_ for more details. + See the `storage documentation `_ for more details. * `NonConvexFlow ` can now have `activity_costs`, `maximum_startups`, and `maximum_shutdowns`. @@ -36,7 +36,7 @@ New features * Namedtuples and tuples as labels work now without problems. This makes it much easier to find objects and results in large energy systems - (`Issue #507 `_). + (`Issue #507 `_). * Groups are now fully lazy. This means that groups are only computed when they are accessed. Previously, whenever nodes where added to an @@ -77,7 +77,7 @@ Bug fixes problem if a wrong time index was passed. In that case the timeincrement was set to one without a warning. Now an error is raised if no timeincrement or valid time index is found - (`Issue #549 `_). + (`Issue #549 `_). Testing ####### diff --git a/doc/whatsnew/v0-3-1.rst b/docs/whatsnew/v0-3-1.rst similarity index 100% rename from doc/whatsnew/v0-3-1.rst rename to docs/whatsnew/v0-3-1.rst diff --git a/doc/whatsnew/v0-3-2.rst b/docs/whatsnew/v0-3-2.rst similarity index 94% rename from doc/whatsnew/v0-3-2.rst rename to docs/whatsnew/v0-3-2.rst index c28fc06cb..4234cb6d8 100644 --- a/doc/whatsnew/v0-3-2.rst +++ b/docs/whatsnew/v0-3-2.rst @@ -27,7 +27,7 @@ Other changes * The license hase been changed from GPLv3 to the MIT license * The BaseModel has been revised (test, docstring, warnings, internal naming) - (`PR #605 `_) + (`PR #605 `_) * Style revision to meet pep8 and other pep rules. Contributors diff --git a/docs/whatsnew/v0-3-3.rst b/docs/whatsnew/v0-3-3.rst new file mode 100644 index 000000000..3ac8e2a10 --- /dev/null +++ b/docs/whatsnew/v0-3-3.rst @@ -0,0 +1,51 @@ +v0.3.3 (January 12, 2020) +++++++++++++++++++++++++++ + + +API changes +########### + +* something + +New features +############ + +* :class:`~oemof.solph.GenericStorage` can now have "fixed_losses", that are independent from storage content. +* It is now possible to determine a schedule for a flow. Flow will be pushed + to the schedule, if possible. + +New components +############## + +* something + +Documentation +############# + +* Improved documentation of ExtractionTurbineCHP + +Known issues +############ + +* something + +Bug fixes +######### + +* something + +Testing +####### + +* something + +Other changes +############# + +* something + +Contributors +############ + +* Uwe Krien +* Jann Launer diff --git a/docs/whatsnew/v0-4-0.rst b/docs/whatsnew/v0-4-0.rst new file mode 100644 index 000000000..b20119d7a --- /dev/null +++ b/docs/whatsnew/v0-4-0.rst @@ -0,0 +1,65 @@ +v0.4.0 (April ??, 2020) ++++++++++++++++++++++++++++ + + +API changes +########### + +* something + +New features +############ + +* Allows having a non equidistant timeindex. By adding the + calculate_timeincrement function to tools/helpers.py a non + equidistant timeincrement can be calculated. The EnergySystem + will now be defined by the timeindex and the calculated + timeincrement. + +* Allows non-convex investments for flows and storages. + With this feature, fix investment costs, which do not dependent on the + nominal capacity, can be considered. + +New components +############## + +* something + +Documentation +############# + +* Improved documentation of ExtractionTurbineCHP + +Known issues +############ + +* something + +Bug fixes +######### + +* something + +Testing +####### + +* something + +Other changes +############# + +* The ``loss_rate`` of :class:`~oemof.solph.components.GenericStorage` + is now defined per time increment. +* The parameters' data type in the docstrings is changed from + `numeric (sequence or scalar)` to `numeric (iterable or scalar)` + (`Issue #673 `_). + +Contributors +############ + +* Caterina Köhl +* Jonathan Amme +* Uwe Krien +* Johannes Röder +* Jann Launer +* Daniel Rank diff --git a/nose.cfg b/nose.cfg deleted file mode 100644 index cee6ab774..000000000 --- a/nose.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[nosetests] -with-coverage=1 -cover-package=oemof diff --git a/oemof/__init__.py b/oemof/__init__.py deleted file mode 100644 index 6e64ab19c..000000000 --- a/oemof/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) - -__version__ = '0.4.0dev' diff --git a/oemof/energy_system.py b/oemof/energy_system.py deleted file mode 100644 index 1111b4669..000000000 --- a/oemof/energy_system.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Basic EnergySystem class - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/energy_system.py - -SPDX-License-Identifier: MIT -""" - -from collections import deque -from pickle import UnpicklingError -import logging -import os - -import blinker -import dill as pickle - -from oemof.groupings import DEFAULT as BY_UID, Grouping, Nodes -from oemof.network import Bus - - -class EnergySystem: - r"""Defining an energy supply system to use oemof's solver libraries. - - Note - ---- - The list of regions is not necessary to use the energy system with solph. - - Parameters - ---------- - entities : list of :class:`Entity `, optional - A list containing the already existing :class:`Entities - ` that should be part of the energy system. - Stored in the :attr:`entities` attribute. - Defaults to `[]` if not supplied. - timeindex : pandas.datetimeindex - Define the time range and increment for the energy system. - groupings : list - The elements of this list are used to construct :class:`Groupings - ` or they are used directly if they - are instances of :class:`Grouping `. - These groupings are then used to aggregate the entities added to this - energy system into :attr:`groups`. - By default, there'll always be one group for each :attr:`uid - ` containing exactly the entity with the - given :attr:`uid `. - See the :ref:`examples ` for more information. - - Attributes - ---------- - entities : list of :class:`Entity ` - A list containing the :class:`Entities ` - that comprise the energy system. If this :class:`EnergySystem` is - set as the :attr:`registry ` - attribute, which is done automatically on :class:`EnergySystem` - construction, newly created :class:`Entities - ` are automatically added to this list on - construction. - groups : dict - results : dictionary - A dictionary holding the results produced by the energy system. - Is `None` while no results are produced. - Currently only set after a call to :meth:`optimize` after which it - holds the return value of :meth:`om.results() - `. - See the documentation of that method for a detailed description of the - structure of the results dictionary. - timeindex : pandas.index, optional - Define the time range and increment for the energy system. This is an - optional attribute but might be import for other functions/methods that - use the EnergySystem class as an input parameter. - - - .. _energy-system-examples: - Examples - -------- - - Regardles of additional groupings, :class:`entities - ` will always be grouped by their :attr:`uid - `: - - >>> from oemof.network import Entity - >>> from oemof.network import Bus, Sink - >>> es = EnergySystem() - >>> bus = Bus(label='electricity') - >>> es.add(bus) - >>> bus is es.groups['electricity'] - True - - For simple user defined groupings, you can just supply a function that - computes a key from an :class:`entity ` and the - resulting groups will be sets of :class:`entities - ` stored under the returned keys, like in this - example, where :class:`entities ` are grouped by - their `type`: - - >>> es = EnergySystem(groupings=[type]) - >>> buses = set(Bus(label="Bus {}".format(i)) for i in range(9)) - >>> es.add(*buses) - >>> components = set(Sink(label="Component {}".format(i)) - ... for i in range(9)) - >>> es.add(*components) - >>> buses == es.groups[Bus] - True - >>> components == es.groups[Sink] - True - - """ - - signals = {} - """A dictionary of blinker_ signals emitted by energy systems. - - Currently only one signal is supported. This signal is emitted whenever a - `Node ` is `add`ed to an energy system. The signal's - `sender` is set to the `node ` that got added to the - energy system so that `nodes ` have an easy way to only - receive signals for when they themselves get added to an energy system. - - .. _blinker: https://pythonhosted.org/blinker/ - """ - - def __init__(self, **kwargs): - self._first_ungrouped_node_index_ = 0 - self._groups = {} - self._groupings = ([BY_UID] + - [g if isinstance(g, Grouping) else Nodes(g) - for g in kwargs.get('groupings', [])]) - self.entities = [] - - self.results = kwargs.get('results') - - self.timeindex = kwargs.get('timeindex') - - self.temporal = kwargs.get('temporal') - - self.add(*kwargs.get('entities', ())) - - def add(self, *nodes): - """Add :class:`nodes ` to this energy system.""" - self.nodes.extend(nodes) - for n in nodes: - self.signals[type(self).add].send(n, EnergySystem=self) - signals[add] = blinker.signal(add) - - @property - def groups(self): - gs = self._groups - deque( - ( - g(n, gs) - for g in self._groupings - for n in self.nodes[self._first_ungrouped_node_index_ :] - ), - maxlen=0, - ) - self._first_ungrouped_node_index_ = len(self.nodes) - return self._groups - - @property - def nodes(self): - return self.entities - - @nodes.setter - def nodes(self, value): - self.entities = value - - def flows(self): - return {(source, target): source.outputs[target] - for source in self.nodes - for target in source.outputs} - - def dump(self, dpath=None, filename=None): - r""" Dump an EnergySystem instance. - """ - if dpath is None: - bpath = os.path.join(os.path.expanduser("~"), '.oemof') - if not os.path.isdir(bpath): - os.mkdir(bpath) - dpath = os.path.join(bpath, 'dumps') - if not os.path.isdir(dpath): - os.mkdir(dpath) - - if filename is None: - filename = 'es_dump.oemof' - - pickle.dump(self.__dict__, open(os.path.join(dpath, filename), 'wb')) - - msg = ('Attributes dumped to: {0}'.format(os.path.join( - dpath, filename))) - logging.debug(msg) - return msg - - def restore(self, dpath=None, filename=None): - r""" Restore an EnergySystem instance. - """ - logging.info( - "Restoring attributes will overwrite existing attributes.") - if dpath is None: - dpath = os.path.join(os.path.expanduser("~"), '.oemof', 'dumps') - - if filename is None: - filename = 'es_dump.oemof' - - try: - self.__dict__ = pickle.load( - open(os.path.join(dpath, filename), "rb")) - except UnpicklingError as e: - if str(e) == "state is not a dictionary": - raise UnpicklingError( - "\n " - "Seems like you're trying to load an energy system " - "dumped with an older\n " - "oemof version. Unfortunetaly we made changes which " - "broke this from\n " - "v0.2.2 (more specifically commit `bec669b`) to its " - "successor.\n " - "If you really need this functionality, please file " - "a bug entitled\n\n " - '"Pickle customization removal breaks ' - '`EnergySystem.restore`"\n\n ' - "at\n\n " - "https://github.com/oemof/oemof/issues\n\n " - "or comment on it if it already exists.") - raise e - - msg = ('Attributes restored from: {0}'.format(os.path.join( - dpath, filename))) - logging.debug(msg) - return msg diff --git a/oemof/graph.py b/oemof/graph.py deleted file mode 100644 index e3fd606e6..000000000 --- a/oemof/graph.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Modules for creating and analysing energy system graphs. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/graph.py - -SPDX-License-Identifier: MIT -""" - -import networkx as nx - - -def create_nx_graph(energy_system=None, remove_nodes=None, filename=None, - remove_nodes_with_substrings=None, remove_edges=None): - """ - Create a `networkx.DiGraph` for the passed energy system and plot it. - See http://networkx.readthedocs.io/en/latest/ for more information. - - Parameters - ---------- - energy_system : `oemof.solph.network.EnergySystem` - - filename : str - Absolute filename (with path) to write your graph in the graphml - format. If no filename is given no file will be written. - - remove_nodes: list of strings - Nodes to be removed e.g. ['node1', node2')] - - remove_nodes_with_substrings: list of strings - Nodes that contain substrings to be removed e.g. ['elec', 'heat')] - - remove_edges: list of string tuples - Edges to be removed e.g. [('resource_gas', 'gas_balance')] - - Examples - -------- - >>> import os - >>> import pandas as pd - >>> from oemof.solph import (Bus, Sink, Transformer, Flow, EnergySystem) - >>> import oemof.graph as grph - >>> datetimeindex = pd.date_range('1/1/2017', periods=3, freq='H') - >>> es = EnergySystem(timeindex=datetimeindex) - >>> b_gas = Bus(label='b_gas', balanced=False) - >>> bel1 = Bus(label='bel1') - >>> bel2 = Bus(label='bel2') - >>> demand_el = Sink(label='demand_el', - ... inputs = {bel1: Flow(nominal_value=85, - ... actual_value=[0.5, 0.25, 0.75], - ... fixed=True)}) - >>> pp_gas = Transformer(label=('pp', 'gas'), - ... inputs={b_gas: Flow()}, - ... outputs={bel1: Flow(nominal_value=41, - ... variable_costs=40)}, - ... conversion_factors={bel1: 0.5}) - >>> line_to2 = Transformer(label='line_to2', - ... inputs={bel1: Flow()}, outputs={bel2: Flow()}) - >>> line_from2 = Transformer(label='line_from2', - ... inputs={bel2: Flow()}, outputs={bel1: Flow()}) - >>> es.add(b_gas, bel1, demand_el, pp_gas, bel2, line_to2, line_from2) - >>> my_graph = grph.create_nx_graph(es) - >>> # export graph as .graphml for programs like Yed where it can be - >>> # sorted and customized. this is especially helpful for large graphs - >>> # grph.create_nx_graph(es, filename="my_graph.graphml") - >>> [my_graph.has_node(n) - ... for n in ['b_gas', 'bel1', "('pp', 'gas')", 'demand_el', 'tester']] - [True, True, True, True, False] - >>> list(nx.attracting_components(my_graph)) - [{'demand_el'}] - >>> sorted(list(nx.strongly_connected_components(my_graph))[1]) - ['bel1', 'bel2', 'line_from2', 'line_to2'] - >>> new_graph = grph.create_nx_graph(energy_system=es, - ... remove_nodes_with_substrings=['b_'], - ... remove_nodes=["('pp', 'gas')"], - ... remove_edges=[('bel2', 'line_from2')], - ... filename='test_graph') - >>> [new_graph.has_node(n) - ... for n in ['b_gas', 'bel1', "('pp', 'gas')", 'demand_el', 'tester']] - [False, True, False, True, False] - >>> my_graph.has_edge("('pp', 'gas')", 'bel1') - True - >>> new_graph.has_edge('bel2', 'line_from2') - False - >>> os.remove('test_graph.graphml') - - Notes - ----- - Needs graphviz and networkx (>= v.1.11) to work properly. - Tested on Ubuntu 16.04 x64 and solydxk (debian 9). - """ - # construct graph from nodes and flows - grph = nx.DiGraph() - - # add nodes - for n in energy_system.nodes: - grph.add_node(str(n.label), label=str(n.label)) - - # add labeled flows on directed edge if an optimization_model has been - # passed or undirected edge otherwise - for n in energy_system.nodes: - for i in n.inputs.keys(): - weight = getattr(energy_system.flows()[(i, n)], - 'nominal_value', None) - if weight is None: - grph.add_edge(str(i.label), str(n.label)) - else: - grph.add_edge(str(i.label), str(n.label), - weigth=format(weight, '.2f')) - - # remove nodes and edges based on precise labels - if remove_nodes is not None: - grph.remove_nodes_from(remove_nodes) - if remove_edges is not None: - grph.remove_edges_from(remove_edges) - - # remove nodes based on substrings - if remove_nodes_with_substrings is not None: - for i in remove_nodes_with_substrings: - remove_nodes = [str(v.label) for v in energy_system.nodes - if i in str(v.label)] - grph.remove_nodes_from(remove_nodes) - - if filename is not None: - if filename[-8:] != '.graphml': - filename = filename + '.graphml' - nx.write_graphml(grph, filename) - - return grph diff --git a/oemof/groupings.py b/oemof/groupings.py deleted file mode 100644 index 7114c6f47..000000000 --- a/oemof/groupings.py +++ /dev/null @@ -1,307 +0,0 @@ -# -*- coding: utf-8 -*- - -""" All you need to create groups of stuff in your energy system. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/groupings.py - -SPDX-License-Identifier: MIT -""" - -from collections.abc import (Hashable, Iterable, Mapping, - MutableMapping as MuMa) -from itertools import chain, filterfalse - -from oemof.network import Edge - - -# TODO: Update docstrings. -# -# * Make them easier to understand. -# * Update them to use nodes instead of entities. -# - -class Grouping: - """ - Used to aggregate :class:`entities ` in an - :class:`energy system ` into - :attr:`groups `. - - The way :class:`Groupings ` work is that each :class:`Grouping` - :obj:`g` of an energy system is called whenever an :class:`entity - ` is added to the energy system (and for each - :class:`entity ` already present, if the energy - system is created with existing enties). - The call :obj:`g(e, groups)`, where :obj:`e` is an :class:`entity - ` and :attr:`groups - ` is a dictionary mapping - group keys to groups, then uses the three functions :meth:`key - `, :meth:`value ` and :meth:`merge - ` in the following way: - - - :meth:`key(e) ` is called to obtain a key :obj:`k` - under which the group should be stored, - - :meth:`value(e) ` is called to obtain a value - :obj:`v` (the actual group) to store under :obj:`k`, - - if you supplied a :func:`filter` argument, :obj:`v` is - :func:`filtered ` using that function, - - otherwise, if there is not yet anything stored under - :obj:`groups[k]`, :obj:`groups[k]` is set to :obj:`v`. Otherwise - :meth:`merge ` is used to figure out how to merge - :obj:`v` into the old value of :obj:`groups[k]`, i.e. - :obj:`groups[k]` is set to :meth:`merge(v, groups[k]) - `. - - Instead of trying to use this class directly, have a look at its - subclasses, like :class:`Nodes`, which should cater for most use cases. - - Notes - ----- - - When overriding methods using any of the constructor parameters, you don't - have access to :obj:`self` in the corresponding function. If you need - access to :obj:`self`, subclass :class:`Grouping` and override the methods - in the subclass. - - A :class:`Grouping` may be called more than once on the same object - :obj:`e`, so one should make sure that user defined :class:`Grouping` - :obj:`g` is idempotent, i.e. :obj:`g(e, g(e, d)) == g(e, d)`. - - Parameters - ---------- - - key: callable or hashable - - Specifies (if not callable) or extracts (if callable) a :meth:`key - ` for each :class:`entity ` of - the :class:`energy system `. - - constant_key: hashable (optional) - - Specifies a constant :meth:`key `. Keys specified using - this parameter are not called but taken as is. - - value: callable, optional - - Overrides the default behaviour of :meth:`value `. - - filter: callable, optional - - If supplied, whatever is returned by :meth:`value` is :func:`filtered - ` through this. Mostly useful in conjunction with - static (i.e. non-callable) :meth:`keys `. - See :meth:`filter` for more details. - - merge: callable, optional - - Overrides the default behaviour of :meth:`merge `. - - """ - - def __init__(self, key=None, constant_key=None, filter=None, **kwargs): - if key and constant_key: - raise TypeError( - "Grouping arguments `key` and `constant_key` are " + - " mutually exclusive.") - if constant_key: - self.key = lambda _: constant_key - elif key: - self.key = key - else: - raise TypeError( - "Grouping constructor missing required argument: " + - "one of `key` or `constant_key`.") - self.filter = filter - for kw in ["value", "merge", "filter"]: - if kw in kwargs: - setattr(self, kw, kwargs[kw]) - - def key(self, node): - """ Obtain a key under which to store the group. - - You have to supply this method yourself using the :obj:`key` parameter - when creating :class:`Grouping` instances. - - Called for every :class:`node ` of the energy - system. Expected to return the key (i.e. a valid :class:`hashable`) - under which the group :meth:`value(node) ` will be - stored. If it should be added to more than one group, return a - :class:`list` (or any other non-:class:`hashable `, - :class:`iterable`) containing the group keys. - - Return :obj:`None` if you don't want to store :obj:`e` in a group. - """ - raise NotImplementedError("\n\n" - "There is no default implementation for `Groupings.key`.\n" - "Congratulations, you managed to execute supposedly " - "unreachable code.\n" - "Please let us know by filing a bug at:\n\n " - "https://github.com/oemof/oemof/issues\n") - - def value(self, e): - """ Generate the group obtained from :obj:`e`. - - This methd returns the actual group obtained from :obj:`e`. Like - :meth:`key `, it is called for every :obj:`e` in the - energy system. If there is no group stored under :meth:`key(e) - `, :obj:`groups[key(e)]` is set to :meth:`value(e) - `. Otherwise :meth:`merge(value(e), groups[key(e)]) - ` is called. - - The default returns the :class:`entity ` - itself. - """ - return e - - def merge(self, new, old): - """ Merge a known :obj:`old` group with a :obj:`new` one. - - This method is called if there is already a value stored under - :obj:`group[key(e)]`. In that case, :meth:`merge(value(e), - group[key(e)]) ` is called and should return the new - group to store under :meth:`key(e) `. - - The default behaviour is to raise an error if :obj:`new` and :obj:`old` - are not identical. - """ - if old is new: - return old - raise ValueError("\nGrouping \n " - "{}:{}\nand\n {}:{}\ncollides.\n".format( - id(old), old, id(new), new) + - "Possibly duplicate uids/labels?") - - def filter(self, group): - """ - :func:`Filter ` the group returned by :meth:`value` - before storing it. - - Should return a boolean value. If the :obj:`group` returned by - :meth:`value` is :class:`iterable `, this - function is used (via Python's :func:`builtin filter - `) to select the values which should be retained in - :obj:`group`. If :obj:`group` is not :class:`iterable - `, it is simply called on :obj:`group` itself - and the return value decides whether :obj:`group` is stored - (:obj:`True`) or not (:obj:`False`). - - """ - raise NotImplementedError("\n\n" - "`Groupings.filter` called without being overridden.\n" - "Congratulations, you managed to execute supposedly " - "unreachable code.\n" - "Please let us know by filing a bug at:\n\n " - "https://github.com/oemof/oemof/issues\n") - - def __call__(self, e, d): - k = self.key(e) if callable(self.key) else self.key - if k is None: - return - v = self.value(e) - if isinstance(v, MuMa): - for k in list(filterfalse(self.filter, v)): - v.pop(k) - elif isinstance(v, Mapping): - v = type(v)(dict((k, v[k]) for k in filter(self.filter, v))) - elif isinstance(v, Iterable): - v = type(v)(filter(self.filter, v)) - elif self.filter and not self.filter(v): - return - if not v: - return - for group in (k if (isinstance(k, Iterable) and not - isinstance(k, Hashable)) - else [k]): - d[group] = (self.merge(v, d[group]) if group in d else v) - - -class Nodes(Grouping): - """ - Specialises :class:`Grouping` to group :class:`nodes ` - into :class:`sets `. - """ - def value(self, e): - """ - Returns a :class:`set` containing only :obj:`e`, so groups are - :class:`sets ` of :class:`node `. - """ - return {e} - - def merge(self, new, old): - """ - :meth:`Updates ` :obj:`old` to be the union of :obj:`old` - and :obj:`new`. - """ - return old.union(new) - - -class Flows(Nodes): - """ - Specialises :class:`Grouping` to group the flows connected to :class:`nodes - ` into :class:`sets `. - Note that this specifically means that the :meth:`key `, and - :meth:`value ` functions act on a set of flows. - """ - def value(self, flows): - """ - Returns a :class:`set` containing only :obj:`flows`, so groups are - :class:`sets ` of flows. - """ - return set(flows) - - def __call__(self, n, d): - flows = ( - {n} - if isinstance(n, Edge) - else set(chain(n.outputs.values(), n.inputs.values())) - ) - super().__call__(flows, d) - - -class FlowsWithNodes(Nodes): - """ - Specialises :class:`Grouping` to act on the flows connected to - :class:`nodes ` and create :class:`sets ` of - :obj:`(source, target, flow)` tuples. - Note that this specifically means that the :meth:`key `, and - :meth:`value ` functions act on sets like these. - """ - def value(self, tuples): - """ - Returns a :class:`set` containing only :obj:`tuples`, so groups are - :class:`sets ` of :obj:`tuples`. - """ - return set(tuples) - - def __call__(self, n, d): - tuples = ( - {(n.input, n.output, n)} - if isinstance(n, Edge) - else set( - chain( - ((n, t, f) for (t, f) in n.outputs.items()), - ((s, n, f) for (s, f) in n.inputs.items()), - ) - ) - ) - super().__call__(tuples, d) - - -def _uid_or_str(node_or_entity): - """ Helper function to support the transition from `Entitie`s to `Node`s. - """ - return (node_or_entity.uid if hasattr(node_or_entity, "uid") - else str(node_or_entity)) - -DEFAULT = Grouping(_uid_or_str) -""" The default :class:`Grouping`. - -This one is always present in an :class:`energy system -`. It stores every :class:`entity -` under its :attr:`uid -` and raises an error if another :class:`entity -` with the same :attr:`uid -` get's added to the :class:`energy system -`. -""" diff --git a/oemof/network.py b/oemof/network.py deleted file mode 100644 index ec94816f3..000000000 --- a/oemof/network.py +++ /dev/null @@ -1,441 +0,0 @@ -# -*- coding: utf-8 -*- - -"""This package (along with its subpackages) contains the classes used to model -energy systems. An energy system is modelled as a graph/network of entities -with very specific constraints on which types of entities are allowed to be -connected. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/network.py - -SPDX-License-Identifier: MIT -""" - -from collections import (namedtuple as NT, Mapping, MutableMapping as MM, - UserDict as UD) -from contextlib import contextmanager -from functools import total_ordering - -# TODO: -# -# * Only allow setting a Node's label if `_delay_registration_` is active -# and/or the node is not yet registered. -# * Only allow setting an Edge's input/output if it is None -# * Document the `register` method. Maybe also document the -# `_delay_registration_` attribute and make it official. This could also be -# a good chance to finally use `blinker` to put an event on -# `_delay_registration_` for deletion/assignment to trigger registration. -# I always had the hunch that using blinker could help to straighten out -# that delayed auto registration hack via partial functions. Maybe this -# could be a good starting point for this. -# * Finally get rid of `Entity`. -# - - -class Inputs(MM): - """ A special helper to map `n1.inputs[n2]` to `n2.outputs[n1]`. - """ - def __init__(self, target): - self.target = target - - def __getitem__(self, key): - return key.outputs.__getitem__(self.target) - - def __delitem__(self, key): - return key.outputs.__delitem__(self.target) - - def __setitem__(self, key, value): - return key.outputs.__setitem__(self.target, value) - - def __iter__(self): - return iter(self.target._in_edges) - - def __len__(self): - return self.target._in_edges.__len__() - - def __repr__(self): - return repr("<{0.__module__}.{0.__name__}: {1!r}>" - .format(type(self), dict(self))) - - -class Outputs(UD): - """ Helper that intercepts modifications to update `Inputs` symmetrically. - """ - def __init__(self, source): - self.source = source - super().__init__() - - def __delitem__(self, key): - key._in_edges.remove(self.source) - return super().__delitem__(key) - - def __setitem__(self, key, value): - key._in_edges.add(self.source) - return super().__setitem__(key, value) - - -@total_ordering -class Node: - """ Represents a Node in an energy system graph. - - Abstract superclass of the two general types of nodes of an energy system - graph, collecting attributes and operations common to all types of nodes. - Users should neither instantiate nor subclass this, but use - :class:`Component`, :class:`Bus`, :class:`Edge` or one of their subclasses - instead. - - .. role:: python(code) - :language: python - - Parameters - ---------- - label: `hashable`, optional - Used as the string representation of this node. If this parameter is - not an instance of :class:`str` it will be converted to a string and - the result will be used as this node's :attr:`label`, which should be - unique with respect to the other nodes in the energy system graph this - node belongs to. If this parameter is not supplied, the string - representation of this node will instead be generated based on this - nodes `class` and `id`. - inputs: list or dict, optional - Either a list of this nodes' input nodes or a dictionary mapping input - nodes to corresponding inflows (i.e. input values). - outputs: list or dict, optional - Either a list of this nodes' output nodes or a dictionary mapping - output nodes to corresponding outflows (i.e. output values). - - Attributes - ---------- - __slots__: str or iterable of str - See the Python documentation on `__slots__ - `_ for more - information. - """ - - registry = None - __slots__ = ["_label", "_in_edges", "_inputs", "_outputs"] - - def __init__(self, *args, **kwargs): - args = list(args) - args.reverse - self._inputs = Inputs(self) - self._outputs = Outputs(self) - for optional in ['label']: - if optional in kwargs: - if args: - raise(TypeError(( - "{}.__init__()\n" - " got multiple values for argument '{}'") - .format(type(self), optional))) - setattr(self, '_' + optional, kwargs[optional]) - else: - if args: - setattr(self, '_' + optional, args.pop()) - self._in_edges = set() - for i in kwargs.get('inputs', {}): - assert isinstance(i, Node), ( - "\n\nInput\n\n {!r}\n\nof\n\n {!r}\n\n" - "not an instance of Node, but of {}." - ).format(i, self, type(i)) - self._in_edges.add(i) - try: - flow = kwargs['inputs'].get(i) - except AttributeError: - flow = None - edge = globals()['Edge'].from_object(flow) - edge.input=i - edge.output=self - for o in kwargs.get('outputs', {}): - assert isinstance(o, Node), ( - "\n\nOutput\n\n {!r}\n\nof\n\n {!r}\n\n" - "not an instance of Node, but of {}." - ).format(o, self, type(o)) - try: - flow = kwargs['outputs'].get(o) - except AttributeError: - flow = None - edge = globals()['Edge'].from_object(flow) - edge.input = self - edge.output = o - - self.register() - """ - This could be slightly more efficient than the loops above, but doesn't - play well with the assertions: - - inputs = kwargs.get('inputs', {}) - self.in_edges = { - Edge(input=i, output=self, - flow=None if not isinstance(inputs, MM) else inputs[i]) - for i in inputs} - - outputs = kwargs.get('outputs', {}) - self.out_edges = { - Edge(input=self, output=o, - flow=None if not isinstance(outputs, MM) else outputs[o]) - for o in outputs} - self.edges = self.in_edges.union(self.out_edges) - """ - - def register(self): - if ( __class__.registry is not None and - not getattr(self, "_delay_registration_", False)): - __class__.registry.add(self) - - def __eq__(self, other): - return id(self) == id(other) - - def __lt__(self, other): - return str(self) < str(other) - - def __hash__(self): - return hash(self.label) - - def __str__(self): - return str(self.label) - - def __repr__(self): - return repr("<{0.__module__}.{0.__name__}: {1!r}>" - .format(type(self), self.label)) - - @property - def label(self): - """ object : - If this node was given a `label` on construction, this - attribute holds the actual object passed as a parameter. Otherwise - :py:`node.label` is a synonym for :py:`str(node)`. - """ - return (self._label if hasattr(self, "_label") - else "<{} #0x{:x}>".format(type(self).__name__, id(self))) - - @label.setter - def label(self, label): - self._label = label - - @property - def inputs(self): - """ dict: - Dictionary mapping input :class:`Nodes ` :obj:`n` to - :class:`Edge`s from :obj:`n` into :obj:`self`. - If :obj:`self` is an :class:`Edge`, returns a dict containing the - :class:`Edge`'s single input node as the key and the flow as the value. - """ - return self._inputs - - @property - def outputs(self): - """ dict: - Dictionary mapping output :class:`Nodes ` :obj:`n` to - :class:`Edges` from :obj:`self` into :obj:`n`. - If :obj:`self` is an :class:`Edge`, returns a dict containing the - :class:`Edge`'s single output node as the key and the flow as the value. - """ - return self._outputs - - -EdgeLabel = NT("EdgeLabel", ['input', 'output']) -class Edge(Node): - """ :class:`Bus`es/:class:`Component`s are always connected by an :class:`Edge`. - - :class:`Edge`s connect a single non-:class:`Edge` Node with another. They - are directed and have a (sequence of) value(s) attached to them so they can - be used to represent a flow from a source/an input to a target/an output. - - Parameters - ---------- - input, output: :class:`Bus` or :class:`Component`, optional - flow, values: object, optional - The (list of) object(s) representing the values flowing from this - edge's input into its output. Note that these two names are aliases of - each other, so `flow` and `values` are mutually exclusive. - - Note that all of these parameters are also set as attributes with the same - name. - """ - Label = EdgeLabel - def __init__(self, input=None, output=None, flow=None, values=None, - **kwargs): - if flow is not None and values is not None: - raise ValueError( - "\n\n`Edge`'s `flow` and `values` keyword arguments are " - "aliases of each other,\nso they're mutually exclusive.\n" - "You supplied:\n" + - " `flow` : {}\n".format(flow) + - " `values`: {}\n".format(values) + - "Choose one.") - if input is None or output is None: - self._delay_registration_ = True - super().__init__(label=Edge.Label(input, output)) - self.values = values if values is not None else flow - if input is not None and output is not None: - input.outputs[output] = self - - @classmethod - def from_object(klass, o): - """ Creates an `Edge` instance from a single object. - - This method inspects its argument and does something different - depending on various cases: - - * If `o` is an instance of `Edge`, `o` is returned unchanged. - * If `o` is a `Mapping`, the instance is created by calling - `klass(**o)`, - * In all other cases, `o` will be used as the `values` keyword - argument to `Edge`s constructor. - """ - if isinstance(o, Edge): - return o - elif isinstance(o, Mapping): - return klass(**o) - else: - return Edge(values=o) - - @property - def flow(self): - return self.values - - @flow.setter - def flow(self, values): - self.values = values - - @property - def input(self): - return self.label.input - - @input.setter - def input(self, i): - old_input = self.input - self.label = Edge.Label(i, self.label.output) - if old_input is None and i is not None and self.output is not None: - del self._delay_registration_ - self.register() - i.outputs[self.output] = self - - @property - def output(self): - return self.label.output - - @output.setter - def output(self, o): - old_output = self.output - self.label = Edge.Label(self.label.input, o) - if old_output is None and o is not None and self.input is not None: - del self._delay_registration_ - if __class__.registry is not None: - __class__.registry.add(self) - o.inputs[self.input] = self - - -class Bus(Node): - pass - - -class Component(Node): - pass - - -class Sink(Component): - pass - - -class Source(Component): - pass - - -class Transformer(Component): - pass - - -# TODO: Adhere to PEP 0257 by listing the exported classes with a short -# summary. -class Entity: - r""" - The most abstract type of vertex in an energy system graph. Since each - entity in an energy system has to be uniquely identifiable and - connected (either via input or via output) to at least one other - entity, these properties are collected here so that they are shared - with descendant classes. - - Parameters - ---------- - uid : string or tuple - Unique component identifier of the entity. - inputs : list - List of Entities acting as input to this Entity. - outputs : list - List of Entities acting as output from this Entity. - geo_data : shapely.geometry object - Geo-spatial data with informations for location/region-shape. The - geometry can be a polygon/multi-polygon for regions, a line fore - transport objects or a point for objects such as transformer sources. - - Attributes - ---------- - registry: :class:`EnergySystem ` - The central registry keeping track of all :class:`Node's ` - created. If this is `None`, :class:`Node` instances are not - kept track of. Assign an :class:`EnergySystem - ` to this attribute to have it - become the a :class:`node ` registry, i.e. all :class:`nodes - ` created are added to its :attr:`nodes - ` - property on construction. - """ - optimization_options = {} - - registry = None - - def __init__(self, **kwargs): - # TODO: @Günni: - # add default argument values to docstrings (if it's possible). - self.uid = kwargs["uid"] - self.inputs = kwargs.get("inputs", []) - self.outputs = kwargs.get("outputs", []) - for e_in in self.inputs: - if self not in e_in.outputs: - e_in.outputs.append(self) - for e_out in self.outputs: - if self not in e_out.inputs: - e_out.inputs.append(self) - self.geo_data = kwargs.get("geo_data", None) - self.regions = [] - self.add_regions(kwargs.get('regions', [])) - if __class__.registry is not None: - __class__.registry.add(self) - - # TODO: @Gunni Yupp! Add docstring. - def add_regions(self, regions): - """Add regions to self.regions - """ - self.regions.extend(regions) - for region in regions: - if self not in region.entities: - region.entities.append(self) - - def __str__(self): - # TODO: @Günni: Unused privat method. No Docstring. - return "<{0} #{1}>".format(type(self).__name__, self.uid) - - -@contextmanager -def registry_changed_to(r): - """ Override registry during execution of a block and restore it afterwards. - """ - backup = Node.registry - Node.registry = r - yield - Node.registry = backup - - -def temporarily_modifies_registry(f): - """ Decorator that disables `Node` registration during `f`'s execution. - - It does so by setting `Node.registry` to `None` while `f` is executing, so - `f` can freely set `Node.registry` to something else. The registration's - original value is restored afterwards. - """ - def result(*xs, **ks): - with registry_changed_to(None): - return f(*xs, **ks) - return result diff --git a/oemof/outputlib/__init__.py b/oemof/outputlib/__init__.py deleted file mode 100644 index be1895318..000000000 --- a/oemof/outputlib/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from oemof.outputlib import processing -from oemof.outputlib import views diff --git a/oemof/solph/__init__.py b/oemof/solph/__init__.py deleted file mode 100644 index 388cbc82a..000000000 --- a/oemof/solph/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from oemof.solph.network import (Sink, Source, Transformer, Bus, Flow, - EnergySystem) -from oemof.solph.models import Model -from oemof.solph.groupings import GROUPINGS -from oemof.solph.options import Investment, NonConvex -from oemof.solph.plumbing import sequence -from oemof.solph import components -from oemof.solph import custom -from oemof.solph import constraints diff --git a/oemof/tools/__init__.py b/oemof/tools/__init__.py deleted file mode 100644 index 2295f961a..000000000 --- a/oemof/tools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from oemof.tools import logger -from oemof.tools import economics -from oemof.tools import helpers diff --git a/oemof/tools/economics.py b/oemof/tools/economics.py deleted file mode 100644 index d617371f4..000000000 --- a/oemof/tools/economics.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Module to collect useful functions for economic calculation. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/tools/economics.py - -SPDX-License-Identifier: MIT -""" - - -def annuity(capex, n, wacc, u=None, cost_decrease=0): - """Calculates the annuity of an initial investment 'capex', considering the - cost of capital 'wacc' during a project horizon 'n' - - In case of a single initial investment, the employed formula reads: - - .. math:: - annuity = capex \cdot \frac{(wacc \cdot (1+wacc)^n)} - {((1 + wacc)^n - 1)} - - In case of repeated investments (due to replacements) at fixed intervals - 'u', the formula yields: - - .. math:: - annuity = capex \cdot \frac{(wacc \cdot (1+wacc)^n)} - {((1 + wacc)^n - 1)} \cdot \left( - \frac{1 - \left( \frac{(1-cost\_decrease)} - {(1+wacc)} \right)^n} - {1 - \left( \frac{(1-cost\_decrease)}{(1+wacc)} - \right)^u} \right) - - Parameters - ---------- - capex : float - Capital expenditure for first investment. Net Present Value (NPV) or - Net Present Cost (NPC) of investment - n : int - Horizon of the analysis, or number of years the annuity wants to be - obtained for (n>=1) - wacc : float - Weighted average cost of capital (0=1) - cost_decrease : float - Annual rate of cost decrease (due to, e.g., price experience curve). - This only influences the result for investments corresponding to - replacements, whenever u 1) or (u < 1) or - (cost_decrease < 0 or cost_decrease > 1)): - raise ValueError("Input arguments for 'annuity' out of bounds!") - - return ( - capex * (wacc*(1+wacc)**n) / ((1 + wacc)**n - 1) * - ((1 - ((1-cost_decrease)/(1+wacc))**n) / - (1 - ((1-cost_decrease)/(1+wacc))**u))) diff --git a/oemof/tools/logger.py b/oemof/tools/logger.py deleted file mode 100644 index e48e2e7b0..000000000 --- a/oemof/tools/logger.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 - -"""Helpers to log your modeling process with oemof. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/oemof/tools/logger.py - -SPDX-License-Identifier: MIT -""" - -import os -from logging import (INFO, DEBUG, getLogger, Formatter, StreamHandler, - handlers, debug, info) -import sys -from oemof.tools import helpers -import oemof - - -def define_logging(logpath=None, logfile='oemof.log', file_format=None, - screen_format=None, file_datefmt=None, screen_datefmt=None, - screen_level=INFO, file_level=DEBUG, - log_version=True, log_path=True, timed_rotating=None): - - r"""Initialise customisable logger. - - Parameters - ---------- - logfile : str - Name of the log file, default: oemof.log - logpath : str - The path for log files. By default a ".oemof' folder is created in your - home directory with subfolder called 'log_files'. - file_format : str - Format of the file output. - Default: "%(asctime)s - %(levelname)s - %(module)s - %(message)s" - screen_format : str - Format of the screen output. - Default: "%(asctime)s-%(levelname)s-%(message)s" - file_datefmt : str - Format of the datetime in the file output. Default: None - screen_datefmt : str - Format of the datetime in the screen output. Default: "%H:%M:%S" - screen_level : int - Level of logging to stdout. Default: 20 (logging.INFO) - file_level : int - Level of logging to file. Default: 10 (logging.DEBUG) - log_version : boolean - If True the actual version or commit is logged while initialising the - logger. - log_path : boolean - If True the used file path is logged while initialising the logger. - timed_rotating : dict - Option to pass parameters to the TimedRotatingFileHandler. - - - Returns - ------- - str : Place where the log file is stored. - - Notes - ----- - By default the INFO level is printed on the screen and the DEBUG level - in a file, but you can easily configure the logger. - Every module that wants to create logging messages has to import the - logging module. The oemof logger module has to be imported once to - initialise it. - - Examples - -------- - To define the default logger you have to import the python logging - library and this function. The first logging message should be the - path where the log file is saved to. - - >>> import logging - >>> from oemof.tools import logger - >>> mypath = logger.define_logging( - ... log_path=True, log_version=True, timed_rotating={'backupCount': 4}, - ... screen_level=logging.ERROR, screen_datefmt = "no_date") - >>> mypath[-9:] - 'oemof.log' - >>> logging.debug("Hallo") - """ - - if logpath is None: - logpath = helpers.extend_basic_path('log_files') - - file = os.path.join(logpath, logfile) - - log = getLogger('') - - # Remove existing handlers to avoid interference. - log.handlers = [] - log.setLevel(DEBUG) - - if file_format is None: - file_format = ( - "%(asctime)s - %(levelname)s - %(module)s - %(message)s") - file_formatter = Formatter(file_format, file_datefmt) - - if screen_format is None: - screen_format = "%(asctime)s-%(levelname)s-%(message)s" - if screen_datefmt is None: - screen_datefmt = "%H:%M:%S" - screen_formatter = Formatter(screen_format, screen_datefmt) - - tmp_formatter = Formatter("%(message)s") - - ch = StreamHandler(sys.stdout) - ch.setFormatter(screen_formatter) - ch.setLevel(screen_level) - log.addHandler(ch) - - timed_rotating_p = { - 'when': 'midnight', - 'backupCount': 10} - - if timed_rotating is not None: - timed_rotating_p.update(timed_rotating) - - fh = handlers.TimedRotatingFileHandler(file, **timed_rotating_p) - fh.setFormatter(tmp_formatter) - fh.setLevel(file_level) - log.addHandler(fh) - - debug("******************************************************") - fh.setFormatter(file_formatter) - if log_path: - info("Path for logging: {0}".format(file)) - - if log_version: - info("Used oemof version: {0}".format(get_version())) - return file - - -def get_version(): - """Returns a string part of the used version. If the commit and the branch - is available the commit and the branch will be returned otherwise the - version number. - - >>> from oemof.tools import logger - >>> v = logger.get_version() - >>> type(v) - - """ - try: - return check_git_branch() - except FileNotFoundError: - return "{0}".format(check_version()) - - -def check_version(): - """Returns the actual version number of the used oemof version. - - >>> from oemof.tools import logger - >>> v = logger.check_version() - >>> int(v.split('.')[0]) - 0 - """ - try: - version = oemof.__version__ - except AttributeError: - version = 'No version found due to internal error.' - return version - - -def check_git_branch(): - """Passes the used branch and commit to the logger - - The following test reacts on a local system different than on Travis-CI. - Therefore, a try/except test is created. - - >>> from oemof.tools import logger - >>> try: - ... v = logger.check_git_branch() - ... except FileNotFoundError: - ... v = 'dsfafasdfsdf' - >>> type(v) - - """ - - path = os.path.join(os.path.dirname( - os.path.realpath(__file__)), os.pardir, - os.pardir, '.git') - - # Reads the name of the branch - f_branch = os.path.join(path, 'HEAD') - f = open(f_branch, "r") - first_line = f.readlines(1) - name_full = first_line[0].replace("\n", "") - name_branch = name_full.replace("ref: refs/heads/", "") - f.close() - - # Reads the code of the last commit used - f_commit = os.path.join(path, 'refs', 'heads', name_branch) - f = open(f_commit, "r") - last_commit = f.read(8) - f.close() - - return "{0}@{1}".format(last_commit, name_branch) diff --git a/setup.cfg b/setup.cfg index 5aef279b9..4b7474e43 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,105 @@ +[bdist_wheel] +universal = 1 + +[flake8] +max-line-length = 79 +exclude = */migrations/* + +[tool:pytest] +# If a pytest section is found in one of the possible config files +# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, +# so if you add a pytest config section elsewhere, +# you will need to delete this section from setup.cfg. +norecursedirs = + .git + .tox + .env + dist + build + migrations + +python_files = + test_*.py + *_test.py + *_tests.py + tests.py +addopts = + -ra + --strict + --ignore=docs/conf.py + --ignore=setup.py + --ignore=ci + --ignore=.eggs + --doctest-modules + --doctest-glob=\*.rst + --tb=short + --pyargs +# The order of these options matters. testpaths comes after addopts so that +# oemof.solph in testpaths is interpreted as +# --pyargs oemof.solph. +# Any tests in the src/ directory (that is, tests installed with the package) +# can be run by any user with pytest --pyargs oemof.solph. +# Packages that are sensitive to the host machine, most famously NumPy, +# include tests with the installed package so that any user can check +# at any time that everything is working properly. +# If you do choose to make installable tests, this will run the installed +# tests as they are actually installed (same principle as when we ensure that +# we always test the installed version of the package). +# If you have no need for this (and your src/ directory is very large), +# you can save a few milliseconds on testing by telling pytest not to search +# the src/ directory by removing +# --pyargs and oemof.solph from the options here. +testpaths = + oemof.solph + tests/ + [metadata] description-file = README.rst + +[tool:isort] +force_single_line = True +line_length = 79 +known_first_party = oemof-solph +default_section = THIRDPARTY +forced_separate = test_oemof-solph +not_skip = __init__.py +skip = migrations + +[matrix] +# This is the configuration for the `./bootstrap.py` script. +# It generates `.travis.yml`, `tox.ini` and `.appveyor.yml`. +# +# Syntax: [alias:] value [!variable[glob]] [&variable[glob]] +# +# alias: +# - is used to generate the tox environment +# - it's optional +# - if not present the alias will be computed from the `value` +# value: +# - a value of "-" means empty +# !variable[glob]: +# - exclude the combination of the current `value` with +# any value matching the `glob` in `variable` +# - can use as many you want +# &variable[glob]: +# - only include the combination of the current `value` +# when there's a value matching `glob` in `variable` +# - can use as many you want + +python_versions = + py36 + py37 + py38 + +dependencies = +# 1.4: Django==1.4.16 !python_versions[py3*] +# 1.5: Django==1.5.11 +# 1.6: Django==1.6.8 +# 1.7: Django==1.7.1 !python_versions[py26] +# Deps commented above are provided as examples. That's what you would use in a Django project. + +coverage_flags = + cover: true + nocov: false +environment_variables = + - diff --git a/setup.py b/setup.py index df6883818..23108cec4 100644 --- a/setup.py +++ b/setup.py @@ -1,42 +1,96 @@ -#! /usr/bin/env python +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function -""" -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/setup.py +import io +import re +from glob import glob +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext -SPDX-License-Identifier: MIT -""" +from setuptools import find_packages +from setuptools import setup -from setuptools import find_packages, setup -import os -import oemof +def read(*names, **kwargs): + with io.open( + join(dirname(__file__), *names), + encoding=kwargs.get("encoding", "utf8"), + ) as fh: + return fh.read() -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -setup(name='oemof', - version=oemof.__version__, - author='oemof developer group', - author_email='oemof@rl-institut.de', - description='The open energy modelling framework', - url='https://oemof.org/', - namespace_package=['oemof'], - long_description=read('README.rst'), - long_description_content_type='text/x-rst', - packages=find_packages(), - license='MIT', - install_requires=['blinker < 2.0', - 'dill < 0.4', - 'numpy >= 1.7.0, < 1.18', - 'pandas >= 0.18.0, < 0.26', - 'pyomo >= 4.4.0, < 5.7', - 'networkx < 3.0'], - extras_require={'dev': ['nose', 'sphinx', 'sphinx_rtd_theme']}, - entry_points={ - 'console_scripts': [ - 'oemof_installation_test = ' + - 'oemof.tools.console_scripts:check_oemof_installation']}) +setup( + name="oemof.solph", + version="0.4.0.dev0", + license="MIT", + description=( + "A model generator for energy system modelling and optimisation." + ), + long_description="%s\n%s" + % ( + re.compile("^.. start-badges.*^.. end-badges", re.M | re.S).sub( + "", read("README.rst") + ), + re.sub(":[a-z]+:`~?(.*?)`", r"``\1``", read("CHANGELOG.rst")), + ), + long_description_content_type="text/x-rst", + author="oemof developer group", + author_email="contact@oemof.org", + url="https://oemof.org", + packages=["oemof"] + ["oemof." + p for p in find_packages("src/oemof")], + package_dir={"": "src"}, + py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], + include_package_data=True, + zip_safe=False, + classifiers=[ + # complete classifier list: + # http://pypi.python.org/pypi?%3Aaction=list_classifiers + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: Unix", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Utilities", + ], + project_urls={ + "Documentation": "https://oemofsolph.readthedocs.io/", + "Changelog": ( + "https://oemofsolph.readthedocs.io/en/latest/changelog.html" + ), + "Issue Tracker": "https://github.com/uvchik/oemof.solph/issues", + }, + keywords=[ + # eg: 'keyword1', 'keyword2', 'keyword3', + ], + python_requires=">=3.6", + install_requires=[ + "blinker", + "dill", + "numpy", + "pandas", + "pyomo >= 4.4.0, < 6.0", + "networkx", + "oemof.tools", + ("oemof.network@https://github.com/oemof/oemof.network/archive/" + "uvchik-test-branch.zip"), + ], + extras_require={"dev": ["pytest", "sphinx", "sphinx_rtd_theme", ], + "dummy": ["oemof"]}, + entry_points={ + "console_scripts": [ + "oemof_installation_test = " + + "oemof.tools.console_scripts:check_oemof_installation" + ] + }, +) diff --git a/src/oemof/solph/__init__.py b/src/oemof/solph/__init__.py new file mode 100644 index 000000000..d70c47999 --- /dev/null +++ b/src/oemof/solph/__init__.py @@ -0,0 +1,21 @@ +__version__ = "0.4.0.dev0" + +from . import constraints # noqa: F401 +from . import custom # noqa: F401 +from .components import ExtractionTurbineCHP # noqa: F401 +from .components import GenericCHP # noqa: F401 +from .components import GenericStorage # noqa: F401 +from .components import OffsetTransformer # noqa: F401 +from .groupings import GROUPINGS # noqa: F401 +from .models import Model # noqa: F401 +from .network import Bus # noqa: F401 +from .network import EnergySystem # noqa: F401 +from .network import Flow # noqa: F401 +from .network import Sink # noqa: F401 +from .network import Source # noqa: F401 +from .network import Transformer # noqa: F401 +from .options import Investment # noqa: F401 +from .options import NonConvex # noqa: F401 +from .plumbing import sequence # noqa: F401 +from .processing import parameter_as_dict # noqa: F401 +from .processing import results # noqa: F401 diff --git a/oemof/solph/blocks.py b/src/oemof/solph/blocks.py similarity index 77% rename from oemof/solph/blocks.py rename to src/oemof/solph/blocks.py index c9da5417f..df35958eb 100644 --- a/oemof/solph/blocks.py +++ b/src/oemof/solph/blocks.py @@ -10,8 +10,14 @@ SPDX-License-Identifier: MIT """ -from pyomo.core import (Var, Set, Constraint, BuildAction, Expression, - NonNegativeReals, Binary, NonNegativeIntegers) +from pyomo.core import Binary +from pyomo.core import BuildAction +from pyomo.core import Constraint +from pyomo.core import Expression +from pyomo.core import NonNegativeIntegers +from pyomo.core import NonNegativeReals +from pyomo.core import Set +from pyomo.core import Var from pyomo.core.base.block import SimpleBlock @@ -237,10 +243,10 @@ def _negative_gradient_flow_rule(model): self.negative_gradient_build = BuildAction( rule=_negative_gradient_flow_rule) - def _integer_flow_rule(block, i, o, t): + def _integer_flow_rule(block, ii, oi, ti): """Force flow variable to NonNegativeInteger values. """ - return self.integer_flow[i, o, t] == m.flow[i, o, t] + return self.integer_flow[ii, oi, ti] == m.flow[ii, oi, ti] self.integer_flow_constr = Constraint(self.INTEGER_FLOWS, m.TIMESTEPS, rule=_integer_flow_rule) @@ -273,7 +279,8 @@ def _objective_expression(self): for i, o in m.FLOWS: if m.flows[i, o].variable_costs[0] is not None: for t in m.TIMESTEPS: - variable_costs += (m.flow[i, o, t] * m.objective_weighting[t] * + variable_costs += (m.flow[i, o, t] * + m.objective_weighting[t] * m.flows[i, o].variable_costs[t]) if m.flows[i, o].positive_gradient['ub'][0] is not None: @@ -303,80 +310,193 @@ def _objective_expression(self): class InvestmentFlow(SimpleBlock): r"""Block for all flows with :attr:`investment` being not None. - **The following sets are created:** (-> see basic sets at - :class:`.Model` ) + See :class:`oemof.solph.options.Investment` for all parameters of the + *Investment* class. - FLOWS - A set of flows with the attribute :attr:`invest` of type - :class:`.options.Investment`. - FIXED_FLOWS - A set of flow with the attribute :attr:`fixed` set to `True` - SUMMED_MAX_FLOWS - A subset of set FLOWS with flows with the attribute :attr:`summed_max` - being not None. - SUMMED_MIN_FLOWS - A subset of set FLOWS with flows with the attribute - :attr:`summed_min` being not None. - MIN_FLOWS - A subset of FLOWS with flows having set a value of not None in the - first timestep. + See :class:`oemof.solph.network.Flow` for all parameters of the *Flow* + class. - **The following variables are created:** + **Variables** - invest :attr:`om.InvestmentFlow.invest[i, o]` - Value of the investment variable i.e. equivalent to the nominal - value of the flows after optimization (indexed by FLOWS) + All *InvestmentFlow* are indexed by a starting and ending node + :math:`(i, o)`, which is omitted in the following for the sake + of convenience. The following variables are created: - **The following constraints are build:** + * :math:`P(t)` + + Actual flow value (created in :class:`oemof.solph.models.BaseModel`). + + * :math:`P_{invest}` + + Value of the investment variable, i.e. equivalent to the nominal + value of the flows after optimization. + + * :math:`b_{invest}` + + Binary variable for the status of the investment, if + :attr:`nonconvex` is `True`. + + **Constraints** + + Depending on the attributes of the *InvestmentFlow* and *Flow*, different + constraints are created. The following constraint is created for all + *InvestmentFlow*:\ + + Upper bound for the flow value - Actual value constraint for fixed invest - flows :attr:`om.InvestmentFlow.fixed[i, o, t]` .. math:: - flow(i, o, t) = actual\_value(i, o, t) \cdot invest(i, o), \\ - \forall (i, o) \in \textrm{FIXED\_FLOWS}, \\ - \forall t \in \textrm{TIMESTEPS}. + P(t) \le ( P_{invest} + P_{exist} ) \cdot f_{max}(t) + + Depeding on the attribute :attr:`nonconvex`, the constraints for the bounds + of the decision variable :math:`P_{invest}` are different:\ + + * :attr:`nonconvex = False` - Lower bound (min) constraint for invest flows - :attr:`om.InvestmentFlow.min[i, o, t]` .. math:: - flow(i, o, t) \geq min(i, o, t) \cdot invest(i, o), \\ - \forall (i, o) \in \textrm{MIN\_FLOWS}, \\ - \forall t \in \textrm{TIMESTEPS}. + P_{invest, min} \le P_{invest} \le P_{invest, max} + + * :attr:`nonconvex = True` - Upper bound (max) constraint for invest flows - :attr:`om.InvestmentFlow.max[i, o, t]` .. math:: - flow(i, o, t) \leq max(i, o, t) \cdot invest(i, o), \\ - \forall (i, o) \in \textrm{FLOWS}, \\ - \forall t \in \textrm{TIMESTEPS}. + & + P_{invest, min} \cdot b_{invest} \le P_{invest}\\ + & + P_{invest} \le P_{invest, max} \cdot b_{invest}\\ + + For all *InvestmentFlow* (independent of the attribute :attr:`nonconvex`), + the following additional constraints are created, if the appropriate + attribute of the *Flow* (see :class:`oemof.solph.network.Flow`) is set: + + * :attr:`fixed=True` + + Actual value constraint for investments with fixed flow values - Flow max sum for invest flow - :attr:`om.InvestmentFlow.summed_max[i, o]` .. math:: - \sum_t flow(i, o, t) \cdot \tau \leq summed\_max(i, o) \ - \cdot invest(i, o) \\ - \forall (i, o) \in \textrm{SUMMED\_MAX\_FLOWS}. + P(t) = ( P_{invest} + P_{exist} ) \cdot f_{actual}(t) + + * :attr:`min != 0` + + Lower bound for the flow values - Flow min sum for invest flow :attr:`om.InvestmentFlow.summed_min[i, o]` .. math:: - \sum_t flow(i, o, t) \cdot \tau \geq summed\_min(i, o) \ - \cdot invest(i, o) \\ - \forall (i, o) \in \textrm{SUMMED\_MIN\_FLOWS}. + P(t) \geq ( P_{invest} + P_{exist} ) \cdot f_{min}(t) + * :attr:`summed_max is not None` - **The following parts of the objective function are created:** + Upper bound for the sum of all flow values (e.g. maximum full load + hours) - Equivalent periodical costs (epc) expression - :attr:`om.InvestmentFlow.investment_costs`: .. math:: - \sum_{i, o} invest(i, o) \cdot ep\_costs(i, o) + \sum_t P(t) \cdot \tau(t) \leq ( P_{invest} + P_{exist} ) + \cdot f_{sum, min} - The expression can be accessed by :attr:`om.InvestmentFlow.variable_costs` - and their value after optimization by - :meth:`om.InvestmentFlow.variable_costs()` . This works similar for - investment costs with :attr:`*.investment_costs` etc. - """ + * :attr:`summed_min is not None` + + Lower bound for the sum of all flow values (e.g. minimum full load + hours) + .. math:: + \sum_t P(t) \cdot \tau(t) \geq ( P_{invest} + P_{exist} ) + \cdot f_{sum, min} + + + **Objective function** + + The part of the objective function added by the *InvestmentFlow* + also depends on whether a convex or nonconvex + *InvestmentFlow* is selected. The following parts of the objective function + are created: + + * :attr:`nonconvex = False` + + .. math:: + P_{invest} \cdot c_{invest,var} + + * :attr:`nonconvex = True` + + .. math:: + P_{invest} \cdot c_{invest,var} + + c_{invest,fix} \cdot b_{invest}\\ + + The total value of all costs of all *InvestmentFlow* can be retrieved + calling :meth:`om.InvestmentFlow.investment_costs.expr()`. + + .. csv-table:: List of Variables (in csv table syntax) + :header: "symbol", "attribute", "explanation" + :widths: 1, 1, 1 + + ":math:`P(t)`", ":py:obj:`flow[n, o, t]`", "Actual flow value" + ":math:`P_{invest}`", ":py:obj:`invest[i, o]`", "Invested flow + capacity" + ":math:`b_{invest}`", ":py:obj:`invest_status[i, o]`", "Binary status + of investment" + + List of Variables (in rst table syntax): + + =================== ============================= ========= + symbol attribute explanation + =================== ============================= ========= + :math:`P(t)` :py:obj:`flow[n, o, t]` Actual flow value + + :math:`P_{invest}` :py:obj:`invest[i, o]` Invested flow capacity + + :math:`b_{invest}` :py:obj:`invest_status[i, o]` Binary status of investment + + =================== ============================= ========= + + Grid table style: + + +--------------------+-------------------------------+-----------------------------+ + | symbol | attribute | explanation | + +====================+===============================+=============================+ + | :math:`P(t)` | :py:obj:`flow[n, o, t]` | Actual flow value | + +--------------------+-------------------------------+-----------------------------+ + | :math:`P_{invest}` | :py:obj:`invest[i, o]` | Invested flow capacity | + +--------------------+-------------------------------+-----------------------------+ + | :math:`b_{invest}` | :py:obj:`invest_status[i, o]` | Binary status of investment | + +--------------------+-------------------------------+-----------------------------+ + + .. csv-table:: List of Parameters + :header: "symbol", "attribute", "explanation" + :widths: 1, 1, 1 + + ":math:`P_{exist}`", ":py:obj:`flows[i, o].investment.existing`", " + Existing flow capacity" + ":math:`P_{invest,min}`", ":py:obj:`flows[i, o].investment.minimum`", " + Minimum investment capacity" + ":math:`P_{invest,max}`", ":py:obj:`flows[i, o].investment.maximum`", " + Maximum investment capacity" + ":math:`c_{invest,var}`", ":py:obj:`flows[i, o].investment.ep_costs` + ", "Variable investment costs" + ":math:`c_{invest,fix}`", ":py:obj:`flows[i, o].investment.offset`", " + Fix investment costs" + ":math:`f_{actual}`", ":py:obj:`flows[i, o].actual_value[t]`", "Normed + fixed value for the flow variable" + ":math:`f_{max}`", ":py:obj:`flows[i, o].max[t]`", "Normed maximum + value of the flow" + ":math:`f_{min}`", ":py:obj:`flows[i, o].min[t]`", "Normed minimum + value of the flow" + ":math:`f_{sum,max}`", ":py:obj:`flows[i, o].summed_max`", "Specific + maximum of summed flow values (per installed capacity)" + ":math:`f_{sum,min}`", ":py:obj:`flows[i, o].summed_min`", "Specific + minimum of summed flow values (per installed capacity)" + ":math:`\tau(t)`", ":py:obj:`timeincrement[t]`", "Time step width for + each time step" + + Note + ---- + In case of a nonconvex investment flow (:attr:`nonconvex=True`), + the existing flow capacity :math:`P_{exist}` needs to be zero. + At least, it is not tested yet, whether this works out, or makes any sense + at all. + + Note + ---- + See also :class:`oemof.solph.network.Flow`, + :class:`oemof.solph.blocks.Flow` and + :class:`oemof.solph.options.Investment` + + """ # noqa: E501 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -397,18 +517,24 @@ def _create(self, group=None): m = self.parent_block() # ######################### SETS ##################################### - self.FLOWS = Set(initialize=[(g[0], g[1]) for g in group]) + self.INVESTFLOWS = Set(initialize=[(g[0], g[1]) for g in group]) + + self.CONVEX_INVESTFLOWS = Set(initialize=[ + (g[0], g[1]) for g in group if g[2].investment.nonconvex is False]) + + self.NON_CONVEX_INVESTFLOWS = Set(initialize=[ + (g[0], g[1]) for g in group if g[2].investment.nonconvex is True]) - self.FIXED_FLOWS = Set( + self.FIXED_INVESTFLOWS = Set( initialize=[(g[0], g[1]) for g in group if g[2].fixed]) - self.SUMMED_MAX_FLOWS = Set(initialize=[ + self.SUMMED_MAX_INVESTFLOWS = Set(initialize=[ (g[0], g[1]) for g in group if g[2].summed_max is not None]) - self.SUMMED_MIN_FLOWS = Set(initialize=[ + self.SUMMED_MIN_INVESTFLOWS = Set(initialize=[ (g[0], g[1]) for g in group if g[2].summed_min is not None]) - self.MIN_FLOWS = Set(initialize=[ + self.MIN_INVESTFLOWS = Set(initialize=[ (g[0], g[1]) for g in group if ( g[2].min[0] != 0 or len(g[2].min) > 1)]) @@ -416,12 +542,18 @@ def _create(self, group=None): def _investvar_bound_rule(block, i, o): """Rule definition for bounds of invest variable. """ - return (m.flows[i, o].investment.minimum, - m.flows[i, o].investment.maximum) - # create variable bounded for flows with investement attribute - self.invest = Var(self.FLOWS, within=NonNegativeReals, + if (i, o) in self.CONVEX_INVESTFLOWS: + return (m.flows[i, o].investment.minimum, + m.flows[i, o].investment.maximum) + elif (i, o) in self.NON_CONVEX_INVESTFLOWS: + return 0, m.flows[i, o].investment.maximum + + # create invest variable for a investment flow + self.invest = Var(self.INVESTFLOWS, within=NonNegativeReals, bounds=_investvar_bound_rule) + # create status variable for a non-convex investment flow + self.invest_status = Var(self.NON_CONVEX_INVESTFLOWS, within=Binary) # ######################### CONSTRAINTS ############################### # TODO: Add gradient constraints @@ -430,10 +562,12 @@ def _investflow_fixed_rule(block, i, o, t): """Rule definition of constraint to fix flow variable of investment flow to (normed) actual value """ - return (m.flow[i, o, t] == ( - (m.flows[i, o].investment.existing + self.invest[i, o]) * - m.flows[i, o].actual_value[t])) - self.fixed = Constraint(self.FIXED_FLOWS, m.TIMESTEPS, + expr = (m.flow[i, o, t] == ( + (m.flows[i, o].investment.existing + self.invest[i, o]) * + m.flows[i, o].actual_value[t])) + + return expr + self.fixed = Constraint(self.FIXED_INVESTFLOWS, m.TIMESTEPS, rule=_investflow_fixed_rule) def _max_investflow_rule(block, i, o, t): @@ -442,9 +576,9 @@ def _max_investflow_rule(block, i, o, t): """ expr = (m.flow[i, o, t] <= ( (m.flows[i, o].investment.existing + self.invest[i, o]) * - m.flows[i, o].max[t])) + m.flows[i, o].max[t])) return expr - self.max = Constraint(self.FLOWS, m.TIMESTEPS, + self.max = Constraint(self.INVESTFLOWS, m.TIMESTEPS, rule=_max_investflow_rule) def _min_investflow_rule(block, i, o, t): @@ -453,9 +587,9 @@ def _min_investflow_rule(block, i, o, t): """ expr = (m.flow[i, o, t] >= ( (m.flows[i, o].investment.existing + self.invest[i, o]) * - m.flows[i, o].min[t])) + m.flows[i, o].min[t])) return expr - self.min = Constraint(self.MIN_FLOWS, m.TIMESTEPS, + self.min = Constraint(self.MIN_INVESTFLOWS, m.TIMESTEPS, rule=_min_investflow_rule) def _summed_max_investflow_rule(block, i, o): @@ -465,9 +599,10 @@ def _summed_max_investflow_rule(block, i, o): expr = (sum(m.flow[i, o, t] * m.timeincrement[t] for t in m.TIMESTEPS) <= m.flows[i, o].summed_max * ( - self.invest[i, o] + m.flows[i, o].investment.existing)) + self.invest[i, o] + + m.flows[i, o].investment.existing)) return expr - self.summed_max = Constraint(self.SUMMED_MAX_FLOWS, + self.summed_max = Constraint(self.SUMMED_MAX_INVESTFLOWS, rule=_summed_max_investflow_rule) def _summed_min_investflow_rule(block, i, o): @@ -476,10 +611,11 @@ def _summed_min_investflow_rule(block, i, o): """ expr = (sum(m.flow[i, o, t] * m.timeincrement[t] for t in m.TIMESTEPS) >= - ((m.flows[i, o].investment.existing + self.invest[i, o]) * + ((m.flows[i, o].investment.existing + + self.invest[i, o]) * m.flows[i, o].summed_min)) return expr - self.summed_min = Constraint(self.SUMMED_MIN_FLOWS, + self.summed_min = Constraint(self.SUMMED_MIN_INVESTFLOWS, rule=_summed_min_investflow_rule) def _objective_expression(self): @@ -487,18 +623,21 @@ def _objective_expression(self): class:`.Investment`. The returned costs are fixed, variable and investment costs. """ - if not hasattr(self, 'FLOWS'): + if not hasattr(self, 'INVESTFLOWS'): return 0 m = self.parent_block() investment_costs = 0 - for i, o in self.FLOWS: - if m.flows[i, o].investment.ep_costs is not None: - investment_costs += (self.invest[i, o] * - m.flows[i, o].investment.ep_costs) - else: - raise ValueError("Missing value for investment costs!") + for i, o in self.CONVEX_INVESTFLOWS: + investment_costs += ( + self.invest[i, o] * m.flows[i, o].investment.ep_costs) + for i, o in self.NON_CONVEX_INVESTFLOWS: + investment_costs += ( + self.invest[i, o] * + m.flows[i, o].investment.ep_costs + + self.invest_status[i, o] * + m.flows[i, o].investment.offset) self.investment_costs = Expression(expr=investment_costs) return investment_costs @@ -533,21 +672,21 @@ def _create(self, group=None): m = self.parent_block() - I = {} - O = {} + ins = {} + outs = {} for n in group: - I[n] = [i for i in n.inputs] - O[n] = [o for o in n.outputs] + ins[n] = [i for i in n.inputs] + outs[n] = [o for o in n.outputs] def _busbalance_rule(block): for t in m.TIMESTEPS: - for n in group: - lhs = sum(m.flow[i, n, t] for i in I[n]) - rhs = sum(m.flow[n, o, t] for o in O[n]) + for g in group: + lhs = sum(m.flow[i, g, t] for i in ins[g]) + rhs = sum(m.flow[g, o, t] for o in outs[g]) expr = (lhs == rhs) # no inflows no outflows yield: 0 == 0 which is True if expr is not True: - block.balance.add((n, t), expr) + block.balance.add((g, t), expr) self.balance = Constraint(group, m.TIMESTEPS, noruleinit=True) self.balance_build = BuildAction(rule=_busbalance_rule) @@ -640,7 +779,7 @@ class NonConvexFlow(SimpleBlock): STARTUPFLOWS A subset of set NONCONVEX_FLOWS with the attribute :attr:`maximum_startups` or :attr:`startup_costs` - being not None. + being not None. MAXSTARTUPFLOWS A subset of set STARTUPFLOWS with the attribute :attr:`maximum_startups` being not None. @@ -695,7 +834,7 @@ class NonConvexFlow(SimpleBlock): \forall (i,o) \in \textrm{STARTUPFLOWS}. Maximum startups constraint - :attr:`om.NonConvexFlow.max_startup_constr[i,o,t]` + :attr:`om.NonConvexFlow.max_startup_constr[i,o,t]` .. math:: \sum_{t \in \textrm{TIMESTEPS}} startup(i, o, t) \leq \ N_{start}(i,o) @@ -709,7 +848,7 @@ class NonConvexFlow(SimpleBlock): \forall (i, o) \in \textrm{SHUTDOWNFLOWS}. Maximum shutdowns constraint - :attr:`om.NonConvexFlow.max_startup_constr[i,o,t]` + :attr:`om.NonConvexFlow.max_startup_constr[i,o,t]` .. math:: \sum_{t \in \textrm{TIMESTEPS}} startup(i, o, t) \leq \ N_{shutdown}(i,o) diff --git a/oemof/solph/components.py b/src/oemof/solph/components.py similarity index 77% rename from oemof/solph/components.py rename to src/oemof/solph/components.py index bd5d1bcf9..1b3175d2c 100644 --- a/oemof/solph/components.py +++ b/src/oemof/solph/components.py @@ -12,68 +12,71 @@ """ import numpy as np +from oemof.network import network +from oemof.solph.network import Transformer as solph_Transformer +from oemof.solph.options import Investment +from oemof.solph.plumbing import sequence as solph_sequence from pyomo.core.base.block import SimpleBlock -from pyomo.environ import (Binary, Set, NonNegativeReals, Var, Constraint, - Expression, BuildAction) - -from oemof import network -from oemof.solph import Transformer as solph_Transformer -from oemof.solph import sequence as solph_sequence -from oemof.solph import Investment +from pyomo.environ import Binary +from pyomo.environ import BuildAction +from pyomo.environ import Constraint +from pyomo.environ import Expression +from pyomo.environ import NonNegativeReals +from pyomo.environ import Set +from pyomo.environ import Var class GenericStorage(network.Transformer): - """ + r""" Component `GenericStorage` to model with basic characteristics of storages. Parameters ---------- - nominal_storage_capacity : numeric + nominal_storage_capacity : numeric, :math:`E_{nom}` Absolute nominal capacity of the storage - invest_relation_input_capacity : numeric or None + invest_relation_input_capacity : numeric or None, :math:`r_{cap,in}` Ratio between the investment variable of the input Flow and the - investment variable of the storage. + investment variable of the storage: + :math:`\dot{E}_{in,invest} = E_{invest} \cdot r_{cap,in}` - .. math:: input\_invest = - capacity\_invest \cdot invest\_relation\_input\_capacity - - invest_relation_output_capacity : numeric or None + invest_relation_output_capacity : numeric or None, :math:`r_{cap,out}` Ratio between the investment variable of the output Flow and the - investment variable of the storage. - - .. math:: output\_invest = - capacity\_invest \cdot invest\_relation\_output\_capacity + investment variable of the storage: + :math:`\dot{E}_{out,invest} = E_{invest} \cdot r_{cap,out}` - invest_relation_input_output : numeric or None + invest_relation_input_output : numeric or None, :math:`r_{in,out}` Ratio between the investment variable of the output Flow and the investment variable of the input flow. This ratio used to fix the flow investments to each other. Values < 1 set the input flow lower than the output and > 1 will set the input flow higher than the output flow. If None no relation - will be set. + will be set: + :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}` - .. math:: input\_invest = - output\_invest \cdot invest\_relation\_input\_output - - initial_storage_level : numeric + initial_storage_level : numeric, :math:`c(-1)` The content of the storage in the first time step of optimization. - balanced : boolian + balanced : boolean Couple storage level of first and last time step. (Total inflow and total outflow are balanced.) - loss_rate : numeric (sequence or scalar) - The relative loss of the storage capacity from between two consecutive - timesteps. - inflow_conversion_factor : numeric (sequence or scalar) + loss_rate : numeric (iterable or scalar) + The relative loss of the storage capacity per timeunit. + fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)` + Losses independent of state of charge between two consecutive + timesteps relative to nominal storage capacity. + fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)` + Losses independent of state of charge and independent of + nominal storage capacity between two consecutive timesteps. + inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)` The relative conversion factor, i.e. efficiency associated with the inflow of the storage. - outflow_conversion_factor : numeric (sequence or scalar) + outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)` see: inflow_conversion_factor - min_storage_level : numeric (sequence or scalar) + min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)` The minimum storaged energy of the storage as fraction of the nominal storage capacity (between 0 and 1). To set different values in every time step use a sequence. - max_storage_level : numeric (sequence or scalar) + max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)` see: min_storage_level investment : :class:`oemof.solph.options.Investment` object Object indicating if a nominal_value of the flow is determined by @@ -132,6 +135,10 @@ def __init__(self, *args, self.initial_storage_level = kwargs.get('initial_storage_level') self.balanced = kwargs.get('balanced', True) self.loss_rate = solph_sequence(kwargs.get('loss_rate', 0)) + self.fixed_losses_relative = solph_sequence( + kwargs.get('fixed_losses_relative', 0)) + self.fixed_losses_absolute = solph_sequence( + kwargs.get('fixed_losses_absolute', 0)) self.inflow_conversion_factor = solph_sequence( kwargs.get('inflow_conversion_factor', 1)) self.outflow_conversion_factor = solph_sequence( @@ -153,7 +160,7 @@ def __init__(self, *args, # Check for old parameter names. This is a temporary fix and should # be removed once a general solution is found. - # TODO: https://github.com/oemof/oemof/issues/560 + # TODO: https://github.com/oemof/oemof-solph/issues/560 renamed_parameters = [ ('nominal_capacity', 'nominal_storage_capacity'), ('initial_capacity', 'initial_storage_level'), @@ -200,6 +207,13 @@ def _check_invest_attributes(self): e2 = ("Overdetermined. Three investment object will be coupled" "with three constraints. Set one invest relation to 'None'.") raise AttributeError(e2) + if (self.investment and + sum(solph_sequence(self.fixed_losses_absolute)) != 0 and + self.investment.existing == 0 and + self.investment.minimum == 0): + e3 = ("With fixed_losses_absolute > 0, either investment.existing " + "or investment.minimum has to be non-zero.") + raise AttributeError(e3) self._set_flows() @@ -247,7 +261,9 @@ class GenericStorageBlock(SimpleBlock): Storage balance :attr:`om.Storage.balance[n, t]` .. math:: E(t) = &E(t-1) \cdot - (1 - \delta(t))) \\ + (1 - \beta(t)) ^{\tau(t)/(t_u)} \\ + &- \gamma(t)\cdot E_{nom} \cdot {\tau(t)/(t_u)}\\ + &- \delta(t) \cdot {\tau(t)/(t_u)}\\ &- \frac{\dot{E}_o(t)}{\eta_o(t)} \cdot \tau(t) + \dot{E}_i(t) \cdot \eta_i(t) \cdot \tau(t) @@ -270,8 +286,17 @@ class GenericStorageBlock(SimpleBlock): initial time step :math:`c_{min}(t)` minimum allowed storage :py:obj:`min_storage_level[t]` :math:`c_{max}(t)` maximum allowed storage :py:obj:`max_storage_level[t]` - :math:`\delta(t)` fraction of lost energy :py:obj:`loss_rate[t]` - (e.g. leakage) per time + :math:`\beta(t)` fraction of lost energy :py:obj:`loss_rate[t]` + as share of + :math:`E(t)` + per time unit + :math:`\gamma(t)` fixed loss of energy :py:obj:`fixed_losses_relative[t]` + relative to + :math:`E_{nom}` per + time unit + :math:`\delta(t)` absolute fixed loss :py:obj:`fixed_losses_absolute[t]` + of energy per + time unit :math:`\dot{E}_i(t)` energy flowing in :py:obj:`inputs` :math:`\dot{E}_o(t)` energy flowing out :py:obj:`outputs` :math:`\eta_i(t)` conversion factor :py:obj:`inflow_conversion_factor[t]` @@ -280,7 +305,13 @@ class GenericStorageBlock(SimpleBlock): :math:`\eta_o(t)` conversion factor when :py:obj:`outflow_conversion_factor[t]` (i.e. efficiency) taking stored energy - :math:`\tau(t)` length of the time step + :math:`\tau(t)` duration of time step + :math:`t_u` time unit of losses + :math:`\beta(t)`, + :math:`\gamma(t)` + :math:`\delta(t)` and + timeincrement + :math:`\tau(t)` =========================== ======================= ========= **The following parts of the objective function are created:** @@ -288,7 +319,7 @@ class GenericStorageBlock(SimpleBlock): Nothing added to the objective function. - """ + """ # noqa: E501 CONSTRAINT_GROUP = True @@ -358,7 +389,10 @@ def _storage_balance_first_rule(block, n): expr = 0 expr += block.capacity[n, 0] expr += - block.init_cap[n] * ( - 1 - n.loss_rate[0]) + 1 - n.loss_rate[0]) ** m.timeincrement[0] + expr += (n.fixed_losses_relative[0] * n.nominal_storage_capacity * + m.timeincrement[0]) + expr += n.fixed_losses_absolute[0] * m.timeincrement[0] expr += (- m.flow[i[n], n, 0] * n.inflow_conversion_factor[0]) * m.timeincrement[0] expr += (m.flow[n, o[n], 0] / @@ -375,7 +409,10 @@ def _storage_balance_rule(block, n, t): expr = 0 expr += block.capacity[n, t] expr += - block.capacity[n, t-1] * ( - 1 - n.loss_rate[t]) + 1 - n.loss_rate[t]) ** m.timeincrement[t] + expr += (n.fixed_losses_relative[t] * n.nominal_storage_capacity * + m.timeincrement[t]) + expr += n.fixed_losses_absolute[t] * m.timeincrement[t] expr += (- m.flow[i[n], n, t] * n.inflow_conversion_factor[t]) * m.timeincrement[t] expr += (m.flow[n, o[n], t] / @@ -388,7 +425,7 @@ def _balanced_storage_rule(block, n): """capacity of last time step == initial capacity if balanced""" return block.capacity[n, m.TIMESTEPS[-1]] == block.init_cap[n] self.balanced_cstr = Constraint(self.STORAGES_BALANCED, - rule=_balanced_storage_rule) + rule=_balanced_storage_rule) def _power_coupled(block, n): """Rule definition for constraint to connect the input power @@ -415,90 +452,220 @@ def _objective_expression(self): class GenericInvestmentStorageBlock(SimpleBlock): - r"""Storage with an :class:`.Investment` object. + r"""Block for all storages with :attr:`Investment` being not None. + See :class:`oemof.solph.options.Investment` for all parameters of the + Investment class. - **The following sets are created:** (-> see basic sets at - :class:`.Model` ) + **Variables** - INVESTSTORAGES - A set with all storages containing an Investment object. - INVEST_REL_CAP_IN - A set with all storages containing an Investment object with coupled - investment of input power and storage capacity - INVEST_REL_CAP_OUT - A set with all storages containing an Investment object with coupled - investment of output power and storage capacity - INVEST_REL_IN_OUT - A set with all storages containing an Investment object with coupled - investment of input and output power - INITIAL_STORAGE_LEVEL - A subset of the set INVESTSTORAGES where elements of the set have an - initial_storage_level attribute. - MIN_INVESTSTORAGES - A subset of INVESTSTORAGES where elements of the set have an - min_storage_level attribute greater than zero for at least one - time step. + All Storages are indexed by :math:`n`, which is omitted in the following + for the sake of convenience. + The following variables are created as attributes of + :attr:`om.InvestmentStorage`: - **The following variables are created:** + * :math:`P_i(t)` - capacity :attr:`om.InvestmentStorage.capacity[n, t]` - Level of the storage (indexed by STORAGES and TIMESTEPS) + Inflow of the storage + (created in :class:`oemof.solph.models.BaseModel`). - invest :attr:`om.InvestmentStorage.invest[n, t]` - Nominal capacity of the storage (indexed by STORAGES) + * :math:`P_o(t)` + Outflow of the storage + (created in :class:`oemof.solph.models.BaseModel`). - **The following constraints are build:** + * :math:`E(t)` - Storage balance - Same as for :class:`.GenericStorageBlock`. + Energy currently stored / Absolute level of storaged energy. + * :math:`E_{invest}` + + Invested (nominal) capacity of the storage. + + * :math:`E(-1)` + + Initial storage capacity (before timestep 0). + + * :math:`b_{invest}` + + Binary variable for the status of the investment, if + :attr:`nonconvex` is `True`. + + **Constraints** + + The following constraints are created for all investment storages: + + Storage balance (Same as for :class:`.GenericStorageBlock`) + + .. math:: E(t) = &E(t-1) \cdot + (1 - \beta(t)) ^{\tau(t)/(t_u)} \\ + &- \gamma(t)\cdot (E_{exist} + E_{invest}) \cdot {\tau(t)/(t_u)}\\ + &- \delta(t) \cdot {\tau(t)/(t_u)}\\ + &- \frac{P_o(t)}{\eta_o(t)} \cdot \tau(t) + + P_i(t) \cdot \eta_i(t) \cdot \tau(t) + + Depending on the attribute :attr:`nonconvex`, the constraints for the + bounds of the decision variable :math:`E_{invest}` are different:\ + + * :attr:`nonconvex = False` - Initial capacity of :class:`.network.Storage` .. math:: - E(n, -1) = invest(n) \cdot c(n, -1), \\ - \forall n \in \textrm{INITIAL\_STORAGE\_LEVEL}. + E_{invest, min} \le E_{invest} \le E_{invest, max} + + * :attr:`nonconvex = True` - Connect the invest variables of the storage and the input flow. - .. math:: InvestmentFlow.invest(source(n), n) + existing = - (invest(n) + existing) * invest\_relation\_input\_capacity(n) \\ - \forall n \in \textrm{INVEST\_REL\_CAP\_IN} + .. math:: + & + E_{invest, min} \cdot b_{invest} \le E_{invest}\\ + & + E_{invest} \le E_{invest, max} \cdot b_{invest}\\ - Connect the invest variables of the storage and the output flow. - .. math:: InvestmentFlow.invest(n, target(n)) + existing = - (invest(n) + existing) * invest\_relation\_output_capacity(n) \\ - \forall n \in \textrm{INVEST\_REL\_CAP\_OUT} + The following constraints are created depending on the attributes of + the :class:`.components.GenericStorage`: - Connect the invest variables of the input and the output flow. - .. math:: InvestmentFlow.invest(source(n), n) + existing == - (InvestmentFlow.invest(n, target(n)) + existing) * - invest\_relation\_input_output(n) \\ - \forall n \in \textrm{INVEST\_REL\_IN\_OUT} + * :attr:`initial_storage_level is None` - Maximal capacity :attr:`om.InvestmentStorage.max_capacity[n, t]` - .. math:: E(n, t) \leq invest(n) \cdot c_{min}(n, t), \\ - \forall n \in \textrm{MAX\_INVESTSTORAGES,} \\ - \forall t \in \textrm{TIMESTEPS}. + Constraint for a variable initial storage level: - Minimal capacity :attr:`om.InvestmentStorage.min_capacity[n, t]` - .. math:: E(n, t) \geq invest(n) \cdot c_{min}(n, t), \\ - \forall n \in \textrm{MIN\_INVESTSTORAGES,} \\ - \forall t \in \textrm{TIMESTEPS}. + .. math:: + E(-1) \le E_{invest} + E_{exist} + * :attr:`initial_storage_level is not None` - **The following parts of the objective function are created:** + An initial value for the storage content is given: + + .. math:: + E(-1) = (E_{invest} + E_{exist}) \cdot c(-1) + + * :attr:`balanced=True` + + The energy content of storage of the first and the last timestep + are set equal: + + .. math:: + E(-1) = E(t_{last}) + + * :attr:`invest_relation_input_capacity is not None` + + Connect the invest variables of the storage and the input flow: + + .. math:: + P_{i,invest} + P_{i,exist} = + (E_{invest} + E_{exist}) \cdot r_{cap,in} + + * :attr:`invest_relation_output_capacity is not None` + + Connect the invest variables of the storage and the output flow: - Equivalent periodical costs (investment costs): .. math:: - \sum_n invest(n) \cdot ep\_costs(n) + P_{o,invest} + P_{o,exist} = + (E_{invest} + E_{exist}) \cdot r_{cap,out} + + * :attr:`invest_relation_input_output is not None` + + Connect the invest variables of the input and the output flow: - The expression can be accessed by - :attr:`om.InvestStorages.investment_costs` and their value after - optimization by :meth:`om.InvestStorages.investment_costs()` . + .. math:: + P_{i,invest} + P_{i,exist} = + (P_{o,invest} + P_{o,exist}) \cdot r_{in,out} + * :attr:`max_storage_level` - The symbols are the same as in:class:`.GenericStorageBlock`. + Rule for upper bound constraint for the storage content: + .. math:: + E(t) \leq E_{invest} \cdot c_{max}(t) + + * :attr:`min_storage_level` + + Rule for lower bound constraint for the storage content: + + .. math:: E(t) \geq E_{invest} \cdot c_{min}(t) + + + **Objective function** + + The part of the objective function added by the investment storages + also depends on whether a convex or nonconvex + investment option is selected. The following parts of the objective + function are created: + + * :attr:`nonconvex = False` + + .. math:: + E_{invest} \cdot c_{invest,var} + + * :attr:`nonconvex = True` + + .. math:: + E_{invest} \cdot c_{invest,var} + + c_{invest,fix} \cdot b_{invest}\\ + + The total value of all investment costs of all *InvestmentStorages* + can be retrieved calling + :meth:`om.GenericInvestmentStorageBlock.investment_costs.expr()`. + + .. csv-table:: List of Variables + :header: "symbol", "attribute", "explanation" + :widths: 1, 1, 1 + + ":math:`P_i(t)`", ":attr:`flow[i[n], n, t]`", "Inflow of the storage" + ":math:`P_o(t)`", ":attr:`flow[n, o[n], t]`", "Outlfow of the storage" + ":math:`E(t)`", ":attr:`capacity[n, t]`", "Actual storage content + (absolute storage level)" + ":math:`E_{invest}`", ":attr:`invest[n, t]`", "Invested (nominal) + capacity of the storage" + ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity + (before timestep 0)" + ":math:`b_{invest}`", ":attr:`invest_status[i, o]`", "Binary variable + for the status of investment" + ":math:`P_{i,invest}`", ":attr:`InvestmentFlow.invest[i[n], n]`", " + Invested (nominal) inflow (Investmentflow)" + ":math:`P_{o,invest}`", ":attr:`InvestmentFlow.invest[n, o[n]]`", " + Invested (nominal) outflow (Investmentflow)" + + .. csv-table:: List of Parameters + :header: "symbol", "attribute", "explanation" + :widths: 1, 1, 1 + + ":math:`E_{exist}`", ":py:obj:`flows[i, o].investment.existing`", " + Existing storage capacity" + ":math:`E_{invest,min}`", ":py:obj:`flows[i, o].investment.minimum`", " + Minimum investment value" + ":math:`E_{invest,max}`", ":py:obj:`flows[i, o].investment.maximum`", " + Maximum investment value" + ":math:`P_{i,exist}`", ":py:obj:`flows[i[n], n].investment.existing` + ", "Existing inflow capacity" + ":math:`P_{o,exist}`", ":py:obj:`flows[n, o[n]].investment.existing` + ", "Existing outlfow capacity" + ":math:`c_{invest,var}`", ":py:obj:`flows[i, o].investment.ep_costs` + ", "Variable investment costs" + ":math:`c_{invest,fix}`", ":py:obj:`flows[i, o].investment.offset`", " + Fix investment costs" + ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", " + Relation of storage capacity and nominal inflow" + ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", " + Relation of storage capacity and nominal outflow" + ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", " + Relation of nominal in- and outflow" + ":math:`\beta(t)`", ":py:obj:`loss_rate[t]`", "Fraction of lost energy + as share of :math:`E(t)` per time unit" + ":math:`\gamma(t)`", ":py:obj:`fixed_losses_relative[t]`", "Fixed loss + of energy relative to :math:`E_{invest} + E_{exist}` per time unit" + ":math:`\delta(t)`", ":py:obj:`fixed_losses_absolute[t]`", "Absolute + fixed loss of energy per time unit" + ":math:`\eta_i(t)`", ":py:obj:`inflow_conversion_factor[t]`", " + Conversion factor (i.e. efficiency) when storing energy" + ":math:`\eta_o(t)`", ":py:obj:`outflow_conversion_factor[t]`", " + Conversion factor when (i.e. efficiency) taking stored energy" + ":math:`c(-1)`", ":py:obj:`initial_storage_level`", "Initial relativ + storage content (before timestep 0)" + ":math:`c_{max}`", ":py:obj:`flows[i, o].max[t]`", "Normed maximum + value of storage content" + ":math:`c_{min}`", ":py:obj:`flows[i, o].min[t]`", "Normed minimum + value of storage content" + ":math:`\tau(t)`", "", "Duration of time step" + ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`, + :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`" """ @@ -518,6 +685,12 @@ def _create(self, group=None): self.INVESTSTORAGES = Set(initialize=[n for n in group]) + self.CONVEX_INVESTSTORAGES = Set(initialize=[ + n for n in group if n.investment.nonconvex is False]) + + self.NON_CONVEX_INVESTSTORAGES = Set(initialize=[ + n for n in group if n.investment.nonconvex is True]) + self.INVESTSTORAGES_BALANCED = Set(initialize=[ n for n in group if n.balanced is True]) @@ -550,38 +723,50 @@ def _create(self, group=None): def _storage_investvar_bound_rule(block, n): """Rule definition to bound the invested storage capacity `invest`. """ - return n.investment.minimum, n.investment.maximum + if n in self.CONVEX_INVESTSTORAGES: + return n.investment.minimum, n.investment.maximum + elif n in self.NON_CONVEX_INVESTSTORAGES: + return 0, n.investment.maximum self.invest = Var(self.INVESTSTORAGES, within=NonNegativeReals, bounds=_storage_investvar_bound_rule) self.init_cap = Var(self.INVESTSTORAGES, within=NonNegativeReals) + # create status variable for a non-convex investment storage + self.invest_status = Var(self.NON_CONVEX_INVESTSTORAGES, within=Binary) + + # ######################### CONSTRAINTS ############################### + i = {n: [i for i in n.inputs][0] for n in group} + o = {n: [o for o in n.outputs][0] for n in group} + + reduced_timesteps = [x for x in m.TIMESTEPS if x > 0] + def _inv_storage_init_cap_max_rule(block, n): + """Constraint for a variable initial storage capacity.""" return block.init_cap[n] <= n.investment.existing + block.invest[n] self.init_cap_limit = Constraint(self.INVESTSTORAGES_NO_INIT_CAP, rule=_inv_storage_init_cap_max_rule) def _inv_storage_init_cap_fix_rule(block, n): + """Constraint for a fixed initial storage capacity.""" return block.init_cap[n] == n.initial_storage_level * ( n.investment.existing + block.invest[n]) self.init_cap_fix = Constraint(self.INVESTSTORAGES_INIT_CAP, rule=_inv_storage_init_cap_fix_rule) - # ######################### CONSTRAINTS ############################### - i = {n: [i for i in n.inputs][0] for n in group} - o = {n: [o for o in n.outputs][0] for n in group} - - reduced_timesteps = [x for x in m.TIMESTEPS if x > 0] - - # storage balance constraint (first time step) def _storage_balance_first_rule(block, n): - """Rule definition for the storage balance of every storage n and - timestep t + """ + Rule definition for the storage balance of every storage n for the + first time step. """ expr = 0 expr += block.capacity[n, 0] expr += - block.init_cap[n] * ( - 1 - n.loss_rate[0]) + 1 - n.loss_rate[0]) ** m.timeincrement[0] + expr += (n.fixed_losses_relative[0] * + (n.investment.existing + self.invest[n]) * + m.timeincrement[0]) + expr += n.fixed_losses_absolute[0] * m.timeincrement[0] expr += (- m.flow[i[n], n, 0] * n.inflow_conversion_factor[0]) * m.timeincrement[0] expr += (m.flow[n, o[n], 0] / @@ -591,15 +776,19 @@ def _storage_balance_first_rule(block, n): self.balance_first = Constraint(self.INVESTSTORAGES, rule=_storage_balance_first_rule) - # storage balance constraint (every time step but the first) def _storage_balance_rule(block, n, t): - """Rule definition for the storage balance of every storage n and - timestep t + """ + Rule definition for the storage balance of every storage n for the + every time step but the first. """ expr = 0 expr += block.capacity[n, t] expr += - block.capacity[n, t - 1] * ( - 1 - n.loss_rate[t]) + 1 - n.loss_rate[t]) ** m.timeincrement[t] + expr += (n.fixed_losses_relative[t] * + (n.investment.existing + self.invest[n]) * + m.timeincrement[t]) + expr += n.fixed_losses_absolute[t] * m.timeincrement[t] expr += (- m.flow[i[n], n, t] * n.inflow_conversion_factor[t]) * m.timeincrement[t] expr += (m.flow[n, o[n], t] / @@ -612,7 +801,7 @@ def _storage_balance_rule(block, n, t): def _balanced_storage_rule(block, n): return block.capacity[n, m.TIMESTEPS[-1]] == block.init_cap[n] self.balanced_cstr = Constraint(self.INVESTSTORAGES_BALANCED, - rule=_balanced_storage_rule) + rule=_balanced_storage_rule) def _power_coupled(block, n): """Rule definition for constraint to connect the input power @@ -676,6 +865,27 @@ def _min_capacity_invest_rule(block, n, t): self.MIN_INVESTSTORAGES, m.TIMESTEPS, rule=_min_capacity_invest_rule) + def maximum_invest_limit(block, n): + """ + Constraint for the maximal investment in non convex investment + storage. + """ + return (n.investment.maximum * self.invest_status[n] - + self.invest[n]) >= 0 + self.limit_max = Constraint( + self.NON_CONVEX_INVESTSTORAGES, rule=maximum_invest_limit) + + def smallest_invest(block, n): + """ + Constraint for the minimal investment in non convex investment + storage if the invest is greater than 0. So the invest variable + can be either 0 or greater than the minimum. + """ + return self.invest[n] - (n.investment.minimum * + self.invest_status[n]) >= 0 + self.limit_min = Constraint( + self.NON_CONVEX_INVESTSTORAGES, rule=smallest_invest) + def _objective_expression(self): """Objective expression with fixed and investement costs.""" if not hasattr(self, 'INVESTSTORAGES'): @@ -683,12 +893,13 @@ def _objective_expression(self): investment_costs = 0 - for n in self.INVESTSTORAGES: - if n.investment.ep_costs is not None: - investment_costs += self.invest[n] * n.investment.ep_costs - else: - raise ValueError("Missing value for investment costs!") - + for n in self.CONVEX_INVESTSTORAGES: + investment_costs += ( + self.invest[n] * n.investment.ep_costs) + for n in self.NON_CONVEX_INVESTSTORAGES: + investment_costs += ( + self.invest[n] * n.investment.ep_costs + + self.invest_status[n] * n.investment.offset) self.investment_costs = Expression(expr=investment_costs) return investment_costs @@ -952,7 +1163,7 @@ class GenericCHPBlock(SimpleBlock): flow w/o distr. heating =============================== =============================== ==== ======================= - """ + """ # noqa: E501 CONSTRAINT_GROUP = True def __init__(self, *args, **kwargs): @@ -1190,14 +1401,16 @@ class ExtractionTurbineCHPBlock(SimpleBlock): \frac{P_{el}(t) + \dot Q_{th}(t) \cdot \beta(t)} {\eta_{el,woExtr}(t)} \\ & - (2)P_{el}(t) \geq \dot Q_{th}(t) \cdot + (2)P_{el}(t) \geq \dot Q_{th}(t) \cdot C_b = + \dot Q_{th}(t) \cdot \frac{\eta_{el,maxExtr}(t)} {\eta_{th,maxExtr}(t)} where :math:`\beta` is defined as: .. math:: - \beta(t) = \frac{\eta_{el,woExtr}(t) - \eta_{el,maxExtr}(t)}{\eta_{th,maxExtr}(t)} + \beta(t) = \frac{\eta_{el,woExtr}(t) - + \eta_{el,maxExtr}(t)}{\eta_{th,maxExtr}(t)} where the first equation is the result of the relation between the input flow and the two output flows, the second equation stems from how the two @@ -1215,7 +1428,7 @@ class ExtractionTurbineCHPBlock(SimpleBlock): :math:`\beta` :py:obj:`main_flow_loss_index[n, t]` P power loss index - :math:`\eta_{el,woExtr}` :py:obj:`conversion_factor_full_condensation [n, t]` P electric efficiency + :math:`\eta_{el,woExtr}` :py:obj:`conversion_factor_full_condensation[n, t]` P electric efficiency without heat extraction :math:`\eta_{el,maxExtr}` :py:obj:`conversion_factors[main_output][n, t]` P electric efficiency with max heat extraction @@ -1223,8 +1436,7 @@ class ExtractionTurbineCHPBlock(SimpleBlock): maximal heat extraction ========================= ==================================================== ==== ========= - - """ + """ # noqa: E501 CONSTRAINT_GROUP = True diff --git a/oemof/tools/console_scripts.py b/src/oemof/solph/console_scripts.py similarity index 100% rename from oemof/tools/console_scripts.py rename to src/oemof/solph/console_scripts.py index 71fc19a94..f754f1c54 100644 --- a/oemof/tools/console_scripts.py +++ b/src/oemof/solph/console_scripts.py @@ -12,8 +12,8 @@ import logging -from oemof import solph import pandas as pd +from oemof import solph def check_oemof_installation(silent=False): diff --git a/oemof/solph/constraints.py b/src/oemof/solph/constraints.py similarity index 86% rename from oemof/solph/constraints.py rename to src/oemof/solph/constraints.py index f971d5588..73066492c 100644 --- a/oemof/solph/constraints.py +++ b/src/oemof/solph/constraints.py @@ -13,7 +13,7 @@ def investment_limit(model, limit=None): - """ Set an absolute limit for the total investment costs of an investment + r""" Set an absolute limit for the total investment costs of an investment optimization problem: .. math:: \sum_{investment\_costs} \leq limit @@ -42,7 +42,7 @@ def investment_rule(m): def emission_limit(om, flows=None, limit=None): - """ + r""" Short handle for generic_integral_limit() with keyword="emission_factor". Note @@ -57,7 +57,7 @@ def emission_limit(om, flows=None, limit=None): def generic_integral_limit(om, keyword, flows=None, limit=None): - """Set a global limit for flows weighted by attribute called keyword. + r"""Set a global limit for flows weighted by attribute called keyword. The attribute named by keyword has to be added to every flow you want to take into account. @@ -100,7 +100,24 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): :math:`w_N(t)` P weight given to Flow named according to `keyword` :math:`\tau(t)` P width of time step :math:`t` :math:`L` P global limit given by keyword `limit` + ================ ==== ===================================================== + Examples + -------- + >>> import pandas as pd + >>> from oemof import solph + >>> date_time_index = pd.date_range('1/1/2012', periods=5, freq='H') + >>> energysystem = solph.EnergySystem(timeindex=date_time_index) + >>> bel = solph.Bus(label='electricityBus') + >>> flow1 = solph.Flow(nominal_value=100, my_factor=0.8) + >>> flow2 = solph.Flow(nominal_value=50) + >>> src1 = solph.Source(label='source1', outputs={bel: flow1}) + >>> src2 = solph.Source(label='source2', outputs={bel: flow2}) + >>> energysystem.add(bel, src1, src2) + >>> model = solph.Model(energysystem) + >>> flow_with_keyword = {(src1, bel): flow1, } + >>> model = solph.constraints.generic_integral_limit( + ... model, "my_factor", flow_with_keyword, limit=777) """ if flows is None: flows = {} diff --git a/oemof/solph/custom.py b/src/oemof/solph/custom.py similarity index 94% rename from oemof/solph/custom.py rename to src/oemof/solph/custom.py index a0931a460..faa3a2793 100644 --- a/oemof/solph/custom.py +++ b/src/oemof/solph/custom.py @@ -11,13 +11,22 @@ SPDX-License-Identifier: MIT """ -from pyomo.core.base.block import SimpleBlock -from pyomo.environ import (Binary, Set, NonNegativeReals, Var, Constraint, - BuildAction, Expression) import logging -from oemof.solph.network import Bus, Transformer, Flow, Sink +from oemof.network.network import Transformer as NetworkTransformer +from oemof.solph.network import Bus +from oemof.solph.network import Flow +from oemof.solph.network import Sink +from oemof.solph.network import Transformer from oemof.solph.plumbing import sequence +from pyomo.core.base.block import SimpleBlock +from pyomo.environ import Binary +from pyomo.environ import BuildAction +from pyomo.environ import Constraint +from pyomo.environ import Expression +from pyomo.environ import NonNegativeReals +from pyomo.environ import Set +from pyomo.environ import Var class ElectricalBus(Bus): @@ -176,7 +185,7 @@ def _voltage_angle_relation(block): rhs = 1 / n.reactance[t] * ( self.voltage_angle[n.input, t] - self.voltage_angle[n.output, t]) - except: + except ValueError: raise ValueError("Error in constraint creation", "of node {}".format(n.label)) block.electrical_flow.add((n, t), (lhs == rhs)) @@ -195,7 +204,7 @@ class Link(Transformer): conversion_factors : dict Dictionary containing conversion factors for conversion of each flow. Keys are the connected tuples (input, output) bus objects. - The dictionary values can either be a scalar or a sequence with length + The dictionary values can either be a scalar or an iterable with length of time horizon for simulation. Note: This component is experimental. Use it with care. @@ -305,7 +314,7 @@ def _input_output_relation(block): self.relation_build = BuildAction(rule=_input_output_relation) -class GenericCAES(Transformer): +class GenericCAES(NetworkTransformer): """ Component `GenericCAES` to model arbitrary compressed air energy storages. @@ -514,45 +523,86 @@ class GenericCAESBlock(SimpleBlock): :header: "symbol", "attribute", "type", "explanation" :widths: 1, 1, 1, 1 - ":math:`ST_{cmp}` ", ":py:obj:`cmp_st[n,t]` ", "V", "Status of compression" + ":math:`ST_{cmp}` ", ":py:obj:`cmp_st[n,t]` ", "V", "Status of + compression" ":math:`{P}_{cmp}` ", ":py:obj:`cmp_p[n,t]`", "V", "Compression power" - ":math:`{P}_{cmp\_max}`", ":py:obj:`cmp_p_max[n,t]`", "V", "Max. compression power" - ":math:`\dot{Q}_{cmp}` ", ":py:obj:`cmp_q_out_sum[n,t]`", "V", "Summed heat flow in compression" - ":math:`\dot{Q}_{cmp\_out}` ", ":py:obj:`cmp_q_waste[n,t]`", "V", "Waste heat flow from compression" - ":math:`ST_{exp}(t)`", ":py:obj:`exp_st[n,t]`", "V", "Status of expansion (binary)" + ":math:`{P}_{cmp\_max}`", ":py:obj:`cmp_p_max[n,t]`", "V", "Max. + compression power" + ":math:`\dot{Q}_{cmp}` ", ":py:obj:`cmp_q_out_sum[n,t]`", "V", "Summed + heat flow in compression" + ":math:`\dot{Q}_{cmp\_out}` ", ":py:obj:`cmp_q_waste[n,t]`", "V", " + Waste heat flow from compression" + ":math:`ST_{exp}(t)`", ":py:obj:`exp_st[n,t]`", "V", "Status of + expansion (binary)" ":math:`P_{exp}(t)`", ":py:obj:`exp_p[n,t]`", "V", "Expansion power" - ":math:`P_{exp\_max}(t)`", ":py:obj:`exp_p_max[n,t]`", "V", "Max. expansion power" - ":math:`\dot{Q}_{exp}(t)`", ":py:obj:`exp_q_in_sum[n,t]`", "V", "Summed heat flow in expansion" - ":math:`\dot{Q}_{exp\_in}(t)`", ":py:obj:`exp_q_fuel_in[n,t]`", "V", "Heat (external) flow into expansion" - ":math:`\dot{Q}_{exp\_add}(t)`", ":py:obj:`exp_q_add_in[n,t]`", "V", "Additional heat flow into expansion" - ":math:`CAV_{fil}(t)`", ":py:obj:`cav_level[n,t]`", "V", "Filling level if CAE" - ":math:`\dot{E}_{cas\_in}(t)`", ":py:obj:`cav_e_in[n,t]`", "V", "Exergy flow into CAS" - ":math:`\dot{E}_{cas\_out}(t)`", ":py:obj:`cav_e_out[n,t]`", "V", "Exergy flow from CAS" - ":math:`TES_{fil}(t)`", ":py:obj:`tes_level[n,t]`", "V", "Filling level of Thermal Energy Storage (TES)" - ":math:`\dot{Q}_{tes\_in}(t)`", ":py:obj:`tes_e_in[n,t]`", "V", "Heat flow into TES" - ":math:`\dot{Q}_{tes\_out}(t)`", ":py:obj:`tes_e_out[n,t]`", "V", "Heat flow from TES" - ":math:`b_{cmp\_max}`", ":py:obj:`cmp_p_max_b[n,t]`", "P", "Specific y-intersection" - ":math:`b_{cmp\_q}`", ":py:obj:`cmp_q_out_b[n,t]`", "P", "Specific y-intersection" - ":math:`b_{exp\_max}`", ":py:obj:`exp_p_max_b[n,t]`", "P", "Specific y-intersection" - ":math:`b_{exp\_q}`", ":py:obj:`exp_q_in_b[n,t]`", "P", "Specific y-intersection" - ":math:`b_{cas\_in}`", ":py:obj:`cav_e_in_b[n,t]`", "P", "Specific y-intersection" - ":math:`b_{cas\_out}`", ":py:obj:`cav_e_out_b[n,t]`", "P", "Specific y-intersection" - ":math:`m_{cmp\_max}`", ":py:obj:`cmp_p_max_m[n,t]`", "P", "Specific slope" - ":math:`m_{cmp\_q}`", ":py:obj:`cmp_q_out_m[n,t]`", "P", "Specific slope" - ":math:`m_{exp\_max}`", ":py:obj:`exp_p_max_m[n,t]`", "P", "Specific slope" - ":math:`m_{exp\_q}`", ":py:obj:`exp_q_in_m[n,t]`", "P", "Specific slope" - ":math:`m_{cas\_in}`", ":py:obj:`cav_e_in_m[n,t]`", "P", "Specific slope" - ":math:`m_{cas\_out}`", ":py:obj:`cav_e_out_m[n,t]`", "P", "Specific slope" - ":math:`P_{cmp\_min}`", ":py:obj:`cmp_p_min[n,t]`", "P", "Min. compression power" - ":math:`r_{cmp\_tes}`", ":py:obj:`cmp_q_tes_share[n,t]`", "P", "Ratio between waste heat flow and heat flow into TES" - ":math:`r_{exp\_tes}`", ":py:obj:`exp_q_tes_share[n,t]`", "P", "Ratio between external heat flow into expansion and heat flows from TES and additional source" - ":math:`\tau`", ":py:obj:`m.timeincrement[n,t]`", "P", "Time interval length" - ":math:`TES_{fil\_max}`", ":py:obj:`tes_level_max[n,t]`", "P", "Max. filling level of TES" - ":math:`CAS_{fil\_max}`", ":py:obj:`cav_level_max[n,t]`", "P", "Max. filling level of TES" - ":math:`\tau`", ":py:obj:`cav_eta_tmp[n,t]`", "P", "Temporal efficiency (loss factor to take intertemporal losses into account)" - ":math:`electrical\_input`", ":py:obj:`flow[list(n.electrical_input.keys())[0], n, t]`", "P", "Electr. power input into compression" - ":math:`electrical\_output`", ":py:obj:`flow[n, list(n.electrical_output.keys())[0], t]`", "P", "Electr. power output of expansion" - ":math:`fuel\_input`", ":py:obj:`flow[list(n.fuel_input.keys())[0], n, t]`", "P", "Heat input (external) into Expansion" + ":math:`P_{exp\_max}(t)`", ":py:obj:`exp_p_max[n,t]`", "V", "Max. + expansion power" + ":math:`\dot{Q}_{exp}(t)`", ":py:obj:`exp_q_in_sum[n,t]`", "V", " + Summed heat flow in expansion" + ":math:`\dot{Q}_{exp\_in}(t)`", ":py:obj:`exp_q_fuel_in[n,t]`", "V", " + Heat (external) flow into expansion" + ":math:`\dot{Q}_{exp\_add}(t)`", ":py:obj:`exp_q_add_in[n,t]`", "V", " + Additional heat flow into expansion" + ":math:`CAV_{fil}(t)`", ":py:obj:`cav_level[n,t]`", "V", "Filling level + if CAE" + ":math:`\dot{E}_{cas\_in}(t)`", ":py:obj:`cav_e_in[n,t]`", "V", " + Exergy flow into CAS" + ":math:`\dot{E}_{cas\_out}(t)`", ":py:obj:`cav_e_out[n,t]`", "V", " + Exergy flow from CAS" + ":math:`TES_{fil}(t)`", ":py:obj:`tes_level[n,t]`", "V", "Filling + level of Thermal Energy Storage (TES)" + ":math:`\dot{Q}_{tes\_in}(t)`", ":py:obj:`tes_e_in[n,t]`", "V", "Heat + flow into TES" + ":math:`\dot{Q}_{tes\_out}(t)`", ":py:obj:`tes_e_out[n,t]`", "V", "Heat + flow from TES" + ":math:`b_{cmp\_max}`", ":py:obj:`cmp_p_max_b[n,t]`", "P", "Specific + y-intersection" + ":math:`b_{cmp\_q}`", ":py:obj:`cmp_q_out_b[n,t]`", "P", "Specific + y-intersection" + ":math:`b_{exp\_max}`", ":py:obj:`exp_p_max_b[n,t]`", "P", "Specific + y-intersection" + ":math:`b_{exp\_q}`", ":py:obj:`exp_q_in_b[n,t]`", "P", "Specific + y-intersection" + ":math:`b_{cas\_in}`", ":py:obj:`cav_e_in_b[n,t]`", "P", "Specific + y-intersection" + ":math:`b_{cas\_out}`", ":py:obj:`cav_e_out_b[n,t]`", "P", "Specific + y-intersection" + ":math:`m_{cmp\_max}`", ":py:obj:`cmp_p_max_m[n,t]`", "P", "Specific + slope" + ":math:`m_{cmp\_q}`", ":py:obj:`cmp_q_out_m[n,t]`", "P", "Specific + slope" + ":math:`m_{exp\_max}`", ":py:obj:`exp_p_max_m[n,t]`", "P", "Specific + slope" + ":math:`m_{exp\_q}`", ":py:obj:`exp_q_in_m[n,t]`", "P", "Specific + slope" + ":math:`m_{cas\_in}`", ":py:obj:`cav_e_in_m[n,t]`", "P", "Specific + slope" + ":math:`m_{cas\_out}`", ":py:obj:`cav_e_out_m[n,t]`", "P", "Specific + slope" + ":math:`P_{cmp\_min}`", ":py:obj:`cmp_p_min[n,t]`", "P", "Min. + compression power" + ":math:`r_{cmp\_tes}`", ":py:obj:`cmp_q_tes_share[n,t]`", "P", "Ratio + between waste heat flow and heat flow into TES" + ":math:`r_{exp\_tes}`", ":py:obj:`exp_q_tes_share[n,t]`", "P", "Ratio + between external heat flow into expansion and heat flows from TES and + additional source" + ":math:`\tau`", ":py:obj:`m.timeincrement[n,t]`", "P", "Time interval + length" + ":math:`TES_{fil\_max}`", ":py:obj:`tes_level_max[n,t]`", "P", "Max. + filling level of TES" + ":math:`CAS_{fil\_max}`", ":py:obj:`cav_level_max[n,t]`", "P", "Max. + filling level of TES" + ":math:`\tau`", ":py:obj:`cav_eta_tmp[n,t]`", "P", "Temporal efficiency + (loss factor to take intertemporal losses into account)" + ":math:`electrical\_input`", " + :py:obj:`flow[list(n.electrical_input.keys())[0], n, t]`", "P", " + Electr. power input into compression" + ":math:`electrical\_output`", " + :py:obj:`flow[n, list(n.electrical_output.keys())[0], t]`", "P", " + Electr. power output of expansion" + ":math:`fuel\_input`", " + :py:obj:`flow[list(n.fuel_input.keys())[0], n, t]`", "P", "Heat input + (external) into Expansion" """ @@ -668,12 +718,12 @@ def cmp_p_max_constr_rule(block, n, t): n.params['cmp_p_max_m'] * self.cav_level[n, t-1] + n.params['cmp_p_max_b']) else: - return (self.cmp_p_max[n, t] == n.params['cmp_p_max_b']) + return self.cmp_p_max[n, t] == n.params['cmp_p_max_b'] self.cmp_p_max_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=cmp_p_max_constr_rule) def cmp_p_max_area_constr_rule(block, n, t): - return (self.cmp_p[n, t] <= self.cmp_p_max[n, t]) + return self.cmp_p[n, t] <= self.cmp_p_max[n, t] self.cmp_p_max_area_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=cmp_p_max_area_constr_rule) @@ -729,13 +779,13 @@ def exp_p_max_constr_rule(block, n, t): n.params['exp_p_max_m'] * self.cav_level[n, t-1] + n.params['exp_p_max_b']) else: - return (self.exp_p_max[n, t] == n.params['exp_p_max_b']) + return self.exp_p_max[n, t] == n.params['exp_p_max_b'] self.exp_p_max_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=exp_p_max_constr_rule) # (13) def exp_p_max_area_constr_rule(block, n, t): - return (self.exp_p[n, t] <= self.exp_p_max[n, t]) + return self.exp_p[n, t] <= self.exp_p_max[n, t] self.exp_p_max_area_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=exp_p_max_area_constr_rule) @@ -817,7 +867,7 @@ def cav_eta_constr_rule(block, n, t): # (24) Cavern: Upper bound def cav_ub_constr_rule(block, n, t): - return (self.cav_level[n, t] <= n.params['cav_level_max']) + return self.cav_level[n, t] <= n.params['cav_level_max'] self.cav_ub_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=cav_ub_constr_rule) @@ -836,7 +886,7 @@ def tes_eta_constr_rule(block, n, t): # (27) TES: Upper bound def tes_ub_constr_rule(block, n, t): - return (self.tes_level[n, t] <= n.params['tes_level_max']) + return self.tes_level[n, t] <= n.params['tes_level_max'] self.tes_ub_constr = Constraint( self.GENERICCAES, m.TIMESTEPS, rule=tes_ub_constr_rule) @@ -1259,8 +1309,8 @@ def _input_output_relation_rule(block): block.input_output_relation.add((g, t), (lhs == rhs)) # main use case - elif g.delay_time < t <=\ - m.TIMESTEPS._bounds[1] - g.delay_time: + elif (g.delay_time < t <= + m.TIMESTEPS._bounds[1] - g.delay_time): # Generator loads from bus lhs = m.flow[g.inflow, g, t] @@ -1316,8 +1366,8 @@ def dsm_up_down_constraint_rule(block): block.dsm_updo_constraint.add((g, t), (lhs == rhs)) # main use case - elif g.delay_time < t <=\ - m.TIMESTEPS._bounds[1] - g.delay_time: + elif g.delay_time < t <= ( + m.TIMESTEPS._bounds[1] - g.delay_time): # DSM up lhs = self.dsm_up[g, t] @@ -1393,8 +1443,8 @@ def dsm_do_constraint_rule(block): block.dsm_do_constraint.add((g, tt), (lhs <= rhs)) # main use case - elif g.delay_time < tt <=\ - m.TIMESTEPS._bounds[1] - g.delay_time: + elif g.delay_time < tt <= ( + m.TIMESTEPS._bounds[1] - g.delay_time): # DSM down lhs = sum(self.dsm_do[g, t, tt] @@ -1449,8 +1499,8 @@ def c2_constraint_rule(block): # add constraint block.C2_constraint.add((g, tt), (lhs <= rhs)) - elif g.delay_time < tt <=\ - m.TIMESTEPS._bounds[1] - g.delay_time: + elif g.delay_time < tt <= ( + m.TIMESTEPS._bounds[1] - g.delay_time): # DSM up/down lhs = self.dsm_up[g, tt] + sum( diff --git a/oemof/solph/groupings.py b/src/oemof/solph/groupings.py similarity index 98% rename from oemof/solph/groupings.py rename to src/oemof/solph/groupings.py index a3b55630c..3f8d7e791 100644 --- a/oemof/solph/groupings.py +++ b/src/oemof/solph/groupings.py @@ -19,8 +19,8 @@ SPDX-License-Identifier: MIT """ +import oemof.network.groupings as groupings from oemof.solph import blocks -import oemof.groupings as groupings def constraint_grouping(node, fallback=lambda *xs, **ks: None): diff --git a/oemof/tools/helpers.py b/src/oemof/solph/helpers.py similarity index 56% rename from oemof/tools/helpers.py rename to src/oemof/solph/helpers.py index 3dd9975cc..9103d51cf 100644 --- a/oemof/tools/helpers.py +++ b/src/oemof/solph/helpers.py @@ -10,10 +10,13 @@ SPDX-License-Identifier: MIT """ - +import datetime as dt import os from collections import MutableMapping +import pandas as pd +from oemof.solph.plumbing import sequence + def get_basic_path(): """Returns the basic oemof path and creates it if necessary. @@ -57,3 +60,36 @@ def flatten(d, parent_key='', sep='_'): else: items.append((new_key, v)) return dict(items) + + +def calculate_timeincrement(timeindex, fill_value=None): + """ + Calculates timeincrement for `timeindex` + + Parameters + ---------- + timeindex: pd.DatetimeIndex + timeindex of energysystem + fill_value: numerical + timeincrement for first timestep in hours + """ + if isinstance(timeindex, pd.DatetimeIndex) and \ + (fill_value and isinstance(fill_value, pd.Timedelta) or + fill_value is None): + if len(set(timeindex)) != len(timeindex): + raise IndexError("No equal DatetimeIndex allowed!") + timeindex = timeindex.to_series() + timeindex_sorted = timeindex.sort_values() + if fill_value: + timeincrement = timeindex_sorted.diff().fillna(value=fill_value) + else: + timeincrement = timeindex_sorted.diff().fillna(method='bfill') + timeincrement_sec = timeincrement.map(dt.timedelta.total_seconds) + timeincrement_hourly = list(timeincrement_sec.map( + lambda x: x/3600)) + timeincrement = sequence(timeincrement_hourly) + return timeincrement + else: + raise AttributeError( + "'timeindex' must be of type 'DatetimeIndex' and " + + "'fill_value' of type 'Timedelta'.") diff --git a/oemof/solph/models.py b/src/oemof/solph/models.py similarity index 99% rename from oemof/solph/models.py rename to src/oemof/solph/models.py index da298022f..c45462e4f 100644 --- a/oemof/solph/models.py +++ b/src/oemof/solph/models.py @@ -7,15 +7,15 @@ SPDX-License-Identifier: MIT """ +import logging +import warnings import pyomo.environ as po -from pyomo.opt import SolverFactory -from pyomo.core.plugins.transform.relax_integrality import RelaxIntegrality from oemof.solph import blocks +from oemof.solph import processing from oemof.solph.plumbing import sequence -from oemof.outputlib import processing -import warnings -import logging +from pyomo.core.plugins.transform.relax_integrality import RelaxIntegrality +from pyomo.opt import SolverFactory class BaseModel(po.ConcreteModel): @@ -65,8 +65,8 @@ def __init__(self, energysystem, **kwargs): self.name = kwargs.get('name', type(self).__name__) self.es = energysystem - self.timeincrement = sequence(kwargs.get('timeincrement', None)) - + self.timeincrement = sequence(kwargs.get('timeincrement', + self.es.timeincrement)) if self.timeincrement[0] is None: try: self.timeincrement = sequence( diff --git a/oemof/solph/network.py b/src/oemof/solph/network.py similarity index 83% rename from oemof/solph/network.py rename to src/oemof/solph/network.py index 1905fe9a4..e318aa50f 100644 --- a/oemof/solph/network.py +++ b/src/oemof/solph/network.py @@ -14,10 +14,13 @@ SPDX-License-Identifier: MIT """ -import oemof.network as on -import oemof.energy_system as es -from oemof.solph.plumbing import sequence +from warnings import warn + +import oemof.network.energy_system as es +import oemof.network.network as on from oemof.solph import blocks +from oemof.solph.plumbing import sequence +from oemof.tools import debugging class EnergySystem(es.EnergySystem): @@ -51,51 +54,51 @@ class Flow(on.Edge): Keyword arguments are used to set the attributes of this flow. Parameters which are handled specially are noted below. - For the case where a parameter can be either a scalar or a sequence, a + For the case where a parameter can be either a scalar or an iterable, a scalar value will be converted to a sequence containing the scalar value at every index. This sequence is then stored under the paramter's key. Parameters ---------- - nominal_value : numeric + nominal_value : numeric, :math:`P_{nom}` The nominal value of the flow. If this value is set the corresponding optimization variable of the flow object will be bounded by this value multiplied with min(lower bound)/max(upper bound). - max : numeric (sequence or scalar) + max : numeric (iterable or scalar), :math:`f_{max}` Normed maximum value of the flow. The flow absolute maximum will be calculated by multiplying :attr:`nominal_value` with :attr:`max` - min : numeric (sequence or scalar) - Nominal minimum value of the flow (see :attr:`max`). - actual_value : numeric (sequence or scalar) - Specific value for the flow variable. Will be multiplied with the + min : numeric (iterable or scalar), :math:`f_{min}` + Normed minimum value of the flow (see :attr:`max`). + actual_value : numeric (iterable or scalar), :math:`f_{actual}` + Normed fixed value for the flow variable. Will be multiplied with the :attr:`nominal_value` to get the absolute value. If :attr:`fixed` is - set to :obj:`True` the flow variable will be fixed to :py:`actual_value + set to :obj:`True` the flow variable will be fixed to `actual_value * nominal_value`, i.e. this value is set exogenous. - positive_gradient : :obj:`dict`, default: :py:`{'ub': None, 'costs': 0}` + positive_gradient : :obj:`dict`, default: `{'ub': None, 'costs': 0}` A dictionary containing the following two keys: - * :py:`'ub'`: numeric (sequence, scalar or None), the normed *upper - bound* on the positive difference (:py:`flow[t-1] < flow[t]`) of + * `'ub'`: numeric (iterable, scalar or None), the normed *upper + bound* on the positive difference (`flow[t-1] < flow[t]`) of two consecutive flow values. - * :py:`'costs``: numeric (scalar or None), the gradient cost per + * `'costs``: numeric (scalar or None), the gradient cost per unit. - negative_gradient : :obj:`dict`, default: :py:`{'ub': None, 'costs': 0}` + negative_gradient : :obj:`dict`, default: `{'ub': None, 'costs': 0}` A dictionary containing the following two keys: - * :py:`'ub'`: numeric (sequence, scalar or None), the normed *upper - bound* on the negative difference (:py:`flow[t-1] > flow[t]`) of + * `'ub'`: numeric (iterable, scalar or None), the normed *upper + bound* on the negative difference (`flow[t-1] > flow[t]`) of two consecutive flow values. - * :py:`'costs``: numeric (scalar or None), the gradient cost per + * `'costs``: numeric (scalar or None), the gradient cost per unit. - summed_max : numeric + summed_max : numeric, :math:`f_{sum,max}` Specific maximum value summed over all timesteps. Will be multiplied with the nominal_value to get the absolute limit. - summed_min : numeric + summed_min : numeric, :math:`f_{sum,min}` see above - variable_costs : numeric (sequence or scalar) + variable_costs : numeric (iterable or scalar) The costs associated with one unit of the flow. If this is set the costs will be added to the objective expression of the optimization problem. @@ -242,6 +245,12 @@ def constraint_group(self): class Sink(on.Sink): """An object with one input flow. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.inputs: + msg = "`Sink` '{0}' constructed without `inputs`." + warn(msg.format(self), debugging.SuspiciousUsageWarning) + def constraint_group(self): pass @@ -249,6 +258,12 @@ def constraint_group(self): class Source(on.Source): """An object with one output flow. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.outputs: + msg = "`Source` '{0}' constructed without `outputs`." + warn(msg.format(self), debugging.SuspiciousUsageWarning) + def constraint_group(self): pass @@ -261,7 +276,7 @@ class Transformer(on.Transformer): conversion_factors : dict Dictionary containing conversion factors for conversion of each flow. Keys are the connected bus objects. - The dictionary values can either be a scalar or a sequence with length + The dictionary values can either be a scalar or an iterable with length of time horizon for simulation. Examples @@ -305,6 +320,13 @@ class Transformer(on.Transformer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if not self.inputs: + msg = "`Transformer` '{0}' constructed without `inputs`." + warn(msg.format(self), debugging.SuspiciousUsageWarning) + if not self.outputs: + msg = "`Transformer` '{0}' constructed without `outputs`." + warn(msg.format(self), debugging.SuspiciousUsageWarning) + self.conversion_factors = { k: sequence(v) for k, v in kwargs.get('conversion_factors', {}).items()} diff --git a/oemof/solph/options.py b/src/oemof/solph/options.py similarity index 58% rename from oemof/solph/options.py rename to src/oemof/solph/options.py index de2913e8d..a87a1b074 100644 --- a/oemof/solph/options.py +++ b/src/oemof/solph/options.py @@ -15,35 +15,77 @@ class Investment: """ Parameters ---------- - maximum : float + maximum : float, :math:`P_{invest,max}` or :math:`E_{invest,max}` Maximum of the additional invested capacity - minimum : float - Minimum of the additional invested capacity - ep_costs : float - Equivalent periodical costs for the investment, if period is one - year these costs are equal to the equivalent annual costs. - existing : float + minimum : float, :math:`P_{invest,min}` or :math:`E_{invest,min}` + Minimum of the additional invested capacity. If `nonconvex` is `True`, + `minimum` defines the threshold for the invested capacity. + ep_costs : float, :math:`c_{invest,var}` + Equivalent periodical costs for the investment per flow capacity. + existing : float, :math:`P_{exist}` or :math:`E_{exist}` Existing / installed capacity. The invested capacity is added on top - of this value. + of this value. Not applicable if `nonconvex` is set to `True`. + nonconvex : bool + If `True`, a binary variable for the status of the investment is + created. This enables additional fix investment costs (*offset*) + independent of the invested flow capacity. Therefore, use the `offset` + parameter. + offset : float, :math:`c_{invest,fix}` + Additional fix investment costs. Only applicable if `nonconvex` is set + to `True`. + + + For the variables, constraints and parts of the objective function, which + are created, see :class:`oemof.solph.blocks.InvestmentFlow` and + :class:`oemof.solph.components.GenericInvestmentStorageBlock`. """ def __init__(self, maximum=float('+inf'), minimum=0, ep_costs=0, - existing=0): + existing=0, nonconvex=False, offset=0): self.maximum = maximum self.minimum = minimum self.ep_costs = ep_costs self.existing = existing + self.nonconvex = nonconvex + self.offset = offset + + self._check_invest_attributes() + self._check_invest_attributes_maximum() + self._check_invest_attributes_offset() + + def _check_invest_attributes(self): + if (self.existing != 0) and (self.nonconvex is True): + e1 = ("Values for 'offset' and 'existing' are given in" + " investement attributes. \n These two options cannot be " + "considered at the same time.") + raise AttributeError(e1) + + def _check_invest_attributes_maximum(self): + if (self.maximum == float('+inf')) and (self.nonconvex is True): + e2 = ("Please provide an maximum investment value in case of" + " nonconvex investemnt (nonconvex=True), which is in the" + " expected magnitude." + " \nVery high maximum values (> 10e8) as maximum investment" + " limit might lead to numeric issues, so that no investment" + " is done, although it is the optimal solution!") + raise AttributeError(e2) + + def _check_invest_attributes_offset(self): + if (self.offset != 0) and (self.nonconvex is False): + e3 = ("If `nonconvex` is `False`, the `offset` parameter will be" + " ignored.") + raise AttributeError(e3) class NonConvex: """ Parameters ---------- - startup_costs : numeric (sequence or scalar) + startup_costs : numeric (iterable or scalar) Costs associated with a start of the flow (representing a unit). - shutdown_costs : numeric (sequence or scalar) + shutdown_costs : numeric (iterable or scalar) Costs associated with the shutdown of the flow (representing a unit). - activity_costs : numeric (sequence or scalar) + activity_costs : numeric (iterable or scalar) Costs associated with the active operation of the flow, independently from the actual output. minimum_uptime : numeric (1 or positive integer) diff --git a/oemof/solph/plumbing.py b/src/oemof/solph/plumbing.py similarity index 76% rename from oemof/solph/plumbing.py rename to src/oemof/solph/plumbing.py index 26f995a5d..6d95c9557 100644 --- a/oemof/solph/plumbing.py +++ b/src/oemof/solph/plumbing.py @@ -9,18 +9,19 @@ SPDX-License-Identifier: MIT """ -from collections import abc, UserList +from collections import UserList +from collections import abc from itertools import repeat -def sequence(sequence_or_scalar): - """ Tests if an object is sequence (except string) or scalar and returns - a the original sequence if object is a sequence and a 'emulated' sequence +def sequence(iterable_or_scalar): + """ Tests if an object is iterable (except string) or scalar and returns + a the original sequence if object is an iterable and a 'emulated' sequence object of class _Sequence if object is a scalar or string. Parameters ---------- - sequence_or_scalar : array-like, None, int, float + iterable_or_scalar : iterable, None, int, float Examples -------- @@ -37,11 +38,11 @@ def sequence(sequence_or_scalar): [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] """ - if (isinstance(sequence_or_scalar, abc.Iterable) and not - isinstance(sequence_or_scalar, str)): - return sequence_or_scalar + if (isinstance(iterable_or_scalar, abc.Iterable) and not + isinstance(iterable_or_scalar, str)): + return iterable_or_scalar else: - return _Sequence(default=sequence_or_scalar) + return _Sequence(default=iterable_or_scalar) class _Sequence(UserList): diff --git a/oemof/outputlib/processing.py b/src/oemof/solph/processing.py similarity index 99% rename from oemof/outputlib/processing.py rename to src/oemof/solph/processing.py index d3ba15f30..d4c1f6370 100644 --- a/oemof/outputlib/processing.py +++ b/src/oemof/solph/processing.py @@ -11,11 +11,12 @@ SPDX-License-Identifier: MIT """ -import pandas as pd import sys -from oemof.network import Node -from oemof.tools.helpers import flatten from itertools import groupby + +import pandas as pd +from oemof.network.network import Node +from oemof.solph.helpers import flatten from pyomo.core.base.var import Var diff --git a/oemof/outputlib/views.py b/src/oemof/solph/views.py similarity index 92% rename from oemof/outputlib/views.py rename to src/oemof/solph/views.py index f927c588a..01395357a 100644 --- a/oemof/outputlib/views.py +++ b/src/oemof/solph/views.py @@ -10,12 +10,12 @@ SPDX-License-Identifier: MIT """ -from collections import OrderedDict import logging -import pandas as pd +from collections import OrderedDict from enum import Enum -from oemof.outputlib.processing import convert_keys_to_strings +import pandas as pd +from oemof.solph.processing import convert_keys_to_strings NONE_REPLACEMENT_STR = '_NONE_' @@ -123,15 +123,14 @@ def filter_nodes(results, option=NodeOption.All, exclude_busses=False): This function filters nodes from results for special needs. At the moment, the following options are available: - * :attr:`NodeOption.All`/:py:`'all'`: - Returns all nodes - * :attr:`NodeOption.HasOutputs`/:py:`'has_outputs'`: + * :attr:`NodeOption.All`: `'all'`: Returns all nodes + * :attr:`NodeOption.HasOutputs`: `'has_outputs'`: Returns nodes with an output flow (eg. Transformer, Source) - * :attr:`NodeOption.HasInputs`/:py:`'has_inputs'`: + * :attr:`NodeOption.HasInputs`: `'has_inputs'`: Returns nodes with an input flow (eg. Transformer, Sink) - * :attr:`NodeOption.HasOnlyOutputs`/:py:`'has_only_outputs'`: + * :attr:`NodeOption.HasOnlyOutputs`: `'has_only_outputs'`: Returns nodes having only output flows (eg. Source) - * :attr:`NodeOption.HasOnlyInputs`/:py:`'has_only_inputs'`: + * :attr:`NodeOption.HasOnlyInputs`: `'has_only_inputs'`: Returns nodes having only input flows (eg. Sink) Additionally, busses can be excluded by setting `exclude_busses` to @@ -227,16 +226,16 @@ def node_input_by_type(results, node_type, droplevel=None): """ Gets all inputs for all nodes of the type `node_type` and returns a dataframe. - Parameter - --------- + Parameters + ---------- results: dict A result dictionary from a solved oemof.solph.Model object node_type: oemof.solph class Specifies the type of the node for that inputs are selected - Usage - -------- - import oemof.solph as solph + Notes + ----- + from oemof import solph from oemof.outputlib import views # solve oemof solph model 'm' @@ -261,15 +260,15 @@ def node_output_by_type(results, node_type, droplevel=None): """ Gets all outputs for all nodes of the type `node_type` and returns a dataframe. - Parameter - --------- + Parameters + ---------- results: dict A result dictionary from a solved oemof.solph.Model object node_type: oemof.solph class Specifies the type of the node for that outputs are selected - Usage - -------- + Notes + ----- import oemof.solph as solph from oemof.outputlib import views @@ -295,8 +294,8 @@ def net_storage_flow(results, node_type): input edge and one output edge both with flows within the domain of non-negative reals. - Parameter - --------- + Parameters + ---------- results: dict A result dictionary from a solved oemof.solph.Model object node_type: oemof.solph class @@ -336,13 +335,14 @@ def net_storage_flow(results, node_type): dataframes = [] for l in labels: - grouper = lambda x1: (lambda fr, to, ty: - 'output' if (fr == l and ty == 'flow') else - 'input' if (to == l and ty == 'flow') else - 'level' if (fr == l and ty != 'flow') else - None)(*x1) - - subset = df.groupby(grouper, axis=1).sum() + subset = df.groupby( + lambda x1: (lambda fr, to, ty: + 'output' if (fr == l and ty == 'flow') else + 'input' if (to == l and ty == 'flow') else + 'level' if (fr == l and ty != 'flow') else + None)(*x1), + axis=1 + ).sum() subset['net_flow'] = subset['output'] - subset['input'] diff --git a/tests/basic_tests.py b/tests/basic_tests.py deleted file mode 100644 index 1a74fc55d..000000000 --- a/tests/basic_tests.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 - - -"""Basic tests. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/tests/basic_tests.py - -SPDX-License-Identifier: MIT -""" -try: - from collections.abc import Iterable -except ImportError: - from collections import Iterable -from pprint import pformat - -from nose.tools import ok_, eq_ -import pandas as pd - -from oemof import energy_system as es -from oemof.network import Entity -from oemof.network import Bus, Transformer -from oemof.network import Bus as NewBus, Node, temporarily_modifies_registry -from oemof.groupings import Grouping, Nodes, Flows, FlowsWithNodes as FWNs - - -class TestsEnergySystem: - - @classmethod - def setUpClass(cls): - cls.timeindex = pd.date_range('1/1/2012', periods=5, freq='H') - - def setup(self): - self.es = es.EnergySystem() - Node.registry = self.es - - def test_entity_registration(self): - bus = Bus(label='bus-uid', type='bus-type') - eq_(self.es.nodes[0], bus) - bus2 = Bus(label='bus-uid2', type='bus-type') - eq_(self.es.nodes[1], bus2) - t1 = Transformer(label='pp_gas', inputs=[bus], outputs=[bus2]) - ok_(t1 in self.es.nodes) - self.es.timeindex = self.timeindex - ok_(len(self.es.timeindex) == 5) - - def test_entity_grouping_on_construction(self): - bus = Bus(label="test bus") - ensys = es.EnergySystem(entities=[bus]) - ok_(ensys.groups[bus.label] is bus) - - def test_that_nodes_is_a_proper_alias_for_entities(self): - b1, b2 = Bus(label="B1"), Bus(label="B2") - eq_(self.es.nodes, [b1, b2]) - empty = [] - self.es.nodes = empty - ok_(self.es.entities is empty) - - def test_that_none_is_not_a_valid_group(self): - def by_uid(n): - if "Not in 'Group'" in n.uid: - return None - else: - return "Group" - - ensys = es.EnergySystem(groupings=[by_uid]) - - ungrouped = [Entity(uid="Not in 'Group': {}".format(i)) - for i in range(10)] - grouped = [Entity(uid="In 'Group': {}".format(i)) - for i in range(10)] - ok_(None not in ensys.groups) - for g in ensys.groups.values(): - for e in ungrouped: - if isinstance(g, Iterable) and not isinstance(g, str): - ok_(e not in g) - for e in grouped: - if isinstance(g, Iterable) and not isinstance(g, str): - ok_(e in g) - - @temporarily_modifies_registry - def test_defining_multiple_groupings_with_one_function(self): - def assign_to_multiple_groups_in_one_go(n): - g1 = n.label[-1] - g2 = n.label[0:3] - return [g1, g2] - - ensy = es.EnergySystem(groupings=[assign_to_multiple_groups_in_one_go]) - Node.registry = ensy - [Node(label=("Foo: " if i % 2 == 0 else "Bar: ") + - "{}".format(i) + ("A" if i < 5 else "B")) for i in - range(10)] - for group in ["Foo", "Bar", "A", "B"]: - eq_(len(ensy.groups[group]), 5, - ("\n Failed testing length of group '{}'." + - "\n Expected: 5" + - "\n Got : {}" + - "\n Group : {}").format( - group, len(ensy.groups[group]), - sorted([e.label for e in ensy.groups[group]]))) - - def test_grouping_filter_parameter(self): - g1 = Grouping(key=lambda e: "The Special One", - filter=lambda e: "special" in str(e)) - g2 = Nodes(key=lambda e: "A Subset", - filter=lambda e: "subset" in str(e)) - ensys = es.EnergySystem(groupings=[g1, g2]) - special = Node(label="special") - subset = set(Node(label="subset: {}".format(i)) for i in range(10)) - others = set(Node(label="other: {}".format(i)) for i in range(10)) - ensys.add(special, *subset) - ensys.add(*others) - eq_(ensys.groups["The Special One"], special) - eq_(ensys.groups["A Subset"], subset) - - def test_proper_filtering(self): - """ `Grouping.filter` should not be "all or nothing". - - There was a bug where, if `Grouping.filter` returned `False` only for - some elements of `Grouping.value(e)`, those elements where actually - retained. - This test makes sure that the bug doesn't resurface again. - """ - g = Nodes(key="group", value=lambda _: {1, 2, 3, 4}, - filter=lambda x: x % 2 == 0) - ensys = es.EnergySystem(groupings=[g]) - special = Node(label="object") - ensys.add(special) - eq_(ensys.groups["group"], {2, 4}) - - def test_non_callable_group_keys(self): - collect_everything = Nodes(key="everything") - g1 = Grouping(key="The Special One", - filter=lambda e: "special" in e.label) - g2 = Nodes(key="A Subset", filter=lambda e: "subset" in e.label) - ensys = es.EnergySystem(groupings=[g1, g2, collect_everything]) - special = Node(label="special") - subset = set(Node(label="subset: {}".format(i)) for i in range(2)) - others = set(Node(label="other: {}".format(i)) for i in range(2)) - everything = subset.union(others) - everything.add(special) - ensys.add(*everything) - eq_(ensys.groups["The Special One"], special) - eq_(ensys.groups["A Subset"], subset) - eq_(ensys.groups["everything"], everything) - - def test_grouping_laziness(self): - """ Energy system `groups` should be fully lazy. - - `Node`s added to an energy system should only be tested for and put - into their respective groups right before the `groups` property of an - energy system is accessed. - """ - group = "Group" - g = Nodes(key=group, filter=lambda n: getattr(n, "group", False)) - self.es = es.EnergySystem(groupings=[g]) - buses = [Bus("Grouped"), Bus("Ungrouped one"), Bus("Ungrouped two")] - self.es.add(buses[0]) - buses[0].group = True - self.es.add(*buses[1:]) - ok_( - group in self.es.groups, - "\nExpected to find\n\n `{!r}`\n\nin `es.groups`.\nGot:\n\n `{}`" - .format( - group, - "\n ".join(pformat(set(self.es.groups.keys())).split("\n")), - ), - ) - ok_( - buses[0] in self.es.groups[group], - "\nExpected\n\n `{}`\n\nin `es.groups['{}']`:\n\n `{}`" - .format( - "\n ".join(pformat(buses[0]).split("\n")), - group, - "\n ".join(pformat(self.es.groups[group]).split("\n")) - ), - ) - - @temporarily_modifies_registry - def test_constant_group_keys(self): - """ Callable keys passed in as `constant_key` should not be called. - - The `constant_key` parameter can be used to specify callable group keys - without having to worry about `Grouping`s trying to call them. This - test makes sure that the parameter is handled correctly. - """ - everything = lambda: "everything" - collect_everything = Nodes(constant_key=everything) - ensys = es.EnergySystem(groupings=[collect_everything]) - Node.registry = ensys - node = Node(label="A Node") - ok_("everything" not in ensys.groups) - ok_(everything in ensys.groups) - eq_(ensys.groups[everything], {node}) - - @temporarily_modifies_registry - def test_flows(self): - key = object() - ensys = es.EnergySystem(groupings=[Flows(key)]) - Node.registry = ensys - flows = (object(), object()) - bus = NewBus(label="A Bus") - Node(label="A Node", inputs={bus: flows[0]}, outputs={bus: flows[1]}) - eq_(ensys.groups[key], set(flows)) - - @temporarily_modifies_registry - def test_flows_with_nodes(self): - key = object() - ensys = es.EnergySystem(groupings=[FWNs(key)]) - Node.registry = ensys - flows = (object(), object()) - bus = NewBus(label="A Bus") - node = Node(label="A Node", - inputs={bus: flows[0]}, outputs={bus: flows[1]}) - eq_(ensys.groups[key], {(bus, node, flows[0]), (node, bus, flows[1])}) - - def test_that_node_additions_are_signalled(self): - """ - When a node gets `add`ed, a corresponding signal should be emitted. - """ - node = Node(label="Node") - - def subscriber(sender, **kwargs): - ok_(sender is node) - ok_(kwargs['EnergySystem'] is self.es) - subscriber.called = True - - subscriber.called = False - - es.EnergySystem.signals[es.EnergySystem.add].connect( - subscriber, sender=node - ) - self.es.add(node) - ok_( - subscriber.called, - ( - "\nExpected `subscriber.called` to be `True`.\n" - "Got {}.\n" - "Probable reason: `subscriber` didn't get called." - ).format(subscriber.called), - ) diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index b281848d7..9899f5e6d 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -9,23 +9,21 @@ SPDX-License-Identifier: MIT """ -from difflib import unified_diff import logging import os.path as ospath import re +from difflib import unified_diff -from nose.tools import eq_, assert_raises import pandas as pd - -from oemof.network import Node -from oemof.tools import helpers -import oemof.solph as solph +from nose.tools import assert_raises +from nose.tools import eq_ +from oemof import solph +from oemof.network.network import Node logging.disable(logging.INFO) class TestsConstraint: - @classmethod def setup_class(cls): cls.objective_pattern = re.compile(r'^objective.*(?=s\.t\.)', @@ -33,7 +31,7 @@ def setup_class(cls): cls.date_time_index = pd.date_range('1/1/2012', periods=3, freq='H') - cls.tmppath = helpers.extend_basic_path('tmp') + cls.tmppath = solph.helpers.extend_basic_path('tmp') logging.info(cls.tmppath) def setup(self): @@ -373,6 +371,49 @@ def test_storage_invest_unbalanced(self): investment=solph.Investment(ep_costs=145)) self.compare_lp_files('storage_invest_unbalanced.lp') + def test_storage_fixed_losses(self): + """ + """ + bel = solph.Bus(label='electricityBus') + + solph.components.GenericStorage( + label='storage_no_invest', + inputs={bel: solph.Flow(nominal_value=16667, variable_costs=56)}, + outputs={bel: solph.Flow(nominal_value=16667, variable_costs=24)}, + nominal_storage_capacity=1e5, + loss_rate=0.13, + fixed_losses_relative=0.01, + fixed_losses_absolute=3, + inflow_conversion_factor=0.97, + outflow_conversion_factor=0.86, + initial_storage_level=0.4) + + self.compare_lp_files('storage_fixed_losses.lp') + + def test_storage_invest_1_fixed_losses(self): + """All invest variables are coupled. The invest variables of the Flows + will be created during the initialisation of the storage e.g. battery + """ + bel = solph.Bus(label='electricityBus') + + solph.components.GenericStorage( + label='storage1', + inputs={bel: solph.Flow(variable_costs=56)}, + outputs={bel: solph.Flow(variable_costs=24)}, + nominal_storage_capacity=None, + loss_rate=0.13, + fixed_losses_relative=0.01, + fixed_losses_absolute=3, + max_storage_level=0.9, + min_storage_level=0.1, + invest_relation_input_capacity=1/6, + invest_relation_output_capacity=1/6, + inflow_conversion_factor=0.97, + outflow_conversion_factor=0.86, + investment=solph.Investment(ep_costs=145, maximum=234)) + + self.compare_lp_files('storage_invest_1_fixed_losses.lp') + def test_transformer(self): """Constraint test of a LinearN1Transformer without Investment. """ @@ -698,3 +739,101 @@ def test_flow_schedule(self): conversion_factors={b_th: 1} ) self.compare_lp_files('flow_schedule.lp') + + def test_nonconvex_investment_storage_without_offset(self): + """All invest variables are coupled. The invest variables of the Flows + will be created during the initialisation of the storage e.g. battery + """ + bel = solph.Bus(label='electricityBus') + + solph.components.GenericStorage( + label='storage_non_convex', + inputs={bel: solph.Flow(variable_costs=56)}, + outputs={bel: solph.Flow(variable_costs=24)}, + nominal_storage_capacity=None, + loss_rate=0.13, + max_storage_level=0.9, + min_storage_level=0.1, + invest_relation_input_capacity=1 / 6, + invest_relation_output_capacity=1 / 6, + inflow_conversion_factor=0.97, + outflow_conversion_factor=0.86, + investment=solph.Investment(ep_costs=141, maximum=244, minimum=12, + nonconvex=True)) + + self.compare_lp_files('storage_invest_without_offset.lp') + + def test_nonconvex_investment_storage_with_offset(self): + """All invest variables are coupled. The invest variables of the Flows + will be created during the initialisation of the storage e.g. battery + """ + bel = solph.Bus(label='electricityBus') + + solph.components.GenericStorage( + label='storagenon_convex', + inputs={bel: solph.Flow(variable_costs=56)}, + outputs={bel: solph.Flow(variable_costs=24)}, + nominal_storage_capacity=None, + loss_rate=0.13, + max_storage_level=0.9, + min_storage_level=0.1, + invest_relation_input_capacity=1 / 6, + invest_relation_output_capacity=1 / 6, + inflow_conversion_factor=0.97, + outflow_conversion_factor=0.86, + investment=solph.Investment(ep_costs=145, minimum=19, offset=5, + nonconvex=True, maximum=1454)) + + self.compare_lp_files('storage_invest_with_offset.lp') + + def test_nonconvex_invest_storage_all_nonconvex(self): + """All invest variables are free and nonconvex.""" + b1 = solph.Bus(label='bus1') + + solph.components.GenericStorage( + label='storage_all_nonconvex', + inputs={b1: solph.Flow(investment=solph.Investment( + nonconvex=True, minimum=5, offset=10, maximum=30, + ep_costs=10))}, + outputs={b1: solph.Flow( + investment=solph.Investment( + nonconvex=True, minimum=8, offset=15, ep_costs=10, + maximum=20))}, + investment=solph.Investment( + nonconvex=True, ep_costs=20, offset=30, minimum=20, + maximum=100)) + + self.compare_lp_files('storage_invest_all_nonconvex.lp') + + def test_nonconvex_invest_sink_without_offset(self): + """ Non convex invest flow without offset, with minimum. + """ + bel = solph.Bus(label='electricityBus') + + solph.Sink(label='sink_nonconvex_invest', inputs={bel: solph.Flow( + summed_max=2.3, variable_costs=25, max=0.8, + investment=solph.Investment(ep_costs=500, minimum=15, + nonconvex=True, maximum=172))}) + self.compare_lp_files('flow_invest_without_offset.lp') + + def test_nonconvex_invest_source_with_offset(self): + """ Non convex invest flow with offset, with minimum. + """ + bel = solph.Bus(label='electricityBus') + + solph.Source(label='source_nonconvex_invest', inputs={bel: solph.Flow( + summed_max=2.3, variable_costs=25, max=0.8, + investment=solph.Investment(ep_costs=500, minimum=15, maximum=20, + offset=34, nonconvex=True))}) + self.compare_lp_files('flow_invest_with_offset.lp') + + def test_nonconvex_invest_source_with_offset_no_minimum(self): + """ Non convex invest flow with offset, without minimum. + """ + bel = solph.Bus(label='electricityBus') + + solph.Source(label='source_nonconvex_invest', inputs={bel: solph.Flow( + summed_max=2.3, variable_costs=25, max=0.8, + investment=solph.Investment(ep_costs=500, maximum=1234, + offset=34, nonconvex=True))}) + self.compare_lp_files('flow_invest_with_offset_no_minimum.lp') diff --git a/tests/lp_files/flow_invest_with_offset.lp b/tests/lp_files/flow_invest_with_offset.lp new file mode 100644 index 000000000..76d217b4b --- /dev/null +++ b/tests/lp_files/flow_invest_with_offset.lp @@ -0,0 +1,68 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++500 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++34 InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) ++25 flow(electricityBus_source_nonconvex_invest_0) ++25 flow(electricityBus_source_nonconvex_invest_1) ++25 flow(electricityBus_source_nonconvex_invest_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: ++1 flow(electricityBus_source_nonconvex_invest_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: ++1 flow(electricityBus_source_nonconvex_invest_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: ++1 flow(electricityBus_source_nonconvex_invest_2) += 0 + +c_u_InvestmentFlow_minimum_rule(electricityBus_source_nonconvex_invest)_: +-1 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++15 InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) +<= 0 + +c_u_InvestmentFlow_maximum_rule(electricityBus_source_nonconvex_invest)_: ++1 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) +-20 InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_0)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_1)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_2)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_2) +<= 0 + +c_u_InvestmentFlow_summed_max(electricityBus_source_nonconvex_invest)_: +-2.2999999999999998 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_0) ++1 flow(electricityBus_source_nonconvex_invest_1) ++1 flow(electricityBus_source_nonconvex_invest_2) +<= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_source_nonconvex_invest_0) <= +inf + 0 <= flow(electricityBus_source_nonconvex_invest_1) <= +inf + 0 <= flow(electricityBus_source_nonconvex_invest_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_source_nonconvex_invest) <= 20 + 0 <= InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) <= 1 +binary + InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) +end diff --git a/tests/lp_files/flow_invest_with_offset_no_minimum.lp b/tests/lp_files/flow_invest_with_offset_no_minimum.lp new file mode 100644 index 000000000..c9ca7dd12 --- /dev/null +++ b/tests/lp_files/flow_invest_with_offset_no_minimum.lp @@ -0,0 +1,67 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++500 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++34 InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) ++25 flow(electricityBus_source_nonconvex_invest_0) ++25 flow(electricityBus_source_nonconvex_invest_1) ++25 flow(electricityBus_source_nonconvex_invest_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: ++1 flow(electricityBus_source_nonconvex_invest_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: ++1 flow(electricityBus_source_nonconvex_invest_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: ++1 flow(electricityBus_source_nonconvex_invest_2) += 0 + +c_l_InvestmentFlow_minimum_rule(electricityBus_source_nonconvex_invest)_: ++1 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) +>= 0 + +c_u_InvestmentFlow_maximum_rule(electricityBus_source_nonconvex_invest)_: ++1 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) +-1234 InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_0)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_1)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_source_nonconvex_invest_2)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_2) +<= 0 + +c_u_InvestmentFlow_summed_max(electricityBus_source_nonconvex_invest)_: +-2.2999999999999998 InvestmentFlow_invest(electricityBus_source_nonconvex_invest) ++1 flow(electricityBus_source_nonconvex_invest_0) ++1 flow(electricityBus_source_nonconvex_invest_1) ++1 flow(electricityBus_source_nonconvex_invest_2) +<= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_source_nonconvex_invest_0) <= +inf + 0 <= flow(electricityBus_source_nonconvex_invest_1) <= +inf + 0 <= flow(electricityBus_source_nonconvex_invest_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_source_nonconvex_invest) <= 1234 + 0 <= InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) <= 1 +binary + InvestmentFlow_invest_status(electricityBus_source_nonconvex_invest) +end diff --git a/tests/lp_files/flow_invest_without_offset.lp b/tests/lp_files/flow_invest_without_offset.lp new file mode 100644 index 000000000..20f835fff --- /dev/null +++ b/tests/lp_files/flow_invest_without_offset.lp @@ -0,0 +1,67 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++500 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++25 flow(electricityBus_sink_nonconvex_invest_0) ++25 flow(electricityBus_sink_nonconvex_invest_1) ++25 flow(electricityBus_sink_nonconvex_invest_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: ++1 flow(electricityBus_sink_nonconvex_invest_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: ++1 flow(electricityBus_sink_nonconvex_invest_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: ++1 flow(electricityBus_sink_nonconvex_invest_2) += 0 + +c_u_InvestmentFlow_minimum_rule(electricityBus_sink_nonconvex_invest)_: +-1 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++15 InvestmentFlow_invest_status(electricityBus_sink_nonconvex_invest) +<= 0 + +c_u_InvestmentFlow_maximum_rule(electricityBus_sink_nonconvex_invest)_: ++1 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) +-172 InvestmentFlow_invest_status(electricityBus_sink_nonconvex_invest) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_sink_nonconvex_invest_0)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++1 flow(electricityBus_sink_nonconvex_invest_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_sink_nonconvex_invest_1)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++1 flow(electricityBus_sink_nonconvex_invest_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_sink_nonconvex_invest_2)_: +-0.80000000000000004 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++1 flow(electricityBus_sink_nonconvex_invest_2) +<= 0 + +c_u_InvestmentFlow_summed_max(electricityBus_sink_nonconvex_invest)_: +-2.2999999999999998 InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) ++1 flow(electricityBus_sink_nonconvex_invest_0) ++1 flow(electricityBus_sink_nonconvex_invest_1) ++1 flow(electricityBus_sink_nonconvex_invest_2) +<= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_sink_nonconvex_invest_0) <= +inf + 0 <= flow(electricityBus_sink_nonconvex_invest_1) <= +inf + 0 <= flow(electricityBus_sink_nonconvex_invest_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_sink_nonconvex_invest) <= 172 + 0 <= InvestmentFlow_invest_status(electricityBus_sink_nonconvex_invest) <= 1 +binary + InvestmentFlow_invest_status(electricityBus_sink_nonconvex_invest) +end diff --git a/tests/lp_files/storage_fixed_losses.lp b/tests/lp_files/storage_fixed_losses.lp new file mode 100644 index 000000000..e99d45113 --- /dev/null +++ b/tests/lp_files/storage_fixed_losses.lp @@ -0,0 +1,66 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++56 flow(electricityBus_storage_no_invest_0) ++56 flow(electricityBus_storage_no_invest_1) ++56 flow(electricityBus_storage_no_invest_2) ++24 flow(storage_no_invest_electricityBus_0) ++24 flow(storage_no_invest_electricityBus_1) ++24 flow(storage_no_invest_electricityBus_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: +-1 flow(electricityBus_storage_no_invest_0) ++1 flow(storage_no_invest_electricityBus_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: +-1 flow(electricityBus_storage_no_invest_1) ++1 flow(storage_no_invest_electricityBus_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: +-1 flow(electricityBus_storage_no_invest_2) ++1 flow(storage_no_invest_electricityBus_2) += 0 + +c_e_GenericStorageBlock_balance_first(storage_no_invest)_: ++1 GenericStorageBlock_capacity(storage_no_invest_0) +-0.96999999999999997 flow(electricityBus_storage_no_invest_0) ++1.1627906976744187 flow(storage_no_invest_electricityBus_0) += 33797 + +c_e_GenericStorageBlock_balance(storage_no_invest_1)_: +-0.87 GenericStorageBlock_capacity(storage_no_invest_0) ++1 GenericStorageBlock_capacity(storage_no_invest_1) +-0.96999999999999997 flow(electricityBus_storage_no_invest_1) ++1.1627906976744187 flow(storage_no_invest_electricityBus_1) += -1003 + +c_e_GenericStorageBlock_balance(storage_no_invest_2)_: +-0.87 GenericStorageBlock_capacity(storage_no_invest_1) ++1 GenericStorageBlock_capacity(storage_no_invest_2) +-0.96999999999999997 flow(electricityBus_storage_no_invest_2) ++1.1627906976744187 flow(storage_no_invest_electricityBus_2) += -1003 + +c_e_GenericStorageBlock_balanced_cstr(storage_no_invest)_: ++1 GenericStorageBlock_capacity(storage_no_invest_2) += 40000 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_storage_no_invest_0) <= 16667 + 0 <= flow(electricityBus_storage_no_invest_1) <= 16667 + 0 <= flow(electricityBus_storage_no_invest_2) <= 16667 + 0 <= flow(storage_no_invest_electricityBus_0) <= 16667 + 0 <= flow(storage_no_invest_electricityBus_1) <= 16667 + 0 <= flow(storage_no_invest_electricityBus_2) <= 16667 + 0 <= GenericStorageBlock_capacity(storage_no_invest_0) <= 100000 + 0 <= GenericStorageBlock_capacity(storage_no_invest_1) <= 100000 + 0 <= GenericStorageBlock_capacity(storage_no_invest_2) <= 100000 +end diff --git a/tests/lp_files/storage_invest_1_fixed_losses.lp b/tests/lp_files/storage_invest_1_fixed_losses.lp new file mode 100644 index 000000000..5a803caaa --- /dev/null +++ b/tests/lp_files/storage_invest_1_fixed_losses.lp @@ -0,0 +1,151 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++145 GenericInvestmentStorageBlock_invest(storage1) ++56 flow(electricityBus_storage1_0) ++56 flow(electricityBus_storage1_1) ++56 flow(electricityBus_storage1_2) ++24 flow(storage1_electricityBus_0) ++24 flow(storage1_electricityBus_1) ++24 flow(storage1_electricityBus_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: +-1 flow(electricityBus_storage1_0) ++1 flow(storage1_electricityBus_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: +-1 flow(electricityBus_storage1_1) ++1 flow(storage1_electricityBus_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: +-1 flow(electricityBus_storage1_2) ++1 flow(storage1_electricityBus_2) += 0 + +c_u_InvestmentFlow_max(electricityBus_storage1_0)_: +-1 InvestmentFlow_invest(electricityBus_storage1) ++1 flow(electricityBus_storage1_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storage1_1)_: +-1 InvestmentFlow_invest(electricityBus_storage1) ++1 flow(electricityBus_storage1_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storage1_2)_: +-1 InvestmentFlow_invest(electricityBus_storage1) ++1 flow(electricityBus_storage1_2) +<= 0 + +c_u_InvestmentFlow_max(storage1_electricityBus_0)_: +-1 InvestmentFlow_invest(storage1_electricityBus) ++1 flow(storage1_electricityBus_0) +<= 0 + +c_u_InvestmentFlow_max(storage1_electricityBus_1)_: +-1 InvestmentFlow_invest(storage1_electricityBus) ++1 flow(storage1_electricityBus_1) +<= 0 + +c_u_InvestmentFlow_max(storage1_electricityBus_2)_: +-1 InvestmentFlow_invest(storage1_electricityBus) ++1 flow(storage1_electricityBus_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_init_cap_limit(storage1)_: ++1 GenericInvestmentStorageBlock_init_cap(storage1) +-1 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_e_GenericInvestmentStorageBlock_balance_first(storage1)_: ++1 GenericInvestmentStorageBlock_capacity(storage1_0) +-0.87 GenericInvestmentStorageBlock_init_cap(storage1) ++0.01 GenericInvestmentStorageBlock_invest(storage1) +-0.96999999999999997 flow(electricityBus_storage1_0) ++1.1627906976744187 flow(storage1_electricityBus_0) += -3 + +c_e_GenericInvestmentStorageBlock_balance(storage1_1)_: +-0.87 GenericInvestmentStorageBlock_capacity(storage1_0) ++1 GenericInvestmentStorageBlock_capacity(storage1_1) ++0.01 GenericInvestmentStorageBlock_invest(storage1) +-0.96999999999999997 flow(electricityBus_storage1_1) ++1.1627906976744187 flow(storage1_electricityBus_1) += -3 + +c_e_GenericInvestmentStorageBlock_balance(storage1_2)_: +-0.87 GenericInvestmentStorageBlock_capacity(storage1_1) ++1 GenericInvestmentStorageBlock_capacity(storage1_2) ++0.01 GenericInvestmentStorageBlock_invest(storage1) +-0.96999999999999997 flow(electricityBus_storage1_2) ++1.1627906976744187 flow(storage1_electricityBus_2) += -3 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(storage1)_: ++1 GenericInvestmentStorageBlock_capacity(storage1_2) +-1 GenericInvestmentStorageBlock_init_cap(storage1) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(storage1)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storage1) ++1 InvestmentFlow_invest(electricityBus_storage1) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(storage1)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storage1) ++1 InvestmentFlow_invest(storage1_electricityBus) += 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage1_0)_: ++1 GenericInvestmentStorageBlock_capacity(storage1_0) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage1_1)_: ++1 GenericInvestmentStorageBlock_capacity(storage1_1) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage1_2)_: ++1 GenericInvestmentStorageBlock_capacity(storage1_2) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage1_0)_: +-1 GenericInvestmentStorageBlock_capacity(storage1_0) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage1_1)_: +-1 GenericInvestmentStorageBlock_capacity(storage1_1) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage1_2)_: +-1 GenericInvestmentStorageBlock_capacity(storage1_2) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage1) +<= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_storage1_0) <= +inf + 0 <= flow(electricityBus_storage1_1) <= +inf + 0 <= flow(electricityBus_storage1_2) <= +inf + 0 <= flow(storage1_electricityBus_0) <= +inf + 0 <= flow(storage1_electricityBus_1) <= +inf + 0 <= flow(storage1_electricityBus_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_storage1) <= +inf + 0 <= InvestmentFlow_invest(storage1_electricityBus) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage1_0) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage1_1) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage1_2) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(storage1) <= 234 + 0 <= GenericInvestmentStorageBlock_init_cap(storage1) <= +inf +end diff --git a/tests/lp_files/storage_invest_all_nonconvex.lp b/tests/lp_files/storage_invest_all_nonconvex.lp new file mode 100644 index 000000000..c450f1569 --- /dev/null +++ b/tests/lp_files/storage_invest_all_nonconvex.lp @@ -0,0 +1,159 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++20 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) ++30 GenericInvestmentStorageBlock_invest_status(storage_all_nonconvex) ++10 InvestmentFlow_invest(bus1_storage_all_nonconvex) ++10 InvestmentFlow_invest(storage_all_nonconvex_bus1) ++10 InvestmentFlow_invest_status(bus1_storage_all_nonconvex) ++15 InvestmentFlow_invest_status(storage_all_nonconvex_bus1) + +s.t. + +c_e_Bus_balance(bus1_0)_: +-1 flow(bus1_storage_all_nonconvex_0) ++1 flow(storage_all_nonconvex_bus1_0) += 0 + +c_e_Bus_balance(bus1_1)_: +-1 flow(bus1_storage_all_nonconvex_1) ++1 flow(storage_all_nonconvex_bus1_1) += 0 + +c_e_Bus_balance(bus1_2)_: +-1 flow(bus1_storage_all_nonconvex_2) ++1 flow(storage_all_nonconvex_bus1_2) += 0 + +c_u_InvestmentFlow_minimum_rule(bus1_storage_all_nonconvex)_: +-1 InvestmentFlow_invest(bus1_storage_all_nonconvex) ++5 InvestmentFlow_invest_status(bus1_storage_all_nonconvex) +<= 0 + +c_u_InvestmentFlow_minimum_rule(storage_all_nonconvex_bus1)_: +-1 InvestmentFlow_invest(storage_all_nonconvex_bus1) ++8 InvestmentFlow_invest_status(storage_all_nonconvex_bus1) +<= 0 + +c_u_InvestmentFlow_maximum_rule(bus1_storage_all_nonconvex)_: ++1 InvestmentFlow_invest(bus1_storage_all_nonconvex) +-30 InvestmentFlow_invest_status(bus1_storage_all_nonconvex) +<= 0 + +c_u_InvestmentFlow_maximum_rule(storage_all_nonconvex_bus1)_: ++1 InvestmentFlow_invest(storage_all_nonconvex_bus1) +-20 InvestmentFlow_invest_status(storage_all_nonconvex_bus1) +<= 0 + +c_u_InvestmentFlow_max(bus1_storage_all_nonconvex_0)_: +-1 InvestmentFlow_invest(bus1_storage_all_nonconvex) ++1 flow(bus1_storage_all_nonconvex_0) +<= 0 + +c_u_InvestmentFlow_max(bus1_storage_all_nonconvex_1)_: +-1 InvestmentFlow_invest(bus1_storage_all_nonconvex) ++1 flow(bus1_storage_all_nonconvex_1) +<= 0 + +c_u_InvestmentFlow_max(bus1_storage_all_nonconvex_2)_: +-1 InvestmentFlow_invest(bus1_storage_all_nonconvex) ++1 flow(bus1_storage_all_nonconvex_2) +<= 0 + +c_u_InvestmentFlow_max(storage_all_nonconvex_bus1_0)_: +-1 InvestmentFlow_invest(storage_all_nonconvex_bus1) ++1 flow(storage_all_nonconvex_bus1_0) +<= 0 + +c_u_InvestmentFlow_max(storage_all_nonconvex_bus1_1)_: +-1 InvestmentFlow_invest(storage_all_nonconvex_bus1) ++1 flow(storage_all_nonconvex_bus1_1) +<= 0 + +c_u_InvestmentFlow_max(storage_all_nonconvex_bus1_2)_: +-1 InvestmentFlow_invest(storage_all_nonconvex_bus1) ++1 flow(storage_all_nonconvex_bus1_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_init_cap_limit(storage_all_nonconvex)_: ++1 GenericInvestmentStorageBlock_init_cap(storage_all_nonconvex) +-1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) +<= 0 + +c_e_GenericInvestmentStorageBlock_balance_first(storage_all_nonconvex)_: ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_0) +-1 GenericInvestmentStorageBlock_init_cap(storage_all_nonconvex) +-1 flow(bus1_storage_all_nonconvex_0) ++1 flow(storage_all_nonconvex_bus1_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storage_all_nonconvex_1)_: +-1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_0) ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_1) +-1 flow(bus1_storage_all_nonconvex_1) ++1 flow(storage_all_nonconvex_bus1_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storage_all_nonconvex_2)_: +-1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_1) ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_2) +-1 flow(bus1_storage_all_nonconvex_2) ++1 flow(storage_all_nonconvex_bus1_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(storage_all_nonconvex)_: ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_2) +-1 GenericInvestmentStorageBlock_init_cap(storage_all_nonconvex) += 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_all_nonconvex_0)_: ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_0) +-1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_all_nonconvex_1)_: ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_1) +-1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_all_nonconvex_2)_: ++1 GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_2) +-1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) +<= 0 + +c_l_GenericInvestmentStorageBlock_limit_max(storage_all_nonconvex)_: +-1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) ++100 GenericInvestmentStorageBlock_invest_status(storage_all_nonconvex) +>= 0 + +c_l_GenericInvestmentStorageBlock_limit_min(storage_all_nonconvex)_: ++1 GenericInvestmentStorageBlock_invest(storage_all_nonconvex) +-20 GenericInvestmentStorageBlock_invest_status(storage_all_nonconvex) +>= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(bus1_storage_all_nonconvex_0) <= +inf + 0 <= flow(bus1_storage_all_nonconvex_1) <= +inf + 0 <= flow(bus1_storage_all_nonconvex_2) <= +inf + 0 <= flow(storage_all_nonconvex_bus1_0) <= +inf + 0 <= flow(storage_all_nonconvex_bus1_1) <= +inf + 0 <= flow(storage_all_nonconvex_bus1_2) <= +inf + 0 <= InvestmentFlow_invest(bus1_storage_all_nonconvex) <= 30 + 0 <= InvestmentFlow_invest(storage_all_nonconvex_bus1) <= 20 + 0 <= InvestmentFlow_invest_status(bus1_storage_all_nonconvex) <= 1 + 0 <= InvestmentFlow_invest_status(storage_all_nonconvex_bus1) <= 1 + 0 <= GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_0) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_1) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage_all_nonconvex_2) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(storage_all_nonconvex) <= 100 + 0 <= GenericInvestmentStorageBlock_init_cap(storage_all_nonconvex) <= +inf + 0 <= GenericInvestmentStorageBlock_invest_status(storage_all_nonconvex) <= 1 +binary + InvestmentFlow_invest_status(bus1_storage_all_nonconvex) + InvestmentFlow_invest_status(storage_all_nonconvex_bus1) + GenericInvestmentStorageBlock_invest_status(storage_all_nonconvex) +end diff --git a/tests/lp_files/storage_invest_with_offset.lp b/tests/lp_files/storage_invest_with_offset.lp new file mode 100644 index 000000000..e08407b74 --- /dev/null +++ b/tests/lp_files/storage_invest_with_offset.lp @@ -0,0 +1,162 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++145 GenericInvestmentStorageBlock_invest(storagenon_convex) ++5 GenericInvestmentStorageBlock_invest_status(storagenon_convex) ++56 flow(electricityBus_storagenon_convex_0) ++56 flow(electricityBus_storagenon_convex_1) ++56 flow(electricityBus_storagenon_convex_2) ++24 flow(storagenon_convex_electricityBus_0) ++24 flow(storagenon_convex_electricityBus_1) ++24 flow(storagenon_convex_electricityBus_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: +-1 flow(electricityBus_storagenon_convex_0) ++1 flow(storagenon_convex_electricityBus_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: +-1 flow(electricityBus_storagenon_convex_1) ++1 flow(storagenon_convex_electricityBus_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: +-1 flow(electricityBus_storagenon_convex_2) ++1 flow(storagenon_convex_electricityBus_2) += 0 + +c_u_InvestmentFlow_max(electricityBus_storagenon_convex_0)_: +-1 InvestmentFlow_invest(electricityBus_storagenon_convex) ++1 flow(electricityBus_storagenon_convex_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storagenon_convex_1)_: +-1 InvestmentFlow_invest(electricityBus_storagenon_convex) ++1 flow(electricityBus_storagenon_convex_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storagenon_convex_2)_: +-1 InvestmentFlow_invest(electricityBus_storagenon_convex) ++1 flow(electricityBus_storagenon_convex_2) +<= 0 + +c_u_InvestmentFlow_max(storagenon_convex_electricityBus_0)_: +-1 InvestmentFlow_invest(storagenon_convex_electricityBus) ++1 flow(storagenon_convex_electricityBus_0) +<= 0 + +c_u_InvestmentFlow_max(storagenon_convex_electricityBus_1)_: +-1 InvestmentFlow_invest(storagenon_convex_electricityBus) ++1 flow(storagenon_convex_electricityBus_1) +<= 0 + +c_u_InvestmentFlow_max(storagenon_convex_electricityBus_2)_: +-1 InvestmentFlow_invest(storagenon_convex_electricityBus) ++1 flow(storagenon_convex_electricityBus_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_init_cap_limit(storagenon_convex)_: ++1 GenericInvestmentStorageBlock_init_cap(storagenon_convex) +-1 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_e_GenericInvestmentStorageBlock_balance_first(storagenon_convex)_: ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_0) +-0.87 GenericInvestmentStorageBlock_init_cap(storagenon_convex) +-0.96999999999999997 flow(electricityBus_storagenon_convex_0) ++1.1627906976744187 flow(storagenon_convex_electricityBus_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storagenon_convex_1)_: +-0.87 GenericInvestmentStorageBlock_capacity(storagenon_convex_0) ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_1) +-0.96999999999999997 flow(electricityBus_storagenon_convex_1) ++1.1627906976744187 flow(storagenon_convex_electricityBus_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storagenon_convex_2)_: +-0.87 GenericInvestmentStorageBlock_capacity(storagenon_convex_1) ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_2) +-0.96999999999999997 flow(electricityBus_storagenon_convex_2) ++1.1627906976744187 flow(storagenon_convex_electricityBus_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(storagenon_convex)_: ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_2) +-1 GenericInvestmentStorageBlock_init_cap(storagenon_convex) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(storagenon_convex)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storagenon_convex) ++1 InvestmentFlow_invest(electricityBus_storagenon_convex) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(storagenon_convex)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storagenon_convex) ++1 InvestmentFlow_invest(storagenon_convex_electricityBus) += 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storagenon_convex_0)_: ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_0) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storagenon_convex_1)_: ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_1) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storagenon_convex_2)_: ++1 GenericInvestmentStorageBlock_capacity(storagenon_convex_2) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storagenon_convex_0)_: +-1 GenericInvestmentStorageBlock_capacity(storagenon_convex_0) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storagenon_convex_1)_: +-1 GenericInvestmentStorageBlock_capacity(storagenon_convex_1) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storagenon_convex_2)_: +-1 GenericInvestmentStorageBlock_capacity(storagenon_convex_2) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storagenon_convex) +<= 0 + +c_l_GenericInvestmentStorageBlock_limit_max(storagenon_convex)_: +-1 GenericInvestmentStorageBlock_invest(storagenon_convex) ++1454 GenericInvestmentStorageBlock_invest_status(storagenon_convex) +>= 0 + +c_l_GenericInvestmentStorageBlock_limit_min(storagenon_convex)_: ++1 GenericInvestmentStorageBlock_invest(storagenon_convex) +-19 GenericInvestmentStorageBlock_invest_status(storagenon_convex) +>= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_storagenon_convex_0) <= +inf + 0 <= flow(electricityBus_storagenon_convex_1) <= +inf + 0 <= flow(electricityBus_storagenon_convex_2) <= +inf + 0 <= flow(storagenon_convex_electricityBus_0) <= +inf + 0 <= flow(storagenon_convex_electricityBus_1) <= +inf + 0 <= flow(storagenon_convex_electricityBus_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_storagenon_convex) <= +inf + 0 <= InvestmentFlow_invest(storagenon_convex_electricityBus) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storagenon_convex_0) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storagenon_convex_1) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storagenon_convex_2) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(storagenon_convex) <= 1454 + 0 <= GenericInvestmentStorageBlock_init_cap(storagenon_convex) <= +inf + 0 <= GenericInvestmentStorageBlock_invest_status(storagenon_convex) <= 1 +binary + GenericInvestmentStorageBlock_invest_status(storagenon_convex) +end diff --git a/tests/lp_files/storage_invest_without_offset.lp b/tests/lp_files/storage_invest_without_offset.lp new file mode 100644 index 000000000..1ed3ad0c5 --- /dev/null +++ b/tests/lp_files/storage_invest_without_offset.lp @@ -0,0 +1,161 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++141 GenericInvestmentStorageBlock_invest(storage_non_convex) ++56 flow(electricityBus_storage_non_convex_0) ++56 flow(electricityBus_storage_non_convex_1) ++56 flow(electricityBus_storage_non_convex_2) ++24 flow(storage_non_convex_electricityBus_0) ++24 flow(storage_non_convex_electricityBus_1) ++24 flow(storage_non_convex_electricityBus_2) + +s.t. + +c_e_Bus_balance(electricityBus_0)_: +-1 flow(electricityBus_storage_non_convex_0) ++1 flow(storage_non_convex_electricityBus_0) += 0 + +c_e_Bus_balance(electricityBus_1)_: +-1 flow(electricityBus_storage_non_convex_1) ++1 flow(storage_non_convex_electricityBus_1) += 0 + +c_e_Bus_balance(electricityBus_2)_: +-1 flow(electricityBus_storage_non_convex_2) ++1 flow(storage_non_convex_electricityBus_2) += 0 + +c_u_InvestmentFlow_max(electricityBus_storage_non_convex_0)_: +-1 InvestmentFlow_invest(electricityBus_storage_non_convex) ++1 flow(electricityBus_storage_non_convex_0) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storage_non_convex_1)_: +-1 InvestmentFlow_invest(electricityBus_storage_non_convex) ++1 flow(electricityBus_storage_non_convex_1) +<= 0 + +c_u_InvestmentFlow_max(electricityBus_storage_non_convex_2)_: +-1 InvestmentFlow_invest(electricityBus_storage_non_convex) ++1 flow(electricityBus_storage_non_convex_2) +<= 0 + +c_u_InvestmentFlow_max(storage_non_convex_electricityBus_0)_: +-1 InvestmentFlow_invest(storage_non_convex_electricityBus) ++1 flow(storage_non_convex_electricityBus_0) +<= 0 + +c_u_InvestmentFlow_max(storage_non_convex_electricityBus_1)_: +-1 InvestmentFlow_invest(storage_non_convex_electricityBus) ++1 flow(storage_non_convex_electricityBus_1) +<= 0 + +c_u_InvestmentFlow_max(storage_non_convex_electricityBus_2)_: +-1 InvestmentFlow_invest(storage_non_convex_electricityBus) ++1 flow(storage_non_convex_electricityBus_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_init_cap_limit(storage_non_convex)_: ++1 GenericInvestmentStorageBlock_init_cap(storage_non_convex) +-1 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_e_GenericInvestmentStorageBlock_balance_first(storage_non_convex)_: ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_0) +-0.87 GenericInvestmentStorageBlock_init_cap(storage_non_convex) +-0.96999999999999997 flow(electricityBus_storage_non_convex_0) ++1.1627906976744187 flow(storage_non_convex_electricityBus_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storage_non_convex_1)_: +-0.87 GenericInvestmentStorageBlock_capacity(storage_non_convex_0) ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_1) +-0.96999999999999997 flow(electricityBus_storage_non_convex_1) ++1.1627906976744187 flow(storage_non_convex_electricityBus_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(storage_non_convex_2)_: +-0.87 GenericInvestmentStorageBlock_capacity(storage_non_convex_1) ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_2) +-0.96999999999999997 flow(electricityBus_storage_non_convex_2) ++1.1627906976744187 flow(storage_non_convex_electricityBus_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(storage_non_convex)_: ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_2) +-1 GenericInvestmentStorageBlock_init_cap(storage_non_convex) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(storage_non_convex)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storage_non_convex) ++1 InvestmentFlow_invest(electricityBus_storage_non_convex) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(storage_non_convex)_: +-0.16666666666666666 GenericInvestmentStorageBlock_invest(storage_non_convex) ++1 InvestmentFlow_invest(storage_non_convex_electricityBus) += 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_non_convex_0)_: ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_0) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_non_convex_1)_: ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_1) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_capacity(storage_non_convex_2)_: ++1 GenericInvestmentStorageBlock_capacity(storage_non_convex_2) +-0.90000000000000002 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage_non_convex_0)_: +-1 GenericInvestmentStorageBlock_capacity(storage_non_convex_0) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage_non_convex_1)_: +-1 GenericInvestmentStorageBlock_capacity(storage_non_convex_1) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_u_GenericInvestmentStorageBlock_min_capacity(storage_non_convex_2)_: +-1 GenericInvestmentStorageBlock_capacity(storage_non_convex_2) ++0.10000000000000001 GenericInvestmentStorageBlock_invest(storage_non_convex) +<= 0 + +c_l_GenericInvestmentStorageBlock_limit_max(storage_non_convex)_: +-1 GenericInvestmentStorageBlock_invest(storage_non_convex) ++244 GenericInvestmentStorageBlock_invest_status(storage_non_convex) +>= 0 + +c_l_GenericInvestmentStorageBlock_limit_min(storage_non_convex)_: ++1 GenericInvestmentStorageBlock_invest(storage_non_convex) +-12 GenericInvestmentStorageBlock_invest_status(storage_non_convex) +>= 0 + +c_e_ONE_VAR_CONSTANT: +ONE_VAR_CONSTANT = 1.0 + +bounds + 0 <= flow(electricityBus_storage_non_convex_0) <= +inf + 0 <= flow(electricityBus_storage_non_convex_1) <= +inf + 0 <= flow(electricityBus_storage_non_convex_2) <= +inf + 0 <= flow(storage_non_convex_electricityBus_0) <= +inf + 0 <= flow(storage_non_convex_electricityBus_1) <= +inf + 0 <= flow(storage_non_convex_electricityBus_2) <= +inf + 0 <= InvestmentFlow_invest(electricityBus_storage_non_convex) <= +inf + 0 <= InvestmentFlow_invest(storage_non_convex_electricityBus) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage_non_convex_0) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage_non_convex_1) <= +inf + 0 <= GenericInvestmentStorageBlock_capacity(storage_non_convex_2) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(storage_non_convex) <= 244 + 0 <= GenericInvestmentStorageBlock_init_cap(storage_non_convex) <= +inf + 0 <= GenericInvestmentStorageBlock_invest_status(storage_non_convex) <= 1 +binary + GenericInvestmentStorageBlock_invest_status(storage_non_convex) +end diff --git a/tests/regression_tests.py b/tests/regression_tests.py index f598e3792..c7a46806e 100644 --- a/tests/regression_tests.py +++ b/tests/regression_tests.py @@ -10,8 +10,8 @@ """ from nose.tools import ok_ -import oemof +from oemof import solph def test_version_metadata(): - ok_(oemof.__version__) + ok_(solph.__version__) diff --git a/tests/run_nose.py b/tests/run_nose.py index 6203299cd..422885cf7 100644 --- a/tests/run_nose.py +++ b/tests/run_nose.py @@ -1,5 +1,5 @@ -import sys import os +import sys try: import nose diff --git a/tests/solph_tests.py b/tests/solph_tests.py index 6cf26d246..199e6d69e 100644 --- a/tests/solph_tests.py +++ b/tests/solph_tests.py @@ -9,13 +9,15 @@ SPDX-License-Identifier: MIT """ -from nose.tools import ok_ +import os -from oemof.energy_system import EnergySystem as EnSys -from oemof.network import Node -from oemof.solph.blocks import InvestmentFlow as InvFlow -from oemof.solph import Investment import oemof.solph as solph +from nose.tools import ok_ +from oemof.network.energy_system import EnergySystem as EnSys +from oemof.network.network import Node +from oemof.solph import Investment +from oemof.solph.blocks import InvestmentFlow as InvFlow +from oemof.solph.helpers import extend_basic_path class TestsGrouping: @@ -50,3 +52,11 @@ def test_investment_flow_grouping(self): ok_(self.es.groups.get(InvFlow), ("Expected InvestmentFlow group to be nonempty.\n" + "Got: {}").format(self.es.groups.get(InvFlow))) + + +def test_helpers(): + ok_(os.path.isdir(os.path.join(os.path.expanduser('~'), '.oemof'))) + new_dir = extend_basic_path('test_xf67456_dir') + ok_(os.path.isdir(new_dir)) + os.rmdir(new_dir) + ok_(not os.path.isdir(new_dir)) diff --git a/tests/test_components.py b/tests/test_components.py index 1908ea2bd..8001a01fb 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -9,52 +9,59 @@ SPDX-License-Identifier: MIT """ -from nose import tools -from oemof import solph +import warnings +import pytest +from oemof.solph import Bus +from oemof.solph import Flow +from oemof.solph import Investment +from oemof.solph import NonConvex +from oemof.solph import components +from oemof.tools.debugging import SuspiciousUsageWarning # ********* GenericStorage ********* -@tools.raises(AttributeError) + def test_generic_storage_1(): """Duplicate definition inflow.""" - bel = solph.Bus() - solph.components.GenericStorage( - label='storage1', - inputs={bel: solph.Flow(variable_costs=10e10)}, - outputs={bel: solph.Flow(variable_costs=10e10)}, - loss_rate=0.00, initial_storage_level=0, - invest_relation_input_output=1, - invest_relation_output_capacity=1, - invest_relation_input_capacity=1, - investment=solph.Investment(), - inflow_conversion_factor=1, outflow_conversion_factor=0.8) + bel = Bus() + with pytest.raises(AttributeError, match="Overdetermined."): + components.GenericStorage( + label='storage1', + inputs={bel: Flow(variable_costs=10e10)}, + outputs={bel: Flow(variable_costs=10e10)}, + loss_rate=0.00, initial_storage_level=0, + invest_relation_input_output=1, + invest_relation_output_capacity=1, + invest_relation_input_capacity=1, + investment=Investment(), + inflow_conversion_factor=1, outflow_conversion_factor=0.8) -@tools.raises(AttributeError) def test_generic_storage_2(): """Nominal value defined with investment model.""" - bel = solph.Bus() - solph.components.GenericStorage( - label='storage3', - nominal_storage_capacity=45, - inputs={bel: solph.Flow(variable_costs=10e10)}, - outputs={bel: solph.Flow(variable_costs=10e10)}, - loss_rate=0.00, initial_storage_level=0, - invest_relation_input_capacity=1/6, - invest_relation_output_capacity=1/6, - inflow_conversion_factor=1, outflow_conversion_factor=0.8, - investment=solph.Investment(ep_costs=23)) + bel = Bus() + with pytest.raises(AttributeError, match="If an investment object"): + components.GenericStorage( + label='storage3', + nominal_storage_capacity=45, + inputs={bel: Flow(variable_costs=10e10)}, + outputs={bel: Flow(variable_costs=10e10)}, + loss_rate=0.00, initial_storage_level=0, + invest_relation_input_capacity=1/6, + invest_relation_output_capacity=1/6, + inflow_conversion_factor=1, outflow_conversion_factor=0.8, + investment=Investment(ep_costs=23)) def test_generic_storage_3(): """Nominal value defined with investment model.""" - bel = solph.Bus() - solph.components.GenericStorage( + bel = Bus() + components.GenericStorage( label='storage4', nominal_storage_capacity=45, - inputs={bel: solph.Flow(nominal_value=23, variable_costs=10e10)}, - outputs={bel: solph.Flow(nominal_value=7.5, variable_costs=10e10)}, + inputs={bel: Flow(nominal_value=23, variable_costs=10e10)}, + outputs={bel: Flow(nominal_value=7.5, variable_costs=10e10)}, loss_rate=0.00, initial_storage_level=0, inflow_conversion_factor=1, outflow_conversion_factor=0.8) @@ -69,96 +76,179 @@ def test_generic_storage_with_old_parameters(): } # Make sure an `AttributeError` is raised if we supply all deprecated # parameters. - with tools.assert_raises(AttributeError) as caught: - solph.components.GenericStorage( + with pytest.raises(AttributeError) as caught: + components.GenericStorage( label='`GenericStorage` with all deprecated parameters', **deprecated ) for parameter in deprecated: # Make sure every parameter used is mentioned in the exception's # message. - assert parameter in str(caught.exception) + assert parameter in str(caught.value) # Make sure an `AttributeError` is raised for each deprecated # parameter. - tools.assert_raises( + pytest.raises( AttributeError, - solph.components.GenericStorage, + components.GenericStorage, **{ "label": "`GenericStorage` with `{}`".format(parameter), parameter: deprecated[parameter], - } - ) + }) + + +def test_generic_storage_with_non_convex_investment(): + """Tests error if `offset` and `existing` attribute are given.""" + with pytest.raises( + AttributeError, + match=r"Values for 'offset' and 'existing' are given"): + bel = Bus() + components.GenericStorage( + label='storage4', + inputs={bel: Flow()}, + outputs={bel: Flow()}, + invest_relation_input_capacity=1/6, + invest_relation_output_capacity=1/6, + investment=Investment(nonconvex=True, existing=5, maximum=25)) + + +def test_generic_storage_with_non_convex_invest_maximum(): + """No investment maximum at nonconvex investment.""" + with pytest.raises( + AttributeError, + match=r"Please provide an maximum investment value"): + bel = Bus() + components.GenericStorage( + label='storage6', + inputs={bel: Flow()}, + outputs={bel: Flow()}, + invest_relation_input_capacity=1/6, + invest_relation_output_capacity=1/6, + investment=Investment(nonconvex=True)) + + +def test_generic_storage_with_convex_invest_offset(): + """Offset value is given and nonconvex is False.""" + with pytest.raises( + AttributeError, match=r"If `nonconvex` is `False`, the `offset`"): + bel = Bus() + components.GenericStorage( + label='storage6', + inputs={bel: Flow()}, + outputs={bel: Flow()}, + invest_relation_input_capacity=1/6, + invest_relation_output_capacity=1/6, + investment=Investment(offset=10)) + + +def test_generic_storage_with_invest_and_fixed_losses_absolute(): + """ + Storage with fixed losses in the investment mode but no minimum or existing + value is set an AttributeError is raised because this may result in storage + with zero capacity but fixed losses. + """ + msg = (r"With fixed_losses_absolute > 0, either investment.existing or" + " investment.minimum has to be non-zero.") + with pytest.raises(AttributeError, match=msg): + bel = Bus() + components.GenericStorage( + label='storage4', + inputs={bel: Flow()}, + outputs={bel: Flow()}, + investment=Investment(ep_costs=23, minimum=0, existing=0), + fixed_losses_absolute=[0, 0, 4], + ) # ********* OffsetTransformer ********* def test_offsettransformer_wrong_flow_type(): """No NonConvexFlow for Inflow defined.""" - with tools.assert_raises_regexp( - TypeError, 'Input flows must be of type NonConvexFlow!'): - bgas = solph.Bus(label='gasBus') - solph.components.OffsetTransformer( + with pytest.raises( + TypeError, match=r'Input flows must be of type NonConvexFlow!'): + bgas = Bus(label='gasBus') + components.OffsetTransformer( label='gasboiler', - inputs={bgas: solph.Flow()}, + inputs={bgas: Flow()}, coefficients=(-17, 0.9)) def test_offsettransformer_not_enough_coefficients(): - with tools.assert_raises_regexp( + with pytest.raises( ValueError, - 'Two coefficients or coefficient series have to be given.'): - solph.components.OffsetTransformer( + match=r'Two coefficients or coefficient series have to be given.'): + components.OffsetTransformer( label='of1', coefficients=([1, 4, 7])) def test_offsettransformer_too_many_coefficients(): - with tools.assert_raises_regexp( + with pytest.raises( ValueError, - 'Two coefficients or coefficient series have to be given.'): - solph.components.OffsetTransformer( + match=r'Two coefficients or coefficient series have to be given.'): + components.OffsetTransformer( label='of2', coefficients=(1, 4, 7)) def test_offsettransformer_empty(): """No NonConvexFlow for Inflow defined.""" - solph.components.OffsetTransformer() + components.OffsetTransformer() def test_offsettransformer__too_many_input_flows(): """Too many Input Flows defined.""" - with tools.assert_raises_regexp( - ValueError, 'OffsetTransformer` must not have more than 1'): - bgas = solph.Bus(label='GasBus') - bcoal = solph.Bus(label='CoalBus') - solph.components.OffsetTransformer( + with pytest.raises(ValueError, + match=r"OffsetTransformer` must not have more than 1"): + bgas = Bus(label='GasBus') + bcoal = Bus(label='CoalBus') + components.OffsetTransformer( label='ostf_2_in', inputs={ - bgas: solph.Flow( + bgas: Flow( nominal_value=60, min=0.5, max=1.0, - nonconvex=solph.NonConvex()), - bcoal: solph.Flow( + nonconvex=NonConvex()), + bcoal: Flow( nominal_value=30, min=0.3, max=1.0, - nonconvex=solph.NonConvex()) + nonconvex=NonConvex()) }, coefficients=(20, 0.5)) def test_offsettransformer_too_many_output_flows(): """Too many Output Flows defined.""" - with tools.assert_raises_regexp( - ValueError, 'OffsetTransformer` must not have more than 1'): - bm1 = solph.Bus(label='my_offset_Bus1') - bm2 = solph.Bus(label='my_offset_Bus2') + with pytest.raises( + ValueError, match='OffsetTransformer` must not have more than 1'): + bm1 = Bus(label='my_offset_Bus1') + bm2 = Bus(label='my_offset_Bus2') - solph.components.OffsetTransformer( + components.OffsetTransformer( label='ostf_2_out', inputs={ - bm1: solph.Flow( + bm1: Flow( nominal_value=60, min=0.5, max=1.0, - nonconvex=solph.NonConvex()) + nonconvex=NonConvex()) }, - outputs={bm1: solph.Flow(), - bm2: solph.Flow()}, + outputs={bm1: Flow(), + bm2: Flow()}, coefficients=(20, 0.5)) + + +# ********* GenericCHP ********* +def test_generic_chp_without_warning(): + warnings.filterwarnings("error", category=SuspiciousUsageWarning) + bel = Bus(label='electricityBus') + bth = Bus(label='heatBus') + bgas = Bus(label='commodityBus') + components.GenericCHP( + label='combined_cycle_extraction_turbine', + fuel_input={bgas: Flow( + H_L_FG_share_max=[0.183])}, + electrical_output={bel: Flow( + P_max_woDH=[155.946], + P_min_woDH=[68.787], + Eta_el_max_woDH=[0.525], + Eta_el_min_woDH=[0.444])}, + heat_output={bth: Flow( + Q_CW_min=[10.552])}, + Beta=[0.122], back_pressure=False) + warnings.filterwarnings("always", category=SuspiciousUsageWarning) diff --git a/tests/test_console_scripts.py b/tests/test_console_scripts.py index 50d139c82..3ecd589dc 100644 --- a/tests/test_console_scripts.py +++ b/tests/test_console_scripts.py @@ -8,9 +8,8 @@ SPDX-License-Identifier: MIT """ -import oemof.tools.console_scripts as console_scripts +import oemof.solph.console_scripts as console_scripts def test_console_scripts(): console_scripts.check_oemof_installation(silent=False) - pass diff --git a/tests/test_constraints_module.py b/tests/test_constraints_module.py new file mode 100644 index 000000000..5f8c673a0 --- /dev/null +++ b/tests/test_constraints_module.py @@ -0,0 +1,43 @@ +import pandas as pd +from oemof import solph + + +def test_special(): + date_time_index = pd.date_range('1/1/2012', periods=5, freq='H') + energysystem = solph.EnergySystem(timeindex=date_time_index) + bel = solph.Bus(label='electricityBus') + flow1 = solph.Flow(nominal_value=100, my_factor=0.8) + flow2 = solph.Flow(nominal_value=50) + src1 = solph.Source(label='source1', outputs={bel: flow1}) + src2 = solph.Source(label='source2', outputs={bel: flow2}) + energysystem.add(bel, src1, src2) + model = solph.Model(energysystem) + flow_with_keyword = {(src1, bel): flow1, } + solph.constraints.generic_integral_limit( + model, "my_factor", flow_with_keyword, limit=777) + + +def test_something_else(): + date_time_index = pd.date_range('1/1/2012', periods=5, freq='H') + energysystem = solph.EnergySystem(timeindex=date_time_index) + bel1 = solph.Bus(label='electricity1') + bel2 = solph.Bus(label='electricity2') + energysystem.add(bel1, bel2) + energysystem.add(solph.Transformer( + label='powerline_1_2', + inputs={bel1: solph.Flow()}, + outputs={bel2: solph.Flow( + investment=solph.Investment(ep_costs=20))})) + energysystem.add(solph.Transformer( + label='powerline_2_1', + inputs={bel2: solph.Flow()}, + outputs={bel1: solph.Flow( + investment=solph.Investment(ep_costs=20))})) + om = solph.Model(energysystem) + line12 = energysystem.groups['powerline_1_2'] + line21 = energysystem.groups['powerline_2_1'] + solph.constraints.equate_variables( + om, + om.InvestmentFlow.invest[line12, bel2], + om.InvestmentFlow.invest[line21, bel1], + name="my_name") diff --git a/tests/test_groupings.py b/tests/test_groupings.py index 5aaf4f63c..3ba67eb1a 100644 --- a/tests/test_groupings.py +++ b/tests/test_groupings.py @@ -12,9 +12,9 @@ from types import MappingProxyType as MaProTy -from nose.tools import assert_raises, eq_ - -from oemof.groupings import Grouping +from nose.tools import assert_raises +from nose.tools import eq_ +from oemof.network.groupings import Grouping def test_initialization_argument_checks(): diff --git a/tests/test_models.py b/tests/test_models.py index 9092a4605..4b58f6acf 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,12 +9,14 @@ SPDX-License-Identifier: MIT """ +import warnings + import pandas as pd -from oemof import solph -from oemof import outputlib -from nose.tools import eq_, raises from nose import tools -import warnings +from nose.tools import eq_ +from nose.tools import raises +from oemof import solph +from oemof.solph.helpers import calculate_timeincrement def test_timeincrement_with_valid_timeindex(): @@ -43,6 +45,49 @@ def test_timeincrement_list(): eq_(m.timeincrement[3], 3) +def test_nonequ_timeincrement(): + timeindex_hourly = pd.date_range('1/1/2019', periods=2, freq='H') + timeindex_30mins = pd.date_range( + '1/1/2019 03:00:00', periods=2, freq='30min') + timeindex_2h = pd.date_range('1/1/2019 04:00:00', periods=2, freq='2H') + timeindex = timeindex_hourly.append([timeindex_30mins, timeindex_2h]) + timeincrement = calculate_timeincrement(timeindex=timeindex) + eq_(timeincrement, solph.sequence([1.0, 1.0, 2.0, 0.5, 0.5, 2.0])) + + +def test_nonequ_timeincrement_fill(): + timeindex_hourly = pd.date_range('1/1/2019', periods=2, freq='H') + timeindex_30mins = pd.date_range( + '1/1/2019 03:00:00', periods=2, freq='30min') + timeindex_2h = pd.date_range('1/1/2019 04:00:00', periods=2, freq='2H') + timeindex = timeindex_hourly.append([timeindex_30mins, timeindex_2h]) + fvalue = pd.Timedelta(hours=9) + timeincrement = calculate_timeincrement(timeindex=timeindex, + fill_value=fvalue) + eq_(timeincrement, solph.sequence([9.0, 1.0, 2.0, 0.5, 0.5, 2.0])) + + +@raises(IndexError) +def test_nonequ_duplicate_timeindex(): + timeindex_hourly = pd.date_range('1/1/2019', periods=2, freq='H') + timeindex_45mins = pd.date_range('1/1/2019', periods=2, freq='45min') + timeindex = timeindex_hourly.append([timeindex_45mins]) + calculate_timeincrement(timeindex=timeindex) + + +@raises(AttributeError) +def test_nonequ_with_non_valid_timeindex(): + timeindex = 5 + calculate_timeincrement(timeindex=timeindex) + + +@raises(AttributeError) +def test_nonequ_with_non_valid_fill(): + timeindex = pd.date_range('1/1/2019', periods=2, freq='H') + fill_value = 2 + calculate_timeincrement(timeindex=timeindex, fill_value=fill_value) + + def test_optimal_solution(): es = solph.EnergySystem(timeindex=[1]) bel = solph.Bus(label='bus') @@ -53,7 +98,7 @@ def test_optimal_solution(): m = solph.models.Model(es, timeincrement=1) m.solve('cbc') m.results() - outputlib.processing.meta_results(m) + solph.processing.meta_results(m) def test_infeasible_model(): @@ -69,4 +114,4 @@ def test_infeasible_model(): m = solph.models.Model(es, timeincrement=1) m.solve(solver='cbc') assert "Optimization ended with status" in str(w[0].message) - outputlib.processing.meta_results(m) + solph.processing.meta_results(m) diff --git a/tests/test_network_classes.py b/tests/test_network_classes.py index 8041390b2..a88738066 100644 --- a/tests/test_network_classes.py +++ b/tests/test_network_classes.py @@ -10,11 +10,17 @@ """ from traceback import format_exception_only as feo -from nose.tools import assert_raises, eq_, ok_ -from oemof.energy_system import EnergySystem as EnSys -from oemof.network import (Bus, Edge, Node, Transformer, registry_changed_to, - temporarily_modifies_registry) +from nose.tools import assert_raises +from nose.tools import eq_ +from nose.tools import ok_ +from oemof.network.energy_system import EnergySystem as EnSys +from oemof.network.network import Bus +from oemof.network.network import Edge +from oemof.network.network import Node +from oemof.network.network import Transformer +from oemof.network.network import registry_changed_to +from oemof.network.network import temporarily_modifies_registry class TestsNode: diff --git a/tests/test_outputlib/__init__.py b/tests/test_outputlib/__init__.py index 8ac7497dc..242f42977 100644 --- a/tests/test_outputlib/__init__.py +++ b/tests/test_outputlib/__init__.py @@ -1,8 +1,14 @@ import os + import pandas as pd -from oemof.solph import ( - Bus, Sink, Source, Flow, Transformer, Model, EnergySystem) +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import Transformer filename = os.path.join(os.path.dirname(__file__), 'input_data.csv') data = pd.read_csv(filename, sep=",") diff --git a/tests/test_outputlib/test_views.py b/tests/test_outputlib/test_views.py index acbaff53c..9b9f74fe3 100644 --- a/tests/test_outputlib/test_views.py +++ b/tests/test_outputlib/test_views.py @@ -1,10 +1,14 @@ -from nose.tools import eq_, raises -from . import optimization_model, energysystem -from oemof.outputlib import processing, views +from nose.tools import eq_ +from nose.tools import raises +from oemof.solph import processing +from oemof.solph import views +from . import energysystem +from . import optimization_model -class Filter_Test(): + +class TestFilterView: def setup(self): self.results = processing.results(optimization_model) self.param_results = processing.parameter_as_dict(optimization_model) diff --git a/tests/test_processing.py b/tests/test_processing.py index fa6c1683d..a634600f7 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -9,19 +9,27 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_, assert_raises, ok_ import pandas -from pandas.util.testing import assert_series_equal, assert_frame_equal -from oemof.solph import ( - EnergySystem, Bus, Transformer, Flow, Investment, Sink, Model) +from nose.tools import assert_raises +from nose.tools import eq_ +from nose.tools import ok_ +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Investment +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Transformer +from oemof.solph import processing +from oemof.solph import views from oemof.solph.components import GenericStorage -from oemof.outputlib import processing -from oemof.outputlib import views +from pandas.util.testing import assert_frame_equal +from pandas.util.testing import assert_series_equal class TestParameterResult: @classmethod - def setUpClass(cls): + def setup_class(cls): cls.period = 24 cls.es = EnergySystem( timeindex=pandas.date_range( @@ -171,7 +179,11 @@ def test_nodes_with_none_exclusion(self): 'investment_existing': 0, 'investment_maximum': float('inf'), 'investment_minimum': 0, + 'investment_nonconvex': False, + 'investment_offset': 0, 'label': 'storage', + 'fixed_losses_absolute': 0, + 'fixed_losses_relative': 0, 'inflow_conversion_factor': 1, 'loss_rate': 0, 'max_storage_level': 1, @@ -200,7 +212,11 @@ def test_nodes_with_none_exclusion_old_name(self): 'investment_existing': 0, 'investment_maximum': float('inf'), 'investment_minimum': 0, + 'investment_nonconvex': False, + 'investment_offset': 0, 'label': 'storage', + 'fixed_losses_absolute': 0, + 'fixed_losses_relative': 0, 'inflow_conversion_factor': 1, 'loss_rate': 0, 'max_storage_level': 1, diff --git a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py index a6b77edb3..240b61c58 100644 --- a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py +++ b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py @@ -10,22 +10,32 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ -import oemof.solph as solph -from oemof.outputlib import processing, views - import logging import os + import pandas as pd -from oemof.network import Node +from nose.tools import eq_ +from oemof.network import network +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Investment +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import Transformer +from oemof.solph import components +from oemof.solph import constraints +from oemof.solph import processing +from oemof.solph import views def test_connect_invest(): date_time_index = pd.date_range('1/1/2012', periods=24 * 7, freq='H') - energysystem = solph.EnergySystem(timeindex=date_time_index) - Node.registry = energysystem - + energysystem = EnergySystem(timeindex=date_time_index) + network.Node.registry = energysystem + # Read data file full_filename = os.path.join(os.path.dirname(__file__), 'connect_invest.csv') @@ -34,49 +44,49 @@ def test_connect_invest(): logging.info('Create oemof objects') # create electricity bus - bel1 = solph.Bus(label="electricity1") - bel2 = solph.Bus(label="electricity2") + bel1 = Bus(label="electricity1") + bel2 = Bus(label="electricity2") # create excess component for the electricity bus to allow overproduction - solph.Sink(label='excess_bel', inputs={bel2: solph.Flow()}) - solph.Source(label='shortage', outputs={bel2: solph.Flow( + Sink(label='excess_bel', inputs={bel2: Flow()}) + Source(label='shortage', outputs={bel2: Flow( variable_costs=50000)}) # create fixed source object representing wind power plants - solph.Source(label='wind', outputs={bel1: solph.Flow( + Source(label='wind', outputs={bel1: Flow( actual_value=data['wind'], nominal_value=1000000, fixed=True)}) # create simple sink object representing the electrical demand - solph.Sink(label='demand', inputs={bel1: solph.Flow( + Sink(label='demand', inputs={bel1: Flow( actual_value=data['demand_el'], fixed=True, nominal_value=1)}) - storage = solph.components.GenericStorage( + storage = components.GenericStorage( label='storage', - inputs={bel1: solph.Flow(variable_costs=10e10)}, - outputs={bel1: solph.Flow(variable_costs=10e10)}, + inputs={bel1: Flow(variable_costs=10e10)}, + outputs={bel1: Flow(variable_costs=10e10)}, loss_rate=0.00, initial_storage_level=0, invest_relation_input_capacity=1/6, invest_relation_output_capacity=1/6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - investment=solph.Investment(ep_costs=0.2), + investment=Investment(ep_costs=0.2), ) - line12 = solph.Transformer( + line12 = Transformer( label="line12", - inputs={bel1: solph.Flow()}, - outputs={bel2: solph.Flow(investment=solph.Investment(ep_costs=20))}) + inputs={bel1: Flow()}, + outputs={bel2: Flow(investment=Investment(ep_costs=20))}) - line21 = solph.Transformer( + line21 = Transformer( label="line21", - inputs={bel2: solph.Flow()}, - outputs={bel1: solph.Flow(investment=solph.Investment(ep_costs=20))}) + inputs={bel2: Flow()}, + outputs={bel1: Flow(investment=Investment(ep_costs=20))}) - om = solph.Model(energysystem) + om = Model(energysystem) - solph.constraints.equate_variables( + constraints.equate_variables( om, om.InvestmentFlow.invest[line12, bel2], om.InvestmentFlow.invest[line21, bel1], 2) - solph.constraints.equate_variables( + constraints.equate_variables( om, om.InvestmentFlow.invest[line12, bel2], om.GenericInvestmentStorageBlock.invest[storage]) diff --git a/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py b/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py index a49068859..e2f2d09df 100644 --- a/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py +++ b/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py @@ -14,13 +14,18 @@ SPDX-License-Identifier: MIT """ -from nose.tools import ok_ import logging -import pyomo.environ as po + import pandas as pd -from oemof.network import Node -from oemof.solph import (Sink, Transformer, Bus, Flow, - Model, EnergySystem) +import pyomo.environ as po +from nose.tools import ok_ +from oemof.network.network import Node +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Transformer def test_add_constraints_example(solver='cbc', nologg=False): diff --git a/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py b/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py index 58cebd63c..8cc2f8c99 100644 --- a/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py +++ b/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py @@ -12,12 +12,20 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import os + import pandas as pd -import oemof.solph as solph -from oemof.network import Node -from oemof.outputlib import processing, views +from nose.tools import eq_ +from oemof.network.network import Node +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import custom +from oemof.solph import processing +from oemof.solph import views def test_gen_caes(): @@ -30,23 +38,23 @@ def test_gen_caes(): # create an energy system idx = pd.date_range('1/1/2017', periods=periods, freq='H') - es = solph.EnergySystem(timeindex=idx) + es = EnergySystem(timeindex=idx) Node.registry = es # resources - bgas = solph.Bus(label='bgas') + bgas = Bus(label='bgas') - solph.Source(label='rgas', outputs={ - bgas: solph.Flow(variable_costs=20)}) + Source(label='rgas', outputs={ + bgas: Flow(variable_costs=20)}) # power - bel_source = solph.Bus(label='bel_source') - solph.Source(label='source_el', outputs={ - bel_source: solph.Flow(variable_costs=data['price_el_source'])}) + bel_source = Bus(label='bel_source') + Source(label='source_el', outputs={ + bel_source: Flow(variable_costs=data['price_el_source'])}) - bel_sink = solph.Bus(label='bel_sink') - solph.Sink(label='sink_el', inputs={ - bel_sink: solph.Flow(variable_costs=data['price_el_sink'])}) + bel_sink = Bus(label='bel_sink') + Sink(label='sink_el', inputs={ + bel_sink: Flow(variable_costs=data['price_el_sink'])}) # dictionary with parameters for a specific CAES plant # based on thermal modelling and linearization techniques @@ -74,15 +82,15 @@ def test_gen_caes(): } # generic compressed air energy storage (caes) plant - solph.custom.GenericCAES( + custom.GenericCAES( label='caes', - electrical_input={bel_source: solph.Flow()}, - fuel_input={bgas: solph.Flow()}, - electrical_output={bel_sink: solph.Flow()}, + electrical_input={bel_source: Flow()}, + fuel_input={bgas: Flow()}, + electrical_output={bel_sink: Flow()}, params=concept, fixed_costs=0) # create an optimization problem and solve it - om = solph.Model(es) + om = Model(es) # solve model om.solve(solver='cbc') diff --git a/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py b/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py index c2c1763ba..69082c78b 100644 --- a/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py +++ b/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py @@ -12,12 +12,14 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import os -import pandas as pd + import oemof.solph as solph -from oemof.network import Node -from oemof.outputlib import processing, views +import pandas as pd +from nose.tools import eq_ +from oemof.network.network import Node +from oemof.solph import processing +from oemof.solph import views def test_gen_chp(): diff --git a/tests/test_scripts/test_solph/test_lopf/test_lopf.py b/tests/test_scripts/test_solph/test_lopf/test_lopf.py index 2513c4b04..dbb4bc81d 100644 --- a/tests/test_scripts/test_solph/test_lopf/test_lopf.py +++ b/tests/test_scripts/test_solph/test_lopf/test_lopf.py @@ -12,14 +12,19 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import logging -import pandas as pd - -from oemof import outputlib -import oemof.solph as solph +import pandas as pd +from nose.tools import eq_ +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Investment +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source from oemof.solph import custom +from oemof.solph import processing +from oemof.solph import views def test_lopf(solver="cbc"): @@ -27,7 +32,7 @@ def test_lopf(solver="cbc"): # create time index for 192 hours in May. date_time_index = pd.date_range("5/5/2012", periods=1, freq="H") - es = solph.EnergySystem(timeindex=date_time_index) + es = EnergySystem(timeindex=date_time_index) ########################################################################## # Create oemof.solph objects @@ -48,7 +53,7 @@ def test_lopf(solver="cbc"): input=b_el0, output=b_el1, reactance=0.0001, - investment=solph.Investment(ep_costs=10), + investment=Investment(ep_costs=10), min=-1, max=1, ) @@ -77,24 +82,24 @@ def test_lopf(solver="cbc"): ) es.add( - solph.Source( + Source( label="gen_0", - outputs={b_el0: solph.Flow(nominal_value=100, variable_costs=50)}, + outputs={b_el0: Flow(nominal_value=100, variable_costs=50)}, ) ) es.add( - solph.Source( + Source( label="gen_1", - outputs={b_el1: solph.Flow(nominal_value=100, variable_costs=25)}, + outputs={b_el1: Flow(nominal_value=100, variable_costs=25)}, ) ) es.add( - solph.Sink( + Sink( label="load", inputs={ - b_el2: solph.Flow( + b_el2: Flow( nominal_value=100, actual_value=1, fixed=True ) }, @@ -106,14 +111,14 @@ def test_lopf(solver="cbc"): ########################################################################## logging.info("Creating optimisation model") - om = solph.Model(es) + om = Model(es) logging.info("Running lopf on 3-Node exmaple system") om.solve(solver=solver) - results = outputlib.processing.results(om) + results = processing.results(om) - generators = outputlib.views.node_output_by_type(results, solph.Source) + generators = views.node_output_by_type(results, Source) generators_test_results = { (es.groups["gen_0"], es.groups["b_0"], "flow"): 20, @@ -142,4 +147,4 @@ def test_lopf(solver="cbc"): ) # objective function - eq_(round(outputlib.processing.meta_results(om)["objective"]), 3200) + eq_(round(processing.meta_results(om)["objective"]), 3200) diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py index b69a28c04..4b70dbba7 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py @@ -13,12 +13,19 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import os + import pandas as pd -from oemof.solph import (Sink, Source, Transformer, Bus, Flow, Model, - EnergySystem) -from oemof.outputlib import processing, views +from nose.tools import eq_ +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import Transformer +from oemof.solph import processing +from oemof.solph import views def test_dispatch_example(solver='cbc', periods=24*5): diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py index 226f5d169..fda3577fb 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py @@ -12,10 +12,16 @@ """ from nose.tools import eq_ -from oemof.solph import (Sink, Source, Transformer, Bus, Flow, Model, - EnergySystem) -from oemof.outputlib import processing, views -from oemof.network import Node +from oemof.network.network import Node +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import Transformer +from oemof.solph import processing +from oemof.solph import views def test_dispatch_one_time_step(solver='cbc'): diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py index e34944cc5..9d1560f0e 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py @@ -13,14 +13,22 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import os + import pandas as pd -from oemof.solph import (Sink, Source, Transformer, Bus, Flow, Model, - EnergySystem, Investment) +from nose.tools import eq_ +from oemof.network.network import Node +from oemof.solph import Bus +from oemof.solph import EnergySystem +from oemof.solph import Flow +from oemof.solph import Investment +from oemof.solph import Model +from oemof.solph import Sink +from oemof.solph import Source +from oemof.solph import Transformer +from oemof.solph import processing +from oemof.solph import views from oemof.tools import economics -from oemof.outputlib import processing, views -from oemof.network import Node def test_dispatch_example(solver='cbc', periods=24*5): diff --git a/tests/test_scripts/test_solph/test_storage_investment/es_dump_test_2_1dev.oemof b/tests/test_scripts/test_solph/test_storage_investment/es_dump_test_2_1dev.oemof deleted file mode 100644 index 543377b27d79347b33d1be6ee4eae99daaa01a47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72333 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 diff --git a/tests/test_scripts/test_solph/test_storage_investment/es_dump_test_2_3dev.oemof b/tests/test_scripts/test_solph/test_storage_investment/es_dump_test_3_2dev.oemof similarity index 54% rename from tests/test_scripts/test_solph/test_storage_investment/es_dump_test_2_3dev.oemof rename to tests/test_scripts/test_solph/test_storage_investment/es_dump_test_3_2dev.oemof index 6ea9237d51ee3d36dfb542e1a821c6878aee5535..6900e7db3f7aa4ef9a990251e45f21a33e9894cb 100644 GIT binary patch delta 30218 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 O6aE>hdMQ%n;r|a3(6aCV delta 27818 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 diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py index bd599fb1d..ca675734e 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py @@ -9,12 +9,12 @@ SPDX-License-Identifier: MIT """ -import oemof.solph as solph -from oemof.network import Node -from oemof.outputlib import processing, views - import logging + import pandas as pd +from oemof import solph +from oemof.network.network import Node +from oemof.solph import views def test_regression_investment_storage(solver='cbc'): @@ -65,7 +65,7 @@ def test_regression_investment_storage(solver='cbc'): om.solve(solver=solver) # Results - results = processing.results(om) + results = solph.processing.results(om) electricity_bus = views.node(results, 'electricity') my_results = electricity_bus['sequences'].sum(axis=0).to_dict() diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py index 720a8f801..58de918ff 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py @@ -34,19 +34,17 @@ SPDX-License-Identifier: MIT """ -from pickle import UnpicklingError - -from nose.tools import eq_ - -from oemof.tools import economics - -import oemof.solph as solph -from oemof.network import Node -from oemof.outputlib import processing, views - import logging import os +from unittest import skip + import pandas as pd +from nose.tools import eq_ +from oemof import solph +from oemof.network.network import Node +from oemof.solph import processing +from oemof.solph import views +from oemof.tools import economics PP_GAS = None @@ -161,33 +159,18 @@ def test_results_with_actual_dump(): eq_(round(meta['objective']), 423167578261115584) +@skip("Opening an old dump may fail due to different python versions or" + " version of other packages. We can try to reactivate the test with" + " v0.4.0.") def test_results_with_old_dump(): """ - Test again with a stored dump created with v0.2.1dev (896a6d50) + Test again with a stored dump created with v0.3.2dev (896a6d50) """ energysystem = solph.EnergySystem() - error = None - try: - energysystem.restore( - dpath=os.path.dirname(os.path.realpath(__file__)), - filename='es_dump_test_2_1dev.oemof') - except UnpicklingError as e: - error = e - - # Just making sure, the right error is raised. If the error message - # changes, the test has to be changed accordingly. - eq_(len(str(error)), 431) - - # ************************************************** - # Test again with a stored dump created with v0.2.3dev (896a6d50) - energysystem = solph.EnergySystem() energysystem.restore( dpath=os.path.dirname(os.path.realpath(__file__)), - filename='es_dump_test_2_3dev.oemof') - # Note: This internal attribute is new in v.0.3.0, so the dump doesn't - # contain it for obvious reasons. Setting it manually to the correct - # value prevents the test from erroring. - energysystem._first_ungrouped_node_index_ = len(energysystem.nodes) + filename='es_dump_test_3_2dev.oemof') + results = energysystem.results['main'] electricity_bus = views.node(results, 'electricity') @@ -224,4 +207,3 @@ def test_solph_transformer_attributes_before_dump_and_after_restore(): # Compare attributes before dump and after restore eq_(trsf_attr_before_dump, trsf_attr_after_restore) - diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py index 94dfe053b..670e75e6f 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py @@ -34,16 +34,16 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ +import logging +import os from collections import namedtuple import oemof.solph as solph -from oemof.network import Node -from oemof.outputlib import processing, views - -import logging -import os import pandas as pd +from nose.tools import eq_ +from oemof.network.network import Node +from oemof.solph import processing +from oemof.solph import views class Label(namedtuple('solph_label', ['tag1', 'tag2', 'tag3'])): diff --git a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py index d3fabac63..b18984c8c 100644 --- a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py +++ b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py @@ -12,15 +12,14 @@ SPDX-License-Identifier: MIT """ -from nose.tools import eq_ import logging import os -import pandas as pd - -from oemof import outputlib -from oemof.network import Node -import oemof.solph as solph +import pandas as pd +from nose.tools import eq_ +from oemof import solph +from oemof.network.network import Node +from oemof.solph import views def test_variable_chp(filename="variable_chp.csv", solver='cbc'): @@ -99,11 +98,10 @@ def test_variable_chp(filename="variable_chp.csv", solver='cbc'): logging.info('Solve the optimization problem') om.solve(solver=solver) - optimisation_results = outputlib.processing.results(om) - parameter = outputlib.processing.parameter_as_dict(energysystem) + optimisation_results = solph.processing.results(om) + parameter = solph.processing.parameter_as_dict(energysystem) - myresults = outputlib.views.node(optimisation_results, - "('natural', 'gas')") + myresults = views.node(optimisation_results, "('natural', 'gas')") sumresults = myresults['sequences'].sum(axis=0) maxresults = myresults['sequences'].max(axis=0) @@ -130,7 +128,7 @@ def test_variable_chp(filename="variable_chp.csv", solver='cbc'): eq_(parameter[(energysystem.groups["('fixed_chp', 'gas')"], None)] ['scalars']['label'], "('fixed_chp', 'gas')") eq_(parameter[(energysystem.groups["('fixed_chp', 'gas')"], None)] - ['scalars']["conversion_factors_('electricity', 2)"], 0.3) + ['scalars']["conversion_factors_('electricity', 2)"], 0.3) # objective function - eq_(round(outputlib.processing.meta_results(om)['objective']), 326661590) + eq_(round(solph.processing.meta_results(om)['objective']), 326661590) diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 69d959924..ef455ecdc 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -9,8 +9,10 @@ SPDX-License-Identifier: MIT """ +from nose.tools import assert_raises +from nose.tools import eq_ +from nose.tools import ok_ from oemof import solph -from nose.tools import assert_raises, eq_, ok_ def test_transformer_class(): diff --git a/tests/test_warnings.py b/tests/test_warnings.py new file mode 100644 index 000000000..3afef5ddb --- /dev/null +++ b/tests/test_warnings.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 - + +"""Test debugging warning. + +This file is part of project oemof (github.com/oemof/oemof). It's copyrighted +by the contributors recorded in the version control history of the file, +available from its original location oemof/tests/tool_tests.py + +SPDX-License-Identifier: MIT +""" + +import warnings + +from nose.tools import eq_ +from nose.tools import ok_ +from nose.tools import with_setup +from oemof import solph +from oemof.network import network +from oemof.tools.debugging import SuspiciousUsageWarning + + +def setup_func(): + """Explicitly activate the warnings.""" + warnings.filterwarnings("always", category=SuspiciousUsageWarning) + + +@with_setup(setup_func) +def test_that_the_sink_warnings_actually_get_raised(): + """ Sink doesn't warn about potentially erroneous usage. + """ + look_out = network.Bus() + msg = "`Sink` 'test_sink' constructed without `inputs`." + with warnings.catch_warnings(record=True) as w: + solph.Sink(label='test_sink', outputs={look_out: "A typo!"}) + ok_(len(w) == 1) + eq_(msg, str(w[-1].message)) + + +@with_setup(setup_func) +def test_filtered_warning(): + """ Sink doesn't warn about potentially erroneous usage. + """ + warnings.filterwarnings("ignore", category=SuspiciousUsageWarning) + look_out = network.Bus() + with warnings.catch_warnings(record=True) as w: + network.Sink(outputs={look_out: "A typo!"}) + ok_(len(w) == 0) + + +@with_setup(setup_func) +def test_that_the_source_warnings_actually_get_raised(): + """ Source doesn't warn about potentially erroneous usage. + """ + look_out = network.Bus() + msg = "`Source` 'test_source' constructed without `outputs`." + with warnings.catch_warnings(record=True) as w: + solph.Source(label='test_source', inputs={look_out: "A typo!"}) + ok_(len(w) == 1) + eq_(msg, str(w[-1].message)) + + +@with_setup(setup_func) +def test_that_the_solph_source_warnings_actually_get_raised(): + """ Source doesn't warn about potentially erroneous usage. + """ + look_out = network.Bus() + msg = "`Source` 'solph_sink' constructed without `outputs`." + with warnings.catch_warnings(record=True) as w: + solph.Source(label="solph_sink", inputs={look_out: "A typo!"}) + ok_(len(w) == 1) + eq_(msg, str(w[-1].message)) + + +@with_setup(setup_func) +def test_that_the_transformer_warnings_actually_get_raised(): + """ Transformer doesn't warn about potentially erroneous usage. + """ + look_out = network.Bus() + msg = "`Transformer` 'no input' constructed without `inputs`." + with warnings.catch_warnings(record=True) as w: + solph.Transformer(label='no input', outputs={look_out: "No inputs!"}) + ok_(len(w) == 1) + eq_(msg, str(w[-1].message)) + msg = "`Transformer` 'no output' constructed without `outputs`." + with warnings.catch_warnings(record=True) as w: + solph.Transformer(label='no output', + inputs={look_out: "No outputs!"}) + ok_(len(w) == 1) + eq_(msg, str(w[-1].message)) diff --git a/tests/tool_tests.py b/tests/tool_tests.py deleted file mode 100644 index 819d1f147..000000000 --- a/tests/tool_tests.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 - - -"""Test the created constraints against approved constraints. - -This file is part of project oemof (github.com/oemof/oemof). It's copyrighted -by the contributors recorded in the version control history of the file, -available from its original location oemof/tests/tool_tests.py - -SPDX-License-Identifier: MIT -""" - -import os - -from nose.tools import ok_, assert_raises_regexp - -from oemof.tools import logger -from oemof.tools import helpers -from oemof.tools import economics - - -def test_helpers(): - ok_(os.path.isdir(os.path.join(os.path.expanduser('~'), '.oemof'))) - new_dir = helpers.extend_basic_path('test_xf67456_dir') - ok_(os.path.isdir(new_dir)) - os.rmdir(new_dir) - ok_(not os.path.isdir(new_dir)) - - -def test_logger(): - filepath = logger.define_logging() - ok_(isinstance(filepath, str)) - ok_(filepath[-9:] == 'oemof.log') - ok_(os.path.isfile(filepath)) - - -def test_annuity(): - """Test annuity function of economics tool.""" - ok_(round(economics.annuity(1000, 10, 0.1)) == 163) - ok_(round(economics.annuity(capex=1000, wacc=0.1, n=10, u=5)) == 264) - ok_(round(economics.annuity(1000, 10, 0.1, u=5, cost_decrease=0.1)) == 222) - - -def test_annuity_exceptions(): - """Test out-of-bounds-error of the annuity tool.""" - msg = "Input arguments for 'annuity' out of bounds!" - assert_raises_regexp(ValueError, msg, economics.annuity, 1000, 10, 2) - assert_raises_regexp(ValueError, msg, economics.annuity, 1000, 0.5, 1) - assert_raises_regexp( - ValueError, msg, economics.annuity, 1000, 10, 0.1, u=0.3) - assert_raises_regexp( - ValueError, msg, economics.annuity, 1000, 10, 0.1, cost_decrease=-1) diff --git a/tox.ini b/tox.ini index 0371ed4ba..0c0bcffad 100644 --- a/tox.ini +++ b/tox.ini @@ -1,39 +1,121 @@ -# Tox (https://tox.readthedocs.io/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - [tox] -envlist = py,db-first,db-last,db-first-dev +envlist = + clean, + check, + docs, + py36, + py37, + py38, + py38-nocov, + report -[testenv:py] +[testenv] +basepython = + docs: {env:TOXPYTHON:python3.7} + {bootstrap,clean,check,report,codecov,coveralls}: {env:TOXPYTHON:python3} setenv = - PIP_USER = false -install_command = pip install -r269.requirements.txt {opts} {packages} -commands = nosetests --with-doctest -deps = nose + PYTHONPATH={toxinidir}/tests + PYTHONUNBUFFERED=yes +passenv = + * +deps = + pytest + pytest-travis-fold +commands = + {posargs:pytest -vv --ignore=src} + +[testenv:bootstrap] +deps = + jinja2 + matrix +skip_install = true +commands = + python ci/bootstrap.py --no-env + +[testenv:check] +deps = + docutils + check-manifest + flake8 + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py check --strict --metadata --restructuredtext + check-manifest {toxinidir} + flake8 src tests setup.py + isort --verbose --check-only --diff --recursive src tests setup.py + + +[testenv:docs] +usedevelop = true +deps = + -r{toxinidir}/docs/requirements.txt +commands = + sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs -[testenv:db-first-dev] -usedevelop = True +[testenv:coveralls] +deps = + coveralls +skip_install = true +commands = + coveralls [] + + + +[testenv:codecov] +deps = + codecov +skip_install = true +commands = + codecov [] + +[testenv:report] +deps = coverage +skip_install = true +commands = + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = coverage + +[testenv:py36] +basepython = {env:TOXPYTHON:python3.6} setenv = - PIP_USER = false -commands = nosetests --with-doctest + {[testenv]setenv} +usedevelop = true +commands = + {posargs:pytest --cov --cov-report=term-missing -vv} deps = - nose - -r269.requirements.txt + {[testenv]deps} + pytest-cov -[testenv:db-first] +[testenv:py37] +basepython = {env:TOXPYTHON:python3.7} setenv = - PIP_USER = false -commands = nosetests --with-doctest + {[testenv]setenv} +usedevelop = true +commands = + {posargs:pytest --cov --cov-report=term-missing -vv} deps = - nose - -r269.requirements.txt + {[testenv]deps} + pytest-cov -[testenv:db-last] +[testenv:py38] +basepython = {env:TOXPYTHON:python3.8} setenv = - PIP_USER = false -commands = nosetests --with-doctest -deps = nose -extras = -r269.requirements.txt + {[testenv]setenv} +usedevelop = true +commands = + {posargs:pytest --cov --cov-report=term-missing -vv} +deps = + {[testenv]deps} + pytest-cov +[testenv:py38-nocov] +basepython = {env:TOXPYTHON:python3.8} From ecf0c671f54a32a4329ad5c6c1c0bb88258a3589 Mon Sep 17 00:00:00 2001 From: uvchik Date: Tue, 7 Apr 2020 13:49:42 +0200 Subject: [PATCH 25/45] Fix merge in blocks --- src/oemof/solph/blocks.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/oemof/solph/blocks.py b/src/oemof/solph/blocks.py index df35958eb..898f49e16 100644 --- a/src/oemof/solph/blocks.py +++ b/src/oemof/solph/blocks.py @@ -308,7 +308,7 @@ def _objective_expression(self): class InvestmentFlow(SimpleBlock): - r"""Block for all flows with :attr:`investment` being not None. + r"""Block for all flows with :attr:`Investment` being not None. See :class:`oemof.solph.options.Investment` for all parameters of the *Investment* class. @@ -558,6 +558,25 @@ def _investvar_bound_rule(block, i, o): # TODO: Add gradient constraints + + def _min_invest_rule(block, i, o): + """Rule definition for applying a minimum investment + """ + expr = (m.flows[i, o].investment.minimum * + self.invest_status[i, o] <= self.invest[i, o]) + return expr + self.minimum_rule = Constraint( + self.NON_CONVEX_INVESTFLOWS, rule=_min_invest_rule) + + def _max_invest_rule(block, i, o): + """Rule definition for applying a minimum investment + """ + expr = self.invest[i, o] <= ( + m.flows[i, o].investment.maximum * self.invest_status[i, o]) + return expr + self.maximum_rule = Constraint( + self.NON_CONVEX_INVESTFLOWS, rule=_max_invest_rule) + def _investflow_fixed_rule(block, i, o, t): """Rule definition of constraint to fix flow variable of investment flow to (normed) actual value From 36d7ad081339d039185355837d6f1762525583cc Mon Sep 17 00:00:00 2001 From: uvchik Date: Tue, 7 Apr 2020 17:41:10 +0200 Subject: [PATCH 26/45] Fix pep8 issues --- src/oemof/solph/blocks.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/oemof/solph/blocks.py b/src/oemof/solph/blocks.py index 926da2a3f..305e7fb44 100644 --- a/src/oemof/solph/blocks.py +++ b/src/oemof/solph/blocks.py @@ -100,14 +100,14 @@ class Flow(SimpleBlock): **The following parts of the objective function are created:** If :attr:`variable_costs` are set by the user: - .. math:: + .. math:: \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t) The expression can be accessed by :attr:`om.Flow.variable_costs` and their value after optimization by :meth:`om.Flow.variable_costs()` . - If :attr:`schedule`, :attr:`schedule_cost_pos` and :attr:`schedule_cost_neg` are - set by the user: + If :attr:`schedule`, :attr:`schedule_cost_pos` and + :attr:`schedule_cost_neg` are set by the user: .. math:: \sum_{(i,o)} \sum_t schedule_cost_pos(i, o, t) \cdot \ schedule_slack_pos(i, o, t) + schedule_cost_neg(i, o, t) \cdot \ schedule_slack_neg(i, o, t) @@ -158,7 +158,7 @@ def _create(self, group=None): len(g[2].schedule) != 0 or (len(g[2].schedule) == 0 and g[2].schedule[0] is not None))]) - + # ######################### Variables ################################ self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, @@ -175,7 +175,7 @@ def _create(self, group=None): self.schedule_slack_neg = Var(self.SCHEDULE_FLOWS, m.TIMESTEPS, within=NonNegativeReals) - + # set upper bound of gradient variable for i, o, f in group: if m.flows[i, o].positive_gradient['ub'][0] is not None: @@ -298,7 +298,7 @@ def _objective_expression(self): gradient_costs += (self.negative_gradient[i, o, t] * m.flows[i, o].negative_gradient[ 'costs']) - + schedule = m.flows[i, o].schedule if (len(schedule) > 1 or (len(schedule) == 0 and From 2a66fc2295559b53dd26008f75883a3a0457c18c Mon Sep 17 00:00:00 2001 From: uvchik Date: Tue, 7 Apr 2020 17:46:32 +0200 Subject: [PATCH 27/45] Fix codacy issue --- tests/constraint_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index 9899f5e6d..7dd75c9b9 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -723,7 +723,7 @@ def test_dsm_module_interval(self): self.compare_lp_files('dsm_module_interval.lp') def test_flow_schedule(self): - """Contraint test of scheduled flows + """Contraint test of scheduled flows. """ b_gas = solph.Bus(label='bus_gas') b_th = solph.Bus(label='bus_th_penalty') From abab4eaf7216561e67169fd33182d6ccfcf56c2b Mon Sep 17 00:00:00 2001 From: uvchik Date: Tue, 7 Apr 2020 17:49:49 +0200 Subject: [PATCH 28/45] Fix another codacy issue --- tests/constraint_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index 7dd75c9b9..b28dc6938 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -723,8 +723,7 @@ def test_dsm_module_interval(self): self.compare_lp_files('dsm_module_interval.lp') def test_flow_schedule(self): - """Contraint test of scheduled flows. - """ + """Constraint test of scheduled flows.""" b_gas = solph.Bus(label='bus_gas') b_th = solph.Bus(label='bus_th_penalty') From 5f634834ed47a33e96d5b01c6b2858d15d61141a Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 12:57:47 +0200 Subject: [PATCH 29/45] Remove default value dor schedule penalty costs --- oemof/solph/network.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 1905fe9a4..6a90bd598 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -180,11 +180,9 @@ def __init__(self, **kwargs): dictionaries = ['positive_gradient', 'negative_gradient'] defaults = {'fixed': False, 'min': 0, 'max': 1, 'variable_costs': 0, 'positive_gradient': {'ub': None, 'costs': 0}, - 'negative_gradient': {'ub': None, 'costs': 0}, - 'schedule_cost_neg': 0, 'schedule_cost_pos': 0, + 'negative_gradient': {'ub': None, 'costs': 0} } keys = [k for k in kwargs if k != 'label'] - for attribute in set(scalars + sequences + dictionaries + keys): value = kwargs.get(attribute, defaults.get(attribute)) if attribute in dictionaries: @@ -210,13 +208,12 @@ def __init__(self, **kwargs): "nonconvex flows!") if ( len(self.schedule) != 0 and - ((len(self.schedule_cost_pos) == 0 and - not self.schedule_cost_pos[0]) or - (len(self.schedule_cost_neg) == 0 and - not self.schedule_cost_neg[0]))): - raise ValueError("The penalty and schedule attribute need " - "to be used in combination. \n Please set " - "the schedule attribute of the flow.") + (hasattr(self, 'schedule_cost_neg') and + hasattr(self, 'schedule_cost_pos'))): + raise ValueError("The schedule attribute and the associated costs " + "need to be used in combination. \n Please set " + "the `schedule_cost_neg` and `schedule_cost_pos` " + "attributes of the flow.") class Bus(on.Bus): From 3048635a6a94b48450d31dbae08d662727d03037 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 12:59:46 +0200 Subject: [PATCH 30/45] mind the dynamic property of sequence --- oemof/solph/blocks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/oemof/solph/blocks.py b/oemof/solph/blocks.py index c9da5417f..dfe224f96 100644 --- a/oemof/solph/blocks.py +++ b/oemof/solph/blocks.py @@ -144,11 +144,12 @@ def _create(self, group=None): initialize=[(g[0], g[1]) for g in group if g[2].integer]) - self.SCHEDULE_FLOWS = Set( - initialize=[(g[0], g[1]) for g in group if ( - len(g[2].schedule) != 0 or - (len(g[2].schedule) == 0 and - g[2].schedule[0] is not None))]) + self.SCHEDULE_FLOWS = Set(initialize=[ + (g[0], g[1]) for g in group if ( + g[2].schedule[0] is not None or + any([g[2].schedule[i] is not None for i in range( + 0, len(g[2].schedule))]))]) + # ######################### Variables ################################ self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, From 5b5d328f70308703b09489e3a447e21fe248ca1c Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 13:04:58 +0200 Subject: [PATCH 31/45] fixed logical bug in schedule attribute validation --- oemof/solph/network.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 6a90bd598..32e15bdb2 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -206,10 +206,9 @@ def __init__(self, **kwargs): if self.investment and self.nonconvex: raise ValueError("Investment flows cannot be combined with " + "nonconvex flows!") - if ( - len(self.schedule) != 0 and - (hasattr(self, 'schedule_cost_neg') and - hasattr(self, 'schedule_cost_pos'))): + if (len(self.schedule) != 0 and + not hasattr(self, 'schedule_cost_neg') and + not hasattr(self, 'schedule_cost_pos')): raise ValueError("The schedule attribute and the associated costs " "need to be used in combination. \n Please set " "the `schedule_cost_neg` and `schedule_cost_pos` " From 15d32d1690ce9072db996680c0595b55a3ae1ede Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 13:16:36 +0200 Subject: [PATCH 32/45] fixed logical bug in schedule attribute validation --- oemof/solph/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oemof/solph/network.py b/oemof/solph/network.py index 32e15bdb2..152b7c5b4 100644 --- a/oemof/solph/network.py +++ b/oemof/solph/network.py @@ -207,8 +207,8 @@ def __init__(self, **kwargs): raise ValueError("Investment flows cannot be combined with " + "nonconvex flows!") if (len(self.schedule) != 0 and - not hasattr(self, 'schedule_cost_neg') and - not hasattr(self, 'schedule_cost_pos')): + (self.schedule_cost_neg[0] is None or + self.schedule_cost_pos[0] is None)): raise ValueError("The schedule attribute and the associated costs " "need to be used in combination. \n Please set " "the `schedule_cost_neg` and `schedule_cost_pos` " From 0cd492b96f2a3447bdd5e045e8e25b443f26c1e5 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 13:21:57 +0200 Subject: [PATCH 33/45] Adapt tests to removed schedulecosts default value --- tests/test_processing.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index fa6c1683d..793000944 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -99,8 +99,6 @@ def test_flows_with_none_exclusion(self): 'positive_gradient_costs': 0, 'variable_costs': 0, 'label': str(b_el2.outputs[demand].label), - 'schedule_cost_pos': 0, - 'schedule_cost_neg': 0, } ).sort_index() ) @@ -134,8 +132,8 @@ def test_flows_without_none_exclusion(self): 'flow': None, 'values': None, 'label': str(b_el2.outputs[demand].label), - 'schedule_cost_pos': 0, - 'schedule_cost_neg': 0, + 'schedule_cost_pos': None, + 'schedule_cost_neg': None, 'schedule': None, } assert_series_equal( From 564c7e7a91885f5f731d373248d5fd4fe0d15573 Mon Sep 17 00:00:00 2001 From: Caterina Koehl Date: Fri, 10 Apr 2020 13:27:21 +0200 Subject: [PATCH 34/45] added test for schedule attributes validation --- tests/test_solph_network_classes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 69d959924..0534e92de 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -39,3 +39,9 @@ def test_flow_classes(): solph.Flow(investment=solph.Investment(), nonconvex=solph.NonConvex()) with assert_raises(AttributeError): solph.Flow(fixed_costs=34) + with assert_raises(ValueError): + solph.Flow(schedule=[54, 74, 90]) + with assert_raises(ValueError): + solph.Flow(schedule=[54, 74, 90], schedule_cost_neg=1000) + with assert_raises(ValueError): + solph.Flow(schedule=[54, 74, 90], schedule_cost_pos=1000) From c51772b319e85f8c7db97f8dfe4ddffbfa4b99c6 Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 19:34:54 +0200 Subject: [PATCH 35/45] Added whatsnew file --- docs/whatsnew/v0-4-1.rst | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/whatsnew/v0-4-1.rst diff --git a/docs/whatsnew/v0-4-1.rst b/docs/whatsnew/v0-4-1.rst new file mode 100644 index 000000000..dc078cca3 --- /dev/null +++ b/docs/whatsnew/v0-4-1.rst @@ -0,0 +1,49 @@ +v0.4.1 (???) +++++++++++++++++++++++++++ + + +API changes +########### + +* something + +New features +############ + +* It is now possible to determine a schedule for a flow. Flow will be pushed + to the schedule, if possible. + +New components +############## + +* something + +Documentation +############# + +* something + +Known issues +############ + +* something + +Bug fixes +######### + +* something + +Testing +####### + +* something + +Other changes +############# + +* something + +Contributors +############ + +* Caterina Köhl From 2c3f8f1baa5feb8e12e500364dcecf11071cf887 Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 19:37:46 +0200 Subject: [PATCH 36/45] Delete old whatsnew file --- docs/whatsnew/v0-3-3.rst | 51 ---------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 docs/whatsnew/v0-3-3.rst diff --git a/docs/whatsnew/v0-3-3.rst b/docs/whatsnew/v0-3-3.rst deleted file mode 100644 index 3ac8e2a10..000000000 --- a/docs/whatsnew/v0-3-3.rst +++ /dev/null @@ -1,51 +0,0 @@ -v0.3.3 (January 12, 2020) -++++++++++++++++++++++++++ - - -API changes -########### - -* something - -New features -############ - -* :class:`~oemof.solph.GenericStorage` can now have "fixed_losses", that are independent from storage content. -* It is now possible to determine a schedule for a flow. Flow will be pushed - to the schedule, if possible. - -New components -############## - -* something - -Documentation -############# - -* Improved documentation of ExtractionTurbineCHP - -Known issues -############ - -* something - -Bug fixes -######### - -* something - -Testing -####### - -* something - -Other changes -############# - -* something - -Contributors -############ - -* Uwe Krien -* Jann Launer From 0436ab148698c241126e8da7b77d5f690d229eca Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 19:45:27 +0200 Subject: [PATCH 37/45] Added missing line at end of file --- tests/lp_files/flow_schedule.lp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lp_files/flow_schedule.lp b/tests/lp_files/flow_schedule.lp index f748fa345..4aeae6528 100644 --- a/tests/lp_files/flow_schedule.lp +++ b/tests/lp_files/flow_schedule.lp @@ -76,4 +76,4 @@ bounds 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf 0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf -end \ No newline at end of file +end From 134ec9afe3c578baa843c6cb9acc94147d2e64aa Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 19:58:03 +0200 Subject: [PATCH 38/45] Added schedule attributes to Flow class --- src/oemof/solph/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/oemof/solph/network.py b/src/oemof/solph/network.py index 60fc45efc..3ad3c5bbc 100644 --- a/src/oemof/solph/network.py +++ b/src/oemof/solph/network.py @@ -164,7 +164,6 @@ class Flow(on.Edge): >>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_value=100) >>> f1.max[1] 0.99 - """ def __init__(self, **kwargs): @@ -178,7 +177,8 @@ def __init__(self, **kwargs): scalars = ['nominal_value', 'summed_max', 'summed_min', 'investment', 'nonconvex', 'integer'] - sequences = ['fix', 'variable_costs', 'min', 'max'] + sequences = ['fix', 'variable_costs', 'min', 'max', + 'schedule', 'schedule_cost_neg', 'schedule_cost_pos'] dictionaries = ['positive_gradient', 'negative_gradient'] defaults = {'variable_costs': 0, 'positive_gradient': {'ub': None, 'costs': 0}, From 4ba1c2caf44e8f1019948d6fc64ad50ca50f686f Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 19:59:47 +0200 Subject: [PATCH 39/45] Deleted files added by mistake --- test2_generic_chp.csv | 8 -------- test_schedule_flow.csv | 8 -------- 2 files changed, 16 deletions(-) delete mode 100644 test2_generic_chp.csv delete mode 100644 test_schedule_flow.csv diff --git a/test2_generic_chp.csv b/test2_generic_chp.csv deleted file mode 100644 index 424fee774..000000000 --- a/test2_generic_chp.csv +++ /dev/null @@ -1,8 +0,0 @@ -timestep,demand_th,price_el -1,0.88,-50 -2,0.74,100 -3,0.13,50 -4,0.99,50 -5,0.85,-50 -6,0.996,50 -7,0.47,50 \ No newline at end of file diff --git a/test_schedule_flow.csv b/test_schedule_flow.csv deleted file mode 100644 index 424fee774..000000000 --- a/test_schedule_flow.csv +++ /dev/null @@ -1,8 +0,0 @@ -timestep,demand_th,price_el -1,0.88,-50 -2,0.74,100 -3,0.13,50 -4,0.99,50 -5,0.85,-50 -6,0.996,50 -7,0.47,50 \ No newline at end of file From c948e6d3da94aaf35fad955322e6cd9a8e2beffe Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 20:00:08 +0200 Subject: [PATCH 40/45] Renamed penalty_costs to schedule_costs --- src/oemof/solph/blocks.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/oemof/solph/blocks.py b/src/oemof/solph/blocks.py index fe4a109ff..fdbe1fdd1 100644 --- a/src/oemof/solph/blocks.py +++ b/src/oemof/solph/blocks.py @@ -278,7 +278,7 @@ def _objective_expression(self): variable_costs = 0 gradient_costs = 0 - penalty_costs = 0 + schedule_costs = 0 for i, o in m.FLOWS: if m.flows[i, o].variable_costs[0] is not None: @@ -304,11 +304,11 @@ def _objective_expression(self): (len(schedule) == 0 and schedule[0] is not None)): for t in m.TIMESTEPS: - penalty_costs += (self.schedule_slack_pos[i, o, t] * - m.flows[i, o].schedule_cost_pos[t]) - penalty_costs += (self.schedule_slack_neg[i, o, t] * - m.flows[i, o].schedule_cost_neg[t]) - return variable_costs + gradient_costs + penalty_costs + schedule_costs += (self.schedule_slack_pos[i, o, t] * + m.flows[i, o].schedule_cost_pos[t]) + schedule_costs += (self.schedule_slack_neg[i, o, t] * + m.flows[i, o].schedule_cost_neg[t]) + return variable_costs + gradient_costs + schedule_costs class InvestmentFlow(SimpleBlock): From 346e6873be1bed6a724c825a6349aefd21fdc284 Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 20:00:29 +0200 Subject: [PATCH 41/45] Added ValueError tests for schedule attributes --- tests/test_solph_network_classes.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 5248a15ce..baf6eeb0c 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -97,3 +97,34 @@ def test_warning_fixed_still_used(): solph.Flow(fixed=True) assert len(w) != 0 assert msg == str(w[-1].message) + + +def test_missing_schedule_costs_values(): + """ + If schedule attribute is used and negative and positive schedule costs + are missing, an error will be raised. + """ + msg = ("The schedule attribute and the associated costs " + "need to be used in combination. \n Please set " + "the `schedule_cost_neg` and `schedule_cost_pos` " + "attributes of the flow.") + with pytest.raises(ValueError, match=msg): + solph.Flow(schedule=[54, 74, 90]) + + +def test_missing_schedule_neg_cost_value(): + msg = ("The schedule attribute and the associated costs " + "need to be used in combination. \n Please set " + "the `schedule_cost_neg` and `schedule_cost_pos` " + "attributes of the flow.") + with pytest.raises(ValueError, match=msg): + solph.Flow(schedule=[54, 74, 90], schedule_cost_pos=1000) + + +def test_missing_schedule_pos_cost_value(): + msg = ("The schedule attribute and the associated costs " + "need to be used in combination. \n Please set " + "the `schedule_cost_neg` and `schedule_cost_pos` " + "attributes of the flow.") + with pytest.raises(ValueError, match=msg): + solph.Flow(schedule=[54, 74, 90], schedule_cost_neg=1000) From b6cefa2c5b759c012393333ae968e8c57f12a13e Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 20:03:19 +0200 Subject: [PATCH 42/45] Fixed merge conflict --- docs/whatsnew/v0-4-1.rst | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/whatsnew/v0-4-1.rst b/docs/whatsnew/v0-4-1.rst index dc078cca3..0cfe6174c 100644 --- a/docs/whatsnew/v0-4-1.rst +++ b/docs/whatsnew/v0-4-1.rst @@ -2,48 +2,54 @@ v0.4.1 (???) ++++++++++++++++++++++++++ +v0.4.1 (???, 2020) +----------------------- + + API changes -########### +^^^^^^^^^^^^^^^^^^^^ * something + New features -############ +^^^^^^^^^^^^^^^^^^^^ * It is now possible to determine a schedule for a flow. Flow will be pushed to the schedule, if possible. -New components -############## +New components/constraints +^^^^^^^^^^^^^^^^^^^^^^^^^^ * something Documentation -############# +^^^^^^^^^^^^^^^^^^^^ * something -Known issues -############ +Bug fixes +^^^^^^^^^^^^^^^^^^^^ * something -Bug fixes -######### +Known issues +^^^^^^^^^^^^^^^^^^^^ * something + Testing -####### +^^^^^^^^^^^^^^^^^^^^ * something Other changes -############# +^^^^^^^^^^^^^^^^^^^^ * something Contributors -############ +^^^^^^^^^^^^^^^^^^^^ * Caterina Köhl From 5ec2d0dcb0bf70ec23cdae684b15584e4e56861f Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 10 Jun 2020 20:04:54 +0200 Subject: [PATCH 43/45] Fixed merge conflict --- docs/whatsnew/v0-4-1.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/whatsnew/v0-4-1.rst b/docs/whatsnew/v0-4-1.rst index 0cfe6174c..addc28dff 100644 --- a/docs/whatsnew/v0-4-1.rst +++ b/docs/whatsnew/v0-4-1.rst @@ -1,7 +1,3 @@ -v0.4.1 (???) -++++++++++++++++++++++++++ - - v0.4.1 (???, 2020) ----------------------- From f37c52310933761679dc3ae940e44a1790a831bd Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Mon, 31 Aug 2020 12:00:28 +0200 Subject: [PATCH 44/45] Added newfeatures to whatsnew file --- docs/whatsnew/v0-4-2.rst | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/whatsnew/v0-4-2.rst diff --git a/docs/whatsnew/v0-4-2.rst b/docs/whatsnew/v0-4-2.rst new file mode 100644 index 000000000..acd9119a2 --- /dev/null +++ b/docs/whatsnew/v0-4-2.rst @@ -0,0 +1,50 @@ +v0.4.2 (???, 2020) +----------------------- + + +API changes +^^^^^^^^^^^^^^^^^^^^ + +* something + + +New features +^^^^^^^^^^^^^^^^^^^^ + +* It is now possible to determine a schedule for a flow. Flow will be pushed + to the schedule, if possible. + +New components/constraints +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* something + +Documentation +^^^^^^^^^^^^^^^^^^^^ + +* something + +Bug fixes +^^^^^^^^^^^^^^^^^^^^ + +* something + +Known issues +^^^^^^^^^^^^^^^^^^^^ + +* something + +Testing +^^^^^^^^^^^^^^^^^^^^ + +* something + +Other changes +^^^^^^^^^^^^^^^^^^^^ + +* something + +Contributors +^^^^^^^^^^^^^^^^^^^^ + +* Caterina Köhl From 589d230c3f9c0fdb07fca2a9573f981ea32ea3ca Mon Sep 17 00:00:00 2001 From: "c.koehl" Date: Wed, 2 Sep 2020 14:36:30 +0200 Subject: [PATCH 45/45] Deleted todo from older version Deleted line with todo from older oemof version --- src/oemof/solph/blocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/oemof/solph/blocks.py b/src/oemof/solph/blocks.py index a5fa5628e..4ff11cbd7 100644 --- a/src/oemof/solph/blocks.py +++ b/src/oemof/solph/blocks.py @@ -568,7 +568,6 @@ def _investvar_bound_rule(block, i, o): # create status variable for a non-convex investment flow self.invest_status = Var(self.NON_CONVEX_INVESTFLOWS, within=Binary) # ######################### CONSTRAINTS ############################### - # TODO: Add gradient constraints def _min_invest_rule(block, i, o): """Rule definition for applying a minimum investment