From c708e1e2d5e1637e1fe49a6900c1ed88f3a505e2 Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Thu, 11 Jun 2026 17:24:55 +0200 Subject: [PATCH 1/3] 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/3] 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 46326061d6ef984522f190b6c98628f54d72d36d Mon Sep 17 00:00:00 2001 From: Santiago Patterson Date: Wed, 8 Jul 2026 17:21:10 +0200 Subject: [PATCH 3/3] Final parts of the code. Needs testing, write tests and cleaning up. --- .../esdl_asset_mappers/producer_mapper.py | 15 +++++------ .../entities/assets/asset_defaults.py | 2 ++ .../entities/assets/elec_boiler.py | 11 +------- .../entities/assets/gas_heater.py | 25 +++++++------------ 4 files changed, 18 insertions(+), 35 deletions(-) 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 1b483bb9..d87a7621 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 @@ -19,6 +19,7 @@ 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.gas_heater import GasHeater 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 @@ -46,13 +47,11 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: ) 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. - + producer_entity = GasHeater( + asset_name=esdl_asset.esdl_asset.name, + asset_id=esdl_asset.esdl_asset.id, + port_ids=esdl_asset.get_port_ids(), + ) else: producer_entity = ProductionCluster( asset_name=esdl_asset.esdl_asset.name, @@ -60,6 +59,4 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: 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/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index 264fe722..b2d5f6be 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -28,6 +28,7 @@ DEFAULT_POWER = 500000.0 # [W] DEFAULT_MISSING_VALUE = -9999.99 # [-] DEFAULT_ROUGHNESS = 1e-3 # [m] +DEFAULT_GAS_ENERGY_CONTENT = 31.68 * 10.0**6 # [J/Nm3] @dataclass @@ -147,6 +148,7 @@ class HeatBufferDefaults: PROPERTY_HEAT_POWER_PRIMARY = "heat_power_primary" PROPERTY_HEAT_POWER_SECONDARY = "heat_power_secondary" PROPERTY_ELECTRICITY_CONSUMPTION = "electricity_consumption" +PROPERTY_GAS_CONSUMPTION = "gas_consumption" PROPERTY_VOLUME = "volume" PROPERTY_FILL_LEVEL = "fill_level" PROPERTY_TIMESTEP = "time_step" diff --git a/src/omotes_simulator_core/entities/assets/elec_boiler.py b/src/omotes_simulator_core/entities/assets/elec_boiler.py index 3dcb03d2..090f00cb 100644 --- a/src/omotes_simulator_core/entities/assets/elec_boiler.py +++ b/src/omotes_simulator_core/entities/assets/elec_boiler.py @@ -20,10 +20,8 @@ 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__) @@ -35,14 +33,13 @@ class ElecBoiler(ProductionCluster): """ 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. + efficiency: float = 1.0 ) -> None: """ Initialize the ElecBoiler asset. @@ -57,12 +54,6 @@ def __init__( 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. diff --git a/src/omotes_simulator_core/entities/assets/gas_heater.py b/src/omotes_simulator_core/entities/assets/gas_heater.py index d69e68d5..c3b73fae 100644 --- a/src/omotes_simulator_core/entities/assets/gas_heater.py +++ b/src/omotes_simulator_core/entities/assets/gas_heater.py @@ -17,13 +17,12 @@ import logging from omotes_simulator_core.entities.assets.asset_defaults import ( - PROPERTY_ELECTRICITY_CONSUMPTION, PROPERTY_HEAT_SUPPLIED, PROPERTY_HEAT_SUPPLY_SET_POINT, - HeatPumpDefaults, + PROPERTY_GAS_CONSUMPTION, + DEFAULT_GAS_ENERGY_CONTENT, ) 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__) @@ -35,14 +34,13 @@ class GasHeater(ProductionCluster): """ 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. + efficiency: float = 1.0, ) -> None: """ Initialize the GasHeater asset. @@ -51,18 +49,15 @@ def __init__( :param str asset_id: The unique identifier of the asset. :param List[str] port_ids: List of ids of the connected ports. """ + + self.efficiency = efficiency + self.gas_energy_content = DEFAULT_GAS_ENERGY_CONTENT + 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. @@ -74,10 +69,8 @@ def get_gas_consumption(self) -> 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. + return abs(self.get_actual_heat_supplied()) / self.gas_energy_content / self.efficiency def write_to_output(self) -> None: """Method to write time step results to the output dict. @@ -88,6 +81,6 @@ def write_to_output(self) -> None: 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()), + PROPERTY_GAS_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