geoloop 0.0.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. geoloop/axisym/AxisymetricEL.py +751 -0
  2. geoloop/axisym/__init__.py +3 -0
  3. geoloop/bin/Flowdatamain.py +89 -0
  4. geoloop/bin/Lithologymain.py +84 -0
  5. geoloop/bin/Loadprofilemain.py +100 -0
  6. geoloop/bin/Plotmain.py +250 -0
  7. geoloop/bin/Runbatch.py +81 -0
  8. geoloop/bin/Runmain.py +86 -0
  9. geoloop/bin/SingleRunSim.py +928 -0
  10. geoloop/bin/__init__.py +3 -0
  11. geoloop/cli/__init__.py +0 -0
  12. geoloop/cli/batch.py +106 -0
  13. geoloop/cli/main.py +105 -0
  14. geoloop/configuration.py +946 -0
  15. geoloop/constants.py +112 -0
  16. geoloop/geoloopcore/CoaxialPipe.py +503 -0
  17. geoloop/geoloopcore/CustomPipe.py +727 -0
  18. geoloop/geoloopcore/__init__.py +3 -0
  19. geoloop/geoloopcore/b2g.py +739 -0
  20. geoloop/geoloopcore/b2g_ana.py +516 -0
  21. geoloop/geoloopcore/boreholedesign.py +683 -0
  22. geoloop/geoloopcore/getloaddata.py +112 -0
  23. geoloop/geoloopcore/pyg_ana.py +280 -0
  24. geoloop/geoloopcore/pygfield_ana.py +519 -0
  25. geoloop/geoloopcore/simulationparameters.py +130 -0
  26. geoloop/geoloopcore/soilproperties.py +152 -0
  27. geoloop/geoloopcore/strat_interpolator.py +194 -0
  28. geoloop/lithology/__init__.py +3 -0
  29. geoloop/lithology/plot_lithology.py +277 -0
  30. geoloop/lithology/process_lithology.py +695 -0
  31. geoloop/loadflowdata/__init__.py +3 -0
  32. geoloop/loadflowdata/flow_data.py +161 -0
  33. geoloop/loadflowdata/loadprofile.py +325 -0
  34. geoloop/plotting/__init__.py +3 -0
  35. geoloop/plotting/create_plots.py +1142 -0
  36. geoloop/plotting/load_data.py +432 -0
  37. geoloop/utils/RunManager.py +164 -0
  38. geoloop/utils/__init__.py +0 -0
  39. geoloop/utils/helpers.py +841 -0
  40. geoloop-1.0.0.dist-info/METADATA +120 -0
  41. geoloop-1.0.0.dist-info/RECORD +46 -0
  42. geoloop-1.0.0.dist-info/entry_points.txt +2 -0
  43. geoloop-0.0.1.dist-info/licenses/LICENSE → geoloop-1.0.0.dist-info/licenses/LICENSE.md +2 -1
  44. geoloop-0.0.1.dist-info/METADATA +0 -10
  45. geoloop-0.0.1.dist-info/RECORD +0 -6
  46. {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/WHEEL +0 -0
  47. {geoloop-0.0.1.dist-info → geoloop-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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])