diff --git a/CHANGELOG.md b/CHANGELOG.md
index d66d6a8f0..9148ef8c7 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 907101fec..3dc63fe3f 100644
--- a/src/mesido/heat_physics_mixin.py
+++ b/src/mesido/heat_physics_mixin.py
@@ -1427,10 +1427,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_with_2producers.esdl b/tests/models/unit_cases/case_1a/model/1a_with_2producers.esdl
new file mode 100644
index 000000000..3305d78d9
--- /dev/null
+++ b/tests/models/unit_cases/case_1a/model/1a_with_2producers.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 bf6fa8995..76ca22d23 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 HeatProblemWithTechnoEconomicMixin(HeatProblem, TechnoEconomicMixin):
+ pass
+
+
class HeatProblemTvar(HeatProblem):
def energy_system_options(self):
options = super().energy_system_options()
diff --git a/tests/test_heat.py b/tests/test_heat.py
index 96652c71a..e81089070 100644
--- a/tests/test_heat.py
+++ b/tests/test_heat.py
@@ -173,6 +173,76 @@ 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
+ 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
+ - 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