From 5896d1af3a2c0ef5d1fae2c6759a0ec4703a249a Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Wed, 27 May 2026 19:14:36 +0200 Subject: [PATCH 1/4] initial commit --- src/mesido/heat_physics_mixin.py | 8 +- ...ducers_and_supply_temperature_options.esdl | 156 ++++++++++++++++++ tests/models/unit_cases/case_1a/src/run_1a.py | 11 ++ tests/test_varying_temperature.py | 80 +++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl diff --git a/src/mesido/heat_physics_mixin.py b/src/mesido/heat_physics_mixin.py index 760e17826..faa687393 100644 --- a/src/mesido/heat_physics_mixin.py +++ b/src/mesido/heat_physics_mixin.py @@ -1439,9 +1439,15 @@ def __source_heat_to_discharge_path_constraints(self, ensemble_member): f"{sup_carrier}_{supply_temperature}" ) + heat_out_expected = discharge * cp * rho * supply_temperature + if (0.0 < parameters[f"{s}.max_temperature"]) and ( + parameters[f"{s}.max_temperature"] < supply_temperature + ): + heat_out_expected = 0.0 + constraints.extend( self._symmetric_big_m_constraints( - heat_out - discharge * cp * rho * supply_temperature, + heat_out - heat_out_expected, (1.0 - sup_temperature_is_selected) * big_m, constraint_nominal, ) diff --git a/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl b/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl new file mode 100644 index 000000000..d366f66c8 --- /dev/null +++ b/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..a7aec4a35 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 @@ -113,6 +114,16 @@ def constraints(self, ensemble_member): return constraints +class HeatProblemTvarWithTechnoEconomicMixin(HeatProblemTvar, TechnoEconomicMixin): + def temperature_regimes(self, carrier): + temperatures = [] + if carrier == 3625334968694477359: + # supply + temperatures = [71.0, 74.0, 86.0] + + return temperatures + + class QTHProblem( _GoalsAndOptions, QTHMixin, diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index ee8ff20eb..fc68ebd1b 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -12,6 +12,85 @@ class TestVaryingTemperature(TestCase): + def test_1a_heatsource_usage_based_on_varying_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. + In addition, the network has multiple supply temperature options, and we + expect the optimizer to select the lowest feasible supply 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 + - Selection of the lowest feasible supply temperature from available options + - 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 HeatProblemTvarWithTechnoEconomicMixin + + base_folder = Path(run_1a.__file__).resolve().parent.parent + + heat_problem = run_esdl_mesido_optimization( + HeatProblemTvarWithTechnoEconomicMixin, + base_folder=base_folder, + esdl_file_name="1a_with_2producers_and_supply_temperature_options.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 from the options the lowest supply temperature is selected + # for the network supply temperature + np.testing.assert_equal( + 71.0, + results["3625334968694477359_temperature"], + ) + + # 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"], + results["3625334968694477359_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( + results["3625334968694477359_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 @@ -465,6 +544,7 @@ def test_heat_pump_varying_temperature(self): start_time = time.time() a = TestVaryingTemperature() + a.test_1a_heatsource_usage_based_on_varying_supply_temperature() a.test_1a_temperature_variation() a.test_3a_temperature_variation_supply() a.test_3a_temperature_variation_return() From d3b5c8906851076b4ab2ec24f0ee47c5ff1ab407 Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Thu, 28 May 2026 11:26:11 +0200 Subject: [PATCH 2/4] small modification --- tests/test_varying_temperature.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index fc68ebd1b..cd48eca83 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -15,14 +15,13 @@ class TestVaryingTemperature(TestCase): def test_1a_heatsource_usage_based_on_varying_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 + based on the network 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. + temperature that is below the network supply temperature, and the expensive + one has a maximum temperature that is above the network supply temperature. In addition, the network has multiple supply temperature options, and we expect the optimizer to select the lowest feasible supply 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. + We expect the optimizer to use only the expensive heat source. Checks: - Variable operational cost coefficients of heat producers @@ -60,9 +59,11 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): # Check that from the options the lowest supply temperature is selected # for the network supply temperature + carrier_id_number = "3625334968694477359" + temperature_regimes = heat_problem.temperature_regimes(int(carrier_id_number)) np.testing.assert_equal( - 71.0, - results["3625334968694477359_temperature"], + min(temperature_regimes), + results[f"{carrier_id_number}_temperature"], ) # Check that the maximum supply temperature of the cheap heat source @@ -73,7 +74,7 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): ) np.testing.assert_array_less( parameters[f"{rh_cheap_id}.max_temperature"], - results["3625334968694477359_temperature"], + results[f"{carrier_id_number}_temperature"], ) # Check that the maximum supply temperature of the expensive heat source @@ -83,7 +84,7 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): parameters[f"{rh_expensive_id}.max_temperature"], ) np.testing.assert_array_less( - results["3625334968694477359_temperature"], + results[f"{carrier_id_number}_temperature"], parameters[f"{rh_expensive_id}.max_temperature"], ) From d555f0d71eb2afa2b3efe032b5e10aa35a72434c Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Thu, 28 May 2026 14:24:32 +0200 Subject: [PATCH 3/4] list of temperature options are created directly via esdl carries named "Heat_..." --- ...ducers_and_supply_temperature_options.esdl | 1 + tests/models/unit_cases/case_1a/src/run_1a.py | 33 ++++++++++++++++--- tests/test_varying_temperature.py | 5 +-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl b/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl index d366f66c8..7cb04d252 100644 --- a/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl +++ b/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl @@ -150,6 +150,7 @@ + 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 a7aec4a35..054488e61 100644 --- a/tests/models/unit_cases/case_1a/src/run_1a.py +++ b/tests/models/unit_cases/case_1a/src/run_1a.py @@ -115,14 +115,39 @@ def constraints(self, ensemble_member): class HeatProblemTvarWithTechnoEconomicMixin(HeatProblemTvar, TechnoEconomicMixin): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.base_carrier_name = "Heat" + self.base_id_number_mapping = self._get_id_number_mapping_from_carrier_name( + self.base_carrier_name + ) + def temperature_regimes(self, carrier): temperatures = [] - if carrier == 3625334968694477359: - # supply - temperatures = [71.0, 74.0, 86.0] - + if carrier == self.base_id_number_mapping: + temperatures = self._get_related_carrier_temperatures(self.base_carrier_name) return temperatures + def _get_id_number_mapping_from_carrier_name(self, carrier_name: str): + return next( + ( + carrier.get("id_number_mapping") + for carrier in self.esdl_carriers.values() + if carrier.get("name") == carrier_name + ), + None, + ) + + def _get_related_carrier_temperatures(self, carrier_name: str): + return sorted( + carrier["temperature"] + for carrier in self.esdl_carriers.values() + if carrier_name + and carrier.get("name", "").startswith(f"{carrier_name}_") + and carrier["name"][len(f"{carrier_name}_") :].isdigit() + ) + class QTHProblem( _GoalsAndOptions, diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index cd48eca83..41755d1bb 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -19,9 +19,10 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): heat sources, one cheap and one expensive, where the cheap one has a maximum 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. + In addition, the network has multiple supply temperature options, and we expect the optimizer to select the lowest feasible supply temperature. - We expect the optimizer to use only the expensive heat source. Checks: - Variable operational cost coefficients of heat producers @@ -59,7 +60,7 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): # Check that from the options the lowest supply temperature is selected # for the network supply temperature - carrier_id_number = "3625334968694477359" + carrier_id_number = heat_problem._get_id_number_mapping_from_carrier_name("Heat") temperature_regimes = heat_problem.temperature_regimes(int(carrier_id_number)) np.testing.assert_equal( min(temperature_regimes), From de6302592c8446be14b7c5323cf9e6b29c0bb603 Mon Sep 17 00:00:00 2001 From: tolga-akan Date: Fri, 29 May 2026 15:37:55 +0200 Subject: [PATCH 4/4] changelog is updated. branch is updated by main --- CHANGELOG.md | 3 +- tests/models/unit_cases/case_1a/src/run_1a.py | 29 +++++++++++++++---- tests/test_varying_temperature.py | 4 +-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d66d6a8f0..14e55d34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# [Unreleased-main] - 2026-05-13 +# [Unreleased-main] - 2026-05-29 ## 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 varying network supply temperatures ## Changed - Reduced the number of constraints required for headloss calculation with LINEARIZED_N_LINES_EQUALITY setting. 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 d862d59e1..d6128bb9b 100644 --- a/tests/models/unit_cases/case_1a/src/run_1a.py +++ b/tests/models/unit_cases/case_1a/src/run_1a.py @@ -106,20 +106,26 @@ class HeatProblemTvarWithTechnoEconomicMixin(HeatProblemTvar, TechnoEconomicMixi def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.base_carrier_name = "Heat" - self.base_id_number_mapping = self._get_id_number_mapping_from_carrier_name( - self.base_carrier_name - ) + self.base_carrier_id = self._get_id_from_carrier_name(self.base_carrier_name) def temperature_regimes(self, carrier): temperatures = [] - if carrier == self.base_id_number_mapping: + if carrier == self.base_carrier_id: temperatures = self._get_related_carrier_temperatures(self.base_carrier_name) return temperatures - def _get_id_number_mapping_from_carrier_name(self, carrier_name: str): + def _get_id_from_carrier_name(self, carrier_name: str): + """ + Return the id of the carrier with the given name. + + Parameters + ---------- + carrier_name : Name of the carrier for which the id is requested. + """ + return next( ( - carrier.get("id_number_mapping") + carrier.get("id") for carrier in self.esdl_carriers.values() if carrier.get("name") == carrier_name ), @@ -127,6 +133,17 @@ def _get_id_number_mapping_from_carrier_name(self, carrier_name: str): ) def _get_related_carrier_temperatures(self, carrier_name: str): + """ + Return the sorted temperatures of carriers related to the given carrier name. + + A carrier is considered related if its name starts with ``_`` + followed by a numeric suffix, for example ``Heat_70`` or ``Heat_90``. + + Parameters + ---------- + carrier_name : Base name of the carrier group. + """ + return sorted( carrier["temperature"] for carrier in self.esdl_carriers.values() diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py index 51b6ed5bd..7170e0d44 100644 --- a/tests/test_varying_temperature.py +++ b/tests/test_varying_temperature.py @@ -60,8 +60,8 @@ def test_1a_heatsource_usage_based_on_varying_supply_temperature(self): # Check that from the options the lowest supply temperature is selected # for the network supply temperature - carrier_id_number = heat_problem._get_id_number_mapping_from_carrier_name("Heat") - temperature_regimes = heat_problem.temperature_regimes(int(carrier_id_number)) + carrier_id_number = heat_problem._get_id_from_carrier_name("Heat") + temperature_regimes = heat_problem.temperature_regimes(carrier_id_number) np.testing.assert_equal( min(temperature_regimes), results[f"{carrier_id_number}_temperature"],