From b2875c90518f60d31c6031d444d3ca17967c9c77 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Mon, 9 Dec 2024 16:44:53 +0100 Subject: [PATCH 01/32] add ideal heat buffer tracking fill level --- .../entities/assets/asset_defaults.py | 2 + .../entities/assets/heat_buffer.py | 163 ++++++++++++++++++ unit_test/entities/test_heat_buffer.py | 78 +++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/omotes_simulator_core/entities/assets/heat_buffer.py create mode 100644 unit_test/entities/test_heat_buffer.py diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index aa0fa375..11aa2cf6 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -86,6 +86,8 @@ class AtesDefaults: PROPERTY_DIAMETER = "diameter" PROPERTY_ROUGHNESS = "roughness" PROPERTY_ALPHA_VALUE = "alpha_value" +PROPERTY_VOLUME = "volume" +PROPERTY_FILL_LEVEL = "fill_level" # Static members PIPE_DEFAULTS = PipeDefaults() diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py new file mode 100644 index 00000000..0c5457ac --- /dev/null +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -0,0 +1,163 @@ +# Copyright (c) 2023. 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 . + +"""atesCluster class.""" +from typing import Dict + +from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.asset_defaults import ( + DEFAULT_TEMPERATURE, + DEFAULT_TEMPERATURE_DIFFERENCE, + PROPERTY_HEAT_DEMAND, + PROPERTY_MASSFLOW, + PROPERTY_PRESSURE_RETURN, + PROPERTY_PRESSURE_SUPPLY, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_TEMPERATURE_SUPPLY, + PROPERTY_FILL_LEVEL, +) + +from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject +from omotes_simulator_core.solver.network.assets.production_asset import ProductionAsset +from omotes_simulator_core.entities.assets.utils import ( + heat_demand_and_temperature_to_mass_flow, +) +from omotes_simulator_core.solver.utils.fluid_properties import fluid_props + + +class HeatBuffer(AssetAbstract): + """A HeatBuffer represents an asset that consumes heat and produces heat.""" + + temperature_supply: float + """The supply temperature of the asset [K].""" + + temperature_return: float + """The return temperature of the asset [K].""" + + thermal_power_allocation: float + """The thermal for injection (positive) or production (negative) by the asset [W].""" + + mass_flowrate: float + """The flow rate going in or out by the asset [kg/s].""" + + maximum_volume: float + """The maximum volume of the heat storage [m3].""" + + fill_level: float + """The current fill level of the heat storage [%].""" + + def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): + """Initialize a HeatBuffer object. + + :param str asset_name: The name of the asset. + :param str asset_id: The unique identifier of the asset. + """ + super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) + self.temperature_supply = DEFAULT_TEMPERATURE + self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE + self.thermal_power_allocation = 0 # Watt + self.mass_flowrate = 0 # kg/s + self.maximum_volume = 1 # 1000 L + self.fill_level = 0.5 # 50 % + self.timestep = 3600 # seconds + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + + # Output list + self.output: list = [] + + def set_setpoints(self, setpoints: Dict) -> None: + """Placeholder to set the setpoints of an asset prior to a simulation. + + :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 + """ + # Default keys required + necessary_setpoints = { + PROPERTY_TEMPERATURE_SUPPLY, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_HEAT_DEMAND, + } + # Dict to set + setpoints_set = set(setpoints.keys()) + # Check if all setpoints are in the setpoints + if necessary_setpoints.issubset(setpoints_set): + self.thermal_power_allocation = setpoints[PROPERTY_HEAT_DEMAND] + self.temperature_return = setpoints[PROPERTY_TEMPERATURE_RETURN] + self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] + + self._calculate_massflowrate() + self._calculate_fill_level() + self._set_solver_asset_setpoint() + else: + # Print missing setpoints + raise ValueError( + f"The setpoints {necessary_setpoints.difference(setpoints_set)} are missing." + ) + + def _calculate_massflowrate(self) -> None: + """Calculate mass flowrate of the asset.""" + self.mass_flowrate = heat_demand_and_temperature_to_mass_flow( + self.thermal_power_allocation, self.temperature_supply, self.temperature_return + ) + + def _calculate_fill_level(self) -> None: + """Calculate fill level of the storage.""" + density = fluid_props.get_density(self.temperature_supply) + original_fill_level = self.fill_level + new_fill_level = (self.mass_flowrate / density * self.timestep + original_fill_level + * self.maximum_volume) / self.maximum_volume + if new_fill_level >= 0 and new_fill_level <= 1: + self.fill_level = new_fill_level + else: + raise ValueError( + f"The new fill level is {new_fill_level}. It should be between 0 and 1." + ) + + def _set_solver_asset_setpoint(self) -> None: + """Set the setpoint of solver asset.""" + if self.mass_flowrate > 0: + self.solver_asset.supply_temperature = self.temperature_return + else: + self.solver_asset.supply_temperature = self.temperature_supply + self.solver_asset.mass_flow_rate_set_point = self.mass_flowrate # type: ignore + + def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: + """Method to add physical data to the asset. + + :param EsdlAssetObject esdl_asset: The esdl asset object to add the physical data from. + :return: + """ + self.maximum_volume, _ = esdl_asset.get_property( + esdl_property_name="volume", default_value=self.maximum_volume + ) + self.fill_level, _ = esdl_asset.get_property( + esdl_property_name="fillLevel", default_value=self.fill_level + ) + + def write_to_output(self) -> None: + """Placeholder to write the asset to the output. + + The output list is a list of dictionaries, where each dictionary + represents the output of its asset for a specific timestep. + """ + output_dict = { + PROPERTY_MASSFLOW: self.solver_asset.get_mass_flow_rate(1), + PROPERTY_PRESSURE_SUPPLY: self.solver_asset.get_pressure(0), + PROPERTY_PRESSURE_RETURN: self.solver_asset.get_pressure(1), + PROPERTY_TEMPERATURE_SUPPLY: self.solver_asset.get_temperature(0), + PROPERTY_TEMPERATURE_RETURN: self.solver_asset.get_temperature(1), + PROPERTY_FILL_LEVEL: self.fill_level, + } + self.output.append(output_dict) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py new file mode 100644 index 00000000..0a33ccad --- /dev/null +++ b/unit_test/entities/test_heat_buffer.py @@ -0,0 +1,78 @@ +# Copyright (c) 2023. 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 . + +"""Test Ates Cluster entities.""" +import faulthandler +import unittest + +faulthandler.disable() +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer # noqa: E402 + +faulthandler.enable() +from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 + PROPERTY_HEAT_DEMAND, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_TEMPERATURE_SUPPLY, +) + + +class HeatBufferTest(unittest.TestCase): + """Testcase for HeatBuffer class.""" + + def setUp(self) -> None: + """Set up before each test case.""" + # Create a production cluster object + self.heat_buffer = HeatBuffer( + asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"] + ) + faulthandler.disable() + + def tearDown(self): + """Clean up after each test case.""" + faulthandler.enable() + + def test_injection(self) -> None: + """Test injection to Heat Buffer.""" + # Arrange + setpoints = { + PROPERTY_HEAT_DEMAND: 1e4, + PROPERTY_TEMPERATURE_SUPPLY: 353.15, + PROPERTY_TEMPERATURE_RETURN: 313.15, + } + + # Act + self.heat_buffer.set_setpoints(setpoints=setpoints) + + # Assert + self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.155, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.fill_level, 0.72, delta=0.01) + + def test_production(self) -> None: + """Test production from Heat Buffer.""" + # Arrange + setpoints = { + PROPERTY_HEAT_DEMAND: -1e4, + PROPERTY_TEMPERATURE_SUPPLY: 353.15, + PROPERTY_TEMPERATURE_RETURN: 313.15, + } + + # Act + self.heat_buffer.set_setpoints(setpoints=setpoints) + + # Assert + self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.fill_level, 0.27, delta=0.01) From 83ff1b764eacec5333b2a8d725481a876edbab39 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 17 Jan 2025 10:37:50 +0100 Subject: [PATCH 02/32] add volume calculation and update the comments --- .../entities/assets/heat_buffer.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 0c5457ac..2d76d4a5 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -27,6 +27,7 @@ PROPERTY_TEMPERATURE_RETURN, PROPERTY_TEMPERATURE_SUPPLY, PROPERTY_FILL_LEVEL, + PROPERTY_VOLUME ) from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject @@ -38,7 +39,8 @@ class HeatBuffer(AssetAbstract): - """A HeatBuffer represents an asset that consumes heat and produces heat.""" + """A HeatBuffer represents an asset that stores heat. Thus, it has the possibility to supply \ + heat or consume heat for storage.""" temperature_supply: float """The supply temperature of the asset [K].""" @@ -56,7 +58,14 @@ class HeatBuffer(AssetAbstract): """The maximum volume of the heat storage [m3].""" fill_level: float - """The current fill level of the heat storage [%].""" + """The current fill level of the heat storage [fraction 0-1].""" + + current_volume: float + """The current volume of the heat storage [m3].""" + + timestep: float + """The timestep of the heat storage to calculate volume during injection \ + and production [seconds].""" def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): """Initialize a HeatBuffer object. @@ -67,12 +76,12 @@ def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) self.temperature_supply = DEFAULT_TEMPERATURE self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE - self.thermal_power_allocation = 0 # Watt - self.mass_flowrate = 0 # kg/s - self.maximum_volume = 1 # 1000 L - self.fill_level = 0.5 # 50 % - self.timestep = 3600 # seconds - self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + self.thermal_power_allocation = 0 + self.mass_flowrate = 0 + self.maximum_volume = 1 + self.fill_level = 0.5 + self.timestep = 3600 + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since # Output list self.output: list = [] @@ -98,7 +107,7 @@ def set_setpoints(self, setpoints: Dict) -> None: self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] self._calculate_massflowrate() - self._calculate_fill_level() + self._calculate_fill_level_and_volume() self._set_solver_asset_setpoint() else: # Print missing setpoints @@ -112,7 +121,7 @@ def _calculate_massflowrate(self) -> None: self.thermal_power_allocation, self.temperature_supply, self.temperature_return ) - def _calculate_fill_level(self) -> None: + def _calculate_fill_level_and_volume(self) -> None: """Calculate fill level of the storage.""" density = fluid_props.get_density(self.temperature_supply) original_fill_level = self.fill_level @@ -120,6 +129,7 @@ def _calculate_fill_level(self) -> None: * self.maximum_volume) / self.maximum_volume if new_fill_level >= 0 and new_fill_level <= 1: self.fill_level = new_fill_level + self.current_level = new_fill_level * self.maximum_volume else: raise ValueError( f"The new fill level is {new_fill_level}. It should be between 0 and 1." @@ -159,5 +169,6 @@ def write_to_output(self) -> None: PROPERTY_TEMPERATURE_SUPPLY: self.solver_asset.get_temperature(0), PROPERTY_TEMPERATURE_RETURN: self.solver_asset.get_temperature(1), PROPERTY_FILL_LEVEL: self.fill_level, + PROPERTY_VOLUME: self.current_volume, } self.output.append(output_dict) From 71691a1d30c39a9923fea4891cb679638104c852 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 17 Jan 2025 11:04:30 +0100 Subject: [PATCH 03/32] add heat buffer mapper to remove add physical data --- .../esdl_asset_mappers/heat_buffer_mapper.py | 49 +++++++++++++++++++ .../entities/assets/asset_defaults.py | 9 ++++ .../entities/assets/heat_buffer.py | 19 +++---- 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py new file mode 100644 index 00000000..e772323b --- /dev/null +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023. 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 . +"""Module containing the Esdl to HeatBuffer asset mapper class.""" + +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer +from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject +from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract +from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS + + +class EsdlAssetAtesMapper(EsdlMapperAbstract): + """Class to map an ESDL asset to a HeatBuffer entity class.""" + + def to_esdl(self, entity: HeatBuffer) -> EsdlAssetObject: + """Map a HeatBuffer entity to an EsdlAsset.""" + raise NotImplementedError("EsdlAssetHeatBufferMapper.to_esdl()") + + def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: + """Method to map an ESDL asset to a HeatBuffer entity class. + + :param EsdlAssetObject esdl_asset: Object to be converted to a HeatBuffer entity. + :return: Ates object. + """ + heat_buffer_entity = HeatBuffer( + asset_name=esdl_asset.esdl_asset.name, + asset_id=esdl_asset.esdl_asset.id, + port_ids=esdl_asset.get_port_ids(), + maximum_volume=esdl_asset.get_property( + esdl_property_name="volume", default_value=HEAT_BUFFER_DEFAULTS.maximum_volume + )[0], + fill_level=esdl_asset.get_property( + esdl_property_name="fillLevel", default_value=HEAT_BUFFER_DEFAULTS.fill_level + )[0], + ) + + return heat_buffer_entity diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index 11aa2cf6..d5cd9441 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -68,6 +68,14 @@ class AtesDefaults: maximum_flow_discharge: float = 200.0 # m3/h +@dataclass +class HeatBufferDefaults: + """Class containing the default values for Heat Buffer.""" + + maximum_volume: float = 1 # m3 + fill_level: float = 0.5 # fraction 0-1 + + # Default names PROPERTY_HEAT_DEMAND = "heat_demand" PROPERTY_TEMPERATURE_SUPPLY = "temperature_supply" @@ -92,3 +100,4 @@ class AtesDefaults: # Static members PIPE_DEFAULTS = PipeDefaults() ATES_DEFAULTS = AtesDefaults() +HEAT_BUFFER_DEFAULTS = HeatBufferDefaults() diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 2d76d4a5..3c10ff88 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -67,7 +67,14 @@ class HeatBuffer(AssetAbstract): """The timestep of the heat storage to calculate volume during injection \ and production [seconds].""" - def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): + def __init__( + self, + asset_name: str, + asset_id: str, + port_ids: list[str], + maximum_volume: float, + fill_level: float, + ) -> None: """Initialize a HeatBuffer object. :param str asset_name: The name of the asset. @@ -78,8 +85,8 @@ def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE self.thermal_power_allocation = 0 self.mass_flowrate = 0 - self.maximum_volume = 1 - self.fill_level = 0.5 + self.maximum_volume = maximum_volume + self.fill_level = fill_level self.timestep = 3600 self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since @@ -149,12 +156,6 @@ def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: :param EsdlAssetObject esdl_asset: The esdl asset object to add the physical data from. :return: """ - self.maximum_volume, _ = esdl_asset.get_property( - esdl_property_name="volume", default_value=self.maximum_volume - ) - self.fill_level, _ = esdl_asset.get_property( - esdl_property_name="fillLevel", default_value=self.fill_level - ) def write_to_output(self) -> None: """Placeholder to write the asset to the output. From 1ede1fbd3473fa3cb51d20a51b4a4d7d86133631 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Tue, 4 Feb 2025 10:58:05 +0100 Subject: [PATCH 04/32] add new parameters for unit test --- unit_test/entities/test_heat_buffer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 0a33ccad..79d4333d 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -35,7 +35,8 @@ def setUp(self) -> None: """Set up before each test case.""" # Create a production cluster object self.heat_buffer = HeatBuffer( - asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"] + asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], + maximum_volume=1, fill_level=0.5 ) faulthandler.disable() From d511e89c36f23a1c358ee22c995fb11ee1dd29a7 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Tue, 4 Feb 2025 14:45:47 +0100 Subject: [PATCH 05/32] fix current volume calculation --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 3c10ff88..5a925c48 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -87,10 +87,11 @@ def __init__( self.mass_flowrate = 0 self.maximum_volume = maximum_volume self.fill_level = fill_level + self.current_volume = fill_level * maximum_volume self.timestep = 3600 - self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since - - # Output list + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + # using ProductionAsset since heat buffer acts either as producer or consumer, + # positive flow is discharge and negative flow is charge self.output: list = [] def set_setpoints(self, setpoints: Dict) -> None: @@ -136,7 +137,7 @@ def _calculate_fill_level_and_volume(self) -> None: * self.maximum_volume) / self.maximum_volume if new_fill_level >= 0 and new_fill_level <= 1: self.fill_level = new_fill_level - self.current_level = new_fill_level * self.maximum_volume + self.current_volume = new_fill_level * self.maximum_volume else: raise ValueError( f"The new fill level is {new_fill_level}. It should be between 0 and 1." From f564c58f93e62c292791b80bc630d56f153b3b85 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:42:23 +0100 Subject: [PATCH 06/32] rename mapper --- .../adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py index e772323b..59fc9e75 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -21,7 +21,7 @@ from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS -class EsdlAssetAtesMapper(EsdlMapperAbstract): +class EsdlAssetHeatBufferMapper(EsdlMapperAbstract): """Class to map an ESDL asset to a HeatBuffer entity class.""" def to_esdl(self, entity: HeatBuffer) -> EsdlAssetObject: From 703f0e5777ac0c3cd8a7c078ff82fccb515429bb Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:47:18 +0100 Subject: [PATCH 07/32] map esdl water buffer as heat buffer --- .../adapter/transforms/esdl_asset_mapper.py | 4 ++++ 1 file changed, 4 insertions(+) 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 a169873e..f6f192d9 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py @@ -42,6 +42,9 @@ from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.ates_mapper import ( EsdlAssetAtesMapper, ) +from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_buffer_mapper import ( + EsdlAssetHeatBufferMapper, +) # Define the conversion dictionary conversion_dict_mappers: dict[type, type[EsdlMapperAbstract]] = { @@ -53,6 +56,7 @@ esdl.Pipe: EsdlAssetPipeMapper, esdl.HeatPump: EsdlAssetHeatPumpMapper, esdl.ATES: EsdlAssetAtesMapper, + esdl.WaterBuffer: EsdlAssetHeatBufferMapper, } From c609e4a5d65dc2fcaefc90ba2b1f7d5afc90ae26 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:53:58 +0100 Subject: [PATCH 08/32] remove supply and return temperature from controller, it is now set from carrier esdl during mapping --- .../entities/assets/heat_buffer.py | 4 ---- unit_test/entities/test_heat_buffer.py | 10 +++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 5a925c48..487fd28e 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -102,8 +102,6 @@ def set_setpoints(self, setpoints: Dict) -> None: """ # Default keys required necessary_setpoints = { - PROPERTY_TEMPERATURE_SUPPLY, - PROPERTY_TEMPERATURE_RETURN, PROPERTY_HEAT_DEMAND, } # Dict to set @@ -111,8 +109,6 @@ def set_setpoints(self, setpoints: Dict) -> None: # Check if all setpoints are in the setpoints if necessary_setpoints.issubset(setpoints_set): self.thermal_power_allocation = setpoints[PROPERTY_HEAT_DEMAND] - self.temperature_return = setpoints[PROPERTY_TEMPERATURE_RETURN] - self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] self._calculate_massflowrate() self._calculate_fill_level_and_volume() diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 79d4333d..0574ea95 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -23,8 +23,6 @@ faulthandler.enable() from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 PROPERTY_HEAT_DEMAND, - PROPERTY_TEMPERATURE_RETURN, - PROPERTY_TEMPERATURE_SUPPLY, ) @@ -38,6 +36,8 @@ def setUp(self) -> None: asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], maximum_volume=1, fill_level=0.5 ) + self.heat_buffer.temperature_supply = 353.15 + self.heat_buffer.temperature_return = 313.15 faulthandler.disable() def tearDown(self): @@ -49,8 +49,6 @@ def test_injection(self) -> None: # Arrange setpoints = { PROPERTY_HEAT_DEMAND: 1e4, - PROPERTY_TEMPERATURE_SUPPLY: 353.15, - PROPERTY_TEMPERATURE_RETURN: 313.15, } # Act @@ -58,7 +56,7 @@ def test_injection(self) -> None: # Assert self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.155, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) self.assertAlmostEqual(self.heat_buffer.fill_level, 0.72, delta=0.01) def test_production(self) -> None: @@ -66,8 +64,6 @@ def test_production(self) -> None: # Arrange setpoints = { PROPERTY_HEAT_DEMAND: -1e4, - PROPERTY_TEMPERATURE_SUPPLY: 353.15, - PROPERTY_TEMPERATURE_RETURN: 313.15, } # Act From 4f3a415b8be5c7a663cc450b45e261625ba074ff Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Mon, 9 Dec 2024 16:44:53 +0100 Subject: [PATCH 09/32] add ideal heat buffer tracking fill level --- .../entities/assets/asset_defaults.py | 2 + .../entities/assets/heat_buffer.py | 163 ++++++++++++++++++ unit_test/entities/test_heat_buffer.py | 78 +++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/omotes_simulator_core/entities/assets/heat_buffer.py create mode 100644 unit_test/entities/test_heat_buffer.py diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index e446897d..155ef1ac 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -88,6 +88,8 @@ class AtesDefaults: PROPERTY_DIAMETER = "diameter" PROPERTY_ROUGHNESS = "roughness" PROPERTY_ALPHA_VALUE = "alpha_value" +PROPERTY_VOLUME = "volume" +PROPERTY_FILL_LEVEL = "fill_level" PROPERTY_PRESSURE_LOSS = "pressure_loss" PROPERTY_PRESSURE_LOSS_PER_LENGTH = "pressure_loss_per_length" PROPERTY_HEAT_LOSS = "heat_loss" diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py new file mode 100644 index 00000000..0c5457ac --- /dev/null +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -0,0 +1,163 @@ +# Copyright (c) 2023. 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 . + +"""atesCluster class.""" +from typing import Dict + +from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.asset_defaults import ( + DEFAULT_TEMPERATURE, + DEFAULT_TEMPERATURE_DIFFERENCE, + PROPERTY_HEAT_DEMAND, + PROPERTY_MASSFLOW, + PROPERTY_PRESSURE_RETURN, + PROPERTY_PRESSURE_SUPPLY, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_TEMPERATURE_SUPPLY, + PROPERTY_FILL_LEVEL, +) + +from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject +from omotes_simulator_core.solver.network.assets.production_asset import ProductionAsset +from omotes_simulator_core.entities.assets.utils import ( + heat_demand_and_temperature_to_mass_flow, +) +from omotes_simulator_core.solver.utils.fluid_properties import fluid_props + + +class HeatBuffer(AssetAbstract): + """A HeatBuffer represents an asset that consumes heat and produces heat.""" + + temperature_supply: float + """The supply temperature of the asset [K].""" + + temperature_return: float + """The return temperature of the asset [K].""" + + thermal_power_allocation: float + """The thermal for injection (positive) or production (negative) by the asset [W].""" + + mass_flowrate: float + """The flow rate going in or out by the asset [kg/s].""" + + maximum_volume: float + """The maximum volume of the heat storage [m3].""" + + fill_level: float + """The current fill level of the heat storage [%].""" + + def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): + """Initialize a HeatBuffer object. + + :param str asset_name: The name of the asset. + :param str asset_id: The unique identifier of the asset. + """ + super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) + self.temperature_supply = DEFAULT_TEMPERATURE + self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE + self.thermal_power_allocation = 0 # Watt + self.mass_flowrate = 0 # kg/s + self.maximum_volume = 1 # 1000 L + self.fill_level = 0.5 # 50 % + self.timestep = 3600 # seconds + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + + # Output list + self.output: list = [] + + def set_setpoints(self, setpoints: Dict) -> None: + """Placeholder to set the setpoints of an asset prior to a simulation. + + :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 + """ + # Default keys required + necessary_setpoints = { + PROPERTY_TEMPERATURE_SUPPLY, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_HEAT_DEMAND, + } + # Dict to set + setpoints_set = set(setpoints.keys()) + # Check if all setpoints are in the setpoints + if necessary_setpoints.issubset(setpoints_set): + self.thermal_power_allocation = setpoints[PROPERTY_HEAT_DEMAND] + self.temperature_return = setpoints[PROPERTY_TEMPERATURE_RETURN] + self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] + + self._calculate_massflowrate() + self._calculate_fill_level() + self._set_solver_asset_setpoint() + else: + # Print missing setpoints + raise ValueError( + f"The setpoints {necessary_setpoints.difference(setpoints_set)} are missing." + ) + + def _calculate_massflowrate(self) -> None: + """Calculate mass flowrate of the asset.""" + self.mass_flowrate = heat_demand_and_temperature_to_mass_flow( + self.thermal_power_allocation, self.temperature_supply, self.temperature_return + ) + + def _calculate_fill_level(self) -> None: + """Calculate fill level of the storage.""" + density = fluid_props.get_density(self.temperature_supply) + original_fill_level = self.fill_level + new_fill_level = (self.mass_flowrate / density * self.timestep + original_fill_level + * self.maximum_volume) / self.maximum_volume + if new_fill_level >= 0 and new_fill_level <= 1: + self.fill_level = new_fill_level + else: + raise ValueError( + f"The new fill level is {new_fill_level}. It should be between 0 and 1." + ) + + def _set_solver_asset_setpoint(self) -> None: + """Set the setpoint of solver asset.""" + if self.mass_flowrate > 0: + self.solver_asset.supply_temperature = self.temperature_return + else: + self.solver_asset.supply_temperature = self.temperature_supply + self.solver_asset.mass_flow_rate_set_point = self.mass_flowrate # type: ignore + + def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: + """Method to add physical data to the asset. + + :param EsdlAssetObject esdl_asset: The esdl asset object to add the physical data from. + :return: + """ + self.maximum_volume, _ = esdl_asset.get_property( + esdl_property_name="volume", default_value=self.maximum_volume + ) + self.fill_level, _ = esdl_asset.get_property( + esdl_property_name="fillLevel", default_value=self.fill_level + ) + + def write_to_output(self) -> None: + """Placeholder to write the asset to the output. + + The output list is a list of dictionaries, where each dictionary + represents the output of its asset for a specific timestep. + """ + output_dict = { + PROPERTY_MASSFLOW: self.solver_asset.get_mass_flow_rate(1), + PROPERTY_PRESSURE_SUPPLY: self.solver_asset.get_pressure(0), + PROPERTY_PRESSURE_RETURN: self.solver_asset.get_pressure(1), + PROPERTY_TEMPERATURE_SUPPLY: self.solver_asset.get_temperature(0), + PROPERTY_TEMPERATURE_RETURN: self.solver_asset.get_temperature(1), + PROPERTY_FILL_LEVEL: self.fill_level, + } + self.output.append(output_dict) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py new file mode 100644 index 00000000..0a33ccad --- /dev/null +++ b/unit_test/entities/test_heat_buffer.py @@ -0,0 +1,78 @@ +# Copyright (c) 2023. 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 . + +"""Test Ates Cluster entities.""" +import faulthandler +import unittest + +faulthandler.disable() +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer # noqa: E402 + +faulthandler.enable() +from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 + PROPERTY_HEAT_DEMAND, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_TEMPERATURE_SUPPLY, +) + + +class HeatBufferTest(unittest.TestCase): + """Testcase for HeatBuffer class.""" + + def setUp(self) -> None: + """Set up before each test case.""" + # Create a production cluster object + self.heat_buffer = HeatBuffer( + asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"] + ) + faulthandler.disable() + + def tearDown(self): + """Clean up after each test case.""" + faulthandler.enable() + + def test_injection(self) -> None: + """Test injection to Heat Buffer.""" + # Arrange + setpoints = { + PROPERTY_HEAT_DEMAND: 1e4, + PROPERTY_TEMPERATURE_SUPPLY: 353.15, + PROPERTY_TEMPERATURE_RETURN: 313.15, + } + + # Act + self.heat_buffer.set_setpoints(setpoints=setpoints) + + # Assert + self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.155, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.fill_level, 0.72, delta=0.01) + + def test_production(self) -> None: + """Test production from Heat Buffer.""" + # Arrange + setpoints = { + PROPERTY_HEAT_DEMAND: -1e4, + PROPERTY_TEMPERATURE_SUPPLY: 353.15, + PROPERTY_TEMPERATURE_RETURN: 313.15, + } + + # Act + self.heat_buffer.set_setpoints(setpoints=setpoints) + + # Assert + self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.fill_level, 0.27, delta=0.01) From 198cb9e2e04ba0e500c027b9dab0ce2af53ef222 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 17 Jan 2025 10:37:50 +0100 Subject: [PATCH 10/32] add volume calculation and update the comments --- .../entities/assets/heat_buffer.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 0c5457ac..2d76d4a5 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -27,6 +27,7 @@ PROPERTY_TEMPERATURE_RETURN, PROPERTY_TEMPERATURE_SUPPLY, PROPERTY_FILL_LEVEL, + PROPERTY_VOLUME ) from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject @@ -38,7 +39,8 @@ class HeatBuffer(AssetAbstract): - """A HeatBuffer represents an asset that consumes heat and produces heat.""" + """A HeatBuffer represents an asset that stores heat. Thus, it has the possibility to supply \ + heat or consume heat for storage.""" temperature_supply: float """The supply temperature of the asset [K].""" @@ -56,7 +58,14 @@ class HeatBuffer(AssetAbstract): """The maximum volume of the heat storage [m3].""" fill_level: float - """The current fill level of the heat storage [%].""" + """The current fill level of the heat storage [fraction 0-1].""" + + current_volume: float + """The current volume of the heat storage [m3].""" + + timestep: float + """The timestep of the heat storage to calculate volume during injection \ + and production [seconds].""" def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): """Initialize a HeatBuffer object. @@ -67,12 +76,12 @@ def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) self.temperature_supply = DEFAULT_TEMPERATURE self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE - self.thermal_power_allocation = 0 # Watt - self.mass_flowrate = 0 # kg/s - self.maximum_volume = 1 # 1000 L - self.fill_level = 0.5 # 50 % - self.timestep = 3600 # seconds - self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + self.thermal_power_allocation = 0 + self.mass_flowrate = 0 + self.maximum_volume = 1 + self.fill_level = 0.5 + self.timestep = 3600 + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since # Output list self.output: list = [] @@ -98,7 +107,7 @@ def set_setpoints(self, setpoints: Dict) -> None: self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] self._calculate_massflowrate() - self._calculate_fill_level() + self._calculate_fill_level_and_volume() self._set_solver_asset_setpoint() else: # Print missing setpoints @@ -112,7 +121,7 @@ def _calculate_massflowrate(self) -> None: self.thermal_power_allocation, self.temperature_supply, self.temperature_return ) - def _calculate_fill_level(self) -> None: + def _calculate_fill_level_and_volume(self) -> None: """Calculate fill level of the storage.""" density = fluid_props.get_density(self.temperature_supply) original_fill_level = self.fill_level @@ -120,6 +129,7 @@ def _calculate_fill_level(self) -> None: * self.maximum_volume) / self.maximum_volume if new_fill_level >= 0 and new_fill_level <= 1: self.fill_level = new_fill_level + self.current_level = new_fill_level * self.maximum_volume else: raise ValueError( f"The new fill level is {new_fill_level}. It should be between 0 and 1." @@ -159,5 +169,6 @@ def write_to_output(self) -> None: PROPERTY_TEMPERATURE_SUPPLY: self.solver_asset.get_temperature(0), PROPERTY_TEMPERATURE_RETURN: self.solver_asset.get_temperature(1), PROPERTY_FILL_LEVEL: self.fill_level, + PROPERTY_VOLUME: self.current_volume, } self.output.append(output_dict) From 7612d81d6cd35f58e4634c3bd29b46f1060b9bfa Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 17 Jan 2025 11:04:30 +0100 Subject: [PATCH 11/32] add heat buffer mapper to remove add physical data --- .../esdl_asset_mappers/heat_buffer_mapper.py | 49 +++++++++++++++++++ .../entities/assets/asset_defaults.py | 9 ++++ .../entities/assets/heat_buffer.py | 19 +++---- 3 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py new file mode 100644 index 00000000..e772323b --- /dev/null +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023. 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 . +"""Module containing the Esdl to HeatBuffer asset mapper class.""" + +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer +from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject +from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract +from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS + + +class EsdlAssetAtesMapper(EsdlMapperAbstract): + """Class to map an ESDL asset to a HeatBuffer entity class.""" + + def to_esdl(self, entity: HeatBuffer) -> EsdlAssetObject: + """Map a HeatBuffer entity to an EsdlAsset.""" + raise NotImplementedError("EsdlAssetHeatBufferMapper.to_esdl()") + + def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: + """Method to map an ESDL asset to a HeatBuffer entity class. + + :param EsdlAssetObject esdl_asset: Object to be converted to a HeatBuffer entity. + :return: Ates object. + """ + heat_buffer_entity = HeatBuffer( + asset_name=esdl_asset.esdl_asset.name, + asset_id=esdl_asset.esdl_asset.id, + port_ids=esdl_asset.get_port_ids(), + maximum_volume=esdl_asset.get_property( + esdl_property_name="volume", default_value=HEAT_BUFFER_DEFAULTS.maximum_volume + )[0], + fill_level=esdl_asset.get_property( + esdl_property_name="fillLevel", default_value=HEAT_BUFFER_DEFAULTS.fill_level + )[0], + ) + + return heat_buffer_entity diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index 155ef1ac..495e2b46 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -68,6 +68,14 @@ class AtesDefaults: maximum_flow_discharge: float = 200.0 # m3/h +@dataclass +class HeatBufferDefaults: + """Class containing the default values for Heat Buffer.""" + + maximum_volume: float = 1 # m3 + fill_level: float = 0.5 # fraction 0-1 + + # Default names PROPERTY_HEAT_DEMAND = "heat_demand" PROPERTY_HEAT_DEMAND_SET_POINT = "heat_demand_set_point" @@ -99,3 +107,4 @@ class AtesDefaults: # Static members PIPE_DEFAULTS = PipeDefaults() ATES_DEFAULTS = AtesDefaults() +HEAT_BUFFER_DEFAULTS = HeatBufferDefaults() diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 2d76d4a5..3c10ff88 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -67,7 +67,14 @@ class HeatBuffer(AssetAbstract): """The timestep of the heat storage to calculate volume during injection \ and production [seconds].""" - def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): + def __init__( + self, + asset_name: str, + asset_id: str, + port_ids: list[str], + maximum_volume: float, + fill_level: float, + ) -> None: """Initialize a HeatBuffer object. :param str asset_name: The name of the asset. @@ -78,8 +85,8 @@ def __init__(self, asset_name: str, asset_id: str, port_ids: list[str]): self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE self.thermal_power_allocation = 0 self.mass_flowrate = 0 - self.maximum_volume = 1 - self.fill_level = 0.5 + self.maximum_volume = maximum_volume + self.fill_level = fill_level self.timestep = 3600 self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since @@ -149,12 +156,6 @@ def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: :param EsdlAssetObject esdl_asset: The esdl asset object to add the physical data from. :return: """ - self.maximum_volume, _ = esdl_asset.get_property( - esdl_property_name="volume", default_value=self.maximum_volume - ) - self.fill_level, _ = esdl_asset.get_property( - esdl_property_name="fillLevel", default_value=self.fill_level - ) def write_to_output(self) -> None: """Placeholder to write the asset to the output. From 86a88db058ad4613940dffb804272c1d45d88d92 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Tue, 4 Feb 2025 10:58:05 +0100 Subject: [PATCH 12/32] add new parameters for unit test --- unit_test/entities/test_heat_buffer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 0a33ccad..79d4333d 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -35,7 +35,8 @@ def setUp(self) -> None: """Set up before each test case.""" # Create a production cluster object self.heat_buffer = HeatBuffer( - asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"] + asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], + maximum_volume=1, fill_level=0.5 ) faulthandler.disable() From 39cbbdf5f1be4efeb1bfc86a134cb60cd5c12744 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Tue, 4 Feb 2025 14:45:47 +0100 Subject: [PATCH 13/32] fix current volume calculation --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 3c10ff88..5a925c48 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -87,10 +87,11 @@ def __init__( self.mass_flowrate = 0 self.maximum_volume = maximum_volume self.fill_level = fill_level + self.current_volume = fill_level * maximum_volume self.timestep = 3600 - self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) # since - - # Output list + self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) + # using ProductionAsset since heat buffer acts either as producer or consumer, + # positive flow is discharge and negative flow is charge self.output: list = [] def set_setpoints(self, setpoints: Dict) -> None: @@ -136,7 +137,7 @@ def _calculate_fill_level_and_volume(self) -> None: * self.maximum_volume) / self.maximum_volume if new_fill_level >= 0 and new_fill_level <= 1: self.fill_level = new_fill_level - self.current_level = new_fill_level * self.maximum_volume + self.current_volume = new_fill_level * self.maximum_volume else: raise ValueError( f"The new fill level is {new_fill_level}. It should be between 0 and 1." From c0b64b6c07f289852ff1e755408271245cafa2cb Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:42:23 +0100 Subject: [PATCH 14/32] rename mapper --- .../adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py index e772323b..59fc9e75 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -21,7 +21,7 @@ from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS -class EsdlAssetAtesMapper(EsdlMapperAbstract): +class EsdlAssetHeatBufferMapper(EsdlMapperAbstract): """Class to map an ESDL asset to a HeatBuffer entity class.""" def to_esdl(self, entity: HeatBuffer) -> EsdlAssetObject: From 51310f5b175c796ead4b7750fbeb8467e4b34fcb Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:47:18 +0100 Subject: [PATCH 15/32] map esdl water buffer as heat buffer --- .../adapter/transforms/esdl_asset_mapper.py | 4 ++++ 1 file changed, 4 insertions(+) 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 4eb29cb3..06a4eee1 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py @@ -34,6 +34,9 @@ from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract +from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_buffer_mapper import ( + EsdlAssetHeatBufferMapper, +) # Define the conversion dictionary conversion_dict_mappers: dict[type, type[EsdlMapperAbstract]] = { @@ -49,6 +52,7 @@ esdl.Pipe: EsdlAssetPipeMapper, esdl.HeatPump: EsdlAssetHeatPumpMapper, esdl.ATES: EsdlAssetAtesMapper, + esdl.WaterBuffer: EsdlAssetHeatBufferMapper, } From 77cc69c24e9b08e7160b892b52ef649156595e4c Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 7 Feb 2025 09:53:58 +0100 Subject: [PATCH 16/32] remove supply and return temperature from controller, it is now set from carrier esdl during mapping --- .../entities/assets/heat_buffer.py | 4 ---- unit_test/entities/test_heat_buffer.py | 10 +++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 5a925c48..487fd28e 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -102,8 +102,6 @@ def set_setpoints(self, setpoints: Dict) -> None: """ # Default keys required necessary_setpoints = { - PROPERTY_TEMPERATURE_SUPPLY, - PROPERTY_TEMPERATURE_RETURN, PROPERTY_HEAT_DEMAND, } # Dict to set @@ -111,8 +109,6 @@ def set_setpoints(self, setpoints: Dict) -> None: # Check if all setpoints are in the setpoints if necessary_setpoints.issubset(setpoints_set): self.thermal_power_allocation = setpoints[PROPERTY_HEAT_DEMAND] - self.temperature_return = setpoints[PROPERTY_TEMPERATURE_RETURN] - self.temperature_supply = setpoints[PROPERTY_TEMPERATURE_SUPPLY] self._calculate_massflowrate() self._calculate_fill_level_and_volume() diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 79d4333d..0574ea95 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -23,8 +23,6 @@ faulthandler.enable() from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 PROPERTY_HEAT_DEMAND, - PROPERTY_TEMPERATURE_RETURN, - PROPERTY_TEMPERATURE_SUPPLY, ) @@ -38,6 +36,8 @@ def setUp(self) -> None: asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], maximum_volume=1, fill_level=0.5 ) + self.heat_buffer.temperature_supply = 353.15 + self.heat_buffer.temperature_return = 313.15 faulthandler.disable() def tearDown(self): @@ -49,8 +49,6 @@ def test_injection(self) -> None: # Arrange setpoints = { PROPERTY_HEAT_DEMAND: 1e4, - PROPERTY_TEMPERATURE_SUPPLY: 353.15, - PROPERTY_TEMPERATURE_RETURN: 313.15, } # Act @@ -58,7 +56,7 @@ def test_injection(self) -> None: # Assert self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.155, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) self.assertAlmostEqual(self.heat_buffer.fill_level, 0.72, delta=0.01) def test_production(self) -> None: @@ -66,8 +64,6 @@ def test_production(self) -> None: # Arrange setpoints = { PROPERTY_HEAT_DEMAND: -1e4, - PROPERTY_TEMPERATURE_SUPPLY: 353.15, - PROPERTY_TEMPERATURE_RETURN: 313.15, } # Act From 4dc972ec77c727f56d97e88fc08edc164ed92f8e Mon Sep 17 00:00:00 2001 From: "Mike van Meerkerk (Deltares)" Date: Thu, 3 Apr 2025 14:48:07 +0200 Subject: [PATCH 17/32] Minor code changes --- .../adapter/transforms/esdl_asset_mapper.py | 9 ++--- .../assets/controller/controller_storage.py | 21 ++++++++--- .../entities/assets/heat_buffer.py | 37 ++++++++++++------- .../entities/network_controller.py | 22 +++++++---- .../simulation/networksimulation.py | 11 ++++-- 5 files changed, 64 insertions(+), 36 deletions(-) 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 ce2ec81d..dbd8f8a8 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py @@ -22,6 +22,9 @@ from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.consumer_mapper import ( EsdlAssetConsumerMapper, ) +from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_buffer_mapper import ( + EsdlAssetHeatBufferMapper, +) from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_pump_mapper import ( EsdlAssetHeatPumpMapper, ) @@ -34,12 +37,6 @@ from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract -from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_buffer_mapper import ( - EsdlAssetHeatBufferMapper, -) -from omotes_simulator_core.adapter.transforms.esdl_asset_mappers.heat_buffer_mapper import ( - EsdlAssetHeatBufferMapper, -) # Define the conversion dictionary conversion_dict_mappers: dict[type, type[EsdlMapperAbstract]] = { diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_storage.py b/src/omotes_simulator_core/entities/assets/controller/controller_storage.py index d40e3b7a..5e6d0796 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_storage.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_storage.py @@ -15,9 +15,10 @@ """Module containing the classes for the controller.""" import datetime +import logging import pandas as pd -import logging + from omotes_simulator_core.entities.assets.controller.asset_controller_abstract import ( AssetControllerAbstract, ) @@ -48,6 +49,8 @@ def __init__( self.temperature_supply = temperature_supply self.profile: pd.DataFrame = profile self.start_index = 0 + + # Theoretical maximum charge and discharge power of the storage. self.max_charge_power: float = max_charge_power self.max_discharge_power: float = max_discharge_power @@ -57,21 +60,27 @@ def get_heat_power(self, time: datetime.datetime) -> float: :param datetime.datetime time: Time for which to get the heat demand. :return: float with the heat demand. """ + # Check if the selected time is in the profile. + # TODO: Current implementation loops over the entire profile; should be improved! + # TODO: Unclear why there is a timestep of 1 hour in the profile. for index in range(self.start_index, len(self.profile)): if abs((self.profile["date"][index].to_pydatetime() - time).total_seconds()) < 3600: self.start_index = index if self.profile["values"][index] > self.max_charge_power: logging.warning( - f"Storage of {self.name} is higher than maximum charge power of asset" - f" at time {time}." + "Storage of %s is higher than maximum charge power of asset at time %s.", + self.name, + time, ) return self.max_charge_power elif self.profile["values"][index] < self.max_discharge_power: logging.warning( - f"Storage of {self.name} is higher than maximum discharge power of asset" - f" at time {time}." + "Storage of %s is higher than maximum discharge power of asset at time %s.", + self.name, + time, ) return self.max_discharge_power else: return float(self.profile["values"][index]) - return 0 + # TODO: The loop is not complete as the asset also has a fill-level that should not surpass the maximum fill-level. + return 0.0 diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 487fd28e..af18aa43 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -20,21 +20,20 @@ from omotes_simulator_core.entities.assets.asset_defaults import ( DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE_DIFFERENCE, + PROPERTY_FILL_LEVEL, PROPERTY_HEAT_DEMAND, PROPERTY_MASSFLOW, PROPERTY_PRESSURE_RETURN, PROPERTY_PRESSURE_SUPPLY, PROPERTY_TEMPERATURE_RETURN, PROPERTY_TEMPERATURE_SUPPLY, - PROPERTY_FILL_LEVEL, - PROPERTY_VOLUME + PROPERTY_VOLUME, ) - from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject -from omotes_simulator_core.solver.network.assets.production_asset import ProductionAsset from omotes_simulator_core.entities.assets.utils import ( heat_demand_and_temperature_to_mass_flow, ) +from omotes_simulator_core.solver.network.assets.production_asset import HeatBoundary from omotes_simulator_core.solver.utils.fluid_properties import fluid_props @@ -63,8 +62,8 @@ class HeatBuffer(AssetAbstract): current_volume: float """The current volume of the heat storage [m3].""" - timestep: float - """The timestep of the heat storage to calculate volume during injection \ + accumulation_time: float + """The accumulation_time of the heat storage to calculate volume during injection and production [seconds].""" def __init__( @@ -81,17 +80,27 @@ def __init__( :param str asset_id: The unique identifier of the asset. """ super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) + + # Supply and return temperature of the asset [K] self.temperature_supply = DEFAULT_TEMPERATURE self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE - self.thermal_power_allocation = 0 - self.mass_flowrate = 0 + + # Volume properties of the asset: maximum volume [m3], fill level [fraction 0-1], current + # volume [m3] self.maximum_volume = maximum_volume self.fill_level = fill_level self.current_volume = fill_level * maximum_volume - self.timestep = 3600 - self.solver_asset = ProductionAsset(name=self.name, _id=self.asset_id) - # using ProductionAsset since heat buffer acts either as producer or consumer, + + # Thermal power allocation [W] for injection (positive) or production (negative) by the + # asset and mass flowrate [kg/s] going in or out by the asset + self.thermal_power_allocation = 0 + self.mass_flowrate = 0 + + # HeatBoundary since heat buffer acts either as producer or consumer, # positive flow is discharge and negative flow is charge + self.solver_asset = HeatBoundary(name=self.name, _id=self.asset_id) + + self.accumulation_time = 3600 self.output: list = [] def set_setpoints(self, setpoints: Dict) -> None: @@ -129,8 +138,10 @@ def _calculate_fill_level_and_volume(self) -> None: """Calculate fill level of the storage.""" density = fluid_props.get_density(self.temperature_supply) original_fill_level = self.fill_level - new_fill_level = (self.mass_flowrate / density * self.timestep + original_fill_level - * self.maximum_volume) / self.maximum_volume + new_fill_level = ( + self.mass_flowrate / density * self.accumulation_time + + original_fill_level * self.maximum_volume + ) / self.maximum_volume if new_fill_level >= 0 and new_fill_level <= 1: self.fill_level = new_fill_level self.current_volume = new_fill_level * self.maximum_volume diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 3707984c..f29edb0f 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -17,16 +17,24 @@ import datetime import logging -from omotes_simulator_core.entities.network_controller_abstract import NetworkControllerAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( - PROPERTY_TEMPERATURE_SUPPLY, - PROPERTY_TEMPERATURE_RETURN, PROPERTY_HEAT_DEMAND, PROPERTY_SET_PRESSURE, + PROPERTY_TEMPERATURE_RETURN, + PROPERTY_TEMPERATURE_SUPPLY, +) +from omotes_simulator_core.entities.assets.controller.controller_consumer import ( + ControllerConsumer, +) +from omotes_simulator_core.entities.assets.controller.controller_producer import ( + ControllerProducer, +) +from omotes_simulator_core.entities.assets.controller.controller_storage import ( + ControllerStorage, +) +from omotes_simulator_core.entities.network_controller_abstract import ( + NetworkControllerAbstract, ) -from omotes_simulator_core.entities.assets.controller.controller_producer import ControllerProducer -from omotes_simulator_core.entities.assets.controller.controller_consumer import ControllerConsumer -from omotes_simulator_core.entities.assets.controller.controller_storage import ControllerStorage logger = logging.getLogger(__name__) @@ -122,7 +130,7 @@ def get_total_discharge_storage(self) -> float: :return float: Total heat discharge of all storages. """ - # TODO add limit based on state of charge + return float(sum([storage.max_discharge_power for storage in self.storages])) def get_total_charge_storage(self) -> float: diff --git a/src/omotes_simulator_core/simulation/networksimulation.py b/src/omotes_simulator_core/simulation/networksimulation.py index 4346845b..81377bd4 100644 --- a/src/omotes_simulator_core/simulation/networksimulation.py +++ b/src/omotes_simulator_core/simulation/networksimulation.py @@ -14,14 +14,17 @@ # along with this program. If not, see . """Simulates an heat network for the specified duration.""" +import logging +from datetime import timedelta, timezone from typing import Callable + from pandas import DataFrame + from omotes_simulator_core.entities.heat_network import HeatNetwork from omotes_simulator_core.entities.network_controller import NetworkController -from omotes_simulator_core.entities.simulation_configuration import SimulationConfiguration -from datetime import timedelta, timezone - -import logging +from omotes_simulator_core.entities.simulation_configuration import ( + SimulationConfiguration, +) logger = logging.getLogger(__name__) From 1ca12958c564c2e8ea1187d326077ca5a3c879cc Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 8 Aug 2025 17:38:13 +0200 Subject: [PATCH 18/32] run formatting --- .../assets/controller/controller_storage.py | 16 ++++++++-------- unit_test/entities/test_heat_buffer.py | 7 +++++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_storage.py b/src/omotes_simulator_core/entities/assets/controller/controller_storage.py index d9854188..c3bdbe80 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_storage.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_storage.py @@ -30,14 +30,14 @@ class ControllerStorage(AssetControllerAbstract): """Class to store the storage for the controller asset.""" def __init__( - self, - name: str, - identifier: str, - temperature_in: float, - temperature_out: float, - max_charge_power: float, - max_discharge_power: float, - profile: pd.DataFrame, + self, + name: str, + identifier: str, + temperature_in: float, + temperature_out: float, + max_charge_power: float, + max_discharge_power: float, + profile: pd.DataFrame, ): """Constructor for the storage. diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 0574ea95..65d4126e 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -33,8 +33,11 @@ def setUp(self) -> None: """Set up before each test case.""" # Create a production cluster object self.heat_buffer = HeatBuffer( - asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], - maximum_volume=1, fill_level=0.5 + asset_name="heat_buffer", + asset_id="heat_buffer_id", + port_ids=["test1", "test2"], + maximum_volume=1, + fill_level=0.5, ) self.heat_buffer.temperature_supply = 353.15 self.heat_buffer.temperature_return = 313.15 From d47c6fdf5e17de8fc22ebd0da7c34beba327a7e2 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Sat, 9 Aug 2025 08:37:59 +0200 Subject: [PATCH 19/32] reformat heat_buffer_mapper.py --- .../transforms/esdl_asset_mappers/heat_buffer_mapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py index 59fc9e75..2826183a 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -14,11 +14,11 @@ # along with this program. If not, see . """Module containing the Esdl to HeatBuffer asset mapper class.""" -from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract +from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer from omotes_simulator_core.simulation.mappers.mappers import EsdlMapperAbstract -from omotes_simulator_core.entities.assets.asset_defaults import HEAT_BUFFER_DEFAULTS class EsdlAssetHeatBufferMapper(EsdlMapperAbstract): From 73bab55e750297189949e52593d1864149ee7652 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Mon, 11 Aug 2025 10:38:12 +0200 Subject: [PATCH 20/32] implement stratified buffer tank with 5 layers --- .../esdl_asset_mappers/heat_buffer_mapper.py | 7 +- .../entities/assets/asset_defaults.py | 2 +- .../entities/assets/heat_buffer.py | 122 ++++++++++++------ 3 files changed, 83 insertions(+), 48 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py index 2826183a..68ff0bd0 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -38,11 +38,8 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: asset_name=esdl_asset.esdl_asset.name, asset_id=esdl_asset.esdl_asset.id, port_ids=esdl_asset.get_port_ids(), - maximum_volume=esdl_asset.get_property( - esdl_property_name="volume", default_value=HEAT_BUFFER_DEFAULTS.maximum_volume - )[0], - fill_level=esdl_asset.get_property( - esdl_property_name="fillLevel", default_value=HEAT_BUFFER_DEFAULTS.fill_level + volume=esdl_asset.get_property( + esdl_property_name="volume", default_value=HEAT_BUFFER_DEFAULTS.volume )[0], ) diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index 1c59ddf1..b9d5a336 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -72,7 +72,7 @@ class AtesDefaults: class HeatBufferDefaults: """Class containing the default values for Heat Buffer.""" - maximum_volume: float = 1 # m3 + volume: float = 1 # m3 fill_level: float = 0.5 # fraction 0-1 diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 6b38f4f1..485bcae4 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -13,21 +13,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""atesCluster class.""" +"""Heat Buffer class.""" +from cmath import isinf from typing import Dict +import numpy as np + from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( DEFAULT_TEMPERATURE, - DEFAULT_TEMPERATURE_DIFFERENCE, - PROPERTY_FILL_LEVEL, PROPERTY_HEAT_DEMAND, PROPERTY_MASSFLOW, PROPERTY_PRESSURE_RETURN, PROPERTY_PRESSURE_SUPPLY, PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, - PROPERTY_VOLUME, ) from omotes_simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject from omotes_simulator_core.entities.assets.utils import ( @@ -53,14 +53,8 @@ class HeatBuffer(AssetAbstract): mass_flowrate: float """The flow rate going in or out by the asset [kg/s].""" - maximum_volume: float - """The maximum volume of the heat storage [m3].""" - - fill_level: float - """The current fill level of the heat storage [fraction 0-1].""" - - current_volume: float - """The current volume of the heat storage [m3].""" + volume: float + """The volume of the heat storage [m3].""" accumulation_time: float """The accumulation_time to calculate volume during injection and production [seconds].""" @@ -70,8 +64,7 @@ def __init__( asset_name: str, asset_id: str, port_ids: list[str], - maximum_volume: float, - fill_level: float, + volume: float, ) -> None: """Initialize a HeatBuffer object. @@ -81,14 +74,19 @@ def __init__( super().__init__(asset_name=asset_name, asset_id=asset_id, connected_ports=port_ids) # Supply and return temperature of the asset [K] - self.temperature_supply = DEFAULT_TEMPERATURE - self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE - - # Volume properties of the asset: maximum volume [m3], fill level [fraction 0-1], current - # volume [m3] - self.maximum_volume = maximum_volume - self.fill_level = fill_level - self.current_volume = fill_level * maximum_volume + self.temperature_in = DEFAULT_TEMPERATURE + self.temperature_out = DEFAULT_TEMPERATURE + + # Volume properties of the asset: volume [m3], fill level [fraction 0-1] + self.tank_volume = volume + self.num_layer = 5 + self.layer_volume = self.tank_volume / self.num_layer + self.layer_mass = self.tank_volume * fluid_props.get_density( + (self.temperature_in + self.temperature_out) / 2 + ) + self.layer_temperature = np.linspace( + self.temperature_in, self.temperature_out, self.num_layer + ) # Thermal power allocation [W] for injection (positive) or production (negative) by the # asset and mass flowrate [kg/s] going in or out by the asset @@ -101,6 +99,7 @@ def __init__( self.accumulation_time = 3600 self.output: list = [] + self.first_time_step = True def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. @@ -110,16 +109,37 @@ def set_setpoints(self, setpoints: Dict) -> None: """ # Default keys required necessary_setpoints = { + PROPERTY_TEMPERATURE_IN, + PROPERTY_TEMPERATURE_OUT, PROPERTY_HEAT_DEMAND, } # Dict to set setpoints_set = set(setpoints.keys()) # Check if all setpoints are in the setpoints if necessary_setpoints.issubset(setpoints_set): + # negative is charging and positive is discharging self.thermal_power_allocation = -setpoints[PROPERTY_HEAT_DEMAND] + if self.first_time_step: + self.temperature_in = setpoints[PROPERTY_TEMPERATURE_IN] + self.temperature_out = setpoints[PROPERTY_TEMPERATURE_OUT] + self.first_time_step = False + else: + # After the first time step: use solver temperature + if self.thermal_power_allocation < 0: + self.temperature_in = self.solver_asset.get_temperature(0) + else: + self.temperature_out = self.solver_asset.get_temperature(1) + + if self.thermal_power_allocation < 0: + # when charging the output temperature is the bottom temperature of the tank + self.temperature_out = self.layer_temperature[-1] + else: + # when discharging the input temperature is the upper temperature of the tank + self.temperature_in = self.layer_temperature[0] + self._calculate_massflowrate() - self._calculate_fill_level_and_volume() + self._calculate_new_temperature() self._set_solver_asset_setpoint() else: # Print missing setpoints @@ -130,31 +150,51 @@ def set_setpoints(self, setpoints: Dict) -> None: def _calculate_massflowrate(self) -> None: """Calculate mass flowrate of the asset.""" self.mass_flowrate = heat_demand_and_temperature_to_mass_flow( - self.thermal_power_allocation, self.temperature_supply, self.temperature_return + self.thermal_power_allocation, self.temperature_in, self.temperature_out ) + if isinf(self.mass_flowrate): + self.mass_flowrate = 0 - def _calculate_fill_level_and_volume(self) -> None: - """Calculate fill level of the storage.""" - density = fluid_props.get_density(self.temperature_supply) - original_fill_level = self.fill_level - new_fill_level = ( - self.mass_flowrate / density * self.accumulation_time - + original_fill_level * self.maximum_volume - ) / self.maximum_volume - if new_fill_level >= 0 and new_fill_level <= 1: - self.fill_level = new_fill_level - self.current_volume = new_fill_level * self.maximum_volume + def _calculate_new_temperature(self) -> None: + """Calculate new temperature of the tank storage.""" + # heat exchange top side + new_temperature = self.layer_temperature.copy() + + if self.mass_flowrate > 0: + new_temperature[0] += min( + 1, self.mass_flowrate * self.accumulation_time / self.layer_mass + ) * (self.temperature_in - self.layer_temperature[0]) + + # heat exchange between layer + for ii in range(self.num_layer - 1): + new_temperature[ii + 1] += min( + 1, self.mass_flowrate * self.accumulation_time / self.layer_mass + ) * (new_temperature[ii] - new_temperature[ii + 1]) + + self.layer_temperature = new_temperature + + self.temperature_out = self.layer_temperature[-1] else: - raise ValueError( - f"The new fill level is {new_fill_level}. It should be between 0 and 1." - ) + new_temperature[-1] += min( + 1, abs(self.mass_flowrate) * self.accumulation_time / self.layer_mass + ) * (self.temperature_out - self.layer_temperature[-1]) + + # heat exchange between layer + for ii in range(self.num_layer - 1, 0, -1): + new_temperature[ii - 1] += min( + 1, abs(self.mass_flowrate) * self.accumulation_time / self.layer_mass + ) * (new_temperature[ii] - new_temperature[ii - 1]) + + self.layer_temperature = new_temperature + + self.temperature_in = self.layer_temperature[0] def _set_solver_asset_setpoint(self) -> None: """Set the setpoint of solver asset.""" if self.mass_flowrate > 0: - self.solver_asset.supply_temperature = self.temperature_return + self.solver_asset.supply_temperature = self.temperature_out else: - self.solver_asset.supply_temperature = self.temperature_supply + self.solver_asset.supply_temperature = self.temperature_in self.solver_asset.mass_flow_rate_set_point = self.mass_flowrate # type: ignore def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: @@ -176,7 +216,5 @@ def write_to_output(self) -> None: PROPERTY_PRESSURE_RETURN: self.solver_asset.get_pressure(1), PROPERTY_TEMPERATURE_IN: self.solver_asset.get_temperature(0), PROPERTY_TEMPERATURE_OUT: self.solver_asset.get_temperature(1), - PROPERTY_FILL_LEVEL: self.fill_level, - PROPERTY_VOLUME: self.current_volume, } self.output.append(output_dict) From 45b2f9249872c3bcb27bcd5849cba6b4fd3afb2d Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Mon, 11 Aug 2025 10:51:01 +0200 Subject: [PATCH 21/32] fix unit test --- unit_test/entities/test_heat_buffer.py | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 65d4126e..81d28b24 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -23,6 +23,8 @@ faulthandler.enable() from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 PROPERTY_HEAT_DEMAND, + PROPERTY_TEMPERATURE_IN, + PROPERTY_TEMPERATURE_OUT, ) @@ -36,11 +38,8 @@ def setUp(self) -> None: asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], - maximum_volume=1, - fill_level=0.5, + volume=1, ) - self.heat_buffer.temperature_supply = 353.15 - self.heat_buffer.temperature_return = 313.15 faulthandler.disable() def tearDown(self): @@ -52,27 +51,33 @@ def test_injection(self) -> None: # Arrange setpoints = { PROPERTY_HEAT_DEMAND: 1e4, + PROPERTY_TEMPERATURE_IN: 363, + PROPERTY_TEMPERATURE_OUT: 283, } # Act - self.heat_buffer.set_setpoints(setpoints=setpoints) + # charging for 1 day + for _ii in range(0, 24): + self.heat_buffer.first_time_step = True + self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.fill_level, 0.72, delta=0.01) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[2], 350.49, delta=0.01) def test_production(self) -> None: """Test production from Heat Buffer.""" # Arrange setpoints = { - PROPERTY_HEAT_DEMAND: -1e4, + PROPERTY_HEAT_DEMAND: -1e3, + PROPERTY_TEMPERATURE_IN: 363, + PROPERTY_TEMPERATURE_OUT: 283, } # Act - self.heat_buffer.set_setpoints(setpoints=setpoints) + # discharging for 1 day + for _ii in range(0, 24): + self.heat_buffer.first_time_step = True + self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.temperature_supply, 353.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.temperature_return, 313.15, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.fill_level, 0.27, delta=0.01) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[2], 297.54, delta=0.01) From 05f03211732dbc77ab82209bd308d34840fe2ff7 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Thu, 28 Aug 2025 13:36:07 +0200 Subject: [PATCH 22/32] remove fault handler --- unit_test/entities/test_heat_buffer.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 81d28b24..f3079e41 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -13,19 +13,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Test Ates Cluster entities.""" -import faulthandler +"""Test Heat Buffer entities.""" import unittest -faulthandler.disable() -from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer # noqa: E402 - -faulthandler.enable() -from omotes_simulator_core.entities.assets.asset_defaults import ( # noqa: E402 +from omotes_simulator_core.entities.assets.asset_defaults import ( PROPERTY_HEAT_DEMAND, PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, ) +from omotes_simulator_core.entities.assets.heat_buffer import HeatBuffer class HeatBufferTest(unittest.TestCase): @@ -40,11 +36,6 @@ def setUp(self) -> None: port_ids=["test1", "test2"], volume=1, ) - faulthandler.disable() - - def tearDown(self): - """Clean up after each test case.""" - faulthandler.enable() def test_injection(self) -> None: """Test injection to Heat Buffer.""" From 4d63fef0c7eca41ac068cfa5023ea998f4f51656 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 29 Aug 2025 10:28:54 +0200 Subject: [PATCH 23/32] fix typo layer volume --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 485bcae4..ddf862fe 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -81,7 +81,7 @@ def __init__( self.tank_volume = volume self.num_layer = 5 self.layer_volume = self.tank_volume / self.num_layer - self.layer_mass = self.tank_volume * fluid_props.get_density( + self.layer_mass = self.layer_volume * fluid_props.get_density( (self.temperature_in + self.temperature_out) / 2 ) self.layer_temperature = np.linspace( From e357c8e3c49cf5a94b38ac6b5f0618ab196d98e9 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 29 Aug 2025 11:36:57 +0200 Subject: [PATCH 24/32] fix test heat buffer after correction layer_mass --- unit_test/entities/test_heat_buffer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index f3079e41..7f8fa8b6 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -16,6 +16,8 @@ """Test Heat Buffer entities.""" import unittest +import numpy as np + from omotes_simulator_core.entities.assets.asset_defaults import ( PROPERTY_HEAT_DEMAND, PROPERTY_TEMPERATURE_IN, @@ -41,7 +43,7 @@ def test_injection(self) -> None: """Test injection to Heat Buffer.""" # Arrange setpoints = { - PROPERTY_HEAT_DEMAND: 1e4, + PROPERTY_HEAT_DEMAND: 1e3, PROPERTY_TEMPERATURE_IN: 363, PROPERTY_TEMPERATURE_OUT: 283, } @@ -53,7 +55,8 @@ def test_injection(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[2], 350.49, delta=0.01) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 355.59, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 311.92, delta=0.1) def test_production(self) -> None: """Test production from Heat Buffer.""" @@ -64,6 +67,8 @@ def test_production(self) -> None: PROPERTY_TEMPERATURE_OUT: 283, } + self.heat_buffer.layer_temperature = np.array([355.59, 341.36, 327.13, 317.25, 311.92]) + # Act # discharging for 1 day for _ii in range(0, 24): @@ -71,4 +76,5 @@ def test_production(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[2], 297.54, delta=0.01) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 331.23, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 287.87, delta=0.1) From 99780f092b71d350b35e3d13231f7cf3eea7722a Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 29 Aug 2025 11:47:32 +0200 Subject: [PATCH 25/32] change comparison value --- unit_test/entities/test_heat_buffer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 7f8fa8b6..775fd2f9 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -55,8 +55,8 @@ def test_injection(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 355.59, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 311.92, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 351.70, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 302.56, delta=0.1) def test_production(self) -> None: """Test production from Heat Buffer.""" @@ -76,5 +76,5 @@ def test_production(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 331.23, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 287.87, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 331.30, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 287.90, delta=0.1) From f7b33bd9cf3eb0e081629ac589c0d97f210e7dfa Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Wed, 10 Sep 2025 18:19:13 +0200 Subject: [PATCH 26/32] add energy calculation --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index ddf862fe..97068447 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -100,6 +100,7 @@ def __init__( self.accumulation_time = 3600 self.output: list = [] self.first_time_step = True + self.energy = 0 def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. @@ -174,6 +175,7 @@ def _calculate_new_temperature(self) -> None: self.layer_temperature = new_temperature self.temperature_out = self.layer_temperature[-1] + else: new_temperature[-1] += min( 1, abs(self.mass_flowrate) * self.accumulation_time / self.layer_mass @@ -189,6 +191,10 @@ def _calculate_new_temperature(self) -> None: self.temperature_in = self.layer_temperature[0] + self.energy = self.energy + self.mass_flowrate * self.accumulation_time / 3600 * 4180 * ( + self.temperature_in - self.temperature_out + ) + def _set_solver_asset_setpoint(self) -> None: """Set the setpoint of solver asset.""" if self.mass_flowrate > 0: From 0685f579c804488c4c17679a8ff5c09ca968e378 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Thu, 11 Sep 2025 14:39:40 +0200 Subject: [PATCH 27/32] add stored energy calculation --- .../entities/assets/heat_buffer.py | 40 ++++++++----------- unit_test/entities/test_heat_buffer.py | 26 ++++++------ 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 97068447..85bc699b 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -56,8 +56,8 @@ class HeatBuffer(AssetAbstract): volume: float """The volume of the heat storage [m3].""" - accumulation_time: float - """The accumulation_time to calculate volume during injection and production [seconds].""" + energy: float + """The stored energy in the storage [Wh].""" def __init__( self, @@ -97,10 +97,9 @@ def __init__( # positive flow is discharge and negative flow is charge self.solver_asset = HeatBoundary(name=self.name, _id=self.asset_id) - self.accumulation_time = 3600 self.output: list = [] self.first_time_step = True - self.energy = 0 + self.energy = 0.0 def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. @@ -128,16 +127,11 @@ def set_setpoints(self, setpoints: Dict) -> None: else: # After the first time step: use solver temperature if self.thermal_power_allocation < 0: - self.temperature_in = self.solver_asset.get_temperature(0) - else: + self.temperature_in = self.layer_temperature[0] self.temperature_out = self.solver_asset.get_temperature(1) - - if self.thermal_power_allocation < 0: - # when charging the output temperature is the bottom temperature of the tank - self.temperature_out = self.layer_temperature[-1] - else: - # when discharging the input temperature is the upper temperature of the tank - self.temperature_in = self.layer_temperature[0] + else: + self.temperature_in = self.solver_asset.get_temperature(0) + self.temperature_out = self.layer_temperature[-1] self._calculate_massflowrate() self._calculate_new_temperature() @@ -162,36 +156,36 @@ def _calculate_new_temperature(self) -> None: new_temperature = self.layer_temperature.copy() if self.mass_flowrate > 0: - new_temperature[0] += min( - 1, self.mass_flowrate * self.accumulation_time / self.layer_mass - ) * (self.temperature_in - self.layer_temperature[0]) + new_temperature[0] += min(1, self.mass_flowrate * self.time_step / self.layer_mass) * ( + self.temperature_in - new_temperature[0] + ) # heat exchange between layer for ii in range(self.num_layer - 1): new_temperature[ii + 1] += min( - 1, self.mass_flowrate * self.accumulation_time / self.layer_mass + 1, self.mass_flowrate * self.time_step / self.layer_mass ) * (new_temperature[ii] - new_temperature[ii + 1]) self.layer_temperature = new_temperature - self.temperature_out = self.layer_temperature[-1] + self.temperature_out = float(self.layer_temperature[-1]) else: new_temperature[-1] += min( - 1, abs(self.mass_flowrate) * self.accumulation_time / self.layer_mass - ) * (self.temperature_out - self.layer_temperature[-1]) + 1, abs(self.mass_flowrate) * self.time_step / self.layer_mass + ) * (self.temperature_out - new_temperature[-1]) # heat exchange between layer for ii in range(self.num_layer - 1, 0, -1): new_temperature[ii - 1] += min( - 1, abs(self.mass_flowrate) * self.accumulation_time / self.layer_mass + 1, abs(self.mass_flowrate) * self.time_step / self.layer_mass ) * (new_temperature[ii] - new_temperature[ii - 1]) self.layer_temperature = new_temperature - self.temperature_in = self.layer_temperature[0] + self.temperature_in = float(self.layer_temperature[0]) - self.energy = self.energy + self.mass_flowrate * self.accumulation_time / 3600 * 4180 * ( + self.energy = self.energy + self.mass_flowrate * self.time_step / 3600 * 4180 * ( self.temperature_in - self.temperature_out ) diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 775fd2f9..0b79da4f 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -36,16 +36,16 @@ def setUp(self) -> None: asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], - volume=1, + volume=25, ) def test_injection(self) -> None: """Test injection to Heat Buffer.""" # Arrange setpoints = { - PROPERTY_HEAT_DEMAND: 1e3, - PROPERTY_TEMPERATURE_IN: 363, - PROPERTY_TEMPERATURE_OUT: 283, + PROPERTY_HEAT_DEMAND: 5e3, + PROPERTY_TEMPERATURE_IN: 85 + 273.15, + PROPERTY_TEMPERATURE_OUT: 25 + 273.15, } # Act @@ -55,19 +55,21 @@ def test_injection(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 351.70, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 302.56, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 358.14, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 357.54, delta=0.1) def test_production(self) -> None: """Test production from Heat Buffer.""" # Arrange setpoints = { - PROPERTY_HEAT_DEMAND: -1e3, - PROPERTY_TEMPERATURE_IN: 363, - PROPERTY_TEMPERATURE_OUT: 283, + PROPERTY_HEAT_DEMAND: -5e3, + PROPERTY_TEMPERATURE_IN: 85 + 273.15, + PROPERTY_TEMPERATURE_OUT: 25 + 273.15, } - self.heat_buffer.layer_temperature = np.array([355.59, 341.36, 327.13, 317.25, 311.92]) + self.heat_buffer.layer_temperature = np.array( + [358.14869781, 358.13745031, 358.08685218, 357.92903466, 357.54565629] + ) # Act # discharging for 1 day @@ -76,5 +78,5 @@ def test_production(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 331.30, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 287.90, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 298.76, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 298.15, delta=0.1) From ac2769339881bed8ed25aa3952bc817d891f07cd Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Sat, 13 Sep 2025 09:49:54 +0200 Subject: [PATCH 28/32] change to ODE solver --- .../entities/assets/heat_buffer.py | 88 ++++++++++++------- unit_test/entities/test_heat_buffer.py | 6 +- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 85bc699b..4f0d1f92 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -15,9 +15,10 @@ """Heat Buffer class.""" from cmath import isinf -from typing import Dict +from typing import Dict, no_type_check import numpy as np +from scipy.integrate import solve_ivp from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( @@ -81,9 +82,8 @@ def __init__( self.tank_volume = volume self.num_layer = 5 self.layer_volume = self.tank_volume / self.num_layer - self.layer_mass = self.layer_volume * fluid_props.get_density( - (self.temperature_in + self.temperature_out) / 2 - ) + self.density = fluid_props.get_density((self.temperature_in + self.temperature_out) / 2) + self.layer_mass = self.layer_volume * self.density self.layer_temperature = np.linspace( self.temperature_in, self.temperature_out, self.num_layer ) @@ -126,12 +126,12 @@ def set_setpoints(self, setpoints: Dict) -> None: self.first_time_step = False else: # After the first time step: use solver temperature - if self.thermal_power_allocation < 0: - self.temperature_in = self.layer_temperature[0] + if self.thermal_power_allocation > 0: + self.temperature_in = setpoints[PROPERTY_TEMPERATURE_IN] self.temperature_out = self.solver_asset.get_temperature(1) else: self.temperature_in = self.solver_asset.get_temperature(0) - self.temperature_out = self.layer_temperature[-1] + self.temperature_out = setpoints[PROPERTY_TEMPERATURE_OUT] self._calculate_massflowrate() self._calculate_new_temperature() @@ -152,36 +152,61 @@ def _calculate_massflowrate(self) -> None: def _calculate_new_temperature(self) -> None: """Calculate new temperature of the tank storage.""" - # heat exchange top side - new_temperature = self.layer_temperature.copy() - if self.mass_flowrate > 0: - new_temperature[0] += min(1, self.mass_flowrate * self.time_step / self.layer_mass) * ( - self.temperature_in - new_temperature[0] - ) + @no_type_check + def tank_ode_charge(t, T): + """ODE 1D tank storage for charging. - # heat exchange between layer - for ii in range(self.num_layer - 1): - new_temperature[ii + 1] += min( - 1, self.mass_flowrate * self.time_step / self.layer_mass - ) * (new_temperature[ii] - new_temperature[ii + 1]) + T: array of layer temperatures [°K] + Returns: dT/dt for each layer + """ + dTdt = np.zeros_like(T) - self.layer_temperature = new_temperature + frac = abs(self.mass_flowrate) / (self.density * self.layer_volume) - self.temperature_out = float(self.layer_temperature[-1]) + # --- Layer 0 (top layer) mixes with inlet flow --- + dTdt[0] = frac * (self.temperature_in - T[0]) - else: - new_temperature[-1] += min( - 1, abs(self.mass_flowrate) * self.time_step / self.layer_mass - ) * (self.temperature_out - new_temperature[-1]) + # --- Remaining layers: plug-flow approximation --- + for i in range(1, self.num_layer): + dTdt[i] = frac * (T[i - 1] - T[i]) + return dTdt - # heat exchange between layer - for ii in range(self.num_layer - 1, 0, -1): - new_temperature[ii - 1] += min( - 1, abs(self.mass_flowrate) * self.time_step / self.layer_mass - ) * (new_temperature[ii] - new_temperature[ii - 1]) + @no_type_check + def tank_ode_discharge(t, T): + """ODE 1D tank storage for discharging. - self.layer_temperature = new_temperature + T: array of layer temperatures [°K] + Returns: dT/dt for each layer + """ + dTdt = np.zeros_like(T) + + frac = abs(self.mass_flowrate) / (self.density * self.layer_volume) + + # --- Layer -1 (bottom layer) mixes with inlet flow --- + dTdt[-1] = frac * (self.temperature_out - T[-1]) + + # --- Remaining layers: plug-flow approximation --- + for i in reversed(range(0, self.num_layer - 1)): + dTdt[i] = frac * (T[i + 1] - T[i]) + return dTdt + + if self.mass_flowrate > 0: + + sol = solve_ivp( + tank_ode_charge, (0, self.time_step), self.layer_temperature, method="RK45" + ) + for i in range(self.num_layer): + self.layer_temperature[i] = sol.y[i][-1] + + self.temperature_out = float(self.layer_temperature[-1]) + + else: + sol = solve_ivp( + tank_ode_discharge, (0, self.time_step), self.layer_temperature, method="RK45" + ) + for i in range(self.num_layer): + self.layer_temperature[i] = sol.y[i][-1] self.temperature_in = float(self.layer_temperature[0]) @@ -191,10 +216,11 @@ def _calculate_new_temperature(self) -> None: def _set_solver_asset_setpoint(self) -> None: """Set the setpoint of solver asset.""" - if self.mass_flowrate > 0: + if self.mass_flowrate >= 0: self.solver_asset.supply_temperature = self.temperature_out else: self.solver_asset.supply_temperature = self.temperature_in + self.solver_asset.mass_flow_rate_set_point = self.mass_flowrate # type: ignore def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None: diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index 0b79da4f..d6d6bd73 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -36,7 +36,7 @@ def setUp(self) -> None: asset_name="heat_buffer", asset_id="heat_buffer_id", port_ids=["test1", "test2"], - volume=25, + volume=1, ) def test_injection(self) -> None: @@ -56,7 +56,7 @@ def test_injection(self) -> None: # Assert self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 358.14, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 357.54, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 354.16, delta=0.1) def test_production(self) -> None: """Test production from Heat Buffer.""" @@ -78,5 +78,5 @@ def test_production(self) -> None: self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 298.76, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 302.23, delta=0.1) self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 298.15, delta=0.1) From b491dce34e4bf3cc3dc521f34464a1222a85d788 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Wed, 17 Sep 2025 13:51:08 +0200 Subject: [PATCH 29/32] add time in asset, integration test with ESDL --- .../controller_storage_mapper.py | 4 +- .../adapter/transforms/esdl_asset_mapper.py | 1 + .../esdl_asset_mappers/heat_buffer_mapper.py | 2 +- .../adapter/transforms/string_to_esdl.py | 2 + .../entities/assets/asset_abstract.py | 9 + .../entities/assets/heat_buffer.py | 27 ++- .../entities/heat_network.py | 1 + testdata/test_heat_buffer.esdl | 225 ++++++++++++++++++ .../test_controller_storage_mapper.py | 4 +- 9 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 testdata/test_heat_buffer.esdl diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_storage_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_storage_mapper.py index d1d0c89f..65ca0b19 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_storage_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mappers/controller_storage_mapper.py @@ -48,8 +48,8 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> ControllerStorage: charge_power = esdl_asset.get_property( esdl_property_name="maxChargeRate", default_value=np.inf ) - temperature_in = esdl_asset.get_temperature("In", "Return") - temperature_out = esdl_asset.get_temperature("Out", "Supply") + temperature_in = esdl_asset.get_temperature("In", "Supply") + temperature_out = esdl_asset.get_temperature("Out", "Return") profile = pd.DataFrame() # esdl_asset.get_profile() contr_storage = ControllerStorage( name=esdl_asset.esdl_asset.name, 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 dbd8f8a8..b984643d 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mapper.py @@ -53,6 +53,7 @@ esdl.HeatPump: EsdlAssetHeatPumpMapper, esdl.ATES: EsdlAssetAtesMapper, esdl.WaterBuffer: EsdlAssetHeatBufferMapper, + esdl.HeatStorage: EsdlAssetHeatBufferMapper, } diff --git a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py index 68ff0bd0..2dfbfb23 100644 --- a/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/esdl_asset_mappers/heat_buffer_mapper.py @@ -40,7 +40,7 @@ def to_entity(self, esdl_asset: EsdlAssetObject) -> AssetAbstract: port_ids=esdl_asset.get_port_ids(), volume=esdl_asset.get_property( esdl_property_name="volume", default_value=HEAT_BUFFER_DEFAULTS.volume - )[0], + ), ) return heat_buffer_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 085d9a3e..da65ec82 100644 --- a/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py +++ b/src/omotes_simulator_core/adapter/transforms/string_to_esdl.py @@ -38,6 +38,8 @@ class StringEsdlAssetMapper: esdl.Joint: "joint", esdl.ATES: "storage", esdl.HeatPump: "pump", + esdl.HeatStorage: "storage", + esdl.WaterBuffer: "storage", } str_to_type_dict = reverse_dict(original_dict=type_to_str_dict) diff --git a/src/omotes_simulator_core/entities/assets/asset_abstract.py b/src/omotes_simulator_core/entities/assets/asset_abstract.py index f9ce9ea9..9179f092 100644 --- a/src/omotes_simulator_core/entities/assets/asset_abstract.py +++ b/src/omotes_simulator_core/entities/assets/asset_abstract.py @@ -18,6 +18,7 @@ from abc import ABC, abstractmethod from pandas import DataFrame, concat +from datetime import datetime from omotes_simulator_core.entities.assets.asset_defaults import ( PROPERTY_MASSFLOW, @@ -65,6 +66,7 @@ def __init__(self, asset_name: str, asset_id: str, connected_ports: list[str]) - self.connected_ports = connected_ports self.outputs = [[] for _ in range(len(self.connected_ports))] self.time_step: float = 3600 # s + self.time = None def __repr__(self) -> str: """Method to print string with the name of the asset.""" @@ -146,6 +148,13 @@ def set_time_step(self, time_step: float) -> None: """ self.time_step = time_step + def set_time(self, time: datetime) -> None: + """Placeholder to set the time for the asset. + + :param float time: The time to set for the asset. + """ + self.time = time + def is_converged(self) -> bool: """Check if the asset has converged. diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index 4f0d1f92..b708bbcc 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -84,22 +84,18 @@ def __init__( self.layer_volume = self.tank_volume / self.num_layer self.density = fluid_props.get_density((self.temperature_in + self.temperature_out) / 2) self.layer_mass = self.layer_volume * self.density - self.layer_temperature = np.linspace( - self.temperature_in, self.temperature_out, self.num_layer - ) + self.layer_temperature = np.ones(self.num_layer) * (DEFAULT_TEMPERATURE) # Thermal power allocation [W] for injection (positive) or production (negative) by the # asset and mass flowrate [kg/s] going in or out by the asset self.thermal_power_allocation = 0 self.mass_flowrate = 0 - - # HeatBoundary since heat buffer acts either as producer or consumer, - # positive flow is discharge and negative flow is charge self.solver_asset = HeatBoundary(name=self.name, _id=self.asset_id) self.output: list = [] self.first_time_step = True - self.energy = 0.0 + self.energy_stored = 0.0 + self.current_time = None def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. @@ -107,6 +103,10 @@ 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 """ + # don't do any calculation if the time is still the same to avoid storage's state problem + if self.current_time == self.time: + return + self.current_time = self.time # Default keys required necessary_setpoints = { PROPERTY_TEMPERATURE_IN, @@ -126,7 +126,7 @@ def set_setpoints(self, setpoints: Dict) -> None: self.first_time_step = False else: # After the first time step: use solver temperature - if self.thermal_power_allocation > 0: + if self.thermal_power_allocation >= 0: self.temperature_in = setpoints[PROPERTY_TEMPERATURE_IN] self.temperature_out = self.solver_asset.get_temperature(1) else: @@ -191,7 +191,7 @@ def tank_ode_discharge(t, T): dTdt[i] = frac * (T[i + 1] - T[i]) return dTdt - if self.mass_flowrate > 0: + if self.mass_flowrate >= 0: sol = solve_ivp( tank_ode_charge, (0, self.time_step), self.layer_temperature, method="RK45" @@ -210,8 +210,13 @@ def tank_ode_discharge(t, T): self.temperature_in = float(self.layer_temperature[0]) - self.energy = self.energy + self.mass_flowrate * self.time_step / 3600 * 4180 * ( - self.temperature_in - self.temperature_out + self.energy_stored = ( + self.energy_stored + + self.mass_flowrate + * self.time_step + / 3600 + * 4180 + * (self.temperature_in - self.temperature_out) ) def _set_solver_asset_setpoint(self) -> None: diff --git a/src/omotes_simulator_core/entities/heat_network.py b/src/omotes_simulator_core/entities/heat_network.py index bf9130fe..886c431a 100644 --- a/src/omotes_simulator_core/entities/heat_network.py +++ b/src/omotes_simulator_core/entities/heat_network.py @@ -55,6 +55,7 @@ def run_time_step( for py_asset in self.assets: if py_asset.asset_id in controller_input: py_asset.set_time_step(time_step) + py_asset.set_time(time) py_asset.set_setpoints(controller_input[py_asset.asset_id]) self.solver.solve() diff --git a/testdata/test_heat_buffer.esdl b/testdata/test_heat_buffer.esdl new file mode 100644 index 00000000..924bb26a --- /dev/null +++ b/testdata/test_heat_buffer.esdl @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unit_test/adapters/transforms/controller_mappers/test_controller_storage_mapper.py b/unit_test/adapters/transforms/controller_mappers/test_controller_storage_mapper.py index e78f2be6..92c4db5d 100644 --- a/unit_test/adapters/transforms/controller_mappers/test_controller_storage_mapper.py +++ b/unit_test/adapters/transforms/controller_mappers/test_controller_storage_mapper.py @@ -47,7 +47,7 @@ def test_to_entity_method(self): controller_storage = self.mapper.to_entity(storage_assets[0]) # Assert - self.assertEqual(controller_storage.temperature_out, 353.15) - self.assertEqual(controller_storage.temperature_in, 313.15) + self.assertEqual(controller_storage.temperature_in, 353.15) + self.assertEqual(controller_storage.temperature_out, 313.15) self.assertEqual(controller_storage.max_charge_power, 11.61e6) self.assertEqual(controller_storage.max_discharge_power, 11.61e6) From 3ab1f8c8c40b710dc460c4c48d460b0f71260e9c Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Wed, 17 Sep 2025 14:06:31 +0200 Subject: [PATCH 30/32] fix linting, formatting, typecheck --- ci/win32/format.cmd | 2 +- src/omotes_simulator_core/entities/assets/asset_abstract.py | 4 ++-- src/omotes_simulator_core/entities/assets/heat_buffer.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/win32/format.cmd b/ci/win32/format.cmd index 8a871a77..be50a1ab 100644 --- a/ci/win32/format.cmd +++ b/ci/win32/format.cmd @@ -5,5 +5,5 @@ pushd . cd /D "%~dp0" cd ..\..\ black ./src/omotes_simulator_core ./unit_test/ -isort --diff ./src/omotes_simulator_core ./unit_test/ +isort ./src/omotes_simulator_core ./unit_test/ popd diff --git a/src/omotes_simulator_core/entities/assets/asset_abstract.py b/src/omotes_simulator_core/entities/assets/asset_abstract.py index 9179f092..d01a4f45 100644 --- a/src/omotes_simulator_core/entities/assets/asset_abstract.py +++ b/src/omotes_simulator_core/entities/assets/asset_abstract.py @@ -16,9 +16,9 @@ """Abstract class for asset.""" from abc import ABC, abstractmethod +from datetime import datetime from pandas import DataFrame, concat -from datetime import datetime from omotes_simulator_core.entities.assets.asset_defaults import ( PROPERTY_MASSFLOW, @@ -66,7 +66,7 @@ def __init__(self, asset_name: str, asset_id: str, connected_ports: list[str]) - self.connected_ports = connected_ports self.outputs = [[] for _ in range(len(self.connected_ports))] self.time_step: float = 3600 # s - self.time = None + self.time = datetime.now() def __repr__(self) -> str: """Method to print string with the name of the asset.""" diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index b708bbcc..bc319f10 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -19,6 +19,7 @@ import numpy as np from scipy.integrate import solve_ivp +from datetime import datetime from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( @@ -95,7 +96,7 @@ def __init__( self.output: list = [] self.first_time_step = True self.energy_stored = 0.0 - self.current_time = None + self.current_time = datetime.now() def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. From 721516d8cb847b18d1d8656527496e614a16063f Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Wed, 17 Sep 2025 14:21:27 +0200 Subject: [PATCH 31/32] fix formatting and unit test buffer with time --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 2 +- unit_test/entities/test_heat_buffer.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index bc319f10..ac42ed35 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -15,11 +15,11 @@ """Heat Buffer class.""" from cmath import isinf +from datetime import datetime from typing import Dict, no_type_check import numpy as np from scipy.integrate import solve_ivp -from datetime import datetime from omotes_simulator_core.entities.assets.asset_abstract import AssetAbstract from omotes_simulator_core.entities.assets.asset_defaults import ( diff --git a/unit_test/entities/test_heat_buffer.py b/unit_test/entities/test_heat_buffer.py index d6d6bd73..5e1fa1d2 100644 --- a/unit_test/entities/test_heat_buffer.py +++ b/unit_test/entities/test_heat_buffer.py @@ -15,6 +15,7 @@ """Test Heat Buffer entities.""" import unittest +from datetime import datetime import numpy as np @@ -52,6 +53,7 @@ def test_injection(self) -> None: # charging for 1 day for _ii in range(0, 24): self.heat_buffer.first_time_step = True + self.heat_buffer.set_time(datetime(2023, 1, 1, _ii, 0, 0)) self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert @@ -75,8 +77,9 @@ def test_production(self) -> None: # discharging for 1 day for _ii in range(0, 24): self.heat_buffer.first_time_step = True + self.heat_buffer.set_time(datetime(2023, 1, 1, _ii, 0, 0)) self.heat_buffer.set_setpoints(setpoints=setpoints) # Assert self.assertAlmostEqual(self.heat_buffer.layer_temperature[0], 302.23, delta=0.1) - self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 298.15, delta=0.1) + self.assertAlmostEqual(self.heat_buffer.layer_temperature[-1], 298.16, delta=0.1) From edf3df5ad9bfb00b8371a0c783b111289c7ddec3 Mon Sep 17 00:00:00 2001 From: Ryvo Octaviano Date: Fri, 19 Sep 2025 09:36:40 +0200 Subject: [PATCH 32/32] update typecheck for numpy --- src/omotes_simulator_core/entities/assets/heat_buffer.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_buffer.py b/src/omotes_simulator_core/entities/assets/heat_buffer.py index ac42ed35..b6b6be69 100644 --- a/src/omotes_simulator_core/entities/assets/heat_buffer.py +++ b/src/omotes_simulator_core/entities/assets/heat_buffer.py @@ -16,7 +16,7 @@ """Heat Buffer class.""" from cmath import isinf from datetime import datetime -from typing import Dict, no_type_check +from typing import Dict import numpy as np from scipy.integrate import solve_ivp @@ -154,8 +154,7 @@ def _calculate_massflowrate(self) -> None: def _calculate_new_temperature(self) -> None: """Calculate new temperature of the tank storage.""" - @no_type_check - def tank_ode_charge(t, T): + def tank_ode_charge(t: float, T: np.ndarray) -> np.ndarray: """ODE 1D tank storage for charging. T: array of layer temperatures [°K] @@ -173,8 +172,7 @@ def tank_ode_charge(t, T): dTdt[i] = frac * (T[i - 1] - T[i]) return dTdt - @no_type_check - def tank_ode_discharge(t, T): + def tank_ode_discharge(t: float, T: np.ndarray) -> np.ndarray: """ODE 1D tank storage for discharging. T: array of layer temperatures [°K]