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.
- 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 +535 -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 +697 -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 +1137 -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.0b1.dist-info/METADATA +112 -0
- geoloop-1.0.0b1.dist-info/RECORD +46 -0
- geoloop-1.0.0b1.dist-info/entry_points.txt +2 -0
- geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0b1.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.0b1.dist-info}/WHEEL +0 -0
- {geoloop-0.0.1.dist-info → geoloop-1.0.0b1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.integrate import odeint
|
|
3
|
+
from scipy.interpolate import UnivariateSpline
|
|
4
|
+
|
|
5
|
+
from geoloop.axisym.AxisymetricEL import AxiGrid
|
|
6
|
+
from geoloop.geoloopcore.boreholedesign import BoreholeDesign
|
|
7
|
+
from geoloop.geoloopcore.CoaxialPipe import CoaxialPipe
|
|
8
|
+
from geoloop.geoloopcore.CustomPipe import CustomPipe
|
|
9
|
+
from geoloop.geoloopcore.simulationparameters import SimulationParameters
|
|
10
|
+
from geoloop.geoloopcore.soilproperties import SoilProperties
|
|
11
|
+
|
|
12
|
+
IFLUXF = [1, 0, 0]
|
|
13
|
+
JFLUXF = [0, 1, 0]
|
|
14
|
+
KFLUXF = [0, 0, 1]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class B2G:
|
|
18
|
+
"""
|
|
19
|
+
Class for depth dependent variation in pipe temperatures and borehole wall temperatures as well as soil properties,
|
|
20
|
+
using a 2D axisymmetric finite volume model
|
|
21
|
+
|
|
22
|
+
The model is based on a modified approach from the work of Cazorla-Marin [1, 2, 3].
|
|
23
|
+
|
|
24
|
+
The modification is that the thermal resistance and Tb node of pygfunction is used for the borehole wall,
|
|
25
|
+
and 3 additional nodes are included.
|
|
26
|
+
|
|
27
|
+
The Tb node is subject to heat flow determined from the thermal resistance network :Rbinv @ (Tf - Tb np.ones)
|
|
28
|
+
|
|
29
|
+
- q = sum ( qi = Rbinv @ (Tf - Tb np.ones)).
|
|
30
|
+
|
|
31
|
+
Currently, the model is with finite volume formulation without Lax-Wendroff explicit finite volume scheme
|
|
32
|
+
(LW scheme not implemented yet).
|
|
33
|
+
This is a 2nd order explicit scheme, with the thermal resistance network of the borehole wall and the fluid nodes.
|
|
34
|
+
it practically limits the number of vertical nodes to ca. 10.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
custom_pipe : CustomPipe
|
|
39
|
+
Pipe configuration object with geometric and thermal properties.
|
|
40
|
+
is_coaxial : bool
|
|
41
|
+
True if the pipe is a coaxial configuration.
|
|
42
|
+
ag : AxiGrid
|
|
43
|
+
Axisymmetric finite volume grid (initialized later).
|
|
44
|
+
|
|
45
|
+
References
|
|
46
|
+
----------
|
|
47
|
+
[1] Cazorla Marín, A.: Modelling and experimental validation of an innovative coaxial helical borehole heat exchanger
|
|
48
|
+
for a dual source heat pump system, PhD, Universitat Politècnica de València, Valencia (Spain),
|
|
49
|
+
https://doi.org/10.4995/Thesis/10251/125696, 2019.
|
|
50
|
+
[2] Cazorla-Marín, A., Montagud-Montalvá, C., Tinti, F., and Corberán, J. M.: A novel TRNSYS type of a coaxial borehole
|
|
51
|
+
heat exchanger for both short and mid term simulations: B2G model, Applied Thermal Engineering, 164, 114500,
|
|
52
|
+
https://doi.org/10.1016/j.applthermaleng.2019.114500, 2020.
|
|
53
|
+
[3] Cazorla-Marín, A., Montagud-Montalvá, C., Corberán, J. M., Montero, Á., and Magraner, T.: A TRNSYS assisting tool
|
|
54
|
+
for the estimation of ground thermal properties applied to TRT (thermal response test) data: B2G model, Applied
|
|
55
|
+
Thermal Engineering, 185, 116370, https://doi.org/10.1016/j.applthermaleng.2020.116370, 2021.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, custom_pipe: CustomPipe) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialize the B2G model with a given custom pipe configuration.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
custom_pipe : CustomPipe
|
|
65
|
+
Object containing borehole geometry, pipe arrangement, and thermal properties.
|
|
66
|
+
"""
|
|
67
|
+
self.custom_pipe = custom_pipe
|
|
68
|
+
if isinstance(self.custom_pipe, CoaxialPipe):
|
|
69
|
+
self.is_coaxial = True
|
|
70
|
+
else:
|
|
71
|
+
self.is_coaxial = False
|
|
72
|
+
|
|
73
|
+
def runsimulation(
|
|
74
|
+
self,
|
|
75
|
+
bh_design: BoreholeDesign,
|
|
76
|
+
soil_props: SoilProperties,
|
|
77
|
+
sim_params: SimulationParameters,
|
|
78
|
+
) -> tuple:
|
|
79
|
+
"""
|
|
80
|
+
Run the borehole-to-ground simulation using finite difference axisymmetric grid.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
bh_design : BoreholeDesign
|
|
85
|
+
Borehole geometry and thermal resistances.
|
|
86
|
+
soil_props : SoilProperties
|
|
87
|
+
Soil properties and ground temperature profile.
|
|
88
|
+
sim_params : SimulationParameters
|
|
89
|
+
Simulation settings: time array, inlet temperatures, flow rates, etc.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
hours : ndarray
|
|
94
|
+
Time array in hours.
|
|
95
|
+
Q_b : ndarray
|
|
96
|
+
Borehole thermal power [W].
|
|
97
|
+
flowrate : ndarray
|
|
98
|
+
Mass flow rate [kg/s].
|
|
99
|
+
qsign : ndarray
|
|
100
|
+
Sign of heat extraction (-1 for extraction, +1 for injection).
|
|
101
|
+
T_fi : ndarray
|
|
102
|
+
Fluid inlet temperatures [°C].
|
|
103
|
+
T_fo : ndarray
|
|
104
|
+
Fluid outlet temperatures [°C].
|
|
105
|
+
T_bave : ndarray
|
|
106
|
+
Average borehole wall temperature [°C].
|
|
107
|
+
z : ndarray
|
|
108
|
+
Depth coordinates [m].
|
|
109
|
+
T_b : ndarray
|
|
110
|
+
Borehole wall temperature field [°C].
|
|
111
|
+
T_f : ndarray
|
|
112
|
+
Pipe fluid temperature field [°C].
|
|
113
|
+
qzb : ndarray
|
|
114
|
+
Vertical heat flux along borehole [W/m].
|
|
115
|
+
h_fpipes : ndarray
|
|
116
|
+
Convective film coefficients for each pipe.
|
|
117
|
+
result : ndarray
|
|
118
|
+
Raw solution array from the finite difference solver.
|
|
119
|
+
zstart : ndarray
|
|
120
|
+
Lower boundary of each vertical cell [m].
|
|
121
|
+
zend : ndarray
|
|
122
|
+
Upper boundary of each vertical cell [m].
|
|
123
|
+
"""
|
|
124
|
+
custom_pipe = self.custom_pipe
|
|
125
|
+
nx = sim_params.nsegments + 1
|
|
126
|
+
z = np.linspace(custom_pipe.b.D, custom_pipe.b.D + custom_pipe.b.H, nx)
|
|
127
|
+
|
|
128
|
+
# Define vertical cell boundaries
|
|
129
|
+
zstart = z * 1.0
|
|
130
|
+
zend = z * 1.0
|
|
131
|
+
dz = np.diff(z)
|
|
132
|
+
zstart[-1] -= dz[-1]
|
|
133
|
+
zend[0:-1] = zstart[0:-1] + 0.5 * dz
|
|
134
|
+
|
|
135
|
+
# Borehole resistances
|
|
136
|
+
k_g = bh_design.get_k_g(zstart, zend)
|
|
137
|
+
R_p = bh_design.get_r_p(z)
|
|
138
|
+
# create the right structure for thermal resistances
|
|
139
|
+
self.custom_pipe.init_thermal_resistances(k_g, R_p)
|
|
140
|
+
|
|
141
|
+
# Soil conductivity
|
|
142
|
+
k_s = soil_props.get_k_s(zstart, zend, sim_params.isample)
|
|
143
|
+
|
|
144
|
+
# Time and mass flow scaling
|
|
145
|
+
hours = sim_params.time / 3600.0
|
|
146
|
+
m_flow = sim_params.m_flow[0]
|
|
147
|
+
qscale = sim_params.m_flow / m_flow
|
|
148
|
+
h_fpipes = np.zeros((qscale.shape[0], custom_pipe.nPipes))
|
|
149
|
+
h_fpipes[:] = custom_pipe.h_f
|
|
150
|
+
|
|
151
|
+
# solve for temperature distribution
|
|
152
|
+
T_f, T_b, dtf, qzb, result = self.get_temperature_depthvar(
|
|
153
|
+
hours,
|
|
154
|
+
qscale,
|
|
155
|
+
sim_params.Tin,
|
|
156
|
+
soil_props,
|
|
157
|
+
nr=sim_params.nr,
|
|
158
|
+
rmax=sim_params.rmax,
|
|
159
|
+
nsegments=nx,
|
|
160
|
+
k=k_s,
|
|
161
|
+
alfa=soil_props.alfa,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
T_fi = T_f[:, 0, 0]
|
|
165
|
+
T_fo = T_f[:, 0, custom_pipe.nPipes - 1]
|
|
166
|
+
flowrate = sim_params.m_flow
|
|
167
|
+
Q_b = (T_fo - T_fi) * custom_pipe.cp_f * flowrate
|
|
168
|
+
qsign = np.sign(Q_b)
|
|
169
|
+
T_bave = np.average(T_b, axis=1)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
hours,
|
|
173
|
+
Q_b,
|
|
174
|
+
flowrate,
|
|
175
|
+
qsign,
|
|
176
|
+
T_fi,
|
|
177
|
+
T_fo,
|
|
178
|
+
T_bave,
|
|
179
|
+
z,
|
|
180
|
+
T_b,
|
|
181
|
+
T_f,
|
|
182
|
+
-qzb,
|
|
183
|
+
h_fpipes,
|
|
184
|
+
result,
|
|
185
|
+
zstart,
|
|
186
|
+
zend,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def modify_par_ag(self, ny_add: int, param: np.ndarray) -> np.ndarray:
|
|
190
|
+
"""
|
|
191
|
+
Modify the dimension of the AxiGrid object parameter, by adding ny_add nodes in the y direction to represent
|
|
192
|
+
the pipes.
|
|
193
|
+
|
|
194
|
+
This member function is used internally to modify the original grid parameters of the AxiGrid object.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
ny_add : int
|
|
199
|
+
Number of additional nodes to insert in the y-direction for pipes.
|
|
200
|
+
param : np.ndarray
|
|
201
|
+
Original parameter array (e.g., k, vol, overcp, rcf, rcbulk, axyz).
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
np.ndarray
|
|
206
|
+
Modified parameter array with added pipe nodes.
|
|
207
|
+
"""
|
|
208
|
+
grid = self.ag
|
|
209
|
+
ag_ny = grid.ny + ny_add
|
|
210
|
+
grid_mesh = np.arange(self.ag.nx * ag_ny, dtype=float).reshape(
|
|
211
|
+
self.ag.nx, ag_ny, 1
|
|
212
|
+
)
|
|
213
|
+
grid_mesh *= 0
|
|
214
|
+
grid_mesh[:, ny_add + 1 :, :] = param[:, 1:, :]
|
|
215
|
+
return grid_mesh
|
|
216
|
+
|
|
217
|
+
def modify_trans_ag(self, ny_add: int, transmission: np.ndarray) -> np.ndarray:
|
|
218
|
+
"""
|
|
219
|
+
Modify the dimension of the AxiGrid object transmission or flux (at faces), by adding ny_add nodes in the y
|
|
220
|
+
direction to represent the pipes.
|
|
221
|
+
|
|
222
|
+
This member function is used internally to modify the original grid parameters of the AxiGrid object.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
ny_add : int
|
|
227
|
+
Number of additional nodes to insert in the y-direction.
|
|
228
|
+
transmission : np.ndarray
|
|
229
|
+
Original transmission/flux array.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
np.ndarray
|
|
234
|
+
Modified transmission array with added pipe nodes.
|
|
235
|
+
"""
|
|
236
|
+
grid = self.ag
|
|
237
|
+
ag_ny = grid.ny + ny_add
|
|
238
|
+
grid_transmission = np.arange(3 * self.ag.nx * ag_ny, dtype=float).reshape(
|
|
239
|
+
3, self.ag.nx, ag_ny, 1
|
|
240
|
+
)
|
|
241
|
+
grid_transmission *= 0
|
|
242
|
+
grid_transmission[:, :, ny_add + 1 :, :] = transmission[:, :, 1:, :]
|
|
243
|
+
return grid_transmission
|
|
244
|
+
|
|
245
|
+
def modify_ag(self) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Modify the axisymmetric grid to include the pipes and the borehole heat capacity.
|
|
248
|
+
|
|
249
|
+
This member function is used internally by initAG to modify the original grid parameters of the AxiGrid object.
|
|
250
|
+
|
|
251
|
+
Replace the heat capacity of the second node in the axisymmetric grid to take into account the borehole heat
|
|
252
|
+
capacity, corrected for the pipes (which are treated by the pipe nodes).
|
|
253
|
+
|
|
254
|
+
In addition, insert additional nodes, starting from node 0 to include the pipes
|
|
255
|
+
and roll the properties of the original axisymmetric grid to include the pipes.
|
|
256
|
+
"""
|
|
257
|
+
g = self.ag
|
|
258
|
+
n_pipes = self.custom_pipe.nPipes
|
|
259
|
+
ny_add = n_pipes - 1
|
|
260
|
+
|
|
261
|
+
for i in range(g.nx):
|
|
262
|
+
j = 1
|
|
263
|
+
if self.is_coaxial:
|
|
264
|
+
a1 = (
|
|
265
|
+
self.custom_pipe.r_in[0] ** 2 - self.custom_pipe.r_out[1] ** 2
|
|
266
|
+
) * np.pi
|
|
267
|
+
a2 = self.custom_pipe.r_in[1] ** 2 * np.pi
|
|
268
|
+
areapipes = a1 + a2
|
|
269
|
+
else:
|
|
270
|
+
rpipes = self.custom_pipe.r_in # was r_out, but heat capacity of pipes is only fluid, rest should be connected
|
|
271
|
+
# the
|
|
272
|
+
areapipes = np.sum(rpipes**2 * np.pi)
|
|
273
|
+
|
|
274
|
+
axitoparea = g.axisumdr[i][1] ** 2 * np.pi - areapipes
|
|
275
|
+
|
|
276
|
+
g.vol[i][j][0] = axitoparea * g.dx[i].item()
|
|
277
|
+
g.overcp[i][j][0] = 1.0 / (g.vol[i][j][0] * g.rcbulk[i][j][0])
|
|
278
|
+
|
|
279
|
+
# adjust the vertical transimission coefficient, based on the new volume, using Langevin approach
|
|
280
|
+
for i in range(g.nx - 1):
|
|
281
|
+
j = 1
|
|
282
|
+
ia = 0
|
|
283
|
+
g.txyz[ia][i][j][0] = 1 / (
|
|
284
|
+
0.5 * g.dx[i] ** 2 / (g.k[i][j][0] * g.vol[i][j][0])
|
|
285
|
+
+ 0.5 * g.dx[i + 1] ** 2 / (g.k[i + 1][j][0] * g.vol[i + 1][j][0])
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# from here add new nodes (remember that the first node is corresponding to the first inlet
|
|
289
|
+
# add nodes for the pipes and set properties of the first npipe nodes to 0
|
|
290
|
+
g.axisumdr = self.modify_par_ag(ny_add, g.axisumdr)
|
|
291
|
+
g.axidr = self.modify_par_ag(ny_add, g.axidr)
|
|
292
|
+
g.axicellrmid = self.modify_par_ag(ny_add, g.axicellrmid)
|
|
293
|
+
g.k = self.modify_par_ag(ny_add, g.k)
|
|
294
|
+
g.vol = self.modify_par_ag(ny_add, g.vol)
|
|
295
|
+
g.overcp = self.modify_par_ag(ny_add, g.overcp)
|
|
296
|
+
g.rcf = self.modify_par_ag(ny_add, g.rcf)
|
|
297
|
+
g.rcbulk = self.modify_par_ag(ny_add, g.rcbulk)
|
|
298
|
+
g.axyz = self.modify_par_ag(ny_add, g.axyz)
|
|
299
|
+
|
|
300
|
+
g.txyz = self.modify_trans_ag(ny_add, g.txyz)
|
|
301
|
+
g.fxyz = self.modify_trans_ag(ny_add, g.fxyz)
|
|
302
|
+
|
|
303
|
+
for i in range(g.nx):
|
|
304
|
+
if self.is_coaxial:
|
|
305
|
+
# modify the properties for the tubes. Needed are the fluxes, overcp
|
|
306
|
+
g.vol[i, 1, 0] = g.dx[i] * np.pi * self.custom_pipe.r_in[1] ** 2
|
|
307
|
+
g.vol[i, 0, 0] = (
|
|
308
|
+
g.dx[i]
|
|
309
|
+
* np.pi
|
|
310
|
+
* (self.custom_pipe.r_in[0] ** 2 - self.custom_pipe.r_out[1] ** 2)
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
# modify the properties for the tubes. Needed are the fluxes, overcp
|
|
314
|
+
g.vol[i, 0:n_pipes, 0] = g.dx[i] * np.pi * self.custom_pipe.r_in**2
|
|
315
|
+
|
|
316
|
+
for i in range(g.nx):
|
|
317
|
+
g.rcf[i, 0:n_pipes, 0] = self.custom_pipe.cp_f * self.custom_pipe.rho_f
|
|
318
|
+
g.rcbulk[i, 0:n_pipes, 0] = g.rcf[i, 0:n_pipes, 0]
|
|
319
|
+
g.overcp[:, 0:n_pipes, :] = 1 / (
|
|
320
|
+
g.vol[:, 0:n_pipes, :] * g.rcbulk[:, 0:n_pipes, :]
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Set mass flux through the pipes
|
|
324
|
+
pflux = np.asarray(self.custom_pipe.m_flow_pipe / self.custom_pipe.rho_f)
|
|
325
|
+
for i in range(g.nx):
|
|
326
|
+
g.fxyz[0, i, 0:n_pipes, 0] = pflux
|
|
327
|
+
|
|
328
|
+
# Adjust transmission for first pipe row
|
|
329
|
+
for i in range(g.nx):
|
|
330
|
+
j = 0
|
|
331
|
+
ia = 1
|
|
332
|
+
g.txyz[ia, i, j, 0] = g.dx[i] / self.custom_pipe._Rd[i][0][1]
|
|
333
|
+
|
|
334
|
+
g.ny += ny_add
|
|
335
|
+
g.dirichlet = g.overcp * 0 + 1
|
|
336
|
+
|
|
337
|
+
def init_ag(
|
|
338
|
+
self,
|
|
339
|
+
soil_props: SoilProperties,
|
|
340
|
+
nr: int = 35,
|
|
341
|
+
rmax: float = 10,
|
|
342
|
+
T_f_instart: float = 0,
|
|
343
|
+
nsegments: int = 10,
|
|
344
|
+
k: float | np.ndarray = 2,
|
|
345
|
+
rcf: float | np.ndarray = 4e6,
|
|
346
|
+
rcr: float | np.ndarray = 3e6,
|
|
347
|
+
por: float | np.ndarray = 0.4,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""
|
|
350
|
+
Initialize the axisymmetric grid (AxiGrid) for the borehole-to-ground model.
|
|
351
|
+
|
|
352
|
+
Sets up the radial and vertical grid, adds pipe nodes, and initializes
|
|
353
|
+
ground and fluid temperatures.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
soil_props : SoilProperties
|
|
358
|
+
Soil properties object to obtain ground temperatures.
|
|
359
|
+
nr : int, optional
|
|
360
|
+
Maximum number of radial nodes (should be odd), by default 35
|
|
361
|
+
rmax : float, optional
|
|
362
|
+
Radius of the midpoint of the last cell, by default 10, is a constant
|
|
363
|
+
T_f_instart : float, optional
|
|
364
|
+
Initial fluid temperature used as default to set up the grid initial temperatures, and fluid conductivity
|
|
365
|
+
and heat capacities
|
|
366
|
+
nsegments : int, optional
|
|
367
|
+
Number of nodes (nx) in the vertical direction, by default 10
|
|
368
|
+
k : float or np.ndarray, optional
|
|
369
|
+
Bulk thermal conductivity in the radial direction, by default 2.0
|
|
370
|
+
rcf : float or np.ndarray, optional
|
|
371
|
+
Volumetric heat capacity of fluid [J/m³·K], by default 4e6
|
|
372
|
+
rcr : float or np.ndarray, optional
|
|
373
|
+
Volumetric heat capacity of rock [J/m³·K], by default 3e6
|
|
374
|
+
por : float or np.ndarray, optional
|
|
375
|
+
Porosity of the cells, by default 0.4
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
None
|
|
380
|
+
"""
|
|
381
|
+
cpipe = self.custom_pipe
|
|
382
|
+
rw = cpipe.b.r_b
|
|
383
|
+
xmin = cpipe.b.D
|
|
384
|
+
xmax = xmin + cpipe.b.H
|
|
385
|
+
|
|
386
|
+
# nr needs to be odd for rmax to scale as multiple of rwmin
|
|
387
|
+
if nr % 2 == 0:
|
|
388
|
+
nr += 1
|
|
389
|
+
|
|
390
|
+
# scaling factor of cell size and spacing: factor = size cell [i+2] / size cell [i]
|
|
391
|
+
factor = 1.5
|
|
392
|
+
# how much of the cell size is corresponding the left (smaller cell) vs right (larger cell)
|
|
393
|
+
ratio = factor**0.5 - 1
|
|
394
|
+
rwmin = (1 - ratio**2) * rw
|
|
395
|
+
|
|
396
|
+
nx = nsegments
|
|
397
|
+
self.ag = AxiGrid(
|
|
398
|
+
nx,
|
|
399
|
+
nr,
|
|
400
|
+
xmin,
|
|
401
|
+
xmax,
|
|
402
|
+
rwmin,
|
|
403
|
+
rmax,
|
|
404
|
+
k,
|
|
405
|
+
rcf,
|
|
406
|
+
rcr,
|
|
407
|
+
por,
|
|
408
|
+
firstwellcell=True,
|
|
409
|
+
endcellbound=True,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Modify grid to add pipe nodes
|
|
413
|
+
self.modify_ag()
|
|
414
|
+
|
|
415
|
+
# Initialize ground temperatures along vertical nodes
|
|
416
|
+
tvd = self.ag.x
|
|
417
|
+
Tg_profile = soil_props.getTg(tvd)
|
|
418
|
+
self.ag.initTempX(Tg_profile)
|
|
419
|
+
|
|
420
|
+
# set the inlet temperatures
|
|
421
|
+
self.ag.init_vals[0, : cpipe.nInlets, 0] = T_f_instart
|
|
422
|
+
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
def get_temperature_depthvar(
|
|
426
|
+
self,
|
|
427
|
+
hours: np.ndarray,
|
|
428
|
+
qscale: np.ndarray,
|
|
429
|
+
T_f_in: np.ndarray,
|
|
430
|
+
soil_props: SoilProperties,
|
|
431
|
+
nr: int = 35,
|
|
432
|
+
rmax: float = 10,
|
|
433
|
+
nsegments: int = 10,
|
|
434
|
+
k: float | np.ndarray = 2.0,
|
|
435
|
+
alfa: float = 1e-6,
|
|
436
|
+
) -> tuple:
|
|
437
|
+
"""
|
|
438
|
+
Compute the time-dependent temperature profiles for fluid and borehole wall.
|
|
439
|
+
|
|
440
|
+
Solves the 2D axisymmetric finite difference heat transfer for a borehole
|
|
441
|
+
including the thermal interaction between fluid, pipes, grout, and ground.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
hours : np.ndarray
|
|
446
|
+
Array of time points in hours.
|
|
447
|
+
qscale : np.ndarray
|
|
448
|
+
Scaling factor for mass flow / heat rate.
|
|
449
|
+
T_f_in : np.ndarray
|
|
450
|
+
Array of inlet fluid temperatures [°C] (takes first component as inlet temperature).
|
|
451
|
+
soil_props : SoilProperties
|
|
452
|
+
Soil properties object for ground temperatures.
|
|
453
|
+
nr : int, optional
|
|
454
|
+
Maximum number of radial nodes (should be odd), by default 35.
|
|
455
|
+
rmax : float, optional
|
|
456
|
+
Radius of the midpoint of the last cell, by default 10.
|
|
457
|
+
nsegments : int, optional
|
|
458
|
+
Number of nodes in the vertical direction, by default 10.
|
|
459
|
+
k : float or np.ndarray, optional
|
|
460
|
+
Thermal conductivity (radial), by default 2.0.
|
|
461
|
+
alfa : float, optional
|
|
462
|
+
Thermal diffusivity of soil [m²/s], by default 1e-6.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
T_f : np.ndarray
|
|
467
|
+
Fluid temperature [hours, vertical nodes, pipes].
|
|
468
|
+
T_b : np.ndarray
|
|
469
|
+
Borehole wall temperature [hours, vertical nodes].
|
|
470
|
+
dtf : np.ndarray
|
|
471
|
+
Temperature difference between outlet and inlet [hours, vertical nodes].
|
|
472
|
+
qzb : np.ndarray
|
|
473
|
+
Heat flux to borehole [hours, vertical nodes].
|
|
474
|
+
result : np.ndarray
|
|
475
|
+
Full 4D result array from the solver [hours, vertical nodes, radial nodes, axial nodes].
|
|
476
|
+
"""
|
|
477
|
+
# initialze axisymmetrica
|
|
478
|
+
self.init_ag(
|
|
479
|
+
soil_props,
|
|
480
|
+
nr=nr,
|
|
481
|
+
rmax=rmax,
|
|
482
|
+
T_f_instart=T_f_in[0],
|
|
483
|
+
nsegments=nsegments,
|
|
484
|
+
k=k,
|
|
485
|
+
rcr=k / alfa,
|
|
486
|
+
por=0.0,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
dt = 3600
|
|
490
|
+
|
|
491
|
+
# Compute derivative of inlet temperature using spline
|
|
492
|
+
x = hours
|
|
493
|
+
y = T_f_in
|
|
494
|
+
spl = UnivariateSpline(x, y, k=4, s=0)
|
|
495
|
+
T_f_in_derivs = np.asarray(spl.derivatives(x))[:, 1]
|
|
496
|
+
|
|
497
|
+
# Run heat loop solver
|
|
498
|
+
result = ode_heatloop(hours, dt, qscale, T_f_in_derivs, self)
|
|
499
|
+
|
|
500
|
+
npipes = self.custom_pipe.nPipes
|
|
501
|
+
T_f = result[:, :, 0:npipes, :].reshape(len(hours), self.ag.nx, npipes)
|
|
502
|
+
T_b = result[:, :, npipes, :].reshape(len(hours), self.ag.nx)
|
|
503
|
+
|
|
504
|
+
# Mass flow and heat capacity
|
|
505
|
+
mflow = self.custom_pipe.m_flow * qscale
|
|
506
|
+
dtf = T_f[:, :, npipes - 1] - T_f[:, :, 0]
|
|
507
|
+
|
|
508
|
+
# Calculate heat flux to borehole wall
|
|
509
|
+
qzb = dtf * 1.0
|
|
510
|
+
for i in range(len(x)):
|
|
511
|
+
qzb[i, :] *= mflow[i] * self.custom_pipe.cp_f
|
|
512
|
+
|
|
513
|
+
for it in range(len(x)):
|
|
514
|
+
for i in range(self.ag.nx):
|
|
515
|
+
temppipe = T_f[it, i].reshape(npipes)
|
|
516
|
+
# get the borewall temperature
|
|
517
|
+
temp_b = T_b[it, i]
|
|
518
|
+
|
|
519
|
+
if self.is_coaxial:
|
|
520
|
+
qpipe = (self.ag.dx[i] / self.custom_pipe._Rd[i][0][0]) * (
|
|
521
|
+
temppipe[0] - temp_b
|
|
522
|
+
)
|
|
523
|
+
# get the heatflow (negative is heat flow to borehole) for each of the pipes to borehole wall
|
|
524
|
+
else:
|
|
525
|
+
qpipe = (
|
|
526
|
+
self.custom_pipe.R1[i]
|
|
527
|
+
@ (temppipe - np.ones(npipes) * temp_b)
|
|
528
|
+
* self.ag.dx[i]
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
qsum = np.sum(qpipe)
|
|
532
|
+
qzb[it, i] = qsum
|
|
533
|
+
|
|
534
|
+
qzb *= -1
|
|
535
|
+
|
|
536
|
+
return T_f, T_b, dtf, qzb, result
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def ode_heatloop(
|
|
540
|
+
time: np.ndarray, dt: float, qscale: np.ndarray, T_f_in_derivs: np.ndarray, b2g: B2G
|
|
541
|
+
) -> np.ndarray:
|
|
542
|
+
"""
|
|
543
|
+
Integrates the borehole heat transfer equations over time using an ODE solver.
|
|
544
|
+
|
|
545
|
+
Solves the transient 2D axisymmetric heat transfer between fluid, pipes, borehole, and ground.
|
|
546
|
+
Uses a simple explicit upwind scheme and Dirichlet boundary conditions for the inlet temperatures.
|
|
547
|
+
|
|
548
|
+
Parameters
|
|
549
|
+
----------
|
|
550
|
+
time : np.ndarray
|
|
551
|
+
Array of time points [h] for integration.
|
|
552
|
+
dt : float
|
|
553
|
+
Timestep scaling in seconds.
|
|
554
|
+
qscale : np.ndarray
|
|
555
|
+
Time array of scaling factors of the reference flow (time dimension)
|
|
556
|
+
T_f_in_derivs : np.ndarray
|
|
557
|
+
Time derivative of inlet fluid temperature [°C/h].
|
|
558
|
+
b2g : B2G
|
|
559
|
+
Borehole-to-ground model containing pipe and axisymmetric grid information.
|
|
560
|
+
|
|
561
|
+
Returns
|
|
562
|
+
-------
|
|
563
|
+
np.ndarray
|
|
564
|
+
Temperature array of shape (len(time), nx, ny, nz), where nx, ny, nz are
|
|
565
|
+
the dimensions of b2g.ag (grid).
|
|
566
|
+
"""
|
|
567
|
+
grid = b2g.ag
|
|
568
|
+
|
|
569
|
+
class ScaleFunc:
|
|
570
|
+
"""Interpolates a time-dependent scaling factor or derivative."""
|
|
571
|
+
|
|
572
|
+
def __init__(self, scale: np.ndarray, time: np.ndarray):
|
|
573
|
+
self.scale = scale
|
|
574
|
+
self.time = time
|
|
575
|
+
|
|
576
|
+
def get_scale(self, t: float) -> float:
|
|
577
|
+
return np.interp(t, self.time, self.scale)
|
|
578
|
+
|
|
579
|
+
# Create interpolation function for time-dependent quantities
|
|
580
|
+
qscale_t = ScaleFunc(qscale, time)
|
|
581
|
+
T_f_in_derivs_t = ScaleFunc(T_f_in_derivs, time)
|
|
582
|
+
|
|
583
|
+
def ode(
|
|
584
|
+
y: np.ndarray, t: float, dt: float, g: AxiGrid, custom_pipe: CustomPipe
|
|
585
|
+
) -> np.ndarray:
|
|
586
|
+
"""
|
|
587
|
+
Compute dT/dt for the grid at time t.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
y : np.ndarray
|
|
592
|
+
Flattened temperature array of shape (nx*ny*nz).
|
|
593
|
+
t : float
|
|
594
|
+
Current simulation time.
|
|
595
|
+
dt : float
|
|
596
|
+
Timestep scaling.
|
|
597
|
+
g : AxiGrid
|
|
598
|
+
Axisymmetric grid.
|
|
599
|
+
custom_pipe : CustomPipe
|
|
600
|
+
Pipe configuration with flow and thermal resistance information.
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
np.ndarray
|
|
605
|
+
Flattened array of temperature derivatives (dT/dt * dt).
|
|
606
|
+
"""
|
|
607
|
+
# use npipes in ny direction to set the fluid fluxes. This is all arranged with the
|
|
608
|
+
s = y.reshape(g.nx, g.ny, g.nz)
|
|
609
|
+
dsdt = np.zeros_like(s)
|
|
610
|
+
|
|
611
|
+
nPipes = custom_pipe.nPipes
|
|
612
|
+
nInlets = custom_pipe.nInlets
|
|
613
|
+
|
|
614
|
+
qscale = qscale_t.get_scale(t)
|
|
615
|
+
Tf_der = T_f_in_derivs_t.get_scale(t)
|
|
616
|
+
|
|
617
|
+
for k in range(g.nz):
|
|
618
|
+
for i in range(g.nx):
|
|
619
|
+
# get for the depth segment the pipe temperatures
|
|
620
|
+
temppipe = s[i, 0:nPipes, :].reshape(nPipes)
|
|
621
|
+
|
|
622
|
+
# get the borewall temperature
|
|
623
|
+
temp_b = s[i, nPipes, 0]
|
|
624
|
+
# get the heatflow (negative is heat flow to borehole) for each of the pipes to borehole wall
|
|
625
|
+
if isinstance(custom_pipe, CoaxialPipe):
|
|
626
|
+
qpipe = temppipe * 0
|
|
627
|
+
qpipe[0] = (1.0 / custom_pipe._Rd[i][0][0]) * (temppipe[0] - temp_b)
|
|
628
|
+
else:
|
|
629
|
+
qpipe = custom_pipe.R1[i] @ (temppipe - np.ones(nPipes) * temp_b)
|
|
630
|
+
# get the
|
|
631
|
+
qsum = np.sum(qpipe)
|
|
632
|
+
# if (i == 5):
|
|
633
|
+
# # print("qsum", qsum)
|
|
634
|
+
|
|
635
|
+
for j in range(g.ny):
|
|
636
|
+
# dsdt[i][j][k] = 0.0
|
|
637
|
+
# in boundary condtion list?
|
|
638
|
+
|
|
639
|
+
# else do diffusion and fluxes
|
|
640
|
+
for ia in range(2):
|
|
641
|
+
ii = i + IFLUXF[ia]
|
|
642
|
+
jj = j + JFLUXF[ia]
|
|
643
|
+
kk = k
|
|
644
|
+
if isinstance(custom_pipe, CoaxialPipe):
|
|
645
|
+
usecpipe = (ia == 1) and (j == 0)
|
|
646
|
+
else:
|
|
647
|
+
usecpipe = (ia == 1) and (j < nPipes)
|
|
648
|
+
if usecpipe:
|
|
649
|
+
jj = nPipes
|
|
650
|
+
ok = g.checkindex(ii, jj, kk)
|
|
651
|
+
if ok:
|
|
652
|
+
dtemp = s[i][j][k] - s[ii][jj][kk]
|
|
653
|
+
# diffusion, for the pipe nodes g.txyz[1][i][j=0..npipe-1][k] should not be used instead ,
|
|
654
|
+
# handled with the heatflux condition from the cpipe R matrix
|
|
655
|
+
|
|
656
|
+
if usecpipe:
|
|
657
|
+
dsdt[i][j][k] -= g.overcp[i][j][k] * qpipe[j] * g.dx[i]
|
|
658
|
+
dsdt[ii][jj][kk] += (
|
|
659
|
+
g.overcp[ii][jj][kk] * qpipe[j] * g.dx[ii]
|
|
660
|
+
)
|
|
661
|
+
else:
|
|
662
|
+
dsdt[i][j][k] -= (
|
|
663
|
+
g.overcp[i][j][k] * g.txyz[ia][i][j][k] * dtemp
|
|
664
|
+
)
|
|
665
|
+
dsdt[ii][jj][kk] += (
|
|
666
|
+
g.overcp[ii][jj][kk] * g.txyz[ia][i][j][k] * dtemp
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if usecpipe and isinstance(custom_pipe, CoaxialPipe):
|
|
670
|
+
# j ==0
|
|
671
|
+
dtemp = s[i][j][k] - s[ii][1][kk]
|
|
672
|
+
dsdt[i][j][k] -= (
|
|
673
|
+
g.overcp[i][j][k] * g.txyz[ia][i][j][k] * dtemp
|
|
674
|
+
)
|
|
675
|
+
dsdt[ii][1][kk] += (
|
|
676
|
+
g.overcp[ii][1][kk] * g.txyz[ia][i][j][k] * dtemp
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# fluxes [m3/s] upwind scheme, this does not implement LAX-WENDROF
|
|
680
|
+
# these fluxes are used in the pipe nodes, and should be set
|
|
681
|
+
# in the correct way , i.e.
|
|
682
|
+
# - they correspond to downward (positive) and upward flowrates (negative) (ia==0)
|
|
683
|
+
# - the correponding overcp agrees with pipe fluid volume density and heat capacity
|
|
684
|
+
flux = g.fxyz[ia][i][j][k] * qscale
|
|
685
|
+
|
|
686
|
+
if (ia == 0) and (j < nInlets) and (i == g.nx - 2):
|
|
687
|
+
if j == 0:
|
|
688
|
+
enthsum = 0
|
|
689
|
+
cpsum = 0
|
|
690
|
+
for m in range(nInlets):
|
|
691
|
+
flux = g.fxyz[ia][i][m][k] * qscale
|
|
692
|
+
dtemp = s[i][m][k] - s[ii][m][k]
|
|
693
|
+
# for an inlet pipe the flux should always be >=0
|
|
694
|
+
if flux > 0:
|
|
695
|
+
enthsum += dtemp * flux * g.rcf[ii][m][k]
|
|
696
|
+
cpsum += 1 / g.overcp[ii][m][k]
|
|
697
|
+
if cpsum > 0:
|
|
698
|
+
for m in range(nInlets):
|
|
699
|
+
dsdt[ii][m][k] += enthsum / cpsum
|
|
700
|
+
else:
|
|
701
|
+
if flux > 0:
|
|
702
|
+
dsdt[ii][jj][kk] += (
|
|
703
|
+
dtemp
|
|
704
|
+
* flux
|
|
705
|
+
* g.rcf[ii][jj][kk]
|
|
706
|
+
* g.overcp[ii][jj][kk]
|
|
707
|
+
)
|
|
708
|
+
elif flux < 0:
|
|
709
|
+
dsdt[i][j][k] += (
|
|
710
|
+
dtemp
|
|
711
|
+
* flux
|
|
712
|
+
* g.rcf[i][j][k]
|
|
713
|
+
* g.overcp[i][j][k]
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# correct the bottom nodes
|
|
717
|
+
i = g.nx - 1
|
|
718
|
+
hsum = 0
|
|
719
|
+
cpsum = 0
|
|
720
|
+
for j in range(nPipes):
|
|
721
|
+
hsum += dsdt[i][j][0] / g.overcp[i][j][0]
|
|
722
|
+
cpsum += 1.0 / g.overcp[i][j][0]
|
|
723
|
+
for j in range(nPipes):
|
|
724
|
+
dsdt[i][j][0] = hsum / cpsum
|
|
725
|
+
|
|
726
|
+
# set the inlet temeratures to change according to the inpu derivatives
|
|
727
|
+
dsdt[0, 0:nInlets, 0] = Tf_der / dt
|
|
728
|
+
|
|
729
|
+
# overrule nodal changes which have fixed (dirichlet) boundary condition
|
|
730
|
+
dsdt *= g.dirichlet
|
|
731
|
+
dsdt_scaled = dsdt * dt
|
|
732
|
+
dydt = dsdt_scaled.reshape(g.nx * g.ny * g.nz)
|
|
733
|
+
|
|
734
|
+
return dydt
|
|
735
|
+
|
|
736
|
+
init_vals = grid.init_vals.reshape(grid.nx * grid.ny * grid.nz)
|
|
737
|
+
resultode = odeint(ode, init_vals, time, args=(dt, grid, b2g.custom_pipe))
|
|
738
|
+
|
|
739
|
+
return resultode.reshape(len(time), grid.nx, grid.ny, grid.nz)
|