Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 0 additions & 2 deletions src/omotes_simulator_core/entities/assets/asset_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@

connected_ports: list[str]
"""List of ids of the connected ports."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je verwijdert solver_asset: Waarom? Alles refereert intern nog steeds naar solver_asset...

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
Expand Down Expand Up @@ -106,9 +104,9 @@
"""
for i in range(len(self.connected_ports)):
output_dict_temp = {
PROPERTY_MASSFLOW: sign_output(i) * self.solver_asset.get_mass_flow_rate(i),

Check failure on line 107 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: "AssetAbstract" has no attribute "solver_asset" [attr-defined]
PROPERTY_PRESSURE: self.solver_asset.get_pressure(i),

Check failure on line 108 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: "AssetAbstract" has no attribute "solver_asset" [attr-defined]
PROPERTY_TEMPERATURE: self.solver_asset.get_temperature(i),

Check failure on line 109 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: "AssetAbstract" has no attribute "solver_asset" [attr-defined]
PROPERTY_VOLUMEFLOW: sign_output(i) * self.get_volume_flow_rate(i),
}
self.outputs[i].append(output_dict_temp)
Expand All @@ -122,8 +120,8 @@
:param int i: The index of the port.
:return float: The volume flow rate.
"""
rho = fluid_props.get_density(self.solver_asset.get_temperature(i))

Check failure on line 123 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: "AssetAbstract" has no attribute "solver_asset" [attr-defined]
return self.solver_asset.get_mass_flow_rate(i) / rho

Check failure on line 124 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: "AssetAbstract" has no attribute "solver_asset" [attr-defined]

Check failure on line 124 in src/omotes_simulator_core/entities/assets/asset_abstract.py

View workflow job for this annotation

GitHub Actions / Typecheck (3.11)

error: Returning Any from function declared to return "float" [no-any-return]

@abstractmethod
def write_to_output(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
49 changes: 36 additions & 13 deletions src/omotes_simulator_core/entities/assets/ates_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -171,23 +169,30 @@ 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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persoonlijk vind ik de -1 * explicieter, waardoor het direct zichtbaar is dat er nog iets met de waarde gebeurt. De - is te missen.

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kan je een kort comment toevoegen wat je hier aan het doen bent (ivm verwarring over sign/property)

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()
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(
Expand Down Expand Up @@ -279,7 +284,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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waarom vervangen? Als dit niet nodig is graag ook comment aanpassen.

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))
Expand Down Expand Up @@ -315,3 +320,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) < (
abs(self.thermal_power_allocation) * 0.001
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""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,
PROPERTY_SET_PRESSURE,
PROPERTY_TEMPERATURE_IN,
PROPERTY_TEMPERATURE_OUT,
PROPERTY_BYPASS,
SECONDARY,
)
from omotes_simulator_core.entities.assets.controller.asset_controller_abstract import (
Expand All @@ -41,26 +40,39 @@ 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ik heb hier wel een vraag over. Zetten we nu standaard temperaturen in dit asset met set_asset of is dit alleen een initialisatie stap?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Klopt de volgorder van de temperaturen; ik zou verwachtten dat PRIM_OUT gleijk moet zijn aan SEC_IN. Dit lijkt nu niet het geval?

return {
self.id: {
PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand,
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,
SECONDARY + PROPERTY_SET_PRESSURE: False,
PRIMARY + PROPERTY_SET_PRESSURE: False,
PROPERTY_BYPASS: True,
}
}
else:
return {
self.id: {
PRIMARY + PROPERTY_HEAT_DEMAND: heat_demand / self.factor,
PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30,
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 + 40,
SECONDARY + PROPERTY_SET_PRESSURE: False,
PRIMARY + PROPERTY_SET_PRESSURE: False,
PROPERTY_BYPASS: False,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@

import datetime

from numpy.ma.core import product

from omotes_simulator_core.entities.assets.asset_defaults import (
Comment on lines 17 to 21
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing product from numpy.ma.core relies on NumPy’s internal/private module structure. Prefer numpy.prod(...) or math.prod(...) (and import from the public namespace) to avoid breakage across NumPy versions.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just import numpy or import math.prod.

PROPERTY_HEAT_DEMAND,
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 (
Expand Down Expand Up @@ -49,7 +53,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."""
Expand All @@ -69,7 +73,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:
Expand All @@ -91,39 +95,39 @@ 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 (
return float(
sum([consumer.get_heat_demand(time) for consumer in self.consumers])
* self.factor_to_first_network
* product(self.factor_to_first_network)
)

def get_total_discharge_storage(self) -> float:
"""Method to get the total storage discharge of the network corrected to the first network.

: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)
)

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]))
* 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:
Expand Down Expand Up @@ -166,6 +170,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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waarom heb je de temperatuur nodig voor een storage? Deze wordt toch niet geset maar gebasseerd op de internal state?

PROPERTY_TEMPERATURE_IN: storage.temperature_in,
}
return storage_settings

Expand All @@ -180,6 +186,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

Expand Down Expand Up @@ -226,15 +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.heat_transfer_assets_sec:
return self.heat_transfer_assets_sec[0].id
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, SECONDARY + PROPERTY_SET_PRESSURE)
if self.heat_transfer_assets_prim:
return (self.heat_transfer_assets_prim[0].id, PRIMARY + PROPERTY_SET_PRESSURE)
raise ValueError("No asset found for which the pressure can be set.")
4 changes: 2 additions & 2 deletions src/omotes_simulator_core/entities/assets/demand_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convergence tolerance should be non-negative. Since consumer profiles can yield negative demands, self.thermal_power_allocation * 0.001 can become negative and make this check always fail. Use abs(self.thermal_power_allocation) * 0.001 for the tolerance.

Suggested change
self.thermal_power_allocation * 0.001
abs(self.thermal_power_allocation) * 0.001

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zou hier inderdaad niet nog een extra abs ergens omheen moeten?

)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waarom de -? Het is toch het verschil tussen 'sec en prim'

- self.solver_asset.get_heat_power_secondary() # type: ignore
),
}
Expand Down
Loading
Loading