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,928 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pygfunction as gt
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
from scipy.optimize import minimize
|
|
11
|
+
|
|
12
|
+
from geoloop.configuration import LithologyConfig, SingleRunConfig, load_nested_config
|
|
13
|
+
from geoloop.geoloopcore.b2g import B2G
|
|
14
|
+
from geoloop.geoloopcore.b2g_ana import B2G_ana
|
|
15
|
+
from geoloop.geoloopcore.boreholedesign import BoreholeDesign
|
|
16
|
+
from geoloop.geoloopcore.pyg_ana import PYG_ana
|
|
17
|
+
from geoloop.geoloopcore.pygfield_ana import (
|
|
18
|
+
PYGFIELD_ana,
|
|
19
|
+
visualize_3d_borehole_field,
|
|
20
|
+
visualize_gfunc,
|
|
21
|
+
)
|
|
22
|
+
from geoloop.geoloopcore.simulationparameters import SimulationParameters
|
|
23
|
+
from geoloop.geoloopcore.soilproperties import SoilProperties
|
|
24
|
+
from geoloop.lithology.process_lithology import ProcessLithologyToThermalConductivity
|
|
25
|
+
from geoloop.utils.helpers import save_singlerun_results
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def optimize_forkeys(
|
|
29
|
+
config: dict,
|
|
30
|
+
copcrit: float,
|
|
31
|
+
optimize_keys: list[str],
|
|
32
|
+
optimize_bounds: list[tuple[float, float]],
|
|
33
|
+
isample: int,
|
|
34
|
+
) -> tuple[Any, dict]:
|
|
35
|
+
"""
|
|
36
|
+
Optimizes selected configuration parameters based on a specified COP criterion.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
config : dict
|
|
41
|
+
Configuration dictionary for a single sample, modified in-place.
|
|
42
|
+
copcrit : float
|
|
43
|
+
Target COP value used in the optimization objective function.
|
|
44
|
+
optimize_keys : list[str]
|
|
45
|
+
List of configuration keys to adjust during optimization and optimize for.
|
|
46
|
+
optimize_bounds : list[tuple[float, float]]
|
|
47
|
+
Upper and lower value bounds for each optimization key.
|
|
48
|
+
isample : int
|
|
49
|
+
Index of the sampled model run.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
tuple
|
|
54
|
+
First element is the optimizer result object (`scipy.optimize.OptimizeResult`).
|
|
55
|
+
Second element is the updated config dictionary.
|
|
56
|
+
"""
|
|
57
|
+
x0 = np.zeros(len(optimize_keys))
|
|
58
|
+
boundsval = []
|
|
59
|
+
for i, key in enumerate(optimize_keys):
|
|
60
|
+
boundsval.append((optimize_bounds[i][0], optimize_bounds[i][1]))
|
|
61
|
+
x0[i] = 0.5 * sum(boundsval[i])
|
|
62
|
+
|
|
63
|
+
method = "Nelder-Mead"
|
|
64
|
+
method = "Powell"
|
|
65
|
+
|
|
66
|
+
# ftol is tolerance for output power error of optimized point
|
|
67
|
+
result = minimize(
|
|
68
|
+
optimize_keys_func,
|
|
69
|
+
x0,
|
|
70
|
+
args=(optimize_keys, config, copcrit, isample),
|
|
71
|
+
bounds=boundsval,
|
|
72
|
+
method=method,
|
|
73
|
+
options={"maxiter": 50, "ftol": 100},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return result, config
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def optimize_keys_func(
|
|
80
|
+
x: NDArray[np.float64],
|
|
81
|
+
optimize_keys: list[str],
|
|
82
|
+
config: dict,
|
|
83
|
+
copcrit: float,
|
|
84
|
+
isample: int,
|
|
85
|
+
) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Objective function used for parameter optimization.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
x : np.ndarray
|
|
92
|
+
Current optimization parameter values.
|
|
93
|
+
optimize_keys : list[str]
|
|
94
|
+
List of configuration keys to optimize for and identifying which config fields to modify.
|
|
95
|
+
config : dict
|
|
96
|
+
Current configuration dictionary that is modified in-place.
|
|
97
|
+
copcrit : float
|
|
98
|
+
Target COP value used in the optimization objective function.
|
|
99
|
+
isample : int
|
|
100
|
+
Sample index for simulation.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
float
|
|
105
|
+
Value of the objective function (to be minimized).
|
|
106
|
+
"""
|
|
107
|
+
# Assign new values into config
|
|
108
|
+
for i, key in enumerate(optimize_keys):
|
|
109
|
+
if key == "r_out":
|
|
110
|
+
# if coaxial optimize the inner (last) radius
|
|
111
|
+
if config["type"] == "COAXIAL":
|
|
112
|
+
config[key][-1] = x[i]
|
|
113
|
+
else:
|
|
114
|
+
# scale all tubes with radius
|
|
115
|
+
config[key] = np.zeros(len(config[key])) + x[i]
|
|
116
|
+
else:
|
|
117
|
+
config[key] = x[i]
|
|
118
|
+
|
|
119
|
+
print(f"Optimizing '{key}': testing value {config[key]}")
|
|
120
|
+
|
|
121
|
+
# Create simulation instance and run
|
|
122
|
+
single_run = SingleRun.from_config(SingleRunConfig(**config))
|
|
123
|
+
result = single_run.run(isample)
|
|
124
|
+
|
|
125
|
+
qb = result.Q_b
|
|
126
|
+
cop = abs(qb / result.qloop)
|
|
127
|
+
dploop = result.dploop
|
|
128
|
+
|
|
129
|
+
dploopcrit = config.get("dploopcrit")
|
|
130
|
+
|
|
131
|
+
retval = Jfunc(qb, cop, copcrit, dploop, dploopcrit=dploopcrit)
|
|
132
|
+
|
|
133
|
+
return retval
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def Jfunc(
|
|
137
|
+
qb: NDArray[np.float64],
|
|
138
|
+
cop: NDArray[np.float64],
|
|
139
|
+
copcrit: float,
|
|
140
|
+
dploop: NDArray[np.float64],
|
|
141
|
+
dploopcrit: float | None = None,
|
|
142
|
+
) -> float:
|
|
143
|
+
"""
|
|
144
|
+
Objective function for optimizing heat yield, used to evaluate optimization result.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
qb : np.ndarray
|
|
149
|
+
Heat balance result array.
|
|
150
|
+
cop : np.ndarray
|
|
151
|
+
COP result array.
|
|
152
|
+
copcrit : float
|
|
153
|
+
COP threshold criterion.
|
|
154
|
+
dploop : np.ndarray
|
|
155
|
+
Pumping pressure values.
|
|
156
|
+
dploopcrit : float, optional
|
|
157
|
+
Pumping pressure limit.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
float
|
|
162
|
+
Objective value to minimize (negative of last qb result or penalty).
|
|
163
|
+
"""
|
|
164
|
+
retval = qb
|
|
165
|
+
|
|
166
|
+
# COP penalty
|
|
167
|
+
x = cop - copcrit
|
|
168
|
+
if x[-1] < 0:
|
|
169
|
+
retval = np.minimum(0, x * 1e3)
|
|
170
|
+
|
|
171
|
+
# Pumping pressure penalty
|
|
172
|
+
if dploopcrit is not None:
|
|
173
|
+
x = dploopcrit - dploop
|
|
174
|
+
if x[-1] < 0:
|
|
175
|
+
retval = np.minimum(0, np.minimum(retval, 0) + x * 1e3)
|
|
176
|
+
|
|
177
|
+
return -retval[-1]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SingleRunResult:
|
|
181
|
+
"""
|
|
182
|
+
Container class holding the results of a single BHE simulation run.
|
|
183
|
+
|
|
184
|
+
Stores time series, depth-dependent, and spatial thermal field output data.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
hours: np.ndarray,
|
|
190
|
+
Q_b: np.ndarray,
|
|
191
|
+
flowrate: np.ndarray,
|
|
192
|
+
qsign: np.ndarray,
|
|
193
|
+
T_fi: np.ndarray,
|
|
194
|
+
T_fo: np.ndarray,
|
|
195
|
+
T_bave: np.ndarray,
|
|
196
|
+
dploop: np.ndarray,
|
|
197
|
+
qloop: np.ndarray,
|
|
198
|
+
z: np.ndarray,
|
|
199
|
+
zseg: np.ndarray,
|
|
200
|
+
T_b: np.ndarray,
|
|
201
|
+
T_f: np.ndarray,
|
|
202
|
+
qzb: np.ndarray,
|
|
203
|
+
Re_in: np.ndarray,
|
|
204
|
+
Re_out: np.ndarray,
|
|
205
|
+
nPipes: int,
|
|
206
|
+
k_s: np.ndarray,
|
|
207
|
+
k_g: np.ndarray,
|
|
208
|
+
Tg: np.ndarray,
|
|
209
|
+
numerical_result: np.ndarray,
|
|
210
|
+
ag: object,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Initialize simulation result container.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
hours : np.ndarray
|
|
218
|
+
Time values of the simulation results [h].
|
|
219
|
+
Q_b : np.ndarray
|
|
220
|
+
Produced power over time [W].
|
|
221
|
+
flowrate : np.ndarray
|
|
222
|
+
Mass flow rate over time [kg/s].
|
|
223
|
+
qsign : np.ndarray
|
|
224
|
+
Sign of power production (-1 = extraction, +1 = injection).
|
|
225
|
+
T_fi : np.ndarray
|
|
226
|
+
Inlet fluid temperature over time [°C].
|
|
227
|
+
T_fo : np.ndarray
|
|
228
|
+
Outlet fluid temperature over time [°C].
|
|
229
|
+
T_bave : np.ndarray
|
|
230
|
+
Average borehole wall temperature over time [°C].
|
|
231
|
+
dploop : np.ndarray
|
|
232
|
+
Required pressure for loop pumping over time [bar].
|
|
233
|
+
qloop : np.ndarray
|
|
234
|
+
Power required to drive loop pumping over time [W].
|
|
235
|
+
z : np.ndarray
|
|
236
|
+
Node-based depth coordinates used for `T_f` output [m].
|
|
237
|
+
zseg : np.ndarray
|
|
238
|
+
Segment-based depth coordinates used for `T_b` and `qzb` [m].
|
|
239
|
+
T_b : np.ndarray
|
|
240
|
+
Borehole wall temperature as function of time and depth [°C].
|
|
241
|
+
Shape: (n_time, n_depth_segments)
|
|
242
|
+
T_f : np.ndarray
|
|
243
|
+
Fluid temperature as function of time, depth and pipe index [°C].
|
|
244
|
+
Shape: (n_time, n_depth_nodes, nPipes)
|
|
245
|
+
qzb : np.ndarray
|
|
246
|
+
Distributed heat flux at borehole wall [W/m].
|
|
247
|
+
Shape: (n_time, n_depth_segments)
|
|
248
|
+
Re_in : np.ndarray
|
|
249
|
+
Reynolds number in inlet pipes over time [-].
|
|
250
|
+
Re_out : np.ndarray
|
|
251
|
+
Reynolds number in outlet pipes over time [-].
|
|
252
|
+
nPipes : int
|
|
253
|
+
Number of pipes considered in the simulation [-].
|
|
254
|
+
k_s : np.ndarray
|
|
255
|
+
Depth-interpolated subsurface thermal conductivity [W/mK].
|
|
256
|
+
k_g : np.ndarray
|
|
257
|
+
Depth-interpolated grout thermal conductivity [W/mK].
|
|
258
|
+
Tg : np.ndarray
|
|
259
|
+
Depth-interpolated ground temperature [°C].
|
|
260
|
+
numerical_result : np.ndarray
|
|
261
|
+
Full 4D temperature field output from a BHE simulation with the numerical finite volume model.
|
|
262
|
+
Dimension order: (time, x, r, z)
|
|
263
|
+
ag : object
|
|
264
|
+
Axial grid object (grid properties for finite volume model).
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
self.hours = hours
|
|
268
|
+
self.Q_b = Q_b
|
|
269
|
+
self.flowrate = flowrate
|
|
270
|
+
self.qsign = qsign
|
|
271
|
+
self.T_fi = T_fi
|
|
272
|
+
self.T_fo = T_fo
|
|
273
|
+
self.T_bave = T_bave
|
|
274
|
+
self.dploop = dploop
|
|
275
|
+
self.qloop = qloop
|
|
276
|
+
self.z = z
|
|
277
|
+
self.zseg = zseg
|
|
278
|
+
self.T_b = T_b
|
|
279
|
+
self.T_f = T_f
|
|
280
|
+
self.qzb = qzb
|
|
281
|
+
self.Re_in = Re_in
|
|
282
|
+
self.Re_out = Re_out
|
|
283
|
+
self.nPipes = nPipes
|
|
284
|
+
self.k_s = k_s
|
|
285
|
+
self.k_g = k_g
|
|
286
|
+
self.Tg = Tg
|
|
287
|
+
self.numerical_result = numerical_result
|
|
288
|
+
self.ag = ag
|
|
289
|
+
|
|
290
|
+
def save_T_field_FINVOL(self, outpath: str | Path) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Save the full 4D temperature field to an HDF5/NetCDF file.
|
|
293
|
+
|
|
294
|
+
The output quantity corresponds to the finite-volume radial
|
|
295
|
+
temperature field stored in `self.numerical_result`.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
outpath : str or pathlib.Path
|
|
300
|
+
Base output file path (without `_FINVOL_T.h5` suffix).
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
None
|
|
305
|
+
"""
|
|
306
|
+
# Create and export DataArray for the temperature results in the radial field
|
|
307
|
+
time_coord = self.hours
|
|
308
|
+
x_coord = self.ag.x
|
|
309
|
+
r_coord = self.ag.axicellrmid[0].flatten()
|
|
310
|
+
|
|
311
|
+
result_da = xr.DataArray(
|
|
312
|
+
self.numerical_result,
|
|
313
|
+
dims=["time", "x", "r", "z"],
|
|
314
|
+
coords={"time": time_coord, "x": x_coord, "r": r_coord},
|
|
315
|
+
name="Temperature (Deg C)",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Export temperature field data
|
|
319
|
+
outpath = Path(outpath) # ensure it’s a Path
|
|
320
|
+
result_outpath = outpath.with_name(outpath.stem + "_FINVOL_T.h5")
|
|
321
|
+
|
|
322
|
+
result_da.to_netcdf(result_outpath, group="Temperature", engine="h5netcdf")
|
|
323
|
+
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
def get_n_pipes(self) -> int:
|
|
327
|
+
"""
|
|
328
|
+
Return the number of pipes used in the simulation.
|
|
329
|
+
|
|
330
|
+
Returns
|
|
331
|
+
-------
|
|
332
|
+
int
|
|
333
|
+
Number of pipes.
|
|
334
|
+
"""
|
|
335
|
+
return self.nPipes
|
|
336
|
+
|
|
337
|
+
def getzseg(self) -> np.ndarray:
|
|
338
|
+
"""
|
|
339
|
+
Return segment-based depth coordinates.
|
|
340
|
+
|
|
341
|
+
These correspond to the depth resolution used for results such as
|
|
342
|
+
borehole wall temperature (`T_b`) and borehole heat flux (`qzb`).
|
|
343
|
+
|
|
344
|
+
Returns
|
|
345
|
+
-------
|
|
346
|
+
np.ndarray
|
|
347
|
+
Depth values in meters, defined at segment mid-points.
|
|
348
|
+
"""
|
|
349
|
+
return self.zseg
|
|
350
|
+
|
|
351
|
+
def gethours(self) -> np.ndarray:
|
|
352
|
+
"""
|
|
353
|
+
Return the simulation time coordinate.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
np.ndarray
|
|
358
|
+
Time values in hours.
|
|
359
|
+
"""
|
|
360
|
+
return self.hours
|
|
361
|
+
|
|
362
|
+
def getz(self) -> np.ndarray:
|
|
363
|
+
"""
|
|
364
|
+
Return node-based depth coordinates.
|
|
365
|
+
|
|
366
|
+
These correspond to the depth resolution used for pipe fluid
|
|
367
|
+
temperature (`T_f`) results.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
np.ndarray
|
|
372
|
+
Depth values in meters, defined at nodal locations.
|
|
373
|
+
"""
|
|
374
|
+
return self.z
|
|
375
|
+
|
|
376
|
+
def getResultAttributesTimeseriesScalar(self) -> list[str]:
|
|
377
|
+
"""
|
|
378
|
+
Return a list of available scalar time-series result keys.
|
|
379
|
+
|
|
380
|
+
These correspond to simulation outputs varying only in time.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
list of str
|
|
385
|
+
Names of time-dependent scalar result variables.
|
|
386
|
+
"""
|
|
387
|
+
return [
|
|
388
|
+
"hours",
|
|
389
|
+
"Q_b",
|
|
390
|
+
"flowrate",
|
|
391
|
+
"qsign",
|
|
392
|
+
"T_fi",
|
|
393
|
+
"T_fo",
|
|
394
|
+
"T_bave",
|
|
395
|
+
"dploop",
|
|
396
|
+
"qloop",
|
|
397
|
+
"Re_in",
|
|
398
|
+
"Re_out",
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
def getResultAttributesTimeseriesDepthseg(self) -> list[str]:
|
|
402
|
+
"""
|
|
403
|
+
Return time-series attributes defined on depth segments.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
list of str
|
|
408
|
+
Variable names indexed by (time, depth segment).
|
|
409
|
+
"""
|
|
410
|
+
return ["T_b", "qzb"]
|
|
411
|
+
|
|
412
|
+
def getResultAttributesTimeserieDepth(self) -> list[str]:
|
|
413
|
+
"""
|
|
414
|
+
Return time-series attributes defined on depth nodes.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
list of str
|
|
419
|
+
Variable names indexed by (time, depth node).
|
|
420
|
+
"""
|
|
421
|
+
return ["T_f"]
|
|
422
|
+
|
|
423
|
+
def getResultAttributesDepthseg(self) -> list[str]:
|
|
424
|
+
"""
|
|
425
|
+
Return static (time-independent) depth-segment results.
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
list of str
|
|
430
|
+
Variable names indexed by depth segment.
|
|
431
|
+
"""
|
|
432
|
+
return ["k_s", "k_g", "Tg"]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class SingleRun:
|
|
436
|
+
"""
|
|
437
|
+
Class for parameters for running BHE model which consists of various compartments
|
|
438
|
+
which can be adapted, and facilitates in creating and updating related objects.
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
These include:
|
|
442
|
+
|
|
443
|
+
A. BoreholeDesign
|
|
444
|
+
- the borehole dimensions, and pipe configuration and dimensions
|
|
445
|
+
- the material properties and roughness of pipes
|
|
446
|
+
- the grout properties
|
|
447
|
+
- the working fluid properties
|
|
448
|
+
- pump efficiency
|
|
449
|
+
- borehole field parameters (only for madrid/curved borehole field)
|
|
450
|
+
- N number of boreholes (used to scale flow over each borehole)
|
|
451
|
+
- R radius of circular arranged boreholes(only for ilted borehole field)
|
|
452
|
+
|
|
453
|
+
B. SoilProperties
|
|
454
|
+
- surface temperature and gradient
|
|
455
|
+
- thermal properties of soil
|
|
456
|
+
|
|
457
|
+
C. OperationalParameters
|
|
458
|
+
- inlet temperature of power timeseries
|
|
459
|
+
- mass rate
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
def __init__(
|
|
463
|
+
self,
|
|
464
|
+
borehole_design: BoreholeDesign,
|
|
465
|
+
soil_properties: SoilProperties,
|
|
466
|
+
simulation_parameters: SimulationParameters,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Construct a SingleRun object.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
borehole_design : BoreholeDesign
|
|
474
|
+
Borehole, pipe, grout, and geometry configuration.
|
|
475
|
+
soil_properties : SoilProperties
|
|
476
|
+
Ground temperature and thermal properties.
|
|
477
|
+
simulation_parameters : SimulationParameters
|
|
478
|
+
Time-dependent operational parameters and solver configuration.
|
|
479
|
+
"""
|
|
480
|
+
self.bh_design = borehole_design
|
|
481
|
+
self.soil_props = soil_properties
|
|
482
|
+
self.sim_params = simulation_parameters
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def from_config(cls, config: SingleRunConfig) -> "SingleRun":
|
|
486
|
+
"""
|
|
487
|
+
Create a ``SingleRun`` instance from a configuration dictionary.
|
|
488
|
+
|
|
489
|
+
If ``dploopcrit`` is present in the configuration, the mass flow rate
|
|
490
|
+
is automatically scaled to match the allowed pumping pressure.
|
|
491
|
+
|
|
492
|
+
Parameters
|
|
493
|
+
----------
|
|
494
|
+
config : SingleRunConfig
|
|
495
|
+
Configuration object (typically loaded from a JSON file).
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
SingleRun
|
|
500
|
+
Configured instance of ``SingleRun``.
|
|
501
|
+
"""
|
|
502
|
+
borehole_design = BoreholeDesign.from_config(config)
|
|
503
|
+
soil_properties = SoilProperties.from_config(config)
|
|
504
|
+
simulation_parameters = SimulationParameters.from_config(config)
|
|
505
|
+
|
|
506
|
+
# if dploopcrit has been specified adjust the flowrate in based on the allowed pressure if necessary
|
|
507
|
+
# the mflow is set
|
|
508
|
+
if config.dploopcrit:
|
|
509
|
+
dploopcrit = config.dploopcrit
|
|
510
|
+
flowrate = simulation_parameters.m_flow
|
|
511
|
+
tempfluid = simulation_parameters.Tin[0]
|
|
512
|
+
|
|
513
|
+
mflowscale = borehole_design.findflowrate_dploop(
|
|
514
|
+
dploopcrit, tempfluid, flowrate, simulation_parameters.eff
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
simulation_parameters.m_flow *= mflowscale
|
|
518
|
+
|
|
519
|
+
single_run = cls(borehole_design, soil_properties, simulation_parameters)
|
|
520
|
+
|
|
521
|
+
return single_run
|
|
522
|
+
|
|
523
|
+
def run(self, isample: int) -> SingleRunResult | None:
|
|
524
|
+
"""
|
|
525
|
+
Run a single borehole heat exchanger (BHE) simulation for one sample.
|
|
526
|
+
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
isample : int
|
|
530
|
+
Index of the sample to run. Use ``-1`` for the base case.
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
SingleRunResult
|
|
535
|
+
Object containing the simulation results, including:
|
|
536
|
+
|
|
537
|
+
**Time-dependent outputs**
|
|
538
|
+
- hours : ndarray of shape (nt,)
|
|
539
|
+
Simulation time in hours.
|
|
540
|
+
- Q_b : ndarray of shape (nt,)
|
|
541
|
+
Extracted/injected borehole power [W].
|
|
542
|
+
- flowrate : ndarray of shape (nt,)
|
|
543
|
+
Mass flow rate [kg/s].
|
|
544
|
+
- qsign : ndarray of shape (nt,)
|
|
545
|
+
Sign of heat extraction/injection.
|
|
546
|
+
- T_fi : ndarray of shape (nt,)
|
|
547
|
+
Inlet fluid temperature [°C].
|
|
548
|
+
- T_fo : ndarray of shape (nt,)
|
|
549
|
+
Outlet fluid temperature [°C].
|
|
550
|
+
- T_bave : ndarray of shape (nt,)
|
|
551
|
+
Average borehole wall temperature [°C].
|
|
552
|
+
- dploop : ndarray of shape (nt,)
|
|
553
|
+
Pressure drop across the loop [bar].
|
|
554
|
+
- qloop : ndarray of shape (nt,)
|
|
555
|
+
Pumping power [W].
|
|
556
|
+
- Re_in, Re_out : ndarray of shape (nt,)
|
|
557
|
+
Reynolds numbers in inlet/outlet pipes.
|
|
558
|
+
|
|
559
|
+
**Depth-dependent outputs**
|
|
560
|
+
- z : ndarray of shape (nz,)
|
|
561
|
+
Depth coordinates for fluid temperatures.
|
|
562
|
+
- zseg : ndarray of shape (nseg,)
|
|
563
|
+
Depth segment coordinates for wall/grout quantities.
|
|
564
|
+
- T_b : ndarray of shape (nt, nseg)
|
|
565
|
+
Borehole wall temperature [°C].
|
|
566
|
+
- T_f : ndarray of shape (nt, npipes, nz)
|
|
567
|
+
Fluid temperature in pipes [°C].
|
|
568
|
+
- qzb : ndarray of shape (nt, nseg)
|
|
569
|
+
Heat flux along the borehole wall [W/m].
|
|
570
|
+
- k_s : ndarray of shape (nseg,)
|
|
571
|
+
Soil conductivity interpolated over segments.
|
|
572
|
+
- k_g : ndarray of shape (nseg,)
|
|
573
|
+
Grout conductivity over segments.
|
|
574
|
+
- Tg : ndarray of shape (nseg,)
|
|
575
|
+
Undisturbed ground temperature [°C].
|
|
576
|
+
|
|
577
|
+
Notes
|
|
578
|
+
-----
|
|
579
|
+
The executed simulation model is determined by ``operPar.model_type``.
|
|
580
|
+
Supported models include:
|
|
581
|
+
``FINVOL``, ``ANALYTICAL``, ``PYG``, and ``PYGFIELD``.
|
|
582
|
+
"""
|
|
583
|
+
# Local shortcuts for readability
|
|
584
|
+
bh_design = self.bh_design
|
|
585
|
+
sim_params = self.sim_params
|
|
586
|
+
soil_props = self.soil_props
|
|
587
|
+
|
|
588
|
+
# Set sample index and flow conditions
|
|
589
|
+
bh_design.m_flow = sim_params.m_flow[0]
|
|
590
|
+
sim_params.isample = isample
|
|
591
|
+
|
|
592
|
+
# Custom pipe configuration
|
|
593
|
+
bh_design.customPipe = bh_design.get_custom_pipe()
|
|
594
|
+
custom_pipe = bh_design.customPipe
|
|
595
|
+
|
|
596
|
+
ag = None
|
|
597
|
+
result = None
|
|
598
|
+
|
|
599
|
+
# Model selection
|
|
600
|
+
if (sim_params.run_type == SimulationParameters.TIN) and (
|
|
601
|
+
sim_params.model_type == SimulationParameters.FINVOL
|
|
602
|
+
):
|
|
603
|
+
b2g = B2G(custom_pipe)
|
|
604
|
+
|
|
605
|
+
(
|
|
606
|
+
hours,
|
|
607
|
+
Q_b,
|
|
608
|
+
flowrate,
|
|
609
|
+
qsign,
|
|
610
|
+
T_fi,
|
|
611
|
+
T_fo,
|
|
612
|
+
T_bave,
|
|
613
|
+
z,
|
|
614
|
+
T_b,
|
|
615
|
+
T_f,
|
|
616
|
+
qzb,
|
|
617
|
+
h_fpipes,
|
|
618
|
+
result,
|
|
619
|
+
zstart,
|
|
620
|
+
zend,
|
|
621
|
+
) = b2g.runsimulation(bh_design, soil_props, sim_params)
|
|
622
|
+
|
|
623
|
+
zseg = z
|
|
624
|
+
ag = b2g.ag
|
|
625
|
+
|
|
626
|
+
# calculate interpolated k_s, k_g, Tg
|
|
627
|
+
k_s = soil_props.get_k_s(zstart, zend, sim_params.isample)
|
|
628
|
+
k_g = bh_design.get_k_g(zstart, zend)
|
|
629
|
+
Tg = soil_props.getTg(zseg)
|
|
630
|
+
|
|
631
|
+
elif sim_params.model_type == SimulationParameters.ANALYTICAL:
|
|
632
|
+
b2g_ana = B2G_ana(custom_pipe, soil_props, sim_params)
|
|
633
|
+
|
|
634
|
+
(
|
|
635
|
+
hours,
|
|
636
|
+
Q_b,
|
|
637
|
+
flowrate,
|
|
638
|
+
qsign,
|
|
639
|
+
T_fi,
|
|
640
|
+
T_fo,
|
|
641
|
+
T_bave,
|
|
642
|
+
z,
|
|
643
|
+
zseg,
|
|
644
|
+
T_b,
|
|
645
|
+
T_f,
|
|
646
|
+
qzb,
|
|
647
|
+
h_fpipes,
|
|
648
|
+
) = b2g_ana.runsimulation()
|
|
649
|
+
|
|
650
|
+
# calculate interpolated k_s, k_g, Tg
|
|
651
|
+
zz = np.linspace(
|
|
652
|
+
custom_pipe.b.D,
|
|
653
|
+
custom_pipe.b.D + custom_pipe.b.H,
|
|
654
|
+
sim_params.nsegments + 1,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
k_s = soil_props.get_k_s(zz[0:-1], zz[1:], sim_params.isample)
|
|
658
|
+
k_g = bh_design.get_k_g(zz[0:-1], zz[1:])
|
|
659
|
+
Tg = soil_props.getTg(zseg)
|
|
660
|
+
|
|
661
|
+
elif sim_params.model_type == SimulationParameters.PYG:
|
|
662
|
+
pyg_ana = PYG_ana(custom_pipe, soil_props, sim_params)
|
|
663
|
+
|
|
664
|
+
(
|
|
665
|
+
hours,
|
|
666
|
+
Q_b,
|
|
667
|
+
flowrate,
|
|
668
|
+
qsign,
|
|
669
|
+
T_fi,
|
|
670
|
+
T_fo,
|
|
671
|
+
T_bave,
|
|
672
|
+
z,
|
|
673
|
+
zseg,
|
|
674
|
+
T_b,
|
|
675
|
+
T_f,
|
|
676
|
+
qzb,
|
|
677
|
+
h_fpipes,
|
|
678
|
+
) = pyg_ana.runsimulation()
|
|
679
|
+
|
|
680
|
+
zz = np.linspace(
|
|
681
|
+
custom_pipe.b.D,
|
|
682
|
+
custom_pipe.b.D + custom_pipe.b.H,
|
|
683
|
+
sim_params.nsegments + 1,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
k_s = soil_props.get_k_s(zz[0:-1], zz[1:], sim_params.isample)
|
|
687
|
+
k_g = bh_design.get_k_g(zz[0:-1], zz[1:])
|
|
688
|
+
Tg = soil_props.getTg(zseg)
|
|
689
|
+
|
|
690
|
+
elif sim_params.model_type == SimulationParameters.PYGFIELD:
|
|
691
|
+
pygfield_ana = PYGFIELD_ana(bh_design, custom_pipe, soil_props, sim_params)
|
|
692
|
+
self.pygfield_ana = pygfield_ana
|
|
693
|
+
|
|
694
|
+
(
|
|
695
|
+
hours,
|
|
696
|
+
Q_b,
|
|
697
|
+
flowrate,
|
|
698
|
+
qsign,
|
|
699
|
+
T_fi,
|
|
700
|
+
T_fo,
|
|
701
|
+
T_bave,
|
|
702
|
+
z,
|
|
703
|
+
zseg,
|
|
704
|
+
T_b,
|
|
705
|
+
T_f,
|
|
706
|
+
qzb,
|
|
707
|
+
h_fpipes,
|
|
708
|
+
) = pygfield_ana.runsimulation()
|
|
709
|
+
|
|
710
|
+
zz = np.linspace(
|
|
711
|
+
custom_pipe.b.D,
|
|
712
|
+
custom_pipe.b.D + custom_pipe.b.H,
|
|
713
|
+
sim_params.nsegments + 1,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
k_s = soil_props.get_k_s(zz[0:-1], zz[1:], sim_params.isample)
|
|
717
|
+
k_g = bh_design.get_k_g(zz[0:-1], zz[1:])
|
|
718
|
+
Tg = soil_props.getTg(zseg)
|
|
719
|
+
|
|
720
|
+
else:
|
|
721
|
+
sim_params.modelsupported() # raises error
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
# calculate the pumping pressure [bar]
|
|
725
|
+
dploop, qloop = bh_design.calculate_dploop(T_f, z, flowrate, sim_params.eff)
|
|
726
|
+
|
|
727
|
+
# build result object
|
|
728
|
+
self.singlerun_result = SingleRunResult(
|
|
729
|
+
hours,
|
|
730
|
+
Q_b,
|
|
731
|
+
flowrate,
|
|
732
|
+
qsign,
|
|
733
|
+
T_fi,
|
|
734
|
+
T_fo,
|
|
735
|
+
T_bave,
|
|
736
|
+
dploop,
|
|
737
|
+
qloop,
|
|
738
|
+
z,
|
|
739
|
+
zseg,
|
|
740
|
+
T_b,
|
|
741
|
+
T_f,
|
|
742
|
+
qzb,
|
|
743
|
+
bh_design.Re_in,
|
|
744
|
+
bh_design.Re_out,
|
|
745
|
+
(custom_pipe.nInlets + custom_pipe.nOutlets),
|
|
746
|
+
k_s,
|
|
747
|
+
k_g,
|
|
748
|
+
Tg,
|
|
749
|
+
result,
|
|
750
|
+
ag,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return self.singlerun_result
|
|
754
|
+
|
|
755
|
+
def visualize_pipes(self, filename: str | Path) -> None:
|
|
756
|
+
"""
|
|
757
|
+
Visualize a cross-section of the borehole with the pipe configuration.
|
|
758
|
+
|
|
759
|
+
Parameters
|
|
760
|
+
----------
|
|
761
|
+
filename : str or Path
|
|
762
|
+
Full file path (including `.png` extension) for saving the figure.
|
|
763
|
+
|
|
764
|
+
Returns
|
|
765
|
+
-------
|
|
766
|
+
None
|
|
767
|
+
The function saves the visualization to disk.
|
|
768
|
+
"""
|
|
769
|
+
self.bh_design.visualize_pipes(filename)
|
|
770
|
+
|
|
771
|
+
def plot_borefield_and_gfunc(self, filepath: str | Path) -> None:
|
|
772
|
+
"""
|
|
773
|
+
Plot a 3D visualization of the borefield layout and its associated
|
|
774
|
+
g-function, if the pygfield model is used in the simulation.
|
|
775
|
+
|
|
776
|
+
Parameters
|
|
777
|
+
----------
|
|
778
|
+
filepath : str or pathlib.Path
|
|
779
|
+
Base output path. The function appends suffixes such as
|
|
780
|
+
`'_borefield.png'` and `'_gfunction.png'`.
|
|
781
|
+
|
|
782
|
+
Returns
|
|
783
|
+
-------
|
|
784
|
+
None
|
|
785
|
+
The function saves the borefield and g-function visualizations.
|
|
786
|
+
"""
|
|
787
|
+
if hasattr(self, "pygfield_ana"):
|
|
788
|
+
# for curved boreholes use custom plotting method
|
|
789
|
+
if isinstance(self.pygfield_ana.borefield, list):
|
|
790
|
+
plt = visualize_3d_borehole_field(self.pygfield_ana.borefield)
|
|
791
|
+
|
|
792
|
+
# for rectangular or circular straight boreholes use pygfunction plotting method
|
|
793
|
+
else:
|
|
794
|
+
plt = gt.borefield.Borefield.visualize_field(
|
|
795
|
+
self.pygfield_ana.borefield
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
filename = filepath.with_name(filepath.name + "_borefield.png")
|
|
799
|
+
plt.savefig(filename)
|
|
800
|
+
|
|
801
|
+
# plot g-function for borehole field
|
|
802
|
+
plt = visualize_gfunc(self.pygfield_ana.gfunc)
|
|
803
|
+
filename = filepath.with_name(filepath.name + "_gfunction.png")
|
|
804
|
+
plt.savefig(filename)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def main_single_run_sim(config_file_path: Path | str) -> None:
|
|
808
|
+
"""
|
|
809
|
+
Execute a complete SingleRun borehole heat exchanger (BHE) simulation,
|
|
810
|
+
including optional optimization, result saving, and visualization.
|
|
811
|
+
|
|
812
|
+
Parameters
|
|
813
|
+
----------
|
|
814
|
+
config_file_path: str | Path
|
|
815
|
+
The path to the `.json` configuration file.
|
|
816
|
+
|
|
817
|
+
Workflow
|
|
818
|
+
--------
|
|
819
|
+
1. Parse and load the configuration file.
|
|
820
|
+
2. Apply optional lithology-to-conductivity preprocessing.
|
|
821
|
+
3. Optionally perform flow-rate or parameter optimization.
|
|
822
|
+
4. Initialize a ``SingleRun`` simulation from the config.
|
|
823
|
+
5. Execute the simulation.
|
|
824
|
+
6. Save results into structured output directories.
|
|
825
|
+
7. Generate visualizations:
|
|
826
|
+
- Pipe cross-section
|
|
827
|
+
- Borefield layout (for PYGFIELD)
|
|
828
|
+
- Optional T-field (for FINVOL)
|
|
829
|
+
|
|
830
|
+
Returns
|
|
831
|
+
-------
|
|
832
|
+
None
|
|
833
|
+
The function writes outputs to disk and prints status information.
|
|
834
|
+
"""
|
|
835
|
+
# Start time
|
|
836
|
+
start_time = time.time()
|
|
837
|
+
np.seterr(all="raise")
|
|
838
|
+
|
|
839
|
+
# Load configuration
|
|
840
|
+
keysneeded = []
|
|
841
|
+
keysoptional = [
|
|
842
|
+
"litho_k_param",
|
|
843
|
+
"loadprofile",
|
|
844
|
+
"borefield",
|
|
845
|
+
"flow_data",
|
|
846
|
+
"variables_config",
|
|
847
|
+
]
|
|
848
|
+
config_dict = load_nested_config(config_file_path, keysneeded, keysoptional)
|
|
849
|
+
|
|
850
|
+
config = SingleRunConfig(**config_dict) # validated Pydantic object
|
|
851
|
+
|
|
852
|
+
config.lithology_to_k = None
|
|
853
|
+
# lithology to conductivity (optional)
|
|
854
|
+
if config.litho_k_param:
|
|
855
|
+
# in a single run always set the base case to True
|
|
856
|
+
config.litho_k_param["basecase"] = True
|
|
857
|
+
lithology_to_k = ProcessLithologyToThermalConductivity.from_config(
|
|
858
|
+
LithologyConfig(**config.litho_k_param)
|
|
859
|
+
)
|
|
860
|
+
lithology_to_k.create_multi_thermcon_profiles()
|
|
861
|
+
config.lithology_to_k = lithology_to_k
|
|
862
|
+
|
|
863
|
+
# Optional optimization
|
|
864
|
+
isample = -1
|
|
865
|
+
|
|
866
|
+
if config.dooptimize:
|
|
867
|
+
config_dump = config.model_dump()
|
|
868
|
+
|
|
869
|
+
cop_crit = config.copcrit
|
|
870
|
+
optimize_keys = config.optimize_keys
|
|
871
|
+
optimize_bounds = config.optimize_keys_bounds
|
|
872
|
+
|
|
873
|
+
print(f"Optimizing flow rate for COP: {cop_crit}")
|
|
874
|
+
if config.dploopcrit:
|
|
875
|
+
print(f"Maximum pressure constraint: {config.dploopcrit}")
|
|
876
|
+
print(f"Optimizing keys: {optimize_keys}")
|
|
877
|
+
print(f"Bounds: {optimize_bounds}")
|
|
878
|
+
|
|
879
|
+
_, config_new = optimize_forkeys(
|
|
880
|
+
config_dump, cop_crit, optimize_keys, optimize_bounds, isample
|
|
881
|
+
)
|
|
882
|
+
config = SingleRunConfig(**config_new)
|
|
883
|
+
|
|
884
|
+
# run simulation
|
|
885
|
+
single_run = SingleRun.from_config(config)
|
|
886
|
+
result = single_run.run(isample)
|
|
887
|
+
|
|
888
|
+
# output directory setup
|
|
889
|
+
if config.run_name:
|
|
890
|
+
runfolder = config.run_name
|
|
891
|
+
else:
|
|
892
|
+
runfolder = Path(config_file_path).stem
|
|
893
|
+
config.run_name = runfolder
|
|
894
|
+
|
|
895
|
+
out_dir = (
|
|
896
|
+
config.base_dir / runfolder
|
|
897
|
+
) # Check if the runfolder already exists, and create it if not
|
|
898
|
+
out_dir.mkdir(parents=True, exist_ok=True) # creates all missing directories
|
|
899
|
+
|
|
900
|
+
basename = f"{runfolder}_{config.model_type[0]}_{config.run_type[0]}"
|
|
901
|
+
outpath = out_dir / basename
|
|
902
|
+
|
|
903
|
+
# Save results
|
|
904
|
+
save_singlerun_results(config, result, outpath)
|
|
905
|
+
|
|
906
|
+
if config.model_type == "FINVOL":
|
|
907
|
+
save_Tfield_res = config.save_Tfield_res
|
|
908
|
+
if save_Tfield_res:
|
|
909
|
+
if result.ag is not None:
|
|
910
|
+
# Only save Tfield results if FINVOL model is used, so ag != None
|
|
911
|
+
result.save_T_field_FINVOL(outpath)
|
|
912
|
+
|
|
913
|
+
# visualizations
|
|
914
|
+
pipe_image = out_dir / f"{basename}_bhdesign.png"
|
|
915
|
+
single_run.visualize_pipes(pipe_image)
|
|
916
|
+
|
|
917
|
+
if config.model_type == "PYGFIELD":
|
|
918
|
+
single_run.plot_borefield_and_gfunc(outpath)
|
|
919
|
+
|
|
920
|
+
# wrap up
|
|
921
|
+
print("Calculation complete")
|
|
922
|
+
|
|
923
|
+
elapsed = time.time() - start_time
|
|
924
|
+
print(f"Elapsed time: {elapsed:.2f} seconds")
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
if __name__ == "__main__":
|
|
928
|
+
main_single_run_sim(sys.argv[1])
|