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
geoloop/configuration.py
ADDED
|
@@ -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
|