Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def write_to_output(self) -> None:
"""

def get_timeseries(self) -> DataFrame:
"""Get timeseries as a dataframe from a asset.
"""Get timeseries as a dataframe from an asset.

The header is a tuple of the port id and the property name.
"""
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: 37 additions & 12 deletions src/omotes_simulator_core/entities/assets/ates_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ 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:
if self.mass_flowrate <= 0:
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,6 @@ 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 @@ -173,21 +170,31 @@ def set_setpoints(self, setpoints: dict) -> None:
if necessary_setpoints.issubset(setpoints_set):
self.thermal_power_allocation = -1 * setpoints[PROPERTY_HEAT_DEMAND]
if self.first_time_step:
self.temperature_in = setpoints[PROPERTY_TEMPERATURE_IN]
self.temperature_out = setpoints[PROPERTY_TEMPERATURE_OUT]
# Depending on the sign of the power allocation the ATES is charging or discharging.
# If positive then charging and the Flow direction is negative, So in and out
# temperature are switch, since they are not set on flow direction but on port.
if self.thermal_power_allocation >= 0:
Copy link
Copy Markdown
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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gedaan

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 @@ -315,3 +322,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,10 +14,9 @@
# 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_BYPASS,
PROPERTY_HEAT_DEMAND,
PROPERTY_SET_PRESSURE,
PROPERTY_TEMPERATURE_IN,
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
Copy Markdown
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
Copy Markdown
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ja temepraturen worden nu hard gezet,
Voor Bypass mode maakt het niet uit, want dan worden ze niet gebruikt.
In normale modus moeten we eigenlijk nog de waardes ophalen uit de carrier en die gebruiken.
Maar dit zat in de vorige implementatie ook nog niet

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,
PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 30,
PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50,
SECONDARY + PROPERTY_HEAT_DEMAND: heat_demand * -1 * self.factor,
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

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,
SECONDARY,
)
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
* float(np.prod(np.array(self.factor_to_first_network, dtype=float)))
)

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])
* float(np.prod(np.array(self.factor_to_first_network, dtype=float)))
)

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])
* float(np.prod(np.array(self.factor_to_first_network, dtype=float)))
)

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])
* float(np.prod(np.array(self.factor_to_first_network, dtype=float)))
)

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
Copy Markdown
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ATES gebruikt ze om mass flow uit te rekenen. en ideal storage zal dat ook doen. In iede geval voor initiele gok

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,18 @@ 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 the key.

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.
The controller can then add this in the set points dicts. For heattransfer assets also the
controller key needs to be return, either primary or secondary.
"""
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) < (
abs(self.thermal_power_allocation) * 0.001
)
Loading
Loading