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,695 @@
1
+ import math
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import xarray as xr
7
+
8
+ from geoloop.configuration import LithologyConfig
9
+ from geoloop.constants import lithology_properties_xlsx
10
+ from geoloop.geoloopcore.strat_interpolator import TgInterpolator
11
+
12
+
13
+ class ThermalConductivityCalculator:
14
+ """
15
+ A class to calculate subsurface thermal conductivity and porosity based on lithological and thermal parameters.
16
+
17
+ Attributes
18
+ ----------
19
+ phi0 : float
20
+ Porosity at the surface.
21
+ kv_phi0_20 : float
22
+ Thermal conductivity at 20°C (W/mK).
23
+ sorting_factor : float
24
+ Sorting factor, describing the degree of sorting in sediments.
25
+ anisotropy : float
26
+ Anisotropy factor, describing the anisotropy in sediments.
27
+ c_p : float
28
+ Specific heat capacity (J/kgK).
29
+ rho : float
30
+ Density (kg/m³).
31
+ k_athy : float
32
+ Compaction constant, used in the porosity-depth relation.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ phi0: float,
38
+ kv_phi0_20: float,
39
+ sorting_factor: float,
40
+ anisotropy: float,
41
+ c_p: float,
42
+ rho: float,
43
+ k_athy: float,
44
+ ) -> None:
45
+ self.phi0 = phi0
46
+ self.kv_phi0_20 = kv_phi0_20
47
+ self.sorting_factor = sorting_factor
48
+ self.anisotropy = anisotropy
49
+ self.c_p = c_p
50
+ self.rho = rho
51
+ self.k_athy = k_athy
52
+
53
+ def calculate_porosity(self, depth: float) -> float:
54
+ """
55
+ Calculates porosity at a given depth using Athy's exponential compaction model.
56
+
57
+ Parameters
58
+ ----------
59
+ depth : float
60
+ Depth in meters.
61
+
62
+ Returns
63
+ -------
64
+ float
65
+ Porosity (fraction between 0 and 1).
66
+ """
67
+ phi_base = 0
68
+ phi = phi_base + (self.phi0 - phi_base) * math.exp(-self.k_athy * depth * 0.001)
69
+
70
+ return phi
71
+
72
+ def calculate_k_compaction_correction(self, phi: float) -> float:
73
+ """
74
+ Apply a compaction correction to the reference vertical thermal conductivity at phi 0.
75
+
76
+ Parameters
77
+ ----------
78
+ phi : float
79
+ Porosity (fraction).
80
+
81
+ Returns
82
+ -------
83
+ float
84
+ Corrected vertical thermal conductivity at 20°C (W/m·K).
85
+ """
86
+
87
+ phi_factor = phi / self.phi0
88
+ kv_phi_20 = self.kv_phi0_20 * math.pow(self.sorting_factor, phi_factor)
89
+
90
+ return kv_phi_20
91
+
92
+ def calculate_kh_rock_matrix(
93
+ self, temperature_top: float, temperature_base: float, phi: float
94
+ ) -> float:
95
+ """
96
+ Estimate the horizontal rock-matrix thermal conductivity (dimensionless kh_matrix)
97
+ using an empirical formula that depends on compaction-corrected conductivities
98
+ and temperature.
99
+
100
+ Parameters
101
+ ----------
102
+ temperature_top : float
103
+ Temperature at the top of the segment (°C).
104
+ temperature_base : float
105
+ Temperature at the base of the segment (°C).
106
+ phi : float
107
+ Effective porosity for the segment.
108
+
109
+ Returns
110
+ -------
111
+ float
112
+ Estimated matrix conductivity parameter kh_matrix used in bulk mixing.
113
+ """
114
+ # set default thermal conductivity value representative for basement lithology
115
+ kh_matrix = 1.6
116
+
117
+ temperature_average = 0.5 * (temperature_top + temperature_base)
118
+
119
+ if self.sorting_factor != -1:
120
+ # Calculate vertical bulk conductivity after compaction correction, given the porosity
121
+ kv_phi_20 = self.calculate_k_compaction_correction(phi)
122
+
123
+ # Calculate horizontal bulk conductivity based on corrected vertical bulk thermal conducitivty
124
+ kh_phi_20 = 0.5 * (
125
+ self.kv_phi0_20 + (2 * self.kv_phi0_20 * self.anisotropy) - kv_phi_20
126
+ )
127
+
128
+ # Calculate horizontal matrix conductivity
129
+ kh_matrix = (358 * ((1.0227 * kh_phi_20) - 1.882)) * (
130
+ (1 / (temperature_average + 273)) - 0.00068
131
+ ) + 1.84
132
+ else:
133
+ print(
134
+ "Error: Basement lithology used for sediments, or no sorting factor applied"
135
+ )
136
+
137
+ return kh_matrix
138
+
139
+ def calculate_k_water(self, temperature_base: float) -> float:
140
+ """
141
+ Calculates the thermal conductivity of water at the given temperature.
142
+
143
+ Parameters
144
+ ----------
145
+ temperature_base : float
146
+ Temperature in °C at the base of the segment.
147
+
148
+ Returns
149
+ -------
150
+ float
151
+ Thermal conductivity of water (W/m·K).
152
+ """
153
+ temperature_base = min(temperature_base, 120)
154
+
155
+ k_water = (
156
+ 5.62
157
+ + (0.02022 * temperature_base)
158
+ - (0.0000823 * (temperature_base * temperature_base))
159
+ ) * 0.1
160
+
161
+ return k_water
162
+
163
+ def calculate_kh_bulk(
164
+ self, temperature_base: float, kh_matrix: float, phi: float
165
+ ) -> float:
166
+ """
167
+ Compute bulk horizontal thermal conductivity by combining matrix and pore fluid.
168
+
169
+ Uses a geometric mixing law: k_bulk = KxRM^(1-phi) * kw^(phi).
170
+
171
+ Parameters
172
+ ----------
173
+ temperature_base : float
174
+ Temperature at segment base (°C).
175
+ kh_matrix : float
176
+ Horizontal thermal conductivity of the rock matrix (W/mK).
177
+ phi : float
178
+ Porosity (fraction).
179
+
180
+ Returns
181
+ -------
182
+ float
183
+ Bulk horizontal thermal conductivity (W/m·K).
184
+ """
185
+ k_water = self.calculate_k_water(temperature_base)
186
+ phi_rev = 1 - phi
187
+ kh_bulk = math.pow(kh_matrix, phi_rev) * (math.pow(k_water, phi))
188
+
189
+ return kh_bulk
190
+
191
+
192
+ class ThermalConductivityResults:
193
+ """
194
+ Class that stores the results of thermal conductivity calculations, including depth profiles of lithology fraction,
195
+ porosity, and bulk thermal conductivity.
196
+
197
+ Attributes
198
+ ----------
199
+ sample : int
200
+ Sample number or identifier.
201
+ depth : np.ndarray
202
+ Array of depth values corresponding to the intervals with different subsurface properties.
203
+ lithology_a_fraction : np.ndarray
204
+ Array of lithology fraction values for the first lithology in each depth-interval.
205
+ phi : np.ndarray
206
+ Array of porosity values corresponding to the depth intervals.
207
+ kx : np.ndarray
208
+ Array of bulk thermal conductivity values corresponding to the depth intervals.
209
+ """
210
+
211
+ def __init__(self, sample, depth, lithology_a_fraction, phi, kh_bulk):
212
+ self.sample = sample
213
+ self.depth = depth
214
+ self.lithology_a_fraction = lithology_a_fraction
215
+ self.phi = phi
216
+ self.kh_bulk = kh_bulk
217
+
218
+
219
+ class ProcessLithologyToThermalConductivity:
220
+ """
221
+ Handles the calculation procedure of subsurface thermal conductivities based on lithological data.
222
+
223
+ Attributes
224
+ ----------
225
+ lithology_props_df : pd.DataFrame
226
+ DataFrame containing physical properties and thermal parameters for different lithologies, adopted from Hantschel & Kauerauf (2009).
227
+ borehole_df : pd.DataFrame
228
+ DataFrame containing borehole lithological description (depth and lithology data).
229
+ Tg : float
230
+ Surface temperature (in °C).
231
+ Tgrad : float
232
+ Average geothermal gradient (in °C/km).
233
+ z_Tg : float
234
+ Depth at which the (sub)surface temperature `Tg` is measured.
235
+ phi_scale : float
236
+ Scaling factor for porosity over depth. Induces uniform variation in porosity values during sampling of different porosity-depth profiles in a stochastic (MC) simulation.
237
+ lithology_scale : float
238
+ Scaling factor for lithology fraction over depth. Induces uniform variation in lithology fraction values during sampling of different porosity-depth profiles in a stochastic (MC) simulation.
239
+ lithology_error : float
240
+ Depth-independent error applied to lithology fraction during sampling of different porosity-depth profiles in a stochastic (MC) simulation.
241
+ nsamples : int
242
+ Number of thermal conductivity-depth profiles to generate and store.
243
+ basecase : bool
244
+ Whether to run the simulation in "base case" mode. In base case mode, no scaling and/or error values are applied to the porosity and lithology profiles in the thermal conductivity calculation.
245
+ out_dir : str
246
+ Path to the directory where files with subsurface properties are saved.
247
+ out_table : str
248
+ Name of the file (.h5) for storing the table with subsurface properties.
249
+ read_from_table : bool
250
+ Whether to read precomputed results of subsurface thermal conductivities from an existing file or compute real-time.
251
+
252
+ Raises
253
+ ------
254
+ ValueError
255
+ If any required input is missing or incompatible.
256
+ """
257
+
258
+ def __init__(
259
+ self,
260
+ borehole_df: pd.DataFrame,
261
+ Tg: float,
262
+ Tgrad: float,
263
+ z_Tg: float,
264
+ phi_scale: float,
265
+ lithology_scale: float,
266
+ lithology_error: float,
267
+ nsamples: int,
268
+ basecase: bool,
269
+ out_dir: Path,
270
+ out_table: str,
271
+ read_from_table: bool,
272
+ ) -> None:
273
+ # Read lithology properties reference table (Excel)
274
+ self.lithology_props_df = pd.read_excel(lithology_properties_xlsx)
275
+
276
+ self.borehole_df = borehole_df
277
+ self.lithology_props_dict = self.create_lithology_props_dict()
278
+ self.borehole_lithology_props = (
279
+ self.create_borehole_lithology_props_classification()
280
+ )
281
+
282
+ self.Tg = Tg
283
+ self.Tgrad = Tgrad
284
+ self.z_Tg = z_Tg
285
+ self.interpolator_Tg = TgInterpolator(self.z_Tg, self.Tg, self.Tgrad)
286
+
287
+ # Sampling/stochastic options
288
+ self.phi_scale = phi_scale
289
+ self.lithology_scale = lithology_scale
290
+ self.lithology_error = lithology_error
291
+ self.nsamples = nsamples
292
+ self.basecase = basecase
293
+ if self.basecase:
294
+ self.nsamples = 1
295
+ self.samples = np.arange(self.nsamples)
296
+
297
+ # Directory paths and output file name
298
+ self.out_dir = out_dir
299
+ self.out_table = out_table
300
+ self.read_from_table = read_from_table
301
+
302
+ def create_lithology_props_dict(self) -> dict[int, "ThermalConductivityCalculator"]:
303
+ """
304
+ Creates a dictionary of physical and thermal properties for different lithologies, in the correct format for
305
+ the thermal conductivity calculations. Property values are adopted from the Hantschel & Kauerauf (HK) database.
306
+
307
+ Dictionary maps the physical and thermal properties from the HK classification to the corresponding
308
+ variables in the thermal conductivity calculations.
309
+
310
+ Returns
311
+ -------
312
+ dict
313
+ Mapping `lithology ID -> ThermalConductivityCalculator`.
314
+ """
315
+
316
+ lithology_props_dict = {}
317
+ for _, row in self.lithology_props_df.iterrows():
318
+ obj_key = row["ID"]
319
+ # map columns in HK table to constructor arguments (preserve original semantics)
320
+ obj_values = {
321
+ "kv_phi0_20": row["K [W/mK]"],
322
+ "anisotropy": row["anisotropy"],
323
+ "c_p": row["cp [J/kgK]"],
324
+ "sorting_factor": row["sorting (-1 = 0 sorting)"],
325
+ "rho": row["rho [kg/m3]"],
326
+ "phi0": row["phi0 [%/100]"],
327
+ "k_athy": row["k_athy (compaction par.) [1/km]"],
328
+ }
329
+
330
+ # create calculator instance
331
+ lithology_props_dict[obj_key] = ThermalConductivityCalculator(**obj_values)
332
+
333
+ return lithology_props_dict
334
+
335
+ def create_borehole_lithology_props_classification(self) -> pd.DataFrame:
336
+ """
337
+ Merge borehole lithology sheet with HK classification table and create
338
+ columns with HK IDs for lithology a and b.
339
+
340
+ Returns
341
+ -------
342
+ pandas.DataFrame
343
+ DataFrame containing lithological classification along borehole, with thermal properties and added columns 'Lithology_ID_a' and 'Lithology_ID_b'.
344
+ """
345
+ # Merge borehole description data with HK classification for formation along borehole
346
+ # Merge for lithology a
347
+ borehole_lithology_a = pd.merge(
348
+ self.borehole_df,
349
+ self.lithology_props_df[["Lithology", "ID"]],
350
+ left_on="Lithology_a",
351
+ right_on="Lithology",
352
+ how="left",
353
+ )
354
+ borehole_lithology_a.rename(columns={"ID": "Lithology_ID_a"}, inplace=True)
355
+ borehole_lithology_a = borehole_lithology_a.drop("Lithology", axis=1)
356
+
357
+ # Merge for lithology b
358
+ borehole_lithology_ab = pd.merge(
359
+ borehole_lithology_a,
360
+ self.lithology_props_df[["Lithology", "ID"]],
361
+ left_on="Lithology_b",
362
+ right_on="Lithology",
363
+ how="left",
364
+ )
365
+ borehole_lithology_ab.rename(columns={"ID": "Lithology_ID_b"}, inplace=True)
366
+ borehole_lithology_ab = borehole_lithology_ab.drop("Lithology", axis=1)
367
+
368
+ return borehole_lithology_ab
369
+
370
+ @classmethod
371
+ def from_config(
372
+ cls, config: LithologyConfig
373
+ ) -> "ProcessLithologyToThermalConductivity":
374
+ """
375
+ Create a ProcessLithologyToThermalConductivity instance from a configuration dict.
376
+
377
+ Parameters
378
+ ----------
379
+ config : LithologyConfig
380
+ Configuration dictionary with required keys.
381
+
382
+ Returns
383
+ -------
384
+ ProcessLithologyToThermalConductivity
385
+ Initialized instance.
386
+ """
387
+
388
+ borehole_path = config.borehole_lithology_path
389
+ borehole_df = pd.read_excel(
390
+ borehole_path, sheet_name=config.borehole_lithology_sheetname
391
+ )
392
+
393
+ return cls(
394
+ borehole_df=borehole_df,
395
+ Tg=config.Tg,
396
+ Tgrad=config.Tgrad,
397
+ z_Tg=config.z_Tg,
398
+ phi_scale=config.phi_scale,
399
+ lithology_scale=config.lithology_scale,
400
+ lithology_error=config.lithology_error,
401
+ nsamples=config.n_samples,
402
+ basecase=config.basecase,
403
+ out_dir=config.out_dir_lithology,
404
+ out_table=config.out_table,
405
+ read_from_table=config.read_from_table,
406
+ )
407
+
408
+ def create_single_thermcon_profile(
409
+ self, isample: int
410
+ ) -> ThermalConductivityResults:
411
+ """
412
+ Calculates depth-dependent subsurface properties, for a specific sample of the loaded or calculated subsurface
413
+ properties table.
414
+
415
+ Parameters
416
+ ----------
417
+ isample : int
418
+ Sample index (0 is basecase).
419
+
420
+ Returns
421
+ -------
422
+ ThermalConductivityResults
423
+ Object with arrays depth, lithology fraction, porosity and kh_bulk.
424
+ """
425
+
426
+ borehole_lithology_df = self.borehole_lithology_props
427
+ depth = np.asarray(borehole_lithology_df["Depth"])
428
+
429
+ # map HK IDs to calculators (lists aligned with depth)
430
+ lithology_to_k_a = [
431
+ self.lithology_props_dict[litho]
432
+ for litho in borehole_lithology_df["Lithology_ID_a"]
433
+ ]
434
+ lithology_to_k_b = [
435
+ self.lithology_props_dict[litho]
436
+ for litho in borehole_lithology_df["Lithology_ID_b"]
437
+ ]
438
+
439
+ # Lists to store bulk thermal conductivity values for each version
440
+ kh_bulk = []
441
+ phi_samples = []
442
+ lithology_a_fraction_samples = []
443
+
444
+ # Choose whether to sample the depth-scaling error or use basecase lithology fractions
445
+ if self.basecase or (isample == 0):
446
+ # For single run use basecase porosity and lithology fraction
447
+ lithology_a_fraction = self.borehole_lithology_props["Lithology_a_fraction"]
448
+ phi_scale_error = 0
449
+ else:
450
+ # Else sample within scaling error
451
+ lithology_a_fraction = np.clip(
452
+ np.random.uniform(
453
+ self.borehole_lithology_props["Lithology_a_fraction"]
454
+ - self.lithology_scale,
455
+ self.borehole_lithology_props["Lithology_a_fraction"]
456
+ + self.lithology_scale,
457
+ ),
458
+ a_min=0,
459
+ a_max=1,
460
+ )
461
+
462
+ # Implement a uniform error for phi
463
+ phi_scale_error = np.random.uniform(-self.phi_scale, self.phi_scale)
464
+
465
+ # For every depth segment
466
+ for i in range(len(depth)):
467
+ # Choose whether to sample the depth-random error or use basecase lithology fractions
468
+ if self.basecase or (isample == 0):
469
+ # For single run use basecase lithology fraction
470
+ lithology_a_fraction_sampled = lithology_a_fraction[i]
471
+ else:
472
+ # Sample values for every depth segment within the error ranges for lithology
473
+ lithology_a_fraction_sampled = np.clip(
474
+ np.random.uniform(
475
+ lithology_a_fraction[i] - self.lithology_error,
476
+ lithology_a_fraction[i] + self.lithology_error,
477
+ ),
478
+ a_min=0,
479
+ a_max=1,
480
+ )
481
+
482
+ # Calculate porosity per lithology with uniform error applied
483
+ phi_litho_a = np.clip(
484
+ lithology_to_k_a[i].calculate_porosity(depth[i]) + phi_scale_error,
485
+ a_min=0,
486
+ a_max=1,
487
+ )
488
+ phi_litho_b = np.clip(
489
+ lithology_to_k_b[i].calculate_porosity(depth[i]) + phi_scale_error,
490
+ a_min=0,
491
+ a_max=1,
492
+ )
493
+
494
+ # Calculate effective porosity for the combined lithology, weighted by lithology fraction
495
+ phi_sampled = (phi_litho_a * lithology_a_fraction_sampled) + (
496
+ phi_litho_b * (1 - lithology_a_fraction_sampled)
497
+ )
498
+
499
+ # Append sampled values to lists
500
+ phi_samples.append(phi_sampled)
501
+ lithology_a_fraction_samples.append(lithology_a_fraction_sampled)
502
+
503
+ # Calculate temperature for the current depth segment
504
+ if i == 0:
505
+ temperature_top = self.interpolator_Tg.getTg(depth[0])
506
+ else:
507
+ temperature_top = self.interpolator_Tg.getTg(depth[i - 1])
508
+ temperature_base = self.interpolator_Tg.getTg(depth[i])
509
+
510
+ # Calculate horizontal matrix thermal conductivity of lithology a and lithology b
511
+ kh_matrix_lithology_a = lithology_to_k_a[i].calculate_kh_rock_matrix(
512
+ temperature_top, temperature_base, phi_sampled
513
+ )
514
+ kh_matrix_lithology_b = lithology_to_k_b[i].calculate_kh_rock_matrix(
515
+ temperature_top, temperature_base, phi_sampled
516
+ )
517
+
518
+ # geometric mixing by lithology fraction (geometric mean)
519
+ if lithology_a_fraction_sampled == 0:
520
+ kh_matrix_segment = kh_matrix_lithology_b
521
+ elif lithology_a_fraction_sampled == 1:
522
+ kh_matrix_segment = kh_matrix_lithology_a
523
+ else:
524
+ kh_matrix_segment = math.pow(
525
+ kh_matrix_lithology_a, lithology_a_fraction_sampled
526
+ ) * (
527
+ math.pow(kh_matrix_lithology_b, (1 - lithology_a_fraction_sampled))
528
+ )
529
+
530
+ # Calculate horizontal bulk thermal conductivity from porosity and combined matrix thermal conductivity
531
+ kh_bulk_segment = lithology_to_k_a[i].calculate_kh_bulk(
532
+ temperature_base, kh_matrix_segment, phi_sampled
533
+ )
534
+
535
+ # Append the calculated value of the bulk thermal conductivity to the list for each segment
536
+ kh_bulk.append(kh_bulk_segment)
537
+
538
+ # Convert lists to numpy arrays
539
+ kh_bulk = np.asarray(kh_bulk)
540
+ lithology_a_fraction_samples = np.asarray(lithology_a_fraction_samples)
541
+ phi_samples = np.asarray(phi_samples)
542
+
543
+ # Create and return a ThermalConductivityResults object
544
+ k_calc_results = ThermalConductivityResults(
545
+ isample, depth, lithology_a_fraction_samples, phi_samples, kh_bulk
546
+ )
547
+
548
+ return k_calc_results
549
+
550
+ def create_multi_thermcon_profiles(self) -> None:
551
+ """
552
+ Create thermal conductivity profiles for all requested (stochastic) samples (or read from .h5 file).
553
+
554
+ If `read_from_table` is True, read data from `out_dir/out_table`. Otherwise compute samples in run time.
555
+
556
+ Returns
557
+ -------
558
+ None (results are stored internally).
559
+ """
560
+ if self.read_from_table:
561
+ path = self.out_dir / self.out_table
562
+ lithology_k_ds = xr.open_dataset(path, group="litho_k", engine="h5netcdf")
563
+ kh_bulk_da = lithology_k_ds["kh_bulk"]
564
+
565
+ kh_bulk_results = []
566
+ for sample in range(len(kh_bulk_da.isel({"depth": 0}))):
567
+ depth = kh_bulk_da.depth.values
568
+ kh_bulk = kh_bulk_da.isel({"n_samples": sample}).values
569
+ kh_bulk_i = ThermalConductivityResults(
570
+ depth=depth,
571
+ kh_bulk=kh_bulk,
572
+ sample=sample,
573
+ phi=None,
574
+ lithology_a_fraction=None,
575
+ )
576
+ kh_bulk_results.append(kh_bulk_i)
577
+
578
+ else:
579
+ kh_bulk_results = []
580
+ for s in self.samples:
581
+ ssres = self.create_single_thermcon_profile(int(s))
582
+ kh_bulk_results.append(ssres)
583
+
584
+ self.kh_bulk_results = kh_bulk_results
585
+
586
+ def get_thermcon_sample_profile(self, isample: int) -> ThermalConductivityResults:
587
+ """
588
+ Retrieves depth-dependent subsurface properties for a specific sample in the precomputed table.
589
+
590
+ Parameters
591
+ ----------
592
+ isample : int
593
+ Sample index (use -1 to request basecase which is index 0).
594
+
595
+ Returns
596
+ -------
597
+ ThermalConductivityResults
598
+ Results container for the requested sample.
599
+ """
600
+ if self.basecase | isample == -1:
601
+ return self.kh_bulk_results[0]
602
+ else:
603
+ return self.kh_bulk_results[isample]
604
+
605
+ def save_thermcon_sample_profiles(self) -> None:
606
+ """
607
+ Save the computed sample profiles to a NetCDF (.h5) file using xarray.
608
+
609
+ The dataset will contain variables:
610
+ - kh_bulk (depth x n_samples)
611
+ - phi (depth x n_samples)
612
+ - lithology_a_fraction (depth x n_samples)
613
+
614
+ Returns
615
+ -------
616
+ None (results are saved directly to the specified output file).
617
+ """
618
+ # build empty dataset and coordinates
619
+ if not self.kh_bulk_results:
620
+ raise RuntimeError(
621
+ "No results available to save. Run create_multi_thermcon_profiles() first."
622
+ )
623
+
624
+ lithology_k_ds = xr.Dataset()
625
+
626
+ # Add 'depth' coordinate
627
+ depth_values = self.get_thermcon_sample_profile(
628
+ 0
629
+ ).depth # Assuming all samples have the same depth values
630
+ lithology_k_ds = lithology_k_ds.assign_coords(depth=depth_values)
631
+
632
+ # Add lithology a and b
633
+ lithology_k_ds["lithology_a"] = xr.DataArray(
634
+ self.borehole_df["Lithology_a"],
635
+ dims=("depth"),
636
+ coords={"depth": depth_values},
637
+ )
638
+ lithology_k_ds["lithology_b"] = xr.DataArray(
639
+ self.borehole_df["Lithology_b"],
640
+ dims=("depth"),
641
+ coords={"depth": depth_values},
642
+ )
643
+
644
+ for isample in range(self.nsamples):
645
+ ssres = self.get_thermcon_sample_profile(isample)
646
+
647
+ # Create variables if they don't exist
648
+ if "kh_bulk" not in lithology_k_ds:
649
+ lithology_k_ds["kh_bulk"] = xr.DataArray(
650
+ np.nan,
651
+ dims=("depth", "n_samples"),
652
+ coords={"depth": depth_values, "n_samples": range(self.nsamples)},
653
+ )
654
+ if "phi" not in lithology_k_ds:
655
+ lithology_k_ds["phi"] = xr.DataArray(
656
+ np.nan,
657
+ dims=("depth", "n_samples"),
658
+ coords={"depth": depth_values, "n_samples": range(self.nsamples)},
659
+ )
660
+ if "lithology_a_fraction" not in lithology_k_ds:
661
+ lithology_k_ds["lithology_a_fraction"] = xr.DataArray(
662
+ np.nan,
663
+ dims=("depth", "n_samples"),
664
+ coords={"depth": depth_values, "n_samples": range(self.nsamples)},
665
+ )
666
+
667
+ # Assign variables to the dataset
668
+ lithology_k_ds["kh_bulk"].loc[{"n_samples": isample}] = ssres.kh_bulk
669
+ lithology_k_ds["phi"].loc[{"n_samples": isample}] = ssres.phi
670
+ lithology_k_ds["lithology_a_fraction"].loc[{"n_samples": isample}] = (
671
+ ssres.lithology_a_fraction
672
+ )
673
+
674
+ # Save results to HDF5 file using xarray
675
+ out_path = self.out_dir / self.out_table
676
+ lithology_k_ds.to_netcdf(out_path, group="litho_k", engine="h5netcdf")
677
+
678
+ def get_start_end_depths(self):
679
+ """
680
+ Retrieves the start and end depths for all intervals with different subsurface properties.
681
+
682
+ Returns
683
+ -------
684
+ (zstart, zend) : tuple of numpy arrays
685
+ zstart: start depth of each segment (first element 0)
686
+ zend: end depth array taken from results
687
+ """
688
+ if not self.kh_bulk_results:
689
+ raise RuntimeError(
690
+ "No results available. Run create_multi_thermcon_profiles() first."
691
+ )
692
+ zend = self.kh_bulk_results[0].depth
693
+ zstart = np.roll(zend, 1)
694
+ zstart[0] = 0
695
+ return zstart, zend
@@ -0,0 +1,3 @@
1
+ """
2
+ This package contains the modules with the main classes and functions for creating heat load profiles for Geoloop simulations.
3
+ """