omotes-simulator-core 0.0.3.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omotes_simulator_core-0.0.3.dev0.dist-info/LICENSE +621 -0
- omotes_simulator_core-0.0.3.dev0.dist-info/METADATA +55 -0
- omotes_simulator_core-0.0.3.dev0.dist-info/RECORD +59 -0
- omotes_simulator_core-0.0.3.dev0.dist-info/WHEEL +5 -0
- omotes_simulator_core-0.0.3.dev0.dist-info/top_level.txt +1 -0
- simulator_core/__init__.py +15 -0
- simulator_core/__main__.py +21 -0
- simulator_core/adapter/__init__.py +16 -0
- simulator_core/adapter/presentation/__init__.py +16 -0
- simulator_core/adapter/transforms/__init__.py +16 -0
- simulator_core/adapter/transforms/esdl_asset_mapper.py +53 -0
- simulator_core/adapter/transforms/mappers.py +226 -0
- simulator_core/adapter/transforms/string_to_esdl.py +66 -0
- simulator_core/adapter/transforms/transform_utils.py +34 -0
- simulator_core/entities/__init__.py +16 -0
- simulator_core/entities/assets/__init__.py +16 -0
- simulator_core/entities/assets/asset_abstract.py +116 -0
- simulator_core/entities/assets/asset_defaults.py +68 -0
- simulator_core/entities/assets/controller_classes.py +81 -0
- simulator_core/entities/assets/demand_cluster.py +117 -0
- simulator_core/entities/assets/esdl_asset_object.py +60 -0
- simulator_core/entities/assets/junction.py +64 -0
- simulator_core/entities/assets/pipe.py +174 -0
- simulator_core/entities/assets/production_cluster.py +222 -0
- simulator_core/entities/assets/utils.py +154 -0
- simulator_core/entities/esdl_object.py +90 -0
- simulator_core/entities/heat_network.py +82 -0
- simulator_core/entities/network_controller.py +67 -0
- simulator_core/entities/simulation_configuration.py +31 -0
- simulator_core/entities/utility/__init__.py +16 -0
- simulator_core/entities/utility/influxdb_reader.py +129 -0
- simulator_core/infrastructure/__init__.py +16 -0
- simulator_core/infrastructure/app.py +55 -0
- simulator_core/infrastructure/simulation_manager.py +55 -0
- simulator_core/infrastructure/utils.py +45 -0
- simulator_core/py.typed +0 -0
- simulator_core/simulation/__init__.py +18 -0
- simulator_core/simulation/mappers/__init__.py +16 -0
- simulator_core/simulation/mappers/mappers.py +35 -0
- simulator_core/simulation/networksimulation.py +63 -0
- simulator_core/solver/__init__.py +15 -0
- simulator_core/solver/matrix/__init__.py +15 -0
- simulator_core/solver/matrix/core_enum.py +26 -0
- simulator_core/solver/matrix/equation_object.py +56 -0
- simulator_core/solver/matrix/matrix.py +125 -0
- simulator_core/solver/matrix/utility.py +38 -0
- simulator_core/solver/network/__init__.py +15 -0
- simulator_core/solver/network/assets/__init__.py +15 -0
- simulator_core/solver/network/assets/base_asset.py +303 -0
- simulator_core/solver/network/assets/base_item.py +63 -0
- simulator_core/solver/network/assets/base_node_item.py +67 -0
- simulator_core/solver/network/assets/boundary.py +83 -0
- simulator_core/solver/network/assets/fall_type.py +225 -0
- simulator_core/solver/network/assets/node.py +274 -0
- simulator_core/solver/network/assets/production_asset.py +171 -0
- simulator_core/solver/network/assets/solver_pipe.py +541 -0
- simulator_core/solver/network/network.py +398 -0
- simulator_core/solver/solver.py +80 -0
- simulator_core/solver/utils/fluid_properties.py +173 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""ProductionCluster class."""
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Dict
|
|
19
|
+
|
|
20
|
+
from simulator_core.entities.assets.asset_abstract import AssetAbstract
|
|
21
|
+
from simulator_core.entities.assets.asset_defaults import (
|
|
22
|
+
DEFAULT_NODE_HEIGHT,
|
|
23
|
+
DEFAULT_PRESSURE,
|
|
24
|
+
DEFAULT_TEMPERATURE,
|
|
25
|
+
DEFAULT_TEMPERATURE_DIFFERENCE,
|
|
26
|
+
PROPERTY_HEAT_DEMAND,
|
|
27
|
+
PROPERTY_MASSFLOW,
|
|
28
|
+
PROPERTY_PRESSURE_RETURN,
|
|
29
|
+
PROPERTY_PRESSURE_SUPPLY,
|
|
30
|
+
PROPERTY_SET_PRESSURE,
|
|
31
|
+
PROPERTY_TEMPERATURE_RETURN,
|
|
32
|
+
PROPERTY_TEMPERATURE_SUPPLY,
|
|
33
|
+
)
|
|
34
|
+
from simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject
|
|
35
|
+
from simulator_core.entities.assets.utils import (
|
|
36
|
+
heat_demand_and_temperature_to_mass_flow,
|
|
37
|
+
)
|
|
38
|
+
from simulator_core.solver.network.assets.production_asset import ProductionAsset
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProductionCluster(AssetAbstract):
|
|
42
|
+
"""A ProductionCluster represents an asset that produces heat."""
|
|
43
|
+
|
|
44
|
+
thermal_production_required: float | None
|
|
45
|
+
"""The thermal production required by the asset [W]."""
|
|
46
|
+
|
|
47
|
+
temperature_supply: float
|
|
48
|
+
"""The supply temperature of the asset [K]."""
|
|
49
|
+
|
|
50
|
+
temperature_return: float
|
|
51
|
+
"""The return temperature of the asset [K]."""
|
|
52
|
+
|
|
53
|
+
pressure_supply: float
|
|
54
|
+
"""The supply pressure of the asset [Pa]."""
|
|
55
|
+
|
|
56
|
+
control_mass_flow: bool
|
|
57
|
+
"""Flag to indicate whether the mass flow rate is controlled.
|
|
58
|
+
If True, the mass flow rate is controlled. If False, the mass flow rate is not controlled
|
|
59
|
+
and the pressure is predescribed.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
controlled_mass_flow: float | None
|
|
63
|
+
"""The controlled mass flow of the asset [kg/s]."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, asset_name: str, asset_id: str):
|
|
66
|
+
"""Initialize a ProductionCluster object.
|
|
67
|
+
|
|
68
|
+
:param str asset_name: The name of the asset.
|
|
69
|
+
:param str asset_id: The unique identifier of the asset.
|
|
70
|
+
:param PandapipesNet pandapipe_net: Pandapipes network object to register asset to.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(asset_name=asset_name, asset_id=asset_id)
|
|
73
|
+
self.height_m = DEFAULT_NODE_HEIGHT
|
|
74
|
+
# DemandCluster thermal and mass flow specifications
|
|
75
|
+
self.thermal_production_required = None
|
|
76
|
+
self.temperature_supply = DEFAULT_TEMPERATURE
|
|
77
|
+
self.temperature_return = DEFAULT_TEMPERATURE - DEFAULT_TEMPERATURE_DIFFERENCE
|
|
78
|
+
# DemandCluster pressure specifications
|
|
79
|
+
self.pressure_supply = DEFAULT_PRESSURE
|
|
80
|
+
self.control_mass_flow = False
|
|
81
|
+
# Controlled mass flow
|
|
82
|
+
self.controlled_mass_flow = None
|
|
83
|
+
self.solver_asset = ProductionAsset(
|
|
84
|
+
name=uuid.uuid4(),
|
|
85
|
+
pre_scribe_mass_flow=False,
|
|
86
|
+
set_pressure=self.pressure_supply,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def add_physical_data(self, esdl_asset: EsdlAssetObject) -> None:
|
|
90
|
+
"""Method to add physical data to the asset.
|
|
91
|
+
|
|
92
|
+
:param EsdlAssetObject esdl_asset: The ESDL asset object containing the physical data.
|
|
93
|
+
:return:
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def _set_supply_temperature(self, temperature_supply: float) -> None:
|
|
97
|
+
"""Set the supply temperature of the asset.
|
|
98
|
+
|
|
99
|
+
:param float temperature_supply: The supply temperature of the asset.
|
|
100
|
+
The temperature should be supplied in Kelvin.
|
|
101
|
+
"""
|
|
102
|
+
# Set the temperature of the circulation pump mass flow
|
|
103
|
+
self.temperature_supply = temperature_supply
|
|
104
|
+
self.solver_asset.supply_temperature = self.temperature_supply
|
|
105
|
+
|
|
106
|
+
def _set_return_temperature(self, temperature_return: float) -> None:
|
|
107
|
+
"""Set the return temperature of the asset.
|
|
108
|
+
|
|
109
|
+
:param float temperature_return: The return temperature of the asset.
|
|
110
|
+
The temperature should be supplied in Kelvin.
|
|
111
|
+
"""
|
|
112
|
+
# Set the return temperature of the asset
|
|
113
|
+
self.temperature_return = temperature_return
|
|
114
|
+
|
|
115
|
+
def _set_heat_demand(self, heat_demand: float) -> None:
|
|
116
|
+
"""Set the heat demand of the asset.
|
|
117
|
+
|
|
118
|
+
:param float heat_demand: The heat demand of the asset.
|
|
119
|
+
The heat demand should be supplied in Watts.
|
|
120
|
+
"""
|
|
121
|
+
# Calculate the mass flow rate
|
|
122
|
+
self.controlled_mass_flow = heat_demand_and_temperature_to_mass_flow(
|
|
123
|
+
thermal_demand=heat_demand,
|
|
124
|
+
temperature_supply=self.temperature_supply,
|
|
125
|
+
temperature_return=self.temperature_return,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Check if the mass flow rate is positive
|
|
129
|
+
if self.controlled_mass_flow < 0.0:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"The mass flow rate {self.controlled_mass_flow} of the asset {self.name}"
|
|
132
|
+
+ " is negative."
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
# Set the mass flow rate of the control valve
|
|
136
|
+
self.solver_asset.mass_flow_rate_set_point = self.controlled_mass_flow # type: ignore
|
|
137
|
+
|
|
138
|
+
def _set_pressure_or_mass_flow_control(self, pressure_supply: bool) -> None:
|
|
139
|
+
"""Set the asset to predescribe either the pressure or the mass flow rate.
|
|
140
|
+
|
|
141
|
+
:param bool pressure_supply: True when the pressure needs to be set
|
|
142
|
+
"""
|
|
143
|
+
if pressure_supply:
|
|
144
|
+
self.solver_asset.pre_scribe_mass_flow = False # type: ignore
|
|
145
|
+
else:
|
|
146
|
+
self.solver_asset.pre_scribe_mass_flow = True # type: ignore
|
|
147
|
+
|
|
148
|
+
def set_pressure_supply(self, pressure_supply: float) -> None:
|
|
149
|
+
"""Set the supply pressure of the asset.
|
|
150
|
+
|
|
151
|
+
:param float pressure_supply: The supply pressure of the asset.
|
|
152
|
+
The pressure should be supplied in Pascal.
|
|
153
|
+
"""
|
|
154
|
+
# Check if the pressure is positive
|
|
155
|
+
if pressure_supply < 0.0:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"The pressure {pressure_supply} of the asset {self.name} can not be negative."
|
|
158
|
+
)
|
|
159
|
+
# Set the supply pressure of the asset
|
|
160
|
+
self.pressure_supply = pressure_supply
|
|
161
|
+
# Set the pressure of the solver asset
|
|
162
|
+
self.solver_asset.set_pressure = self.pressure_supply # type: ignore
|
|
163
|
+
|
|
164
|
+
def set_setpoints(self, setpoints: Dict) -> None:
|
|
165
|
+
"""Set the setpoints of the asset.
|
|
166
|
+
|
|
167
|
+
:param Dict setpoints: The setpoints of the asset in a dictionary,
|
|
168
|
+
as "property_name": value pairs.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
# Default keys required
|
|
172
|
+
necessary_setpoints = {
|
|
173
|
+
PROPERTY_TEMPERATURE_SUPPLY,
|
|
174
|
+
PROPERTY_TEMPERATURE_RETURN,
|
|
175
|
+
PROPERTY_HEAT_DEMAND,
|
|
176
|
+
PROPERTY_SET_PRESSURE,
|
|
177
|
+
}
|
|
178
|
+
# Dict to set
|
|
179
|
+
setpoints_set = set(setpoints.keys())
|
|
180
|
+
# Check if all setpoints are in the setpoints
|
|
181
|
+
if necessary_setpoints.issubset(setpoints_set):
|
|
182
|
+
# Set the setpoints
|
|
183
|
+
self._set_pressure_or_mass_flow_control(setpoints[PROPERTY_SET_PRESSURE])
|
|
184
|
+
self._set_supply_temperature(setpoints[PROPERTY_TEMPERATURE_SUPPLY])
|
|
185
|
+
self._set_return_temperature(setpoints[PROPERTY_TEMPERATURE_RETURN])
|
|
186
|
+
self._set_heat_demand(setpoints[PROPERTY_HEAT_DEMAND])
|
|
187
|
+
else:
|
|
188
|
+
# Print missing setpoints
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"The setpoints {necessary_setpoints.difference(setpoints_set)} are missing."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def update(self) -> None:
|
|
194
|
+
"""Update the asset properties to the results from the previous (timestep) simulation.
|
|
195
|
+
|
|
196
|
+
Sets the values of the supply temperature, return temperature and heat demand
|
|
197
|
+
to the values of the previous simulation. In addition, the mass flow rate is set
|
|
198
|
+
to the value of the previous simulation.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def write_to_output(self) -> None:
|
|
202
|
+
"""Write the output of the asset to the output list.
|
|
203
|
+
|
|
204
|
+
The output list is a list of dictionaries, where each dictionary
|
|
205
|
+
represents the output of its asset for a specific timestep.
|
|
206
|
+
|
|
207
|
+
The output of the asset is a dictionary with the following keys:
|
|
208
|
+
- PROPERTY_HEAT_DEMAND: The heat demand of the asset.
|
|
209
|
+
- PROPERTY_TEMPERATURE_SUPPLY: The supply temperature of the asset.
|
|
210
|
+
- PROPERTY_TEMPERATURE_RETURN: The return temperature of the asset.
|
|
211
|
+
- PROPERTY_PRESSURE_SUPPLY: The supply pressure of the asset.
|
|
212
|
+
- PROPERTY_PRESSURE_RETURN: The return pressure of the asset.
|
|
213
|
+
- PROPERTY_MASSFLOW: The mass flow rate of the asset.
|
|
214
|
+
"""
|
|
215
|
+
output_dict = {
|
|
216
|
+
PROPERTY_MASSFLOW: self.solver_asset.get_mass_flow_rate(1),
|
|
217
|
+
PROPERTY_PRESSURE_SUPPLY: self.solver_asset.get_pressure(0),
|
|
218
|
+
PROPERTY_PRESSURE_RETURN: self.solver_asset.get_pressure(1),
|
|
219
|
+
PROPERTY_TEMPERATURE_SUPPLY: self.solver_asset.get_temperature(0),
|
|
220
|
+
PROPERTY_TEMPERATURE_RETURN: self.solver_asset.get_temperature(1),
|
|
221
|
+
}
|
|
222
|
+
self.output.append(output_dict)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""Utility functions for assets."""
|
|
17
|
+
from enum import IntEnum
|
|
18
|
+
|
|
19
|
+
from pandapipes import pandapipesNet
|
|
20
|
+
import numpy as np
|
|
21
|
+
from simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject
|
|
22
|
+
from typing import List, Tuple
|
|
23
|
+
from simulator_core.solver.utils.fluid_properties import fluid_props
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def heat_demand_and_temperature_to_mass_flow(
|
|
27
|
+
thermal_demand: float,
|
|
28
|
+
temperature_supply: float,
|
|
29
|
+
temperature_return: float
|
|
30
|
+
) -> float:
|
|
31
|
+
"""Calculate the mass flow rate that is required to meet the thermal demand.
|
|
32
|
+
|
|
33
|
+
:param float thermal_demand: The thermal demand of the asset. The thermal demand should be
|
|
34
|
+
supplied in Watts.
|
|
35
|
+
:param float temperature_supply: The temperature that the asset delivers to the "to_junction".
|
|
36
|
+
The temperature should be supplied in Kelvin. The supply temperature is used to calculate
|
|
37
|
+
the specific heat capacity of the fluid.
|
|
38
|
+
:param float temperature_return: The temperature that the asset receives from the
|
|
39
|
+
"from_junction". The temperature should be supplied in Kelvin.
|
|
40
|
+
"""
|
|
41
|
+
heat_capacity = fluid_props.get_heat_capacity(
|
|
42
|
+
(temperature_return + temperature_supply) / 2)
|
|
43
|
+
return thermal_demand / ((temperature_supply - temperature_return) * float(heat_capacity))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def mass_flow_and_temperature_to_heat_demand(
|
|
47
|
+
temperature_supply: float,
|
|
48
|
+
temperature_return: float,
|
|
49
|
+
mass_flow: float,
|
|
50
|
+
) -> float:
|
|
51
|
+
"""Calculate the thermal demand that is met by the mass flow rate.
|
|
52
|
+
|
|
53
|
+
:param float temperature_supply: The temperature that the asset delivers to the "to_junction".
|
|
54
|
+
The temperature should be supplied in Kelvin. The supply temperature is used to calculate
|
|
55
|
+
the specific heat capacity of the fluid.
|
|
56
|
+
:param float temperature_return: The temperature that the asset receives from the
|
|
57
|
+
"from_junction". The temperature should be supplied in Kelvin.
|
|
58
|
+
:param float mass_flow: The mass flow rate that is used to meet the thermal demand. The mass
|
|
59
|
+
flow rate should be supplied in kg/s.
|
|
60
|
+
"""
|
|
61
|
+
internal_energy1 = fluid_props.get_ie(temperature_supply)
|
|
62
|
+
internal_energy2 = fluid_props.get_ie(temperature_return)
|
|
63
|
+
return mass_flow * (internal_energy1 - internal_energy2)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_thermal_conductivity_table(esdl_asset: EsdlAssetObject) -> Tuple[List[float], List[float]]:
|
|
67
|
+
"""Retrieve the thermal conductivity table of the asset.
|
|
68
|
+
|
|
69
|
+
:param EsdlAssetObject esdl_asset: The asset of which the heat transfer table should be
|
|
70
|
+
retrieved.
|
|
71
|
+
:return: The diameters and thermal conductivities of the asset in a tuple with the diameters
|
|
72
|
+
as the first element and the thermal conductivities as the second element. Each element
|
|
73
|
+
represents a layer of the asset.
|
|
74
|
+
"""
|
|
75
|
+
# create heat transfer table
|
|
76
|
+
layer_thicknesses = []
|
|
77
|
+
diameters = []
|
|
78
|
+
heat_coef = []
|
|
79
|
+
if esdl_asset.esdl_asset.material is not None:
|
|
80
|
+
if hasattr(esdl_asset.esdl_asset.material, "component"):
|
|
81
|
+
material_object = esdl_asset.esdl_asset.material.component
|
|
82
|
+
elif hasattr(esdl_asset.esdl_asset.material, "reference"):
|
|
83
|
+
material_object = esdl_asset.esdl_asset.material.reference.component
|
|
84
|
+
else:
|
|
85
|
+
raise NotImplementedError("Unknown material type or reference.")
|
|
86
|
+
# Append initial internal diameter
|
|
87
|
+
diameters.append(esdl_asset.esdl_asset.innerDiameter)
|
|
88
|
+
# Loop over material object
|
|
89
|
+
for layer in material_object:
|
|
90
|
+
layer_thicknesses.append(layer.layerWidth * 2)
|
|
91
|
+
diameters.append(sum(layer_thicknesses) + esdl_asset.esdl_asset.innerDiameter)
|
|
92
|
+
heat_coef.append(layer.matter.thermalConductivity)
|
|
93
|
+
return diameters, heat_coef
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def calculate_inverse_heat_transfer_coefficient(
|
|
97
|
+
inner_diameter: np.ndarray, outer_diameter: np.ndarray, thermal_conductivity: np.ndarray
|
|
98
|
+
) -> np.ndarray:
|
|
99
|
+
"""Calculate the inverse heat transfer coefficient of a pipe.
|
|
100
|
+
|
|
101
|
+
:param inner_diameter: Inner diameter of the pipe in m
|
|
102
|
+
:param outer_diameter: Outer diameter of the pipe in m
|
|
103
|
+
:param thermal_conductivity: Thermal conductivity of the pipe material in W/(m K)
|
|
104
|
+
:return: Inverse heat transfer coefficient in W/(m^2 K)
|
|
105
|
+
"""
|
|
106
|
+
return np.array((inner_diameter * np.log(outer_diameter / inner_diameter))
|
|
107
|
+
/ (2.0 * thermal_conductivity))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def mass_flow_to_volume_flow(
|
|
111
|
+
mass_flow_rate: float,
|
|
112
|
+
temperature_fluid: float,
|
|
113
|
+
pandapipes_net: pandapipesNet,
|
|
114
|
+
) -> float:
|
|
115
|
+
"""Calculate the volume flow rate from mass flow rate.
|
|
116
|
+
|
|
117
|
+
:param float mass_flow_rate: the mass flow rate in kg/s
|
|
118
|
+
:param float temperature_fluid: the fluid temperature in K
|
|
119
|
+
:param pandapipesNet pandapipes_net: The pandapipes network used to calculate
|
|
120
|
+
the specific density.
|
|
121
|
+
|
|
122
|
+
:return float volume_flow rate: the volume flow rate in m3/s
|
|
123
|
+
"""
|
|
124
|
+
density_fluid = pandapipes_net.fluid.get_density(temperature_fluid)
|
|
125
|
+
if (density_fluid == 0) | (density_fluid is None):
|
|
126
|
+
raise ValueError("The density of the fluid is zero or None.")
|
|
127
|
+
volume_flowrate = mass_flow_rate / density_fluid
|
|
128
|
+
return float(volume_flowrate)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def volume_flow_to_mass_flow(
|
|
132
|
+
volume_flowrate: float,
|
|
133
|
+
temperature_fluid: float,
|
|
134
|
+
pandapipes_net: pandapipesNet,
|
|
135
|
+
) -> float:
|
|
136
|
+
"""Calculate the mass flowrate from volume flowrate.
|
|
137
|
+
|
|
138
|
+
:param float volume_flowrate: the volume flowrate in m3/s
|
|
139
|
+
:param float temperature_fluid: the fluid temperature in K
|
|
140
|
+
:param pandapipesNet net: The pandapipes network used to calculate the specific density.
|
|
141
|
+
|
|
142
|
+
:return float mass_flowrate: the mass flowrate in kg/s
|
|
143
|
+
"""
|
|
144
|
+
density_fluid = pandapipes_net.fluid.get_density(temperature_fluid)
|
|
145
|
+
mass_flowrate = volume_flowrate * density_fluid
|
|
146
|
+
|
|
147
|
+
return float(mass_flowrate)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Port(IntEnum):
|
|
151
|
+
"""Simple enumeration class to set if it is In (from) or out(to) port of asset."""
|
|
152
|
+
|
|
153
|
+
In = 0
|
|
154
|
+
Out = 1
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""Esdl asset wrapper class."""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import List, Tuple
|
|
20
|
+
|
|
21
|
+
from esdl import InPort, OutPort
|
|
22
|
+
from esdl.esdl_handler import EnergySystemHandler
|
|
23
|
+
|
|
24
|
+
from simulator_core.adapter.transforms.string_to_esdl import StringEsdlAssetMapper
|
|
25
|
+
from simulator_core.entities.assets.esdl_asset_object import EsdlAssetObject
|
|
26
|
+
from simulator_core.entities.assets.utils import Port
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EsdlObject:
|
|
32
|
+
"""EsdlObject class is a wrapper around PyEsdl."""
|
|
33
|
+
|
|
34
|
+
energy_system_handler: EnergySystemHandler
|
|
35
|
+
|
|
36
|
+
def __init__(self, esdl_energysystem_handler: EnergySystemHandler) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Constructor for EsdlObject.
|
|
39
|
+
|
|
40
|
+
:param esdl_energysystem: PyEsdl EnergySystem object
|
|
41
|
+
"""
|
|
42
|
+
self.energy_system_handler = esdl_energysystem_handler
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
"""Returns a string describing the esdl file object."""
|
|
46
|
+
return str(self.energy_system_handler)
|
|
47
|
+
|
|
48
|
+
def get_all_assets_of_type(self, esdl_asset_type: str) -> list[EsdlAssetObject]:
|
|
49
|
+
"""
|
|
50
|
+
Returns a list of all the esdl assets of the specified type in the esdl file.
|
|
51
|
+
|
|
52
|
+
If the type is not found an empty list is returned.
|
|
53
|
+
:param esdl_asset_type: str of the asset type assets need to be gathered.
|
|
54
|
+
"""
|
|
55
|
+
output_list = []
|
|
56
|
+
for asset_type in StringEsdlAssetMapper().to_esdl(esdl_asset_type):
|
|
57
|
+
output_list += [
|
|
58
|
+
EsdlAssetObject(asset)
|
|
59
|
+
for asset in self.energy_system_handler.get_all_instances_of_type(asset_type)
|
|
60
|
+
]
|
|
61
|
+
return output_list
|
|
62
|
+
|
|
63
|
+
def get_connected_assets(self, asset_id: str, port: Port) -> List[Tuple[str, Port]]:
|
|
64
|
+
"""Method to get the id's of connected assets from the esdl.
|
|
65
|
+
|
|
66
|
+
This returns a list of list with the connected asset id and the port to which it is
|
|
67
|
+
connected to the asset.
|
|
68
|
+
First it is set if the request is an in or outport. Then the connected ports to the asset
|
|
69
|
+
are added to a list. In the final step all assets elong to the ports are listed together
|
|
70
|
+
with the type of port they are connected to.
|
|
71
|
+
|
|
72
|
+
:param str id: id of the asset for which we want to know the connected assets
|
|
73
|
+
:param Port port: port for which the connected assets need to be returned.
|
|
74
|
+
:return: List of list which the id of the connected assets and the connected port.
|
|
75
|
+
"""
|
|
76
|
+
# TODO 1. Add support for components with multiple in and outports, like heat exchanger
|
|
77
|
+
# TODO 2. What if it is connected to a joint?
|
|
78
|
+
connected_assets = []
|
|
79
|
+
esdl_asset = self.energy_system_handler.get_by_id(asset_id)
|
|
80
|
+
|
|
81
|
+
type_port = OutPort if port == Port.Out else InPort
|
|
82
|
+
connected_port_ids = []
|
|
83
|
+
for esdl_port in esdl_asset.port:
|
|
84
|
+
if isinstance(esdl_port, type_port):
|
|
85
|
+
connected_port_ids = esdl_port.connectedTo
|
|
86
|
+
break
|
|
87
|
+
for connected_port_id in connected_port_ids:
|
|
88
|
+
connected_port_type = Port.Out if isinstance(connected_port_id, OutPort) else Port.In
|
|
89
|
+
connected_assets.append((connected_port_id.energyasset.id, connected_port_type))
|
|
90
|
+
return connected_assets
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""HeatNetwork entity class."""
|
|
17
|
+
import datetime
|
|
18
|
+
from typing import Callable, List, Tuple
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
from simulator_core.entities.assets.asset_abstract import AssetAbstract
|
|
23
|
+
from simulator_core.entities.assets.junction import Junction
|
|
24
|
+
from simulator_core.solver.network.network import Network
|
|
25
|
+
from simulator_core.solver.solver import Solver
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HeatNetwork:
|
|
29
|
+
"""Class to store information on the heat network."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, conversion_factory: Callable[[Network], Tuple[List[AssetAbstract],
|
|
32
|
+
List[Junction]]]) -> None:
|
|
33
|
+
"""Constructor of heat network class.
|
|
34
|
+
|
|
35
|
+
:param conversion_factory: method to convert the esdl network to pandapipes
|
|
36
|
+
assets and junctions and returns list of both
|
|
37
|
+
"""
|
|
38
|
+
self.network = Network()
|
|
39
|
+
self.assets, self.junctions = conversion_factory(self.network)
|
|
40
|
+
self.solver = Solver(self.network)
|
|
41
|
+
|
|
42
|
+
def run_time_step(self, time: datetime.datetime, controller_input: dict) -> None:
|
|
43
|
+
"""Method to simulate a time step.
|
|
44
|
+
|
|
45
|
+
It first sets the controller input to the assets and then simulates the time step.
|
|
46
|
+
|
|
47
|
+
:param float time: Timestep for which to simulate the model
|
|
48
|
+
:param dict controller_input: Dict specifying the heat demand for the different assets.
|
|
49
|
+
:return: None
|
|
50
|
+
"""
|
|
51
|
+
for py_asset in self.assets:
|
|
52
|
+
if py_asset.asset_id in controller_input:
|
|
53
|
+
py_asset.set_setpoints(controller_input[py_asset.asset_id])
|
|
54
|
+
self.solver.solve()
|
|
55
|
+
|
|
56
|
+
def plot_network(self) -> None:
|
|
57
|
+
"""Method to plot the network.
|
|
58
|
+
|
|
59
|
+
plots the network in a simple plot all junctions are translated to a circle.
|
|
60
|
+
:return:
|
|
61
|
+
"""
|
|
62
|
+
def store_output(self) -> None:
|
|
63
|
+
"""Method to store the output data.
|
|
64
|
+
|
|
65
|
+
This method takes the data from the pandapipes dataframe and stores it into our own
|
|
66
|
+
dataframe. This is needed since we have the possibility to redo a timestep when results are
|
|
67
|
+
not converged for the input of the controller.
|
|
68
|
+
:return: None
|
|
69
|
+
"""
|
|
70
|
+
for py_asset in self.assets:
|
|
71
|
+
py_asset.write_to_output()
|
|
72
|
+
|
|
73
|
+
def gather_output(self) -> pd.DataFrame:
|
|
74
|
+
"""Method to gather output of all assets and return it as a dict.
|
|
75
|
+
|
|
76
|
+
:return: Dict with for all asset a dict with all outputs.
|
|
77
|
+
"""
|
|
78
|
+
# Todo what do we do with the junction output do we need it?
|
|
79
|
+
result = pd.DataFrame()
|
|
80
|
+
for py_asset in self.assets:
|
|
81
|
+
result = pd.concat([result, py_asset.get_timeseries()], axis=1)
|
|
82
|
+
return result
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""NetworkController entity."""
|
|
17
|
+
import datetime
|
|
18
|
+
|
|
19
|
+
from simulator_core.entities.assets.asset_defaults import (PROPERTY_TEMPERATURE_SUPPLY,
|
|
20
|
+
PROPERTY_TEMPERATURE_RETURN,
|
|
21
|
+
PROPERTY_HEAT_DEMAND,
|
|
22
|
+
PROPERTY_SET_PRESSURE)
|
|
23
|
+
from simulator_core.entities.assets.controller_classes import ControllerSource, ControllerConsumer
|
|
24
|
+
from typing import List
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NetworkController:
|
|
28
|
+
"""Class to store the network controller."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, consumers: List[ControllerConsumer], sources: List[ControllerSource]) \
|
|
31
|
+
-> None:
|
|
32
|
+
"""Constructor for controller for a heat network."""
|
|
33
|
+
self.sources = sources
|
|
34
|
+
self.consumers = consumers
|
|
35
|
+
|
|
36
|
+
def run_time_step(self, time: datetime.datetime) -> dict:
|
|
37
|
+
"""Method to get the controller inputs for the network.
|
|
38
|
+
|
|
39
|
+
:param float time: Time step for which to run the controller.
|
|
40
|
+
:return: dict with the key the asset id and the heat demand for that asset.
|
|
41
|
+
"""
|
|
42
|
+
# TODO add also the possibility to return mass flow rate instead of heat demand.
|
|
43
|
+
|
|
44
|
+
controller_input = {}
|
|
45
|
+
for consumer in self.consumers:
|
|
46
|
+
controller_input[consumer.id] = {PROPERTY_HEAT_DEMAND: consumer.get_heat_demand(time),
|
|
47
|
+
PROPERTY_TEMPERATURE_RETURN:
|
|
48
|
+
consumer.temperature_return,
|
|
49
|
+
PROPERTY_TEMPERATURE_SUPPLY:
|
|
50
|
+
consumer.temperature_supply}
|
|
51
|
+
for source in self.sources:
|
|
52
|
+
controller_input[source.id] = {PROPERTY_HEAT_DEMAND: self.get_total_demand(time)
|
|
53
|
+
/ len(self.sources),
|
|
54
|
+
PROPERTY_TEMPERATURE_RETURN: source.temperature_return,
|
|
55
|
+
PROPERTY_TEMPERATURE_SUPPLY: source.temperature_supply,
|
|
56
|
+
PROPERTY_SET_PRESSURE: False}
|
|
57
|
+
# setting the first source to set the pressure for now.
|
|
58
|
+
controller_input[self.sources[0].id][PROPERTY_SET_PRESSURE] = True
|
|
59
|
+
return controller_input
|
|
60
|
+
|
|
61
|
+
def get_total_demand(self, time: datetime.datetime) -> float:
|
|
62
|
+
"""Method to get the total heat demand of the network."""
|
|
63
|
+
return sum([consumer.get_heat_demand(time) for consumer in self.consumers])
|
|
64
|
+
|
|
65
|
+
def get_total_supply(self) -> float:
|
|
66
|
+
"""Method to get the total heat supply of the network."""
|
|
67
|
+
return float(sum([source.power for source in self.sources]))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""Configuration parameters for the simulation that are not included in the ESDL."""
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SimulationConfiguration:
|
|
25
|
+
"""Class to store configuration parameters of the simulations."""
|
|
26
|
+
|
|
27
|
+
simulation_id: uuid.UUID
|
|
28
|
+
name: str
|
|
29
|
+
timestep: int
|
|
30
|
+
start: datetime
|
|
31
|
+
stop: datetime
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) 2023. Deltares & TNO
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""__init__.py file for initialization code."""
|