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,541 @@
|
|
|
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
|
+
"""module containing pipe class."""
|
|
16
|
+
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Dict
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from scipy.optimize import root
|
|
22
|
+
|
|
23
|
+
from simulator_core.entities.assets.asset_defaults import (
|
|
24
|
+
DEFAULT_MISSING_VALUE,
|
|
25
|
+
PROPERTY_DIAMETER,
|
|
26
|
+
PROPERTY_LENGTH,
|
|
27
|
+
PROPERTY_ROUGHNESS,
|
|
28
|
+
)
|
|
29
|
+
from simulator_core.solver.matrix.core_enum import NUMBER_CORE_QUANTITIES, IndexEnum
|
|
30
|
+
from simulator_core.solver.network.assets.fall_type import FallType
|
|
31
|
+
from simulator_core.solver.utils.fluid_properties import fluid_props
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SolverPipe(FallType):
|
|
35
|
+
"""Class to represent a pipe in a network."""
|
|
36
|
+
|
|
37
|
+
lambda_loss: float = 0.01
|
|
38
|
+
""" The dimensionless lambda loss value for the pipe. """
|
|
39
|
+
|
|
40
|
+
loss_coefficient: float = 0.0
|
|
41
|
+
r""" The loss coefficient of the pipe equal to .. math:: \frac{\lambda L}{2D A^2 g} """
|
|
42
|
+
|
|
43
|
+
reynolds_number: float = 0.0
|
|
44
|
+
r""" The Reynolds number of the flow in the pipe .. math:: \frac{v D}{\nu} """
|
|
45
|
+
|
|
46
|
+
alpha_value: float = 0.0
|
|
47
|
+
""" The heat transfer coefficient of the pipe with units [W/m^2/K] """
|
|
48
|
+
|
|
49
|
+
ambient_temperature: float = 293.15
|
|
50
|
+
""" The ambient temperature of the pipe with units [K] """
|
|
51
|
+
|
|
52
|
+
_grid_size: int = 10
|
|
53
|
+
""" The number of grid points in the internal grid of the pipe. """
|
|
54
|
+
|
|
55
|
+
_use_fluid_capacity: bool = False
|
|
56
|
+
""" A boolean to determine if the fluid capacity is used in the heat transfer calculation. """
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
name: uuid.UUID,
|
|
61
|
+
length: float = 1000.0,
|
|
62
|
+
diameter: float = 0.2,
|
|
63
|
+
roughness: float = 0.001,
|
|
64
|
+
):
|
|
65
|
+
"""Constructor of pipe class.
|
|
66
|
+
|
|
67
|
+
:param uuid.UUID name: The unique identifier of the pipe.
|
|
68
|
+
:param float length: The length of the pip [m] with a default value of 1000.0 m.
|
|
69
|
+
:param float diameter: The diameter of the pipe [m] with a default value of 0.2 m.
|
|
70
|
+
:param float roughness: The roughness of the pipe [m] with a default value of 1E-3 m.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(name=name)
|
|
73
|
+
# Set the physical properties of the pipe
|
|
74
|
+
self.length: float = length
|
|
75
|
+
self.diameter: float = diameter
|
|
76
|
+
self.roughness: float = roughness
|
|
77
|
+
# Calculate the area of the pipe
|
|
78
|
+
self.area: float = np.pi * self.diameter**2 / 4
|
|
79
|
+
# Create internal grid
|
|
80
|
+
self._internal_energy_grid = np.zeros((self._grid_size + 1, 1))
|
|
81
|
+
|
|
82
|
+
def set_physical_properties(self, physical_properties: Dict[str, float]) -> None:
|
|
83
|
+
"""Method to set the physical properties of the pipe.
|
|
84
|
+
|
|
85
|
+
:param physical_properties: dictionary containing the physical properties of the pipe.
|
|
86
|
+
expected properties are: length [m], diameter [m], roughness [m]
|
|
87
|
+
"""
|
|
88
|
+
expected_properties = [PROPERTY_LENGTH, PROPERTY_DIAMETER, PROPERTY_ROUGHNESS]
|
|
89
|
+
|
|
90
|
+
for expected_property in expected_properties:
|
|
91
|
+
if expected_property not in physical_properties:
|
|
92
|
+
raise ValueError(f"Property {expected_property} is missing in physical_properties")
|
|
93
|
+
if hasattr(self, expected_property):
|
|
94
|
+
setattr(self, expected_property, physical_properties[expected_property])
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Property {expected_property} is not a valid property " f"of the pipe"
|
|
98
|
+
)
|
|
99
|
+
# Update the area of the pipe
|
|
100
|
+
self.area = np.pi * self.diameter**2 / 4
|
|
101
|
+
|
|
102
|
+
def update_loss_coefficient(self) -> None:
|
|
103
|
+
r"""Method to update the loss coefficient of the pipe.
|
|
104
|
+
|
|
105
|
+
The loss coefficient is calculated using the following equation:
|
|
106
|
+
|
|
107
|
+
.. math:: \lambda \frac{L}{D} \frac{1}{2} \frac{1}{\rho A^2}
|
|
108
|
+
|
|
109
|
+
using the mass flow rate to velocity conversion:
|
|
110
|
+
|
|
111
|
+
.. math:: v = \frac{\dot{m}}{\rho A}
|
|
112
|
+
|
|
113
|
+
with the density of the fluid defined at the first connection point.
|
|
114
|
+
"""
|
|
115
|
+
self.calc_lambda_loss()
|
|
116
|
+
density = fluid_props.get_density(
|
|
117
|
+
fluid_props.get_t(self.prev_sol[IndexEnum.internal_energy])
|
|
118
|
+
)
|
|
119
|
+
self.loss_coefficient = (
|
|
120
|
+
self.lambda_loss
|
|
121
|
+
* (self.length / self.diameter)
|
|
122
|
+
* (1 / 2)
|
|
123
|
+
* (1 / (self.area**2 * density))
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# TODO: Do we want to implement a dependency on the connection point?
|
|
127
|
+
def calculate_reynolds_number(
|
|
128
|
+
self,
|
|
129
|
+
mass_flow_rate: float = DEFAULT_MISSING_VALUE,
|
|
130
|
+
temperature: float = DEFAULT_MISSING_VALUE,
|
|
131
|
+
) -> float:
|
|
132
|
+
r"""Method to calculate the Reynolds number of the flow in the pipe.
|
|
133
|
+
|
|
134
|
+
The Reynolds number is calculated using the following equation:
|
|
135
|
+
|
|
136
|
+
.. math:: \frac{v D}{\nu}
|
|
137
|
+
|
|
138
|
+
with the fluid properties defined in the fluid properties module. If
|
|
139
|
+
the mass flow rate and temperature are not provided, the mass flow rate
|
|
140
|
+
and temperature at the first connection point from the previous solution
|
|
141
|
+
are used.
|
|
142
|
+
|
|
143
|
+
:param float mass_flow_rate: The mass flow rate of the fluid (kg/s).
|
|
144
|
+
:param float temperature: The temperature of the fluid (K).
|
|
145
|
+
"""
|
|
146
|
+
# Retrieve properties from previous solution
|
|
147
|
+
if mass_flow_rate == DEFAULT_MISSING_VALUE:
|
|
148
|
+
mass_flow_rate = self.prev_sol[IndexEnum.discharge]
|
|
149
|
+
if temperature == DEFAULT_MISSING_VALUE:
|
|
150
|
+
temperature = fluid_props.get_t(self.prev_sol[IndexEnum.internal_energy])
|
|
151
|
+
# Calculate the Reynolds number
|
|
152
|
+
density = fluid_props.get_density(temperature)
|
|
153
|
+
discharge = mass_flow_rate / density
|
|
154
|
+
velocity = discharge / self.area
|
|
155
|
+
return velocity * self.diameter / fluid_props.get_viscosity(temperature)
|
|
156
|
+
|
|
157
|
+
def calc_lambda_loss(self) -> None:
|
|
158
|
+
r"""Method to calculate the lambda loss of the pipe.
|
|
159
|
+
|
|
160
|
+
For Reynolds numbers lower than 100:
|
|
161
|
+
.. math:: \lambda = 0.64
|
|
162
|
+
|
|
163
|
+
For Reynolds numbers between 100 and 2000:
|
|
164
|
+
.. math:: \lambda = \frac{64}{Re}
|
|
165
|
+
|
|
166
|
+
For Reynolds numbers higher than 2000 use the Colebrook-White equation:
|
|
167
|
+
.. math::
|
|
168
|
+
|
|
169
|
+
\frac{1}{\lambda} = -2 \log \left( \frac{\epsilon}{3.71 D} +
|
|
170
|
+
\frac{2.51}{Re \lambda} \right)
|
|
171
|
+
"""
|
|
172
|
+
# Update the Reynolds number
|
|
173
|
+
self.reynolds_number = self.calculate_reynolds_number()
|
|
174
|
+
# Determine the loss
|
|
175
|
+
if self.reynolds_number < 100:
|
|
176
|
+
self.lambda_loss = 0.64
|
|
177
|
+
elif self.reynolds_number < 2000:
|
|
178
|
+
self.lambda_loss = 64 / self.reynolds_number
|
|
179
|
+
elif self.reynolds_number < 4000:
|
|
180
|
+
# Interpolate between f calculated with Reynolds = 2000.0 and f calculated with
|
|
181
|
+
# Reynolds = 4000
|
|
182
|
+
f_lower = 64.0 / 2000.0
|
|
183
|
+
f_upper = self._colebrook_white(reynolds_number=4000.0)
|
|
184
|
+
self.lambda_loss = f_lower + ((f_upper - f_lower) / 2000.0) * (
|
|
185
|
+
self.reynolds_number - 2000.0
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
self.lambda_loss = self._colebrook_white()
|
|
189
|
+
|
|
190
|
+
def _colebrook_white_objective(self, lambda_guess: float) -> float:
|
|
191
|
+
r"""Root function for the Colebrook-White equation.
|
|
192
|
+
|
|
193
|
+
Solves the following optimization objective:
|
|
194
|
+
|
|
195
|
+
.. math::
|
|
196
|
+
|
|
197
|
+
- 2 \log \left( \frac{\epsilon}{D 3.71} + \frac{2.51}{Re \sqrt{\lambda}}
|
|
198
|
+
\right) - \frac{1}{\sqrt{\lambda}} = 0
|
|
199
|
+
|
|
200
|
+
:param float lambda_val: The friction factor.
|
|
201
|
+
:return: float, the friction factor.
|
|
202
|
+
"""
|
|
203
|
+
# Calculate the friction factor using the Colebrook-White equation
|
|
204
|
+
return float(
|
|
205
|
+
(
|
|
206
|
+
-2
|
|
207
|
+
* np.log10(
|
|
208
|
+
(2.51 / (self.reynolds_number * np.sqrt(lambda_guess)))
|
|
209
|
+
+ (self.roughness / (3.7 * self.diameter))
|
|
210
|
+
)
|
|
211
|
+
- 1.0 / np.sqrt(lambda_guess)
|
|
212
|
+
).item()
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _colebrook_white(
|
|
216
|
+
self,
|
|
217
|
+
lambda_guess: float = 0.001,
|
|
218
|
+
convergence_limit: float = 1e-4,
|
|
219
|
+
reynolds_number: float = DEFAULT_MISSING_VALUE,
|
|
220
|
+
) -> float:
|
|
221
|
+
r"""Method to iteratively calculate the Colebrook-White friction factor.
|
|
222
|
+
|
|
223
|
+
:param float lambda_guess: The initial guess for the 1/\sqrt{friction factor}.
|
|
224
|
+
:param float convergence_limit: The convergence limit for the iterative calculation.
|
|
225
|
+
:param float reynolds_number: The Reynolds number of the flow in the pipe.
|
|
226
|
+
:return: float, the friction factor.
|
|
227
|
+
"""
|
|
228
|
+
if reynolds_number == DEFAULT_MISSING_VALUE:
|
|
229
|
+
reynolds_number = self.reynolds_number
|
|
230
|
+
# Iterative calculation
|
|
231
|
+
return float(
|
|
232
|
+
root(
|
|
233
|
+
self._colebrook_white_objective, lambda_guess, method="hybr", tol=convergence_limit
|
|
234
|
+
).x[0]
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def _calculate_graetz_number(
|
|
238
|
+
self, temperature: float, mass_flow_rate: float = DEFAULT_MISSING_VALUE
|
|
239
|
+
) -> float:
|
|
240
|
+
r"""Calculate the Graetz number of the flow in the pipe.
|
|
241
|
+
|
|
242
|
+
If the mass flow rate is not provided, the mass flow rate from the previous solution is
|
|
243
|
+
used.
|
|
244
|
+
|
|
245
|
+
The Graetz number is calculated using the following equation:
|
|
246
|
+
|
|
247
|
+
.. math:: \text{Gz} = \frac{\alpha A}{D v}
|
|
248
|
+
|
|
249
|
+
If the velocity is zero, a large number is returned to indicate a low heat
|
|
250
|
+
transfer coefficient.
|
|
251
|
+
|
|
252
|
+
:param float temperature: The temperature of the fluid.
|
|
253
|
+
:param float mass_flow_rate: The mass flow rate of the fluid.
|
|
254
|
+
:return: float, the Graetz number.
|
|
255
|
+
"""
|
|
256
|
+
# Calculate the Reynolds number
|
|
257
|
+
reynolds_number = self.calculate_reynolds_number(
|
|
258
|
+
mass_flow_rate=mass_flow_rate, temperature=temperature
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Calculate the Prandtl number
|
|
262
|
+
prandtl_number = self.calculate_prandtl_number(temperature=temperature)
|
|
263
|
+
|
|
264
|
+
# Check velocity
|
|
265
|
+
if abs(reynolds_number) >= 1e-6:
|
|
266
|
+
return self.diameter / self.length * reynolds_number * prandtl_number
|
|
267
|
+
else:
|
|
268
|
+
# Return a large number so that the heat transfer coefficient is very low
|
|
269
|
+
return 10.0
|
|
270
|
+
|
|
271
|
+
def calculate_prandtl_number(self, temperature: float = DEFAULT_MISSING_VALUE) -> float:
|
|
272
|
+
r"""Calculate the Prandtl number of the fluid.
|
|
273
|
+
|
|
274
|
+
The Prandtl number is calculated using the following equation:
|
|
275
|
+
|
|
276
|
+
.. math:: \text{Pr} = \frac{\mu}{\alpha}
|
|
277
|
+
|
|
278
|
+
with the dynamic viscosity and thermal diffusivity of the fluid. The thermal diffusivity is
|
|
279
|
+
calculated as:
|
|
280
|
+
|
|
281
|
+
.. math:: \alpha = \frac{k}{\rho c_p}
|
|
282
|
+
|
|
283
|
+
which is the ratio of the dynamic viscosity to the thermal diffusivity of the fluid.
|
|
284
|
+
|
|
285
|
+
:param float temperature: The temperature of the fluid.
|
|
286
|
+
:return: float, the Prandtl number.
|
|
287
|
+
"""
|
|
288
|
+
# Check the temperature
|
|
289
|
+
if temperature == DEFAULT_MISSING_VALUE:
|
|
290
|
+
temperature = fluid_props.get_t(self.prev_sol[IndexEnum.internal_energy])
|
|
291
|
+
|
|
292
|
+
# Calculate the thermal diffusivity
|
|
293
|
+
thermal_diffusivity = fluid_props.get_thermal_conductivity(temperature) / (
|
|
294
|
+
fluid_props.get_density(temperature) * fluid_props.get_heat_capacity(temperature)
|
|
295
|
+
)
|
|
296
|
+
# Calculate the Prandtl number
|
|
297
|
+
return fluid_props.get_viscosity(temperature) / thermal_diffusivity
|
|
298
|
+
|
|
299
|
+
def _calculate_heat_transfer_coefficient_fluid(
|
|
300
|
+
self, temperature: float, mass_flow_rate: float
|
|
301
|
+
) -> float:
|
|
302
|
+
r"""Calculate the heat transfer coefficient of the fluid.
|
|
303
|
+
|
|
304
|
+
The heat transfer coefficient is calculated using the Nusselt number and the thermal
|
|
305
|
+
conductivity of the fluid.
|
|
306
|
+
|
|
307
|
+
If the Reynolds number is less than 1e4, the Nusselt number is calculated using the
|
|
308
|
+
Graetz number. For a Graetz number greater than 0.1, the Nusselt number is set to 3.66.
|
|
309
|
+
For a Graetz number less than 0.1, the Nusselt number is calculated using the following
|
|
310
|
+
equation:
|
|
311
|
+
|
|
312
|
+
.. math:: Nu = 1.62 \cdot \text{Gz}^{-1/3}
|
|
313
|
+
|
|
314
|
+
Otherwise, the Nusselt number is calculated using the Reynolds number and
|
|
315
|
+
|
|
316
|
+
.. math:: Nu = 0.023 \cdot \text{Re}^{0.8} \cdot \text{Pr}^{0.33}
|
|
317
|
+
|
|
318
|
+
where Pr is the Prandtl number of the fluid as:
|
|
319
|
+
|
|
320
|
+
.. math:: \text{Pr} = \frac{\mu}{\alpha}
|
|
321
|
+
|
|
322
|
+
The heat transfer coefficient is then calculated using the following equation:
|
|
323
|
+
|
|
324
|
+
.. math:: \alpha = Nu \cdot k / D
|
|
325
|
+
|
|
326
|
+
Source: R. Mudde, 'Fysische transportverschijnselen', 1998 p. 133 - p.134
|
|
327
|
+
|
|
328
|
+
:param float temperature: The temperature of the fluid.
|
|
329
|
+
:param float mass_flow_rate: The mass flow rate of the fluid.
|
|
330
|
+
"""
|
|
331
|
+
# Calculate the Reynolds number
|
|
332
|
+
reynolds_number = self.calculate_reynolds_number(
|
|
333
|
+
mass_flow_rate=mass_flow_rate, temperature=temperature
|
|
334
|
+
)
|
|
335
|
+
# Determine the Nusselt number
|
|
336
|
+
if reynolds_number < 1e4:
|
|
337
|
+
# Laminar flow assumptions
|
|
338
|
+
graetz_number = self._calculate_graetz_number(
|
|
339
|
+
temperature=temperature, mass_flow_rate=mass_flow_rate
|
|
340
|
+
)
|
|
341
|
+
# TODO: Discuss Graetz is defined for (1/Gz) < 0.05 and (1/Gz) > 0.1
|
|
342
|
+
if (1 / graetz_number) >= 0.1:
|
|
343
|
+
nusselt_number = 3.66
|
|
344
|
+
else:
|
|
345
|
+
nusselt_number = 1.62 * graetz_number ** (-1 / 3)
|
|
346
|
+
else:
|
|
347
|
+
prandtl_number = self.calculate_prandtl_number(temperature=temperature)
|
|
348
|
+
nusselt_number = 0.023 * reynolds_number**0.8 * prandtl_number**0.33
|
|
349
|
+
# Calculate the heat transfer coefficient
|
|
350
|
+
return nusselt_number * fluid_props.get_thermal_conductivity(temperature) / self.diameter
|
|
351
|
+
|
|
352
|
+
def _determine_flow_direction(self) -> tuple[float, int, int, int]:
|
|
353
|
+
"""Determine the flow direction and iteration indices for the pipe.
|
|
354
|
+
|
|
355
|
+
Method determines the flow direction and iteration indices for the
|
|
356
|
+
internal energy grid of the pipe. The method returns the mass flow rate,
|
|
357
|
+
the start index, the end index, and the step size for the iteration.
|
|
358
|
+
|
|
359
|
+
:return tuple[float, int, int, int]: The mass flow rate, the start index,
|
|
360
|
+
the end index, and the step size.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
# Reset the internal energy grid
|
|
364
|
+
self._internal_energy_grid = np.zeros((self._grid_size + 1, 1))
|
|
365
|
+
# Determine the flow direction
|
|
366
|
+
if self.prev_sol[IndexEnum.discharge] < 0:
|
|
367
|
+
# Flow from connection point 1 to connection point 0
|
|
368
|
+
start_index = self._grid_size - 1
|
|
369
|
+
end_index = -1
|
|
370
|
+
step = -1
|
|
371
|
+
# Set the internal energy at the connection point
|
|
372
|
+
self._internal_energy_grid[-1] = self.prev_sol[
|
|
373
|
+
IndexEnum.internal_energy + NUMBER_CORE_QUANTITIES
|
|
374
|
+
]
|
|
375
|
+
# Retrieve the mass flow rate
|
|
376
|
+
mass_flow_rate = abs(self.prev_sol[IndexEnum.discharge])
|
|
377
|
+
elif self.prev_sol[IndexEnum.discharge] > 0:
|
|
378
|
+
# Flow from connection point 0 to connection point 1
|
|
379
|
+
start_index = 1
|
|
380
|
+
end_index = self._grid_size + 1
|
|
381
|
+
step = 1
|
|
382
|
+
# Set the internal energy at the connection point
|
|
383
|
+
self._internal_energy_grid[0] = self.prev_sol[IndexEnum.internal_energy]
|
|
384
|
+
# Retrieve the mass flow rate
|
|
385
|
+
mass_flow_rate = self.prev_sol[IndexEnum.discharge]
|
|
386
|
+
else:
|
|
387
|
+
# No flow
|
|
388
|
+
start_index = 1
|
|
389
|
+
end_index = 0
|
|
390
|
+
step = 1
|
|
391
|
+
# Set the internal energy grid
|
|
392
|
+
self._internal_energy_grid[:] = fluid_props.get_ie(self.ambient_temperature)
|
|
393
|
+
# Retrieve the mass flow rate
|
|
394
|
+
mass_flow_rate = self.prev_sol[IndexEnum.discharge]
|
|
395
|
+
return mass_flow_rate, start_index, end_index, step
|
|
396
|
+
|
|
397
|
+
def _calculate_heat_loss_grid_point(self, iteration_index: int) -> float:
|
|
398
|
+
r"""Calculate the heat loss over a grid point.
|
|
399
|
+
|
|
400
|
+
The '_internal_energy_grid' method is used to calculate the internal energy
|
|
401
|
+
at the grid point. Thereafter, the temperature of the fluid is calculated
|
|
402
|
+
using the "get_t" method of the fluid properties. The heat loss is then
|
|
403
|
+
calculated using the following equation:
|
|
404
|
+
|
|
405
|
+
.. math::
|
|
406
|
+
|
|
407
|
+
\alpha A \left( T_{i} - T_{\text{ambient}} \right)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
:param int iteration_index: The index of the grid point.
|
|
411
|
+
"""
|
|
412
|
+
# Retrieve the internal energy
|
|
413
|
+
internal_energy_current = self._internal_energy_grid[iteration_index]
|
|
414
|
+
# Retrieve the temperature of the fluid
|
|
415
|
+
temperature_current = fluid_props.get_t(internal_energy_current)
|
|
416
|
+
# Calculate the heat loss
|
|
417
|
+
return -(
|
|
418
|
+
self.alpha_value
|
|
419
|
+
* np.pi
|
|
420
|
+
* self.diameter
|
|
421
|
+
* self.length
|
|
422
|
+
/ self._grid_size
|
|
423
|
+
* (temperature_current - self.ambient_temperature)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def _calculate_total_heat_transfer_coefficient(
|
|
427
|
+
self, temperature: float, mass_flow_rate: float
|
|
428
|
+
) -> float:
|
|
429
|
+
r"""Calculate the total heat transfer coefficient of the pipe.
|
|
430
|
+
|
|
431
|
+
The total heat transfer coefficient is calculated using the following equation:
|
|
432
|
+
|
|
433
|
+
.. math::
|
|
434
|
+
|
|
435
|
+
\frac{1}{\alpha} = \frac{1}{\alpha_{\text{fluid}}} +
|
|
436
|
+
\frac{1}{\alpha_{\text{wall}}}
|
|
437
|
+
|
|
438
|
+
where the heat transfer coefficient of the fluid is calculated using the
|
|
439
|
+
'_calculate_heat_transfer_coefficient_fluid' method and the heat transfer coefficient of the
|
|
440
|
+
wall is the alpha value of the pipe.
|
|
441
|
+
|
|
442
|
+
:param float temperature: The temperature of the fluid.
|
|
443
|
+
:param float mass_flow_rate: The mass flow rate of the fluid.
|
|
444
|
+
:return: float, the total heat transfer coefficient.
|
|
445
|
+
"""
|
|
446
|
+
if self._use_fluid_capacity:
|
|
447
|
+
# Determine the heat transfer coefficient including fluid properties
|
|
448
|
+
heat_transfer_coefficient_fluid = self._calculate_heat_transfer_coefficient_fluid(
|
|
449
|
+
temperature=temperature,
|
|
450
|
+
mass_flow_rate=mass_flow_rate,
|
|
451
|
+
)
|
|
452
|
+
if self.alpha_value > 0.0:
|
|
453
|
+
return 1 / (1 / heat_transfer_coefficient_fluid + 1 / self.alpha_value)
|
|
454
|
+
else:
|
|
455
|
+
return heat_transfer_coefficient_fluid
|
|
456
|
+
else:
|
|
457
|
+
return self.alpha_value
|
|
458
|
+
|
|
459
|
+
def _internal_energy_steady_state_objective(
|
|
460
|
+
self,
|
|
461
|
+
internal_energy_iteration: float,
|
|
462
|
+
internal_energy: float,
|
|
463
|
+
mass_flow_rate: float,
|
|
464
|
+
) -> float:
|
|
465
|
+
r"""Root function for the internal energy steady state equation.
|
|
466
|
+
|
|
467
|
+
Solves the following optimization objective:
|
|
468
|
+
|
|
469
|
+
.. math::
|
|
470
|
+
|
|
471
|
+
\dot{m} \left( e_{i-1} - e_{i} \right) - \alpha A \left( T_{i} - T_{\text{ambient}}
|
|
472
|
+
\right) = 0
|
|
473
|
+
|
|
474
|
+
:param float internal_energy_iteration: The internal energy at the current grid point.
|
|
475
|
+
:param float internal_energy: The internal energy at the previous grid point.
|
|
476
|
+
:param float mass_flow_rate: The mass flow rate of the fluid.
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
# Calculate the temperature of the fluid
|
|
480
|
+
temperature = fluid_props.get_t(internal_energy_iteration)
|
|
481
|
+
# Calculate the total heat transfer coefficient
|
|
482
|
+
total_heat_transfer_coefficient = self._calculate_total_heat_transfer_coefficient(
|
|
483
|
+
temperature=temperature, mass_flow_rate=mass_flow_rate
|
|
484
|
+
)
|
|
485
|
+
# Return the objective function
|
|
486
|
+
element_size = self.length / self._grid_size
|
|
487
|
+
objective = mass_flow_rate * (
|
|
488
|
+
internal_energy - internal_energy_iteration
|
|
489
|
+
) - total_heat_transfer_coefficient * np.pi * self.diameter * element_size * (
|
|
490
|
+
temperature - self.ambient_temperature
|
|
491
|
+
)
|
|
492
|
+
return float(np.array(objective).item())
|
|
493
|
+
|
|
494
|
+
def _update_internal_energy_grid(self) -> float:
|
|
495
|
+
r"""Update the internal energy grid of the pipe.
|
|
496
|
+
|
|
497
|
+
Iteratively calculate the internal energy at each grid point of the pipe.
|
|
498
|
+
|
|
499
|
+
Solve the following equation for the internal energy at each grid point:
|
|
500
|
+
.. math::
|
|
501
|
+
|
|
502
|
+
\dot{m} \left( e_{i-1} - e_{i} \right) = \alpha A \left( T_{i} -
|
|
503
|
+
T_{\text{ambient}} \right)
|
|
504
|
+
|
|
505
|
+
:return: float, the heat supplied by the pipe (W).
|
|
506
|
+
"""
|
|
507
|
+
# Retrieve the flow direction
|
|
508
|
+
mass_flow_rate, start_index, end_index, step = self._determine_flow_direction()
|
|
509
|
+
|
|
510
|
+
heat_supplied = 0.0
|
|
511
|
+
# Update the internal energy grid
|
|
512
|
+
for iteration_index in np.arange(start_index, end_index, step):
|
|
513
|
+
# Retrieve the previous value of the internal energy
|
|
514
|
+
previous_internal_energy = self._internal_energy_grid[iteration_index - step]
|
|
515
|
+
# Use root finding to determine the internal energy at the current grid point
|
|
516
|
+
internal_energy_iteration = root(
|
|
517
|
+
self._internal_energy_steady_state_objective,
|
|
518
|
+
previous_internal_energy,
|
|
519
|
+
args=(previous_internal_energy, mass_flow_rate),
|
|
520
|
+
method="hybr",
|
|
521
|
+
tol=1e-6,
|
|
522
|
+
).x[0]
|
|
523
|
+
# Update the internal energy grid
|
|
524
|
+
self._internal_energy_grid[iteration_index] = internal_energy_iteration
|
|
525
|
+
# Calculate the loss over the grid cell
|
|
526
|
+
heat_loss_grid_cell = self._calculate_heat_loss_grid_point(
|
|
527
|
+
iteration_index=iteration_index
|
|
528
|
+
)
|
|
529
|
+
# Update the heat supplied
|
|
530
|
+
heat_supplied += heat_loss_grid_cell
|
|
531
|
+
return heat_supplied
|
|
532
|
+
|
|
533
|
+
def update_heat_supplied(self) -> None:
|
|
534
|
+
"""Calculate the heat supplied by the pipe.
|
|
535
|
+
|
|
536
|
+
Definition: positive if heat is supplied to the fluid, negative if heat is extracted from
|
|
537
|
+
the fluid.
|
|
538
|
+
|
|
539
|
+
"""
|
|
540
|
+
# Update the internal energy grid
|
|
541
|
+
self.heat_supplied = self._update_internal_energy_grid()
|