From bcfd842118a2428aa39b17dab7323fc7eeaca0c3 Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Tue, 25 Aug 2026 09:50:16 +0800 Subject: [PATCH 1/2] Move test_gas.py --- tests/{ => unit/models}/test_gas.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ => unit/models}/test_gas.py (100%) diff --git a/tests/test_gas.py b/tests/unit/models/test_gas.py similarity index 100% rename from tests/test_gas.py rename to tests/unit/models/test_gas.py From cd06f3cdda25efeb3a3aa8624e612671b849558c Mon Sep 17 00:00:00 2001 From: Mike Turner Date: Thu, 27 Aug 2026 15:29:37 +0800 Subject: [PATCH 2/2] Split tests into separate integration and unit tests --- src/echo/models/thermal/storage.py | 2 +- tests/conftest.py | 32 +++++ .../test_agnostic_integration.py | 0 tests/{ => integration}/test_asset_types.py | 0 .../test_contingency_capacity.py | 0 .../test_contingency_positive_capacity.py | 0 .../test_demand_tariffs.py} | 28 +---- tests/{ => integration}/test_electrical.py | 0 tests/{ => integration}/test_evs.py | 0 .../test_hybrid_inverter_behaviour.py | 0 .../test_inverter_efficiency_behaviour.py | 0 .../test_load_control_schemes.py | 0 .../test_objective_functions.py | 0 tests/{ => integration}/test_optimiser.py | 0 tests/{ => integration}/test_path_tracing.py | 0 .../test_peak_power_objective.py | 0 .../test_peak_power_optimisation.py | 0 .../test_rebuild_optimisation_graph.py} | 34 +---- .../test_simple_controlled_load.py | 0 .../test_simple_solar_curtailment.py | 2 - .../{ => integration}/test_slack_variables.py | 2 - tests/{ => integration}/test_tariffs.py | 0 .../test_thermal_models_optimisation.py | 0 tests/test_agnostic_models.py | 118 ------------------ tests/test_thermal_models.py | 77 ------------ .../test_partitioned_multicommodity.py | 52 ++++++++ .../agnostic/tellegen/test_three_way_valve.py | 43 +++++++ tests/unit/models/base/test_port.py | 34 +++++ .../{test_gas.py => gas/test_gas_base.py} | 2 +- tests/unit/models/thermal/conftest.py | 27 ++++ .../models/thermal/test_simple_chiller.py | 16 +++ .../models/thermal/test_simple_heatpump.py | 30 +++++ .../test_simple_heatpump_dualoutput.py | 13 ++ tests/{ => unit}/test_utils.py | 0 tests/{ => unit}/test_visualisation.py | 0 35 files changed, 252 insertions(+), 260 deletions(-) rename tests/{ => integration}/test_agnostic_integration.py (100%) rename tests/{ => integration}/test_asset_types.py (100%) rename tests/{ => integration}/test_contingency_capacity.py (100%) rename tests/{ => integration}/test_contingency_positive_capacity.py (100%) rename tests/{test_new_demand_tariffs.py => integration/test_demand_tariffs.py} (90%) rename tests/{ => integration}/test_electrical.py (100%) rename tests/{ => integration}/test_evs.py (100%) rename tests/{ => integration}/test_hybrid_inverter_behaviour.py (100%) rename tests/{ => integration}/test_inverter_efficiency_behaviour.py (100%) rename tests/{ => integration}/test_load_control_schemes.py (100%) rename tests/{ => integration}/test_objective_functions.py (100%) rename tests/{ => integration}/test_optimiser.py (100%) rename tests/{ => integration}/test_path_tracing.py (100%) rename tests/{ => integration}/test_peak_power_objective.py (100%) rename tests/{ => integration}/test_peak_power_optimisation.py (100%) rename tests/{test_base.py => integration/test_rebuild_optimisation_graph.py} (86%) rename tests/{ => integration}/test_simple_controlled_load.py (100%) rename tests/{ => integration}/test_simple_solar_curtailment.py (99%) rename tests/{ => integration}/test_slack_variables.py (99%) rename tests/{ => integration}/test_tariffs.py (100%) rename tests/{ => integration}/test_thermal_models_optimisation.py (100%) delete mode 100644 tests/test_agnostic_models.py delete mode 100644 tests/test_thermal_models.py create mode 100644 tests/unit/models/agnostic/tellegen/test_partitioned_multicommodity.py create mode 100644 tests/unit/models/agnostic/tellegen/test_three_way_valve.py create mode 100644 tests/unit/models/base/test_port.py rename tests/unit/models/{test_gas.py => gas/test_gas_base.py} (94%) create mode 100644 tests/unit/models/thermal/conftest.py create mode 100644 tests/unit/models/thermal/test_simple_chiller.py create mode 100644 tests/unit/models/thermal/test_simple_heatpump.py create mode 100644 tests/unit/models/thermal/test_simple_heatpump_dualoutput.py rename tests/{ => unit}/test_utils.py (100%) rename tests/{ => unit}/test_visualisation.py (100%) diff --git a/src/echo/models/thermal/storage.py b/src/echo/models/thermal/storage.py index 5c3d0eb7..9ef2ad1b 100644 --- a/src/echo/models/thermal/storage.py +++ b/src/echo/models/thermal/storage.py @@ -30,7 +30,7 @@ class ThermalStorage(Node): discharging_power_limit: NegativeFloat = ( None # Maximum energy flow out of the storage at each interval, in energy_flow_units ) - ambient_temp: dict = None # Ambient temp, formatted as dict with expansion-time keys + ambient_temp: dict | None = None # Ambient temp, formatted as dict with expansion-time keys ambient_temp_ref: str | None # Ambient temp by column name reference in profile dataframe ins_transmittance: NonNegativeFloat = ( 0 # Thermal transmittance U-value of Thermal Energy Storage insulation in W/sqm*C diff --git a/tests/conftest.py b/tests/conftest.py index d095e248..9913def1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,10 @@ import os +import pyomo.environ as en import pytest +from echo.models.scenario import EchoConcreteModel, ScenarioSettings, engine_settings_from_environment + @pytest.fixture def can_optimiser_do_non_linear_optimisation() -> bool: @@ -13,3 +16,32 @@ def can_optimiser_do_non_linear_optimisation() -> bool: return False return True + + +@pytest.fixture +def empty_model(): + DEFAULT_NUMBER_OF_INTERVALS = 6 + + def _empty_model(number_of_intervals: int = DEFAULT_NUMBER_OF_INTERVALS) -> EchoConcreteModel: + model = EchoConcreteModel() + engine_settings = engine_settings_from_environment() + scenario_settings = ScenarioSettings( + interval_duration=30, + number_of_intervals=number_of_intervals, + number_of_expansion_intervals=1, + ) + model.small_m = en.Param(initialize=engine_settings.small_m) + model.big_m = en.Param(initialize=engine_settings.big_m) + model.scenario_settings = scenario_settings + model.Time = en.RangeSet(0, scenario_settings.number_of_intervals - 1) + if scenario_settings.number_of_expansion_intervals == 0: + model.Expansion = en.RangeSet(0, 0) + else: + model.Expansion = en.RangeSet(0, scenario_settings.number_of_expansion_intervals - 1) + discount_rates = {} + for ep in range(0, scenario_settings.number_of_expansion_intervals): + discount_rates[ep] = 1 / ((1 + scenario_settings.discount_rate) ** ep) + model.discount_rates = en.Param(model.Expansion, initialize=discount_rates) + return model + + return _empty_model diff --git a/tests/test_agnostic_integration.py b/tests/integration/test_agnostic_integration.py similarity index 100% rename from tests/test_agnostic_integration.py rename to tests/integration/test_agnostic_integration.py diff --git a/tests/test_asset_types.py b/tests/integration/test_asset_types.py similarity index 100% rename from tests/test_asset_types.py rename to tests/integration/test_asset_types.py diff --git a/tests/test_contingency_capacity.py b/tests/integration/test_contingency_capacity.py similarity index 100% rename from tests/test_contingency_capacity.py rename to tests/integration/test_contingency_capacity.py diff --git a/tests/test_contingency_positive_capacity.py b/tests/integration/test_contingency_positive_capacity.py similarity index 100% rename from tests/test_contingency_positive_capacity.py rename to tests/integration/test_contingency_positive_capacity.py diff --git a/tests/test_new_demand_tariffs.py b/tests/integration/test_demand_tariffs.py similarity index 90% rename from tests/test_new_demand_tariffs.py rename to tests/integration/test_demand_tariffs.py index bd23ae82..8cadc4f8 100644 --- a/tests/test_new_demand_tariffs.py +++ b/tests/integration/test_demand_tariffs.py @@ -1,39 +1,15 @@ import numpy as np -import pyomo.environ as en from echo.configuration import Units from echo.models.agnostic import FlexPort from echo.models.base import Node, OptimisationGraph from echo.models.electrical import ElectricalDemand, ElectricalGeneration -from echo.models.scenario import EchoConcreteModel, ScenarioSettings, engine_settings_from_environment +from echo.models.scenario import ScenarioSettings, engine_settings_from_environment from echo.objectives.base import ObjectiveSet from echo.objectives.tariff import DemandCharge, DemandTariffObjective, ExportDemandCharge, ImportDemandCharge from echo.optimiser import optimise -def empty_model(number_of_intervals: int = 6): - model = EchoConcreteModel() - engine_settings = engine_settings_from_environment() - scenario_settings = ScenarioSettings( - interval_duration=30, - number_of_intervals=number_of_intervals, - number_of_expansion_intervals=1, - ) - model.small_m = en.Param(initialize=engine_settings.small_m) - model.big_m = en.Param(initialize=engine_settings.big_m) - model.scenario_settings = scenario_settings - model.Time = en.RangeSet(0, scenario_settings.number_of_intervals - 1) - if scenario_settings.number_of_expansion_intervals == 0: - model.Expansion = en.RangeSet(0, 0) - else: - model.Expansion = en.RangeSet(0, scenario_settings.number_of_expansion_intervals - 1) - discount_rates = {} - for ep in range(0, scenario_settings.number_of_expansion_intervals): - discount_rates[ep] = 1 / ((1 + scenario_settings.discount_rate) ** ep) - model.discount_rates = en.Param(model.Expansion, initialize=discount_rates) - return model - - def test_system_import_demand_tariff(): """Test that we correctly calculate the max import demand""" @@ -261,7 +237,7 @@ def test_demand_tariff_read_and_implemented_correctly(): assert round(max_demand_3[0]) == max_in_window_3 == round(optimise_results.objective.args[2].value) -def test_demand_tariff_objective_apply_constraints_for_closure_issues(): +def test_demand_tariff_objective_apply_constraints_for_closure_issues(empty_model): model = empty_model(number_of_intervals=9) expansion_periods = 1 diff --git a/tests/test_electrical.py b/tests/integration/test_electrical.py similarity index 100% rename from tests/test_electrical.py rename to tests/integration/test_electrical.py diff --git a/tests/test_evs.py b/tests/integration/test_evs.py similarity index 100% rename from tests/test_evs.py rename to tests/integration/test_evs.py diff --git a/tests/test_hybrid_inverter_behaviour.py b/tests/integration/test_hybrid_inverter_behaviour.py similarity index 100% rename from tests/test_hybrid_inverter_behaviour.py rename to tests/integration/test_hybrid_inverter_behaviour.py diff --git a/tests/test_inverter_efficiency_behaviour.py b/tests/integration/test_inverter_efficiency_behaviour.py similarity index 100% rename from tests/test_inverter_efficiency_behaviour.py rename to tests/integration/test_inverter_efficiency_behaviour.py diff --git a/tests/test_load_control_schemes.py b/tests/integration/test_load_control_schemes.py similarity index 100% rename from tests/test_load_control_schemes.py rename to tests/integration/test_load_control_schemes.py diff --git a/tests/test_objective_functions.py b/tests/integration/test_objective_functions.py similarity index 100% rename from tests/test_objective_functions.py rename to tests/integration/test_objective_functions.py diff --git a/tests/test_optimiser.py b/tests/integration/test_optimiser.py similarity index 100% rename from tests/test_optimiser.py rename to tests/integration/test_optimiser.py diff --git a/tests/test_path_tracing.py b/tests/integration/test_path_tracing.py similarity index 100% rename from tests/test_path_tracing.py rename to tests/integration/test_path_tracing.py diff --git a/tests/test_peak_power_objective.py b/tests/integration/test_peak_power_objective.py similarity index 100% rename from tests/test_peak_power_objective.py rename to tests/integration/test_peak_power_objective.py diff --git a/tests/test_peak_power_optimisation.py b/tests/integration/test_peak_power_optimisation.py similarity index 100% rename from tests/test_peak_power_optimisation.py rename to tests/integration/test_peak_power_optimisation.py diff --git a/tests/test_base.py b/tests/integration/test_rebuild_optimisation_graph.py similarity index 86% rename from tests/test_base.py rename to tests/integration/test_rebuild_optimisation_graph.py index 137c2032..c8b0ce12 100644 --- a/tests/test_base.py +++ b/tests/integration/test_rebuild_optimisation_graph.py @@ -1,9 +1,8 @@ -import numpy as np import pytest from echo.configuration import Units from echo.models.agnostic import FlexPort, TellegenNode -from echo.models.base import Node, OptimisationGraph, Port +from echo.models.base import Node, OptimisationGraph from echo.models.electrical import EVV0G, EVV1G, EVV2G, ElectricalGeneration, Inverter from echo.models.prebuilt import FlexElectricalNode from echo.models.scenario import ScenarioSettings, engine_settings_from_environment @@ -12,37 +11,6 @@ from echo.optimiser import optimise -def test_port_proccess_initial_value_types(): - port = Port(port_name="port_name") - - # Assert the initial value is None before it's set - assert port.initial_value is None - - # 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} - - # 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 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 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} - - # 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_port_reference_is_made_in_edge(): """This test whether a port on a node and on an edge are references or copies. diff --git a/tests/test_simple_controlled_load.py b/tests/integration/test_simple_controlled_load.py similarity index 100% rename from tests/test_simple_controlled_load.py rename to tests/integration/test_simple_controlled_load.py diff --git a/tests/test_simple_solar_curtailment.py b/tests/integration/test_simple_solar_curtailment.py similarity index 99% rename from tests/test_simple_solar_curtailment.py rename to tests/integration/test_simple_solar_curtailment.py index 9eda68f6..d1758716 100644 --- a/tests/test_simple_solar_curtailment.py +++ b/tests/integration/test_simple_solar_curtailment.py @@ -8,8 +8,6 @@ from echo.objectives.base import ObjectiveSet, TotalFlow, TotalImportFlow from echo.optimiser import optimise -N_INTERVALS = 48 - def test_solar_generation_limited_by_inverter_size(): expansion_periods = 1 diff --git a/tests/test_slack_variables.py b/tests/integration/test_slack_variables.py similarity index 99% rename from tests/test_slack_variables.py rename to tests/integration/test_slack_variables.py index 701727cd..71736d58 100644 --- a/tests/test_slack_variables.py +++ b/tests/integration/test_slack_variables.py @@ -8,8 +8,6 @@ from echo.objectives.base import ObjectiveSet, TotalFlow from echo.optimiser import optimise -N_INTERVALS = 48 - def test_export_slack_var_is_minimised(): """Connect curtailable solar to a connection pt with a flow constraint and slack vars enabled. diff --git a/tests/test_tariffs.py b/tests/integration/test_tariffs.py similarity index 100% rename from tests/test_tariffs.py rename to tests/integration/test_tariffs.py diff --git a/tests/test_thermal_models_optimisation.py b/tests/integration/test_thermal_models_optimisation.py similarity index 100% rename from tests/test_thermal_models_optimisation.py rename to tests/integration/test_thermal_models_optimisation.py diff --git a/tests/test_agnostic_models.py b/tests/test_agnostic_models.py deleted file mode 100644 index 69afb934..00000000 --- a/tests/test_agnostic_models.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Unit testing for thermal models individual classes""" - -import pyomo.environ as en -import pytest - -from echo.configuration import Units -from echo.models.agnostic import FlexPort, PartitionedMultiCommodityTellegenNode, ThreeWayValveNode -from echo.models.scenario import EchoConcreteModel, ScenarioSettings, engine_settings_from_environment - - -def empty_model(): - model = EchoConcreteModel() - engine_settings = engine_settings_from_environment() - scenario_settings = ScenarioSettings( - interval_duration=30, - number_of_intervals=6, - number_of_expansion_intervals=1, - ) - model.small_m = en.Param(initialize=engine_settings.small_m) - model.big_m = en.Param(initialize=engine_settings.big_m) - model.scenario_settings = scenario_settings - model.Time = en.RangeSet(0, scenario_settings.number_of_intervals - 1) - if scenario_settings.number_of_expansion_intervals == 0: - model.Expansion = en.RangeSet(0, 0) - else: - model.Expansion = en.RangeSet(0, scenario_settings.number_of_expansion_intervals - 1) - discount_rates = {} - for ep in range(0, scenario_settings.number_of_expansion_intervals): - discount_rates[ep] = 1 / ((1 + scenario_settings.discount_rate) ** ep) - model.discount_rates = en.Param(model.Expansion, initialize=discount_rates) - return model - - -def test_partitioned_muticommodity_tellegen_node_default_partition(): - """Test asset creation with default partition""" - node = PartitionedMultiCommodityTellegenNode(ports={"port_1": FlexPort(units=Units.KW)}) - assert node.ports["port_1"] in node.partitions[node.default_partition] - - -def test_partitioned_muticommodity_tellegen_node(): - """Test asset creation with two partitions""" - node = PartitionedMultiCommodityTellegenNode( - partitions={ - "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - } - ) - all_partition_ports = [_port.uid for v in node.partitions.values() for _port in v] - all_ports = [_port.uid for _port in node.ports.values()] - assert all_partition_ports == all_ports - - -def test_partitioned_node_add_port(): - """Test asset creation""" - - node = PartitionedMultiCommodityTellegenNode( - partitions={ - "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - } - ) - start_number_ports = len(node.ports) - node.add_port(name="test_port", port=FlexPort(units=Units.KWT)) - assert node.ports["test_port"] - assert node.ports["test_port"] in node.partitions[node.default_partition] - assert len(node.ports) == start_number_ports + 1 - - -def test_partitioned_node_error(): - """Test only ports or partitions validation error""" - with pytest.raises(Exception): - PartitionedMultiCommodityTellegenNode( - partitions={ - "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], - }, - ports={"port_1": FlexPort(units=Units.KW)}, - ) - - -def test_threeway_tellegen_node(): - """Test asset creation and pyomo constraints""" - - node = ThreeWayValveNode( - node_name="three_way_valve", - units=Units.KW, - input_port_name="port_1", - output_port_name_1="port_2", - output_port_name_2="port_3", - ) - model = empty_model() - node.add_node_to_model(model, profile=None) - node.apply_node_constraints(model) - assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_1) is not None - assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_2) is not None - assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_1) is not None - assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_2) is not None - - -def test_threeway_tellegen_node_add_port(): - """Test asset creation, add ports and pyomo constraints""" - - node = ThreeWayValveNode( - node_name="three_way_valve", - units=Units.KW, - input_port_name="port_1", - output_port_name_1="port_2", - output_port_name_2="port_3", - ) - node.add_port(name="port_4", port=FlexPort(units=Units.KW)) - node.add_port(name="port_5", port=FlexPort(units=Units.KW)) - model = empty_model() - node.add_node_to_model(model, profile=None) - node.apply_node_constraints(model) - assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_1) is not None - assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_2) is not None - assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_1) is not None - assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_2) is not None diff --git a/tests/test_thermal_models.py b/tests/test_thermal_models.py deleted file mode 100644 index 9729e173..00000000 --- a/tests/test_thermal_models.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Unit testing for thermal models individual classes""" - -import pytest - -from echo.configuration import Units -from echo.models.thermal import SimpleChiller, SimpleHeatPump, SimpleHeatPumpDualOutput -from echo.utils import TimeSeriesData, expand_as_dict - -NUMBER_INTERVALS = 5 -INTERVAL_DURATION = 30 -NUM_EXPANSION_PERIODS = 1 - - -cooling_cop_data = TimeSeriesData( - value=[2, 3, 1, 2, 2.5], - num_time_intervals=NUMBER_INTERVALS, - num_expansion_intervals=NUM_EXPANSION_PERIODS, -) -cooling_cop_dict = expand_as_dict(cooling_cop_data) - -heating_cop_data = TimeSeriesData( - value=[4, 3, 5, 2, 2.5], - num_time_intervals=NUMBER_INTERVALS, - num_expansion_intervals=NUM_EXPANSION_PERIODS, -) -heating_cop_dict = expand_as_dict(heating_cop_data) - - -def test_simple_chiller(): - """Test asset creation""" - SimpleChiller(max_cooling_capacity=20, cooling_cop_time_series=cooling_cop_dict) - - -def test_simple_chiller_cop_error(): - """Test non negative cop validation error""" - with pytest.raises(Exception): - cooling_cop_dict_neg = cooling_cop_dict.copy() - cooling_cop_dict_neg[(0, 0)] *= -1 - SimpleChiller(max_cooling_capacity=20, cooling_cop_time_series=cooling_cop_dict_neg) - - -def test_simple_heatpump(): - """Test creation and assert default ports.""" - hp = SimpleHeatPump( - cooling_cop_time_series=cooling_cop_dict, - heating_cop_time_series=heating_cop_dict, - ) - assert len(hp.ports) == 2 - - -def test_simple_heatpump_dual_output(): - """Test creation and assert default ports.""" - hp_dual_output = SimpleHeatPumpDualOutput( - cooling_cop_time_series=cooling_cop_dict, - heating_cop_time_series=heating_cop_dict, - dual_output=True, - ) - assert len(hp_dual_output.ports) == 3 - assert len([p for p in hp_dual_output.ports.values() if p.units == Units.KWT]) == 2 - - -def test_simple_heatpump_cop_error(): - """Test non negative cop validation error""" - with pytest.raises(Exception): - cooling_cop_dict_neg = cooling_cop_dict.copy() - cooling_cop_dict_neg[(0, 0)] *= -1 - SimpleHeatPump( - cooling_cop_time_series=cooling_cop_dict_neg, - heating_cop_time_series=heating_cop_dict, - ) - with pytest.raises(Exception): - heating_cop_dict_neg = heating_cop_dict.copy() - heating_cop_dict_neg[(0, 0)] *= -1 - SimpleHeatPump( - cooling_cop_time_series=cooling_cop_dict, - heating_cop_time_series=heating_cop_dict_neg, - ) diff --git a/tests/unit/models/agnostic/tellegen/test_partitioned_multicommodity.py b/tests/unit/models/agnostic/tellegen/test_partitioned_multicommodity.py new file mode 100644 index 00000000..7d5278b1 --- /dev/null +++ b/tests/unit/models/agnostic/tellegen/test_partitioned_multicommodity.py @@ -0,0 +1,52 @@ +import pytest + +from echo.configuration import Units +from echo.models.agnostic.flex import FlexPort +from echo.models.agnostic.tellegen.partitioned_multi_commodity import PartitionedMultiCommodityTellegenNode + + +def test_partitioned_muticommodity_tellegen_node_default_partition(): + """Test asset creation with default partition""" + node = PartitionedMultiCommodityTellegenNode(ports={"port_1": FlexPort(units=Units.KW)}) + assert node.ports["port_1"] in node.partitions[node.default_partition] + + +def test_partitioned_muticommodity_tellegen_node(): + """Test asset creation with two partitions""" + node = PartitionedMultiCommodityTellegenNode( + partitions={ + "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + } + ) + all_partition_ports = [_port.uid for v in node.partitions.values() for _port in v] + all_ports = [_port.uid for _port in node.ports.values()] + assert all_partition_ports == all_ports + + +def test_partitioned_node_add_port(): + """Test asset creation""" + + node = PartitionedMultiCommodityTellegenNode( + partitions={ + "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + } + ) + start_number_ports = len(node.ports) + node.add_port(name="test_port", port=FlexPort(units=Units.KWT)) + assert node.ports["test_port"] + assert node.ports["test_port"] in node.partitions[node.default_partition] + assert len(node.ports) == start_number_ports + 1 + + +def test_partitioned_node_error(): + """Test only ports or partitions validation error""" + with pytest.raises(Exception): + PartitionedMultiCommodityTellegenNode( + partitions={ + "partition_1": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + "partition_2": [FlexPort(units=Units.KW), FlexPort(units=Units.KW)], + }, + ports={"port_1": FlexPort(units=Units.KW)}, + ) diff --git a/tests/unit/models/agnostic/tellegen/test_three_way_valve.py b/tests/unit/models/agnostic/tellegen/test_three_way_valve.py new file mode 100644 index 00000000..37bdb6d2 --- /dev/null +++ b/tests/unit/models/agnostic/tellegen/test_three_way_valve.py @@ -0,0 +1,43 @@ +from echo.configuration import Units +from echo.models.agnostic.flex import FlexPort +from echo.models.agnostic.tellegen.three_way_valve import ThreeWayValveNode + + +def test_threeway_tellegen_node(empty_model): + """Test asset creation and pyomo constraints""" + + node = ThreeWayValveNode( + node_name="three_way_valve", + units=Units.KW, + input_port_name="port_1", + output_port_name_1="port_2", + output_port_name_2="port_3", + ) + model = empty_model() + node.add_node_to_model(model, profile=None) + node.apply_node_constraints(model) + assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_1) is not None + assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_2) is not None + assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_1) is not None + assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_2) is not None + + +def test_threeway_tellegen_node_add_port(empty_model): + """Test asset creation, add ports and pyomo constraints""" + + node = ThreeWayValveNode( + node_name="three_way_valve", + units=Units.KW, + input_port_name="port_1", + output_port_name_1="port_2", + output_port_name_2="port_3", + ) + node.add_port(name="port_4", port=FlexPort(units=Units.KW)) + node.add_port(name="port_5", port=FlexPort(units=Units.KW)) + model = empty_model() + node.add_node_to_model(model, profile=None) + node.apply_node_constraints(model) + assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_1) is not None + assert getattr(model, node.constraint_neg_flow_mutually_exclusive_port_2) is not None + assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_1) is not None + assert getattr(model, node.constraint_pos_flow_mutually_exclusive_port_2) is not None diff --git a/tests/unit/models/base/test_port.py b/tests/unit/models/base/test_port.py new file mode 100644 index 00000000..ef1f903f --- /dev/null +++ b/tests/unit/models/base/test_port.py @@ -0,0 +1,34 @@ +import numpy as np + +from echo.models.base.port import Port + + +def test_port_proccess_initial_value_types(): + port = Port(port_name="port_name") + + # Assert the initial value is None before it's set + assert port.initial_value is None + + # 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} + + # 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 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 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} + + # 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} diff --git a/tests/unit/models/test_gas.py b/tests/unit/models/gas/test_gas_base.py similarity index 94% rename from tests/unit/models/test_gas.py rename to tests/unit/models/gas/test_gas_base.py index 3d1fcdbe..f3ddb0b6 100644 --- a/tests/unit/models/test_gas.py +++ b/tests/unit/models/gas/test_gas_base.py @@ -1,5 +1,5 @@ from echo.configuration import FlowConstraint, Flows, OptimisationType, Units -from echo.models.gas import FixedGasPort, FlexGasPort +from echo.models.gas.base import FixedGasPort, FlexGasPort def test_build_flex_gas_port(): diff --git a/tests/unit/models/thermal/conftest.py b/tests/unit/models/thermal/conftest.py new file mode 100644 index 00000000..f6b51a73 --- /dev/null +++ b/tests/unit/models/thermal/conftest.py @@ -0,0 +1,27 @@ +import pytest + +from echo.utils import TimeSeriesData, expand_as_dict + + +@pytest.fixture +def cooling_cop_dict(): + raw_cops = [2, 3, 1, 2, 2.5] + return expand_as_dict( + TimeSeriesData( + value=raw_cops, + num_time_intervals=len(raw_cops), + num_expansion_intervals=1, + ) + ) + + +@pytest.fixture +def heating_cop_dict(): + raw_cops = [4, 3, 5, 2, 2.5] + return expand_as_dict( + TimeSeriesData( + value=raw_cops, + num_time_intervals=len(raw_cops), + num_expansion_intervals=1, + ) + ) diff --git a/tests/unit/models/thermal/test_simple_chiller.py b/tests/unit/models/thermal/test_simple_chiller.py new file mode 100644 index 00000000..758db62d --- /dev/null +++ b/tests/unit/models/thermal/test_simple_chiller.py @@ -0,0 +1,16 @@ +import pytest + +from echo.models.thermal import SimpleChiller + + +def test_simple_chiller(cooling_cop_dict): + """Test asset creation""" + SimpleChiller(max_cooling_capacity=20, cooling_cop_time_series=cooling_cop_dict) + + +def test_simple_chiller_cop_error(cooling_cop_dict): + """Test non negative cop validation error""" + with pytest.raises(Exception): + cooling_cop_dict_neg = cooling_cop_dict.copy() + cooling_cop_dict_neg[(0, 0)] *= -1 + SimpleChiller(max_cooling_capacity=20, cooling_cop_time_series=cooling_cop_dict_neg) diff --git a/tests/unit/models/thermal/test_simple_heatpump.py b/tests/unit/models/thermal/test_simple_heatpump.py new file mode 100644 index 00000000..97764942 --- /dev/null +++ b/tests/unit/models/thermal/test_simple_heatpump.py @@ -0,0 +1,30 @@ +import pytest + +from echo.models.thermal import SimpleHeatPump + + +def test_simple_heatpump(cooling_cop_dict, heating_cop_dict): + """Test creation and assert default ports.""" + hp = SimpleHeatPump( + cooling_cop_time_series=cooling_cop_dict, + heating_cop_time_series=heating_cop_dict, + ) + assert len(hp.ports) == 2 + + +def test_simple_heatpump_cop_error(cooling_cop_dict, heating_cop_dict): + """Test non negative cop validation error""" + with pytest.raises(Exception): + cooling_cop_dict_neg = cooling_cop_dict.copy() + cooling_cop_dict_neg[(0, 0)] *= -1 + SimpleHeatPump( + cooling_cop_time_series=cooling_cop_dict_neg, + heating_cop_time_series=heating_cop_dict, + ) + with pytest.raises(Exception): + heating_cop_dict_neg = heating_cop_dict.copy() + heating_cop_dict_neg[(0, 0)] *= -1 + SimpleHeatPump( + cooling_cop_time_series=cooling_cop_dict, + heating_cop_time_series=heating_cop_dict_neg, + ) diff --git a/tests/unit/models/thermal/test_simple_heatpump_dualoutput.py b/tests/unit/models/thermal/test_simple_heatpump_dualoutput.py new file mode 100644 index 00000000..f68a1008 --- /dev/null +++ b/tests/unit/models/thermal/test_simple_heatpump_dualoutput.py @@ -0,0 +1,13 @@ +from echo.configuration import Units +from echo.models.thermal import SimpleHeatPumpDualOutput + + +def test_simple_heatpump_dual_output(cooling_cop_dict, heating_cop_dict): + """Test creation and assert default ports.""" + hp_dual_output = SimpleHeatPumpDualOutput( + cooling_cop_time_series=cooling_cop_dict, + heating_cop_time_series=heating_cop_dict, + dual_output=True, + ) + assert len(hp_dual_output.ports) == 3 + assert len([p for p in hp_dual_output.ports.values() if p.units == Units.KWT]) == 2 diff --git a/tests/test_utils.py b/tests/unit/test_utils.py similarity index 100% rename from tests/test_utils.py rename to tests/unit/test_utils.py diff --git a/tests/test_visualisation.py b/tests/unit/test_visualisation.py similarity index 100% rename from tests/test_visualisation.py rename to tests/unit/test_visualisation.py