geoloop 0.0.1__py3-none-any.whl → 1.0.0b1__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 (47) hide show
  1. geoloop/axisym/AxisymetricEL.py +751 -0
  2. geoloop/axisym/__init__.py +3 -0
  3. geoloop/bin/Flowdatamain.py +89 -0
  4. geoloop/bin/Lithologymain.py +84 -0
  5. geoloop/bin/Loadprofilemain.py +100 -0
  6. geoloop/bin/Plotmain.py +250 -0
  7. geoloop/bin/Runbatch.py +81 -0
  8. geoloop/bin/Runmain.py +86 -0
  9. geoloop/bin/SingleRunSim.py +928 -0
  10. geoloop/bin/__init__.py +3 -0
  11. geoloop/cli/__init__.py +0 -0
  12. geoloop/cli/batch.py +106 -0
  13. geoloop/cli/main.py +105 -0
  14. geoloop/configuration.py +946 -0
  15. geoloop/constants.py +112 -0
  16. geoloop/geoloopcore/CoaxialPipe.py +503 -0
  17. geoloop/geoloopcore/CustomPipe.py +727 -0
  18. geoloop/geoloopcore/__init__.py +3 -0
  19. geoloop/geoloopcore/b2g.py +739 -0
  20. geoloop/geoloopcore/b2g_ana.py +535 -0
  21. geoloop/geoloopcore/boreholedesign.py +683 -0
  22. geoloop/geoloopcore/getloaddata.py +112 -0
  23. geoloop/geoloopcore/pyg_ana.py +280 -0
  24. geoloop/geoloopcore/pygfield_ana.py +519 -0
  25. geoloop/geoloopcore/simulationparameters.py +130 -0
  26. geoloop/geoloopcore/soilproperties.py +152 -0
  27. geoloop/geoloopcore/strat_interpolator.py +194 -0
  28. geoloop/lithology/__init__.py +3 -0
  29. geoloop/lithology/plot_lithology.py +277 -0
  30. geoloop/lithology/process_lithology.py +697 -0
  31. geoloop/loadflowdata/__init__.py +3 -0
  32. geoloop/loadflowdata/flow_data.py +161 -0
  33. geoloop/loadflowdata/loadprofile.py +325 -0
  34. geoloop/plotting/__init__.py +3 -0
  35. geoloop/plotting/create_plots.py +1137 -0
  36. geoloop/plotting/load_data.py +432 -0
  37. geoloop/utils/RunManager.py +164 -0
  38. geoloop/utils/__init__.py +0 -0
  39. geoloop/utils/helpers.py +841 -0
  40. geoloop-1.0.0b1.dist-info/METADATA +112 -0
  41. geoloop-1.0.0b1.dist-info/RECORD +46 -0
  42. geoloop-1.0.0b1.dist-info/entry_points.txt +2 -0
  43. geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0b1.dist-info/licenses/LICENSE.md +2 -1
  44. geoloop-0.0.1.dist-info/METADATA +0 -10
  45. geoloop-0.0.1.dist-info/RECORD +0 -6
  46. {geoloop-0.0.1.dist-info → geoloop-1.0.0b1.dist-info}/WHEEL +0 -0
  47. {geoloop-0.0.1.dist-info → geoloop-1.0.0b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,683 @@
1
+ from collections.abc import Sequence
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pygfunction as gt
6
+ from pygfunction.media import Fluid
7
+ from scipy import optimize
8
+
9
+ from geoloop.configuration import SingleRunConfig
10
+ from geoloop.geoloopcore.CoaxialPipe import CoaxialPipe
11
+ from geoloop.geoloopcore.CustomPipe import (
12
+ CustomPipe,
13
+ thermal_resistance_pipe,
14
+ thermal_resistance_pipe_insulated,
15
+ )
16
+ from geoloop.geoloopcore.strat_interpolator import StratInterpolator
17
+
18
+
19
+ class BoreholeDesign:
20
+ COAXIAL: str = "COAXIAL"
21
+ """Configuration type for coaxial pipes."""
22
+
23
+ UTUBE: str = "UTUBE"
24
+ """Configuration type for U-tube pipes."""
25
+
26
+ """
27
+ Representation of a borehole heat exchanger (BHE) design, including
28
+ geometric properties, pipe configuration, and material parameters.
29
+
30
+ The class supports two configurations:
31
+
32
+ **COAXIAL**
33
+ - One inlet (outer pipe) and one outlet (inner pipe).
34
+ - Pipes are concentric and centered at (0, 0).
35
+ - No `pos` specification is needed.
36
+
37
+ **UTUBE**
38
+ - One or more inlet and outlet pipes.
39
+ - Pipes are arranged radially symmetric around the borehole center.
40
+ - Inlets are assumed connected to outlets at the borehole bottom.
41
+ - Radial positions must be constant for all inlets and constant for
42
+ all outlets.
43
+
44
+ Notes
45
+ -----
46
+ - Depth-dependent grout conductivity is supported via interpolation.
47
+ - Optional thermal insulation can be applied to the outlet pipe(s).
48
+ - For PYGFIELD models, additional parameters define field layout and
49
+ borehole inclination.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ type: str,
55
+ H: float,
56
+ D: float,
57
+ r_b: float,
58
+ r_in: list[float],
59
+ r_out: list[float],
60
+ k_p: float | list[float],
61
+ k_g: float | list[float],
62
+ pos: list[list[float]] = [[0.0, 0.0], [0.0, 0.0]],
63
+ J: int = 3,
64
+ nInlets: int = 1,
65
+ m_flow: float = 1.0,
66
+ T_f: float = 10.0,
67
+ fluid_str: str = "Water",
68
+ fluid_percent: float = 100.0,
69
+ epsilon: float = 1e-6,
70
+ z_k_g: list[float] | None = None,
71
+ ncalcsegments: int = 1,
72
+ insu_z: float = 0.0,
73
+ insu_dr: float = 0.0,
74
+ insu_k: float = 0.03,
75
+ N: int = 1,
76
+ M: int = 1,
77
+ R: float = 3.0,
78
+ inclination_start: float = 0.0,
79
+ inclination_end: float = 0.0,
80
+ num_tiltedsegments: int = 1,
81
+ ) -> None:
82
+ """
83
+ Initialize a borehole design with geometry, pipe configuration, and material
84
+ properties.
85
+
86
+ Parameters
87
+ ----------
88
+ type : {"COAXIAL", "UTUBE"}
89
+ Borehole configuration type.
90
+ H : float
91
+ Borehole length (m).
92
+ D : float
93
+ Buried depth of borehole top (m).
94
+ r_b : float
95
+ Borehole radius (m).
96
+ r_in : array_like
97
+ Inner radii of pipe(s) (m).
98
+ r_out : array_like
99
+ Outer radii of pipe(s) (m).
100
+ k_p : float or array_like
101
+ Thermal conductivity of the pipe material (W/m·K).
102
+ k_g : float or array_like
103
+ Grout thermal conductivity. If array-like, depth-dependent values
104
+ must be supplied along with `z_k_g`.
105
+ pos : tuple of float, optional
106
+ Radial position of pipe(s) for UTUBE layouts. Ignored for COAXIAL.
107
+ J : int, optional
108
+ Number of segments used in `pygfunction` internal pipe discretization.
109
+ nInlets : int, optional
110
+ Number of inlet pipes (UTUBE).
111
+ m_flow : float, optional
112
+ Mass flow rate of circulating fluid (kg/s).
113
+ T_f : float, optional
114
+ Initial fluid temperature (°C).
115
+ fluid_str : {"Water", "MPG", "MEG", "MMA", "MEA"}, optional
116
+ Fluid type for hydraulic and thermal properties.
117
+ fluid_percent : float, optional
118
+ Percentage of the working fluid relative to water.
119
+ epsilon : float, optional
120
+ Pipe roughness for hydraulic calculations (m).
121
+ z_k_g : array_like, optional
122
+ Depth breakpoints corresponding to grout thermal conductivities.
123
+ ncalcsegments : int, optional
124
+ Number of vertical discretization segments for thermal calculations.
125
+ insu_z : float, optional
126
+ Depth up to which outlet pipes are insulated (m).
127
+ insu_dr : float, optional
128
+ Fraction of pipe wall thickness that is insulated.
129
+ insu_k : float, optional
130
+ Insulation thermal conductivity (W/m·K).
131
+ N, : int, optional
132
+ Total nr of boreholes in borehole field for PYGFIELD models.
133
+ M : int, optional
134
+ Boreholes per side of the borehole field for PYGFIELD models (only equally sided fields are supported).
135
+ R : float, optional
136
+ Borefield radius or spacing parameter.
137
+ inclination_start : float, optional
138
+ Start angle of borehole inclination (degrees).
139
+ inclination_end : float, optional
140
+ End angle of borehole inclination (degrees).
141
+ num_tiltedsegments : int, optional
142
+ Number of segments for inclined/tilted borehole discretization.
143
+
144
+ Notes
145
+ -----
146
+ - The thermal resistance of insulated pipes is computed in `getR_p`.
147
+ - Depth-dependent grout conductivity is evaluated via `StratInterpolator`.
148
+ - A `CustomPipe` or `CoaxialPipe` object is created automatically.
149
+ """
150
+ self.D = D
151
+ self.H = H
152
+ self.r_b = r_b
153
+ self.r_in = np.asarray(r_in)
154
+ self.r_out = np.asarray(r_out)
155
+ self.n_p = len(r_out)
156
+ self.nInlets = nInlets
157
+ self.k_p = k_p * np.ones(self.n_p)
158
+ self.pos = np.asarray(pos)
159
+ self.ncalcsegments = ncalcsegments
160
+
161
+ dz = H / ncalcsegments
162
+ zmin = D + 0.5 * dz
163
+ zmax = D + H - 0.5 * dz
164
+ self.zseg = np.linspace(zmin, zmax, ncalcsegments)
165
+
166
+ self.k_g = np.atleast_1d(k_g)
167
+ if z_k_g == None:
168
+ self.z_k_g = np.ones_like(k_g)
169
+ else:
170
+ self.z_k_g = np.asarray(z_k_g)
171
+
172
+ self.interpolatorKg = StratInterpolator(self.z_k_g, self.k_g, stepvalue=True)
173
+
174
+ self.type = type
175
+ self.J = J
176
+ self.m_flow = m_flow
177
+ self.T_f = T_f
178
+ self.fluid_str = fluid_str
179
+ self.fluid_percent = fluid_percent
180
+ self.epsilon = epsilon
181
+
182
+ self.insu_z = insu_z
183
+ self.insu_k = insu_k
184
+ self.insu_dr = insu_dr
185
+
186
+ # (JDvW) I think these can be set to None creating the borehole design object, only created from config with None
187
+ self.Re_in = None
188
+ self.Re_out = None
189
+
190
+ self.N = N
191
+ self.M = M
192
+ self.R = R
193
+ self.inclination_start = inclination_start
194
+ self.inclination_end = inclination_end
195
+ self.num_tiltedsegments = num_tiltedsegments
196
+
197
+ # modify the R_p for the insulated part assume the thickness of the r_in/r_out constant
198
+
199
+ self.customPipe = self.get_custom_pipe()
200
+
201
+ def get_r_p(self, z: np.ndarray) -> list[np.ndarray]:
202
+ """
203
+ Compute pipe thermal resistance at specific depths.
204
+
205
+ Insulation is applied only to outlet pipes (i.e., pipes with index
206
+ ``>= nInlets``) and only for depths shallower than `insu_z`.
207
+
208
+ Parameters
209
+ ----------
210
+ z : array_like of float
211
+ Depth values (m) at which pipe resistances are evaluated.
212
+
213
+ Returns
214
+ -------
215
+ list of ndarray
216
+ List containing pipe resistances for each depth.
217
+ Each entry contains an array of size ``n_p``.
218
+ """
219
+ npipes = len(self.r_out)
220
+
221
+ R_p = []
222
+ for i in range(len(z)):
223
+ rp = thermal_resistance_pipe(self.r_in, self.r_out, self.k_p)
224
+ if (self.insu_z > 0) and (z[i] < self.insu_z):
225
+ for ip in range(self.nInlets, npipes):
226
+ if self.insu_dr > 0:
227
+ rp[ip] = thermal_resistance_pipe_insulated(
228
+ self.r_in[ip],
229
+ self.r_out[ip],
230
+ self.insu_dr,
231
+ self.k_p[ip],
232
+ self.insu_k,
233
+ )
234
+ R_p.append(rp)
235
+
236
+ return R_p
237
+
238
+ def get_k_g(self, zstart: Sequence[float], zend: Sequence[float]) -> np.ndarray:
239
+ """
240
+ Interpolate grout conductivity for a list of vertical intervals.
241
+
242
+ Parameters
243
+ ----------
244
+ zstart : sequence of float
245
+ Lower bounds of intervals.
246
+ zend : sequence of float
247
+ Upper bounds of intervals.
248
+
249
+ Returns
250
+ -------
251
+ np.ndarray
252
+ Grout conductivity per interval.
253
+ """
254
+ k_g = self.interpolatorKg.interp(zstart, zend)
255
+
256
+ return k_g
257
+
258
+ def get_custom_pipe(self) -> CustomPipe:
259
+ """
260
+ Construct and return the appropriate pipe model instance (CoaxialPipe or CustomPipe).
261
+
262
+ Returns
263
+ -------
264
+ CoaxialPipe | CustomPipe
265
+ Constructed pipe representation compatible with the rest of the code.
266
+ """
267
+ # create pygfunction borehole
268
+ self.borehole = gt.boreholes.Borehole(self.H, self.D, self.r_b, x=0.0, y=0.0)
269
+
270
+ # compute grout thermal conductivity with depth
271
+ zz = np.linspace(self.D, self.D + self.H, self.ncalcsegments + 1)
272
+ k_g = self.get_k_g(zz[:-1], zz[1:])
273
+
274
+ # compute pipe resistances
275
+ R_p = self.get_r_p(self.zseg)
276
+
277
+ custom_pipe = None
278
+ if self.type == BoreholeDesign.COAXIAL:
279
+ custom_pipe = CoaxialPipe(
280
+ self.r_in,
281
+ self.r_out,
282
+ self.borehole,
283
+ k_g,
284
+ self.k_p,
285
+ J=self.J,
286
+ m_flow=self.m_flow,
287
+ fluid_str=self.fluid_str,
288
+ percent=self.fluid_percent,
289
+ epsilon=self.epsilon,
290
+ ncalcsegments=self.ncalcsegments,
291
+ R_p=R_p,
292
+ T_f=self.T_f,
293
+ )
294
+ elif self.type == BoreholeDesign.UTUBE:
295
+ custom_pipe = CustomPipe(
296
+ self.pos,
297
+ self.r_in,
298
+ self.r_out,
299
+ self.borehole,
300
+ k_g,
301
+ self.k_p,
302
+ J=self.J,
303
+ nInlets=self.nInlets,
304
+ m_flow=self.m_flow,
305
+ fluid_str=self.fluid_str,
306
+ percent=self.fluid_percent,
307
+ epsilon=self.epsilon,
308
+ ncalcsegments=self.ncalcsegments,
309
+ R_p=R_p,
310
+ )
311
+ else:
312
+ print(
313
+ "error in BoreholeDesign:getCustomPipe type of boreholedesign not recognized ",
314
+ self.type,
315
+ )
316
+
317
+ return custom_pipe
318
+
319
+ @classmethod
320
+ def from_config(cls, config: SingleRunConfig) -> "BoreholeDesign":
321
+ """
322
+ Create a BoreholeDesign instance from a configuration object.
323
+
324
+ The configuration object should contain keys consistent with the
325
+ `BoreholeDesign` constructor arguments.
326
+
327
+ Parameters
328
+ ----------
329
+ config : SingleRunConfig
330
+ Object containing borehole design parameters.
331
+
332
+ Returns
333
+ -------
334
+ BoreholeDesign
335
+ Configured instance.
336
+ """
337
+ if config.r_in is None:
338
+ # Example: derive inner radius from r_out using SDR
339
+ r_out = np.asarray(config.r_out)
340
+ if config.SDR:
341
+ config.r_in = r_out - ((2 * r_out) / config.SDR)
342
+ else:
343
+ raise ValueError("r_in missing and no SDR provided.")
344
+
345
+ return cls(
346
+ type=config.type,
347
+ H=config.H,
348
+ D=config.D,
349
+ r_b=config.r_b,
350
+ r_in=config.r_in,
351
+ r_out=config.r_out,
352
+ k_p=config.k_p,
353
+ k_g=config.k_g,
354
+ pos=config.pos,
355
+ nInlets=config.nInlets,
356
+ m_flow=config.m_flow,
357
+ T_f=config.Tin,
358
+ fluid_str=config.fluid_str,
359
+ fluid_percent=config.fluid_percent,
360
+ epsilon=config.epsilon,
361
+ z_k_g=config.z_k_g,
362
+ ncalcsegments=config.nsegments,
363
+ insu_z=config.insu_z,
364
+ insu_dr=config.insu_dr,
365
+ insu_k=config.insu_k,
366
+ N=config.field_N,
367
+ M=config.field_M,
368
+ R=config.field_R,
369
+ inclination_start=config.field_inclination_start,
370
+ inclination_end=config.field_inclination_end,
371
+ num_tiltedsegments=config.field_segments,
372
+ )
373
+
374
+ def visualize_pipes(self, filename: str | Path) -> None:
375
+ """
376
+ Vsualize the borehole design, including pipe layout and create a figure.
377
+
378
+ Parameters
379
+ ----------
380
+ filename : str
381
+ Output path for the saved figure.
382
+
383
+ Returns
384
+ -------
385
+ None
386
+
387
+ """
388
+ fig = self.customPipe.visualize_pipes()
389
+ fig.savefig(filename)
390
+
391
+ def fluid_friction_factor(
392
+ self,
393
+ ipipe: int,
394
+ m_flow_pipe: float,
395
+ mu_f: float,
396
+ rho_f: float,
397
+ epsilon: float,
398
+ tol: float = 1e-6,
399
+ ) -> tuple[float, float, float]:
400
+ """
401
+ Evaluate the Darcy-Weisbach friction factor. It calculates the hydraulic diameter D, and the cross_section flow
402
+ area, depending on the pipe configuration.
403
+
404
+ Parameters
405
+ ----------
406
+ m_flow_pipe : float
407
+ Fluid mass flow rate (in kg/s) into the pipe.
408
+ mu_f : float
409
+ Fluid dynamic viscosity (in kg/m-s).
410
+ rho_f : float
411
+ Fluid density (in kg/m3).
412
+ epsilon : float
413
+ Pipe roughness (in meters).
414
+ tol : float
415
+ Relative convergence tolerance on Darcy friction factor.
416
+ Default is 1.0e-6.
417
+
418
+ Returns
419
+ -------
420
+ fDarcy : float
421
+ Darcy friction factor.
422
+ dpdl: float
423
+ Pressure loss
424
+ Re : float
425
+ Reynolds number.
426
+ """
427
+ if (self.type == BoreholeDesign.UTUBE) or (ipipe == 1):
428
+ # Hydraulic diameter
429
+ D_h = 2.0 * self.r_in[ipipe]
430
+ A_cs = np.pi * self.r_in[ipipe] ** 2
431
+ else:
432
+ # hydraulic parameters for coaxial
433
+ A_cs = np.pi * (self.r_in[0] ** 2 - self.r_out[1] ** 2)
434
+ D_h = 2 * (self.r_in[0] - self.r_out[1])
435
+
436
+ # Relative roughness
437
+ E = epsilon / D_h
438
+ # Fluid velocity
439
+ V_flow = m_flow_pipe / rho_f
440
+ V = V_flow / A_cs
441
+ # Reynolds number
442
+ Re = rho_f * V * D_h / mu_f
443
+
444
+ if Re == 0:
445
+ fDarcy = np.nan
446
+ elif Re < 2.3e3:
447
+ # Darcy friction factor for laminar flow
448
+ fDarcy = 64.0 / Re
449
+ else:
450
+ # Colebrook-White equation for rough pipes
451
+ fDarcy = 0.02
452
+ df = 1.0e99
453
+ while abs(df / fDarcy) > tol:
454
+ one_over_sqrt_f = -2.0 * np.log10(
455
+ E / 3.7 + 2.51 / (Re * np.sqrt(fDarcy))
456
+ )
457
+ fDarcy_new = 1.0 / one_over_sqrt_f**2
458
+ df = fDarcy_new - fDarcy
459
+ fDarcy = fDarcy_new
460
+
461
+ dpdl = fDarcy * 0.5 * rho_f * V * V / D_h
462
+
463
+ return fDarcy, dpdl, Re
464
+
465
+ def dploop_nosyphon(
466
+ self, tempfluid: np.ndarray, flowrate: np.ndarray, effpump: float
467
+ ) -> tuple[np.ndarray, np.ndarray]:
468
+ """
469
+ Compute loop pressure drop (no syphon) and pumping power.
470
+
471
+ Parameters
472
+ ----------
473
+ tempfluid : ndarray
474
+ Fluid temperature time series (°C).
475
+ flowrate : ndarray
476
+ Mass flow rate time series (kg/s).
477
+ effpump : float
478
+ Pump efficiency (0–1).
479
+
480
+ Returns
481
+ -------
482
+ dpsumtime : ndarray
483
+ Pressure drop time series (bar).
484
+ qpump : ndarray
485
+ Power [W] required to drive the pumping
486
+ """
487
+ qpump = flowrate * 0
488
+ dpsumtime = flowrate * 0
489
+ f = Fluid(self.fluid_str, self.fluid_percent, T=tempfluid)
490
+
491
+ ipipes = [0, self.n_p - 1]
492
+ npipes = [self.nInlets, self.n_p - self.nInlets]
493
+
494
+ Re_in_out = np.ones((len(ipipes), len(flowrate)))
495
+
496
+ for itime in range(len(flowrate)):
497
+ dpsum = 0
498
+
499
+ for ip in range(len(ipipes)):
500
+ ipipe = ipipes[ip] # Index of the current pipe
501
+ mflow = flowrate / npipes[ip] # Distribute flowrate across pipes
502
+
503
+ # Get dynamic viscosity and density for the fluid at the current temperature
504
+ mu_f = f.dynamic_viscosity()
505
+ rho_f = f.density()
506
+
507
+ # Calculate friction factor, pressure drop, and Reynolds number
508
+ fDarcy, dpdl, Re = self.fluid_friction_factor(
509
+ ipipe, mflow[itime], mu_f, rho_f, self.epsilon
510
+ )
511
+
512
+ # Sum the pressure drop over the borehole height
513
+ dpsum += dpdl * self.borehole.H
514
+
515
+ # Store the Reynolds nr. for the different pipes over time in one array
516
+ Re_in_out[ip, itime] = Re
517
+
518
+ # Calculate the required pumping power for this time step
519
+ qpump[itime] = dpsum * (flowrate[itime] / rho_f) / effpump
520
+
521
+ # Store the pressure drop at this time step
522
+ dpsumtime[itime] = dpsum
523
+
524
+ # store the reynolds nr. over time for the inlet and outlet pipes separately
525
+ self.Re_in = Re_in_out[0]
526
+ self.Re_out = Re_in_out[1]
527
+
528
+ # Return the total pressure drop (converted to bar) and pumping power (W)
529
+ return dpsumtime / 1e5, qpump
530
+
531
+ def findflowrate_dploop(
532
+ self,
533
+ dplooptarget: float,
534
+ tempfluid: np.ndarray,
535
+ flowrate: float,
536
+ effpump: float,
537
+ ) -> float:
538
+ """
539
+ Calculate flowrate matching with dplooptarget based on root finding algorithm
540
+ (brentq is used with a scaling of the given flowrate, in range 0.01-10).
541
+
542
+ Parameters
543
+ ----------
544
+ dplooptarget : float
545
+ Target pumping pressure (bar).
546
+ tempfluid : ndarray
547
+ Fluid temperatures (°C).
548
+ flowrate : float
549
+ Initial flow rate (kg/s) used as scale reference.
550
+ effpump : float
551
+ Pump efficiency.
552
+
553
+ Returns
554
+ -------
555
+ float
556
+ Mass flow rate (kg/s) that results in the desired pumping pressure.
557
+ """
558
+
559
+ sol = optimize.root_scalar(
560
+ self.root_func_dploop,
561
+ args=(dplooptarget, tempfluid, flowrate, effpump),
562
+ bracket=[0.01, 100],
563
+ method="brentq",
564
+ )
565
+ return sol.root
566
+
567
+ def root_func_dploop(
568
+ self,
569
+ mflowscale: float,
570
+ dplooptarget: float,
571
+ tempfluid: np.ndarray,
572
+ flowrate: float,
573
+ effpump: float,
574
+ ) -> float:
575
+ """
576
+ Objective function used by ``findflowrate_dploop`` for root finding function.
577
+
578
+ Parameters
579
+ ----------
580
+ mflowscale : float
581
+ Scaling factor applied to nominal flow rate.
582
+ dplooptarget : float
583
+ Target pumping pressure (bar).
584
+ tempfluid : ndarray
585
+ Fluid temperatures (°C).
586
+ flowrate : float
587
+ Nominal mass flow rate (kg/s).
588
+ effpump : float
589
+ Pump efficiency.
590
+
591
+ Returns
592
+ -------
593
+ float
594
+ Difference between actual and target pumping pressure.
595
+ """
596
+ dploop, _ = self.dploop_nosyphon(tempfluid, flowrate * mflowscale, effpump)
597
+
598
+ return max(dploop) - dplooptarget
599
+
600
+ def calculate_dploop(
601
+ self, T_f: np.ndarray, z: np.ndarray, flowrate: np.ndarray, effpump: float
602
+ ) -> tuple[np.ndarray, np.ndarray]:
603
+ """
604
+ Pumping pressure, pumping power and the Reynolds number are calculated for the in- and outlets in the loop. Assumption is that the flow and temperature are
605
+ divided equally and symmetrically over the pipes. (i.e. the pipe diameters of respectively the inlet and outlet
606
+ pipes is the same and the in- and oulet pipes alternate in the pipe design).
607
+ Re_in, Re_uit are stored over time (Re_in represents the Reynolds number that is the same in all inlets, Re_out represents the Reynolds number that represents all outlets)
608
+
609
+ Parameters
610
+ ----------
611
+ T_f : ndarray
612
+ Fluid temperature profile [ntime, nz, npipes] (°C).
613
+ z : ndarray
614
+ Along hole depth discretization (m).
615
+ flowrate : ndarray
616
+ Mass flow rate time series (kg/s).
617
+ effpump : float
618
+ Pump efficiency.
619
+
620
+ Returns
621
+ -------
622
+ dpsumtime : ndarray
623
+ Pumping pressure (bar) for each time.
624
+ qpump : ndarray
625
+ Pumping power (W) for each time.
626
+ """
627
+ nz = len(z)
628
+ qpump = flowrate * 0
629
+ dpsumtime = flowrate * 0
630
+
631
+ ipipes = [0, self.n_p - 1]
632
+ npipes = [self.nInlets, self.n_p - self.nInlets]
633
+ rhosum = [0, 0]
634
+
635
+ Re_meanz = np.ones(
636
+ (len(ipipes), len(flowrate))
637
+ ) # Store average Re for each time step
638
+
639
+ for itime in range(len(flowrate)):
640
+ dpsum = 0
641
+
642
+ for ip in range(len(ipipes)):
643
+ ipipe = ipipes[ip]
644
+ mflow = flowrate / (npipes[ip])
645
+ tz = T_f[itime, :, ipipe].reshape(nz)
646
+ Resum = 0
647
+ rhosum[ip] = 0
648
+
649
+ for k in range(nz):
650
+ try:
651
+ f = Fluid(self.fluid_str, self.fluid_percent, T=tz[k])
652
+ except:
653
+ print("Error in def dploop")
654
+ return
655
+
656
+ mu_f = f.dynamic_viscosity()
657
+ rho_f = f.density()
658
+ fDarcy, dpdl, Re = self.fluid_friction_factor(
659
+ ipipe, mflow[itime], mu_f, rho_f, self.epsilon
660
+ )
661
+ Resum += Re
662
+
663
+ if k > 0:
664
+ dpsum += 0.5 * dpdl * (z[k] - z[k - 1])
665
+ rhosum[ip] += rho_f * (z[k] - z[k - 1])
666
+ if k < len(tz) - 1:
667
+ dpsum += 0.5 * dpdl * (z[k + 1] - z[k])
668
+ rhosum[ip] += rho_f * (z[k + 1] - z[k])
669
+
670
+ # Store the depth-average reynold nr over depth for the different pipes over time in one array
671
+ Re_meanz[ip, itime] = Resum / nz
672
+
673
+ dprho = (rhosum[1] - rhosum[0]) * 9.81
674
+
675
+ dpsum += dprho
676
+ qpump[itime] = dpsum * (flowrate[itime] / rho_f) / effpump
677
+ dpsumtime[itime] = dpsum
678
+
679
+ # Store Re values for in- and outlet pipes separately
680
+ self.Re_in = Re_meanz[0]
681
+ self.Re_out = Re_meanz[1]
682
+
683
+ return dpsumtime / 1e5, qpump # Return pressure in bar