From c708e1e2d5e1637e1fe49a6900c1ed88f3a505e2 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Thu, 11 Jun 2026 17:24:55 +0200 Subject: [PATCH 1/2] Started the implementation of the assets. --- .../adapter/transforms/esdl_asset_mapper.py | 1 + .../esdl_asset_mappers/producer_mapper.py | 32 ++++++- .../adapter/transforms/string_to_esdl.py | 4 + .../entities/assets/elec_boiler.py | 90 ++++++++++++++++++ .../entities/assets/gas_heater.py | 93 +++++++++++++++++++ 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/omotes_simulator_core/entities/assets/elec_boiler.py create mode 100644 src/omotes_simulator_core/entities/assets/gas_heater.py diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py index bda33c94..d0ba11b2 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py @@ -47,6 +47,7 @@ esdl.ResidualHeatSource: EsdlAssetProducerMapper, esdl.SolarCollector: EsdlAssetProducerMapper, esdl.GasHeater: EsdlAssetProducerMapper, + esdl.ElectricBoiler: EsdlAssetProducerMapper, esdl.Consumer: EsdlAssetConsumerMapper, esdl.GenericConsumer: EsdlAssetConsumerMapper, esdl.HeatingDemand: EsdlAssetConsumerMapper, diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/producer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/producer_mapper.py index c365deea..1b483bb9 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/producer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/producer_mapper.py @@ -15,7 +15,10 @@ """Module containing the Esdl to Producer asset mapper class.""" +import esdl + from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.elec_boiler import ElecBoiler from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.entities.assets.production_cluster import ProductionCluster from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract @@ -34,10 +37,29 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: :param EsdlAssetObject esdl_asset: Object to be converted to a producer entity. :return: Producer object. """ - producer_entity = ProductionCluster( - asset_name=esdl_asset.esdl_asset.name, - asset_id=esdl_asset.esdl_asset.id, - port_ids=esdl_asset.get_port_ids(), - ) + + if type(esdl_asset.esdl_asset) == esdl.ElectricBoiler: + producer_entity = ElecBoiler( + asset_name=esdl_asset.esdl_asset.name, + asset_id=esdl_asset.esdl_asset.id, + port_ids=esdl_asset.get_port_ids(), + ) + + elif type(esdl_asset.esdl_asset) == esdl.GasHeater: + # producer_entity = ElecBoiler( # type:ignore + # asset_name=esdl_asset.esdl_asset.name, + # asset_id=esdl_asset.esdl_asset.id, + # port_ids=esdl_asset.get_port_ids(), + # ) + pass # TODO: create and link the gas heater mapper here. + + else: + producer_entity = ProductionCluster( + asset_name=esdl_asset.esdl_asset.name, + asset_id=esdl_asset.esdl_asset.id, + port_ids=esdl_asset.get_port_ids(), + ) + + # TODO: call eboiler or gas heater entities from here. return producer_entity diff --git a/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py b/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py index 83b3519e..72abf9ac 100644 --- a/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py +++ b/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py @@ -34,6 +34,8 @@ class OmotesAssetLabels(str, Enum): ATES = "ates" HEAT_PUMP = "heat_pump" HEAT_EXCHANGER = "heat_exchanger" + ELECTRIC_BOILER = "electric_boiler" + GAS_HEATER = "gas_heater" class StringEsdlAssetMapper: @@ -69,6 +71,8 @@ def __init__(self) -> None: OmotesAssetLabels.ATES: [esdl.ATES], OmotesAssetLabels.HEAT_PUMP: [esdl.HeatPump], OmotesAssetLabels.HEAT_EXCHANGER: [esdl.HeatExchange], + OmotesAssetLabels.ELECTRIC_BOILER: [esdl.ElectricBoiler], + OmotesAssetLabels.GAS_HEATER: [esdl.GasHeater], } self.type_to_label_map: dict[Type[esdl.Asset], OmotesAssetLabels] = { diff --git a/src/omotes_simulator_core/entities/assets/elec_boiler.py b/src/omotes_simulator_core/entities/assets/elec_boiler.py new file mode 100644 index 00000000..3dcb03d2 --- /dev/null +++ b/src/omotes_simulator_core/entities/assets/elec_boiler.py @@ -0,0 +1,90 @@ +# Copyright (c) 2026. Deltares & TNO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""ElecBoiler class.""" +import logging + +from omotes_simulator_core.entities.assets.asset_defaults import ( + PROPERTY_ELECTRICITY_CONSUMPTION, + PROPERTY_HEAT_SUPPLIED, + PROPERTY_HEAT_SUPPLY_SET_POINT, + HeatPumpDefaults, +) +from omotes_simulator_core.entities.assets.production_cluster import ProductionCluster +from omotes_simulator_core.solver.network.assets.production_asset import HeatBoundary + +logger = logging.getLogger(__name__) + + +class ElecBoiler(ProductionCluster): + """An electric boiler asset. + + It represents a two port electric boiler that adds heat to the network by consuming electricity. + """ + + efficiency: float + """The efficiency of the electric boiler [-].""" + + def __init__( + self, + asset_name: str, + asset_id: str, + port_ids: list[str], + efficiency: float = HeatPumpDefaults.coefficient_of_performance, #TODO: Make sure this takes a default of 1.0. + ) -> None: + """ + Initialize the ElecBoiler asset. + + :param str asset_name: The name of the asset. + :param str asset_id: The unique identifier of the asset. + :param List[str] port_ids: List of ids of the connected ports. + """ + super().__init__( + asset_name=asset_name, + asset_id=asset_id, + port_ids=port_ids, + ) + self.efficiency = efficiency + self.solver_asset = HeatBoundary( + name=self.name, + _id=self.asset_id, + pre_scribe_mass_flow=False, + set_pressure=self.pressure_supply, + ) + + def get_electric_power_consumption(self) -> float: + """Calculate the electric power consumption of the electric boiler. + + The electric power consumption is calculated as the ratio between the heat + supplied by the boiler to the network and the efficiency of the boiler. + + :return: float + The electric power consumption of the electric boiler. + """ + + return abs(self.get_actual_heat_supplied()) / self.efficiency + + def write_to_output(self) -> None: + """Method to write time step results to the output dict. + + The output list is a list of dictionaries, where each dictionary + represents the output of the asset for a specific timestep. + """ + output_dict_temp = { + PROPERTY_HEAT_SUPPLY_SET_POINT: self.heat_demand_set_point, + PROPERTY_HEAT_SUPPLIED: self.get_actual_heat_supplied(), + PROPERTY_ELECTRICITY_CONSUMPTION: (self.get_electric_power_consumption()), + } + self.outputs[1][-1].update(output_dict_temp) # Outputs appended to the out port. \ No newline at end of file diff --git a/src/omotes_simulator_core/entities/assets/gas_heater.py b/src/omotes_simulator_core/entities/assets/gas_heater.py new file mode 100644 index 00000000..d69e68d5 --- /dev/null +++ b/src/omotes_simulator_core/entities/assets/gas_heater.py @@ -0,0 +1,93 @@ +# Copyright (c) 2026. Deltares & TNO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""GasHeater class.""" +import logging + +from omotes_simulator_core.entities.assets.asset_defaults import ( + PROPERTY_ELECTRICITY_CONSUMPTION, + PROPERTY_HEAT_SUPPLIED, + PROPERTY_HEAT_SUPPLY_SET_POINT, + HeatPumpDefaults, +) +from omotes_simulator_core.entities.assets.production_cluster import ProductionCluster +from omotes_simulator_core.solver.network.assets.production_asset import HeatBoundary + +logger = logging.getLogger(__name__) + + +class GasHeater(ProductionCluster): + """A gas heater asset. + + It represents a two port gas heater that adds heat to the network by consuming gas. + """ + + efficiency: float + """The efficiency of the gas heater [-].""" + + def __init__( + self, + asset_name: str, + asset_id: str, + port_ids: list[str], + efficiency: float = HeatPumpDefaults.coefficient_of_performance, #TODO: Make sure this takes a default of 1.0. + ) -> None: + """ + Initialize the GasHeater asset. + + :param str asset_name: The name of the asset. + :param str asset_id: The unique identifier of the asset. + :param List[str] port_ids: List of ids of the connected ports. + """ + super().__init__( + asset_name=asset_name, + asset_id=asset_id, + port_ids=port_ids, + ) + self.efficiency = efficiency + self.solver_asset = HeatBoundary( + name=self.name, + _id=self.asset_id, + pre_scribe_mass_flow=False, + set_pressure=self.pressure_supply, + ) + + def get_gas_consumption(self) -> float: + """Calculate the gas power consumption of the gas heater. + + The gas power consumption is calculated as the ratio between the heat + supplied by the heater to the network and the efficiency of the heater. + + :return: float + The gas power consumption of the gas heater. + """ + + # self.Gas_demand_mass_flow / 1000.0 * self.energy_content * self.efficiency + # - self.Heat_source + + return 1.0 # TODO: Fix this, it needs a gas energy content value. Check if it is already used somewhere else. + + def write_to_output(self) -> None: + """Method to write time step results to the output dict. + + The output list is a list of dictionaries, where each dictionary + represents the output of the asset for a specific timestep. + """ + output_dict_temp = { + PROPERTY_HEAT_SUPPLY_SET_POINT: self.heat_demand_set_point, + PROPERTY_HEAT_SUPPLIED: self.get_actual_heat_supplied(), + PROPERTY_ELECTRICITY_CONSUMPTION: (self.get_gas_consumption()), + } + self.outputs[1][-1].update(output_dict_temp) # Outputs appended to the out port. \ No newline at end of file From 42d2e0df8faa87aab4e599ae0138d78552b535ee Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Fri, 26 Jun 2026 16:47:54 +0200 Subject: [PATCH 2/2] Fixed issue with the controller and the temperature setting. Simulation can now run the eboiler. --- .../adapter/transforms/controller_mapper.py | 9 ++++ .../entities/assets/esdl_asset_object.py | 10 ++++ testdata/test1_eboiler.esdl | 49 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 testdata/test1_eboiler.esdl diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py index 672e6550..9abd1ee7 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py @@ -121,8 +121,17 @@ def to_entity( ControllerProducerMapper().to_entity(esdl_asset=esdl_asset) for esdl_asset in esdl_object.get_all_assets_of_type(OmotesAssetLabels.HEAT_PUMP) if esdl_asset.get_number_of_ports() == 2 + ] + [ + ControllerProducerMapper().to_entity(esdl_asset=esdl_asset) + for esdl_asset in esdl_object.get_all_assets_of_type(OmotesAssetLabels.GAS_HEATER) + ] + [ + ControllerProducerMapper().to_entity(esdl_asset=esdl_asset) + for esdl_asset in esdl_object.get_all_assets_of_type(OmotesAssetLabels.ELECTRIC_BOILER) ] + # ELECTRIC_BOILER = "electric_boiler" + # GAS_HEATER = "gas_heater" + storages = self.convert_heat_storages_and_ates(esdl_object) # if there are no heat transfer assets, all assets can be stored into one network. diff --git a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py index f1d1902b..8a3019ad 100644 --- a/src/omotes_simulator_core/entities/assets/esdl_asset_object.py +++ b/src/omotes_simulator_core/entities/assets/esdl_asset_object.py @@ -186,6 +186,16 @@ def get_temperatures_asset(self, side: str | None = None) -> Temperatures: in_flow=self.get_temperature("In", "Return"), out_flow=self.get_temperature("Out", "Supply"), ) + elif self.get_esdl_type() == OmotesAssetLabels.GAS_HEATER: + temperatures = Temperatures( + in_flow=self.get_temperature("In", "Return"), + out_flow=self.get_temperature("Out", "Supply"), + ) + elif self.get_esdl_type() == OmotesAssetLabels.ELECTRIC_BOILER: + temperatures = Temperatures( + in_flow=self.get_temperature("In", "Return"), + out_flow=self.get_temperature("Out", "Supply"), + ) elif (self.get_esdl_type() == OmotesAssetLabels.ATES) | ( self.get_esdl_type() == OmotesAssetLabels.STORAGE ): diff --git a/testdata/test1_eboiler.esdl b/testdata/test1_eboiler.esdl new file mode 100644 index 00000000..bb6a587a --- /dev/null +++ b/testdata/test1_eboiler.esdl @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +