From 6d1ce9a8bfc4f6f098d02ce6c4a2740ac5e686d3 Mon Sep 17 00:00:00 2001 From: Paul Slavin Date: Tue, 8 Feb 2022 15:49:22 +0000 Subject: [PATCH 1/5] WIP: New node classes; run-pv tests --- examples/pv/run-pv.py | 16 +++++++ pywr_dcopf/core.py | 103 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/examples/pv/run-pv.py b/examples/pv/run-pv.py index fe4b9d5..6a1c5e8 100644 --- a/examples/pv/run-pv.py +++ b/examples/pv/run-pv.py @@ -11,11 +11,27 @@ import logging logger = logging.getLogger(__name__) +import networkx as nx +from pywr._core import BaseInput, BaseOutput, BaseLink + +from pywr.solvers import solver_registry def main(filename): base, ext = os.path.splitext(filename) + print(filename) m = Model.load(filename, solver='glpk-dcopf') + print(m.solver) + + bus2 = m.nodes["bus2"] + print(f"{isinstance(bus2, BaseLink)=}") + + routes = nx.all_simple_paths(m.graph, m.nodes["pv2"], m.nodes["load2"]) + print([*routes]) + #routes = nx.all_simple_paths(m.graph, BaseInput, BaseOutput) + routes = m.find_all_routes(BaseInput, BaseOutput, valid=(BaseLink, BaseInput, BaseOutput)) + print([*routes]) + gen1 = NumpyArrayNodeRecorder(m, m.nodes['gen1']) pv2 = NumpyArrayNodeRecorder(m, m.nodes['pv2']) ProgressRecorder(m) diff --git a/pywr_dcopf/core.py b/pywr_dcopf/core.py index ca120f9..8f5cb5e 100644 --- a/pywr_dcopf/core.py +++ b/pywr_dcopf/core.py @@ -3,10 +3,38 @@ from six import with_metaclass from pywr.parameters import load_parameter, pop_kwarg_parameter, load_parameter_values from pywr._core import AbstractNode, Node as BaseNode, Storage as BaseStorage, StorageInput, StorageOutput, AbstractStorage -from pywr.nodes import Node, NodeMeta, Drawable, Connectable +from pywr.nodes import ( + NodeMeta, + Drawable, + Connectable, + Loadable +) + + +class Bus(BaseNode, Loadable, Drawable, Connectable, metaclass=NodeMeta): + + def __init__(self, model, name, *args, **kwargs): + max_flow = kwargs.pop("max_flow", None) + min_flow = kwargs.pop("min_flow", None) + cost = kwargs.pop("cost", None) + super().__init__(model, name, *args, **kwargs) + + cost = load_parameter(model, cost) + min_flow = load_parameter(model, min_flow) + max_flow = load_parameter(model, max_flow) + + if cost is None: + cost = 0.0 + if min_flow is None: + min_flow = 0.0 + if max_flow is None: + max_flow = 0.0 + + self.cost = cost + self.min_flow = min_flow + self.max_flow = max_flow -class Bus(with_metaclass(NodeMeta, Drawable, Connectable, AbstractNode)): @classmethod def load(cls, data, model): name = data.pop('name') @@ -33,9 +61,33 @@ def load(cls, data, model): return node -class Generator(with_metaclass(NodeMeta, Drawable, Connectable, BaseNode)): +class Generator(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): + + def __init__(self, model, name, *args, **kwargs): + max_flow = kwargs.pop("max_flow", None) + min_flow = kwargs.pop("min_flow", None) + cost = kwargs.pop("cost", None) + super().__init__(model, name, *args, **kwargs) + + cost = load_parameter(model, cost) + min_flow = load_parameter(model, min_flow) + max_flow = load_parameter(model, max_flow) + + if cost is None: + cost = 0.0 + if min_flow is None: + min_flow = 0.0 + if max_flow is None: + max_flow = 0.0 + + self.cost = cost + self.min_flow = min_flow + self.max_flow = max_flow + + @classmethod def load(cls, data, model): + print("*** Generator.load ***") name = data.pop('name') cost = data.pop('cost', 0.0) min_flow = data.pop('min_flow', None) @@ -60,7 +112,8 @@ def load(cls, data, model): return node -class PiecewiseGenerator(with_metaclass(NodeMeta, Drawable, Connectable, BaseNode)): +class PiecewiseGenerator(BaseNode, Loadable, Drawable, Connectable, metaclass=NodeMeta): + def __init__(self, model, name, **kwargs): self.allow_isolated = True costs = kwargs.pop('cost') @@ -69,7 +122,7 @@ def __init__(self, model, name, **kwargs): if len(costs) != len(max_flows): raise ValueError("Piecewise max_flow and cost keywords must be the same length.") - # Setup internall generators + # Setup internal generators self.subgenerators = [] for i, (max_flow, cost) in enumerate(zip(max_flows, costs)): generator = Generator(model, name=f'{name} Sub-generator[{i}]') @@ -103,7 +156,30 @@ def load(cls, data, model): return cls(model, name, cost=costs, max_flow=max_flows, **data) -class Load(with_metaclass(NodeMeta, Drawable, Connectable, BaseNode)): +class Load(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): + + def __init__(self, model, name, **kwargs): + min_flow = kwargs.pop('min_flow', None) + max_flow = kwargs.pop('max_flow', None) + cost = kwargs.pop('cost', None) + super().__init__( model, name, **kwargs) + + cost = load_parameter(model, cost) + min_flow = load_parameter(model, min_flow) + max_flow = load_parameter(model, max_flow) + + if cost is None: + cost = 0.0 + if min_flow is None: + min_flow = 0.0 + if max_flow is None: + max_flow = 0.0 + + self.cost = cost + self.min_flow = min_flow + self.max_flow = max_flow + + @classmethod def load(cls, data, model): name = data.pop('name') @@ -130,12 +206,17 @@ def load(cls, data, model): return node -class Line(with_metaclass(NodeMeta, Drawable, Connectable, BaseNode)): +class Line(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): - def __init__(self, *args, **kwargs): + def __init__(self, model, name, *args, **kwargs): self.reactance = kwargs.pop('reactance', 0.1) self.loss = kwargs.pop('loss', 0.0) - super().__init__(*args, **kwargs) + max_flow = kwargs.pop('max_flow', None) + super().__init__(model, name, *args, **kwargs) + + max_flow = load_parameter(model, max_flow) + if max_flow is not None: + self.max_flow = max_flow @classmethod @@ -152,8 +233,8 @@ def load(cls, data, model): return node -class Battery(with_metaclass(NodeMeta, Drawable, Connectable, BaseStorage)): - """A generic storage Node +class Battery(BaseStorage, Loadable, Connectable, Drawable, metaclass=NodeMeta): + """ A generic storage Node In terms of connections in the network the Storage node behaves like any other node, provided there is only 1 input and 1 output. If there are From cc77028c0899bea51cd1ea71053c1e693dbfbc75 Mon Sep 17 00:00:00 2001 From: Paul Slavin Date: Wed, 9 Feb 2022 12:09:46 +0000 Subject: [PATCH 2/5] Remove cls.load; new ctors for Batt and PWG; Tests pass --- pywr_dcopf/core.py | 248 ++++++++++++--------------------------------- 1 file changed, 66 insertions(+), 182 deletions(-) diff --git a/pywr_dcopf/core.py b/pywr_dcopf/core.py index 8f5cb5e..246897e 100644 --- a/pywr_dcopf/core.py +++ b/pywr_dcopf/core.py @@ -1,8 +1,14 @@ +""" + This module defines types used by the glpk-dcopf solver. +""" import numpy as np -from six import with_metaclass -from pywr.parameters import load_parameter, pop_kwarg_parameter, load_parameter_values -from pywr._core import AbstractNode, Node as BaseNode, Storage as BaseStorage, StorageInput, StorageOutput, AbstractStorage +from pywr.parameters import ( + load_parameter, + pop_kwarg_parameter, + load_parameter_values +) + from pywr.nodes import ( NodeMeta, Drawable, @@ -10,6 +16,23 @@ Loadable ) +from pywr._core import ( + Node as BaseNode, + Storage as BaseStorage, + StorageInput, + StorageOutput, + AbstractStorage +) + +__all__ = ( + "Battery", + "Bus", + "Generator", + "Line", + "Load", + "PiecewiseGenerator" +) + class Bus(BaseNode, Loadable, Drawable, Connectable, metaclass=NodeMeta): @@ -35,32 +58,6 @@ def __init__(self, model, name, *args, **kwargs): self.max_flow = max_flow - @classmethod - def load(cls, data, model): - name = data.pop('name') - cost = data.pop('cost', 0.0) - min_flow = data.pop('min_flow', None) - max_flow = data.pop('max_flow', None) - - data.pop('type') - node = cls(model=model, name=name, **data) - - cost = load_parameter(model, cost) - min_flow = load_parameter(model, min_flow) - max_flow = load_parameter(model, max_flow) - if cost is None: - cost = 0.0 - if min_flow is None: - min_flow = 0.0 - if max_flow is None: - max_flow = 0.0 - node.cost = cost - node.min_flow = min_flow - node.max_flow = max_flow - - return node - - class Generator(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): def __init__(self, model, name, *args, **kwargs): @@ -85,36 +82,9 @@ def __init__(self, model, name, *args, **kwargs): self.max_flow = max_flow - @classmethod - def load(cls, data, model): - print("*** Generator.load ***") - name = data.pop('name') - cost = data.pop('cost', 0.0) - min_flow = data.pop('min_flow', None) - max_flow = data.pop('max_flow', None) - - data.pop('type') - node = cls(model=model, name=name, **data) - - cost = load_parameter(model, cost) - min_flow = load_parameter(model, min_flow) - max_flow = load_parameter(model, max_flow) - if cost is None: - cost = 0.0 - if min_flow is None: - min_flow = 0.0 - if max_flow is None: - max_flow = 0.0 - node.cost = cost - node.min_flow = min_flow - node.max_flow = max_flow - - return node - - class PiecewiseGenerator(BaseNode, Loadable, Drawable, Connectable, metaclass=NodeMeta): - def __init__(self, model, name, **kwargs): + def __init__(self, model, name, *args, **kwargs): self.allow_isolated = True costs = kwargs.pop('cost') max_flows = kwargs.pop('max_flow') @@ -130,31 +100,19 @@ def __init__(self, model, name, **kwargs): generator.cost = cost self.subgenerators.append(generator) - super().__init__(model, name, **kwargs) + super().__init__(model, name, *args, **kwargs) def iter_slots(self, slot_name=None, is_connector=True): for generator in self.subgenerators: yield generator - def after(self, timestep): - """ - Set total flow on this link as sum of sublinks - """ + def after(self, timestep, adjustment=None): + """ Set total flow on this link as sum of sublinks """ for generator in self.subgenerators: self.commit_all(generator.flow) # Make sure save is done after setting aggregated flow super().after(timestep) - @classmethod - def load(cls, data, model): - name = data.pop('name') - costs = data.pop('cost') - max_flows = data.pop('max_flow') - data.pop('type') - costs = [load_parameter(model, c) for c in costs] - max_flows = [load_parameter(model, mf) for mf in max_flows] - return cls(model, name, cost=costs, max_flow=max_flows, **data) - class Load(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): @@ -162,7 +120,7 @@ def __init__(self, model, name, **kwargs): min_flow = kwargs.pop('min_flow', None) max_flow = kwargs.pop('max_flow', None) cost = kwargs.pop('cost', None) - super().__init__( model, name, **kwargs) + super().__init__(model, name, **kwargs) cost = load_parameter(model, cost) min_flow = load_parameter(model, min_flow) @@ -180,36 +138,12 @@ def __init__(self, model, name, **kwargs): self.max_flow = max_flow - @classmethod - def load(cls, data, model): - name = data.pop('name') - cost = data.pop('cost', 0.0) - min_flow = data.pop('min_flow', None) - max_flow = data.pop('max_flow', None) - - data.pop('type') - node = cls(model=model, name=name, **data) - - cost = load_parameter(model, cost) - min_flow = load_parameter(model, min_flow) - max_flow = load_parameter(model, max_flow) - if cost is None: - cost = 0.0 - if min_flow is None: - min_flow = 0.0 - if max_flow is None: - max_flow = 0.0 - node.cost = cost - node.min_flow = min_flow - node.max_flow = max_flow - - return node - - class Line(BaseNode, Loadable, Connectable, Drawable, metaclass=NodeMeta): + default_reactance = 0.1 + def __init__(self, model, name, *args, **kwargs): - self.reactance = kwargs.pop('reactance', 0.1) + self.reactance = kwargs.pop('reactance', Line.default_reactance) self.loss = kwargs.pop('loss', 0.0) max_flow = kwargs.pop('max_flow', None) super().__init__(model, name, *args, **kwargs) @@ -219,59 +153,54 @@ def __init__(self, model, name, *args, **kwargs): self.max_flow = max_flow - @classmethod - def load(cls, data, model): - name = data.pop('name') - data.pop('type') - max_flow = data.pop('max_flow', None) - node = cls(model=model, name=name, **data) - - max_flow = load_parameter(model, max_flow) - if max_flow is not None: - node.max_flow = max_flow - - return node - - class Battery(BaseStorage, Loadable, Connectable, Drawable, metaclass=NodeMeta): - """ A generic storage Node + """ Shares the behaviour of a generic Storage Node. - In terms of connections in the network the Storage node behaves like any - other node, provided there is only 1 input and 1 output. If there are - multiple sub-nodes the connections need to be explicit about which they - are connecting to. For example: + In terms of connections in the network the Storage node behaves like any + other node, provided there is only 1 input and 1 output. If there are + multiple sub-nodes the connections need to be explicit about which they + are connecting to. For example: - >>> storage(model, 'reservoir', num_outputs=1, num_inputs=2) - >>> supply.connect(storage) - >>> storage.connect(demand1, from_slot=0) - >>> storage.connect(demand2, from_slot=1) + >>> storage(model, 'reservoir', num_outputs=1, num_inputs=2) + >>> supply.connect(storage) + >>> storage.connect(demand1, from_slot=0) + >>> storage.connect(demand2, from_slot=1) - The attribtues of the sub-nodes can be modified directly (and - independently). For example: + The attributes of the sub-nodes can be modified directly (and + independently). For example: - >>> storage.outputs[0].max_flow = 15.0 + >>> storage.outputs[0].max_flow = 15.0 - If a recorder is set on the storage node, instead of recording flow it - records changes in storage. Any recorders set on the output or input - sub-nodes record flow as normal. + If a recorder is set on the storage node, instead of recording flow it + records changes in storage. Any recorders set on the output or input + sub-nodes record flow as normal. """ def __init__(self, model, name, num_outputs=1, num_inputs=1, *args, **kwargs): - # cast number of inputs/outputs to integer - # this is needed if values come in as strings sometimes - num_outputs = int(num_outputs) - num_inputs = int(num_inputs) + # Ensure num_inputs/num_outputs are ints + try: + num_outputs = int(num_outputs) + num_inputs = int(num_inputs) + except (TypeError, ValueError) as err: + raise err.__class__(f"Invalid argument for num_inputs/num_outputs: {str(err)}") + + if "initial_volume" not in kwargs and "initial_volume_pc" not in kwargs: + raise ValueError("Initial volume must be specified in absolute or relative terms.") min_volume = pop_kwarg_parameter(kwargs, 'min_volume', 0.0) - if min_volume is None: - min_volume = 0.0 max_volume = pop_kwarg_parameter(kwargs, 'max_volume', 0.0) + initial_volume = kwargs.pop('initial_volume', 0.0) + try: + initial_volume = float(initial_volume) + except (TypeError, ValueError): + initial_volume = load_parameter_values(model, initial_volume) + initial_volume_pc = kwargs.pop('initial_volume_pc', None) cost = pop_kwarg_parameter(kwargs, 'cost', 0.0) position = kwargs.pop("position", {}) - super().__init__(model, name, **kwargs) + super().__init__(model, name, *args, **kwargs) # TODO this doesn't need multiple inputs and outputs self.outputs = [] @@ -298,7 +227,7 @@ def __init__(self, model, name, num_outputs=1, num_inputs=1, *args, **kwargs): for node in self.inputs: self.model.graph.add_node(node) - def after(self, ts): + def after(self, ts, adjustment=None): AbstractStorage.after(self, ts) for i, si in enumerate(self.model.scenarios.combinations): @@ -317,48 +246,3 @@ def after(self, ts): self._current_pc[i] = self._volume[i] / mxv except ZeroDivisionError: self._current_pc[i] = np.nan - - @classmethod - def load(cls, data, model): - name = data.pop('name') - num_inputs = int(data.pop('inputs', 1)) - num_outputs = int(data.pop('outputs', 1)) - - if 'initial_volume' not in data and 'initial_volume_pc' not in data: - raise ValueError('Initial volume must be specified in absolute or relative terms.') - - initial_volume = data.pop('initial_volume', 0.0) - initial_volume_pc = data.pop('initial_volume_pc', None) - max_volume = data.pop('max_volume') - min_volume = data.pop('min_volume', 0.0) - cost = data.pop('cost', 0.0) - - data.pop('type', None) - # Create the instance - node = cls(model=model, name=name, num_inputs=num_inputs, num_outputs=num_outputs, **data) - - # Load the parameters after the instance has been created to prevent circular - # loading errors - - # Try to coerce initial volume to float. - try: - initial_volume = float(initial_volume) - except TypeError: - initial_volume = load_parameter_values(model, initial_volume) - node.initial_volume = initial_volume - node.initial_volume_pc = initial_volume_pc - - max_volume = load_parameter(model, max_volume) - if max_volume is not None: - node.max_volume = max_volume - - min_volume = load_parameter(model, min_volume) - if min_volume is not None: - node.min_volume = min_volume - - cost = load_parameter(model, cost) - if cost is None: - cost = 0.0 - node.cost = cost - - return node From 1c35e2dfef8ac8c87a40b3f69cb051e946be0f0f Mon Sep 17 00:00:00 2001 From: Paul Slavin Date: Wed, 9 Feb 2022 15:13:34 +0000 Subject: [PATCH 3/5] Add test for Battery.initial_volume_pc; Restore example run script --- examples/pv/run-pv.py | 16 ------ tests/models/simple-pv-battery.json | 86 +++++++++++++++++++++++++++++ tests/test_core.py | 23 ++++++++ 3 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 tests/models/simple-pv-battery.json diff --git a/examples/pv/run-pv.py b/examples/pv/run-pv.py index 6a1c5e8..fe4b9d5 100644 --- a/examples/pv/run-pv.py +++ b/examples/pv/run-pv.py @@ -11,27 +11,11 @@ import logging logger = logging.getLogger(__name__) -import networkx as nx -from pywr._core import BaseInput, BaseOutput, BaseLink - -from pywr.solvers import solver_registry def main(filename): base, ext = os.path.splitext(filename) - print(filename) m = Model.load(filename, solver='glpk-dcopf') - print(m.solver) - - bus2 = m.nodes["bus2"] - print(f"{isinstance(bus2, BaseLink)=}") - - routes = nx.all_simple_paths(m.graph, m.nodes["pv2"], m.nodes["load2"]) - print([*routes]) - #routes = nx.all_simple_paths(m.graph, BaseInput, BaseOutput) - routes = m.find_all_routes(BaseInput, BaseOutput, valid=(BaseLink, BaseInput, BaseOutput)) - print([*routes]) - gen1 = NumpyArrayNodeRecorder(m, m.nodes['gen1']) pv2 = NumpyArrayNodeRecorder(m, m.nodes['pv2']) ProgressRecorder(m) diff --git a/tests/models/simple-pv-battery.json b/tests/models/simple-pv-battery.json new file mode 100644 index 0000000..1224f2b --- /dev/null +++ b/tests/models/simple-pv-battery.json @@ -0,0 +1,86 @@ +{ + "metadata": { + "title": "Simple network with solar PV input.", + "description": "Time varying radiation input.", + "minimum_version": "1.1" + }, + "timestepper": { + "start": "2001-01-01", + "end": "2001-02-01", + "timestep": "H" + }, + "nodes": [ + { + "name": "gen1", + "type": "generator", + "max_flow": 200, + "cost": 20 + }, + { + "name": "pv2", + "type": "generator", + "max_flow": "solar_power" + }, + { + "name": "load2", + "type": "load", + "max_flow": 200, + "cost": -100 + }, + { + "name": "bus1", + "type": "bus" + }, + { + "name": "bus2", + "type": "bus" + }, + { + "name": "line12", + "type": "line", + "reactance": 0.1 + }, + { + "name": "AAA", + "type": "battery", + "initial_volume_pc": 60, + "max_volume": 150, + "cost": -15.00 + } + ], + "edges": [ + ["gen1", "bus1"], + ["bus1", "line12"], + ["line12", "bus2"], + ["pv2", "bus2"], + ["load2", "bus2"], + ["AAA", "bus2"] + ], + "parameters": { + "solar_power": { + "type": "aggregated", + "agg_func": "product", + "parameters": [ + "daily_direct_radiation", + "solar_pv_efficiency", + "solar_pv_area" + ] + }, + "solar_pv_efficiency": { + "type": "constant", + "value": 1.0 + }, + "solar_pv_area": { + "type": "constant", + "value": 100.0e-3, + "comment": "Assume 100,000 m2 (10 ha), but also convert from W to MW" + }, + "daily_direct_radiation": { + "type": "dataframe", + "url": "data/r_0001_cntr_hly.h5", + "key": "r_0001_cntr_hly", + "column": "dirradt_htotal", + "comment": "Units: Wh/m2;" + } + } +} diff --git a/tests/test_core.py b/tests/test_core.py index 76daa56..8ba0273 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -217,3 +217,26 @@ def test_simple_battery(): assert df.shape[0] == 745 # TODO add better assertions + + +def test_battery_volume_percentage(): + """ + Tests initial_volume_pc attr of Battery + """ + + m = Model.load(os.path.join(TEST_FOLDER, 'models', 'simple-pv-battery.json'), solver='glpk-dcopf') + + gen1 = NumpyArrayNodeRecorder(m, m.nodes['gen1']) + battery1 = NumpyArrayStorageRecorder(m, m.nodes['AAA']) + + m.setup() + m.run() + + df = pandas.concat({ + 'gen1': gen1.to_dataframe(), + 'AAA': battery1.to_dataframe() + }, axis=1) + + assert df.shape[0] == 745 + assert np.isclose(df.loc["2001-01-02"].iloc[-3]["gen1"][0], 114.58) + assert np.isclose(df.loc["2001-01-02"].iloc[-4]["AAA"][0], 85.42) From 53f2903f1b8c0f1a47b6ae67a2f56473a390b4d5 Mon Sep 17 00:00:00 2001 From: Paul Slavin Date: Fri, 11 Feb 2022 10:55:50 +0000 Subject: [PATCH 4/5] Use explicit period to make df index in test less cryptic --- tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 8ba0273..4aa2308 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -238,5 +238,5 @@ def test_battery_volume_percentage(): }, axis=1) assert df.shape[0] == 745 - assert np.isclose(df.loc["2001-01-02"].iloc[-3]["gen1"][0], 114.58) - assert np.isclose(df.loc["2001-01-02"].iloc[-4]["AAA"][0], 85.42) + assert np.isclose(df.loc[pandas.Period('2001-01-02 20:00', 'H')].loc["AAA", 0], 85.42) + assert np.isclose(df.loc[pandas.Period('2001-01-02 21:00', 'H')].loc["gen1", 0], 114.58) From a0549073fe8aa8d1417365679916dda2372136db Mon Sep 17 00:00:00 2001 From: Paul Slavin Date: Wed, 23 Feb 2022 21:20:07 +0000 Subject: [PATCH 5/5] Remove position from ctor kwargs before super call --- pywr_dcopf/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pywr_dcopf/core.py b/pywr_dcopf/core.py index 246897e..1219079 100644 --- a/pywr_dcopf/core.py +++ b/pywr_dcopf/core.py @@ -40,6 +40,7 @@ def __init__(self, model, name, *args, **kwargs): max_flow = kwargs.pop("max_flow", None) min_flow = kwargs.pop("min_flow", None) cost = kwargs.pop("cost", None) + kwargs.pop("position", None) super().__init__(model, name, *args, **kwargs) cost = load_parameter(model, cost) @@ -64,6 +65,7 @@ def __init__(self, model, name, *args, **kwargs): max_flow = kwargs.pop("max_flow", None) min_flow = kwargs.pop("min_flow", None) cost = kwargs.pop("cost", None) + kwargs.pop("position", None) super().__init__(model, name, *args, **kwargs) cost = load_parameter(model, cost) @@ -88,6 +90,7 @@ def __init__(self, model, name, *args, **kwargs): self.allow_isolated = True costs = kwargs.pop('cost') max_flows = kwargs.pop('max_flow') + kwargs.pop("position", None) if len(costs) != len(max_flows): raise ValueError("Piecewise max_flow and cost keywords must be the same length.") @@ -120,6 +123,7 @@ def __init__(self, model, name, **kwargs): min_flow = kwargs.pop('min_flow', None) max_flow = kwargs.pop('max_flow', None) cost = kwargs.pop('cost', None) + kwargs.pop("position", None) super().__init__(model, name, **kwargs) cost = load_parameter(model, cost) @@ -146,6 +150,7 @@ def __init__(self, model, name, *args, **kwargs): self.reactance = kwargs.pop('reactance', Line.default_reactance) self.loss = kwargs.pop('loss', 0.0) max_flow = kwargs.pop('max_flow', None) + kwargs.pop("position", None) super().__init__(model, name, *args, **kwargs) max_flow = load_parameter(model, max_flow)