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
|
@@ -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
|