From ae3a6a02b5e2f5101e0c7d4974abf99ea0b9429c Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 09:37:36 +0100 Subject: [PATCH 01/10] Added possibility in controller to set the heatpump to bypass mode --- .../adapter/transforms/controller_mapper.py | 10 ++- .../controller/controller_heat_transfer.py | 42 +++++++----- .../assets/controller/controller_network.py | 22 ++++--- .../entities/assets/production_cluster.py | 6 +- .../entities/network_controller.py | 66 +++++++++++++++++-- 5 files changed, 107 insertions(+), 39 deletions(-) diff --git a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py index 8fdfaaa3..be785de9 100644 --- a/src/omotes_simulator_core/adapter/transforms/controller_mapper.py +++ b/src/omotes_simulator_core/adapter/transforms/controller_mapper.py @@ -158,9 +158,15 @@ def to_entity( "The network is looped via the heat pumps and heat exchangers, " "which is not supported." ) + # find first networks with no buffers + for j in range(len(networks)): + if not networks[j].storages: + break - for i in range(1, len(networks)): - networks[i].path = graph.get_path(str(i), "0") + for i in range(len(networks)): + if i == j: + continue + networks[i].path = graph.get_path(str(i), str(j)) if len(networks[i].path) > 3: raise RuntimeError( "The network is connected via more then two stages which is not supported." diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index f7ce1e68..fe77bae5 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -41,26 +41,34 @@ def __init__(self, name: str, identifier: str, factor: float): super().__init__(name, identifier) self.factor = factor - def set_asset(self, heat_demand: float) -> dict[str, dict[str, float]]: + def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[str, float]]: """Method to set the asset to the given heat demand. The supply and return temperatures are also set. :param float heat_demand: Heat demand to set. + :param bypass: When true the heat exchange is bypassed, so the heat demand is not reduced by the factor. Default is False. """ - # TODO set correct values also for prim and secondary side. - return { - self.id: { - PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, - PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30, - PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 40, - SECONDARY - + PROPERTY_HEAT_DEMAND: np.abs(heat_demand) - * self.factor - * ( - np.sign(heat_demand) * -1 - ), # Invert sign of secondary heat demand, as it is opposite to primary side. - SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, - SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, - PROPERTY_SET_PRESSURE: False, + if bypass: + return { + self.id: { + PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, + PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 50, + PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 80, + SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * 1, + SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, + SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, + PROPERTY_SET_PRESSURE: False, + } + } + else: + return { + self.id: { + PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, + PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30, + PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 40, + SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * self.factor, + SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, + SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, + PROPERTY_SET_PRESSURE: False, + } } - } diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index 17442f89..5cdade65 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -16,6 +16,8 @@ import datetime +from numpy.ma.core import product + from omotes_simulator_core.entities.assets.asset_defaults import ( PROPERTY_HEAT_DEMAND, PROPERTY_SET_PRESSURE, @@ -49,7 +51,7 @@ class ControllerNetwork: """List of all producers in the network.""" storages: list[ControllerAtesStorage | ControllerIdealHeatStorage] """List of all storages in the network.""" - factor_to_first_network: float + factor_to_first_network: list[float] """Factor to calculate power in the first network in the list of networks.""" path: list[str] """Path from this network to the first network in the total system.""" @@ -69,7 +71,7 @@ def __init__( self.consumers = consumers_in self.producers = producers_in self.storages = storages_in - self.factor_to_first_network = factor_to_first_network + self.factor_to_first_network = [factor_to_first_network] self.path: list[str] = [] def exists(self, identifier: str) -> bool: @@ -91,9 +93,8 @@ def exists(self, identifier: str) -> bool: def get_total_heat_demand(self, time: datetime.datetime) -> float: """Method which the total heat demand at the given time corrected to the first network.""" - return ( - sum([consumer.get_heat_demand(time) for consumer in self.consumers]) - * self.factor_to_first_network + return sum([consumer.get_heat_demand(time) for consumer in self.consumers]) * product( + self.factor_to_first_network ) def get_total_discharge_storage(self) -> float: @@ -121,9 +122,8 @@ def get_total_supply(self) -> float: :return float: Total heat supply of all producers. """ - return ( - float(sum([producer.power for producer in self.producers])) - * self.factor_to_first_network + return float(sum([producer.power for producer in self.producers])) * product( + self.factor_to_first_network ) def set_supply_to_max(self, priority: int = 0) -> dict: @@ -233,8 +233,10 @@ def set_pressure(self) -> str: The network can thus pass back the id for which asset the pressure needs to be set. The controller can then do this. """ - if self.heat_transfer_assets_sec: - return self.heat_transfer_assets_sec[0].id if self.producers: return self.producers[0].id + if self.heat_transfer_assets_sec: + return self.heat_transfer_assets_sec[0].id + if self.heat_transfer_assets_prim: + return self.heat_transfer_assets_prim[0].id raise ValueError("No asset found for which the pressure can be set.") diff --git a/src/omotes_simulator_core/entities/assets/production_cluster.py b/src/omotes_simulator_core/entities/assets/production_cluster.py index 24567386..74fbcecb 100644 --- a/src/omotes_simulator_core/entities/assets/production_cluster.py +++ b/src/omotes_simulator_core/entities/assets/production_cluster.py @@ -124,8 +124,8 @@ def _set_heat_demand(self, heat_demand: float) -> None: """ # Calculate the mass flow rate self.heat_demand_set_point = heat_demand - self.controlled_mass_flow = heat_demand_and_temperature_to_mass_flow( - thermal_demand=-1 * heat_demand, + self.controlled_mass_flow = -heat_demand_and_temperature_to_mass_flow( + thermal_demand=heat_demand, temperature_in=self.temperature_in, temperature_out=self.temperature_out, ) @@ -248,7 +248,7 @@ def is_converged(self) -> bool: :return: True if the asset has converged, False otherwise """ if self.solver_asset.pre_scribe_mass_flow: # type: ignore - return abs(self.get_actual_heat_supplied() - self.heat_demand_set_point) < ( + return abs(self.get_actual_heat_supplied() + self.heat_demand_set_point) < ( abs(self.heat_demand_set_point * 0.001) ) else: diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 7e5d5392..0c89a6ae 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -12,7 +12,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Module for new controller which can also cope with Heat pumps and heat exchangers.""" +"""Module for controller which can also cope with Heat pumps and heat exchangers.""" import datetime import logging @@ -53,19 +53,19 @@ def update_networks_factor(self) -> None: """Method to update the factor of the networks taken into account the changing COP.""" for network in self.networks: current_network = network - network.factor_to_first_network = 1 + network.factor_to_first_network = [1] for step in network.path: if current_network == self.networks[int(step)]: continue for asset in current_network.heat_transfer_assets_prim: if self.networks[int(step)].exists(asset.id): - network.factor_to_first_network *= asset.factor - current_network = self.networks[int(step)] + network.factor_to_first_network.append(asset.factor) + # current_network = self.networks[int(step)] break for asset in current_network.heat_transfer_assets_sec: if self.networks[int(step)].exists(asset.id): - network.factor_to_first_network /= asset.factor - current_network = self.networks[int(step)] + network.factor_to_first_network.append(1 / asset.factor) + # current_network = self.networks[int(step)] break def update_setpoints(self, time: datetime.datetime) -> dict: @@ -86,6 +86,50 @@ def update_setpoints(self, time: datetime.datetime) -> dict: self.update_networks_factor() total_demand = sum([network.get_total_heat_demand(time) for network in self.networks]) total_supply = sum([network.get_total_supply() for network in self.networks]) + if total_supply > total_demand: + # total supply is larger than demand, so demand can be set to required demand. + consumers = self._set_consumer_to_demand(time) + surplus_supply = total_supply - total_demand + # Check charge capacity from storage + total_charge_storage = sum( + [network.get_total_charge_storage() for network in self.networks] + ) + if total_charge_storage > surplus_supply: + # there is more charge capacity than surplus supply, so we can set source to max and storages to charge with the surplus supply. + producers = self._set_producers_to_max() + storages = self._set_storages_charge_power(surplus_supply) + else: + # The storage can charge to max. The sources need to be capped. + storages = self._set_all_storages_charge_to_max() + producers = self._set_producers_based_on_priority( + surplus_supply + total_charge_storage + ) + else: + # total supply is lower than demand, so we need to check if there is enough discharge capacity from storage. + total_discharge_storage = sum( + [network.get_total_discharge_storage() for network in self.networks] + ) + if (total_supply + total_discharge_storage) <= total_demand: + logger.warning( + f"Total supply + storage is lower than total demand at time: {time}" + f"Consumers are capped to the available power." + ) + factor = (total_supply + total_discharge_storage) / total_demand + producers = self._set_producers_to_max() + + storages = self._set_all_storages_discharge_to_max() + consumers = self._set_consumer_to_demand(time, factor=factor) + else: + # there is enough supply + storage to cover the demand. sources to max and storages to deliver the rest. + consumers = self._set_consumer_to_demand(time) + surplus_demand = total_supply - total_demand + producers = self._set_producers_to_max() + storages = self._set_storages_discharge_power(surplus_demand) + producers.update(consumers) + producers.update(storages) + + # Getting the settings for the heat transfer assets + heat_transfer = {} total_charge_storage = sum( [network.get_total_charge_storage() for network in self.networks] ) @@ -165,8 +209,16 @@ def update_setpoints(self, time: datetime.datetime) -> dict: # this might look weird, but we know there is only one primary or secondary asset. # So we can directly set it. for asset in network.heat_transfer_assets_prim: - heat_transfer.update(asset.set_asset(total_heat_supply)) + if total_heat_supply > 0: + heat_transfer.update(asset.set_asset(total_heat_supply)) + else: + heat_transfer.update(asset.set_asset(total_heat_supply, True)) for asset in network.heat_transfer_assets_sec: + if total_heat_supply > 0: + heat_transfer.update(asset.set_asset(-total_heat_supply, True)) + else: + heat_transfer.update(asset.set_asset(-total_heat_supply)) + producers.update(heat_transfer) heat_transfer.update(asset.set_asset(-total_heat_supply)) # Update the asset setpoints with the heat transfer setpoints. From 085e6db6f075249cd0ea022e335fdf74436408c7 Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 13:15:49 +0100 Subject: [PATCH 02/10] Fixed two small errors which occured due to rebasing --- .../assets/controller/controller_network.py | 14 ++++++-------- .../entities/network_controller.py | 2 -- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index 5cdade65..eb655050 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -102,20 +102,18 @@ def get_total_discharge_storage(self) -> float: :return float: Total heat discharge of all storages. """ - return ( - float(sum([storage.effective_max_discharge_power for storage in self.storages])) - * self.factor_to_first_network - ) + return float( + sum([storage.effective_max_discharge_power for storage in self.storages]) + ) * product(self.factor_to_first_network) def get_total_charge_storage(self) -> float: """Method to get the total storage charge of the network corrected to the first network. :return float: Total heat charge of all storages. """ - return ( - float(sum([storage.effective_max_charge_power for storage in self.storages])) - * self.factor_to_first_network - ) + return float( + sum([storage.effective_max_charge_power for storage in self.storages]) + ) * product(self.factor_to_first_network[:-2]) def get_total_supply(self) -> float: """Method to get the total heat supply of the network. diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 0c89a6ae..f6766236 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -218,8 +218,6 @@ def update_setpoints(self, time: datetime.datetime) -> dict: heat_transfer.update(asset.set_asset(-total_heat_supply, True)) else: heat_transfer.update(asset.set_asset(-total_heat_supply)) - producers.update(heat_transfer) - heat_transfer.update(asset.set_asset(-total_heat_supply)) # Update the asset setpoints with the heat transfer setpoints. asset_setpoints.update(heat_transfer) From 30065d2a4ec73866e7cda9880946103a42b2d20c Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 13:40:23 +0100 Subject: [PATCH 03/10] Small fixes in the controller to get the correct output --- .../controller/controller_heat_transfer.py | 2 +- .../assets/controller/controller_network.py | 4 + .../entities/network_controller.py | 76 ++++--------------- 3 files changed, 20 insertions(+), 62 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index fe77bae5..d1b655a0 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -54,7 +54,7 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 50, PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 80, - SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * 1, + SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * -1, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, PROPERTY_SET_PRESSURE: False, diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index eb655050..56a55fc9 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -164,6 +164,8 @@ def set_storage_charge_power(self, factor: float = 1) -> dict: for storage in self.storages: storage_settings[storage.id] = { PROPERTY_HEAT_DEMAND: +1 * storage.effective_max_charge_power * factor, + PROPERTY_TEMPERATURE_OUT: storage.temperature_out, + PROPERTY_TEMPERATURE_IN: storage.temperature_in, } return storage_settings @@ -178,6 +180,8 @@ def set_storage_discharge_power(self, factor: float = 1) -> dict: # Discharging is negative (e.g., heat from component/system to the network) storage_settings[storage.id] = { PROPERTY_HEAT_DEMAND: -1 * storage.effective_max_discharge_power * factor, + PROPERTY_TEMPERATURE_OUT: storage.temperature_out, + PROPERTY_TEMPERATURE_IN: storage.temperature_in, } return storage_settings diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index f6766236..78d67976 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -86,9 +86,14 @@ def update_setpoints(self, time: datetime.datetime) -> dict: self.update_networks_factor() total_demand = sum([network.get_total_heat_demand(time) for network in self.networks]) total_supply = sum([network.get_total_supply() for network in self.networks]) + + # Initialize the producer, consumer, and storage setpoints dicts. + producer_setpoints: AssetSetpointsDict = {} + consumer_setpoints: AssetSetpointsDict = {} + storage_setpoints: AssetSetpointsDict = {} if total_supply > total_demand: # total supply is larger than demand, so demand can be set to required demand. - consumers = self._set_consumer_to_demand(time) + consumer_setpoints = self._set_consumer_to_demand(time) surplus_supply = total_supply - total_demand # Check charge capacity from storage total_charge_storage = sum( @@ -96,12 +101,12 @@ def update_setpoints(self, time: datetime.datetime) -> dict: ) if total_charge_storage > surplus_supply: # there is more charge capacity than surplus supply, so we can set source to max and storages to charge with the surplus supply. - producers = self._set_producers_to_max() - storages = self._set_storages_charge_power(surplus_supply) + producer_setpoints = self._set_producers_to_max() + storage_setpoints = self._set_storages_charge_power(surplus_supply) else: # The storage can charge to max. The sources need to be capped. - storages = self._set_all_storages_charge_to_max() - producers = self._set_producers_based_on_priority( + storage_setpoints = self._set_all_storages_charge_to_max() + producer_setpoints = self._set_producers_based_on_priority( surplus_supply + total_charge_storage ) else: @@ -115,67 +120,16 @@ def update_setpoints(self, time: datetime.datetime) -> dict: f"Consumers are capped to the available power." ) factor = (total_supply + total_discharge_storage) / total_demand - producers = self._set_producers_to_max() + producer_setpoints = self._set_producers_to_max() - storages = self._set_all_storages_discharge_to_max() - consumers = self._set_consumer_to_demand(time, factor=factor) + storage_setpoints = self._set_all_storages_discharge_to_max() + consumer_setpoints = self._set_consumer_to_demand(time, factor=factor) else: # there is enough supply + storage to cover the demand. sources to max and storages to deliver the rest. - consumers = self._set_consumer_to_demand(time) + consumer_setpoints = self._set_consumer_to_demand(time) surplus_demand = total_supply - total_demand - producers = self._set_producers_to_max() - storages = self._set_storages_discharge_power(surplus_demand) - producers.update(consumers) - producers.update(storages) - - # Getting the settings for the heat transfer assets - heat_transfer = {} - total_charge_storage = sum( - [network.get_total_charge_storage() for network in self.networks] - ) - total_discharge_storage = sum( - [network.get_total_discharge_storage() for network in self.networks] - ) - - # Initialize the producer, consumer, and storage setpoints dicts. - producer_setpoints: AssetSetpointsDict = {} - consumer_setpoints: AssetSetpointsDict = {} - storage_setpoints: AssetSetpointsDict = {} - - if (total_supply + total_discharge_storage) <= total_demand: - logger.warning( - "Total supply + storage is lower than total demand at time: %s" - "Consumers are capped to the available power.", - time, - ) - factor = (total_supply + total_discharge_storage) / total_demand - # Define setpoints - producer_setpoints = self._set_producers_to_max() - storage_setpoints = self._set_all_storages_discharge_to_max() - consumer_setpoints = self._set_consumer_to_demand(time, factor=factor) - else: - # Set consumer to requested demand. - consumer_setpoints = self._set_consumer_to_demand(time, factor=1.0) - # Set producers and storages based on the supply and demand, and the charge and - # discharge capacity of the storage. - if total_supply >= total_demand: - # there is a surplus of supply we can charge the storage, storage becomes consumer. - surplus_supply = total_supply - total_demand - if surplus_supply <= total_charge_storage: - storage_setpoints = self._set_storages_charge_power(surplus_supply) - producer_setpoints = self._set_producers_to_max() - elif surplus_supply > total_charge_storage: - # need to cap the power of the source based on priority - storage_setpoints = self._set_storages_charge_power(total_charge_storage) - producer_setpoints = self._set_producers_based_on_priority( - total_demand + total_charge_storage - ) - else: - # there is a deficit of supply we can discharge the storage, storage becomes - # producer. - deficit_supply = total_demand - total_supply - storage_setpoints = self._set_storages_discharge_power(deficit_supply) producer_setpoints = self._set_producers_to_max() + storage_setpoints = self._set_storages_discharge_power(surplus_demand) # Update the asset setpoints with the setpoints of the producers, consumers, # and storages. From 0d2ff22cdcc8ebf1ee412327fcc24a83caab3829 Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 14:12:27 +0100 Subject: [PATCH 04/10] Added bypass mode to heatpump --- .../entities/assets/heat_pump.py | 18 ++++- .../network/assets/heat_transfer_asset.py | 71 +++++++++++++------ 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index 6f298ab4..f5104555 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -63,6 +63,12 @@ class HeatPump(AssetAbstract): and the pressure is predescribed. """ + control_mass_flow_primary: bool + """Flag to indicate whether the mass flow rate on the primary side is controlled. + If True, the mass flow rate is controlled. If False, the mass flow rate is not controlled + and the pressure is predescribed. + """ + coefficient_of_performance: float """Coefficient of perfomance for the heat pump.""" @@ -129,7 +135,10 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: temperature_in=self.temperature_in_secondary, temperature_out=self.temperature_out_secondary, ) - self.control_mass_flow_secondary = not (setpoints_secondary[PROPERTY_SET_PRESSURE]) + self.control_mass_flow_secondary = not ( + setpoints_secondary[PROPERTY_SET_PRESSURE] + & (setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] < 0) + ) # Assign setpoints to the HeatTransferAsset solver asset self.solver_asset.temperature_in_secondary = self.temperature_in_secondary # type: ignore @@ -176,6 +185,10 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: temperature_in=self.temperature_in_primary, temperature_out=self.temperature_out_primary, ) + self.control_mass_flow_primary = not ( + setpoints_primary[PROPERTY_SET_PRESSURE] + & (setpoints_primary[PRIMARY + PROPERTY_HEAT_DEMAND] < 0) + ) # Assign setpoints to the HeatTransferAsset solver asset self.solver_asset.temperature_in_primary = self.temperature_in_primary # type: ignore @@ -183,6 +196,9 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: self.solver_asset.mass_flow_initialization_primary = ( # type: ignore self.mass_flow_initialization_primary ) + self.solver_asset.pre_scribe_mass_flow_primary = ( # type: ignore + self.control_mass_flow_primary + ) def set_setpoints(self, setpoints: Dict) -> None: """Placeholder to set the setpoints of an asset prior to a simulation. diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index 8f17d98f..b1a49e27 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -44,6 +44,7 @@ def __init__( mass_flow_initialization_primary: float = -20.0, heat_transfer_coefficient: float = 1.0, pre_scribe_mass_flow_secondary: bool = False, + pre_scribe_mass_flow_primary: bool = False, temperature_out_secondary: float = 293.15, mass_flow_rate_set_point_secondary: float = -80.0, pressure_set_point_secondary: float = 10000.0, @@ -94,6 +95,9 @@ def __init__( # Define the flag that indicates whether the mass flow rate or the pressure is prescribed # at the hot side of the heat pump self.pre_scribe_mass_flow_secondary = pre_scribe_mass_flow_secondary + # Define the flag that indicates whether the mass flow rate or the pressure is prescribed + # at the cold side of the heat pump + self.pre_scribe_mass_flow_primary = pre_scribe_mass_flow_primary # Define the mass flow rate set point for the asset on the secondary side self.mass_flow_rate_rate_set_point_secondary = mass_flow_rate_set_point_secondary # Define the pressure set point for the asset @@ -382,34 +386,57 @@ def get_equations(self) -> list[EquationObject]: self.secondary_side_outflow, ]: equations.append(self.get_press_to_node_equation(connection_point=connection_point)) - - # -- Internal continuity (1x) -- - # Add the internal continuity equation at the primary side. - equations.append( - self.add_continuity_equation( - connection_point_1=self.primary_side_inflow, - connection_point_2=self.primary_side_outflow, - ) - ) - # -- Energy balance equation for the heat transfer asset (1x) -- - # Defines the energy balance between the primary and secondary side of the - # heat transfer asset. - # If the mass flow at the inflow node of the primary and secondary side is not zero, - if (iteration_flow_direction_primary != FlowDirection.ZERO) or ( - iteration_flow_direction_secondary != FlowDirection.ZERO - ): + if self.pre_scribe_mass_flow_primary: + # -- Internal continuity (1x) -- + # Add the internal continuity equation at the primary side. equations.append( - self.prescribe_mass_flow_at_connection_point( - connection_point=self.primary_side_inflow, - mass_flow_value=self.get_mass_flow_from_prev_solution(), + self.add_continuity_equation( + connection_point_1=self.primary_side_inflow, + connection_point_2=self.primary_side_outflow, ) ) - # If the mass flow at the inflow node of the primary and secondary side is zero, + # -- Energy balance equation for the heat transfer asset (1x) -- + # Defines the energy balance between the primary and secondary side of the + # heat transfer asset. + # If the mass flow at the inflow node of the primary and secondary side is not zero, + if (iteration_flow_direction_primary != FlowDirection.ZERO) or ( + iteration_flow_direction_secondary != FlowDirection.ZERO + ): + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=self.primary_side_inflow, + mass_flow_value=self.get_mass_flow_from_prev_solution(), + ) + ) + # If the mass flow at the inflow node of the primary and secondary side is zero, + else: + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=self.primary_side_inflow, + mass_flow_value=0, + ) + ) else: + if iteration_flow_direction_secondary == FlowDirection.ZERO: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary + else: + if iteration_flow_direction_secondary == FlowDirection.POSITIVE: + pset_out = self.pressure_set_point_secondary / 2 + pset_in = self.pressure_set_point_secondary + else: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary / 2 equations.append( - self.prescribe_mass_flow_at_connection_point( + self.prescribe_pressure_at_connection_point( connection_point=self.primary_side_inflow, - mass_flow_value=0, + pressure_value=pset_in, + ) + ) + equations.append( + self.prescribe_pressure_at_connection_point( + connection_point=self.primary_side_outflow, + pressure_value=pset_out, ) ) # Return the equations From 23d30256eb95f35fd77084bf855a9edd3d5267f4 Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 17:03:21 +0100 Subject: [PATCH 05/10] Rewrote equations setting of heatpump. --- .../entities/assets/heat_pump.py | 6 +- .../network/assets/heat_transfer_asset.py | 252 ++++++++++++++---- 2 files changed, 209 insertions(+), 49 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index f5104555..6a8d1024 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -131,7 +131,7 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_OUT] self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( - thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND], + thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] * -1, temperature_in=self.temperature_in_secondary, temperature_out=self.temperature_out_secondary, ) @@ -145,7 +145,7 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: self.solver_asset.temperature_out_secondary = ( # type: ignore self.temperature_out_secondary ) - self.solver_asset.mass_flow_rate_secondary = self.mass_flow_secondary # type: ignore + self.solver_asset.mass_flow_rate_rate_set_point_secondary = self.mass_flow_secondary # type: ignore self.solver_asset.pre_scribe_mass_flow_secondary = ( # type: ignore self.control_mass_flow_secondary ) @@ -180,7 +180,7 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: # Assign setpoints to the HeatPump asset self.temperature_in_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_OUT] - self.mass_flow_initialization_primary = heat_demand_and_temperature_to_mass_flow( + self.mass_flow_initialization_primary = -heat_demand_and_temperature_to_mass_flow( thermal_demand=setpoints_primary[PRIMARY + PROPERTY_HEAT_DEMAND], temperature_in=self.temperature_in_primary, temperature_out=self.temperature_out_primary, diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index b1a49e27..438d24a4 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -104,9 +104,11 @@ def __init__( self.pressure_set_point_secondary = pressure_set_point_secondary # Define flow directions self.flow_direction_primary = self.flow_direction(self.mass_flow_initialization_primary) + self.iteration_flow_direction_primary = self.flow_direction_primary self.flow_direction_secondary = self.flow_direction( self.mass_flow_rate_rate_set_point_secondary ) + self.iteration_flow_direction_secondary = self.flow_direction_secondary # Define connection points ( self.primary_side_inflow, @@ -128,9 +130,9 @@ def flow_direction(self, mass_flow: float) -> FlowDirection: The flow direction of the heat transfer asset. """ if mass_flow > MASSFLOW_ZERO_LIMIT: - return FlowDirection.NEGATIVE - elif mass_flow < -MASSFLOW_ZERO_LIMIT: return FlowDirection.POSITIVE + elif mass_flow < -MASSFLOW_ZERO_LIMIT: + return FlowDirection.NEGATIVE else: return FlowDirection.ZERO @@ -165,54 +167,212 @@ def get_ordered_connection_point_list(self) -> list[int]: """ # Determine the connection points based on the flow direction if ( - self.flow_direction_primary == FlowDirection.NEGATIVE - and self.flow_direction_secondary == FlowDirection.POSITIVE + self.iteration_flow_direction_primary == FlowDirection.NEGATIVE + and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE ): return [1, 0, 2, 3] elif ( - self.flow_direction_primary == FlowDirection.POSITIVE - and self.flow_direction_secondary == FlowDirection.POSITIVE + self.iteration_flow_direction_primary == FlowDirection.POSITIVE + and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE ): return [0, 1, 2, 3] elif ( - self.flow_direction_primary == FlowDirection.POSITIVE - and self.flow_direction_secondary == FlowDirection.NEGATIVE + self.iteration_flow_direction_primary == FlowDirection.POSITIVE + and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE ): return [0, 1, 3, 2] elif ( - self.flow_direction_primary == FlowDirection.NEGATIVE - and self.flow_direction_secondary == FlowDirection.NEGATIVE + self.iteration_flow_direction_primary == FlowDirection.NEGATIVE + and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE ): return [1, 0, 3, 2] elif ( - self.flow_direction_primary == FlowDirection.ZERO - and self.flow_direction_secondary == FlowDirection.ZERO + self.iteration_flow_direction_primary == FlowDirection.ZERO + and self.iteration_flow_direction_secondary == FlowDirection.ZERO ): return [0, 1, 2, 3] elif ( - self.flow_direction_primary == FlowDirection.ZERO - and self.flow_direction_secondary == FlowDirection.POSITIVE + self.iteration_flow_direction_primary == FlowDirection.ZERO + and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE ): return [0, 1, 2, 3] elif ( - self.flow_direction_primary == FlowDirection.ZERO - and self.flow_direction_secondary == FlowDirection.NEGATIVE + self.iteration_flow_direction_primary == FlowDirection.ZERO + and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE ): return [0, 1, 3, 2] elif ( - self.flow_direction_primary == FlowDirection.POSITIVE - and self.flow_direction_secondary == FlowDirection.ZERO + self.iteration_flow_direction_primary == FlowDirection.POSITIVE + and self.iteration_flow_direction_secondary == FlowDirection.ZERO ): return [0, 1, 2, 3] elif ( - self.flow_direction_primary == FlowDirection.NEGATIVE - and self.flow_direction_secondary == FlowDirection.ZERO + self.iteration_flow_direction_primary == FlowDirection.NEGATIVE + and self.iteration_flow_direction_secondary == FlowDirection.ZERO ): return [1, 0, 2, 3] else: return [0, 1, 2, 3] def get_equations(self) -> list[EquationObject]: + equations = [] + # pressure to node equations + for connection_point in range(4): + equations.append(self.get_press_to_node_equation(connection_point=connection_point)) + + # Internal energy to node equations + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=0, + use_relative_indexing=True, + ) + ] + < 0.0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=0, + supply_temperature=self.temperature_out_primary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=1, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=1)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=1, + supply_temperature=self.temperature_out_primary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=2, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=2)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=2, + supply_temperature=self.temperature_out_secondary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=3, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=3, + supply_temperature=self.temperature_out_secondary, + ) + ) + + # set mass flow rate or pressure + if self.pre_scribe_mass_flow_secondary: + mset = self.mass_flow_rate_rate_set_point_secondary + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=2, + mass_flow_value=mset, + ) + ) + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=3, + mass_flow_value=mset * -1, + ) + ) + else: + if self.iteration_flow_direction_secondary == FlowDirection.ZERO: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary + else: + if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: + pset_out = self.pressure_set_point_secondary / 2 + pset_in = self.pressure_set_point_secondary + else: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary / 2 + equations.append( + self.prescribe_pressure_at_connection_point( + connection_point=2, + pressure_value=pset_in, + ) + ) + equations.append( + self.prescribe_pressure_at_connection_point( + connection_point=3, + pressure_value=pset_out, + ) + ) + # set mass flow rate or pressure + if self.pre_scribe_mass_flow_primary: + mset = self.mass_flow_rate_rate_set_point_secondary + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=0, + mass_flow_value=mset, + ) + ) + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=1, + mass_flow_value=mset * -1, + ) + ) + else: + if self.iteration_flow_direction_secondary == FlowDirection.ZERO: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary + else: + if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: + pset_out = self.pressure_set_point_secondary / 2 + pset_in = self.pressure_set_point_secondary + else: + pset_out = self.pressure_set_point_secondary + pset_in = self.pressure_set_point_secondary / 2 + equations.append( + self.prescribe_pressure_at_connection_point( + connection_point=0, + pressure_value=pset_in, + ) + ) + equations.append( + self.prescribe_pressure_at_connection_point( + connection_point=1, + pressure_value=pset_out, + ) + ) + return equations + + def get_equations_old(self) -> list[EquationObject]: r"""Return the heat transfer equations. The method returns the heat transfer equations for the heat transfer asset. @@ -266,15 +426,9 @@ def get_equations(self) -> list[EquationObject]: self.flow_direction_secondary = self.flow_direction( self.mass_flow_rate_rate_set_point_secondary ) - ( - self.primary_side_inflow, - self.primary_side_outflow, - self.secondary_side_inflow, - self.secondary_side_outflow, - ) = self.get_ordered_connection_point_list() - if np.all(np.abs(self.prev_sol[0:-1:3]) < MASSFLOW_ZERO_LIMIT): - iteration_flow_direction_primary = self.flow_direction( + if np.all(np.abs(self.prev_sol[0:-1:3]) > MASSFLOW_ZERO_LIMIT): + self.iteration_flow_direction_primary = self.flow_direction( self.prev_sol[ self.get_index_matrix( property_name="mass_flow_rate", @@ -283,7 +437,7 @@ def get_equations(self) -> list[EquationObject]: ) ] ) - iteration_flow_direction_secondary = self.flow_direction( + self.iteration_flow_direction_secondary = self.flow_direction( self.prev_sol[ self.get_index_matrix( property_name="mass_flow_rate", @@ -293,8 +447,15 @@ def get_equations(self) -> list[EquationObject]: ] ) else: - iteration_flow_direction_primary = self.flow_direction_primary - iteration_flow_direction_secondary = self.flow_direction_secondary + self.iteration_flow_direction_primary = self.flow_direction_primary + self.iteration_flow_direction_secondary = self.flow_direction_secondary + + ( + self.primary_side_inflow, + self.primary_side_outflow, + self.secondary_side_inflow, + self.secondary_side_outflow, + ) = self.get_ordered_connection_point_list() # Initialize the equations list equations = [] @@ -309,7 +470,8 @@ def get_equations(self) -> list[EquationObject]: ) # Add the internal energy equations at connection points 1, and 3 to set # the temperature through internal energy at the outlet of the heat transfer asset. - if iteration_flow_direction_primary != FlowDirection.ZERO: + if self.iteration_flow_direction_primary != FlowDirection.ZERO: + equations.append( self.prescribe_temperature_at_connection_point( connection_point=self.primary_side_outflow, @@ -322,7 +484,7 @@ def get_equations(self) -> list[EquationObject]: connection_point=self.primary_side_outflow ) ) - if iteration_flow_direction_secondary != FlowDirection.ZERO: + if self.iteration_flow_direction_secondary != FlowDirection.ZERO: equations.append( self.prescribe_temperature_at_connection_point( connection_point=self.secondary_side_outflow, @@ -338,28 +500,26 @@ def get_equations(self) -> list[EquationObject]: # -- Mass flow rate or pressure on secondary side (2x) -- # Prescribe the pressure at the secondary side of the heat transfer asset. if self.pre_scribe_mass_flow_secondary: - if iteration_flow_direction_secondary == FlowDirection.ZERO: - mset = 0.0 - else: - mset = self.mass_flow_rate_rate_set_point_secondary + + mset = self.mass_flow_rate_rate_set_point_secondary equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=self.secondary_side_inflow, - mass_flow_value=mset * self.flow_direction_secondary.value, + mass_flow_value=mset, ) ) equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=self.secondary_side_outflow, - mass_flow_value=mset * self.flow_direction_secondary.value * -1, + mass_flow_value=mset * -1, ) ) else: - if iteration_flow_direction_secondary == FlowDirection.ZERO: + if self.iteration_flow_direction_secondary == FlowDirection.ZERO: pset_out = self.pressure_set_point_secondary pset_in = self.pressure_set_point_secondary else: - if iteration_flow_direction_secondary == FlowDirection.POSITIVE: + if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: pset_out = self.pressure_set_point_secondary / 2 pset_in = self.pressure_set_point_secondary else: @@ -399,13 +559,13 @@ def get_equations(self) -> list[EquationObject]: # Defines the energy balance between the primary and secondary side of the # heat transfer asset. # If the mass flow at the inflow node of the primary and secondary side is not zero, - if (iteration_flow_direction_primary != FlowDirection.ZERO) or ( - iteration_flow_direction_secondary != FlowDirection.ZERO + if (self.iteration_flow_direction_primary != FlowDirection.ZERO) or ( + self.iteration_flow_direction_secondary != FlowDirection.ZERO ): equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=self.primary_side_inflow, - mass_flow_value=self.get_mass_flow_from_prev_solution(), + mass_flow_value=-1 * self.pre_scribe_mass_flow_primary, ) ) # If the mass flow at the inflow node of the primary and secondary side is zero, @@ -417,11 +577,11 @@ def get_equations(self) -> list[EquationObject]: ) ) else: - if iteration_flow_direction_secondary == FlowDirection.ZERO: + if self.iteration_flow_direction_secondary == FlowDirection.ZERO: pset_out = self.pressure_set_point_secondary pset_in = self.pressure_set_point_secondary else: - if iteration_flow_direction_secondary == FlowDirection.POSITIVE: + if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: pset_out = self.pressure_set_point_secondary / 2 pset_in = self.pressure_set_point_secondary else: From d6167d00695a0564dd97163bffba76c0ff79dc43 Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Feb 2026 17:24:18 +0100 Subject: [PATCH 06/10] Corrected flow direction setting --- .../solver/network/assets/heat_transfer_asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index 438d24a4..06e91966 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -300,13 +300,13 @@ def get_equations(self) -> list[EquationObject]: equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=2, - mass_flow_value=mset, + mass_flow_value=-mset, ) ) equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=3, - mass_flow_value=mset * -1, + mass_flow_value=mset, ) ) else: From 4300c0350137572f1a8453dc97c55073cf6fff1e Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 26 Feb 2026 09:57:55 +0100 Subject: [PATCH 07/10] Fixed unit test and made bug fixes based ont he test --- .../controller/controller_heat_transfer.py | 2 +- .../assets/controller/controller_network.py | 2 +- .../entities/assets/heat_exchanger.py | 2 +- .../entities/network_controller.py | 10 +-- .../network/assets/heat_transfer_asset.py | 2 +- .../controller/test_controller_network.py | 10 ++- .../controller/test_controller_new_class.py | 12 ++-- unit_test/entities/test_ates_cluster.py | 4 +- unit_test/entities/test_heat_exchanger.py | 14 +++- unit_test/entities/test_heat_pump.py | 27 ++++++-- unit_test/entities/test_production_cluster.py | 4 +- .../integration/test_heat_transfer_asset.py | 32 ++++++--- .../assets/test_heat_transfer_asset.py | 69 ++++++++++++------- 13 files changed, 126 insertions(+), 64 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index d1b655a0..6f5a8d71 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -66,7 +66,7 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30, PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 40, - SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * self.factor, + SECONDARY + PROPERTY_HEAT_DEMAND: -heat_demand * self.factor, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, PROPERTY_SET_PRESSURE: False, diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index 56a55fc9..68ddf0d3 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -113,7 +113,7 @@ def get_total_charge_storage(self) -> float: """ return float( sum([storage.effective_max_charge_power for storage in self.storages]) - ) * product(self.factor_to_first_network[:-2]) + ) * product(self.factor_to_first_network) def get_total_supply(self) -> float: """Method to get the total heat supply of the network. diff --git a/src/omotes_simulator_core/entities/assets/heat_exchanger.py b/src/omotes_simulator_core/entities/assets/heat_exchanger.py index 42a89599..a807dd73 100644 --- a/src/omotes_simulator_core/entities/assets/heat_exchanger.py +++ b/src/omotes_simulator_core/entities/assets/heat_exchanger.py @@ -210,7 +210,7 @@ def write_to_output(self) -> None: self.solver_asset.get_heat_power_primary() # type: ignore ), PROPERTY_HEAT_LOSS: ( - self.solver_asset.get_heat_power_primary() # type: ignore + -self.solver_asset.get_heat_power_primary() # type: ignore - self.solver_asset.get_heat_power_secondary() # type: ignore ), } diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 78d67976..71d833ff 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -60,12 +60,12 @@ def update_networks_factor(self) -> None: for asset in current_network.heat_transfer_assets_prim: if self.networks[int(step)].exists(asset.id): network.factor_to_first_network.append(asset.factor) - # current_network = self.networks[int(step)] + current_network = self.networks[int(step)] break for asset in current_network.heat_transfer_assets_sec: if self.networks[int(step)].exists(asset.id): network.factor_to_first_network.append(1 / asset.factor) - # current_network = self.networks[int(step)] + current_network = self.networks[int(step)] break def update_setpoints(self, time: datetime.datetime) -> dict: @@ -107,7 +107,7 @@ def update_setpoints(self, time: datetime.datetime) -> dict: # The storage can charge to max. The sources need to be capped. storage_setpoints = self._set_all_storages_charge_to_max() producer_setpoints = self._set_producers_based_on_priority( - surplus_supply + total_charge_storage + total_demand + total_charge_storage ) else: # total supply is lower than demand, so we need to check if there is enough discharge capacity from storage. @@ -127,7 +127,7 @@ def update_setpoints(self, time: datetime.datetime) -> dict: else: # there is enough supply + storage to cover the demand. sources to max and storages to deliver the rest. consumer_setpoints = self._set_consumer_to_demand(time) - surplus_demand = total_supply - total_demand + surplus_demand = total_demand - total_supply producer_setpoints = self._set_producers_to_max() storage_setpoints = self._set_storages_discharge_power(surplus_demand) @@ -166,7 +166,7 @@ def update_setpoints(self, time: datetime.datetime) -> dict: if total_heat_supply > 0: heat_transfer.update(asset.set_asset(total_heat_supply)) else: - heat_transfer.update(asset.set_asset(total_heat_supply, True)) + heat_transfer.update(asset.set_asset(-total_heat_supply, True)) for asset in network.heat_transfer_assets_sec: if total_heat_supply > 0: heat_transfer.update(asset.set_asset(-total_heat_supply, True)) diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index 06e91966..ca678057 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -334,7 +334,7 @@ def get_equations(self) -> list[EquationObject]: ) # set mass flow rate or pressure if self.pre_scribe_mass_flow_primary: - mset = self.mass_flow_rate_rate_set_point_secondary + mset = self.mass_flow_initialization_primary equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=0, diff --git a/unit_test/entities/controller/test_controller_network.py b/unit_test/entities/controller/test_controller_network.py index f035b6dc..92103ef3 100644 --- a/unit_test/entities/controller/test_controller_network.py +++ b/unit_test/entities/controller/test_controller_network.py @@ -61,7 +61,7 @@ def test_init(self): self.assertEqual(self.controller_network.consumers, self.consumers) self.assertEqual(self.controller_network.producers, self.producers) self.assertEqual(self.controller_network.storages, self.storages) - self.assertEqual(self.controller_network.factor_to_first_network, self.factor) + self.assertEqual(self.controller_network.factor_to_first_network, [self.factor]) self.assertEqual(self.controller_network.path, []) def test_exists(self): @@ -248,9 +248,13 @@ def test_set_all_storages_discharge_to_max(self): { storage1.id: { PROPERTY_HEAT_DEMAND: -20, + PROPERTY_TEMPERATURE_OUT: 50, + PROPERTY_TEMPERATURE_IN: 40, }, storage2.id: { PROPERTY_HEAT_DEMAND: -25, + PROPERTY_TEMPERATURE_OUT: 50, + PROPERTY_TEMPERATURE_IN: 40, }, }, ) @@ -278,9 +282,13 @@ def test_set_all_storages_charge_to_max(self): { storage1.id: { PROPERTY_HEAT_DEMAND: 10, + PROPERTY_TEMPERATURE_OUT: 50, + PROPERTY_TEMPERATURE_IN: 40, }, storage2.id: { PROPERTY_HEAT_DEMAND: 15, + PROPERTY_TEMPERATURE_OUT: 50, + PROPERTY_TEMPERATURE_IN: 40, }, }, ) diff --git a/unit_test/entities/controller/test_controller_new_class.py b/unit_test/entities/controller/test_controller_new_class.py index aaf724d7..d984b39b 100644 --- a/unit_test/entities/controller/test_controller_new_class.py +++ b/unit_test/entities/controller/test_controller_new_class.py @@ -81,9 +81,9 @@ def test_update_networks_factor_prim(self): # act self.controller.update_networks_factor() # assert - self.assertEqual(self.network1.factor_to_first_network, 1) - self.assertEqual(self.network2.factor_to_first_network, 2) - self.assertEqual(self.network3.factor_to_first_network, 6) + self.assertEqual(self.network1.factor_to_first_network, [1]) + self.assertEqual(self.network2.factor_to_first_network, [1, 2]) + self.assertEqual(self.network3.factor_to_first_network, [1, 3, 2]) def test_update_networks_factor_sec(self): # arrange @@ -99,9 +99,9 @@ def test_update_networks_factor_sec(self): # act self.controller.update_networks_factor() # assert - self.assertEqual(self.network1.factor_to_first_network, 1) - self.assertEqual(self.network2.factor_to_first_network, 0.5) - self.assertEqual(self.network3.factor_to_first_network, 0.16666666666666666) + self.assertEqual(self.network1.factor_to_first_network, [1]) + self.assertEqual(self.network2.factor_to_first_network, [1, 0.5]) + self.assertEqual(self.network3.factor_to_first_network, [1, 0.3333333333333333, 0.5]) def setup_update_set_points(self): """Helper method to set up the networks with assets. diff --git a/unit_test/entities/test_ates_cluster.py b/unit_test/entities/test_ates_cluster.py index 43bc0698..b06059f8 100644 --- a/unit_test/entities/test_ates_cluster.py +++ b/unit_test/entities/test_ates_cluster.py @@ -74,7 +74,7 @@ def test_injection_ates(self) -> None: self.ates_cluster.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.ates_cluster.hot_well_temperature, 358.15, delta=0.1) + self.assertAlmostEqual(self.ates_cluster.hot_well_temperature, 358.6696, delta=0.1) self.assertAlmostEqual(self.ates_cluster.cold_well_temperature, 290.15, delta=0.1) def test_production_ates(self) -> None: @@ -93,5 +93,5 @@ def test_production_ates(self) -> None: self.ates_cluster.set_setpoints(setpoints=setpoints) # Assert - self.assertAlmostEqual(self.ates_cluster.hot_well_temperature, 355.54, delta=0.1) + self.assertAlmostEqual(self.ates_cluster.hot_well_temperature, 290.1549, delta=0.1) self.assertAlmostEqual(self.ates_cluster.cold_well_temperature, 308.17, delta=0.1) diff --git a/unit_test/entities/test_heat_exchanger.py b/unit_test/entities/test_heat_exchanger.py index 33fef9c7..0f0cba67 100644 --- a/unit_test/entities/test_heat_exchanger.py +++ b/unit_test/entities/test_heat_exchanger.py @@ -55,8 +55,12 @@ def setUp(self) -> None: self.heat_exchanger.solver_asset.get_index_matrix( property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False ) + ] = -2.0 + self.heat_exchanger.solver_asset.prev_sol[ + self.heat_exchanger.solver_asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=1, use_relative_indexing=False + ) ] = 2.0 - self.heat_exchanger.solver_asset.prev_sol[ self.heat_exchanger.solver_asset.get_index_matrix( property_name="internal_energy", connection_point=1, use_relative_indexing=False @@ -73,7 +77,11 @@ def setUp(self) -> None: property_name="mass_flow_rate", connection_point=2, use_relative_indexing=False ) ] = 1.0 - + self.heat_exchanger.solver_asset.prev_sol[ + self.heat_exchanger.solver_asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=3, use_relative_indexing=False + ) + ] = -1.0 self.heat_exchanger.solver_asset.prev_sol[ self.heat_exchanger.solver_asset.get_index_matrix( property_name="internal_energy", connection_point=3, use_relative_indexing=False @@ -89,6 +97,6 @@ def test_write_to_output(self): self.heat_exchanger.write_to_output() # Assert - self.assertEqual(self.heat_exchanger.outputs[1][-1][PROPERTY_HEAT_POWER_PRIMARY], 10.0) + self.assertEqual(self.heat_exchanger.outputs[1][-1][PROPERTY_HEAT_POWER_PRIMARY], -10.0) self.assertEqual(self.heat_exchanger.outputs[1][-1][PROPERTY_HEAT_LOSS], 5.0) self.assertEqual(self.heat_exchanger.outputs[0][-1][PROPERTY_HEAT_POWER_SECONDARY], 5.0) diff --git a/unit_test/entities/test_heat_pump.py b/unit_test/entities/test_heat_pump.py index c491964f..e7d9f852 100644 --- a/unit_test/entities/test_heat_pump.py +++ b/unit_test/entities/test_heat_pump.py @@ -63,6 +63,11 @@ def setUp(self) -> None: property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False ) ] = 2.0 + self.heat_pump.solver_asset.prev_sol[ + self.heat_pump.solver_asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=1, use_relative_indexing=False + ) + ] = -2.0 self.heat_pump.solver_asset.prev_sol[ self.heat_pump.solver_asset.get_index_matrix( @@ -80,6 +85,11 @@ def setUp(self) -> None: property_name="mass_flow_rate", connection_point=2, use_relative_indexing=False ) ] = 1.0 + self.heat_pump.solver_asset.prev_sol[ + self.heat_pump.solver_asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=3, use_relative_indexing=False + ) + ] = -1.0 self.heat_pump.solver_asset.prev_sol[ self.heat_pump.solver_asset.get_index_matrix( @@ -91,27 +101,29 @@ def test_set_setpoints_secondary(self): setpoints = { SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 15.0, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 25.0, - SECONDARY + PROPERTY_HEAT_DEMAND: 310, + SECONDARY + PROPERTY_HEAT_DEMAND: -310, PROPERTY_SET_PRESSURE: True, # Boolean value } with patch( "omotes_simulator_core.entities.assets.heat_pump." "heat_demand_and_temperature_to_mass_flow", - return_value=321.0, + return_value=-321.0, ) as mock_calc: self.heat_pump._set_setpoints_secondary(setpoints) # Self attributes self.assertEqual(self.heat_pump.temperature_in_secondary, 273.15 + 15.0) self.assertEqual(self.heat_pump.temperature_out_secondary, 273.15 + 25.0) - self.assertEqual(self.heat_pump.mass_flow_secondary, 321.0) + self.assertEqual(self.heat_pump.mass_flow_secondary, -321.0) self.assertEqual(self.heat_pump.control_mass_flow_secondary, False) # Solver asset attributes self.assertEqual(self.heat_pump.solver_asset.temperature_in_secondary, 273.15 + 15.0) self.assertEqual(self.heat_pump.solver_asset.temperature_out_secondary, 273.15 + 25.0) - self.assertEqual(self.heat_pump.solver_asset.mass_flow_rate_secondary, 321.0) + self.assertEqual( + self.heat_pump.solver_asset.mass_flow_rate_rate_set_point_secondary, -321.0 + ) self.assertEqual(self.heat_pump.solver_asset.pre_scribe_mass_flow_secondary, False) mock_calc.assert_called_once_with( @@ -143,6 +155,7 @@ def test_set_setpoints_primary(self): PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 10.0, PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 20.0, PRIMARY + PROPERTY_HEAT_DEMAND: 300, + PROPERTY_SET_PRESSURE: False, } with patch( @@ -155,12 +168,12 @@ def test_set_setpoints_primary(self): # Self attributes self.assertEqual(self.heat_pump.temperature_in_primary, 273.15 + 10.0) self.assertEqual(self.heat_pump.temperature_out_primary, 273.15 + 20.0) - self.assertEqual(self.heat_pump.mass_flow_initialization_primary, 125) + self.assertEqual(self.heat_pump.mass_flow_initialization_primary, -125) # Solver asset attributes self.assertEqual(self.heat_pump.solver_asset.temperature_in_primary, 273.15 + 10.0) self.assertEqual(self.heat_pump.solver_asset.temperature_out_primary, 273.15 + 20.0) - self.assertEqual(self.heat_pump.solver_asset.mass_flow_initialization_primary, 125) + self.assertEqual(self.heat_pump.solver_asset.mass_flow_initialization_primary, -125) mock_calc.assert_called_once_with( thermal_demand=300, temperature_in=273.15 + 10.0, temperature_out=273.15 + 20.0 @@ -209,7 +222,7 @@ def test_set_setpoints_calls_both_primary_and_secondary(self): self.assertEqual(self.heat_pump.solver_asset.temperature_in_secondary, 280.0) self.assertEqual(self.heat_pump.solver_asset.temperature_out_secondary, 270.0) self.assertEqual(self.heat_pump.solver_asset.pre_scribe_mass_flow_secondary, True) - self.assertEqual(self.heat_pump.mass_flow_initialization_primary, 125) + self.assertEqual(self.heat_pump.mass_flow_initialization_primary, -125) self.assertEqual(self.heat_pump.mass_flow_secondary, 125) self.assertEqual(mock_calc.call_count, 2) diff --git a/unit_test/entities/test_production_cluster.py b/unit_test/entities/test_production_cluster.py index 92b40cb7..c7d2e4c7 100644 --- a/unit_test/entities/test_production_cluster.py +++ b/unit_test/entities/test_production_cluster.py @@ -284,10 +284,10 @@ def test_is_converged_pass(self): The convergence criteria is set to 0.1% of the heat demand set point. """ # Arrange - self.production_cluster.heat_demand_set_point = 100.0 + self.production_cluster.heat_demand_set_point = -100.0 def get_actual_heat_supplied(_): - return self.production_cluster.heat_demand_set_point * (1 - 0.001) + return -self.production_cluster.heat_demand_set_point * (1 - 0.001) with patch( "omotes_simulator_core.entities.assets.production_cluster." diff --git a/unit_test/integration/test_heat_transfer_asset.py b/unit_test/integration/test_heat_transfer_asset.py index 7d899cdc..01c16655 100644 --- a/unit_test/integration/test_heat_transfer_asset.py +++ b/unit_test/integration/test_heat_transfer_asset.py @@ -90,8 +90,10 @@ def test_heat_transfer_asset_primary_positive_secondary_positive_flow(self) -> N self.heat_transfer_asset.temperature_out_primary = 20 + 273.15 self.heat_transfer_asset.temperature_out_secondary = 70 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = 1.0 - 1.0 / 3.0 - self.heat_transfer_asset.mass_flow_initialization_primary = -1 + self.heat_transfer_asset.mass_flow_initialization_primary = -77.55 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -1 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -187,8 +189,10 @@ def test_heat_transfer_asset_primary_negative_secondary_positive_flow(self) -> N self.heat_transfer_asset.temperature_out_primary = 20 + 273.15 self.heat_transfer_asset.temperature_out_secondary = 70 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = 1.0 - 1.0 / 3.0 - self.heat_transfer_asset.mass_flow_initialization_primary = +1 + self.heat_transfer_asset.mass_flow_initialization_primary = 77.55 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -1 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -283,8 +287,10 @@ def test_heat_transfer_asset_primary_positive_secondary_negative_flow(self) -> N self.heat_transfer_asset.temperature_out_primary = 20 + 273.15 self.heat_transfer_asset.temperature_out_secondary = 70 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = 1.0 - 1.0 / 3.0 - self.heat_transfer_asset.mass_flow_initialization_primary = -1 + self.heat_transfer_asset.mass_flow_initialization_primary = -77.55 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = +1 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -380,7 +386,10 @@ def test_heat_transfer_asset_positive_heat_transfer_coefficient(self) -> None: self.heat_transfer_asset.temperature_out_primary = 20 + 273.15 self.heat_transfer_asset.temperature_out_secondary = 70 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = 1.0 - 1.0 / 5.0 - self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -38.76 + self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -20.0 + self.heat_transfer_asset.mass_flow_initialization_primary = -38.76 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -406,7 +415,7 @@ def test_heat_transfer_asset_positive_heat_transfer_coefficient(self) -> None: property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False ) ], - -93.07, + -38.76, 2, ) self.assertAlmostEqual( @@ -467,6 +476,9 @@ def test_heat_transfer_asset_heat_transfer_coefficient_of_one(self) -> None: self.heat_transfer_asset.temperature_out_secondary = 70 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = 1.0 # - 1.0 / 5.0 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -38.76 + self.heat_transfer_asset.mass_flow_initialization_primary = -38.76 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -561,8 +573,10 @@ def test_heat_transfer_asset_negative_heat_transfer_coefficient(self) -> None: self.heat_transfer_asset.temperature_out_primary = 30 + 273.15 self.heat_transfer_asset.temperature_out_secondary = 40 + 273.15 self.heat_transfer_asset.heat_transfer_coefficient = -1 * (1.0 - 1.0 / 5.0) - self.heat_transfer_asset.mass_flow_initialization_primary = -1 + self.heat_transfer_asset.mass_flow_initialization_primary = -93.07 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = -1 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 70 + 273.15 @@ -679,6 +693,8 @@ def test_heat_transfer_asset_zero_flow(self) -> None: self.heat_transfer_asset.heat_transfer_coefficient = 1.0 - 1.0 / 5.0 self.heat_transfer_asset.mass_flow_initialization_primary = 0.0 self.heat_transfer_asset.mass_flow_rate_rate_set_point_secondary = 0.0 + self.heat_transfer_asset.pre_scribe_mass_flow_primary = True + self.heat_transfer_asset.pre_scribe_mass_flow_secondary = False # Set the temperature of the demand self.demand_asset.supply_temperature = 40 + 273.15 @@ -723,7 +739,7 @@ def test_heat_transfer_asset_zero_flow(self) -> None: property_name="internal_energy", connection_point=0, use_relative_indexing=False ) ], - fluid_props.get_ie(self.network.get_node(primary_in).initial_temperature), + fluid_props.get_ie(20 + 273.15), 2, ) self.assertAlmostEqual( @@ -741,7 +757,7 @@ def test_heat_transfer_asset_zero_flow(self) -> None: property_name="internal_energy", connection_point=2, use_relative_indexing=False ) ], - fluid_props.get_ie(self.network.get_node(secondary_in).initial_temperature), + fluid_props.get_ie(70 + 273.15), 2, ) self.assertAlmostEqual( diff --git a/unit_test/solver/network/assets/test_heat_transfer_asset.py b/unit_test/solver/network/assets/test_heat_transfer_asset.py index 691cdf80..4a9f7e53 100644 --- a/unit_test/solver/network/assets/test_heat_transfer_asset.py +++ b/unit_test/solver/network/assets/test_heat_transfer_asset.py @@ -74,12 +74,12 @@ def test_get_equations_initial_conditions_prescribe_pressure_secondary( # Assert self.assertEqual(len(equations), 12) - self.assertEqual(mock_add_continuity_equation.call_count, 1) + self.assertEqual(mock_add_continuity_equation.call_count, 0) self.assertEqual(mock_get_press_to_node_equation.call_count, 4) - self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 2) - self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 1) - self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 0) - self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 4) + self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 4) + self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 0) + self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 4) + self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 0) self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 0) self.assertEqual( ( @@ -127,12 +127,12 @@ def test_get_equations_initial_conditions_prescribe_mass_flow_secondary( # Assert self.assertEqual(len(equations), 12) - self.assertEqual(mock_add_continuity_equation.call_count, 1) + self.assertEqual(mock_add_continuity_equation.call_count, 0) self.assertEqual(mock_get_press_to_node_equation.call_count, 4) - self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 0) - self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 3) - self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 0) - self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 4) + self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 2) + self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 2) + self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 4) + self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 0) self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 0) self.assertEqual( ( @@ -210,12 +210,12 @@ def test_get_equations_zero_flow_prescribe_pressure_secondary( # Assert self.assertEqual(len(equations), 12) - self.assertEqual(mock_add_continuity_equation.call_count, 1) + self.assertEqual(mock_add_continuity_equation.call_count, 0) self.assertEqual(mock_get_press_to_node_equation.call_count, 4) - self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 2) - self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 1) - self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 0) - self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 4) + self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 4) + self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 0) + self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 4) + self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 0) self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 0) self.assertEqual( ( @@ -294,13 +294,13 @@ def test_get_equations_flow_prescribe_mass_flow_secondary( # Assert self.assertEqual(len(equations), 12) - self.assertEqual(mock_add_continuity_equation.call_count, 1) + self.assertEqual(mock_add_continuity_equation.call_count, 0) self.assertEqual(mock_get_press_to_node_equation.call_count, 4) - self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 0) - self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 3) + self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 2) + self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 2) self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 2) self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 2) - self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 1) + self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 0) self.assertEqual( ( mock_add_continuity_equation.call_count @@ -377,13 +377,13 @@ def test_get_equations_with_flow_prescribe_pressure_secondary( # Assert self.assertEqual(len(equations), 12) - self.assertEqual(mock_add_continuity_equation.call_count, 1) + self.assertEqual(mock_add_continuity_equation.call_count, 0) self.assertEqual(mock_get_press_to_node_equation.call_count, 4) - self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 2) - self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 1) + self.assertEqual(mock_prescribe_pressure_at_connection_point.call_count, 4) + self.assertEqual(mock_prescribe_mass_flow_at_connection_point.call_count, 0) self.assertEqual(mock_prescribe_temperature_at_connection_point.call_count, 2) self.assertEqual(mock_get_internal_energy_to_node_equation.call_count, 2) - self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 1) + self.assertEqual(mock_get_mass_flow_from_prev_solution.call_count, 0) self.assertEqual( ( mock_add_continuity_equation.call_count @@ -414,7 +414,11 @@ def test_get_heat_power_primary(self): property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False ) ] = 1.0 - + self.asset.prev_sol[ + self.asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=1, use_relative_indexing=False + ) + ] = -1.0 # Act heat_power = self.asset.get_heat_power_primary() @@ -439,6 +443,11 @@ def test_get_heat_power_secondary(self): property_name="mass_flow_rate", connection_point=2, use_relative_indexing=False ) ] = 1.0 + self.asset.prev_sol[ + self.asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=3, use_relative_indexing=False + ) + ] = -1.0 # Act heat_power = self.asset.get_heat_power_secondary() @@ -466,7 +475,11 @@ def test_get_electric_power_consumption(self): property_name="mass_flow_rate", connection_point=0, use_relative_indexing=False ) ] = 2.0 - + self.asset.prev_sol[ + self.asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=1, use_relative_indexing=False + ) + ] = 2.0 # --- Secondary side self.asset.prev_sol[ self.asset.get_index_matrix( @@ -483,7 +496,11 @@ def test_get_electric_power_consumption(self): property_name="mass_flow_rate", connection_point=2, use_relative_indexing=False ) ] = 1.0 - + self.asset.prev_sol[ + self.asset.get_index_matrix( + property_name="mass_flow_rate", connection_point=3, use_relative_indexing=False + ) + ] = 1.0 # Act electric_power = self.asset.get_electric_power_consumption() From 86feca1d0361756129e7dec6b171c05aadab81f1 Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 26 Feb 2026 16:27:24 +0100 Subject: [PATCH 08/10] Fxiing typing and linting issues --- .../entities/assets/ates_cluster.py | 2 +- .../controller/controller_heat_transfer.py | 5 +-- .../assets/controller/controller_network.py | 16 ++++--- .../entities/assets/heat_pump.py | 4 +- .../entities/network_controller.py | 9 ++-- .../network/assets/heat_transfer_asset.py | 42 +++++++++++++++++++ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/ates_cluster.py b/src/omotes_simulator_core/entities/assets/ates_cluster.py index e024335d..909cf647 100644 --- a/src/omotes_simulator_core/entities/assets/ates_cluster.py +++ b/src/omotes_simulator_core/entities/assets/ates_cluster.py @@ -279,7 +279,7 @@ def _init_rosim(self) -> None: } # initially charging 12 weeks with 85-35 temperature 1 MW logger.info("initializing ates with charging for 12 weeks") - for i in range(12): + for i in range(0): logger.info(f"charging ates week {i + 1}") self.set_time_step(3600 * 24 * 7) self.set_time(datetime(2023, 1, i + 1, 0, 0, 0)) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index 6f5a8d71..0e5c7db7 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -14,8 +14,6 @@ # along with this program. If not, see . """Module containing the class for a heat trasnfer asset.""" -import numpy as np - from omotes_simulator_core.entities.assets.asset_defaults import ( PRIMARY, PROPERTY_HEAT_DEMAND, @@ -46,7 +44,8 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ The supply and return temperatures are also set. :param float heat_demand: Heat demand to set. - :param bypass: When true the heat exchange is bypassed, so the heat demand is not reduced by the factor. Default is False. + :param bypass: When true the heat exchange is bypassed, so the heat demand is not + reduced by the factor. Default is False. """ if bypass: return { diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index 68ddf0d3..a9588a11 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -93,8 +93,9 @@ def exists(self, identifier: str) -> bool: def get_total_heat_demand(self, time: datetime.datetime) -> float: """Method which the total heat demand at the given time corrected to the first network.""" - return sum([consumer.get_heat_demand(time) for consumer in self.consumers]) * product( - self.factor_to_first_network + return float( + sum([consumer.get_heat_demand(time) for consumer in self.consumers]) + * product(self.factor_to_first_network) ) def get_total_discharge_storage(self) -> float: @@ -104,7 +105,8 @@ def get_total_discharge_storage(self) -> float: """ return float( sum([storage.effective_max_discharge_power for storage in self.storages]) - ) * product(self.factor_to_first_network) + * product(self.factor_to_first_network) + ) def get_total_charge_storage(self) -> float: """Method to get the total storage charge of the network corrected to the first network. @@ -113,15 +115,17 @@ def get_total_charge_storage(self) -> float: """ return float( sum([storage.effective_max_charge_power for storage in self.storages]) - ) * product(self.factor_to_first_network) + * product(self.factor_to_first_network) + ) def get_total_supply(self) -> float: """Method to get the total heat supply of the network. :return float: Total heat supply of all producers. """ - return float(sum([producer.power for producer in self.producers])) * product( - self.factor_to_first_network + return float( + sum([producer.power for producer in self.producers]) + * product(self.factor_to_first_network) ) def set_supply_to_max(self, priority: int = 0) -> dict: diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index 6a8d1024..bcb9fbfb 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -145,7 +145,9 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: self.solver_asset.temperature_out_secondary = ( # type: ignore self.temperature_out_secondary ) - self.solver_asset.mass_flow_rate_rate_set_point_secondary = self.mass_flow_secondary # type: ignore + self.solver_asset.mass_flow_rate_rate_set_point_secondary = ( + self.mass_flow_secondary + ) # type: ignore self.solver_asset.pre_scribe_mass_flow_secondary = ( # type: ignore self.control_mass_flow_secondary ) diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 71d833ff..850b3535 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -100,7 +100,8 @@ def update_setpoints(self, time: datetime.datetime) -> dict: [network.get_total_charge_storage() for network in self.networks] ) if total_charge_storage > surplus_supply: - # there is more charge capacity than surplus supply, so we can set source to max and storages to charge with the surplus supply. + # there is more charge capacity than surplus supply, so we can set source to + # max and storages to charge with the surplus supply. producer_setpoints = self._set_producers_to_max() storage_setpoints = self._set_storages_charge_power(surplus_supply) else: @@ -110,7 +111,8 @@ def update_setpoints(self, time: datetime.datetime) -> dict: total_demand + total_charge_storage ) else: - # total supply is lower than demand, so we need to check if there is enough discharge capacity from storage. + # total supply is lower than demand, so we need to check if there is enough discharge + # capacity from storage. total_discharge_storage = sum( [network.get_total_discharge_storage() for network in self.networks] ) @@ -125,7 +127,8 @@ def update_setpoints(self, time: datetime.datetime) -> dict: storage_setpoints = self._set_all_storages_discharge_to_max() consumer_setpoints = self._set_consumer_to_demand(time, factor=factor) else: - # there is enough supply + storage to cover the demand. sources to max and storages to deliver the rest. + # there is enough supply + storage to cover the demand. sources to max and + # storages to deliver the rest. consumer_setpoints = self._set_consumer_to_demand(time) surplus_demand = total_demand - total_supply producer_setpoints = self._set_producers_to_max() diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index ca678057..1ff599dc 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -215,6 +215,48 @@ def get_ordered_connection_point_list(self) -> list[int]: return [0, 1, 2, 3] def get_equations(self) -> list[EquationObject]: + r"""Return the heat transfer equations. + + The method returns the heat transfer equations for the heat transfer asset. + + The internal energy at the connection points with mass inflow are linked to the nodes. + + .. math:: + + u_{connection_point} = u_{node} + + The temperature is prescribed through the internal energy at the outlet on the + primary and secondary side of the heat transfer asset. + + .. math:: + + u_{connection_point} = u_{supply_temperature} + + The mass flow rate or pressure is prescribed at the secondary side of the heat transfer + asset. + + On the primary side, continuity of mass flow rate is enforced. + + .. math:: + + \dot{m}_{0} + \dot{m}_{1} = 0 + + If the mass flow at the inflow node of the primary and secondary side is not zero, we + prescribe the follwoing energy balance equation for the heat transfer asset: + + .. math:: + + \dot{m}_0 \left{ u_0 - u_1 \right} + C \left{ u_2 \dot{m}_2 + u_3 \dot{m}_3 \right} = 0 + + If the mass flow at the inflow node of the primary and secondary side is zero, we prescribe + the mass flow rate at the primary side of the heat transfer asset. + + .. math:: + + \dot{m}_{asset} = 10.0 + + :return: List[EquationObject] + """ equations = [] # pressure to node equations for connection_point in range(4): From 1341a825f1e07ea3765f90114090890d9d38915b Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 5 Mar 2026 16:57:47 +0100 Subject: [PATCH 09/10] Added bypass option to controller output. Added bypass mode equations in heattransfer solver asset. --- .../entities/assets/asset_abstract.py | 2 - .../entities/assets/asset_defaults.py | 1 + .../entities/assets/ates_cluster.py | 27 ++- .../controller/controller_heat_transfer.py | 7 +- .../entities/assets/demand_cluster.py | 4 +- .../entities/assets/heat_pump.py | 21 +- .../network/assets/heat_transfer_asset.py | 194 +++++++++++------- 7 files changed, 170 insertions(+), 86 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/asset_abstract.py b/src/omotes_simulator_core/entities/assets/asset_abstract.py index 3378041f..023af64a 100644 --- a/src/omotes_simulator_core/entities/assets/asset_abstract.py +++ b/src/omotes_simulator_core/entities/assets/asset_abstract.py @@ -45,8 +45,6 @@ class AssetAbstract(ABC): connected_ports: list[str] """List of ids of the connected ports.""" - solver_asset: BaseAsset - """The asset object use for the solver.""" asset_type = "asset_abstract" """The type of the asset.""" number_of_con_points: int = 2 diff --git a/src/omotes_simulator_core/entities/assets/asset_defaults.py b/src/omotes_simulator_core/entities/assets/asset_defaults.py index d5b2eb91..503fe5c4 100644 --- a/src/omotes_simulator_core/entities/assets/asset_defaults.py +++ b/src/omotes_simulator_core/entities/assets/asset_defaults.py @@ -130,6 +130,7 @@ class HeatBufferDefaults: PROPERTY_VELOCITY_SUPPLY = "velocity_supply" PROPERTY_VELOCITY_RETURN = "velocity_return" PROPERTY_SET_PRESSURE = "set_pressure" +PROPERTY_BYPASS = "bypass" PROPERTY_LENGTH = "length" PROPERTY_DIAMETER = "diameter" PROPERTY_ROUGHNESS = "roughness" diff --git a/src/omotes_simulator_core/entities/assets/ates_cluster.py b/src/omotes_simulator_core/entities/assets/ates_cluster.py index 909cf647..390440df 100644 --- a/src/omotes_simulator_core/entities/assets/ates_cluster.py +++ b/src/omotes_simulator_core/entities/assets/ates_cluster.py @@ -158,9 +158,7 @@ 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 """ - if self.current_time == self.time: - return - self.current_time = self.time + # Default keys required necessary_setpoints = { PROPERTY_TEMPERATURE_IN, @@ -186,8 +184,11 @@ def set_setpoints(self, setpoints: dict) -> None: self.temperature_out = self.cold_well_temperature self._calculate_massflowrate() - self._run_rosim() + if self.current_time != self.time: + self._run_rosim() + self.current_time = self.time self._set_solver_asset_setpoint() + else: # Print missing setpoints logger.error( @@ -315,3 +316,21 @@ def _run_rosim(self) -> None: self.hot_well_temperature = celcius_to_kelvin(ates_temperature[0]) # convert to K self.cold_well_temperature = celcius_to_kelvin(ates_temperature[1]) # convert to K + + def get_heat_supplied(self) -> float: + """Get the actual heat supplied by the asset. + + :return float: The actual heat supplied by the asset [W]. + """ + return ( + self.solver_asset.get_internal_energy(1) - self.solver_asset.get_internal_energy(0) + ) * self.solver_asset.get_mass_flow_rate(0) + + def is_converged(self) -> bool: + """Check if the asset has converged with accepted error of 0.1%. + + :return: True if the asset has converged, False otherwise + """ + return abs(self.get_heat_supplied() + self.thermal_power_allocation) < ( + self.thermal_power_allocation * 0.001 + ) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index 0e5c7db7..46d9747d 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -20,6 +20,7 @@ PROPERTY_SET_PRESSURE, PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, + PROPERTY_BYPASS, SECONDARY, ) from omotes_simulator_core.entities.assets.controller.asset_controller_abstract import ( @@ -51,12 +52,13 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ return { self.id: { PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, - PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 50, - PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 80, + PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, + PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * -1, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, PROPERTY_SET_PRESSURE: False, + PROPERTY_BYPASS: True, } } else: @@ -69,5 +71,6 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, PROPERTY_SET_PRESSURE: False, + PROPERTY_BYPASS: False, } } diff --git a/src/omotes_simulator_core/entities/assets/demand_cluster.py b/src/omotes_simulator_core/entities/assets/demand_cluster.py index 94a9ae0b..84a8078d 100644 --- a/src/omotes_simulator_core/entities/assets/demand_cluster.py +++ b/src/omotes_simulator_core/entities/assets/demand_cluster.py @@ -134,6 +134,6 @@ def is_converged(self) -> bool: :return: True if the asset has converged, False otherwise """ - return abs(self.get_heat_supplied() - (-self.thermal_power_allocation)) < ( - (-self.thermal_power_allocation) * 0.001 + return abs(self.get_heat_supplied() - self.thermal_power_allocation) < ( + self.thermal_power_allocation * 0.001 ) diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index bcb9fbfb..373d6bf7 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -29,6 +29,7 @@ PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, SECONDARY, + PROPERTY_BYPASS, ) from omotes_simulator_core.entities.assets.utils import heat_demand_and_temperature_to_mass_flow from omotes_simulator_core.solver.network.assets.heat_transfer_asset import HeatTransferAsset @@ -101,6 +102,7 @@ def __init__( pressure_set_point_secondary=DEFAULT_PRESSURE, heat_transfer_coefficient=self.coefficient_of_performance, ) + self.first_time_step = True def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: """The secondary side of the heat pump acts as a producer of heat. @@ -117,6 +119,7 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: SECONDARY + PROPERTY_TEMPERATURE_OUT, SECONDARY + PROPERTY_HEAT_DEMAND, PROPERTY_SET_PRESSURE, + PROPERTY_BYPASS, } # Dict to set setpoints_set = set(setpoints_secondary.keys()) @@ -128,10 +131,15 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: ) # Assign setpoints to the HeatPump asset - self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] + if self.first_time_step or self.solver_asset.prev_sol[0] == 0.0: + self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] + else: + self.temperature_in_secondary = self.solver_asset.get_temperature(0) + + # self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_OUT] self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( - thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] * -1, + thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND], temperature_in=self.temperature_in_secondary, temperature_out=self.temperature_out_secondary, ) @@ -151,6 +159,7 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: self.solver_asset.pre_scribe_mass_flow_secondary = ( # type: ignore self.control_mass_flow_secondary ) + self.solver_asset.bypass_mode = setpoints_secondary[PROPERTY_BYPASS] # type: ignore def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: """The primary side of the heat pump acts as a consumer of heat. @@ -180,7 +189,12 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: ) # Assign setpoints to the HeatPump asset - self.temperature_in_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_IN] + if self.first_time_step or self.solver_asset.prev_sol[0] == 0.0: + self.temperature_in_primary = setpoints_primary[SECONDARY + PROPERTY_TEMPERATURE_IN] + else: + self.temperature_in_primary = self.solver_asset.get_temperature(0) + + # self.temperature_in_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_OUT] self.mass_flow_initialization_primary = -heat_demand_and_temperature_to_mass_flow( thermal_demand=setpoints_primary[PRIMARY + PROPERTY_HEAT_DEMAND], @@ -212,6 +226,7 @@ def set_setpoints(self, setpoints: Dict) -> None: self._set_setpoints_primary(setpoints_primary=setpoints) # Set the setpoints for the secondary side of the heat pump self._set_setpoints_secondary(setpoints_secondary=setpoints) + self.first_time_step = False def write_to_output(self) -> None: """Get output power and electricity consumption of the asset. diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index 1ff599dc..d01eefcf 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -116,6 +116,7 @@ def __init__( self.secondary_side_inflow, self.secondary_side_outflow, ) = self.get_ordered_connection_point_list() + self.bypass_mode = False def flow_direction(self, mass_flow: float) -> FlowDirection: """Returns the flow direction of the heat transfer asset. @@ -263,78 +264,10 @@ def get_equations(self) -> list[EquationObject]: equations.append(self.get_press_to_node_equation(connection_point=connection_point)) # Internal energy to node equations - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=0, - use_relative_indexing=True, - ) - ] - < 0.0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) + if not self.bypass_mode: + self.set_internal_energy_equations(equations) else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=0, - supply_temperature=self.temperature_out_primary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=1, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=1)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=1, - supply_temperature=self.temperature_out_primary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=2, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=2)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=2, - supply_temperature=self.temperature_out_secondary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=3, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=3, - supply_temperature=self.temperature_out_secondary, - ) - ) + self.set_internal_energy_equations_bypass(equations) # set mass flow rate or pressure if self.pre_scribe_mass_flow_secondary: @@ -342,13 +275,13 @@ def get_equations(self) -> list[EquationObject]: equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=2, - mass_flow_value=-mset, + mass_flow_value=mset, ) ) equations.append( self.prescribe_mass_flow_at_connection_point( connection_point=3, - mass_flow_value=mset, + mass_flow_value=-mset, ) ) else: @@ -414,6 +347,121 @@ def get_equations(self) -> list[EquationObject]: ) return equations + def set_internal_energy_equations(self, equations): + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=0, + use_relative_indexing=True, + ) + ] + < 0.0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=0, + supply_temperature=self.temperature_out_primary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=1, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=1)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=1, + supply_temperature=self.temperature_out_primary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=2, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=2)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=2, + supply_temperature=self.temperature_out_secondary, + ) + ) + if ( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=3, + use_relative_indexing=True, + ) + ] + < 0 + ): + equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) + else: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=3, + supply_temperature=self.temperature_out_secondary, + ) + ) + + def set_internal_energy_equations_bypass(self, equations): + equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) + equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) + equation_object = EquationObject() + # Short-circuiting the primary and secondary side of the heat transfer asset. + equation_object.indices = np.array( + [ + self.get_index_matrix( + property_name="internal_energy", + connection_point=1, + use_relative_indexing=False, + ), + self.get_index_matrix( + property_name="internal_energy", + connection_point=3, + use_relative_indexing=False, + ), + ] + ) + equation_object.coefficients = np.array([1.0, -1.0]) + equation_object.rhs = 0.0 + equations.append(equation_object) + equation_object2 = EquationObject() + equation_object2.indices = np.array( + [ + self.get_index_matrix( + property_name="internal_energy", + connection_point=0, + use_relative_indexing=False, + ), + self.get_index_matrix( + property_name="internal_energy", + connection_point=2, + use_relative_indexing=False, + ), + ] + ) + equation_object2.coefficients = np.array([1.0, -1.0]) + equation_object2.rhs = 0.0 + equations.append(equation_object2) + def get_equations_old(self) -> list[EquationObject]: r"""Return the heat transfer equations. From 4c11a17753201150de20dfd9a6fa1c506d9e0bee Mon Sep 17 00:00:00 2001 From: Sam van der Zwan Date: Thu, 12 Mar 2026 08:35:14 +0100 Subject: [PATCH 10/10] Fixed issues, now have mode for normal and bypass operation --- .../entities/assets/ates_cluster.py | 24 +- .../controller/controller_heat_transfer.py | 14 +- .../assets/controller/controller_network.py | 12 +- .../entities/assets/heat_pump.py | 13 +- .../entities/assets/utils.py | 5 +- .../entities/network_controller.py | 7 +- .../infrastructure/app.py | 9 +- .../network/assets/heat_transfer_asset.py | 524 ++++++++---------- 8 files changed, 270 insertions(+), 338 deletions(-) diff --git a/src/omotes_simulator_core/entities/assets/ates_cluster.py b/src/omotes_simulator_core/entities/assets/ates_cluster.py index 390440df..80d31553 100644 --- a/src/omotes_simulator_core/entities/assets/ates_cluster.py +++ b/src/omotes_simulator_core/entities/assets/ates_cluster.py @@ -147,9 +147,9 @@ def _calculate_massflowrate(self) -> None: def _set_solver_asset_setpoint(self) -> None: """Set the setpoint of solver asset.""" if self.mass_flowrate >= 0: - self.solver_asset.supply_temperature = self.cold_well_temperature # injection - else: self.solver_asset.supply_temperature = self.hot_well_temperature # production + else: + self.solver_asset.supply_temperature = self.cold_well_temperature # injection self.solver_asset.mass_flow_rate_set_point = self.mass_flowrate # type: ignore def set_setpoints(self, setpoints: dict) -> None: @@ -169,19 +169,23 @@ def set_setpoints(self, setpoints: dict) -> None: setpoints_set = set(setpoints.keys()) # Check if all setpoints are in the setpoints if necessary_setpoints.issubset(setpoints_set): - self.thermal_power_allocation = -1 * setpoints[PROPERTY_HEAT_DEMAND] + 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] + if self.thermal_power_allocation >= 0: + self.temperature_in = setpoints[PROPERTY_TEMPERATURE_OUT] + self.temperature_out = setpoints[PROPERTY_TEMPERATURE_IN] + else: + 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_out = self.solver_asset.get_temperature(0) self.temperature_in = self.hot_well_temperature - self.temperature_out = self.solver_asset.get_temperature(1) else: - self.temperature_in = self.solver_asset.get_temperature(0) - self.temperature_out = self.cold_well_temperature + self.temperature_in = self.cold_well_temperature + self.temperature_out = self.solver_asset.get_temperature(1) self._calculate_massflowrate() if self.current_time != self.time: @@ -331,6 +335,6 @@ def is_converged(self) -> bool: :return: True if the asset has converged, False otherwise """ - return abs(self.get_heat_supplied() + self.thermal_power_allocation) < ( - self.thermal_power_allocation * 0.001 + return abs(self.get_heat_supplied() - self.thermal_power_allocation) < ( + abs(self.thermal_power_allocation) * 0.001 ) diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py index 46d9747d..acc83587 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py @@ -57,20 +57,22 @@ def set_asset(self, heat_demand: float, bypass: bool = False) -> dict[str, dict[ SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * -1, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, - PROPERTY_SET_PRESSURE: False, + SECONDARY + PROPERTY_SET_PRESSURE: False, + PRIMARY + PROPERTY_SET_PRESSURE: False, PROPERTY_BYPASS: True, } } else: return { self.id: { - PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand, + PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand / self.factor, PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30, - PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 40, - SECONDARY + PROPERTY_HEAT_DEMAND: -heat_demand * self.factor, + PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, + SECONDARY + PROPERTY_HEAT_DEMAND: -heat_demand, SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, - SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, - PROPERTY_SET_PRESSURE: False, + SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 40, + SECONDARY + PROPERTY_SET_PRESSURE: False, + PRIMARY + PROPERTY_SET_PRESSURE: False, PROPERTY_BYPASS: False, } } diff --git a/src/omotes_simulator_core/entities/assets/controller/controller_network.py b/src/omotes_simulator_core/entities/assets/controller/controller_network.py index a9588a11..ce1fb017 100644 --- a/src/omotes_simulator_core/entities/assets/controller/controller_network.py +++ b/src/omotes_simulator_core/entities/assets/controller/controller_network.py @@ -23,6 +23,8 @@ PROPERTY_SET_PRESSURE, PROPERTY_TEMPERATURE_IN, PROPERTY_TEMPERATURE_OUT, + SECONDARY, + PRIMARY, ) from omotes_simulator_core.entities.assets.controller.controller_consumer import ControllerConsumer from omotes_simulator_core.entities.assets.controller.controller_heat_transfer import ( @@ -232,17 +234,17 @@ def get_total_supply_priority(self, priority: int) -> float: sum([producer.power for producer in self.producers if producer.priority == priority]) ) - def set_pressure(self) -> str: - """Returns the id of the asset for which the pressure can be set for this network. + def set_pressure(self) -> tuple[str, str]: + """Returns the id of the asset for which the pressure can be set for this network and the key in the set points dict. The controller needs to set per hydraulic separated part of the system the pressure. The network can thus pass back the id for which asset the pressure needs to be set. The controller can then do this. """ if self.producers: - return self.producers[0].id + return (self.producers[0].id, PROPERTY_SET_PRESSURE) if self.heat_transfer_assets_sec: - return self.heat_transfer_assets_sec[0].id + return (self.heat_transfer_assets_sec[0].id, SECONDARY + PROPERTY_SET_PRESSURE) if self.heat_transfer_assets_prim: - return self.heat_transfer_assets_prim[0].id + return (self.heat_transfer_assets_prim[0].id, PRIMARY + PROPERTY_SET_PRESSURE) raise ValueError("No asset found for which the pressure can be set.") diff --git a/src/omotes_simulator_core/entities/assets/heat_pump.py b/src/omotes_simulator_core/entities/assets/heat_pump.py index 373d6bf7..6b266152 100644 --- a/src/omotes_simulator_core/entities/assets/heat_pump.py +++ b/src/omotes_simulator_core/entities/assets/heat_pump.py @@ -118,7 +118,7 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: SECONDARY + PROPERTY_TEMPERATURE_IN, SECONDARY + PROPERTY_TEMPERATURE_OUT, SECONDARY + PROPERTY_HEAT_DEMAND, - PROPERTY_SET_PRESSURE, + SECONDARY + PROPERTY_SET_PRESSURE, PROPERTY_BYPASS, } # Dict to set @@ -138,14 +138,13 @@ def _set_setpoints_secondary(self, setpoints_secondary: Dict) -> None: # self.temperature_in_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_IN] self.temperature_out_secondary = setpoints_secondary[SECONDARY + PROPERTY_TEMPERATURE_OUT] - self.mass_flow_secondary = heat_demand_and_temperature_to_mass_flow( + self.mass_flow_secondary = -heat_demand_and_temperature_to_mass_flow( thermal_demand=setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND], temperature_in=self.temperature_in_secondary, temperature_out=self.temperature_out_secondary, ) self.control_mass_flow_secondary = not ( - setpoints_secondary[PROPERTY_SET_PRESSURE] - & (setpoints_secondary[SECONDARY + PROPERTY_HEAT_DEMAND] < 0) + setpoints_secondary[SECONDARY + PROPERTY_SET_PRESSURE] ) # Assign setpoints to the HeatTransferAsset solver asset @@ -178,6 +177,7 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: PRIMARY + PROPERTY_TEMPERATURE_IN, PRIMARY + PROPERTY_TEMPERATURE_OUT, PRIMARY + PROPERTY_HEAT_DEMAND, + PRIMARY + PROPERTY_SET_PRESSURE, } # Dict to set setpoints_set = set(setpoints_primary.keys()) @@ -201,10 +201,7 @@ def _set_setpoints_primary(self, setpoints_primary: Dict) -> None: temperature_in=self.temperature_in_primary, temperature_out=self.temperature_out_primary, ) - self.control_mass_flow_primary = not ( - setpoints_primary[PROPERTY_SET_PRESSURE] - & (setpoints_primary[PRIMARY + PROPERTY_HEAT_DEMAND] < 0) - ) + self.control_mass_flow_primary = not (setpoints_primary[PRIMARY + PROPERTY_SET_PRESSURE]) # Assign setpoints to the HeatTransferAsset solver asset self.solver_asset.temperature_in_primary = self.temperature_in_primary # type: ignore diff --git a/src/omotes_simulator_core/entities/assets/utils.py b/src/omotes_simulator_core/entities/assets/utils.py index be434324..128c72e8 100644 --- a/src/omotes_simulator_core/entities/assets/utils.py +++ b/src/omotes_simulator_core/entities/assets/utils.py @@ -35,8 +35,9 @@ def heat_demand_and_temperature_to_mass_flow( :param float temperature_in: The temperature that the asset receives from the "from_junction". The temperature should be supplied in Kelvin. """ - heat_capacity = fluid_props.get_heat_capacity((temperature_in + temperature_out) / 2) - return thermal_demand / ((temperature_out - temperature_in) * float(heat_capacity)) + internal_energy1 = fluid_props.get_ie(temperature_in) + internal_energy2 = fluid_props.get_ie(temperature_out) + return thermal_demand / (internal_energy2 - internal_energy1) def mass_flow_and_temperature_to_heat_demand( diff --git a/src/omotes_simulator_core/entities/network_controller.py b/src/omotes_simulator_core/entities/network_controller.py index 850b3535..e0e90c1e 100644 --- a/src/omotes_simulator_core/entities/network_controller.py +++ b/src/omotes_simulator_core/entities/network_controller.py @@ -60,13 +60,12 @@ def update_networks_factor(self) -> None: for asset in current_network.heat_transfer_assets_prim: if self.networks[int(step)].exists(asset.id): network.factor_to_first_network.append(asset.factor) - current_network = self.networks[int(step)] break for asset in current_network.heat_transfer_assets_sec: if self.networks[int(step)].exists(asset.id): network.factor_to_first_network.append(1 / asset.factor) - current_network = self.networks[int(step)] break + current_network = self.networks[int(step)] def update_setpoints(self, time: datetime.datetime) -> dict: """Method to get the controller inputs for the network. @@ -181,8 +180,8 @@ def update_setpoints(self, time: datetime.datetime) -> dict: # Set the pressure. for network in self.networks: - pressure_set_asset = network.set_pressure() - asset_setpoints[pressure_set_asset][PROPERTY_SET_PRESSURE] = True + pressure_set_asset, key = network.set_pressure() + asset_setpoints[pressure_set_asset][key] = True return asset_setpoints diff --git a/src/omotes_simulator_core/infrastructure/app.py b/src/omotes_simulator_core/infrastructure/app.py index a7f9d91e..936e98e1 100644 --- a/src/omotes_simulator_core/infrastructure/app.py +++ b/src/omotes_simulator_core/infrastructure/app.py @@ -41,9 +41,9 @@ def run(file_path: str | None = None) -> pd.DataFrame: config = SimulationConfiguration( simulation_id=uuid.uuid1(), name="test run", - timestep=3600, - start=datetime.strptime("2019-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"), - stop=datetime.strptime("2019-01-01T01:00:00", "%Y-%m-%dT%H:%M:%S"), + timestep=24 * 3600, + start=datetime.strptime("2019-01-18T00:00:00", "%Y-%m-%dT%H:%M:%S"), + stop=datetime.strptime("2019-02-01T00:00:00", "%Y-%m-%dT%H:%M:%S"), ) esdl_file_path = sys.argv[1] if file_path is None else file_path @@ -63,9 +63,10 @@ def run(file_path: str | None = None) -> pd.DataFrame: level=logging.INFO, format="%(asctime)s [%(levelname)s]:%(name)s - %(message)s" ) t1 = datetime.now() - result = run(r".\testdata\test1.esdl") + result = run(r".\testdata\heat_pump_bypass.esdl") t2 = datetime.now() logger.info(f"Results dataframe shape=({result.shape})") + result.to_csv(r".\testdata\heat_pump_bypass_results.csv", index=False) logger.info(f"Execution time: {t2 - t1}") logger.debug(result.head()) diff --git a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py index d01eefcf..df27920a 100644 --- a/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py +++ b/src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py @@ -44,7 +44,6 @@ def __init__( mass_flow_initialization_primary: float = -20.0, heat_transfer_coefficient: float = 1.0, pre_scribe_mass_flow_secondary: bool = False, - pre_scribe_mass_flow_primary: bool = False, temperature_out_secondary: float = 293.15, mass_flow_rate_set_point_secondary: float = -80.0, pressure_set_point_secondary: float = 10000.0, @@ -96,8 +95,8 @@ def __init__( # at the hot side of the heat pump self.pre_scribe_mass_flow_secondary = pre_scribe_mass_flow_secondary # Define the flag that indicates whether the mass flow rate or the pressure is prescribed - # at the cold side of the heat pump - self.pre_scribe_mass_flow_primary = pre_scribe_mass_flow_primary + # at the hot side of the heat pump + self.pre_scribe_mass_flow_primary = pre_scribe_mass_flow_secondary # Define the mass flow rate set point for the asset on the secondary side self.mass_flow_rate_rate_set_point_secondary = mass_flow_rate_set_point_secondary # Define the pressure set point for the asset @@ -109,6 +108,7 @@ def __init__( self.mass_flow_rate_rate_set_point_secondary ) self.iteration_flow_direction_secondary = self.flow_direction_secondary + # Define connection points ( self.primary_side_inflow, @@ -131,9 +131,9 @@ def flow_direction(self, mass_flow: float) -> FlowDirection: The flow direction of the heat transfer asset. """ if mass_flow > MASSFLOW_ZERO_LIMIT: - return FlowDirection.POSITIVE - elif mass_flow < -MASSFLOW_ZERO_LIMIT: return FlowDirection.NEGATIVE + elif mass_flow < -MASSFLOW_ZERO_LIMIT: + return FlowDirection.POSITIVE else: return FlowDirection.ZERO @@ -168,54 +168,60 @@ def get_ordered_connection_point_list(self) -> list[int]: """ # Determine the connection points based on the flow direction if ( - self.iteration_flow_direction_primary == FlowDirection.NEGATIVE - and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE + self.flow_direction_primary == FlowDirection.NEGATIVE + and self.flow_direction_secondary == FlowDirection.POSITIVE ): return [1, 0, 2, 3] elif ( - self.iteration_flow_direction_primary == FlowDirection.POSITIVE - and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE + self.flow_direction_primary == FlowDirection.POSITIVE + and self.flow_direction_secondary == FlowDirection.POSITIVE ): return [0, 1, 2, 3] elif ( - self.iteration_flow_direction_primary == FlowDirection.POSITIVE - and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE + self.flow_direction_primary == FlowDirection.POSITIVE + and self.flow_direction_secondary == FlowDirection.NEGATIVE ): return [0, 1, 3, 2] elif ( - self.iteration_flow_direction_primary == FlowDirection.NEGATIVE - and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE + self.flow_direction_primary == FlowDirection.NEGATIVE + and self.flow_direction_secondary == FlowDirection.NEGATIVE ): return [1, 0, 3, 2] elif ( - self.iteration_flow_direction_primary == FlowDirection.ZERO - and self.iteration_flow_direction_secondary == FlowDirection.ZERO + self.flow_direction_primary == FlowDirection.ZERO + and self.flow_direction_secondary == FlowDirection.ZERO ): return [0, 1, 2, 3] elif ( - self.iteration_flow_direction_primary == FlowDirection.ZERO - and self.iteration_flow_direction_secondary == FlowDirection.POSITIVE + self.flow_direction_primary == FlowDirection.ZERO + and self.flow_direction_secondary == FlowDirection.POSITIVE ): return [0, 1, 2, 3] elif ( - self.iteration_flow_direction_primary == FlowDirection.ZERO - and self.iteration_flow_direction_secondary == FlowDirection.NEGATIVE + self.flow_direction_primary == FlowDirection.ZERO + and self.flow_direction_secondary == FlowDirection.NEGATIVE ): return [0, 1, 3, 2] elif ( - self.iteration_flow_direction_primary == FlowDirection.POSITIVE - and self.iteration_flow_direction_secondary == FlowDirection.ZERO + self.flow_direction_primary == FlowDirection.POSITIVE + and self.flow_direction_secondary == FlowDirection.ZERO ): return [0, 1, 2, 3] elif ( - self.iteration_flow_direction_primary == FlowDirection.NEGATIVE - and self.iteration_flow_direction_secondary == FlowDirection.ZERO + self.flow_direction_primary == FlowDirection.NEGATIVE + and self.flow_direction_secondary == FlowDirection.ZERO ): return [1, 0, 2, 3] else: return [0, 1, 2, 3] def get_equations(self) -> list[EquationObject]: + + if self.bypass_mode: + return self.get_equations_bypass() + return self.get_equations_normal() + + def get_equations_normal(self) -> list[EquationObject]: r"""Return the heat transfer equations. The method returns the heat transfer equations for the heat transfer asset. @@ -258,30 +264,101 @@ def get_equations(self) -> list[EquationObject]: :return: List[EquationObject] """ - equations = [] - # pressure to node equations - for connection_point in range(4): - equations.append(self.get_press_to_node_equation(connection_point=connection_point)) + # Check if there are four nodes connected to the asset + if len(self.connected_nodes) != 4: + raise ValueError("The number of connected nodes must be 4!") + # Check if the number of unknowns is 12 + if self.number_of_unknowns != 12: + raise ValueError("The number of unknowns must be 12!") + # Set connection points based on the flow direction + self.flow_direction_primary = self.flow_direction(self.prev_sol[0]) + self.flow_direction_secondary = self.flow_direction(self.prev_sol[6]) + ( + self.primary_side_inflow, + self.primary_side_outflow, + self.secondary_side_inflow, + self.secondary_side_outflow, + ) = self.get_ordered_connection_point_list() - # Internal energy to node equations - if not self.bypass_mode: - self.set_internal_energy_equations(equations) + if np.all(np.abs(self.prev_sol[0:-1:3]) < MASSFLOW_ZERO_LIMIT): + self.iteration_flow_direction_primary = self.flow_direction( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=self.primary_side_inflow, + use_relative_indexing=True, + ) + ] + ) + self.iteration_flow_direction_secondary = self.flow_direction( + self.prev_sol[ + self.get_index_matrix( + property_name="mass_flow_rate", + connection_point=self.secondary_side_inflow, + use_relative_indexing=True, + ) + ] + ) else: - self.set_internal_energy_equations_bypass(equations) + self.iteration_flow_direction_primary = self.flow_direction_primary + self.iteration_flow_direction_secondary = self.flow_direction_secondary + # Initialize the equations list + equations = [] - # set mass flow rate or pressure + # -- Internal energy (4x) -- + # Add the internal energy equations at connection points 0, and 2 to define + # the connection with the nodes. + equations.append( + self.get_internal_energy_to_node_equation(connection_point=self.primary_side_inflow) + ) + equations.append( + self.get_internal_energy_to_node_equation(connection_point=self.secondary_side_inflow) + ) + # Add the internal energy equations at connection points 1, and 3 to set + # the temperature through internal energy at the outlet of the heat transfer asset. + if self.iteration_flow_direction_primary != FlowDirection.ZERO: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=self.primary_side_outflow, + supply_temperature=self.temperature_out_primary, + ) + ) + else: + equations.append( + self.get_internal_energy_to_node_equation( + connection_point=self.primary_side_outflow + ) + ) + if self.iteration_flow_direction_secondary != FlowDirection.ZERO: + equations.append( + self.prescribe_temperature_at_connection_point( + connection_point=self.secondary_side_outflow, + supply_temperature=self.temperature_out_secondary, + ) + ) + else: + equations.append( + self.get_internal_energy_to_node_equation( + connection_point=self.secondary_side_outflow + ) + ) + # -- Mass flow rate or pressure on secondary side (2x) -- + # Prescribe the pressure at the secondary side of the heat transfer asset. if self.pre_scribe_mass_flow_secondary: - mset = self.mass_flow_rate_rate_set_point_secondary + if self.iteration_flow_direction_secondary == FlowDirection.ZERO: + mset = self.mass_flow_rate_rate_set_point_secondary + else: + mset = self.mass_flow_rate_rate_set_point_secondary equations.append( self.prescribe_mass_flow_at_connection_point( - connection_point=2, - mass_flow_value=mset, + connection_point=self.secondary_side_inflow, + mass_flow_value=-mset, ) ) equations.append( self.prescribe_mass_flow_at_connection_point( - connection_point=3, - mass_flow_value=-mset, + connection_point=self.secondary_side_outflow, + mass_flow_value=mset, ) ) else: @@ -297,37 +374,63 @@ def get_equations(self) -> list[EquationObject]: pset_in = self.pressure_set_point_secondary / 2 equations.append( self.prescribe_pressure_at_connection_point( - connection_point=2, + connection_point=self.secondary_side_inflow, pressure_value=pset_in, ) ) equations.append( self.prescribe_pressure_at_connection_point( - connection_point=3, + connection_point=self.secondary_side_outflow, pressure_value=pset_out, ) ) - # set mass flow rate or pressure + # -- Pressure (4x) -- + # Connect the pressure at the nodes to the asset + for connection_point in [ + self.primary_side_inflow, + self.primary_side_outflow, + self.secondary_side_inflow, + self.secondary_side_outflow, + ]: + equations.append(self.get_press_to_node_equation(connection_point=connection_point)) + + # -- Internal continuity (1x) -- + # Add the internal continuity equation at the primary side. + + # -- Energy balance equation for the heat transfer asset (1x) -- + # Defines the energy balance between the primary and secondary side of the + # heat transfer asset. + # If the mass flow at the inflow node of the primary and secondary side is not zero, if self.pre_scribe_mass_flow_primary: - mset = self.mass_flow_initialization_primary equations.append( - self.prescribe_mass_flow_at_connection_point( - connection_point=0, - mass_flow_value=mset, + self.add_continuity_equation( + connection_point_1=self.primary_side_inflow, + connection_point_2=self.primary_side_outflow, ) ) - equations.append( - self.prescribe_mass_flow_at_connection_point( - connection_point=1, - mass_flow_value=mset * -1, + if (self.iteration_flow_direction_primary != FlowDirection.ZERO) or ( + self.iteration_flow_direction_secondary != FlowDirection.ZERO + ): + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=self.primary_side_inflow, + mass_flow_value=self.mass_flow_initialization_primary, + ) + ) + # If the mass flow at the inflow node of the primary and secondary side is zero, + else: + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=self.primary_side_inflow, + mass_flow_value=self.mass_flow_initialization_primary, + ) ) - ) else: - if self.iteration_flow_direction_secondary == FlowDirection.ZERO: + if self.iteration_flow_direction_primary == FlowDirection.ZERO: pset_out = self.pressure_set_point_secondary pset_in = self.pressure_set_point_secondary else: - if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: + if self.iteration_flow_direction_primary == FlowDirection.POSITIVE: pset_out = self.pressure_set_point_secondary / 2 pset_in = self.pressure_set_point_secondary else: @@ -335,134 +438,20 @@ def get_equations(self) -> list[EquationObject]: pset_in = self.pressure_set_point_secondary / 2 equations.append( self.prescribe_pressure_at_connection_point( - connection_point=0, + connection_point=self.primary_side_inflow, pressure_value=pset_in, ) ) equations.append( self.prescribe_pressure_at_connection_point( - connection_point=1, + connection_point=self.primary_side_outflow, pressure_value=pset_out, ) ) + # Return the equations return equations - def set_internal_energy_equations(self, equations): - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=0, - use_relative_indexing=True, - ) - ] - < 0.0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=0, - supply_temperature=self.temperature_out_primary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=1, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=1)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=1, - supply_temperature=self.temperature_out_primary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=2, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=2)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=2, - supply_temperature=self.temperature_out_secondary, - ) - ) - if ( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=3, - use_relative_indexing=True, - ) - ] - < 0 - ): - equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) - else: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=3, - supply_temperature=self.temperature_out_secondary, - ) - ) - - def set_internal_energy_equations_bypass(self, equations): - equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) - equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) - equation_object = EquationObject() - # Short-circuiting the primary and secondary side of the heat transfer asset. - equation_object.indices = np.array( - [ - self.get_index_matrix( - property_name="internal_energy", - connection_point=1, - use_relative_indexing=False, - ), - self.get_index_matrix( - property_name="internal_energy", - connection_point=3, - use_relative_indexing=False, - ), - ] - ) - equation_object.coefficients = np.array([1.0, -1.0]) - equation_object.rhs = 0.0 - equations.append(equation_object) - equation_object2 = EquationObject() - equation_object2.indices = np.array( - [ - self.get_index_matrix( - property_name="internal_energy", - connection_point=0, - use_relative_indexing=False, - ), - self.get_index_matrix( - property_name="internal_energy", - connection_point=2, - use_relative_indexing=False, - ), - ] - ) - equation_object2.coefficients = np.array([1.0, -1.0]) - equation_object2.rhs = 0.0 - equations.append(equation_object2) - - def get_equations_old(self) -> list[EquationObject]: + def get_equations_bypass(self) -> list[EquationObject]: r"""Return the heat transfer equations. The method returns the heat transfer equations for the heat transfer asset. @@ -505,103 +494,27 @@ def get_equations_old(self) -> list[EquationObject]: :return: List[EquationObject] """ - # Check if there are four nodes connected to the asset - if len(self.connected_nodes) != 4: - raise ValueError("The number of connected nodes must be 4!") - # Check if the number of unknowns is 12 - if self.number_of_unknowns != 12: - raise ValueError("The number of unknowns must be 12!") - # Set connection points based on the flow direction - self.flow_direction_primary = self.flow_direction(self.mass_flow_initialization_primary) - self.flow_direction_secondary = self.flow_direction( - self.mass_flow_rate_rate_set_point_secondary - ) - - if np.all(np.abs(self.prev_sol[0:-1:3]) > MASSFLOW_ZERO_LIMIT): - self.iteration_flow_direction_primary = self.flow_direction( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=self.primary_side_inflow, - use_relative_indexing=True, - ) - ] - ) - self.iteration_flow_direction_secondary = self.flow_direction( - self.prev_sol[ - self.get_index_matrix( - property_name="mass_flow_rate", - connection_point=self.secondary_side_inflow, - use_relative_indexing=True, - ) - ] - ) - else: - self.iteration_flow_direction_primary = self.flow_direction_primary - self.iteration_flow_direction_secondary = self.flow_direction_secondary - - ( - self.primary_side_inflow, - self.primary_side_outflow, - self.secondary_side_inflow, - self.secondary_side_outflow, - ) = self.get_ordered_connection_point_list() - # Initialize the equations list equations = [] + # pressure to node equations + for connection_point in range(4): + equations.append(self.get_press_to_node_equation(connection_point=connection_point)) - # -- Internal energy (4x) -- - # Add the internal energy equations at connection points 0, and 2 to define - # the connection with the nodes. - equations.append( - self.get_internal_energy_to_node_equation(connection_point=self.primary_side_inflow) - ) - equations.append( - self.get_internal_energy_to_node_equation(connection_point=self.secondary_side_inflow) - ) - # Add the internal energy equations at connection points 1, and 3 to set - # the temperature through internal energy at the outlet of the heat transfer asset. - if self.iteration_flow_direction_primary != FlowDirection.ZERO: + # Internal energy to node equations + self.set_internal_energy_equations_bypass(equations) - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=self.primary_side_outflow, - supply_temperature=self.temperature_out_primary, - ) - ) - else: - equations.append( - self.get_internal_energy_to_node_equation( - connection_point=self.primary_side_outflow - ) - ) - if self.iteration_flow_direction_secondary != FlowDirection.ZERO: - equations.append( - self.prescribe_temperature_at_connection_point( - connection_point=self.secondary_side_outflow, - supply_temperature=self.temperature_out_secondary, - ) - ) - else: - equations.append( - self.get_internal_energy_to_node_equation( - connection_point=self.secondary_side_outflow - ) - ) - # -- Mass flow rate or pressure on secondary side (2x) -- - # Prescribe the pressure at the secondary side of the heat transfer asset. + # set mass flow rate or pressure if self.pre_scribe_mass_flow_secondary: - mset = self.mass_flow_rate_rate_set_point_secondary equations.append( self.prescribe_mass_flow_at_connection_point( - connection_point=self.secondary_side_inflow, - mass_flow_value=mset, + connection_point=2, + mass_flow_value=-mset, ) ) equations.append( self.prescribe_mass_flow_at_connection_point( - connection_point=self.secondary_side_outflow, - mass_flow_value=mset * -1, + connection_point=3, + mass_flow_value=mset, ) ) else: @@ -617,55 +530,31 @@ def get_equations_old(self) -> list[EquationObject]: pset_in = self.pressure_set_point_secondary / 2 equations.append( self.prescribe_pressure_at_connection_point( - connection_point=self.secondary_side_inflow, + connection_point=2, pressure_value=pset_in, ) ) equations.append( self.prescribe_pressure_at_connection_point( - connection_point=self.secondary_side_outflow, + connection_point=3, pressure_value=pset_out, ) ) - # -- Pressure (4x) -- - # Connect the pressure at the nodes to the asset - for connection_point in [ - self.primary_side_inflow, - self.primary_side_outflow, - self.secondary_side_inflow, - self.secondary_side_outflow, - ]: - equations.append(self.get_press_to_node_equation(connection_point=connection_point)) + # set mass flow rate or pressure if self.pre_scribe_mass_flow_primary: - # -- Internal continuity (1x) -- - # Add the internal continuity equation at the primary side. + mset = self.mass_flow_initialization_primary equations.append( - self.add_continuity_equation( - connection_point_1=self.primary_side_inflow, - connection_point_2=self.primary_side_outflow, + self.prescribe_mass_flow_at_connection_point( + connection_point=0, + mass_flow_value=mset, ) ) - # -- Energy balance equation for the heat transfer asset (1x) -- - # Defines the energy balance between the primary and secondary side of the - # heat transfer asset. - # If the mass flow at the inflow node of the primary and secondary side is not zero, - if (self.iteration_flow_direction_primary != FlowDirection.ZERO) or ( - self.iteration_flow_direction_secondary != FlowDirection.ZERO - ): - equations.append( - self.prescribe_mass_flow_at_connection_point( - connection_point=self.primary_side_inflow, - mass_flow_value=-1 * self.pre_scribe_mass_flow_primary, - ) - ) - # If the mass flow at the inflow node of the primary and secondary side is zero, - else: - equations.append( - self.prescribe_mass_flow_at_connection_point( - connection_point=self.primary_side_inflow, - mass_flow_value=0, - ) + equations.append( + self.prescribe_mass_flow_at_connection_point( + connection_point=1, + mass_flow_value=mset * -1, ) + ) else: if self.iteration_flow_direction_secondary == FlowDirection.ZERO: pset_out = self.pressure_set_point_secondary @@ -679,19 +568,59 @@ def get_equations_old(self) -> list[EquationObject]: pset_in = self.pressure_set_point_secondary / 2 equations.append( self.prescribe_pressure_at_connection_point( - connection_point=self.primary_side_inflow, + connection_point=0, pressure_value=pset_in, ) ) equations.append( self.prescribe_pressure_at_connection_point( - connection_point=self.primary_side_outflow, + connection_point=1, pressure_value=pset_out, ) ) - # Return the equations return equations + def set_internal_energy_equations_bypass(self, equations): + equations.append(self.get_internal_energy_to_node_equation(connection_point=0)) + equations.append(self.get_internal_energy_to_node_equation(connection_point=3)) + equation_object = EquationObject() + # Short-circuiting the primary and secondary side of the heat transfer asset. + equation_object.indices = np.array( + [ + self.get_index_matrix( + property_name="internal_energy", + connection_point=1, + use_relative_indexing=False, + ), + self.get_index_matrix( + property_name="internal_energy", + connection_point=3, + use_relative_indexing=False, + ), + ] + ) + equation_object.coefficients = np.array([1.0, -1.0]) + equation_object.rhs = 0.0 + equations.append(equation_object) + equation_object2 = EquationObject() + equation_object2.indices = np.array( + [ + self.get_index_matrix( + property_name="internal_energy", + connection_point=0, + use_relative_indexing=False, + ), + self.get_index_matrix( + property_name="internal_energy", + connection_point=2, + use_relative_indexing=False, + ), + ] + ) + equation_object2.coefficients = np.array([1.0, -1.0]) + equation_object2.rhs = 0.0 + equations.append(equation_object2) + def get_mass_flow_from_prev_solution(self) -> float: r"""Determine the mass flow rate from the previous solution. @@ -968,12 +897,9 @@ def get_electric_power_consumption(self) -> float: """Calculate the electric power consumption of the heat transfer asset. The electric power consumption is calculated as the absolute difference between the - heat power on the primary and secondary side, divided by the heat transfer coefficient. + heat power on the primary and secondary side. :return: float The electric power consumption of the heat transfer asset. """ - return ( - abs(self.get_heat_power_primary() - self.get_heat_power_secondary()) - / self.heat_transfer_coefficient - ) + return abs(abs(self.get_heat_power_primary()) - abs(self.get_heat_power_secondary()))