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.
- geoloop/axisym/AxisymetricEL.py +751 -0
- geoloop/axisym/__init__.py +3 -0
- geoloop/bin/Flowdatamain.py +89 -0
- geoloop/bin/Lithologymain.py +84 -0
- geoloop/bin/Loadprofilemain.py +100 -0
- geoloop/bin/Plotmain.py +250 -0
- geoloop/bin/Runbatch.py +81 -0
- geoloop/bin/Runmain.py +86 -0
- geoloop/bin/SingleRunSim.py +928 -0
- geoloop/bin/__init__.py +3 -0
- geoloop/cli/__init__.py +0 -0
- geoloop/cli/batch.py +106 -0
- geoloop/cli/main.py +105 -0
- geoloop/configuration.py +946 -0
- geoloop/constants.py +112 -0
- geoloop/geoloopcore/CoaxialPipe.py +503 -0
- geoloop/geoloopcore/CustomPipe.py +727 -0
- geoloop/geoloopcore/__init__.py +3 -0
- geoloop/geoloopcore/b2g.py +739 -0
- geoloop/geoloopcore/b2g_ana.py +516 -0
- geoloop/geoloopcore/boreholedesign.py +683 -0
- geoloop/geoloopcore/getloaddata.py +112 -0
- geoloop/geoloopcore/pyg_ana.py +280 -0
- geoloop/geoloopcore/pygfield_ana.py +519 -0
- geoloop/geoloopcore/simulationparameters.py +130 -0
- geoloop/geoloopcore/soilproperties.py +152 -0
- geoloop/geoloopcore/strat_interpolator.py +194 -0
- geoloop/lithology/__init__.py +3 -0
- geoloop/lithology/plot_lithology.py +277 -0
- geoloop/lithology/process_lithology.py +695 -0
- geoloop/loadflowdata/__init__.py +3 -0
- geoloop/loadflowdata/flow_data.py +161 -0
- geoloop/loadflowdata/loadprofile.py +325 -0
- geoloop/plotting/__init__.py +3 -0
- geoloop/plotting/create_plots.py +1142 -0
- geoloop/plotting/load_data.py +432 -0
- geoloop/utils/RunManager.py +164 -0
- geoloop/utils/__init__.py +0 -0
- geoloop/utils/helpers.py +841 -0
- geoloop-1.0.0.dist-info/METADATA +120 -0
- geoloop-1.0.0.dist-info/RECORD +46 -0
- geoloop-1.0.0.dist-info/entry_points.txt +2 -0
- geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0.dist-info/licenses/LICENSE.md +2 -1
- geoloop-0.0.1.dist-info/METADATA +0 -10
- geoloop-0.0.1.dist-info/RECORD +0 -6
- {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/WHEEL +0 -0
- {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
|