From 30d66bd9111371a6d03e2144358a252cefe2ef06 Mon Sep 17 00:00:00 2001 From: Ashraf Almohagry Date: Fri, 13 Feb 2026 16:48:37 +0100 Subject: [PATCH 1/5] Maximum power input for Heatpump --- .../adapter/transforms/controller_mapper.py | 2 +- .../controller_heat_transfer_mapper.py | 2 + .../esdl_asset_mappers/heat_pump_mapper.py | 1 + .../entities/assets/asset_defaults.py | 2 +- .../controller/controller_heat_transfer.py | 24 ++++++- .../entities/assets/heat_pump.py | 66 +++++++++++++++++-- .../entities/network_controller.py | 23 +++++++ .../network/assets/heat_transfer_asset.py | 7 +- unit_test/entities/test_heat_pump.py | 8 +-- .../assets/test_heat_transfer_asset.py | 6 +- 10 files changed, 120 insertions(+), 21 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py index 7ceec108..e1d06715 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py @@ -231,7 +231,7 @@ def heat_transfer_assets_to_network( belongs = False for network in network_list: if belongs_to_network(heat_transfer_asset.id + "_secondary", network, graph): - network.heat_transfer_primary.append(heat_transfer_asset) + network.heat_transfer_secondary.append(heat_transfer_asset) belongs = True break if not belongs: diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py index 93ebd9c7..a97a1a05 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py @@ -46,10 +46,12 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerHeatTransferAsset: coefficient_of_performance = esdl_asset.get_property( esdl_property_name="COP", default_value=HeatPumpDefaults.coefficient_of_performance ) + max_power = esdl_asset.get_property(esdl_property_name="power", default_value=None) contr_heat_transfer = ControllerHeatTransferAsset( name=esdl_asset.esdl_asset.name, identifier=esdl_asset.esdl_asset.id, factor=coefficient_of_performance, + max_power=max_power, ) return contr_heat_transfer diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py index 240f3e0d..4bfed0e5 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py @@ -44,6 +44,7 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: coefficient_of_performance=esdl_asset.get_property( "COP", HeatPumpDefaults.coefficient_of_performance ), + maximum_power=esdl_asset.get_property("power", None), ) elif hp_ports == 2: # Air to water heat pump case. heatpump_entity = AirToWaterHeatPump( # type:ignore diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index 12dbad0d..e47bcae3 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -91,7 +91,7 @@ class HeatPumpDefaults: :param float coefficient_of_performance: The coefficient of performance of the heat pump [-]. """ - coefficient_of_performance: float = 1 - 1 / 4.0 + coefficient_of_performance: float = 4.0 @dataclass diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index d5dbf9fb..05febea5 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -30,14 +30,36 @@ class ControllerHeatTransferAsset(AssetControllerAbstract): """Class for controlling a heat transfer asset.""" - def __init__(self, name: str, identifier: str, factor: float): + max_power: float | None + """Maximum electrical power of the heat pump [W]. None means no limit.""" + + def __init__( + self, + name: str, + identifier: str, + factor: float, + max_power: float | None = None, + ): """Constructor of the class, which sets all attributes. :param str name: Name of the consumer. :param str identifier: Unique identifier of the consumer. + :param float factor: The COP (heat pump) or efficiency (heat exchanger) factor. + :param float | None max_power: Maximum electrical power for heat pump [W]. """ super().__init__(name, identifier) self.factor = factor + self.max_power = max_power + + def get_max_secondary_power(self) -> float | None: + """Get the maximum secondary (hot) side power output. + + For heat pump: max_secondary = max_electrical_power * COP. + :return: Maximum secondary power [W], or None if no limit. + """ + if self.max_power is None: + return None + return self.max_power * self.factor def set_asset(self, heat_demand: float) -> dict[str, dict[str, float]]: """Method to set the asset to the given heat demand. diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index 7ea10fe2..fbb01ebe 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -20,6 +20,7 @@ from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( DEFAULT_PRESSURE, + HeatPumpDefaults, PRIMARY, PROPERTY_ELECTRICITY_CONSUMPTION, PROPERTY_HEAT_DEMAND, @@ -66,18 +67,49 @@ class HeatPump(AssetAbstract): coefficient_of_performance: float """Coefficient of perfomance for the heat pump.""" + maximum_power: float | None + """Maximum electrical input power of the heat pump [W]. """ + + _heat_demand_secondary_capped: float | None + """Capped secondary heat demand setpoint [W].""" + def __init__( self, asset_name: str, asset_id: str, connected_ports: list[str], - coefficient_of_performance: float = 1 - 1 / 4.0, + coefficient_of_performance: float = HeatPumpDefaults.coefficient_of_performance, + maximum_power: float | None = None, ) -> None: """Initialize a new HeatPump instance. + The heat transfer coefficient of the heat pump is derived from the coefficient of + performance using the relationship between the cold side (primary side) and hot + side (secondary side). + + **Relationship between the COP and the heat transfer coefficient as defined in the heat + transfer asset:** + + Starting from the energy balance and COP definition: + + .. math:: + + Q_{hot} = W_{el} + Q_{cold}\\ + c = \frac{Q_{cold}}{Q_{hot}} \\ + W_{el} = \frac{Q_{hot}}{COP}\\ + Q_{hot} = \frac{Q_{hot}}{COP} + Q_{cold}\\ + 1 = \frac{1}{COP} + c\\ + c = 1 - \frac{1}{COP} + + The dimensionless ratio :math:`c = Q_{cold}/Q_{hot}` is used in the heat transfer asset as + the heat transfer coefficient to couple the primary and secondary sides. + + :param asset_name: The name of the asset. :param asset_id: The unique identifier of the asset. :connected_ports: The unique identifiers of the ports of the asset. + :param coefficient_of_performance: The COP of the heat pump [-]. + :param maximum_power: Maximum electrical input power [W]. """ super().__init__( asset_name=asset_name, @@ -86,6 +118,8 @@ def __init__( ) # Set the coefficient of performance self.coefficient_of_performance = coefficient_of_performance + self.maximum_power = maximum_power + self._heat_demand_secondary_capped = None # Define solver asset self.solver_asset = HeatTransferAsset( @@ -93,7 +127,7 @@ def __init__( _id=self.asset_id, pre_scribe_mass_flow_secondary=False, pressure_set_point_secondary=DEFAULT_PRESSURE, - heat_transfer_coefficient=self.coefficient_of_performance, + heat_transfer_coefficient=1.0 - 1.0 / self.coefficient_of_performance, ) def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: @@ -124,8 +158,23 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: # Assign setpoints to the HeatPump asset self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_OUT] + heat_demand_secondary = setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] + + # Limit heat demand based on maximum electrical power cap + if self.maximum_power is not None: + required_electric_power = heat_demand_secondary / self.coefficient_of_performance + if required_electric_power > self.maximum_power: + capped_heat_demand_secondary = self.maximum_power * self.coefficient_of_performance + logger.warning( + f"maximum electrical power of heat pump {self.name} exceeded. " + f"maximum power input {self.maximum_power} W is used. ", + extra={"esdl_object_id": self.asset_id}, + ) + heat_demand_secondary = capped_heat_demand_secondary + + self._heat_demand_secondary_capped = heat_demand_secondary self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( - thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND], + thermal_demand=heat_demand_secondary, temperature_in=self.temperature_in_secondary, temperature_out=self.temperature_out_secondary, ) @@ -190,10 +239,15 @@ def set_setpoints(self, setpoints: Dict) -> None: :param Dict setpoints: The setpoints that should be set for the asset. The keys of the dictionary are the names of the setpoints and the values are the values """ - # Set the setpoints for the primary side of the heat pump - self._set_setpoints_primary(setpoints_primary=setpoints) - # Set the setpoints for the secondary side of the heat pump + # Set the secondary side first to apply electrical power capping if necessary. self._set_setpoints_secondary(setpoints_secondary=setpoints) + # Set primary setpoint based on capped secondary side using c=Q_cold/Q_hot = 1 - 1/COP. + setpoints_primary = dict(setpoints) + if self._heat_demand_secondary_capped is not None: + setpoints_primary[PRIMARY + PROPERTY_HEAT_DEMAND] = ( + self._heat_demand_secondary_capped * (1.0 - 1.0 / self.coefficient_of_performance) + ) + self._set_setpoints_primary(setpoints_primary=setpoints_primary) def write_to_output(self) -> None: """Get output power and electricity consumption of the asset. diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 52f8c2ae..ba6a2c96 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -141,6 +141,29 @@ def update_setpoints(self, time: datetime.datetime) -> dict: total_heat_supply -= producers[consumer.id][PROPERTY_HEAT_DEMAND] for storage in network.storages: total_heat_supply += producers[storage.id][PROPERTY_HEAT_DEMAND] + + # Check if heat transfer asset has power limit (secondary side) + for asset in network.heat_transfer_assets_sec: + max_secondary = asset.get_max_secondary_power() + if max_secondary is not None: + requested_secondary = abs(total_heat_supply) + if requested_secondary > max_secondary: + # Scale down consumers in this network proportionally + scale_factor = max_secondary / requested_secondary + for consumer in network.consumers: + if consumer.id in producers: + current = producers[consumer.id][PROPERTY_HEAT_DEMAND] + scaled = current * scale_factor + producers[consumer.id][PROPERTY_HEAT_DEMAND] = scaled + # Recalculate total_heat_supply after scaling + total_heat_supply = 0 + for producer in network.producers: + total_heat_supply += producers[producer.id][PROPERTY_HEAT_DEMAND] + for consumer in network.consumers: + total_heat_supply -= producers[consumer.id][PROPERTY_HEAT_DEMAND] + for storage in network.storages: + total_heat_supply += producers[storage.id][PROPERTY_HEAT_DEMAND] + # this might look weird, but we know there is only one primary or secondary asset. # So we can directly set it. for asset in network.heat_transfer_assets_prim: diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index 8f17d98f..1dd60dc7 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -691,12 +691,9 @@ def get_electric_power_consumption(self) -> float: """Calculate the electric power consumption of the heat transfer asset. The electric power consumption is calculated as the absolute difference between the - heat power on the primary and secondary side, divided by the heat transfer coefficient. + heat power on the primary and secondary side. :return: float The electric power consumption of the heat transfer asset. """ - return ( - abs(self.get_heat_power_primary() - self.get_heat_power_secondary()) - / self.heat_transfer_coefficient - ) + return abs(abs(self.get_heat_power_primary()) - abs(self.get_heat_power_secondary())) diff --git a/unit_test/entities/test_heat_pump.py b/unit_test/entities/test_heat_pump.py index c491964f..8d6cbd76 100644 --- a/unit_test/entities/test_heat_pump.py +++ b/unit_test/entities/test_heat_pump.py @@ -57,7 +57,7 @@ def setUp(self) -> None: self.heat_pump.solver_asset.get_index_matrix( property_name="internal_energy", connection_point=0, use_relative_indexing=False ) - ] = 5.0 + ] = 10.0 self.heat_pump.solver_asset.prev_sol[ self.heat_pump.solver_asset.get_index_matrix( property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False @@ -68,7 +68,7 @@ def setUp(self) -> None: self.heat_pump.solver_asset.get_index_matrix( property_name="internal_energy", connection_point=1, use_relative_indexing=False ) - ] = 10.0 + ] = 5.0 self.heat_pump.solver_asset.prev_sol[ self.heat_pump.solver_asset.get_index_matrix( @@ -222,6 +222,6 @@ def test_write_to_output(self): self.heat_pump.write_to_output() # Assert - self.assertEqual(self.heat_pump.outputs[1][-1][PROPERTY_HEAT_POWER_PRIMARY], 10.0) - self.assertEqual(self.heat_pump.outputs[1][-1][PROPERTY_ELECTRICITY_CONSUMPTION], 1.0) + self.assertEqual(self.heat_pump.outputs[1][-1][PROPERTY_HEAT_POWER_PRIMARY], -10.0) + self.assertEqual(self.heat_pump.outputs[1][-1][PROPERTY_ELECTRICITY_CONSUMPTION], 5.0) self.assertEqual(self.heat_pump.outputs[0][-1][PROPERTY_HEAT_POWER_SECONDARY], 5.0) diff --git a/unit_test/solver/network/assets/test_heat_transfer_asset.py b/unit_test/solver/network/assets/test_heat_transfer_asset.py index 691cdf80..2b09fc6d 100644 --- a/unit_test/solver/network/assets/test_heat_transfer_asset.py +++ b/unit_test/solver/network/assets/test_heat_transfer_asset.py @@ -455,12 +455,12 @@ def test_get_electric_power_consumption(self): self.asset.get_index_matrix( property_name="internal_energy", connection_point=0, use_relative_indexing=False ) - ] = 5.0 + ] = 10.0 self.asset.prev_sol[ self.asset.get_index_matrix( property_name="internal_energy", connection_point=1, use_relative_indexing=False ) - ] = 10.0 + ] = 5.0 self.asset.prev_sol[ self.asset.get_index_matrix( property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False @@ -488,4 +488,4 @@ def test_get_electric_power_consumption(self): electric_power = self.asset.get_electric_power_consumption() # Assert - self.assertEqual(electric_power, 1.0) + self.assertEqual(electric_power, 5.0) From 6dc1b6c6e66f290a469f946ef2177d889ad97560 Mon Sep 17 00:00:00 2001 From: Ashraf Almohagry Date: Fri, 13 Feb 2026 17:00:54 +0100 Subject: [PATCH 2/5] updated lint and formatting. --- src/omotes_simulator_core/entities/assets/heat_pump.py | 4 ++-- unit_test/entities/test_air_to_water_heat_pump.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index fbb01ebe..914484bb 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -20,7 +20,6 @@ from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( DEFAULT_PRESSURE, - HeatPumpDefaults, PRIMARY, PROPERTY_ELECTRICITY_CONSUMPTION, PROPERTY_HEAT_DEMAND, @@ -30,6 +29,7 @@ PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, SECONDARY, + HeatPumpDefaults, ) from omotes_simulator_core.entities.assets.utils import heat_demand_and_temperature_to_mass_flow from omotes_simulator_core.solver.network.assets.heat_transfer_asset import HeatTransferAsset @@ -81,7 +81,7 @@ def __init__( coefficient_of_performance: float = HeatPumpDefaults.coefficient_of_performance, maximum_power: float | None = None, ) -> None: - """Initialize a new HeatPump instance. + r"""Initialize a new HeatPump instance. The heat transfer coefficient of the heat pump is derived from the coefficient of performance using the relationship between the cold side (primary side) and hot diff --git a/unit_test/entities/test_air_to_water_heat_pump.py b/unit_test/entities/test_air_to_water_heat_pump.py index 70301a44..9eacb5fe 100644 --- a/unit_test/entities/test_air_to_water_heat_pump.py +++ b/unit_test/entities/test_air_to_water_heat_pump.py @@ -281,6 +281,7 @@ def get_mass_flow_rate(_, i: int): def test_get_electric_consumption(self): """Test getting the electric power consumed by the heatpump.""" + # Arrange def get_internal_energy(_, i: int): if i == 0: From 50f01745ae3b731d0fba3b01edbab8daff5fff46 Mon Sep 17 00:00:00 2001 From: Ashraf Almohagry Date: Fri, 13 Feb 2026 17:10:29 +0100 Subject: [PATCH 3/5] fixed lint . --- unit_test/entities/test_air_to_water_heat_pump.py | 1 - 1 file changed, 1 deletion(-) diff --git a/unit_test/entities/test_air_to_water_heat_pump.py b/unit_test/entities/test_air_to_water_heat_pump.py index 9eacb5fe..70301a44 100644 --- a/unit_test/entities/test_air_to_water_heat_pump.py +++ b/unit_test/entities/test_air_to_water_heat_pump.py @@ -281,7 +281,6 @@ def get_mass_flow_rate(_, i: int): def test_get_electric_consumption(self): """Test getting the electric power consumed by the heatpump.""" - # Arrange def get_internal_energy(_, i: int): if i == 0: From b4919e8ddef9e5ede82406e5730ccabaa2ccb1be Mon Sep 17 00:00:00 2001 From: Ashraf Almohagry Date: Thu, 19 Feb 2026 13:19:20 +0100 Subject: [PATCH 4/5] updated based on reviewer's comments. --- .../controller_heat_transfer_mapper.py | 6 +++-- .../esdl_asset_mappers/heat_pump_mapper.py | 2 +- .../entities/assets/asset_defaults.py | 4 ++++ .../controller/controller_heat_transfer.py | 18 ++++----------- .../entities/assets/heat_pump.py | 23 +++++++++++-------- .../entities/network_controller.py | 6 ++--- .../assets/test_heat_transfer_asset.py | 5 ++-- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py index a97a1a05..06ce4253 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py @@ -46,12 +46,14 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerHeatTransferAsset: coefficient_of_performance = esdl_asset.get_property( esdl_property_name="COP", default_value=HeatPumpDefaults.coefficient_of_performance ) - max_power = esdl_asset.get_property(esdl_property_name="power", default_value=None) + max_electrical_power = esdl_asset.get_property( + esdl_property_name="power", default_value=None + ) contr_heat_transfer = ControllerHeatTransferAsset( name=esdl_asset.esdl_asset.name, identifier=esdl_asset.esdl_asset.id, factor=coefficient_of_performance, - max_power=max_power, + max_electrical_power=max_electrical_power, ) return contr_heat_transfer diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py index 4bfed0e5..27a3ba91 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_pump_mapper.py @@ -44,7 +44,7 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: coefficient_of_performance=esdl_asset.get_property( "COP", HeatPumpDefaults.coefficient_of_performance ), - maximum_power=esdl_asset.get_property("power", None), + maximum_electrical_power=esdl_asset.get_property("power", None), ) elif hp_ports == 2: # Air to water heat pump case. heatpump_entity = AirToWaterHeatPump( # type:ignore diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index f2268ee1..da075063 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -88,6 +88,10 @@ class AtesDefaults: class HeatPumpDefaults: """Class containing the default values for a heat pump. + The Coefficient of Performance (COP) is defined as the ratio of heat output to electricity + input: COP = Q_heat_output / electricity_in. For example, a COP of 4 means 1 unit of electricity + produces 4 units of heat by extracting 3 units from the source. + :param float coefficient_of_performance: The coefficient of performance of the heat pump [-]. """ diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index 91f4fb1a..585accc1 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -32,7 +32,7 @@ class ControllerHeatTransferAsset(AssetControllerAbstract): """Class for controlling a heat transfer asset.""" - max_power: float | None + max_electrical_power: float | None """Maximum electrical power of the heat pump [W]. None means no limit.""" def __init__( @@ -40,28 +40,18 @@ def __init__( name: str, identifier: str, factor: float, - max_power: float | None = None, + max_electrical_power: float | None = None, ): """Constructor of the class, which sets all attributes. :param str name: Name of the consumer. :param str identifier: Unique identifier of the consumer. :param float factor: The COP (heat pump) or efficiency (heat exchanger) factor. - :param float | None max_power: Maximum electrical power for heat pump [W]. + :param float | None max_electrical_power: Maximum electrical power for heat pump [W]. """ super().__init__(name, identifier) self.factor = factor - self.max_power = max_power - - def get_max_secondary_power(self) -> float | None: - """Get the maximum secondary (hot) side power output. - - For heat pump: max_secondary = max_electrical_power * COP. - :return: Maximum secondary power [W], or None if no limit. - """ - if self.max_power is None: - return None - return self.max_power * self.factor + self.max_electrical_power = max_electrical_power def set_asset(self, heat_demand: float) -> dict[str, dict[str, float]]: """Method to set the asset to the given heat demand. diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index fd042623..034e8ec7 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -67,7 +67,7 @@ class HeatPump(AssetAbstract): coefficient_of_performance: float """Coefficient of perfomance for the heat pump.""" - maximum_power: float | None + maximum_electrical_power: float | None """Maximum electrical input power of the heat pump [W]. """ _heat_demand_secondary_capped: float | None @@ -79,7 +79,7 @@ def __init__( asset_id: str, connected_ports: list[str], coefficient_of_performance: float = HeatPumpDefaults.coefficient_of_performance, - maximum_power: float | None = None, + maximum_electrical_power: float | None = None, ) -> None: r"""Initialize a new HeatPump instance. @@ -109,7 +109,7 @@ def __init__( :param asset_id: The unique identifier of the asset. :connected_ports: The unique identifiers of the ports of the asset. :param coefficient_of_performance: The COP of the heat pump [-]. - :param maximum_power: Maximum electrical input power [W]. + :param maximum_electrical_power: Maximum electrical input power [W]. """ super().__init__( asset_name=asset_name, @@ -118,7 +118,7 @@ def __init__( ) # Set the coefficient of performance self.coefficient_of_performance = coefficient_of_performance - self.maximum_power = maximum_power + self.maximum_electrical_power = maximum_electrical_power self._heat_demand_secondary_capped = None self._power_cap_warning_logged = False @@ -162,18 +162,20 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: heat_demand_secondary = setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] # Limit heat demand based on maximum electrical power cap - if self.maximum_power is not None: + if self.maximum_electrical_power is not None: required_electric_power = heat_demand_secondary / self.coefficient_of_performance - if abs(required_electric_power) > self.maximum_power: - capped_heat_demand_secondary = self.maximum_power * self.coefficient_of_performance + if abs(required_electric_power) > self.maximum_electrical_power: + capped_heat_demand_secondary = ( + self.maximum_electrical_power * self.coefficient_of_performance + ) if not self._power_cap_warning_logged: logger.warning( f"Maximum electrical power exceeded for heat pump: {self.name}. " - f"Maximum electrical power of {self.maximum_power} W is used. ", + f"Maximum electrical power of {self.maximum_electrical_power} W is used. ", extra={"esdl_object_id": self.asset_id}, ) self._power_cap_warning_logged = True - heat_demand_secondary = -abs(capped_heat_demand_secondary) + heat_demand_secondary = -capped_heat_demand_secondary self._heat_demand_secondary_capped = heat_demand_secondary self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( @@ -284,4 +286,5 @@ def postprocess(self) -> None: :return: None """ - pass + # Reset the power cap warning flag + self._power_cap_warning_logged = False diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 3584e4eb..1534b91a 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -162,10 +162,10 @@ def update_setpoints(self, time: datetime.datetime) -> dict: for storage in network.storages: total_heat_supply -= asset_setpoints[storage.id][PROPERTY_HEAT_DEMAND] - # Check if heat transfer asset (HeatPump) has power limit (secondary side) for asset in network.heat_transfer_assets_sec: - max_secondary = asset.get_max_secondary_power() - if max_secondary is not None: + # Heat transfer asset is a heat pump if electrical power limit is set + if asset.max_electrical_power is not None: + max_secondary = asset.max_electrical_power * asset.factor requested_secondary = abs(total_heat_supply) if requested_secondary > max_secondary: # Scale down consumers in this network proportionally diff --git a/unit_test/solver/network/assets/test_heat_transfer_asset.py b/unit_test/solver/network/assets/test_heat_transfer_asset.py index 2b09fc6d..c23118da 100644 --- a/unit_test/solver/network/assets/test_heat_transfer_asset.py +++ b/unit_test/solver/network/assets/test_heat_transfer_asset.py @@ -449,7 +449,6 @@ def test_get_heat_power_secondary(self): def test_get_electric_power_consumption(self): """Test get_electric_power_consumption method.""" # Arrange - self.asset.heat_transfer_coefficient = 5.0 # --- Primary side self.asset.prev_sol[ self.asset.get_index_matrix( @@ -472,12 +471,12 @@ def test_get_electric_power_consumption(self): self.asset.get_index_matrix( property_name="internal_energy", connection_point=2, use_relative_indexing=False ) - ] = 15.0 + ] = 20.0 self.asset.prev_sol[ self.asset.get_index_matrix( property_name="internal_energy", connection_point=3, use_relative_indexing=False ) - ] = 20.0 + ] = 15.0 self.asset.prev_sol[ self.asset.get_index_matrix( property_name="mass_flow_rate", connection_point=2, use_relative_indexing=False From 87e4e58f744c68905df250a6d07cd130ae659f86 Mon Sep 17 00:00:00 2001 From: Ashraf Almohagry Date: Thu, 19 Feb 2026 15:39:44 +0100 Subject: [PATCH 5/5] added heatpump type for use in the network conroller. --- .../controller_heat_transfer_mapper.py | 3 +++ .../assets/controller/controller_heat_transfer.py | 15 +++++++++++++++ .../entities/assets/heat_pump.py | 5 +++-- .../entities/network_controller.py | 9 +++++++-- .../controller/test_controller_new_class.py | 13 +++++++++++-- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py index 06ce4253..829cadf1 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_heat_transfer_mapper.py @@ -24,6 +24,7 @@ ) from omotes_simulator_core.entities.assets.controller.controller_heat_transfer import ( ControllerHeatTransferAsset, + HeatTransferAssetType, ) from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract @@ -53,6 +54,7 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerHeatTransferAsset: name=esdl_asset.esdl_asset.name, identifier=esdl_asset.esdl_asset.id, factor=coefficient_of_performance, + heat_transfer_type=HeatTransferAssetType.HEAT_PUMP, max_electrical_power=max_electrical_power, ) return contr_heat_transfer @@ -80,5 +82,6 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerHeatTransferAsset: name=esdl_asset.esdl_asset.name, identifier=esdl_asset.esdl_asset.id, factor=coefficient_of_performance, + heat_transfer_type=HeatTransferAssetType.HEAT_EXCHANGER, ) return contr_heat_transfer diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index 585accc1..dc81aa9d 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -14,6 +14,8 @@ # along with this program. If not, see . """Module containing the class for a heat trasnfer asset.""" +from enum import Enum + import numpy as np from omotes_simulator_core.entities.assets.asset_defaults import ( @@ -29,9 +31,19 @@ ) +class HeatTransferAssetType(Enum): + """Enum to distinguish heat transfer asset types.""" + + HEAT_PUMP = "heat_pump" + HEAT_EXCHANGER = "heat_exchanger" + + class ControllerHeatTransferAsset(AssetControllerAbstract): """Class for controlling a heat transfer asset.""" + heat_transfer_type: HeatTransferAssetType + """Type of heat transfer asset (heat pump or heat exchanger).""" + max_electrical_power: float | None """Maximum electrical power of the heat pump [W]. None means no limit.""" @@ -40,6 +52,7 @@ def __init__( name: str, identifier: str, factor: float, + heat_transfer_type: HeatTransferAssetType, max_electrical_power: float | None = None, ): """Constructor of the class, which sets all attributes. @@ -47,10 +60,12 @@ def __init__( :param str name: Name of the consumer. :param str identifier: Unique identifier of the consumer. :param float factor: The COP (heat pump) or efficiency (heat exchanger) factor. + :param HeatTransferAssetType heat_transfer_type: Type of heat transfer asset. :param float | None max_electrical_power: Maximum electrical power for heat pump [W]. """ super().__init__(name, identifier) self.factor = factor + self.heat_transfer_type = heat_transfer_type self.max_electrical_power = max_electrical_power def set_asset(self, heat_demand: float) -> dict[str, dict[str, float]]: diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index 034e8ec7..62ce88dd 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -176,8 +176,8 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: ) self._power_cap_warning_logged = True heat_demand_secondary = -capped_heat_demand_secondary + self._heat_demand_secondary_capped = capped_heat_demand_secondary - self._heat_demand_secondary_capped = heat_demand_secondary self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( thermal_demand=heat_demand_secondary, temperature_in=self.temperature_in_secondary, @@ -286,5 +286,6 @@ def postprocess(self) -> None: :return: None """ - # Reset the power cap warning flag + # Reset the power cap warning flag and heat demand cap for the next time step self._power_cap_warning_logged = False + self._heat_demand_secondary_capped = None diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 1534b91a..1e647097 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -23,6 +23,9 @@ PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, ) +from omotes_simulator_core.entities.assets.controller.controller_heat_transfer import ( + HeatTransferAssetType, +) from omotes_simulator_core.entities.assets.controller.controller_network import ControllerNetwork from omotes_simulator_core.entities.heat_network import HeatNetwork from omotes_simulator_core.entities.network_controller_abstract import NetworkControllerAbstract @@ -163,8 +166,10 @@ def update_setpoints(self, time: datetime.datetime) -> dict: total_heat_supply -= asset_setpoints[storage.id][PROPERTY_HEAT_DEMAND] for asset in network.heat_transfer_assets_sec: - # Heat transfer asset is a heat pump if electrical power limit is set - if asset.max_electrical_power is not None: + if ( + asset.heat_transfer_type == HeatTransferAssetType.HEAT_PUMP + and asset.max_electrical_power is not None + ): max_secondary = asset.max_electrical_power * asset.factor requested_secondary = abs(total_heat_supply) if requested_secondary > max_secondary: diff --git a/unit_test/entities/controller/test_controller_new_class.py b/unit_test/entities/controller/test_controller_new_class.py index aaf724d7..8cef8290 100644 --- a/unit_test/entities/controller/test_controller_new_class.py +++ b/unit_test/entities/controller/test_controller_new_class.py @@ -25,6 +25,7 @@ from omotes_simulator_core.entities.assets.controller.controller_consumer import ControllerConsumer from omotes_simulator_core.entities.assets.controller.controller_heat_transfer import ( ControllerHeatTransferAsset, + HeatTransferAssetType, ) from omotes_simulator_core.entities.assets.controller.controller_network import ControllerNetwork from omotes_simulator_core.entities.assets.controller.controller_producer import ControllerProducer @@ -171,9 +172,17 @@ def setup_update_set_points(self): self.storage1.temperature_in = 40 # Create heat transfer assets - heatpump = ControllerHeatTransferAsset(name="heatpump1", identifier="heatpump1", factor=5.0) + heatpump = ControllerHeatTransferAsset( + name="heatpump1", + identifier="heatpump1", + factor=5.0, + heat_transfer_type=HeatTransferAssetType.HEAT_PUMP, + ) heatpump2 = ControllerHeatTransferAsset( - name="heatpump2", identifier="heatpump2", factor=1.0 + name="heatpump2", + identifier="heatpump2", + factor=1.0, + heat_transfer_type=HeatTransferAssetType.HEAT_PUMP, ) # Build basic network structure