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,152 @@
1
+ import numpy as np
2
+
3
+ from geoloop.configuration import SingleRunConfig
4
+ from geoloop.geoloopcore.strat_interpolator import StratInterpolator, TgInterpolator
5
+ from geoloop.lithology.process_lithology import ProcessLithologyToThermalConductivity
6
+
7
+
8
+ class SoilProperties:
9
+ """
10
+ Store soil thermal properties, including geotherm (temperature vs. depth)
11
+ and thermal conductivity profiles (depth-dependent or lithology-based).
12
+
13
+ Supports:
14
+ - Depth-based interpolation of ground temperature Tg(z)
15
+ - Depth-based or lithology-based interpolation of k_s(z)
16
+ - Scaling of conductivity profiles
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ Tg: list | float,
22
+ k_s: list | float,
23
+ z_Tg: list = None,
24
+ Tgrad: float = 0.0,
25
+ z_k_s: list = None,
26
+ k_s_scale: float = 1.0,
27
+ lithology_to_k: ProcessLithologyToThermalConductivity = None,
28
+ alfa: float = 1e-6,
29
+ ) -> None:
30
+ """
31
+ Initialize SoilProperties, containing ground temperature and thermal
32
+ conductivity as functions of depth.
33
+
34
+ Parameters
35
+ ----------
36
+ Tg : float or list
37
+ Ground temperature(s) used for interpolation.
38
+ - If array-list: temperature at depths specified by `z_Tg`.
39
+ - If float: constant surface temperature for linear gradient computation.
40
+ k_s : float or list
41
+ Thermal conductivity values (W/m·K), possibly depth-dependent.
42
+ If list-like, entries correspond to depths in `z_k_s`.
43
+ z_Tg : list or None, optional
44
+ Depth values (m) corresponding to `Tg` samples.
45
+ If None, temperature is assumed constant or determined by `Tgrad`.
46
+ Tgrad : float, optional
47
+ Vertical geothermal gradient (K/m). Default is 0 (no gradient).
48
+ z_k_s : list or None, optional
49
+ Depth values (m) corresponding to `k_s` samples.
50
+ If None, conductivity is assumed constant at all depths.
51
+ k_s_scale : float, optional
52
+ Scaling factor applied to thermal conductivity profiles (default 1.0).
53
+ lithology_to_k : lithology_to_k or None, optional
54
+ If provided, conductivity is derived from lithology-based profiles
55
+ using the lithology_to_k lookup structure instead of `k_s`/`z_k_s`.
56
+ alfa : float, optional
57
+ Thermal diffusivity of the ground (m²/s). Default is 1e-6.
58
+
59
+ Notes
60
+ -----
61
+ - If `lithology_to_k` is provided, the lithology-based thermal conductivity
62
+ profile overrides `k_s` and `z_k_s`.
63
+ - This constructor initializes two interpolators:
64
+ * `interpolatorTg` – for temperature vs. depth
65
+ * `interpolatorKs` – for conductivity vs. depth
66
+ """
67
+ # subsurface temperature and/or geothermal gradient
68
+ self.Tg = Tg
69
+ self.z_Tg = z_Tg
70
+ self.Tgrad = Tgrad
71
+ self.interpolatorTg = TgInterpolator(self.z_Tg, self.Tg, self.Tgrad)
72
+
73
+ # subsurface bulk thermal conductivity
74
+ self.k_s = np.asarray(k_s)
75
+ if z_k_s == None:
76
+ self.z_k_s = np.ones_like(k_s)
77
+ else:
78
+ self.z_k_s = np.asarray(z_k_s)
79
+ self.k_s_scale = k_s_scale
80
+ self.lithology_to_k = lithology_to_k
81
+
82
+ self.alfa = alfa # thermal diffusivity
83
+
84
+ # interpolate thermal conductivity
85
+ if isinstance(self.lithology_to_k, ProcessLithologyToThermalConductivity):
86
+ # lithology-based thermal conductivity profile
87
+ zstart, zend = self.lithology_to_k.get_start_end_depths()
88
+ zval = self.lithology_to_k.get_thermcon_sample_profile(0).kh_bulk
89
+ self.interpolatorKs = StratInterpolator(zend, zval)
90
+ else:
91
+ self.interpolatorKs = StratInterpolator(
92
+ self.z_k_s, self.k_s, stepvalue=False
93
+ )
94
+
95
+ def getTg(self, z: float | np.ndarray) -> float | np.ndarray:
96
+ """Return ground temperature at depth `z`."""
97
+ return self.interpolatorTg.getTg(z)
98
+
99
+ def get_k_s(
100
+ self, zstart: np.ndarray, zend: np.ndarray, isample: int = 0
101
+ ) -> np.ndarray:
102
+ """
103
+ Retrieves and interpolates thermal conductivity-depth profile for depth-segments in the simulation, for the basecase
104
+ in a single simulation or for the specified sample in a stochastic simulation.
105
+
106
+ Parameters
107
+ ----------
108
+ zstart : np.ndarray
109
+ Array of starting depths of each segment.
110
+ zend : np.ndarray
111
+ Array of ending depths of each segment.
112
+ isample : int, optional
113
+ Index selecting conductivity sample from lithology_to_k (default 0).
114
+
115
+ Returns
116
+ -------
117
+ np.ndarray
118
+ Thermal conductivity-depth profile with depth-resolution for the depth-segments in the simulation.
119
+ """
120
+ if isinstance(self.lithology_to_k, ProcessLithologyToThermalConductivity):
121
+ zval = self.lithology_to_k.get_thermcon_sample_profile(isample).kh_bulk
122
+ self.interpolatorKs.zval = zval
123
+
124
+ ks = self.interpolatorKs.interp(zstart, zend)
125
+ return ks * self.k_s_scale
126
+
127
+ @classmethod
128
+ def from_config(cls, config: SingleRunConfig) -> "SoilProperties":
129
+ """
130
+ Build a SoilProperties object from a configuration object.
131
+
132
+ Parameters
133
+ ----------
134
+ config : SingleRunConfig
135
+ Object containing entries including Tg, k_s, stratigraphy, scaling,
136
+ lithology parameters, and diffusivity.
137
+
138
+ Returns
139
+ -------
140
+ SoilProperties
141
+ """
142
+
143
+ return SoilProperties(
144
+ Tg=config.Tg,
145
+ z_Tg=config.z_Tg,
146
+ Tgrad=config.Tgrad,
147
+ k_s=config.k_s,
148
+ z_k_s=config.z_k_s,
149
+ k_s_scale=config.k_s_scale,
150
+ alfa=config.alfa,
151
+ lithology_to_k=config.lithology_to_k, # <-- already built upstream
152
+ )
@@ -0,0 +1,194 @@
1
+ from typing import Union
2
+
3
+ import numpy as np
4
+
5
+ ArrayLike = Union[np.ndarray, list, float, int]
6
+
7
+
8
+ class TgInterpolator:
9
+ """
10
+ class to manage interpolation of temperature over depth.
11
+ """
12
+
13
+ def __init__(self, z_Tg: ArrayLike | None, Tg: ArrayLike, Tgrad: float) -> None:
14
+ """
15
+ Parameters
16
+ ----------
17
+ z_Tg : array_like or None
18
+ Depth values (m) at which temperature samples in `Tg` are defined.
19
+ If None, `Tg` is treated as a scalar.
20
+ Tg : float or array_like
21
+ Ground temperature value(s).
22
+ - If float: temperature at surface.
23
+ - If array_like: temperature profile at depths `z_Tg`.
24
+ Tgrad : float
25
+ Geothermal gradient (C/m). Used only when `Tg` is scalar.
26
+ """
27
+ self.z_Tg = z_Tg
28
+ self.Tg = Tg
29
+ self.Tgrad = Tgrad
30
+
31
+ def interp_Tg(self, z: ArrayLike) -> np.ndarray:
32
+ """
33
+ Interpolate temperature at depth.
34
+
35
+ Parameters
36
+ ----------
37
+ z : array_like
38
+ Depth(s) in meters.
39
+
40
+ Returns
41
+ -------
42
+ np.ndarray
43
+ Interpolated temperature values.
44
+ """
45
+ return np.interp(z, self.z_Tg, self.Tg)
46
+
47
+ def getTg(self, z: ArrayLike) -> ArrayLike:
48
+ """
49
+ Get ground temperature at depth.
50
+
51
+ Parameters
52
+ ----------
53
+ z : float or array_like
54
+ Depth(s) in meters.
55
+
56
+ Returns
57
+ -------
58
+ float or np.ndarray
59
+ Temperature at depth.
60
+
61
+ Notes
62
+ -----
63
+ - If `Tg` is scalar: Tg + Tgrad * 0.01 * z
64
+ - If `Tg` is array: interpolated from depth-temperature profile
65
+ """
66
+ if np.isscalar(self.Tg):
67
+ return self.Tg + self.Tgrad * 0.01 * z
68
+ else:
69
+ return self.interp_Tg(z)
70
+
71
+
72
+ class StratInterpolator:
73
+ """
74
+ class to manage interpolation of soil properties to averaged segment properties.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ zcoord: ArrayLike,
80
+ zval: ArrayLike,
81
+ nz: int = 10000,
82
+ stepvalue: bool = True,
83
+ ) -> None:
84
+ """
85
+ Parameters
86
+ ----------
87
+ zcoord : array_like
88
+ Depth coordinates (m) defining the property profile.
89
+ zval : array_like
90
+ Property values corresponding to `zcoord`.
91
+ nz : int, optional
92
+ Number of points for the internal fine-resolution discretization.
93
+ stepvalue : bool, optional
94
+ If True:
95
+ Interpret `zval` as piecewise-constant over intervals
96
+ (stepwise profile).
97
+ If False:
98
+ Use linear interpolation between points.
99
+ """
100
+ self.zcoord = zcoord
101
+ self.zval = zval
102
+ self.nz = nz
103
+
104
+ # high-resolution depth array
105
+ self.zp = np.linspace(0, self.zcoord[-1], num=nz)
106
+ self.dz = self.zp[1] - self.zp[0]
107
+
108
+ self.stepvalue = stepvalue
109
+ if self.stepvalue:
110
+ self.init_indices()
111
+
112
+ @property
113
+ def zval(self) -> np.ndarray:
114
+ return self._zval
115
+
116
+ @zval.setter
117
+ def zval(self, value: ArrayLike) -> None:
118
+ self._zval = value
119
+
120
+ def init_indices(self) -> None:
121
+ """
122
+ For the high resolution array self.zp get indices on the courser intervalled zcoord array.
123
+ The indices are determined by searchsorted of np, giving the lower index when on interval zlith[0] to zlith[1].
124
+ This is only used if stepvalue=True.
125
+ """
126
+ self.indexl = self.zcoord.searchsorted(self.zp)
127
+
128
+ def interp(self, zstart: np.ndarray, zend: np.ndarray) -> np.ndarray:
129
+ """
130
+ Interpolate for zstart, zend arrays of the segments, the average value of zval.
131
+ Compute average property value for each depth segment.
132
+
133
+ Parameters
134
+ ----------
135
+ zstart : np.ndarray
136
+ Segment start depths (m).
137
+ zend : np.ndarray
138
+ Segment end depths (m).
139
+
140
+ Returns
141
+ -------
142
+ np.ndarray
143
+ Average property value per segment.
144
+ """
145
+ val = zstart * 0.0
146
+ ilstart = max(0, int(zstart[0] / self.dz))
147
+ for i in range(len(zend)):
148
+ valsum = 0
149
+ ilend = min(self.nz, int(round(zend[i] / self.dz)))
150
+ ilrange = max(1, ilend - ilstart)
151
+ if self.stepvalue:
152
+ for il in range(ilrange):
153
+ valsum += self.zval[self.indexl[il + ilstart]]
154
+ val[i] = valsum / ilrange
155
+ else:
156
+ vals = np.interp(self.zp[ilstart:ilend], self.zcoord, self.zval)
157
+ val[i] = np.sum(vals) / ilrange
158
+ ilstart = min(self.nz - 1, ilend)
159
+ return val
160
+
161
+ def interp_plot(self, zstart: np.ndarray, zend: np.ndarray) -> np.ndarray:
162
+ """
163
+ Generate a high-resolution vector of interpolated property values.
164
+
165
+ Parameters
166
+ ----------
167
+ zstart : np.ndarray
168
+ Segment start depths (m).
169
+ zend : np.ndarray
170
+ Segment end depths (m).
171
+
172
+ Returns
173
+ -------
174
+ np.ndarray
175
+ Interpolated property values along internal grid `zp`.
176
+ """
177
+ val = np.zeros_like(self.zp)
178
+ ilstart = max(0, int(zstart[0] / self.dz))
179
+
180
+ for i in range(len(zend)):
181
+ ilend = min(self.nz, int(round(zend[i] / self.dz)))
182
+ if ilend > ilstart:
183
+ if self.stepvalue:
184
+ val[ilstart:ilend] = self.zval[self.indexl[ilstart]]
185
+ else:
186
+ vals = np.interp(self.zp[ilstart:ilend], self.zcoord, self.zval)
187
+ val[ilstart:ilend] = vals
188
+ ilstart = ilend
189
+
190
+ # Fill the remaining 0 values with the last used value
191
+ last_nonzero_index = (val != 0).nonzero()[0][-1]
192
+ val[last_nonzero_index + 1 :] = val[last_nonzero_index]
193
+
194
+ return val
@@ -0,0 +1,3 @@
1
+ """
2
+ This package contains the modules with the main classes and functions for creating subsurface thermal conductivity models for Geoloop simulations.
3
+ """
@@ -0,0 +1,277 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import xarray as xr
6
+ from matplotlib import pyplot as plt
7
+ from matplotlib.colors import to_hex, to_rgb
8
+
9
+ from geoloop.constants import lithology_colors, lithology_names_english
10
+ from geoloop.geoloopcore.strat_interpolator import StratInterpolator
11
+
12
+
13
+ def mix_hex_colors(colors: list[str], fractions: list[float]) -> str:
14
+ """
15
+ Combine multiple hex colors using given fractional weights.
16
+
17
+ Parameters
18
+ ----------
19
+ colors : list of str
20
+ Hex color strings (e.g. '#aabbcc').
21
+ fractions : list of float
22
+ Fractions for each color. Typically sums to 1. Values may be 0..1.
23
+
24
+ Returns
25
+ -------
26
+ str
27
+ Resulting mixed color as a hex string.
28
+ """
29
+ mixed_color = np.zeros(3)
30
+ for color, fraction in zip(colors, fractions):
31
+ mixed_color += np.array(to_rgb(color)) * fraction
32
+ return to_hex(mixed_color)
33
+
34
+
35
+ def plot_lithology_and_thermcon(
36
+ litho_ds: xr.Dataset,
37
+ kx_plotting_base: np.ndarray,
38
+ interp_obj: StratInterpolator,
39
+ kx_plotting_max: np.ndarray,
40
+ kx_plotting_min: np.ndarray,
41
+ litho_h5path: str | Path,
42
+ out_dir: Path,
43
+ ) -> None:
44
+ """
45
+ Create and save a combined lithology "layer-cake" plot with thermal
46
+ conductivity profiles plotted alongside.
47
+
48
+ The main panel shows lithology as vertically stacked colored bands
49
+ (mixed color when two lithologies are present). A narrow side panel
50
+ shows horizontally stacked bars for lithology fractions. Thermal
51
+ conductivity curves are plotted on a twin x-axis (using the internal
52
+ grid from the StratInterpolator).
53
+
54
+ Parameters
55
+ ----------
56
+ litho_ds : xarray.Dataset
57
+ Dataset containing fields `depth`, lithology type a `lithology_a`, lithology type b `lithology_b`,
58
+ and the fraction of type a `lithology_a_fraction`.
59
+ kx_plotting_base : np.ndarray
60
+ Fine-grid, interpolated base-case thermal conductivity values (aligned with interp_obj.zp).
61
+ interp_obj : StratInterpolator
62
+ Interpolator object; used for the internal z-grid (zp) and interpolation.
63
+ kx_plotting_max : np.ndarray
64
+ Fine-grid, interpolated maximum thermal conductivity values.
65
+ kx_plotting_min : np.ndarray
66
+ Fine-grid, interpolated minimum thermal conductivity values.
67
+ litho_h5path : str | Path
68
+ Path to the source lithology .h5 file (used to build output filename).
69
+ out_dir : Path
70
+ Directory where the figure will be written.
71
+
72
+ Returns
73
+ -------
74
+ None
75
+
76
+ """
77
+ plt.rcParams.update({"font.size": 14})
78
+
79
+ # Create main + side axes: wide main column + narrow side bar
80
+ fig, (ax_main, ax_side) = plt.subplots(
81
+ 1, 2, figsize=(8, 8), gridspec_kw={"width_ratios": [4, 0.8]}
82
+ )
83
+
84
+ # Prepare lists for plotting
85
+ current_depth = 0
86
+ depths = []
87
+ fractions_a = []
88
+ fractions_b = []
89
+ colors_main = []
90
+
91
+ # Build mixed colors and fraction lists
92
+ for i in range(len(litho_ds["depth"])):
93
+ # depth value for this layer (end depth)
94
+ depth = litho_ds["depth"].values[i]
95
+
96
+ lithology_a = litho_ds["lithology_a"].isel(depth=i).item()
97
+ lithology_b = litho_ds["lithology_b"].isel(depth=i).item()
98
+ fraction_a = litho_ds["lithology_a_fraction"].isel(depth=i).item()
99
+ fraction_b = 1 - fraction_a
100
+
101
+ # Prepare data for the side plot
102
+ depths.append((current_depth + depth) / 2) # Center of the bar
103
+ fractions_a.append(fraction_a)
104
+ fractions_b.append(fraction_b)
105
+
106
+ # Prepare data for the main plot
107
+ mixed_color = mix_hex_colors(
108
+ [lithology_colors[lithology_a], lithology_colors[lithology_b]],
109
+ [fraction_a, fraction_b],
110
+ )
111
+ colors_main.append(mixed_color)
112
+
113
+ current_depth = depth
114
+
115
+ # Plot the main lithology column with mixed colors
116
+ for i in range(len(litho_ds["depth"])):
117
+ depth_start = litho_ds["depth"].values[i - 1] if i > 0 else 0
118
+ depth_end = litho_ds["depth"].values[i]
119
+ ax_main.fill_betweenx(
120
+ [depth_start, depth_end], 0, 1, color=colors_main[i], edgecolor=None
121
+ )
122
+
123
+ # Adjust axis limits to cover all the plotted data
124
+ ax_main.set_xlim(0, 1) # Set x-axis to range from 0 to 1 for consistency
125
+ ax_main.set_ylim(
126
+ min(litho_ds["depth"].values), max(litho_ds["depth"].values)
127
+ ) # Make sure the depth range covers all data
128
+
129
+ # Calculate depth ranges
130
+ depth_ranges = [
131
+ (litho_ds["depth"].values[i - 1] if i > 0 else 0, litho_ds["depth"].values[i])
132
+ for i in range(len(litho_ds["depth"]))
133
+ ]
134
+
135
+ # Create the side plot (horizontally stacked bars for fractions)
136
+ for i, (depth_start, depth_end) in enumerate(depth_ranges):
137
+ bar_height = depth_end - depth_start
138
+ # Left segment (fraction A)
139
+ ax_side.barh(
140
+ y=depth_start, # Start of the depth range
141
+ width=fractions_a[i],
142
+ height=bar_height,
143
+ color=lithology_colors[litho_ds["lithology_a"].isel(depth=i).item()],
144
+ align="edge",
145
+ )
146
+ # Right segment (fraction B), stacked by using left=fraction_a
147
+ ax_side.barh(
148
+ y=depth_start, # Start of the depth range
149
+ width=fractions_b[i],
150
+ height=bar_height,
151
+ left=fractions_a[i],
152
+ color=lithology_colors[litho_ds["lithology_b"].isel(depth=i).item()],
153
+ align="edge",
154
+ )
155
+
156
+ # Style the side axis
157
+ ax_side.set_xlabel("Lithology fractions")
158
+ ax_side.set_xlim(0, 1)
159
+ ax_side.set_xticks([0, 0.5, 1])
160
+ ax_side.set_xticklabels(["0", "0.5", "1.0"])
161
+ ax_side.set_ylim(ax_main.get_ylim()) # Align y-axes
162
+ ax_side.invert_yaxis()
163
+
164
+ # Remove y-axis ticks and labels
165
+ ax_side.set_yticks([]) # Remove ticks
166
+ ax_side.set_yticklabels([]) # Remove labels
167
+
168
+ # Spines visibility
169
+ ax_side.spines["top"].set_visible(True)
170
+ ax_side.spines["right"].set_visible(True)
171
+ ax_side.spines["bottom"].set_visible(True)
172
+ ax_side.spines["left"].set_visible(True)
173
+
174
+ # Plot thermal conductivity on a secondary x-axis for the main plot
175
+ ax_main.set_xticks([])
176
+ ax_main.set_ylabel("Depth [m]")
177
+ ax_kx = ax_main.twiny()
178
+ ax_kx.plot(
179
+ kx_plotting_base, interp_obj.zp, color="black", linestyle="-", label="Basecase"
180
+ )
181
+ ax_kx.plot(kx_plotting_max, interp_obj.zp, color="blue", linestyle=":", label="Max")
182
+ ax_kx.plot(kx_plotting_min, interp_obj.zp, color="blue", linestyle=":", label="Min")
183
+ ax_kx.set_xlabel("Thermal Conductivity [W/mk]")
184
+ ax_kx.invert_yaxis()
185
+ ax_kx.legend(bbox_to_anchor=(0.5, -0.01), title="Thermal Conductivity")
186
+
187
+ # Create legend for lithologies
188
+ handles = []
189
+ labels = []
190
+ used_lithologies = set(litho_ds["lithology_a"].values) | set(
191
+ litho_ds["lithology_b"].values
192
+ )
193
+ for lithology_type in used_lithologies:
194
+ english_name = lithology_names_english.get(lithology_type, lithology_type)
195
+ handles.append(
196
+ plt.Rectangle((0, 0), 1, 1, color=lithology_colors[lithology_type])
197
+ )
198
+ labels.append(lithology_type)
199
+ ax_main.legend(
200
+ handles,
201
+ labels,
202
+ loc="upper right",
203
+ bbox_to_anchor=(0.9, -0.01),
204
+ title="Lithology",
205
+ )
206
+
207
+ plt.tight_layout()
208
+
209
+ # Save the figure
210
+ h5_file_name = Path(litho_h5path).name
211
+ fig_name = Path(h5_file_name).stem
212
+ fig_path = out_dir / fig_name
213
+
214
+ plt.savefig(fig_path)
215
+
216
+
217
+ def main_Plot_lithology(litho_h5_path: str) -> None:
218
+ """
219
+ Main function to load data, process thermal conductivity profiles, and generate lithology plots.
220
+
221
+ Parameters
222
+ ----------
223
+ litho_h5_path : str
224
+ Path to the .h5 file containing the dataset of subsurface properties.
225
+
226
+ Returns
227
+ --------
228
+ None
229
+
230
+ """
231
+ base_dir = Path(litho_h5_path).parent
232
+
233
+ # Open dataset (group 'litho_k' expected)
234
+ litho_k_ds = xr.open_dataset(litho_h5_path, group="litho_k", engine="netcdf4")
235
+
236
+ # select only basecase, sample 0
237
+ litho_k_ds_base = litho_k_ds.sel(n_samples=0)
238
+
239
+ z = litho_k_ds_base.depth.values
240
+ zval = litho_k_ds_base.kx.values
241
+
242
+ zend = z
243
+ zstart = z[:-1] * 1
244
+
245
+ # Append 0 at the beginning of zstart (so first segment starts at 0)
246
+ zstart = np.insert(zstart, 0, 0)
247
+
248
+ # Build interpolator & compute fine-grid, interpolated conductivity profiles
249
+ interp_obj = StratInterpolator(zend, zval)
250
+
251
+ # Interpolate basecase thermal conductivity values
252
+ kx_plotting_base = interp_obj.interp_plot(zstart, zend)
253
+
254
+ # Calculate max and min thermal conductivity profiles and interpolate
255
+ kx_max = litho_k_ds.kx.max(dim="n_samples").values
256
+ interp_obj.zval = kx_max
257
+ kx_plotting_max = interp_obj.interp_plot(zstart, zend)
258
+
259
+ kx_min = litho_k_ds.kx.min(dim="n_samples").values
260
+ interp_obj.zval = kx_min
261
+ kx_plotting_min = interp_obj.interp_plot(zstart, zend)
262
+
263
+ # Create the layer cake plot
264
+ plot_lithology_and_thermcon(
265
+ litho_k_ds_base,
266
+ kx_plotting_base,
267
+ interp_obj,
268
+ kx_plotting_max,
269
+ kx_plotting_min,
270
+ litho_h5_path,
271
+ base_dir,
272
+ )
273
+
274
+
275
+ if __name__ == "__main__":
276
+ litho_h5_path = sys.argv[1]
277
+ main_Plot_lithology(litho_h5_path)