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/src/mesido/heat_physics_mixin.py b/src/mesido/heat_physics_mixin.py
index 907101fec..18a7e999d 100644
--- a/src/mesido/heat_physics_mixin.py
+++ b/src/mesido/heat_physics_mixin.py
@@ -1441,9 +1441,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..7cb04d252
--- /dev/null
+++ b/tests/models/unit_cases/case_1a/model/1a_with_2producers_and_supply_temperature_options.esdl
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 bf6fa8995..d6128bb9b 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
@@ -100,6 +101,58 @@ def constraints(self, ensemble_member):
return constraints
+class HeatProblemTvarWithTechnoEconomicMixin(HeatProblemTvar, TechnoEconomicMixin):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.base_carrier_name = "Heat"
+ self.base_carrier_id = self._get_id_from_carrier_name(self.base_carrier_name)
+
+ def temperature_regimes(self, carrier):
+ temperatures = []
+ if carrier == self.base_carrier_id:
+ temperatures = self._get_related_carrier_temperatures(self.base_carrier_name)
+ return temperatures
+
+ 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")
+ for carrier in self.esdl_carriers.values()
+ if carrier.get("name") == carrier_name
+ ),
+ None,
+ )
+
+ 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()
+ if carrier_name
+ and carrier.get("name", "").startswith(f"{carrier_name}_")
+ and carrier["name"][len(f"{carrier_name}_") :].isdigit()
+ )
+
+
class QTHProblem(
_GoalsAndOptions,
QTHMixin,
diff --git a/tests/test_varying_temperature.py b/tests/test_varying_temperature.py
index 927037068..7170e0d44 100644
--- a/tests/test_varying_temperature.py
+++ b/tests/test_varying_temperature.py
@@ -12,6 +12,87 @@
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 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
+ 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.
+
+ 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
+ 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"],
+ )
+
+ # 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[f"{carrier_id_number}_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[f"{carrier_id_number}_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
@@ -475,6 +556,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()