From 0ae44265c2235fe10b61538b8fe7fde03ef50037 Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Tue, 26 May 2026 18:24:12 +0200 Subject: [PATCH 1/4] initial commit --- CHANGELOG.md | 3 +- src/mesido/heat_physics_mixin.py | 8 +- .../model/HP_ATES with return network.esdl | 2 +- tests/models/insulation/model/Insulation.esdl | 2 +- .../case_1a/model/1a_2_producers.esdl | 154 ++++++++++++++++++ tests/models/unit_cases/case_1a/src/run_1a.py | 5 + tests/test_varying_temperature.py | 70 ++++++++ 7 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 tests/models/unit_cases/case_1a/model/1a_2_producers.esdl diff --git a/CHANGELOG.md b/CHANGELOG.md index d7860aa25..952a70d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# [Unreleased-main] - 2026-05-13 +# [Unreleased-main] - 2026-05-26 ## Added - Electricity consumption calculation of geothermal assets, using the defined COP. @@ -12,6 +12,7 @@ - Ramp constraints for heat producers are added. - Maximum and minimum temperature of heat sources are parsed from esdl - Warnings on potential causes of heat demand not being matched are added in the grow workflow +- A heat source asset is eligible for use only when its maximum temperature meets or exceeds the network supply temperature ## Changed - Reduced the number of constraints required for headloss calculation with LINEARIZED_N_LINES_EQUALITY setting. diff --git a/src/mesido/heat_physics_mixin.py b/src/mesido/heat_physics_mixin.py index 760e17826..9b3b1cf77 100644 --- a/src/mesido/heat_physics_mixin.py +++ b/src/mesido/heat_physics_mixin.py @@ -1425,10 +1425,14 @@ def __source_heat_to_discharge_path_constraints(self, ensemble_member): if temp_out_profile is None: if len(supply_temperatures) == 0: + heat_out_expected = discharge * cp * rho * parameters[f"{s}.T_supply"] + if (0.0 < parameters[f"{s}.max_temperature"]) and ( + parameters[f"{s}.max_temperature"] < parameters[f"{s}.T_supply"] + ): + heat_out_expected = 0.0 constraints.append( ( - (heat_out - discharge * cp * rho * parameters[f"{s}.T_supply"]) - / heat_nominal, + (heat_out - heat_out_expected) / heat_nominal, 0.0, 0.0, ) diff --git a/tests/models/ates_temperature/model/HP_ATES with return network.esdl b/tests/models/ates_temperature/model/HP_ATES with return network.esdl index e4ee3e048..6939e1cca 100644 --- a/tests/models/ates_temperature/model/HP_ATES with return network.esdl +++ b/tests/models/ates_temperature/model/HP_ATES with return network.esdl @@ -118,7 +118,7 @@ - + diff --git a/tests/models/insulation/model/Insulation.esdl b/tests/models/insulation/model/Insulation.esdl index 5abc77025..9b481ab1d 100644 --- a/tests/models/insulation/model/Insulation.esdl +++ b/tests/models/insulation/model/Insulation.esdl @@ -266,7 +266,7 @@ - + diff --git a/tests/models/unit_cases/case_1a/model/1a_2_producers.esdl b/tests/models/unit_cases/case_1a/model/1a_2_producers.esdl new file mode 100644 index 000000000..bc061f751 --- /dev/null +++ b/tests/models/unit_cases/case_1a/model/1a_2_producers.esdl @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/models/unit_cases/case_1a/src/run_1a.py b/tests/models/unit_cases/case_1a/src/run_1a.py index 7acad48a9..69884bf29 100644 --- a/tests/models/unit_cases/case_1a/src/run_1a.py +++ b/tests/models/unit_cases/case_1a/src/run_1a.py @@ -3,6 +3,7 @@ from mesido.esdl.profile_parser import ProfileReaderFromFile from mesido.physics_mixin import PhysicsMixin from mesido.qth_not_maintained.qth_mixin import QTHMixin +from mesido.techno_economic_mixin import TechnoEconomicMixin import numpy as np @@ -61,6 +62,10 @@ def solver_options(self): return options +class HeatProblemMinimizeCost(HeatProblem, TechnoEconomicMixin): + pass + + class HeatProblemTvar(HeatProblem): def energy_system_options(self): options = super().energy_system_options() diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index ee8ff20eb..fd3f7118c 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -12,6 +12,76 @@ class TestVaryingTemperature(TestCase): + def test_1a_heatsource_usage_based_on_supply_temperature(self): + """ + This test is to check whether the optimizer selects the expected heat source + based on the supply temperature. We set up a simple network with two residual + heat sources, one cheap and one expensive, where the cheap one has a maximum + supply temperature that is below the demand temperature, and the expensive + one has a maximum supply temperature that is above the demand temperature. + We expect the optimizer to select the expensive heat source to meet + the demand, and thus not use the cheap heat source at all. + + Checks: + - Variable operational cost coefficients of heat producers + - Maximum supply temperature of heat producers + - Check that the expensive heat source is used instead of the cheap heat source + """ + import models.unit_cases.case_1a.src.run_1a as run_1a + from models.unit_cases.case_1a.src.run_1a import HeatProblemMinimizeCost + + base_folder = Path(run_1a.__file__).resolve().parent.parent + + heat_problem = run_esdl_mesido_optimization( + HeatProblemMinimizeCost, + base_folder=base_folder, + esdl_file_name="1a_2_producers.esdl", + esdl_parser=ESDLFileParser, + profile_reader=ProfileReaderFromFile, + input_timeseries_file="timeseries_import.xml", + ) + + results = heat_problem.extract_results() + parameters = heat_problem.parameters(0) + name_to_id_map = heat_problem.esdl_asset_name_to_id_map + + rh_cheap_id = name_to_id_map["ResidualHeat_cheap"] + rh_expensive_id = name_to_id_map["ResidualHeat_expensive"] + + # Check variable operational cost coefficients + np.testing.assert_array_less( + parameters[f"{rh_cheap_id}.variable_operational_cost_coefficient"], + parameters[f"{rh_expensive_id}.variable_operational_cost_coefficient"], + ) + + # Check that the maximum supply temperature of the cheap heat source + # is below the supply temperature + esdl_asset_cheap = heat_problem.esdl_assets[f"{rh_cheap_id}"] + np.testing.assert_equal( + esdl_asset_cheap.attributes["maxTemperature"], + parameters[f"{rh_cheap_id}.max_temperature"], + ) + np.testing.assert_array_less( + parameters[f"{rh_cheap_id}.max_temperature"], + heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed"]["temperature"], + ) + + # Check that the maximum supply temperature of the expensive heat source + # is above the supply temperature + esdl_asset_expensive = heat_problem.esdl_assets[f"{rh_expensive_id}"] + np.testing.assert_equal( + esdl_asset_expensive.attributes["maxTemperature"], + parameters[f"{rh_expensive_id}.max_temperature"], + ) + np.testing.assert_array_less( + heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed_ret"]["temperature"], + parameters[f"{rh_expensive_id}.max_temperature"], + ) + + # Check expensive heat source is used instead of cheap heat source + np.testing.assert_array_less(1e3, results[f"{rh_expensive_id}.Heat_source"]) + np.testing.assert_allclose(0.0, results[f"{rh_cheap_id}.Heat_source"]) + def test_1a_temperature_variation(self): """ This test is to check if the varying network temperature works as expected on a simple From 684196214c17bca8b30690521fe2bfb3bb5839d5 Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Wed, 27 May 2026 12:40:30 +0200 Subject: [PATCH 2/4] simple renaming --- ...{1a_2_producers.esdl => 1a_with_2producers.esdl} | 10 +++++----- tests/models/unit_cases/case_1a/src/run_1a.py | 2 +- tests/test_varying_temperature.py | 13 ++++++------- 3 files changed, 12 insertions(+), 13 deletions(-) rename tests/models/unit_cases/case_1a/model/{1a_2_producers.esdl => 1a_with_2producers.esdl} (95%) diff --git a/tests/models/unit_cases/case_1a/model/1a_2_producers.esdl b/tests/models/unit_cases/case_1a/model/1a_with_2producers.esdl similarity index 95% rename from tests/models/unit_cases/case_1a/model/1a_2_producers.esdl rename to tests/models/unit_cases/case_1a/model/1a_with_2producers.esdl index bc061f751..3305d78d9 100644 --- a/tests/models/unit_cases/case_1a/model/1a_2_producers.esdl +++ b/tests/models/unit_cases/case_1a/model/1a_with_2producers.esdl @@ -1,5 +1,5 @@ - + @@ -32,13 +32,13 @@ - + - + @@ -132,8 +132,8 @@ - - + + diff --git a/tests/models/unit_cases/case_1a/src/run_1a.py b/tests/models/unit_cases/case_1a/src/run_1a.py index 69884bf29..d880d1235 100644 --- a/tests/models/unit_cases/case_1a/src/run_1a.py +++ b/tests/models/unit_cases/case_1a/src/run_1a.py @@ -62,7 +62,7 @@ def solver_options(self): return options -class HeatProblemMinimizeCost(HeatProblem, TechnoEconomicMixin): +class HeatProblemWithTechnoEconomicMixin(HeatProblem, TechnoEconomicMixin): pass diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index fd3f7118c..ce7890c41 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -28,14 +28,14 @@ def test_1a_heatsource_usage_based_on_supply_temperature(self): - Check that the expensive heat source is used instead of the cheap heat source """ import models.unit_cases.case_1a.src.run_1a as run_1a - from models.unit_cases.case_1a.src.run_1a import HeatProblemMinimizeCost + from models.unit_cases.case_1a.src.run_1a import HeatProblemWithTechnoEconomicMixin base_folder = Path(run_1a.__file__).resolve().parent.parent heat_problem = run_esdl_mesido_optimization( - HeatProblemMinimizeCost, + HeatProblemWithTechnoEconomicMixin, base_folder=base_folder, - esdl_file_name="1a_2_producers.esdl", + esdl_file_name="1a_with_2producers.esdl", esdl_parser=ESDLFileParser, profile_reader=ProfileReaderFromFile, input_timeseries_file="timeseries_import.xml", @@ -56,9 +56,8 @@ def test_1a_heatsource_usage_based_on_supply_temperature(self): # Check that the maximum supply temperature of the cheap heat source # is below the supply temperature - esdl_asset_cheap = heat_problem.esdl_assets[f"{rh_cheap_id}"] np.testing.assert_equal( - esdl_asset_cheap.attributes["maxTemperature"], + heat_problem.esdl_assets[f"{rh_cheap_id}"].attributes["maxTemperature"], parameters[f"{rh_cheap_id}.max_temperature"], ) np.testing.assert_array_less( @@ -68,9 +67,8 @@ def test_1a_heatsource_usage_based_on_supply_temperature(self): # Check that the maximum supply temperature of the expensive heat source # is above the supply temperature - esdl_asset_expensive = heat_problem.esdl_assets[f"{rh_expensive_id}"] np.testing.assert_equal( - esdl_asset_expensive.attributes["maxTemperature"], + heat_problem.esdl_assets[f"{rh_expensive_id}"].attributes["maxTemperature"], parameters[f"{rh_expensive_id}.max_temperature"], ) np.testing.assert_array_less( @@ -535,6 +533,7 @@ def test_heat_pump_varying_temperature(self): start_time = time.time() a = TestVaryingTemperature() + a.test_1a_heatsource_usage_based_on_supply_temperature() a.test_1a_temperature_variation() a.test_3a_temperature_variation_supply() a.test_3a_temperature_variation_return() From cac37105539adcfc80c842e0fe5b005d449bb08c Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Wed, 27 May 2026 19:24:41 +0200 Subject: [PATCH 3/4] test location is changed --- tests/test_heat.py | 71 +++++++++++++++++++++++++++++++ tests/test_varying_temperature.py | 69 ------------------------------ 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/tests/test_heat.py b/tests/test_heat.py index 96652c71a..5c9fb08ba 100644 --- a/tests/test_heat.py +++ b/tests/test_heat.py @@ -173,6 +173,77 @@ def energy_system_options(self): np.testing.assert_array_almost_equal(temp_pipe1, temp_input_prof) +class TestHeatSourceTemperature(TestCase): + + def test_heatsource_usage_based_on_supply_temperature(self): + """ + This test is to check whether the optimizer selects the expected heat source + based on the supply temperature. We set up a simple network with two residual + heat sources, one cheap and one expensive, where the cheap one has a maximum + supply temperature that is below the demand temperature, and the expensive + one has a maximum supply temperature that is above the demand temperature. + We expect the optimizer to select the expensive heat source to meet + the demand, and thus not use the cheap heat source at all. + + Checks: + - Variable operational cost coefficients of heat producers + - Maximum supply temperature of heat producers + - Check that the expensive heat source is used instead of the cheap heat source + """ + import models.unit_cases.case_1a.src.run_1a as run_1a + from models.unit_cases.case_1a.src.run_1a import HeatProblemWithTechnoEconomicMixin + + base_folder = Path(run_1a.__file__).resolve().parent.parent + + heat_problem = run_esdl_mesido_optimization( + HeatProblemWithTechnoEconomicMixin, + base_folder=base_folder, + esdl_file_name="1a_with_2producers.esdl", + esdl_parser=ESDLFileParser, + profile_reader=ProfileReaderFromFile, + input_timeseries_file="timeseries_import.xml", + ) + + results = heat_problem.extract_results() + parameters = heat_problem.parameters(0) + name_to_id_map = heat_problem.esdl_asset_name_to_id_map + + rh_cheap_id = name_to_id_map["ResidualHeat_cheap"] + rh_expensive_id = name_to_id_map["ResidualHeat_expensive"] + + # Check variable operational cost coefficients + np.testing.assert_array_less( + parameters[f"{rh_cheap_id}.variable_operational_cost_coefficient"], + parameters[f"{rh_expensive_id}.variable_operational_cost_coefficient"], + ) + + # Check that the maximum supply temperature of the cheap heat source + # is below the supply temperature + np.testing.assert_equal( + heat_problem.esdl_assets[f"{rh_cheap_id}"].attributes["maxTemperature"], + parameters[f"{rh_cheap_id}.max_temperature"], + ) + np.testing.assert_array_less( + parameters[f"{rh_cheap_id}.max_temperature"], + heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed"]["temperature"], + ) + + # Check that the maximum supply temperature of the expensive heat source + # is above the supply temperature + np.testing.assert_equal( + heat_problem.esdl_assets[f"{rh_expensive_id}"].attributes["maxTemperature"], + parameters[f"{rh_expensive_id}.max_temperature"], + ) + np.testing.assert_array_less( + heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed"]["temperature"], + parameters[f"{rh_expensive_id}.max_temperature"], + ) + + # Check expensive heat source is used instead of cheap heat source + np.testing.assert_array_less(1e3, results[f"{rh_expensive_id}.Heat_source"]) + np.testing.assert_allclose(0.0, results[f"{rh_cheap_id}.Heat_source"]) + + class TestMinMaxPressureOptions(TestCase): import models.source_pipe_sink.src.double_pipe_heat as double_pipe_heat from models.source_pipe_sink.src.double_pipe_heat import SourcePipeSink diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index ce7890c41..ee8ff20eb 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -12,74 +12,6 @@ class TestVaryingTemperature(TestCase): - def test_1a_heatsource_usage_based_on_supply_temperature(self): - """ - This test is to check whether the optimizer selects the expected heat source - based on the supply temperature. We set up a simple network with two residual - heat sources, one cheap and one expensive, where the cheap one has a maximum - supply temperature that is below the demand temperature, and the expensive - one has a maximum supply temperature that is above the demand temperature. - We expect the optimizer to select the expensive heat source to meet - the demand, and thus not use the cheap heat source at all. - - Checks: - - Variable operational cost coefficients of heat producers - - Maximum supply temperature of heat producers - - Check that the expensive heat source is used instead of the cheap heat source - """ - import models.unit_cases.case_1a.src.run_1a as run_1a - from models.unit_cases.case_1a.src.run_1a import HeatProblemWithTechnoEconomicMixin - - base_folder = Path(run_1a.__file__).resolve().parent.parent - - heat_problem = run_esdl_mesido_optimization( - HeatProblemWithTechnoEconomicMixin, - base_folder=base_folder, - esdl_file_name="1a_with_2producers.esdl", - esdl_parser=ESDLFileParser, - profile_reader=ProfileReaderFromFile, - input_timeseries_file="timeseries_import.xml", - ) - - results = heat_problem.extract_results() - parameters = heat_problem.parameters(0) - name_to_id_map = heat_problem.esdl_asset_name_to_id_map - - rh_cheap_id = name_to_id_map["ResidualHeat_cheap"] - rh_expensive_id = name_to_id_map["ResidualHeat_expensive"] - - # Check variable operational cost coefficients - np.testing.assert_array_less( - parameters[f"{rh_cheap_id}.variable_operational_cost_coefficient"], - parameters[f"{rh_expensive_id}.variable_operational_cost_coefficient"], - ) - - # Check that the maximum supply temperature of the cheap heat source - # is below the supply temperature - np.testing.assert_equal( - heat_problem.esdl_assets[f"{rh_cheap_id}"].attributes["maxTemperature"], - parameters[f"{rh_cheap_id}.max_temperature"], - ) - np.testing.assert_array_less( - parameters[f"{rh_cheap_id}.max_temperature"], - heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed"]["temperature"], - ) - - # Check that the maximum supply temperature of the expensive heat source - # is above the supply temperature - np.testing.assert_equal( - heat_problem.esdl_assets[f"{rh_expensive_id}"].attributes["maxTemperature"], - parameters[f"{rh_expensive_id}.max_temperature"], - ) - np.testing.assert_array_less( - heat_problem.esdl_carriers["c362f53a-3eaf-4d96-8ee6-944e77359fed_ret"]["temperature"], - parameters[f"{rh_expensive_id}.max_temperature"], - ) - - # Check expensive heat source is used instead of cheap heat source - np.testing.assert_array_less(1e3, results[f"{rh_expensive_id}.Heat_source"]) - np.testing.assert_allclose(0.0, results[f"{rh_cheap_id}.Heat_source"]) - def test_1a_temperature_variation(self): """ This test is to check if the varying network temperature works as expected on a simple @@ -533,7 +465,6 @@ def test_heat_pump_varying_temperature(self): start_time = time.time() a = TestVaryingTemperature() - a.test_1a_heatsource_usage_based_on_supply_temperature() a.test_1a_temperature_variation() a.test_3a_temperature_variation_supply() a.test_3a_temperature_variation_return() From 6592212d908f97e1f328c057b39de89164817fca Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Thu, 28 May 2026 09:42:23 +0200 Subject: [PATCH 4/4] doc string update --- tests/test_heat.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_heat.py b/tests/test_heat.py index 5c9fb08ba..e81089070 100644 --- a/tests/test_heat.py +++ b/tests/test_heat.py @@ -180,10 +180,9 @@ def test_heatsource_usage_based_on_supply_temperature(self): This test is to check whether the optimizer selects the expected heat source based on the supply temperature. We set up a simple network with two residual heat sources, one cheap and one expensive, where the cheap one has a maximum - supply temperature that is below the demand temperature, and the expensive - one has a maximum supply temperature that is above the demand temperature. - We expect the optimizer to select the expensive heat source to meet - the demand, and thus not use the cheap heat source at all. + temperature that is below the network supply temperature, and the expensive + one has a maximum temperature that is above the network supply temperature. + We expect the optimizer to use only the expensive heat source. Checks: - Variable operational cost coefficients of heat producers