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,946 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, model_validator
6
+
7
+
8
+ class LithologyConfig(BaseModel):
9
+ """
10
+ Configuration object for the lithology module.
11
+
12
+ This class defines the input parameters required for processing borehole
13
+ lithology data, scaling lithology thermal properties, and generating
14
+ realizations used in BHE simulations.
15
+
16
+ Attributes
17
+ ----------
18
+ config_file_path : str or Path
19
+ Path to the JSON configuration file that created this object.
20
+ out_dir_lithology : str or Path
21
+ Directory where lithology outputs will be written.
22
+ borehole_lithology_path : str or Path
23
+ Path to the Excel or CSV file containing lithology data.
24
+ borehole_lithology_sheetname : str
25
+ Name of the sheet inside the Excel file that contains lithologic data.
26
+ out_table : str
27
+ Filename for the processed lithology table output.
28
+ read_from_table : bool
29
+ If True, bypass input processing and read from an existing table with thermal conductivity data.
30
+ Tg : int or list of int
31
+ Surface temperature if int, or subsurface temperature values over depth if list.
32
+ Tgrad : int
33
+ Geothermal gradient in °C/m.
34
+ z_Tg : int or list of int
35
+ Depths at which Tg values apply if list.
36
+ phi_scale : float
37
+ Scaling factor over depth for porosity.
38
+ lithology_scale : float
39
+ Depth scaling factor for lithology fractions.
40
+ lithology_error : float
41
+ Random noise scaling applied to lithology fractions.
42
+ basecase : bool
43
+ If True, disables stochasticity in subsurface properties and returns a deterministic profile.
44
+ n_samples : int
45
+ Number of stochastic realizations to generate.
46
+ """
47
+
48
+ config_file_path: str | Path
49
+ out_dir_lithology: str | Path
50
+ input_dir_lithology: str | Path
51
+ borehole_lithology_path: str | Path
52
+ borehole_lithology_sheetname: str
53
+ out_table: str
54
+ read_from_table: bool
55
+ Tg: int | list
56
+ Tgrad: int
57
+ z_Tg: int | list
58
+ phi_scale: float
59
+ lithology_scale: float
60
+ lithology_error: float
61
+ basecase: bool
62
+ n_samples: int
63
+
64
+ # Resolve paths
65
+ @model_validator(mode="after")
66
+ def process_config_paths(self):
67
+ """
68
+ Resolve relative paths in the configuration and ensure output directories exist.
69
+
70
+ Returns
71
+ -------
72
+ LithologyConfig
73
+ Model with absolute, validated paths.
74
+ """
75
+ if not isinstance(self.config_file_path, Path):
76
+ self.config_file_path = Path(self.config_file_path).resolve()
77
+
78
+ base_dir_lithology = self.config_file_path.parent
79
+
80
+ if not isinstance(self.borehole_lithology_path, Path):
81
+ self.borehole_lithology_path = Path(self.borehole_lithology_path)
82
+ if not self.borehole_lithology_path.is_absolute():
83
+ self.borehole_lithology_path = (
84
+ base_dir_lithology
85
+ / Path(self.input_dir_lithology)
86
+ / self.borehole_lithology_path
87
+ ).resolve()
88
+
89
+ if not isinstance(self.out_dir_lithology, Path):
90
+ self.out_dir_lithology = base_dir_lithology / Path(self.out_dir_lithology)
91
+
92
+ return self
93
+
94
+
95
+ class LoadProfileConfig(BaseModel):
96
+ """
97
+ Configuration object for the heat load profile module.
98
+
99
+ Handles input/output directories and parameters defining the thermal load
100
+ time-profile used in simulations.
101
+
102
+ Attributes
103
+ ----------
104
+ config_file_path : str or Path
105
+ Path to the main JSON configuration file.
106
+ lp_outdir : str or Path, optional
107
+ Directory for processed load-profile output.
108
+ lp_inputdir : str or Path, optional
109
+ Directory containing load-profile input files, if type is FROMFILE
110
+ lp_filename : str, optional
111
+ Name of the input file when type is FROMFILE.
112
+ lp_filepath : str or Path, optional
113
+ Resolved absolute path to the input file if type is FROMFILE.
114
+ lp_inputcolumn : str, optional
115
+ Column name to read from the input file, if type is FROMFILE. Default is heating_demand
116
+ lp_type : {"CONSTANT", "VARIABLE", "FROMFILE", "BERNIER"}
117
+ Type of heat load time-profile used.
118
+ lp_base : float or int, optional
119
+ Base value if type is VARIABLE, constant value if type is CONSTANT.
120
+ lp_peak : float or int, optional
121
+ Peak value if type is VARIABLE.
122
+ lp_minscaleflow : float or int, optional
123
+ Minimum scaling factor applied to flow if type is FROMFILE or VARIABLE.
124
+ lp_scale : float or int, optional
125
+ Scaling factor of the heat laod in the time-profile if type is FROMFILE.
126
+ lp_smoothing : str, optional
127
+ Smoothing factor applied to the heat load profile if type is FROMFILE.
128
+ lp_minQ : float or int, optional
129
+ Minimum heat load constraint if type is FROMFILE.
130
+ """
131
+
132
+ config_file_path: str | Path
133
+ lp_outdir: str | Path | None = None
134
+ lp_inputdir: str | Path | None = None
135
+
136
+ lp_filename: str | None = None
137
+ lp_filepath: str | Path | None = None
138
+ lp_inputcolumn: str | None = None
139
+
140
+ lp_type: Literal["CONSTANT", "VARIABLE", "FROMFILE", "BERNIER"]
141
+ lp_base: float | int | None = None
142
+ lp_peak: float | int | None = None
143
+ lp_minscaleflow: float | int | None = None
144
+ lp_scale: float | int | None = 1.0
145
+ lp_smoothing: str | None = None
146
+ lp_minQ: int | float | None = None
147
+
148
+ @model_validator(mode="after")
149
+ def validate_fields(self):
150
+ """
151
+ Validate required fields depending on the selected load-profile type.
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ If required attributes for the selected `lp_type` are missing.
157
+
158
+ Returns
159
+ -------
160
+ LoadProfileConfig
161
+ The validated configuration model.
162
+ """
163
+ # CONSTANT
164
+ if self.lp_type == "CONSTANT":
165
+ missing = []
166
+ if self.lp_base is None:
167
+ missing.append("lp_base")
168
+ if missing:
169
+ raise ValueError(
170
+ f"Missing required fields for lp_type='{self.lp_type}': {', '.join(missing)}"
171
+ )
172
+
173
+ # VARIABLE
174
+ if self.lp_type == "VARIABLE":
175
+ missing = []
176
+ if self.lp_base is None:
177
+ missing.append("lp_base")
178
+ if self.lp_peak is None:
179
+ missing.append("lp_peak")
180
+ if missing:
181
+ raise ValueError(
182
+ f"Missing required fields for lp_type='{self.lp_type}': {', '.join(missing)}"
183
+ )
184
+
185
+ # FROMFILE
186
+ elif self.lp_type == "FROMFILE":
187
+ required = {
188
+ "lp_filename": self.lp_filename,
189
+ "lp_inputdir": self.lp_inputdir,
190
+ "lp_scale": self.lp_scale,
191
+ "lp_minscaleflow": self.lp_minscaleflow,
192
+ "lp_minQ": self.lp_minQ,
193
+ }
194
+ missing = [k for k, v in required.items() if v is None]
195
+ if missing:
196
+ raise ValueError(
197
+ f"Missing required fields for lp_type='FROMFILE': {', '.join(missing)}"
198
+ )
199
+ return self
200
+
201
+ # Resolve paths
202
+ @model_validator(mode="after")
203
+ def process_config_paths(self):
204
+ """
205
+ Resolve relative paths into absolute paths and create directories as needed.
206
+
207
+ Updates `lp_outdir`, `lp_inputdir`, and `lp_filepath` based on the location
208
+ of the main configuration file.
209
+
210
+ Returns
211
+ -------
212
+ LoadProfileConfig
213
+ The updated instance with resolved paths.
214
+ """
215
+ if not isinstance(self.config_file_path, Path):
216
+ self.config_file_path = Path(self.config_file_path).resolve()
217
+
218
+ lp_base_dir = self.config_file_path.parent
219
+
220
+ if self.lp_outdir:
221
+ if not isinstance(self.lp_outdir, Path):
222
+ self.lp_outdir = lp_base_dir / Path(self.lp_outdir)
223
+ if not self.lp_outdir.exists():
224
+ self.lp_outdir.mkdir(parents=True, exist_ok=True)
225
+
226
+ if self.lp_inputdir:
227
+ if not isinstance(self.lp_inputdir, Path):
228
+ self.lp_inputdir = lp_base_dir / Path(self.lp_inputdir)
229
+ if not self.lp_inputdir.exists():
230
+ self.lp_inputdir.mkdir(parents=True, exist_ok=True)
231
+
232
+ if self.lp_filename:
233
+ self.lp_filepath = lp_base_dir / self.lp_inputdir / self.lp_filename
234
+
235
+ return self
236
+
237
+
238
+ class FlowDataConfig(BaseModel):
239
+ """
240
+ Configuration object for defining flow-rate profiles.
241
+
242
+ Supports constant, variable, and file-based flow definitions and resolves
243
+ directory paths automatically.
244
+
245
+ Attributes
246
+ ----------
247
+ config_file_path : str or Path
248
+ Path to the main configuration file.
249
+ fp_type : {"CONSTANT", "VARIABLE", "FROMFILE"}
250
+ Type of flow-rate time-profile.
251
+ fp_outdir : str or Path, optional
252
+ Output directory for processed flow data.
253
+ fp_inputdir : str or Path, optional
254
+ Input directory where flow data files reside, if type FROMFILE.
255
+ fp_filename : str, optional
256
+ Name of input file for file-based profiles if type FROMFILE.
257
+ fp_filepath : Path
258
+ Absolute file path for the input file after resolution if type FROMFILE.
259
+ fp_base : float, optional
260
+ Base flow value if type is VARIABLE.
261
+ fp_peak : float, optional
262
+ Peak flow value if type is VARIABLE, constant value if type is CONSTANT.
263
+ fp_scale : float, optional
264
+ Scaling factor for the flow profile if type is FROMFILE.
265
+ fp_inputcolumn : str, optional
266
+ Column name to read from the file, if type is FROMFILE.
267
+ fp_smoothing : str, optional
268
+ Smoothing method applied to the flow profile, if type is FROMFILE.
269
+ """
270
+
271
+ config_file_path: str | Path
272
+ fp_type: Literal["CONSTANT", "VARIABLE", "FROMFILE"]
273
+ fp_outdir: str | Path | None = None
274
+ fp_inputdir: str | Path | None = None
275
+
276
+ fp_filename: str | None = None
277
+ fp_filepath: str | Path = None
278
+
279
+ fp_base: float | None = None
280
+ fp_peak: float | None = None
281
+ fp_scale: float | None = 1.0
282
+ fp_inputcolumn: str | None = None
283
+ fp_smoothing: str | None = None
284
+
285
+ @model_validator(mode="after")
286
+ def validate_fields(self):
287
+ """
288
+ Ensure that required parameters are provided for the selected flow type.
289
+
290
+ Raises
291
+ ------
292
+ ValueError
293
+ If mandatory attributes for the chosen `fp_type` are missing.
294
+
295
+ Returns
296
+ -------
297
+ FlowDataConfig
298
+ The validated model instance.
299
+ """
300
+ # CONSTANT
301
+ if self.fp_type == "CONSTANT":
302
+ if self.fp_peak is None:
303
+ raise ValueError("fp_peak is required for fp_type='CONSTANT'")
304
+
305
+ # VARIABLE
306
+ elif self.fp_type == "VARIABLE":
307
+ missing = []
308
+ if self.fp_base is None:
309
+ missing.append("fp_base")
310
+ if self.fp_peak is None:
311
+ missing.append("fp_peak")
312
+ if missing:
313
+ raise ValueError(
314
+ f"Missing required fields for fp_type='VARIABLE': {', '.join(missing)}"
315
+ )
316
+
317
+ # FROMFILE
318
+ elif self.fp_type == "FROMFILE":
319
+ required = {
320
+ "fp_filename": self.fp_filename,
321
+ "fp_inputdir": self.fp_inputdir,
322
+ "fp_inputcolumn": self.fp_inputcolumn,
323
+ "fp_scale": self.fp_scale,
324
+ }
325
+ missing = [k for k, v in required.items() if v is None]
326
+ if missing:
327
+ raise ValueError(
328
+ f"Missing required fields for fp_type='FROMFILE': {', '.join(missing)}"
329
+ )
330
+ return self
331
+
332
+ @model_validator(mode="after")
333
+ def process_config_paths(self):
334
+ """
335
+ Resolve relative paths into absolute paths and create missing directories.
336
+
337
+ Updates the resolved file path and ensures the input/output directories exist.
338
+
339
+ Returns
340
+ -------
341
+ FlowDataConfig
342
+ """
343
+ if not isinstance(self.config_file_path, Path):
344
+ self.config_file_path = Path(self.config_file_path).resolve()
345
+
346
+ fp_base_dir = self.config_file_path.parent
347
+
348
+ if self.fp_outdir:
349
+ if not isinstance(self.fp_outdir, Path):
350
+ self.fp_outdir = fp_base_dir / Path(self.fp_outdir)
351
+ if not self.fp_outdir.exists():
352
+ self.fp_outdir.mkdir(parents=True, exist_ok=True)
353
+
354
+ if self.fp_inputdir:
355
+ if not isinstance(self.fp_inputdir, Path):
356
+ self.fp_inputdir = fp_base_dir / Path(self.fp_inputdir)
357
+ if not self.fp_inputdir.exists():
358
+ self.fp_inputdir.mkdir(parents=True, exist_ok=True)
359
+
360
+ if self.fp_filename:
361
+ self.fp_filepath = fp_base_dir / self.fp_inputdir / self.fp_filename
362
+
363
+ return self
364
+
365
+
366
+ class SingleRunConfig(BaseModel, extra="allow"):
367
+ """
368
+ Configuration object for a single geothermal borehole simulation run.
369
+
370
+ This model merges sub-configurations, validates borehole-type-dependent
371
+ parameters, numerical model requirements, and resolves base directories.
372
+
373
+ Attributes
374
+ ----------
375
+ config_file_path : Path
376
+ Path to the main configuration file.
377
+ base_dir : str or Path
378
+ Path to the output folder.
379
+ run_name : str, optional
380
+ Name of the simulation run.
381
+ type : {"UTUBE", "COAXIAL"}
382
+ Borehole heat-exchanger type.
383
+ H: float or int
384
+ Borehole length [m]
385
+ D: float or int
386
+ Buried depth [m].
387
+ r_b : float
388
+ Borehole radius [m].
389
+ r_out : list of float
390
+ Outer pipe radius [m].
391
+ k_p : float or int
392
+ Pipe thermal conductivity [W/mK].
393
+ k_g : float or int or list of float or int
394
+ Grout thermal conductivity (layered or uniform) [W/mK].
395
+ nsegments : int
396
+ Number of model segments along the borehole.
397
+ fluid_str : str
398
+ Fluid type. Must be included in the pygfunction.media.Fluid module.
399
+ fluid_percent : float or int
400
+ Mixture percentage for the fluid dissolved in water.
401
+ m_flow : float
402
+ Mass flow rate [kg/s].
403
+ epsilon : float
404
+ Pipe roughness [m].
405
+ r_in : list of float, optional
406
+ Inner pipe radius [m]. Used if SDR is None.
407
+ pos : list of list of float, optional
408
+ Pipe positions, x,y-coordinates in the borehole. Default `[[0,0][0,0]]` for type COAXIAL.
409
+ nInlets : int, optional
410
+ Number of inlet pipes. Default 1 for type COAXIAL.
411
+ SDR : float, optional
412
+ SDR index for pipe thickness. If None, then r_in is used.
413
+ insu_z : float, optional
414
+ Maximum depth of insulating pipe material [m].
415
+ insu_dr : float
416
+ Fraction of pipe wall thickness that is insulated.
417
+ insu_k : float
418
+ Thermal conductivity of insulation material [W/mK].
419
+ z_k_g : list of float, optional
420
+ Depth breakpoints corresponding to grout thermal conductivities. Used if k_g is list.
421
+ Tin : float or int
422
+ Inlet temperature [°C].
423
+ Q : float or int
424
+ Heat extraction/injection rate [W].
425
+ Tg : int or list of int
426
+ Surface temperature if int, or subsurface temperature values over depth if list.
427
+ Tgrad : int
428
+ Geothermal gradient in °C/m.
429
+ z_Tg : int or list of int, optional
430
+ Depths at which Tg values apply if list. If int or float then it is not used.
431
+ k_s : list of float
432
+ Subsurface bulk thermal conductivity layers [W/mK]. Used if litho_k_param is None.
433
+ z_k_s : list of float
434
+ Maximum depths for soil conductivity layers [m]. Used if litho_k_param is None.
435
+ alfa : float
436
+ Subsurface thermal diffusivity [m2/s].
437
+ k_s_scale : float
438
+ Scaling factor for k_s, uniform over depth.
439
+ model_type : {"FINVOL", "ANALYTICAL", "PYG", "PYGFIELD"}
440
+ Type of model used in simulation.
441
+ run_type : {"TIN", "POWER"}
442
+ Starting point for performance calculation (inlet temperature or heat load).
443
+ nyear: float or int
444
+ Nr. of simulated years [years].
445
+ nled : float or int
446
+ Number of hours per simulated timestep [hours].
447
+ nr : int, optional
448
+ Number of radial model nodes (if model_type FINVOL).
449
+ r_sim : float, optional
450
+ Simulation radial distance [m] (if model_type FINVOL).
451
+ save_Tfield_res : bool
452
+ Flag to save full 3D temperature field for every timestep, if model_type FINVOL.
453
+ dooptimize : bool
454
+ Flag to do optimization simulation.
455
+ optimize_keys : list of str, optional
456
+ Parameter names to optimize for.
457
+ optimize_keys_bounds : list of tuple, optional
458
+ Bounds for optimization variables.
459
+ copcrit : float or int, optinal,
460
+ Minimum COP of the fluid circulation pump. Used if dooptimize = true.
461
+ dploopcrit : float, optional
462
+ Pumping power. Adjusts flow rate accordingly.
463
+ borefield : dict, optional
464
+ Borefield sub-configuration. Required for borehole field simulation.
465
+ variables_config : dict, optional
466
+ Stochastic or optimization sub-configuration. Required for stochastic simulation.
467
+ litho_k_param : dict, optional
468
+ Lithology module sub-configuration.
469
+ loadprofile : dict, optional
470
+ Loadprofile module configuration.
471
+ flow_data : dict, optional
472
+ FlowData module configuration.
473
+ """
474
+
475
+ # Path to the configuration json
476
+ config_file_path: Path = None
477
+
478
+ # Base / paths
479
+ base_dir: str | Path
480
+ run_name: str | None = None
481
+
482
+ # Borehole design
483
+ # Required Parameters
484
+ type: Literal["UTUBE", "COAXIAL"]
485
+ H: float | int
486
+ D: float | int
487
+ r_b: float
488
+ r_out: list[float]
489
+
490
+ k_p: float
491
+ k_g: float | list[float]
492
+
493
+ fluid_str: Literal["water", "MEG", "MPG", "MEA", "MMA"]
494
+ fluid_percent: float | int
495
+ m_flow: float
496
+ epsilon: float
497
+
498
+ # Optional / Conditional for borehole
499
+ r_in: list[float] | None = None
500
+ pos: list[list[float]] = None
501
+ nInlets: int = None
502
+
503
+ SDR: int | float = None
504
+ insu_z: float | int = 0
505
+ insu_dr: float = 0.0
506
+ insu_k: float = 0.03
507
+ z_k_g: list[float] = None
508
+
509
+ Tin: float | int = 10
510
+ Q: float | int = 1000
511
+
512
+ # Subsurface temperature parameters
513
+ Tg: float | int | list
514
+ z_Tg: float | int | list[float] | list[int]
515
+ Tgrad: float
516
+
517
+ # thermal conductivity and thermal diffusivity parameters
518
+ k_s: list[float]
519
+ z_k_s: list[float]
520
+ alfa: float
521
+ k_s_scale: float
522
+
523
+ # model & run types
524
+ model_type: Literal["FINVOL", "ANALYTICAL", "PYG", "PYGFIELD"]
525
+ run_type: Literal["TIN", "POWER"]
526
+
527
+ # time parameters
528
+ nyear: float | int
529
+ nled: float | int
530
+
531
+ # borehole discretization
532
+ nsegments: int
533
+
534
+ # FINVOL-only parameters
535
+ nr: int | None = None
536
+ r_sim: float | None = None
537
+ save_Tfield_res: bool | None = False
538
+
539
+ # Optional optimization
540
+ dooptimize: bool = False
541
+ optimize_keys: list[str] | None = None
542
+ optimize_keys_bounds: list[tuple[float, float]] | None = None
543
+ copcrit: float | None = None
544
+ dploopcrit: float | None = None
545
+
546
+ # Optional linked config files with parameters for submodules
547
+ borefield: dict | None = None
548
+ field_N: int = 1
549
+ field_M: int = 1
550
+ field_R: int = 3
551
+ field_inclination_start: float | int = 0
552
+ field_inclination_end: float | int = 0
553
+ field_segments: int = 1
554
+
555
+ variables_config: dict | None = None
556
+ litho_k_param: dict | None = None
557
+ loadprofile: dict | None = None
558
+ flow_data: dict | None = None
559
+
560
+ @model_validator(mode="before")
561
+ def merge_borefield_and_cast_subdicts(cls, data):
562
+ """
563
+ Merge sub-configuration dictionaries into the root configuration.
564
+
565
+ This merges the `borefield` dictionary directly into the root namespace
566
+ and overwrites or adds parameters from `litho_k_param`.
567
+
568
+ Parameters
569
+ ----------
570
+ data : dict
571
+ Raw configuration dictionary input.
572
+
573
+ Returns
574
+ -------
575
+ dict
576
+ Updated configuration dictionary containing merged fields.
577
+ """
578
+ if not isinstance(data, dict):
579
+ return data
580
+
581
+ # merge borefield key into root
582
+ borefield = data.get("borefield")
583
+ if isinstance(borefield, dict):
584
+ # Borefield values override OR add missing optional fields
585
+ for k, v in borefield.items():
586
+ if k not in data or data[k] is None:
587
+ data[k] = v
588
+
589
+ # Adopt litho_k_param values in root
590
+ litho = data.get("litho_k_param")
591
+ if isinstance(litho, dict):
592
+ for k, v in litho.items():
593
+ if k == "config_file_path":
594
+ continue
595
+ # overwrite root if the key appears both in root and litho_k_param
596
+ if k in data:
597
+ data[k] = v
598
+
599
+ return data
600
+
601
+ # Type-dependent validation
602
+ @model_validator(mode="after")
603
+ def apply_type_logic(self):
604
+ """
605
+ Validate and apply logic depending on the borehole heat-exchanger type.
606
+
607
+ Raises
608
+ ------
609
+ ValueError
610
+ If required fields for UTUBE configurations are missing.
611
+
612
+ Returns
613
+ -------
614
+ SingleRunConfig
615
+ Updated model with defaults or validated fields.
616
+ """
617
+ if self.type == "UTUBE":
618
+ if self.pos is None:
619
+ raise ValueError("UTUBE requires 'pos'")
620
+ if self.nInlets is None:
621
+ raise ValueError("UTUBE requires 'nInlets'")
622
+
623
+ elif self.type == "COAXIAL":
624
+ # override with defaults
625
+ self.pos = [[0, 0], [0, 0]]
626
+ self.nInlets = 1
627
+
628
+ return self
629
+
630
+ @model_validator(mode="after")
631
+ def validate_finvol_fields(self):
632
+ """
633
+ Validate parameters required for FINVOL numerical model.
634
+
635
+ Raises
636
+ ------
637
+ ValueError
638
+ If `nr` or `r_sim` are missing when model_type='FINVOL'.
639
+
640
+ Returns
641
+ -------
642
+ SingleRunConfig
643
+ """
644
+ if self.model_type == "FINVOL":
645
+ # Ensure required fields are provided
646
+ if self.nr is None:
647
+ raise ValueError("nr must be provided when model_type='FINVOL'")
648
+ if self.r_sim is None:
649
+ raise ValueError("r_sim must be provided when model_type='FINVOL'")
650
+
651
+ return self
652
+
653
+ # Resolve paths
654
+ @model_validator(mode="after")
655
+ def process_config_paths(self):
656
+ """
657
+ Resolve the base directory path for the simulation run.
658
+
659
+ Ensures that relative paths are resolved using the config file location and
660
+ that the directory exists or is created.
661
+
662
+ Returns
663
+ -------
664
+ SingleRunConfig
665
+ """
666
+ base_dir_path = Path(self.base_dir)
667
+
668
+ if not base_dir_path.is_absolute():
669
+ if self.config_file_path is None:
670
+ raise ValueError(
671
+ "Cannot resolve relative base_dir: config_path not set"
672
+ )
673
+ elif isinstance(self.config_file_path, str):
674
+ self.config_file_path = Path(self.config_file_path)
675
+ base_dir_path = self.config_file_path.parent / base_dir_path
676
+
677
+ self.base_dir = base_dir_path.resolve()
678
+
679
+ if not self.base_dir.exists():
680
+ self.base_dir.mkdir(parents=True, exist_ok=True)
681
+
682
+ return self
683
+
684
+
685
+ class StochasticRunConfig(BaseModel):
686
+ """
687
+ Configuration object for defining stochastic or optimization variable distributions.
688
+
689
+ Each parameter is described by a tuple specifying the distribution type
690
+ and its numeric bounds.
691
+
692
+ Attributes
693
+ ----------
694
+ n_samples : int, optional
695
+ Number of Monte-Carlo samples.
696
+ k_s_scale, k_p, insu_z, insu_dr, insu_k, m_flow, Tin, H,
697
+ epsilon, alfa, Tgrad, Q, fluid_percent, r_out : tuple(str, float, float)
698
+ Triplets defining: [dist_type, dist1, dist2, (optional dist3)] where dist_type ∈
699
+ {"normal", "uniform", "lognormal", "triangular"}.
700
+
701
+ Notes
702
+ -----
703
+ if dist_name == "normal":
704
+ mean, std_dev = dist[1], dist[2]
705
+
706
+ elif dist_name == "uniform":
707
+ min, max = dist[1], dist[2]
708
+
709
+ elif dist_name == "lognormal":
710
+ mu, sigma = dist[1], dist[2]
711
+
712
+ elif dist_name == "triangular":
713
+ min, peak, max = dist[1], dist[2], dist[3]
714
+ """
715
+
716
+ n_samples: int | None = None
717
+ k_s_scale: tuple[str, float, float] = None
718
+ k_p: tuple[str, float, float] = None
719
+ insu_z: tuple[str, float, float] = None
720
+ insu_dr: tuple[str, float, float] = None
721
+ insu_k: tuple[str, float, float] = None
722
+ m_flow: tuple[str, float, float] = None
723
+ Tin: tuple[str, float, float] = None
724
+ H: tuple[str, float, float] = None
725
+ epsilon: tuple[str, float, float] = None
726
+ alfa: tuple[str, float, float] = None
727
+ Tgrad: tuple[str, float, float] = None
728
+ Q: tuple[str, float, float] = None
729
+ fluid_percent: tuple[str, float, float] = None
730
+ r_out: tuple[str, float, float] = None
731
+
732
+
733
+ class PlotInputConfig(BaseModel):
734
+ """
735
+ Configuration object for plotting simulation results.
736
+
737
+ Controls which simulations to plot, source and output directories, and which
738
+ quantities or layers to visualize.
739
+
740
+ Attributes
741
+ ----------
742
+ config_file_path : Path
743
+ Path to the main configuration file.
744
+ base_dir : str or Path
745
+ Directory where results are located that are plotted.
746
+ run_names : list of str
747
+ Names of simulation runs to include in plots.
748
+ model_types : list of str
749
+ List of model types (ANALYTICAL or FINVOL) corresponding to each simulation.
750
+ run_types : list of str
751
+ Run modes(TIN or POWER) for selected simulations.
752
+ run_modes : list of str
753
+ Type of simulations to plot (SR for single run or MC for stochastic run).
754
+ plot_names : list of str, optional
755
+ Name(s) of simulation(s) that is used in the legend of the plot(s).
756
+ plot_nPipes : list of int
757
+ Index of pipe for dataselection to plot.
758
+ plot_layer_k_s : list of int
759
+ Index of thermal conductivity layer for dataselection of input parameters to plot.
760
+ plot_layer_kg : list of int
761
+ Index of grout thermal conductivity layer for dataselection of input parameters to plot.
762
+ plot_layer_Tg : list of int
763
+ Index of subsurface temperature layer for dataselection of input parameters to plot.
764
+ plot_nz : list of int
765
+ Index of depth layer for dataselection of results to plot.
766
+ plot_ntime : list of int
767
+ Index of timestep for dataselection of results to plot.
768
+ plot_nzseg : list of int
769
+ Index of depth segment for dataselection of results to plot.
770
+ plot_times : list of float
771
+ Timesteps at which plots should be generated.
772
+ plot_time_depth : bool
773
+ Flag that determines whether to generate time– and depth-plots.
774
+ plot_time_parameters : list of str, optional
775
+ Time-dependant parameters to plot. Optional. Only used if plot_time_depth is true. Options: dploop; qloop; flowrate;
776
+ T_fi; T_fo; T_bave; Q_b; COP; Delta_T. With Delta_T = T_fo - T_fi and COP = Q_b/qloop
777
+ plot_depth_parameters : list of str, optional
778
+ Depth-dependant parameters to plot. Optional. Only used if plot_time_depth is true. Options: T_b;
779
+ Tg; T_f; Delta_T
780
+ plot_borehole_temp : list of int, optional
781
+ List of depth-segment slice indices to plot borehole temperature for.
782
+ Only used if plot_time_depth is true. index 0 always works
783
+ plot_crossplot_barplot : bool
784
+ Flag that determines whether to create crossplots and barplots.
785
+ Only compatible with stochastic simulations
786
+ newplot : bool
787
+ Flag to plot the simulation(s) listed in run_names seperately or together.
788
+ Only simulations with the same run_modes can be plot together
789
+ crossplot_vars : list of str
790
+ Variable input parameters and results to target in crossplots and tornado plots.
791
+ Only used if plot_crossplot_barplot is true
792
+ plot_temperature_field : bool
793
+ Flag that determines whether to plot full 3D temperature fields. Only used if model_type is FINVOL.
794
+ """
795
+
796
+ config_file_path: Path = None
797
+
798
+ base_dir: str | Path
799
+ run_names: list[str]
800
+ model_types: list[str]
801
+ run_types: list[str]
802
+ run_modes: list[str]
803
+
804
+ plot_names: list[str] = None
805
+ plot_nPipes: list[int]
806
+ plot_layer_k_s: list[int]
807
+ plot_layer_kg: list[int]
808
+ plot_layer_Tg: list[int]
809
+ plot_nz: list[int]
810
+ plot_ntime: list[int]
811
+ plot_nzseg: list[int]
812
+ plot_times: list[float]
813
+ plot_time_depth: bool
814
+ plot_time_parameters: list[str] | None = None
815
+ plot_depth_parameters: list[str] | None = None
816
+ plot_borehole_temp: list[int] | None = None
817
+ plot_crossplot_barplot: bool
818
+ newplot: bool
819
+ crossplot_vars: list[str]
820
+ plot_temperature_field: bool = False
821
+
822
+ @model_validator(mode="after")
823
+ def process_config_paths(self):
824
+ """
825
+ Resolve the base plotting directory.
826
+
827
+ Uses the config file path to resolve relative paths and ensures the
828
+ directory exists.
829
+
830
+ Returns
831
+ -------
832
+ PlotInputConfig
833
+ """
834
+ base_dir_path = Path(self.base_dir)
835
+
836
+ if not base_dir_path.is_absolute():
837
+ if self.config_file_path is None:
838
+ raise ValueError(
839
+ "Cannot resolve relative base_dir: config_path not set"
840
+ )
841
+ elif isinstance(self.config_file_path, str):
842
+ self.config_file_path = Path(self.config_file_path)
843
+ base_dir_path = self.config_file_path.parent / base_dir_path
844
+
845
+ self.base_dir = base_dir_path.resolve()
846
+
847
+ if not self.base_dir.exists():
848
+ self.base_dir.mkdir(parents=True, exist_ok=True)
849
+
850
+ return self
851
+
852
+
853
+ def load_json(path: str | Path) -> dict:
854
+ """
855
+ Open a JSON configuration file and load parameters into a dictionary.
856
+
857
+ Parameters
858
+ ----------
859
+ path: str | Path
860
+ Path to the JSON configuration file.
861
+
862
+ Returns
863
+ -------
864
+ dict
865
+ Dictionary with parameters and values defined in the JSON file.
866
+ """
867
+ with open(path) as f:
868
+ return json.load(f)
869
+
870
+
871
+ def load_single_config(main_config_path: str | Path) -> dict:
872
+ """
873
+ Load main JSON as dictionary.
874
+
875
+ Parameters
876
+ ----------
877
+ main_config_path : str
878
+ Path to the main JSON configuration file.
879
+
880
+ Returns
881
+ -------
882
+ dict
883
+ Configuration dictionary.
884
+ """
885
+ main_path = Path(main_config_path).resolve()
886
+
887
+ # Load top-level config file
888
+ config = load_json(main_path)
889
+
890
+ config["config_file_path"] = main_path
891
+
892
+ return config
893
+
894
+
895
+ def load_nested_config(
896
+ main_config_path: str, keys_needed: list[str] = [], keys_optional: list[str] = []
897
+ ) -> dict:
898
+ """
899
+ Load main JSON and inject referenced sub-configs as nested dictionaries.
900
+
901
+ Parameters
902
+ ----------
903
+ main_config_path : str
904
+ Path to the main JSON configuration file.
905
+ keys_needed : list[str]
906
+ Keys that must point to valid JSON files.
907
+ keys_optional : list[str]
908
+ Keys that may optionally point to JSON files.
909
+
910
+ Returns
911
+ -------
912
+ dict
913
+ Nested configuration dictionary.
914
+ """
915
+ main_path = Path(main_config_path).resolve()
916
+ base_dir = main_path.parent
917
+
918
+ # Load top-level config file
919
+ config = load_json(main_path)
920
+
921
+ config["config_file_path"] = main_path
922
+
923
+ # required sub-configs
924
+ for key in keys_needed:
925
+ if key not in config:
926
+ raise KeyError(f"Required subconfig key missing: {key}")
927
+
928
+ subpath = base_dir / config[key]
929
+ subconfig = load_json(subpath)
930
+ subconfig["config_file_path"] = subpath
931
+ # Replace file path with actual nested dictionary
932
+ config[key] = subconfig
933
+
934
+ # optional sub-configs
935
+ for key in keys_optional:
936
+ if key in config and config[key]:
937
+ subpath = base_dir / config[key]
938
+
939
+ try:
940
+ subconfig = load_json(subpath)
941
+ subconfig["config_file_path"] = subpath
942
+ config[key] = subconfig # replace path with nested dict
943
+ except FileNotFoundError:
944
+ print(f"Optional config file not found: {subpath}, skipping.")
945
+
946
+ return config