geoloop 0.0.1__py3-none-any.whl → 1.0.0__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 +516 -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 +695 -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 +1142 -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.0.dist-info/METADATA +120 -0
  41. geoloop-1.0.0.dist-info/RECORD +46 -0
  42. geoloop-1.0.0.dist-info/entry_points.txt +2 -0
  43. geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0.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.0.dist-info}/WHEEL +0 -0
  47. {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,727 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ import pygfunction as gt
4
+ from matplotlib.ticker import AutoMinorLocator
5
+
6
+
7
+ def thermal_resistance_pipe(r_in: float, r_out: float, k_p: float) -> float:
8
+ """
9
+ Compute the conductive thermal resistance of a cylindrical pipe wall.
10
+
11
+ Parameters
12
+ ----------
13
+ r_in : float
14
+ Inner radius of the pipe (m).
15
+ r_out : float
16
+ Outer radius of the pipe (m).
17
+ k_p : float
18
+ Thermal conductivity of the pipe material (W/m·K).
19
+
20
+ Returns
21
+ -------
22
+ float
23
+ Thermal resistance of the pipe wall (m·K/W).
24
+ """
25
+ R_p = np.log(r_out / r_in) / (2 * np.pi * k_p)
26
+ return R_p
27
+
28
+
29
+ def thermal_resistance_pipe_insulated(
30
+ r_in: float, r_out: float, insu_dr: float, k_p: float, insu_k: float
31
+ ) -> float:
32
+ """
33
+ Compute the conductive thermal resistance of a pipe with an insulated
34
+ middle section of its wall thickness.
35
+
36
+ The wall is divided into:
37
+ - inner pipe material
38
+ - insulated section
39
+ - outer pipe material
40
+
41
+ Parameters
42
+ ----------
43
+ r_in : float
44
+ Inner radius of the pipe (m).
45
+ r_out : float
46
+ Outer radius of the pipe (m).
47
+ insu_dr : float
48
+ Fraction of the pipe wall thickness that is insulated (0–1).
49
+ k_p : float
50
+ Thermal conductivity of the pipe material (W/m·K).
51
+ insu_k : float
52
+ Thermal conductivity of the insulation (W/m·K).
53
+
54
+ Returns
55
+ -------
56
+ float
57
+ Total radial thermal resistance (m·K/W).
58
+ """
59
+ # Total wall thickness
60
+ wall_thickness = r_out - r_in
61
+ iso_thickness = wall_thickness * insu_dr
62
+
63
+ # Locate insulation symmetrically in the wall
64
+ r_iso_in = r_in + 0.5 * (wall_thickness - iso_thickness)
65
+ r_iso_out = r_iso_in + iso_thickness
66
+
67
+ # Compute resistances for each region
68
+ R_inner = thermal_resistance_pipe(r_in, r_iso_in, k_p)
69
+ R_iso = thermal_resistance_pipe(r_iso_in, r_iso_out, insu_k)
70
+ R_outer = thermal_resistance_pipe(r_iso_out, r_out, k_p)
71
+
72
+ R_p = R_inner + R_iso + R_outer
73
+
74
+ return R_p
75
+
76
+
77
+ class CustomPipe(gt.pipes._BasePipe):
78
+ """
79
+ Pipe model with depth-dependent ambient temperatures.
80
+
81
+ Supports U-tubes and multi-U-tubes with N inlet pipes and M outlet pipes.
82
+ Uses pygfunction (Cimmino & Cook, 2022) for thermal resistance networks.
83
+
84
+ Internal resistances are based on the multipole method of
85
+ Claesson & Hellström (2011). Fluid properties are obtained via pygfunction.
86
+
87
+ Contains information regarding the physical dimensions and thermal
88
+ characteristics of the pipes and the grout material, as well as methods to
89
+ evaluate fluid temperatures and heat extraction rates based on the work of
90
+ Hellstrom [#Single-Hellstrom1991]_. Internal borehole thermal resistances
91
+ are evaluated using the multipole method of Claesson and Hellstrom
92
+ [#Single-Claesson2011b]_.
93
+
94
+ The effective borehole thermal resistance is evaluated using the method
95
+ of Cimmino [#Single-Cimmin2019]_. This is valid for any number of pipes.
96
+
97
+ References
98
+ ----------
99
+ .. [#Cimmino2022] Cimmino, M., & Cook, J.C. (2022). pygfunction 2.2: New features and improvements in accuracy and computational efficiency.
100
+ In Research Conference Proceedings, IGSHPA Annual Conference 2022 (pp. 45-52).
101
+ International Ground Source Heat Pump Association. DOI: https://doi.org/10.22488/okstate.22.00001
102
+ .. [#Single-Hellstrom1991] Hellstrom, G. (1991). Ground heat storage.
103
+ Thermal Analyses of Duct Storage Systems I: Theory. PhD Thesis.
104
+ University of Lund, Department of Mathematical Physics. Lund, Sweden.
105
+ .. [#Single-Claesson2011b] Claesson, J., & Hellstrom, G. (2011).
106
+ Multipole method to calculate borehole thermal resistances in a borehole
107
+ heat exchanger. HVAC&R Research, 17(6), 895-911.
108
+ .. [#Single-Cimmin2019] Cimmino, M. (2019). Semi-analytical method for
109
+ g-function calculation of bore fields with series- and
110
+ parallel-connected boreholes. Science and Technology for the Built
111
+ Environment, 25 (8), 1007-1022.
112
+
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ pos,
118
+ r_in,
119
+ r_out,
120
+ borehole,
121
+ k_g,
122
+ k_p,
123
+ J=3,
124
+ nInlets=1,
125
+ m_flow=1.0,
126
+ T_f=10,
127
+ fluid_str="Water",
128
+ percent=100,
129
+ epsilon=1e-6,
130
+ ncalcsegments=1,
131
+ R_p=[],
132
+ ):
133
+ """
134
+ Initialize a custom borehole pipe model and compute its thermal and
135
+ hydraulic properties.
136
+
137
+ Parameters
138
+ ----------
139
+ pos : list of (float, float)
140
+ Pipe coordinates inside the borehole.
141
+ r_in : float or array_like
142
+ Inner radius of the pipes (m).
143
+ r_out : float or array_like
144
+ Outer radius of the pipes (m).
145
+ borehole : gt.boreholes.Borehole
146
+ Borehole geometry object.
147
+ k_g : float
148
+ Grout thermal conductivity (W/m·K).
149
+ k_p : float
150
+ Pipe thermal conductivity (W/m·K).
151
+ J : int, optional
152
+ Number of multipoles per pipe. Default is 3.
153
+ nInlets : int, optional
154
+ Number of inlet pipes. Default is 1.
155
+ m_flow : float, optional
156
+ Mass flow rate (kg/s). Default is 1.0.
157
+ T_f : float, optional
158
+ Inlet fluid temperature (°C). Default is 10.
159
+ fluid_str : str, optional
160
+ Fluid type. Default is "Water".
161
+ percent : float, optional
162
+ Fluid mixture percentage. Default is 100.
163
+ epsilon : float, optional
164
+ Pipe roughness. Default is 1e-6.
165
+ ncalcsegments : int, optional
166
+ Number of segments for thermal resistance evaluation.
167
+ R_p : list or array, optional
168
+ Precomputed pipe thermal resistances.
169
+
170
+ Attributes
171
+ ----------
172
+ R_p : list of float
173
+ Pipe thermal resistances (m·K/W).
174
+ h_f : ndarray
175
+ Convective heat transfer coefficient per pipe.
176
+ Rd : ndarray
177
+ Δ-circuit thermal resistance per segment.
178
+ R : ndarray
179
+ Thermal resistance matrix.
180
+ R1 : ndarray
181
+ Inverse thermal resistance matrix.
182
+ m_flow_pipe : ndarray
183
+ Mass flow per pipe.
184
+
185
+ Notes
186
+ -----
187
+ The expected array shapes of input parameters and outputs are documented
188
+ for each class method. `nInlets` and `nOutlets` are the number of inlets
189
+ and outlets to the borehole, and both are equal to 1 for a single U-tube
190
+ borehole. `nSegments` is the number of discretized segments along the
191
+ borehole. `nPipes` is the number of pipes (i.e. the number of U-tubes) in
192
+ the borehole, equal to 1. `nDepths` is the number of depths at which
193
+ temperatures are evaluated.
194
+ """
195
+ self.pos = pos
196
+ self.nPipes = len(pos)
197
+
198
+ # convert to arrays if needed
199
+ if np.isscalar(r_in):
200
+ r_in = r_in * np.ones(self.nPipes)
201
+ self.r_in = r_in
202
+ if np.isscalar(r_out):
203
+ r_out = r_out * np.ones(self.nPipes)
204
+
205
+ self.r_out = r_out
206
+ self.b = borehole
207
+ self.k_s = 1.0
208
+ self.k_g = k_g
209
+ self.k_p = k_p
210
+
211
+ self.J = J
212
+ self.nInlets = nInlets
213
+ self.nOutlets = self.nPipes - self.nInlets
214
+ self.ncalcsegments = ncalcsegments
215
+
216
+ # Pipe thermal resistances
217
+ if len(R_p) == 0:
218
+ rp = thermal_resistance_pipe(r_in, r_out, k_p)
219
+ self.R_p = []
220
+ for i in range(ncalcsegments):
221
+ self.R_p.append(rp)
222
+ else:
223
+ self.R_p = R_p
224
+
225
+ # Initialize flow rate and fluid properties including fluid resistisity with pipes
226
+ self.m_flow = m_flow
227
+ self.m_flow_pipe = m_flow * np.ones(self.nPipes)
228
+ self.m_flow_pipe[: self.nInlets] = m_flow / self.nInlets
229
+ self.m_flow_pipe[self.nInlets :] = -m_flow / self.nOutlets
230
+
231
+ # fluid
232
+ fluid = gt.media.Fluid(fluid_str, percent, T=T_f)
233
+
234
+ self.cp_f = fluid.cp # Fluid specific isobaric heat capacity (J/kg.K)
235
+ self.rho_f = fluid.rho # Fluid density (kg/m3)
236
+ self.mu_f = fluid.mu # Fluid dynamic viscosity (kg/m.s)
237
+ self.k_f = fluid.k # Fluid thermal conductivity (W/m.K)
238
+
239
+ self.epsilon = epsilon
240
+
241
+ self.h_f = np.zeros(self.nPipes)
242
+ self.Rd = []
243
+
244
+ # initalise flow scaling
245
+ self.update_scaleflow(1.0)
246
+
247
+ return
248
+
249
+ @property
250
+ def k_g(self) -> float:
251
+ return self._k_g
252
+
253
+ @k_g.setter
254
+ def k_g(self, value: float) -> None:
255
+ self._k_g = value
256
+
257
+ @property
258
+ def R(self) -> np.ndarray:
259
+ return self._R
260
+
261
+ @R.setter
262
+ def R(self, value: np.ndarray) -> None:
263
+ self._R = value
264
+
265
+ @property
266
+ def Rd(self) -> np.ndarray:
267
+ return self._Rd
268
+
269
+ @Rd.setter
270
+ def Rd(self, value: np.ndarray) -> None:
271
+ self._Rd = value
272
+
273
+ @property
274
+ def ncalcsegments(self) -> int:
275
+ return self._ncalcsegments
276
+
277
+ @ncalcsegments.setter
278
+ def ncalcsegments(self, value: int) -> None:
279
+ self._ncalcsegments = value
280
+
281
+ def create_multi_u_tube(self):
282
+ """
283
+ Build a standard pygfunction U-tube / multi-U-tube object
284
+ using depth-independent pipe properties.
285
+
286
+ Returns
287
+ -------
288
+ SingleUTube or MultipleUTube
289
+ pygfunction pipe object.
290
+ """
291
+ pos = self.pos
292
+ rp_in = self.r_in
293
+ rp_out = self.r_out
294
+ borehole = self.b
295
+
296
+ k_s = np.average(self.k_s)
297
+ k_g = np.average(self.k_g)
298
+ h_f = self.h_f[0]
299
+
300
+ R_f_ser = 1.0 / (h_f * 2 * np.pi * rp_in)
301
+ R_p = self.R_p
302
+ # uniform pipe and fluid resisitivty
303
+ Rfp = R_f_ser[0] + R_p[0][0]
304
+
305
+ if len(rp_in) == 2:
306
+ single_u_tube = gt.pipes.SingleUTube(
307
+ pos, rp_in[0], rp_out[0], borehole, k_s, k_g, Rfp
308
+ )
309
+ single_u_tube.h_f = h_f
310
+ return single_u_tube
311
+
312
+ else:
313
+ utube = gt.pipes.MultipleUTube(
314
+ pos,
315
+ rp_in[0],
316
+ rp_out[0],
317
+ borehole,
318
+ k_s,
319
+ k_g,
320
+ Rfp,
321
+ nPipes=self.nInlets,
322
+ config="parallel",
323
+ )
324
+ utube.h_f = h_f
325
+ return utube
326
+
327
+ def update_scaleflow(self, scaleflow: float = 1.0) -> None:
328
+ """
329
+ Update the flow scaling factor and recalculate convective and thermal
330
+ resistances.
331
+
332
+ Parameters
333
+ ----------
334
+ scaleflow : float, optional
335
+ Scaling multiplier applied to the mass flow rate. Default is 1.0.
336
+
337
+ Notes
338
+ -----
339
+ Assumes that `k_g` and `k_s` are arrays of length `ncalcsegments`,
340
+ allowing depth-dependent thermal properties.
341
+ """
342
+ self.scaleflow = scaleflow
343
+
344
+ hfnew = np.ones(self.nPipes)
345
+ for i in range(self.nPipes):
346
+ hfnew[i] = gt.pipes.convective_heat_transfer_coefficient_circular_pipe(
347
+ abs(self.m_flow_pipe[i] * self.scaleflow),
348
+ self.r_in[i],
349
+ self.mu_f,
350
+ self.rho_f,
351
+ self.k_f,
352
+ self.cp_f,
353
+ self.epsilon,
354
+ )
355
+
356
+ hfdif = np.subtract(hfnew, self.h_f)
357
+ hfdot = np.dot(hfdif, hfdif)
358
+
359
+ # Skip update if small change and sizes match
360
+ if (self.ncalcsegments == len(self.Rd)) and (hfdot < 1):
361
+ return
362
+ else:
363
+ self.h_f = hfnew
364
+
365
+ self.R_f = 1.0 / (self.h_f * 2 * np.pi * self.r_in)
366
+
367
+ # Delta-circuit thermal resistances
368
+ self.update_thermal_resistances()
369
+
370
+ def init_thermal_resistances(self, k_g: np.ndarray, R_p: list[np.ndarray]) -> None:
371
+ """
372
+ Initialize depth-dependent thermal resistances using provided
373
+ conductivity and pipe resistance arrays.
374
+
375
+ This routine is called from the B2G.runsimulation method, in order to generate thermal resistances based
376
+ on actual segments determined by len(k_g) and len(R_p)
377
+
378
+ Parameters
379
+ ----------
380
+ k_g : array_like
381
+ Grout thermal conductivity for each depth segment.
382
+ R_p : list of array_like
383
+ Pipe thermal resistance values for each segment.
384
+ """
385
+ self.ncalcsegments = len(k_g)
386
+ self.k_g = k_g
387
+ self.R_p = R_p
388
+
389
+ self.update_scaleflow(1.0)
390
+
391
+ # Precompute inverses of resistance matrices
392
+ self.R1 = []
393
+ for i in range(self.ncalcsegments):
394
+ R1 = np.linalg.inv(self.R[i])
395
+ self.R1.append(R1)
396
+
397
+ def update_thermal_resistances(self, initialize_stored_coeff: bool = False) -> None:
398
+ """
399
+ Update the delta-circuit of thermal resistances.
400
+
401
+ This methods updates the values of the delta-circuit thermal
402
+ resistances based on the provided fluid to outer pipe wall thermal
403
+ resistance.
404
+
405
+ Its dimension corresponds to ncalcsegments (which is dictated by b2g.py (nx nodes)
406
+ or b2g_ana (nsegments) and takes into account depth dependent effects of insulation
407
+
408
+ Parameters
409
+ ----------
410
+ initialize_stored_coeff : bool, optional
411
+ If True, also reinitialize stored coefficients. Default is False.
412
+ """
413
+ self.R = []
414
+ self.Rd = []
415
+
416
+ for k in range(self.ncalcsegments):
417
+ R_fp = self.R_f + self.R_p[k]
418
+
419
+ # Delta-circuit thermal resistances
420
+ (R, Rd) = gt.pipes.thermal_resistances(
421
+ self.pos, self.r_out, self.b.r_b, self.k_s, self.k_g[k], R_fp, J=self.J
422
+ )
423
+
424
+ self.R.append(R)
425
+ self.Rd.append(Rd)
426
+
427
+ if initialize_stored_coeff:
428
+ self._initialize_stored_coefficients()
429
+ return
430
+
431
+ def get_Rs(self, nyear: float, alpha: float = 1e-6) -> float:
432
+ """
433
+ Approximate long-term borehole resistance Rs using an analytical estimate.
434
+ Calculate Rs for simple approximation for Tb-T0 = Rs qc
435
+
436
+ Parameters
437
+ ----------
438
+ nyear : float
439
+ Duration (years) used to compute the effective thermal resistance.
440
+ alpha : float, optional
441
+ Soil thermal diffusivity (m²/s). Default is 1e-6.
442
+
443
+ Returns
444
+ -------
445
+ float
446
+ Approximate long-term borehole resistance Rs (m·K/W).
447
+ """
448
+ r_b = self.b.r_b
449
+ ts = nyear * 3600 * 24 * 365.25
450
+
451
+ Rs = (np.log((4 * alpha * ts) / (r_b**2)) - np.euler_gamma) / (
452
+ 4 * np.pi * self.k_s
453
+ )
454
+ return Rs
455
+
456
+ def get_temperature_depthvar(
457
+ self,
458
+ T_f_in: float,
459
+ signpower: float | np.ndarray,
460
+ Rs: np.ndarray,
461
+ soil_props,
462
+ nsegments: int = 10,
463
+ ) -> tuple:
464
+ """
465
+ Compute fully depth-dependent inlet/outlet fluid temperatures, borehole
466
+ wall temperatures, pipe temperatures, and heat extraction along the borehole.
467
+
468
+ Parameters
469
+ ----------
470
+ T_f_in : float
471
+ Inlet fluid temperature (°C).
472
+ signpower : float
473
+ Sign of the thermal load direction (±1).
474
+ Rs : array_like
475
+ Long-term borehole resistance values for each segment, based on approximation for Tb-T0 = Rs qc.
476
+ soil_props : SoilProperties
477
+ Soil properties object.
478
+ nsegments : int, optional
479
+ Number of depth segments. Default is 10.
480
+
481
+ Returns
482
+ -------
483
+ tuple
484
+ (T_f_out, power, Reff, pipe_temps, borehole_temps, segment_heat_flows)
485
+ """
486
+ bh = self.b
487
+
488
+ hstep = bh.H / nsegments
489
+ zmin = bh.D + 0.5 * hstep
490
+ zmax = bh.D + bh.H - 0.5 * hstep
491
+ zseg = np.linspace(zmin, zmax, nsegments)
492
+
493
+ Tg_borehole = soil_props.getTg(zseg)
494
+ qbz = Tg_borehole * 0.0
495
+
496
+ R1 = np.copy(self._R)
497
+ b = np.arange(nsegments, dtype=float)
498
+
499
+ for i in range(nsegments):
500
+ R1[i] = np.linalg.inv(self._R[i])
501
+ b[i] = np.sum(R1[i] @ np.ones(self.nPipes))
502
+
503
+ # storage arrays
504
+ ptemp = np.arange((nsegments + 1) * self.nPipes, dtype=float).reshape(
505
+ nsegments + 1, self.nPipes
506
+ )
507
+ Tb = np.arange(nsegments, dtype=float)
508
+ Tf = Tb * 0.0
509
+
510
+ # initialize the top temperatures (inlet)
511
+ ptemp[0, : self.nInlets] = T_f_in
512
+
513
+ # iterate for T_f_out such that bottom temperatures become the same
514
+ dtfout = 0
515
+ dtempold = 0
516
+ dtempnew = 1
517
+ T_f_out = T_f_in + 1e-1 * signpower
518
+ iterate = True
519
+
520
+ while abs(dtempnew) > 1e-4 or iterate:
521
+ # set outlet boundary condition guess
522
+ ptemp[0, self.nInlets :] = T_f_out
523
+
524
+ # loop over each segment
525
+ for i in range(nsegments):
526
+ # Tf is only for output purposes
527
+ Tf[i] = np.average(ptemp[i])
528
+
529
+ # prep next segment's temperatures
530
+ ptemp[i + 1] = ptemp[i]
531
+
532
+ # iteration variables within the segment
533
+ dtemp = 1
534
+ tseg = 0.5 * (ptemp[i] + ptemp[i + 1])
535
+
536
+ # check that Rs[i]*b is not close to 1 if so modify with small number
537
+ if Rs[i] < 0:
538
+ Rs[i] = abs(Rs[i])
539
+
540
+ # iterate until pipe temperatures converge
541
+ while np.dot(dtemp, dtemp) > 1e-10:
542
+ tsegold = tseg
543
+
544
+ a = np.sum(R1[i] @ tseg)
545
+ Tb[i] = (Rs[i] * a + Tg_borehole[i]) / (1 + Rs[i] * b[i])
546
+
547
+ q = R1[i] @ (tseg - Tb[i])
548
+
549
+ ptemp[i + 1] = ptemp[i] - q * hstep / (
550
+ self.m_flow_pipe * self.scaleflow * self.cp_f
551
+ )
552
+
553
+ tseg = 0.5 * (ptemp[i] + ptemp[i + 1])
554
+ dtemp = tseg - tsegold
555
+
556
+ qbz[i] = -np.sum(q) * hstep
557
+
558
+ dtempnew = np.sum(
559
+ ptemp[nsegments, self.nInlets :] * self.m_flow_pipe[self.nInlets :]
560
+ ) + np.sum(
561
+ ptemp[nsegments, : self.nInlets] * self.m_flow_pipe[: self.nInlets]
562
+ )
563
+
564
+ # Update outlet temperature guess
565
+ if abs(dtfout) > 0:
566
+ g = dtfout / (dtempnew - dtempold)
567
+ dtfout = -dtempnew * g
568
+ iterate = False
569
+ else:
570
+ dtfout = 1e-1
571
+ iterate = True
572
+
573
+ dtempold = dtempnew
574
+ T_f_out += dtfout
575
+
576
+ # Final power and effective resistance
577
+ power = (T_f_out - T_f_in) * self.m_flow * self.scaleflow * self.cp_f
578
+
579
+ if abs(np.sum(qbz) - power) > 1:
580
+ print("power is not sumq (sumq, power):", np.sum(qbz), ",", power)
581
+
582
+ if abs(power) > 1e-3:
583
+ Reff = -np.average(Tf - Tb) * bh.H / power
584
+ else:
585
+ Reff = -1
586
+
587
+ return T_f_out, power, Reff, ptemp, Tb, qbz
588
+
589
+ def get_temperature_depthvar_power(
590
+ self, power: float | np.ndarray, Rs: np.ndarray, soil_props, nsegments: int = 10
591
+ ) -> tuple:
592
+ """
593
+ Compute inlet and outlet temperatures to satisfy a prescribed power extraction.
594
+
595
+ Parameters
596
+ ----------
597
+ power : float
598
+ Target borehole heat extraction rate (W).
599
+ Rs : array_like
600
+ Long-term borehole resistance per segment, based on approximation for Tb-T0 = Rs qc.
601
+ soil_props : SoilProperties
602
+ Soil properties object.
603
+ nsegments : int, optional
604
+ Number of depth segments. Default is 10.
605
+
606
+ Returns
607
+ -------
608
+ tuple
609
+ (T_f_out, T_f_in, Reff, pipe_temps, Tb, qbz)
610
+ """
611
+ bh = self.b
612
+
613
+ # Initial guess for inlet temperature (assume same as soil at borehole top)
614
+ T_f_in = soil_props.getTg(bh.D)
615
+
616
+ # Initialize iteration variables
617
+ dpowerold = 0
618
+ dpowernew = 1
619
+ dtfin = 0
620
+
621
+ # Iteratively adjust inlet temperature to meet target power
622
+ iterate = True
623
+ while abs(dpowernew) > 1e-3 or iterate:
624
+ (T_f_out, newpower, Reff, ptemp, Tb, qbz) = self.get_temperature_depthvar(
625
+ T_f_in, np.sign(power), Rs, soil_props, nsegments=nsegments
626
+ )
627
+
628
+ # Compute difference between computed and target power
629
+ dpowernew = newpower - power
630
+
631
+ if abs(dtfin) > 0:
632
+ # Use previous iteration to accelerate convergence (gain factor)
633
+ gain = dtfin / (dpowernew - dpowerold)
634
+ dtfin = -dpowernew * gain
635
+ iterate = False
636
+ else:
637
+ # If first iteration, use small fixed step
638
+ dtfin = 1e-1
639
+ iterate = True
640
+
641
+ dpowerold = dpowernew
642
+ # Update inlet temperature for next iteration
643
+ T_f_in += dtfin
644
+
645
+ return T_f_out, T_f_in, Reff, ptemp, Tb, qbz
646
+
647
+ def visualize_pipes(self):
648
+ """
649
+ Plot a cross-sectional diagram of the borehole and pipe layout.
650
+
651
+ Returns
652
+ -------
653
+ matplotlib.figure.Figure
654
+ Figure containing the visualization.
655
+ """
656
+ plt.rc("font", size=12)
657
+ plt.rc("xtick", labelsize=12)
658
+ plt.rc("ytick", labelsize=12)
659
+ plt.rc("lines", lw=1.5, markersize=5.0)
660
+ plt.rc("savefig", dpi=500)
661
+
662
+ fig = plt.figure()
663
+ ax = fig.add_subplot(111)
664
+ ax.set_xlabel(r"$x$ [m]")
665
+ ax.set_ylabel(r"$y$ [m]")
666
+ ax.axis("equal")
667
+
668
+ # Draw major and minor tick marks inwards
669
+ ax.tick_params(
670
+ axis="both",
671
+ which="both",
672
+ direction="in",
673
+ bottom=True,
674
+ top=True,
675
+ left=True,
676
+ right=True,
677
+ )
678
+
679
+ # Auto-adjust minor tick marks
680
+ ax.xaxis.set_minor_locator(AutoMinorLocator())
681
+ ax.yaxis.set_minor_locator(AutoMinorLocator())
682
+
683
+ # Color cycle
684
+ colors = plt.cm.tab20.colors
685
+ lw = plt.rcParams["lines.linewidth"]
686
+
687
+ # Borehole wall outline
688
+ ax.plot(
689
+ [-self.b.r_b, 0.0, self.b.r_b, 0.0],
690
+ [0.0, self.b.r_b, 0.0, -self.b.r_b],
691
+ "k.",
692
+ alpha=0.0,
693
+ )
694
+ borewall = plt.Circle(
695
+ (0.0, 0.0), radius=self.b.r_b, fill=False, color="k", linestyle="--", lw=lw
696
+ )
697
+ ax.add_patch(borewall)
698
+
699
+ # Pipes
700
+ for i in range(self.nPipes):
701
+ # Coordinates of pipes
702
+ (x_in, y_in) = self.pos[i]
703
+
704
+ # Pipe outline (inlet)
705
+ pipe_in_in = plt.Circle(
706
+ (x_in, y_in),
707
+ radius=self.r_in[i],
708
+ fill=False,
709
+ linestyle="-",
710
+ color=colors[i],
711
+ lw=lw,
712
+ )
713
+ pipe_in_out = plt.Circle(
714
+ (x_in, y_in),
715
+ radius=self.r_out[i],
716
+ fill=False,
717
+ linestyle="-",
718
+ color=colors[i],
719
+ lw=lw,
720
+ )
721
+
722
+ ax.text(x_in, y_in, i, ha="center", va="center")
723
+ ax.add_patch(pipe_in_in)
724
+ ax.add_patch(pipe_in_out)
725
+
726
+ plt.tight_layout()
727
+ return fig