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.
Files changed (59) hide show
  1. omotes_simulator_core-0.0.3.dev0.dist-info/LICENSE +621 -0
  2. omotes_simulator_core-0.0.3.dev0.dist-info/METADATA +55 -0
  3. omotes_simulator_core-0.0.3.dev0.dist-info/RECORD +59 -0
  4. omotes_simulator_core-0.0.3.dev0.dist-info/WHEEL +5 -0
  5. omotes_simulator_core-0.0.3.dev0.dist-info/top_level.txt +1 -0
  6. simulator_core/__init__.py +15 -0
  7. simulator_core/__main__.py +21 -0
  8. simulator_core/adapter/__init__.py +16 -0
  9. simulator_core/adapter/presentation/__init__.py +16 -0
  10. simulator_core/adapter/transforms/__init__.py +16 -0
  11. simulator_core/adapter/transforms/esdl_asset_mapper.py +53 -0
  12. simulator_core/adapter/transforms/mappers.py +226 -0
  13. simulator_core/adapter/transforms/string_to_esdl.py +66 -0
  14. simulator_core/adapter/transforms/transform_utils.py +34 -0
  15. simulator_core/entities/__init__.py +16 -0
  16. simulator_core/entities/assets/__init__.py +16 -0
  17. simulator_core/entities/assets/asset_abstract.py +116 -0
  18. simulator_core/entities/assets/asset_defaults.py +68 -0
  19. simulator_core/entities/assets/controller_classes.py +81 -0
  20. simulator_core/entities/assets/demand_cluster.py +117 -0
  21. simulator_core/entities/assets/esdl_asset_object.py +60 -0
  22. simulator_core/entities/assets/junction.py +64 -0
  23. simulator_core/entities/assets/pipe.py +174 -0
  24. simulator_core/entities/assets/production_cluster.py +222 -0
  25. simulator_core/entities/assets/utils.py +154 -0
  26. simulator_core/entities/esdl_object.py +90 -0
  27. simulator_core/entities/heat_network.py +82 -0
  28. simulator_core/entities/network_controller.py +67 -0
  29. simulator_core/entities/simulation_configuration.py +31 -0
  30. simulator_core/entities/utility/__init__.py +16 -0
  31. simulator_core/entities/utility/influxdb_reader.py +129 -0
  32. simulator_core/infrastructure/__init__.py +16 -0
  33. simulator_core/infrastructure/app.py +55 -0
  34. simulator_core/infrastructure/simulation_manager.py +55 -0
  35. simulator_core/infrastructure/utils.py +45 -0
  36. simulator_core/py.typed +0 -0
  37. simulator_core/simulation/__init__.py +18 -0
  38. simulator_core/simulation/mappers/__init__.py +16 -0
  39. simulator_core/simulation/mappers/mappers.py +35 -0
  40. simulator_core/simulation/networksimulation.py +63 -0
  41. simulator_core/solver/__init__.py +15 -0
  42. simulator_core/solver/matrix/__init__.py +15 -0
  43. simulator_core/solver/matrix/core_enum.py +26 -0
  44. simulator_core/solver/matrix/equation_object.py +56 -0
  45. simulator_core/solver/matrix/matrix.py +125 -0
  46. simulator_core/solver/matrix/utility.py +38 -0
  47. simulator_core/solver/network/__init__.py +15 -0
  48. simulator_core/solver/network/assets/__init__.py +15 -0
  49. simulator_core/solver/network/assets/base_asset.py +303 -0
  50. simulator_core/solver/network/assets/base_item.py +63 -0
  51. simulator_core/solver/network/assets/base_node_item.py +67 -0
  52. simulator_core/solver/network/assets/boundary.py +83 -0
  53. simulator_core/solver/network/assets/fall_type.py +225 -0
  54. simulator_core/solver/network/assets/node.py +274 -0
  55. simulator_core/solver/network/assets/production_asset.py +171 -0
  56. simulator_core/solver/network/assets/solver_pipe.py +541 -0
  57. simulator_core/solver/network/network.py +398 -0
  58. simulator_core/solver/solver.py +80 -0
  59. 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()