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