Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,10 +47,15 @@ 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_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,
heat_transfer_type=HeatTransferAssetType.HEAT_PUMP,
max_electrical_power=max_electrical_power,
)
return contr_heat_transfer

Expand All @@ -76,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
Original file line number Diff line number Diff line change
Expand Up @@ -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_electrical_power=esdl_asset.get_property("power", None),
)
elif hp_ports == 2: # Air to water heat pump case.
heatpump_entity = AirToWaterHeatPump( # type:ignore
Expand Down
6 changes: 5 additions & 1 deletion src/omotes_simulator_core/entities/assets/asset_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,14 @@ 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 [-].
"""

coefficient_of_performance: float = 1 - 1 / 4.0
coefficient_of_performance: float = 4.0


@dataclass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""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 (
Expand All @@ -29,17 +31,42 @@
)


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."""

def __init__(self, name: str, identifier: str, factor: float):
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."""

def __init__(
self,
name: str,
identifier: str,
factor: float,
heat_transfer_type: HeatTransferAssetType,
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 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]]:
"""Method to set the asset to the given heat demand.
Expand Down
77 changes: 69 additions & 8 deletions src/omotes_simulator_core/entities/assets/heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,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
Expand Down Expand Up @@ -66,18 +67,49 @@ class HeatPump(AssetAbstract):
coefficient_of_performance: float
"""Coefficient of perfomance for the heat pump."""

maximum_electrical_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_electrical_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
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_electrical_power: Maximum electrical input power [W].
"""
super().__init__(
asset_name=asset_name,
Expand All @@ -86,14 +118,17 @@ def __init__(
)
# Set the coefficient of performance
self.coefficient_of_performance = coefficient_of_performance
self.maximum_electrical_power = maximum_electrical_power
self._heat_demand_secondary_capped = None
self._power_cap_warning_logged = False

# Define solver asset
self.solver_asset = HeatTransferAsset(
name=self.name,
_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:
Expand Down Expand Up @@ -124,8 +159,27 @@ 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_electrical_power is not None:
required_electric_power = heat_demand_secondary / 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_electrical_power} W is used. ",
extra={"esdl_object_id": self.asset_id},
)
self._power_cap_warning_logged = True
heat_demand_secondary = -capped_heat_demand_secondary
self._heat_demand_secondary_capped = 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,
)
Expand Down Expand Up @@ -190,10 +244,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.
Expand Down Expand Up @@ -227,4 +286,6 @@ def postprocess(self) -> None:

:return: None
"""
pass
# 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
27 changes: 27 additions & 0 deletions src/omotes_simulator_core/entities/network_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -162,6 +165,30 @@ def update_setpoints(self, time: datetime.datetime) -> dict:
for storage in network.storages:
total_heat_supply -= asset_setpoints[storage.id][PROPERTY_HEAT_DEMAND]

for asset in network.heat_transfer_assets_sec:
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:
# Scale down consumers in this network proportionally
scale_factor = max_secondary / requested_secondary
for consumer in network.consumers:
if consumer.id in asset_setpoints:
current = asset_setpoints[consumer.id][PROPERTY_HEAT_DEMAND]
scaled = current * scale_factor
asset_setpoints[consumer.id][PROPERTY_HEAT_DEMAND] = scaled
# Recalculate total_heat_supply after scaling
total_heat_supply = 0
for producer in network.producers:
total_heat_supply -= asset_setpoints[producer.id][PROPERTY_HEAT_DEMAND]
for consumer in network.consumers:
total_heat_supply -= asset_setpoints[consumer.id][PROPERTY_HEAT_DEMAND]
for storage in network.storages:
total_heat_supply -= asset_setpoints[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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
13 changes: 11 additions & 2 deletions unit_test/entities/controller/test_controller_new_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions unit_test/entities/test_heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Loading