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,751 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+
5
+ def check_asarray(rmin: float | np.ndarray, nr: int) -> np.ndarray:
6
+ """
7
+ Ensure the input is a NumPy array.
8
+
9
+ If `rmin` is already a NumPy array, it is returned as is.
10
+ Otherwise, a NumPy array of length `nr` filled with `rmin` is created.
11
+
12
+ Parameters
13
+ ----------
14
+ rmin : float or np.ndarray
15
+ Input value or array to be converted/checked.
16
+ nr : int
17
+ Length of the array to create if `rmin` is not an array.
18
+
19
+ Returns
20
+ -------
21
+ np.ndarray
22
+ NumPy array with values from `rmin` or filled with `rmin`.
23
+ """
24
+
25
+ if isinstance(rmin, np.ndarray):
26
+ res = rmin
27
+ else:
28
+ res = np.full([nr], rmin)
29
+ return res
30
+
31
+
32
+ class SimGridRegular:
33
+ """
34
+ A regular 3D grid for simulation.
35
+
36
+ This class sets up a grid with `nx` cells in x, `ny` cells in y, and `nz` cells in z.
37
+ It can handle transmissivity and fluxes in x, y, z directions, volumetric specific
38
+ heat of fluid and rock, and heat production.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ nx: int,
44
+ ny: int,
45
+ nz: int,
46
+ txyz: np.ndarray,
47
+ fxyz: np.ndarray,
48
+ rcf: float,
49
+ rcbulk: float,
50
+ axyz: np.ndarray,
51
+ ):
52
+ """
53
+ Constructor for SimGridRegular.
54
+
55
+ Parameters
56
+ ----------
57
+ nx : int
58
+ Number of grid cells in x-direction.
59
+ ny : int
60
+ Number of grid cells in y-direction.
61
+ nz : int
62
+ Number of grid cells in z-direction.
63
+ txyz : np.ndarray
64
+ Transmissivity array in x, y, z directions (shape: 3 x nx x ny x nz).
65
+ fxyz : np.ndarray
66
+ Flux array in x, y, z directions (shape: 3 x nx x ny x nz).
67
+ rcf : float
68
+ Fluid volumetric specific heat [J m^-3 K^-1].
69
+ rcbulk : float
70
+ Bulk volumetric specific heat [J m^-3 K^-1].
71
+ axyz : np.ndarray
72
+ Heat production integrated over cell volume [W].
73
+ """
74
+
75
+ self.nx = nx
76
+ self.ny = ny
77
+ self.nz = nz
78
+ self.txyz = txyz
79
+ self.fxyz = fxyz
80
+ self.rcf = rcf
81
+ self.rcbulk = rcbulk
82
+ self.axyz = axyz
83
+
84
+ def set_initvalues(self, temp: np.ndarray) -> None:
85
+ """
86
+ Set the initial values of the grid.
87
+
88
+ Parameters
89
+ ----------
90
+ temp : np.ndarray
91
+ Initial values to assign to the grid.
92
+ """
93
+ self.init_vals = temp
94
+
95
+ def checkindex(self, ii: int, jj: int, kk: int) -> bool:
96
+ """
97
+ Check if the given indices are within the grid bounds.
98
+
99
+ Parameters
100
+ ----------
101
+ ii : int
102
+ Index along x-axis.
103
+ jj : int
104
+ Index along y-axis.
105
+ kk : int
106
+ Index along z-axis.
107
+
108
+ Returns
109
+ -------
110
+ bool
111
+ True if the indices are within bounds, False otherwise.
112
+ """
113
+ ok = (
114
+ (ii >= 0)
115
+ and (jj >= 0)
116
+ and (kk >= 0)
117
+ and (ii < self.nx)
118
+ and (jj < self.ny)
119
+ and (kk < self.nz)
120
+ )
121
+ return ok
122
+
123
+ def clearDirichlet(self) -> None:
124
+ """
125
+ Reset Dirichlet boundary conditions.
126
+
127
+ All nodes are set to have no fixed (Dirichlet) boundary condition.
128
+ """
129
+ self.dirichlet = np.arange(self.nx * self.ny * self.nz, dtype=float).reshape(
130
+ self.nx, self.ny, self.nz
131
+ )
132
+ self.dirichlet *= 0
133
+ self.dirichlet += 1.0
134
+
135
+ def addDirichlet(self, indexlist: list[list[int]], value: float) -> None:
136
+ """
137
+ Apply Dirichlet boundary conditions to specified cells.
138
+
139
+ Adds a list of cell indices as dirichlet boundary conditions with the value specified.
140
+
141
+ Parameters
142
+ ----------
143
+ indexlist : list of list of int
144
+ List of cell indices, each as [i, j, k]. ([ [i,j,k], [i,j,k], ..])
145
+ value : float
146
+ Value to apply to the specified cells.
147
+ """
148
+ for i, index in enumerate(indexlist):
149
+ self.dirichlet[index[0]][index[1]][index[2]] = (
150
+ 0.0 # causing the cell not be changed
151
+ )
152
+ self.init_vals[index[0]][index[1]][index[2]] = value # value of the cell
153
+
154
+
155
+ class AxiGrid(SimGridRegular):
156
+ """
157
+ Axisymmetric grid following Langevin (2009).
158
+
159
+ The grid has `nx` cells along the axis, `ny=nr` radial cells, and `nz=1` in tangential direction.
160
+ Radial direction corresponds to the y-axis, axial direction to x, and tangential to z.
161
+
162
+ x[0] = rmin is first cell wall // rmax midpoint of last cell
163
+ x[1]
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ nx: int,
169
+ nr: int,
170
+ xmin: float,
171
+ xmax: float,
172
+ rmin: float | np.ndarray,
173
+ rmax: float,
174
+ k: float | np.ndarray,
175
+ rcf: float | np.ndarray,
176
+ rcr: float | np.ndarray,
177
+ por: float | np.ndarray,
178
+ firstwellcell: bool = True,
179
+ kwf: float = 0.6,
180
+ rcwf: float = 4.2e6,
181
+ drmin: float = 0,
182
+ endcellbound: bool = False,
183
+ ):
184
+ """
185
+ Constructor for the AxiGrid class.
186
+
187
+ Parameters
188
+ ----------
189
+ nx : int
190
+ Number of grid cells along the axis.
191
+ nr : int
192
+ Number of radial cells.
193
+ xmin : float
194
+ Location of the first cell along the axis.
195
+ xmax : float
196
+ Location of the last cell along the axis.
197
+ rmin : float or np.ndarray
198
+ Radius of the first cell face near the radial axis (>0).
199
+ rmax : float
200
+ Radius at the midpoint of the last cell.
201
+ k : float or np.ndarray
202
+ Bulk conductivity in the radial direction (dimension nx).
203
+ rcf : float or np.ndarray
204
+ Volumetric heat capacity of fluid [J m^-3 K^-1] (dimension nx).
205
+ rcr : float or np.ndarray
206
+ Volumetric heat capacity of rock [J m^-3 K^-1] (dimension nx).
207
+ por : float or np.ndarray
208
+ Porosity of cells (dimension nx).
209
+ firstwellcell : bool, optional
210
+ Whether the first cell is a well cell with modified properties (default: True).
211
+ kwf : float, optional
212
+ Well fluid thermal conductivity (used if firstwellcell is True, default: 0.6).
213
+ rcwf : float, optional
214
+ Well fluid volumetric heat capacity [J m^-3 K^-1] (used if firstwellcell is True, default: 4.2e6).
215
+ drmin : float, optional
216
+ Minimum radial increase for cells (default: 0).
217
+ endcellbound : bool, optional
218
+ Apply end cell boundary condition (default: False).
219
+ """
220
+ self.nx = nx
221
+ self.ny = nr
222
+ self.nz = 1
223
+ # x is cell node locations along the x axis
224
+ self.x = np.linspace(xmin, xmax, self.nx)
225
+
226
+ self.initGrid(
227
+ rmin, rmax, k, rcf, rcr, por, firstwellcell, kwf, rcwf, drmin, endcellbound
228
+ )
229
+
230
+ def initfrom_xarray(
231
+ self,
232
+ x_array: np.ndarray,
233
+ nr: int,
234
+ rmin: float | np.ndarray,
235
+ rmax: float,
236
+ k: np.ndarray,
237
+ rcf: np.ndarray,
238
+ rcr: np.ndarray,
239
+ por: np.ndarray,
240
+ firstwellcell: bool = True,
241
+ kwf: float = 0.6,
242
+ rcwf: float = 4.2e6,
243
+ drmin: float = 0,
244
+ endcellbound: bool = False,
245
+ ) -> None:
246
+ """
247
+ Initialize the grid using specified x-array positions.
248
+
249
+ Parameters
250
+ ----------
251
+ x_array : np.ndarray
252
+ Cell midpoint positions along the x-axis.
253
+ nr : int
254
+ Number of cells in radial direction.
255
+ rmin : float or np.ndarray
256
+ Minimum cell face radius closest to the radial axis (>0). Can be an array of size nx.
257
+ rmax : float
258
+ Maximum cell midpoint radius (furthest from radial axis). Constant.
259
+ k : np.ndarray
260
+ Bulk conductivity in radial direction (dimension nx).
261
+ rcf : np.ndarray
262
+ Volumetric heat capacity of fluid [J m^-3 K^-1] (dimension nx).
263
+ rcr : np.ndarray
264
+ Volumetric heat capacity of rock [J m^-3 K^-1] (dimension nx).
265
+ por : np.ndarray
266
+ Porosity of cells (dimension nx).
267
+ firstwellcell : bool, optional
268
+ Whether the first cell is a well cell with modified properties (default: True).
269
+ kwf : float, optional
270
+ Well fluid thermal conductivity (default: 0.6).
271
+ rcwf : float, optional
272
+ Well fluid volumetric heat capacity [J m^-3 K^-1] (default: 4.2e6).
273
+ drmin : float, optional
274
+ Minimum radial increase for cells (default: 0).
275
+ endcellbound : bool, optional
276
+ Apply end cell boundary condition (default: False).
277
+ """
278
+ self.nx = len(x_array)
279
+ self.ny = nr
280
+ self.nz = 1
281
+ self.x = x_array
282
+ self.initGrid(
283
+ rmin, rmax, k, rcf, rcr, por, firstwellcell, kwf, rcwf, drmin, endcellbound
284
+ )
285
+
286
+ def initGrid(
287
+ self,
288
+ rmin: float | np.ndarray,
289
+ rmax: float | np.ndarray,
290
+ k: np.ndarray,
291
+ rcf: np.ndarray,
292
+ rcr: np.ndarray,
293
+ por: np.ndarray,
294
+ firstwellcell: bool,
295
+ kwf: float,
296
+ rcwf: float,
297
+ drmin: float,
298
+ endcellbound: bool,
299
+ ) -> None:
300
+ """
301
+ Initialize the grid with radial and axial properties.
302
+
303
+ Parameters
304
+ ----------
305
+ rmin : float or np.ndarray
306
+ Radius of first cell face closest to radial axis (>0). Can be array of size nx.
307
+ rmax : float or np.ndarray
308
+ Radius of midpoint of last cell. Used to calculate cell sizes along axis.
309
+ k : np.ndarray
310
+ Bulk conductivity in radial direction (dimension nx).
311
+ rcf : np.ndarray
312
+ Volumetric heat capacity of fluid [J m^-3 K^-1] (dimension nx).
313
+ rcr : np.ndarray
314
+ Volumetric heat capacity of rock [J m^-3 K^-1] (dimension nx).
315
+ por : np.ndarray
316
+ Porosity of cells (dimension nx).
317
+ firstwellcell : bool
318
+ Whether the first cell is a well cell with modified properties.
319
+ kwf : float
320
+ Well fluid thermal conductivity (used if firstwellcell is True).
321
+ rcwf : float
322
+ Well fluid volumetric heat capacity [J m^-3 K^-1] (used if firstwellcell is True).
323
+ drmin : float
324
+ Minimum radius increase for cells (optional).
325
+ endcellbound : bool
326
+ Apply end cell boundary condition (optional) in radial direction as Dirichlet.
327
+ """
328
+ self.dx = np.zeros_like(self.x)
329
+ dxgrid = np.diff(self.x)
330
+ for i in range(self.nx):
331
+ if i > 0:
332
+ self.dx[i] += 0.5 * dxgrid[i - 1]
333
+ if i < self.nx - 1:
334
+ self.dx[i] += 0.5 * dxgrid[i]
335
+
336
+ self.rmin = check_asarray(rmin, self.nx)
337
+ self.rmax = check_asarray(rmax, self.nx)
338
+ k = check_asarray(k, self.nx)
339
+ rcf = check_asarray(rcf, self.nx)
340
+ rcr = check_asarray(rcr, self.nx)
341
+ por = check_asarray(por, self.nx)
342
+
343
+ gmesh = np.arange(self.nx * self.ny, dtype=float).reshape(self.nx, self.ny, 1)
344
+ gtrans = np.arange(3 * self.nx * self.ny, dtype=float).reshape(
345
+ 3, self.nx, self.ny, 1
346
+ )
347
+ # cell sizes in r
348
+ self.axidr = gmesh * 0
349
+ self.axisumdr = gmesh * 0
350
+ # cell midpoints in r
351
+ self.axicellrmid = gmesh * 0
352
+ # transmission values
353
+ self.txyz = gtrans * 0
354
+ # flux values
355
+ self.fxyz = gtrans * 0
356
+ # heat production values
357
+ self.axyz = gmesh * 0
358
+
359
+ # properties
360
+ self.k = gmesh * 0
361
+ self.vol = gmesh * 0
362
+ self.overcp = gmesh * 0
363
+ self.rcf = gmesh * 0
364
+ self.rcbulk = gmesh * 0
365
+
366
+ for i in range(self.nx):
367
+ dmin = self.rmin[i]
368
+ logdmin = np.log10(dmin)
369
+ logdmax = np.log10(self.rmax[i])
370
+ # estimate last cell size
371
+ dlog = (logdmax - logdmin) / (self.ny)
372
+
373
+ # axisumdx are cell interfaces starting at first to last cell
374
+ axisumdr = np.logspace(logdmin, logdmax + dlog / 2, self.ny, base=10)
375
+ for ir in range(1, self.ny):
376
+ drscale = 1.2
377
+ if (axisumdr[ir] - axisumdr[ir - 1]) < drmin:
378
+ if ir == 1:
379
+ dr = axisumdr[0] * drscale
380
+ else:
381
+ dr = (axisumdr[ir - 1] - axisumdr[ir - 2]) * drscale
382
+ dr = drmin
383
+ axisumdr[ir] = axisumdr[ir - 1] + dr
384
+ # axidr are cell sizes
385
+ if endcellbound:
386
+ axisumdr = np.logspace(logdmin, logdmax, self.ny, base=10)
387
+ axidr = np.zeros_like(axisumdr)
388
+ axidr[0] = dmin
389
+ axidr[1:] = np.diff(axisumdr)
390
+ axicellrmid = axisumdr - 0.5 * axidr
391
+
392
+ axitoparea = axisumdr**2 * np.pi
393
+ axitoparea[1:] = np.diff(axitoparea)
394
+
395
+ self.axicellrmid[i] = np.reshape(axicellrmid * 1.0, (self.ny, 1))
396
+ self.axidr[i] = np.reshape(axidr, (self.ny, 1))
397
+ self.axisumdr[i] = np.reshape(axisumdr, (self.ny, 1))
398
+ for j in range(self.ny):
399
+ self.k[i][j][0] = k[i]
400
+ self.rcf[i][j][0] = rcf[i]
401
+ if (firstwellcell) and (j == 0):
402
+ self.k[i][j][0] = 30 # 30
403
+ self.rcbulk[i][j][0] = rcwf
404
+ else:
405
+ self.rcbulk[i][j][0] = rcf[i] * por[i] + rcr[i] * (1 - por[i])
406
+ self.vol[i][j][0] = axitoparea[j] * self.dx[i]
407
+ self.overcp[i][j][0] = 1.0 / (self.vol[i][j][0] * self.rcbulk[i][j][0])
408
+
409
+ # setup connection factors, assume only in radial direction (ia=1) for now
410
+ ia = 1
411
+ for i in range(self.nx):
412
+ for j in range(self.ny - 1):
413
+ self.txyz[ia][i][j][0] = (
414
+ 2
415
+ * np.pi
416
+ * self.k[i][j][0]
417
+ * self.dx[i]
418
+ / np.log(self.axicellrmid[i][j + 1][0] / self.axicellrmid[i][j][0])
419
+ )
420
+ ln1 = np.log(self.axicellrmid[i][j + 1][0] / self.axisumdr[i][j][0])
421
+ ln2 = np.log(self.axisumdr[i][j][0] / self.axicellrmid[i][j][0])
422
+ self.txyz[ia][i][j][0] = (
423
+ 2
424
+ * np.pi
425
+ * self.dx[i]
426
+ / (ln1 / self.k[i][j + 1][0] + ln2 / self.k[i][j][0])
427
+ )
428
+ ia = 0
429
+ for i in range(self.nx - 1):
430
+ for j in range(self.ny):
431
+ self.txyz[ia][i][j][0] = 1 / (
432
+ 0.5 * self.dx[i] ** 2 / (self.k[i][j][0] * self.vol[i][j][0])
433
+ + 0.5
434
+ * self.dx[i + 1] ** 2
435
+ / (self.k[i + 1][j][0] * self.vol[i + 1][j][0])
436
+ )
437
+
438
+ def initTempX(self, tempx: np.ndarray) -> None:
439
+ """
440
+ Set the initial temperature values along the x-axis.
441
+
442
+ Parameters
443
+ ----------
444
+ tempx : np.ndarray
445
+ Initial temperatures along the x-axis.
446
+ """
447
+ init_val = self.vol * 0.0
448
+ for i in range(self.nx):
449
+ for j in range(self.ny):
450
+ init_val[i][j][0] = tempx[i]
451
+ self.set_initvalues(init_val)
452
+
453
+ def setWellFlow(self, q: np.ndarray) -> None:
454
+ """
455
+ Set the flow along the well cells in x-direction (`fxyz[0][i][:]` to `q[i]`) defined along the well path (nx).
456
+
457
+ If there are laterals along the well path these can be incorporated by reducing the flow to q=q/nlateral
458
+ at the along hole position of the laterals.
459
+
460
+ Parameters
461
+ ----------
462
+ q : np.ndarray
463
+ Flow along the well bore [m³/s] for each cell along the well path (dimension nx).
464
+ """
465
+ for i in range(self.nx):
466
+ self.fxyz[0, i, 0, :] = q[i]
467
+
468
+ def setWellA(self, a: np.ndarray) -> None:
469
+ """
470
+ Set the heat production along the well cells (a[i,0,:] to a[i]) defined along the well path (nx).
471
+
472
+ Parameters
473
+ ----------
474
+ a : np.ndarray
475
+ Heat production [W] for each cell along the well path (dimension nx).
476
+ """
477
+ for i in range(self.nx):
478
+ self.axyz[i, 0, :] = a[i]
479
+
480
+ def plot_result(
481
+ self,
482
+ result: np.ndarray,
483
+ itime: int,
484
+ rmax: float | None = None,
485
+ fname: str | None = None,
486
+ dif: bool = False,
487
+ ) -> None:
488
+ """
489
+ Plot the simulation result at a specific time step.
490
+
491
+ Parameters
492
+ ----------
493
+ result : np.ndarray
494
+ Result array from ODE integration.
495
+ itime : int
496
+ Time index to plot.
497
+ rmax : float, optional
498
+ Maximum radius to plot (default is None).
499
+ fname : str, optional
500
+ File name to save the figure (default is None, shows plot).
501
+ dif : bool, optional
502
+ Plot the difference with the first timestep if True (default: False).
503
+ """
504
+ c = plt.rcParams["axes.prop_cycle"].by_key()["color"]
505
+
506
+ fig, ax = plt.subplots(1, 1, figsize=(18, 9))
507
+ temp = result[itime].reshape(self.nx, self.ny)
508
+ if dif:
509
+ temp0 = result[0].reshape(self.nx, self.ny)
510
+ temp = temp - temp0
511
+ temptrans = np.transpose(temp)
512
+ xg = self.x
513
+ yg = self.axicellrmid[0].reshape(self.ny)
514
+ cp = ax.contourf(xg, yg, temptrans)
515
+ fig.colorbar(cp) # Add a colorbar to a plot
516
+
517
+ # contour
518
+ if dif:
519
+ ax.set_title("Temperature Difference")
520
+ levels = np.arange(-50, 30, 20)
521
+ else:
522
+ ax.set_title("Temperature")
523
+ levels = np.arange(0, 100, 10)
524
+ decimals = 0
525
+ # cs = plt.contour(xg, yg, temptrans, levels, colors=c[0])
526
+ cs = plt.contour(xg, yg, temptrans, colors=c[0])
527
+ fmt = "%1." + str(decimals) + "f"
528
+ plt.clabel(cs, fmt=fmt)
529
+
530
+ ax.set_ylabel("radius (m)")
531
+ ax.set_xlabel("ahd (m)")
532
+ if rmax != None:
533
+ plt.ylim(0, rmax)
534
+ if fname == None:
535
+ plt.show()
536
+ else:
537
+ plt.savefig(fname)
538
+
539
+
540
+ IFLUXF = [1, 0, 0]
541
+ JFLUXF = [0, 1, 0]
542
+ KFLUXF = [0, 0, 1]
543
+
544
+
545
+ def thomas_heatloop(
546
+ time: np.ndarray,
547
+ dt: float,
548
+ nt: int,
549
+ qscale: np.ndarray,
550
+ t_inlet: float | np.ndarray,
551
+ grid: SimGridRegular,
552
+ ahddif: bool = True,
553
+ fixsurfaceTemp: bool = True,
554
+ ) -> np.ndarray:
555
+ """
556
+ Solve transient heat transport using a mixed explicit/implicit scheme:
557
+ - Explicit in the along-hole direction (except boundary treatment)
558
+ - Implicit in the radial direction using the Thomas tridiagonal algorithm
559
+
560
+ Integrates over the timeseries time, the heat loop specified in grid. This used a mixture of explicit finite difference
561
+ and the implicit thomas algorithm (which is tridiagonal guassian elimination) for the radial diffusion and advective flow.
562
+
563
+ The components of the tridiagonal system (a,b,c,d) are in the range of grid.nx
564
+
565
+ - b[i] = diagonal (referring to node)
566
+ - a[i] = same row, column to the left (referring to node i-1)
567
+ - c[i] = same row column to the right (referring to node i+1)
568
+ - d[i] = RHS for node i (RHS = right hand side)
569
+
570
+ Mathematical documentation available in report Wees et al. (2023), TKI reference: 1921406
571
+ Equation nr. annotations in code refer to equation nr. in Appendix A of the report.
572
+
573
+ Parameters
574
+ ----------
575
+ time : np.ndarray
576
+ 1D array of time steps (arbitrary units). Each entry corresponds to a
577
+ point where the solution is stored.
578
+ dt : float
579
+ Scaling factor that converts the time unit in `time` to seconds.
580
+ nt : int
581
+ Number of intermediate timesteps for each main timestep in `time`.
582
+ The solver uses fixed sub-stepping, not adaptive stepping.
583
+ (It does not use the automated timestepping as in odeint)
584
+ qscale : np.ndarray
585
+ 1D array scaling the reference mass flow rate over time.
586
+ Must be the same length as `time`.
587
+ t_inlet : float or np.ndarray
588
+ Fixed inlet temperature at the top of the well.
589
+ If array: must have same length as `time`.
590
+ grid : SimGridRegular
591
+ Grid specifying geometry, transmissivities, flow field, and
592
+ thermophysical properties (rock and fluid).
593
+ ahddif : bool, optional
594
+ If True, include along-hole diffusion outside the borehole.
595
+ fixsurfaceTemp : bool, optional
596
+ If True, impose Dirichlet (fixed-temperature) conditions at the
597
+ surface nodes in the along-hole direction.
598
+
599
+ Returns
600
+ -------
601
+ np.ndarray
602
+ Temperature field with dimensions
603
+ `(ntimes, nx, ny, nz)`
604
+ where:
605
+ - `time` dimension corresponds to the sampling times in `time`
606
+ - `nx` is the along-hole direction
607
+ - `ny` is the radial direction
608
+ - `nz` is typically 1 (semi-3D axisymmetric model)
609
+ """
610
+ # set up array to be filled with temperature results
611
+ # dimensions: time, nx, ny, nz, dimensions: time, along hole direction, radial direction, z-direction
612
+ # nz = 1, semi-3D grid
613
+ result = np.arange(len(time) * grid.nx * grid.ny * grid.nz, dtype=float).reshape(
614
+ len(time), grid.nx, grid.ny, grid.nz
615
+ )
616
+ nr = grid.ny
617
+ a = np.arange(nr, dtype=float)
618
+ b = a * 0
619
+ c = a * 0
620
+ d = a * 0
621
+ x = a * 0
622
+ tstart = 0
623
+ temp = grid.init_vals * 1.0
624
+
625
+ t_inlet = check_asarray(t_inlet, len(time))
626
+
627
+ result[0] = temp * 1.0
628
+ for itime in range(1, len(time)):
629
+ # dtime = (time[itime] - time[itime-1])/nt, tstart is update at the end of every timstep
630
+ dtime = (time[itime] - tstart) / nt
631
+ dtimedt = dtime * dt
632
+ # time = time[itime]
633
+
634
+ for istep in range(nt):
635
+ scale = np.interp(
636
+ tstart + istep * dtime, time, qscale
637
+ ) # scaling factors for the reference flow interpolated over time
638
+ ia = 1 # flux calculation in radial (dim j) direction
639
+ k = 0 # nz = 1, so no flux to be calculated in z-dimension
640
+
641
+ sign_flux = np.sign(grid.fxyz[0][0][0] * scale)
642
+
643
+ if sign_flux >= 0:
644
+ range_i = np.arange(grid.nx)
645
+ else:
646
+ range_i = np.arange(grid.nx - 1, -1, -1)
647
+
648
+ # i is Along hole index
649
+ # j is radius index
650
+ for i_index, i in enumerate(range_i):
651
+ for j in range(nr):
652
+ a[j] = 0.0
653
+ c[j] = 0.0
654
+ flux = (
655
+ grid.fxyz[0][i][j][k] * scale
656
+ ) # scale fluid flux inside borehole in along hole (ia=0) direction
657
+
658
+ try:
659
+ d[j] = temp[i][j][k] / (
660
+ grid.overcp[i][j][k] * dtimedt
661
+ ) # (overcp = 1/cp) eq nr. (9) - RHS
662
+ except:
663
+ print("this goes wrong in thomas heat loop")
664
+ exit()
665
+
666
+ # add along hole diffusion outside borehole
667
+ adddif = ahddif
668
+ if adddif:
669
+ if (i > 0) and (j > 0):
670
+ d[j] += (temp[i - 1][j][k] - temp[i][j][k]) * grid.txyz[0][
671
+ i - 1
672
+ ][j][k] # RHS in Appendix A.4 - along axis heat conduction
673
+ if (i < grid.nx - 1) and (j > 0):
674
+ d[j] += (temp[i + 1][j][k] - temp[i][j][k]) * grid.txyz[0][
675
+ i
676
+ ][j][k] # RHS in Appendix A.4 - along axis heat conduction
677
+
678
+ b[j] = 1 / (grid.overcp[i][j][k] * dtimedt) # eq nr. (9) left term
679
+
680
+ if j == 0:
681
+ # well bore include the mass rate
682
+ if i_index == 0:
683
+ t_i = t_inlet[
684
+ itime
685
+ ] # for injection set temperature of first cell in along hole direction ('top of well') to inlet temperature, used in eq nr. (14)
686
+ else:
687
+ t_i = temp[range_i[i_index - 1]][j][
688
+ k
689
+ ] # temperature used in factor on RHS in eq nr. (13)
690
+
691
+ d[j] += (
692
+ abs(flux) * t_i * grid.rcbulk[i][j][k]
693
+ ) # factor on RHS in eq nr. (13)
694
+ b[j] += (
695
+ abs(flux) * grid.rcbulk[i][j][k]
696
+ ) # factor on LHS in eq nr.(13)
697
+ a[j] = 0
698
+ else:
699
+ # add Tj-1/2
700
+ a[j] = -grid.txyz[ia][i][j - 1][
701
+ k
702
+ ] # middle term in LHS in eq nr. (9)
703
+
704
+ if j < nr - 1:
705
+ # add Tj+1/2
706
+ c[j] = -grid.txyz[ia][i][j][
707
+ k
708
+ ] # right term in LHS in eq nr. (9)
709
+ b[j] = b[j] - a[j] - c[j] # eq nr. (9)
710
+
711
+ # if boundary condition is set to a fixed surface temperature, temperature in first and last cells in along hole direction is fixed
712
+ if fixsurfaceTemp:
713
+ if (i == 0) or (i == grid.nx - 1):
714
+ if j > 0:
715
+ # force the temperature in the next time step to remain the same
716
+ # for the row of equations set only the diagonal (b), rhs (d) and the off diagonal (a,c) to 0
717
+ b[j] = 1.0 / (grid.overcp[i][j][k] * dtimedt)
718
+ d[j] = temp[i][j][k] / (grid.overcp[i][j][k] * dtimedt)
719
+ c[j] = 0.0
720
+ a[j] = 0.0
721
+ # fix temperature in first cell in along hole direction ('top of well') to inlet temperature, as defined by input
722
+ if (i_index == 0) and (j == 0):
723
+ b[j] = 1.0 / (grid.overcp[i][j][k] * dtimedt)
724
+ d[j] = t_inlet[itime] / (grid.overcp[i][j][k] * dtimedt)
725
+ c[j] = 0.0
726
+ a[j] = 0.0
727
+
728
+ # implement algorithm and solve for temperature
729
+ for j in range(1, nr):
730
+ # forward substitution to create an upper triangular matrix, w is conversion factor to set elements a to 0
731
+ w = a[j] / b[j - 1] # matrix decomposition
732
+ b[j] = b[j] - w * c[j - 1] # matrix decomposition
733
+ d[j] = d[j] - w * d[j - 1] # forward substitution
734
+ # back substitution, start with last element of x
735
+ x[nr - 1] = d[nr - 1] / b[nr - 1]
736
+ for j in range(nr - 1):
737
+ it2 = (
738
+ nr - 2 - j
739
+ ) # iterate backwards over j for remaining elements of x
740
+ x[it2] = (d[it2] - c[it2] * x[it2 + 1]) / b[it2]
741
+
742
+ for j in range(nr):
743
+ temp[i][j][k] = x[j]
744
+
745
+ # end of timestep, append the temp in the result
746
+ result[itime] = temp * 1.0
747
+ # update start of timestep
748
+ tstart = time[itime]
749
+
750
+ res2 = result.reshape(len(time), grid.nx, grid.ny, grid.nz)
751
+ return res2