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