From dd2b51195fc1afe9a072150d3f47d07136482dd0 Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Thu, 3 Sep 2026 11:17:53 +0800 Subject: [PATCH 1/4] Add unit tests for base port class --- src/echo/models/base/port.py | 28 ++- tests/unit/models/base/test_port.py | 362 ++++++++++++++++++++++++++-- 2 files changed, 363 insertions(+), 27 deletions(-) diff --git a/src/echo/models/base/port.py b/src/echo/models/base/port.py index 6082e79..3be0745 100644 --- a/src/echo/models/base/port.py +++ b/src/echo/models/base/port.py @@ -15,7 +15,7 @@ from echo.constants import negative_variable_component, positive_variable_component from echo.exceptions import ConfigurationError from echo.models.base import BaseModel -from echo.models.base.types import ConstraintValueType, InitialValueInput +from echo.models.base.types import ConstraintValueType, InitialValue, InitialValueInput from echo.models.scenario import EchoConcreteModel from echo.utils import ( TimeSeriesData, @@ -121,7 +121,7 @@ def set_flow_constraints( def process_initial_value( self, - initial_val: InitialValueInput, + initial_val: InitialValueInput | str, expansion_periods: int = 1, time_periods: int | None = None, ) -> None: @@ -348,8 +348,8 @@ def _determine_initial_value( self, time_periods: int, expansion_periods: int, - profile: pd.DataFrame, - ) -> dict[tuple[int, int], float]: + profile: pd.DataFrame | None, + ) -> InitialValue: initial_value_scaling = self.initial_value_scaling or 1 @@ -367,7 +367,7 @@ def _determine_initial_value( return initial_val - def add_port_to_model(self, model: EchoConcreteModel, profile: pd.DataFrame) -> None: + def add_port_to_model(self, model: EchoConcreteModel, profile: pd.DataFrame | None) -> None: """Creates pyomo vars, params, and constraints for the port.""" initial_value = self._determine_initial_value( time_periods=len(model.Time), @@ -388,9 +388,15 @@ def add_port_to_model(self, model: EchoConcreteModel, profile: pd.DataFrame) -> if self.import_constraint is FlowConstraint.Fixed: # only apply import/export constraints to variables self._add_import_constraints_to_model(model=model) + if self.import_constraint in [FlowConstraint.Series, FlowConstraint.InRange]: + raise NotImplementedError("Series and InRange import flow constraints are not implemented") + if self.export_constraint is FlowConstraint.Fixed: # only apply these constraints to variables self._add_export_constraints_to_model(model=model) + if self.export_constraint in [FlowConstraint.Series, FlowConstraint.InRange]: + raise NotImplementedError("Series and InRange export flow constraints are not implemented") + if self.active_periods is not None: self._add_active_period_constraints_to_model(model=model) @@ -499,7 +505,7 @@ def set_initial_value_from_array( self.set_initial_value_from_timeseriesdata(time_series_data=time_series_data) def set_active_periods_from_array( - self, array: list[bool], expansion_periods: int = 1, time_periods: int | None = None + self, array: list[bool] | list[int], expansion_periods: int = 1, time_periods: int | None = None ) -> None: """Sets port active periods @@ -511,8 +517,16 @@ def set_active_periods_from_array( if time_periods is None: time_periods = len(array) + # We need an array which only contains 0 or 1 representing inactive (flow fixed to 0) or active (flow can be optimised) + # Convert bools to ints + active_periods_as_ints = [int(i) for i in array] + set_of_active_periods = set(active_periods_as_ints) + + if set_of_active_periods not in [{0}, {1}, {0, 1}]: + raise ValueError("Active periods must be a list of booleans or a list only containing 0's or 1's") + time_series_data = TimeSeriesData( - value=array, + value=active_periods_as_ints, num_time_intervals=time_periods, num_expansion_intervals=expansion_periods, ) diff --git a/tests/unit/models/base/test_port.py b/tests/unit/models/base/test_port.py index ef1f903..ec7d844 100644 --- a/tests/unit/models/base/test_port.py +++ b/tests/unit/models/base/test_port.py @@ -1,34 +1,356 @@ +from dataclasses import dataclass + import numpy as np +import pandas as pd +import pyomo as pyo +import pytest +from pydantic import ValidationError +from echo.configuration import FlowConstraint, Flows, OptimisationType, Units +from echo.exceptions import ConfigurationError from echo.models.base.port import Port +from echo.models.base.types import InitialValue, InitialValueInput +from echo.utils import TimeSeriesData + + +@pytest.mark.parametrize( + "port_params", + [ + { + "flows": Flows.Import, + "import_constraint": FlowConstraint.NoConstraint, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + }, + { + "flows": Flows.Export, + "export_constraint": FlowConstraint.NoConstraint, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + }, + { + "flows": Flows.Both, + "import_constraint": FlowConstraint.NoConstraint, + "export_constraint": FlowConstraint.NoConstraint, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + }, + ], +) +def test_port_verify(port_params): + port = Port(**port_params) + port.verify_port() + +@pytest.mark.parametrize( + "port_params", + [ + {}, # Missing flows, flow_type, input/export constraints, units + { + # Missing flows + "import_constraint": FlowConstraint.NoConstraint, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + }, + { + "flows": Flows.Import, + # Missing import constraint + "flow_type": OptimisationType.Variable, + "units": Units.KW, + }, + { + "flows": Flows.Import, + "import_constraint": FlowConstraint.NoConstraint, + # Missing flow_type + "units": Units.KW, + }, + { + "flows": Flows.Import, + "import_constraint": FlowConstraint.NoConstraint, + "flow_type": OptimisationType.Variable, + # Missing units + }, + { + "flows": Flows.Import, + "import_constraint": FlowConstraint.Fixed, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + # Missing input_constraint_value (since flow constraint is fixed) + }, + { + "flows": Flows.Both, + "import_constraint": FlowConstraint.Fixed, + "import_constraint_value": 1.0, + "export_constraint": FlowConstraint.Fixed, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + # Missing output_constraint_value (since flow constraint is fixed and flows is both) + }, + ], +) +def test_port_raises_configuration_error(port_params: dict): + port = Port(**port_params) + with pytest.raises(ConfigurationError): + port.verify_port() -def test_port_proccess_initial_value_types(): + +@pytest.mark.parametrize( + "raw_initial_val, expected_initial_val", + [ + ([1, 2, 3], {(0, 0): 1, (0, 1): 2, (0, 2): 3}), # ints + ([1.0, 2.0, 3.0], {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0}), # floats + ({(0, 0): 1, (0, 1): 2, (0, 2): 3}, {(0, 0): 1, (0, 1): 2, (0, 2): 3}), # dict of ints + ({(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0}, {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0}), # dict of floats + (np.array([1, 2, 3]), {(0, 0): 1, (0, 1): 2, (0, 2): 3}), # np array of ints + (np.array([1.0, 2.0, 3.0]), {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0}), # np array of floats + ], +) +def test_port_proccess_initial_value(raw_initial_val: list, expected_initial_val: dict): port = Port(port_name="port_name") - # Assert the initial value is None before it's set assert port.initial_value is None + port.process_initial_value(initial_val=raw_initial_val) + assert port.initial_value == expected_initial_val + + +@pytest.mark.parametrize( + "import_constraint_value,export_constraint_value", + [ + ("NOT VALID", 1.0), # value must be float or array-like + (1.0, "NOT VALID"), # value must be float or array-like + (-1.0, -1.0), # import value cannot be negative + (1.0, 1.0), # Export value can't be + ], +) +def test_validation(import_constraint_value, export_constraint_value): + with pytest.raises(ValidationError): + Port( + flows=Flows.Both, + import_constraint=FlowConstraint.Fixed, + import_constraint_value=import_constraint_value, + export_constraint=FlowConstraint.Fixed, + export_constraint_value=export_constraint_value, + flow_type=OptimisationType.Variable, + units=Units.KW, + ) + + +@dataclass +class Method: + name: str + initial_value: InitialValueInput | TimeSeriesData | str + number_of_intervals: int = 3 + profile: pd.DataFrame | None = None + + +@pytest.mark.parametrize( + "method, expected_values", + [ + (Method(name="from_dict", initial_value={(0, 0): 1, (0, 1): 2, (0, 2): 3}), {(0, 0): 1, (0, 1): 2, (0, 2): 3}), + ( + Method(name="from_dict_via_process_function", initial_value={(0, 0): 1, (0, 1): 2, (0, 2): 3}), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method( + name="from_timeseriesdata", + initial_value=TimeSeriesData(value=[1.0, 2.0, 3.0], num_time_intervals=3, num_expansion_intervals=1), + ), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method(name="from_list", initial_value=[1, 2, 3]), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method(name="from_np_array", initial_value=np.array([1.0, 2.0, 3.0])), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method(name="from_list_via_process_function", initial_value=[1, 2, 3]), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method(name="from_np_array_via_process_function", initial_value=np.array([1.0, 2.0, 3.0])), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ( + Method( + name="with_ref", + initial_value="initial_values_col", + profile=pd.DataFrame(pd.DataFrame({"initial_values_col": [1, 2, 3]})), + ), + {(0, 0): 1, (0, 1): 2, (0, 2): 3}, + ), + ], +) +def test_setting_initial_values(empty_model, method: Method, expected_values: InitialValue): + port_params = { + "flows": Flows.Both, + "import_constraint": FlowConstraint.Fixed, + "import_constraint_value": 1.0, + "export_constraint_value": -1.0, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + } + + port = Port(**port_params, port_name="port") + + match method.name: + case "from_dict": + assert isinstance(method.initial_value, dict) + port.set_initial_value(initial_value=method.initial_value) + case "from_dict_via_process_function": + assert isinstance(method.initial_value, dict) + port.process_initial_value(initial_val=method.initial_value) + case "from_timeseriesdata": + assert isinstance(method.initial_value, TimeSeriesData) + port.set_initial_value_from_timeseriesdata(time_series_data=method.initial_value) + case "from_list" | "from_np_array": + assert isinstance(method.initial_value, list) or isinstance(method.initial_value, np.ndarray) + port.set_initial_value_from_array(array=method.initial_value, time_periods=method.number_of_intervals) + case "from_list_via_process_function" | "from_np_array_via_process_function": + assert isinstance(method.initial_value, list) or isinstance(method.initial_value, np.ndarray) + port.process_initial_value(initial_val=method.initial_value, time_periods=method.number_of_intervals) + case "with_ref": + assert isinstance(method.initial_value, str) + initial_val_ref = method.initial_value + port.process_initial_value(initial_val=initial_val_ref) + + model = empty_model(number_of_intervals=method.number_of_intervals) + port.add_port_to_model(model, profile=method.profile) + assert hasattr(model, port.port_name) + assert getattr(model, port.port_name).get_values() == expected_values + + +@pytest.mark.parametrize("active_periods", [[False, True, True], [0, 1, 1]]) +def test_setting_active_periods(empty_model, active_periods): + port_params = { + "flows": Flows.Both, + "import_constraint": FlowConstraint.Fixed, + "import_constraint_value": 1.0, + "export_constraint_value": -1.0, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + } + + port = Port(**port_params, port_name="port") + + number_of_intervals = len(active_periods) + port.set_active_periods_from_array(array=active_periods, time_periods=number_of_intervals) + + model = empty_model(number_of_intervals=number_of_intervals) + port.add_port_to_model(model, profile=None) + assert hasattr(model, f"active_con1_{port.port_name}") + assert hasattr(model, f"active_con2_{port.port_name}") + # It's difficult to test the active periods are set correctly on the model because they are closed over by + # the constraint rules `on_off_rule1` and `on_off_rule2` + + +@pytest.mark.parametrize( + "import_constraint_value, export_constraint_value, expected_upper_bounds, expected_lower_bounds", + [ + (1.0, -1.0, [1.0] * 6, [-1.0] * 6), # same constrain value across all time periods + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], # varying constaint value (across time periods) + [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0], + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0], + ), + ], +) +def test_setting_flow_bounds( + empty_model, import_constraint_value, export_constraint_value, expected_upper_bounds, expected_lower_bounds +): + number_of_intervals = len(expected_upper_bounds) + assert number_of_intervals == len(expected_lower_bounds) + port_params = { + "flows": Flows.Both, + "import_constraint": FlowConstraint.Fixed, + "import_constraint_value": import_constraint_value, + "export_constraint": FlowConstraint.Fixed, + "export_constraint_value": export_constraint_value, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + "slack": False, + } + + port = Port(**port_params, port_name="port") + + model = empty_model(number_of_intervals=number_of_intervals) + port.add_port_to_model(model, profile=None) + + assert hasattr(model, port.port_name) + flow = getattr(model, port.port_name) + lower_bounds = [v.lower for v in flow.values()] + upper_bounds = [v.upper for v in flow.values()] + assert upper_bounds == expected_upper_bounds + assert lower_bounds == expected_lower_bounds + + # Port flow bounds aren't compatible with slack + # Verify no slack variables present on the model + assert not hasattr(model, port.import_slack) + assert not hasattr(model, port.import_slack_max) + assert not hasattr(model, port.export_slack) + assert not hasattr(model, port.export_slack_max) + + # Verify objective lacks contributions (which only happen when slack is enabled) + port.add_objective(model) + assert port.objective == 0 + + +@pytest.mark.parametrize( + "import_constraint_value, export_constraint_value, expected_upper_bounds, expected_lower_bounds", + [ + (1.0, -1.0, [1.0] * 6, [-1.0] * 6), # same constrain value across all time periods + ( + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], # varying constaint value (across time periods) + [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0], + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0], + ), + ], +) +def test_enabling_slack( + empty_model, import_constraint_value, export_constraint_value, expected_upper_bounds, expected_lower_bounds +): + number_of_intervals = len(expected_upper_bounds) + assert number_of_intervals == len(expected_lower_bounds) + port_params = { + "flows": Flows.Both, + "import_constraint": FlowConstraint.Fixed, + "import_constraint_value": import_constraint_value, + "export_constraint": FlowConstraint.Fixed, + "export_constraint_value": export_constraint_value, + "flow_type": OptimisationType.Variable, + "units": Units.KW, + "slack": True, + } + + port = Port(**port_params, port_name="port") - # Assert list of ints is processed - port.process_initial_value(initial_val=[1, 2, 3]) - assert port.initial_value == {(0, 0): 1, (0, 1): 2, (0, 2): 3} + model = empty_model(number_of_intervals=number_of_intervals) + port.add_port_to_model(model, profile=None) - # Assert list of floats is processed - port.process_initial_value(initial_val=[1.0, 2.0, 3.0]) - assert port.initial_value == {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0} + assert hasattr(model, port.port_name) + assert hasattr(model, port.import_slack) + assert hasattr(model, port.import_slack_max) + assert hasattr(model, port.export_slack) + assert hasattr(model, port.export_slack_max) - # Assert a dict of ints is processed - port.process_initial_value(initial_val={(0, 0): 1, (0, 1): 2, (0, 2): 3}) - assert port.initial_value == {(0, 0): 1, (0, 1): 2, (0, 2): 3} + assert port.objective == 0 + port.add_objective(model) + assert isinstance(port.objective, pyo.core.expr.numeric_expr.SumExpression) + assert port.objective.nargs() == 4 # The objective expressions should have 4 terms - # Assert a dict of ints is processed - port.process_initial_value(initial_val={(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0}) - assert port.initial_value == {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0} + # Slack is not compatible with port flow bounds + # Verify all bounds on flow variable are None + flow = getattr(model, port.port_name) + lower_bounds = [v.lower for v in flow.values()] + upper_bounds = [v.upper for v in flow.values()] + assert upper_bounds == [None] * number_of_intervals + assert lower_bounds == [None] * number_of_intervals - # Assert a numpy array of ints is processed - port.process_initial_value(initial_val=np.array([1, 2, 3])) - assert port.initial_value == {(0, 0): 1, (0, 1): 2, (0, 2): 3} - # Assert a numpy array of floats is processed - port.process_initial_value(initial_val=np.array([1.0, 2.0, 3.0])) - assert port.initial_value == {(0, 0): 1.0, (0, 1): 2.0, (0, 2): 3.0} +def test_splitting_flow_variable(empty_model): + pass From b2719562544ed1e6f90b9f8e5994be8e3421a419 Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Fri, 4 Sep 2026 14:40:08 +0800 Subject: [PATCH 2/4] Add unit tests for Node --- src/echo/models/base/node.py | 40 ++++++++---------- tests/unit/models/base/test_node.py | 64 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 tests/unit/models/base/test_node.py diff --git a/src/echo/models/base/node.py b/src/echo/models/base/node.py index 0c74719..85333be 100644 --- a/src/echo/models/base/node.py +++ b/src/echo/models/base/node.py @@ -46,22 +46,25 @@ def add_ports_from_list(self, names: Iterable[str], port_type: type[Port], **kwa for name in names: self.add_port(name, port_type(**kwargs)) - def get_port(self, port_name: str) -> Port: + def get_port(self, port_name: str) -> Port | None: """Returns the Port object with the name port_name. Args: port_name: The name of the Port. Returns: - Port: The port object with the name port_name + Port: The port object with the name port_name, or None if the port isn't found """ - port = self.ports.get(port_name) + return self.ports.get(port_name) - if port is not None: - return port - else: - raise ValueError(f"Port name: {port_name} does not correspond to any Port object.") + def num_ports(self) -> int: + """Returns the number of ports associated with this node. + + Returns: + The number of ports for this node. + """ + return len(self.ports) def verify_node(self) -> None: """Checks there is at least one port associated with this node. @@ -70,9 +73,12 @@ def verify_node(self) -> None: ConfigurationError: If there are no ports present on this node. """ - if bool(self.ports) is False: + if len(self.ports) < 1: raise ConfigurationError("A node must have at least one port.") + for port in self.ports.values(): + port.verify_port() + def add_node_to_model(self, model: EchoConcreteModel, profile: pd.DataFrame) -> None: """Add this node to a concrete model. @@ -81,8 +87,8 @@ def add_node_to_model(self, model: EchoConcreteModel, profile: pd.DataFrame) -> profile: The data associated with this node. """ + self.verify_node() for port in self.ports.values(): - port.verify_port() port.add_port_to_model(model, profile) # @abc.abstractmethod @@ -112,24 +118,13 @@ def add_objective(self, model: EchoConcreteModel) -> None: Returns: None """ - total = 0 - - self.objective += total - - def num_ports(self) -> int: - """Returns the number of ports associated with this node. - - Returns: - The number of ports for this node. - """ - - return len(self.ports) + pass # @abc.abstractmethod def apply_node_constraints(self, model: EchoConcreteModel) -> None: """Apply constraints associated with this node to a concrete model. - Can be overwritten. + Intended to be overridden in subclasses. Define constraints as functions that returns a Constraint, EqualityExpression or InequalityExpression. @@ -152,7 +147,6 @@ def tellegen_node_rule(model: EchoConcreteModel, p: int, t:int) -> EqualityExpre Returns: None """ - pass def get_port_name_to_port_dict_name_map(self) -> dict[str, str]: diff --git a/tests/unit/models/base/test_node.py b/tests/unit/models/base/test_node.py new file mode 100644 index 0000000..0ea5fe6 --- /dev/null +++ b/tests/unit/models/base/test_node.py @@ -0,0 +1,64 @@ +from unittest.mock import MagicMock + +import pytest +import shortuuid + +from echo.exceptions import ConfigurationError +from echo.models.base.node import Node +from echo.models.base.port import Port + + +def test_node_name(): + name = "my-node-name" + node = Node(node_name=name) + assert node.node_name == name + + node = Node() + basename = "node_" + assert node.node_name.startswith(basename) + assert len(node.node_name) == len(shortuuid.uuid()) + len(basename) + + +def test_add_port(): + num_ports = 9 + ports = [Port(port_name=f"port_{i}") for i in range(num_ports)] + + node = Node() + for p in ports: + node.add_port(p.port_name, p) + + assert len(node.ports) == len(ports) + assert node.num_ports() == len(ports) + for p in ports: + assert node.get_port(p.port_name) == p + + +def test_add_ports_from_list(): + num_ports = 9 + port_names = [f"port_{i}" for i in range(num_ports)] + + node = Node() + node.add_ports_from_list(names=port_names, port_type=Port) + + assert len(node.ports) == len(port_names) + assert node.num_ports() == len(port_names) + + +def test_verify_ports_raises_configurationerror(): + node = Node() + with pytest.raises(ConfigurationError): + node.verify_node() + + +def test_add_node_to_model(empty_model): + node = Node(node_name="node_1") + port_instance = MagicMock() + port_instance.verify_port.return_value = None + port_instance.add_port_to_model.return_value = None + + node.add_port("port_1", port_instance) + model = empty_model() + node.add_node_to_model(model, profile=None) + + port_instance.verify_port.assert_called_once() + port_instance.add_port_to_model.assert_called_once_with(model, None) From c7aa9ecbee536ff7cbb9bf2097d4c783706e50ce Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Fri, 4 Sep 2026 14:40:19 +0800 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 379c9ca..f5110c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,11 @@ - Prevent warnings for unconnected (dangling) ports on EVs. - NewSolar node renamed ScaledSolar. -- Decompose the large model modules into smaller more manageable modules. +- Decomposes the large model modules into smaller more manageable modules. +- Trying to add a port to a model with import/export contraints set to `FlowConstraint.Series` or `FlowConstraint.InRange` now raises a `NotImplementedError`. +- When setting active periods on a Port, the `set_active_periods_from_array` method now accepts a list of booleans in addition to a list of 0's and 1's. +- The `get_port` method on Node now returns None rather than raising a ValueError, if no such port with that name can be found. +- `verify_node` now called when adding nodes to models and `verify_node` modified to now verify not only check the node but also verify (via call to `verify_port`) any ports attached to the node. ## Releases From ef0dd22affa4023843654dda1e69634587ff6f90 Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Fri, 4 Sep 2026 14:41:20 +0800 Subject: [PATCH 4/4] Fix long line --- src/echo/models/base/port.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/echo/models/base/port.py b/src/echo/models/base/port.py index 3be0745..5c19532 100644 --- a/src/echo/models/base/port.py +++ b/src/echo/models/base/port.py @@ -517,7 +517,8 @@ def set_active_periods_from_array( if time_periods is None: time_periods = len(array) - # We need an array which only contains 0 or 1 representing inactive (flow fixed to 0) or active (flow can be optimised) + # We need an array which only contains 0 or 1 representing inactive (flow fixed to 0) + # or active (flow can be optimised) # Convert bools to ints active_periods_as_ints = [int(i) for i in array] set_of_active_periods = set(active_periods_as_ints)