fmu-pem 0.0.2__py3-none-any.whl → 0.0.3__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.
- fmu/pem/__main__.py +32 -16
- fmu/pem/forward_models/pem_model.py +19 -27
- fmu/pem/pem_functions/__init__.py +2 -2
- fmu/pem/pem_functions/density.py +32 -38
- fmu/pem/pem_functions/effective_pressure.py +153 -49
- fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
- fmu/pem/pem_functions/fluid_properties.py +447 -245
- fmu/pem/pem_functions/mineral_properties.py +77 -74
- fmu/pem/pem_functions/pressure_sensitivity.py +430 -0
- fmu/pem/pem_functions/regression_models.py +129 -97
- fmu/pem/pem_functions/run_friable_model.py +106 -37
- fmu/pem/pem_functions/run_patchy_cement_model.py +107 -45
- fmu/pem/pem_functions/{run_t_matrix_and_pressure.py → run_t_matrix_model.py} +48 -27
- fmu/pem/pem_utilities/__init__.py +31 -9
- fmu/pem/pem_utilities/cumsum_properties.py +29 -37
- fmu/pem/pem_utilities/delta_cumsum_time.py +8 -13
- fmu/pem/pem_utilities/enum_defs.py +65 -8
- fmu/pem/pem_utilities/export_routines.py +84 -72
- fmu/pem/pem_utilities/fipnum_pvtnum_utilities.py +217 -0
- fmu/pem/pem_utilities/import_config.py +64 -46
- fmu/pem/pem_utilities/import_routines.py +57 -69
- fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
- fmu/pem/pem_utilities/pem_config_validation.py +331 -139
- fmu/pem/pem_utilities/rpm_models.py +473 -100
- fmu/pem/pem_utilities/update_grid.py +3 -2
- fmu/pem/pem_utilities/utils.py +90 -38
- fmu/pem/run_pem.py +70 -39
- fmu/pem/version.py +16 -3
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.3.dist-info}/METADATA +18 -11
- fmu_pem-0.0.3.dist-info/RECORD +39 -0
- fmu_pem-0.0.2.dist-info/RECORD +0 -37
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.3.dist-info}/WHEEL +0 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.3.dist-info}/entry_points.txt +0 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Effective mineral properties are calculated from the individual mineral properties of
|
|
3
|
-
the volume fractions.
|
|
4
|
-
available, this is transformed to shale and sand fractions. A net-to-gross fraction
|
|
5
|
-
can also be estimated from porosity property.
|
|
6
|
-
|
|
7
|
-
If the ntg_calculation_flag is set in the PEM configuration parameter file, this will
|
|
8
|
-
override settings for volume fractions. In that case net-to-gross fraction is either
|
|
9
|
-
read from file, or calculated from porosity.
|
|
3
|
+
the volume fractions.
|
|
10
4
|
"""
|
|
11
5
|
|
|
12
6
|
from pathlib import Path
|
|
13
|
-
from typing import List, Tuple, Union
|
|
14
7
|
from warnings import warn
|
|
15
8
|
|
|
16
9
|
import numpy as np
|
|
@@ -18,70 +11,68 @@ from rock_physics_open.equinor_utilities.std_functions import (
|
|
|
18
11
|
multi_hashin_shtrikman,
|
|
19
12
|
multi_voigt_reuss_hill,
|
|
20
13
|
)
|
|
14
|
+
from xtgeo import Grid
|
|
21
15
|
|
|
22
16
|
from fmu.pem.pem_utilities import (
|
|
23
|
-
|
|
17
|
+
EffectiveMineralProperties,
|
|
24
18
|
PemConfig,
|
|
19
|
+
RockMatrixProperties,
|
|
25
20
|
SimInitProperties,
|
|
26
21
|
filter_and_one_dim,
|
|
27
22
|
get_shale_fraction,
|
|
28
23
|
import_fractions,
|
|
29
|
-
ntg_to_shale_fraction,
|
|
30
|
-
read_ntg_grid,
|
|
31
24
|
reverse_filter_and_restore,
|
|
32
25
|
to_masked_array,
|
|
33
26
|
)
|
|
34
|
-
from fmu.pem.pem_utilities.enum_defs import MineralMixModel
|
|
27
|
+
from fmu.pem.pem_utilities.enum_defs import MineralMixModel
|
|
35
28
|
from fmu.pem.pem_utilities.pem_config_validation import (
|
|
36
29
|
MineralProperties,
|
|
37
30
|
)
|
|
38
31
|
|
|
39
32
|
|
|
40
33
|
def effective_mineral_properties(
|
|
41
|
-
root_dir: Path,
|
|
42
|
-
|
|
34
|
+
root_dir: Path,
|
|
35
|
+
matrix: RockMatrixProperties,
|
|
36
|
+
sim_init: SimInitProperties,
|
|
37
|
+
sim_grid: Grid,
|
|
38
|
+
) -> tuple[np.ma.MaskedArray | None, EffectiveMineralProperties]:
|
|
43
39
|
"""Estimate effective mineral properties for each grid cell
|
|
44
40
|
|
|
45
41
|
Args:
|
|
46
42
|
root_dir: start directory for running of PEM
|
|
47
|
-
|
|
43
|
+
matrix: rock matrix parameters
|
|
48
44
|
sim_init: simulation initial properties
|
|
49
45
|
|
|
50
46
|
Returns:
|
|
51
47
|
shale volume, effective mineral properties
|
|
52
48
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fractions[0] = ntg_to_shale_fraction(fractions[0], sim_init.poro)
|
|
67
|
-
vsh = get_shale_fraction(
|
|
68
|
-
fractions,
|
|
69
|
-
config.rock_matrix.fraction_names,
|
|
70
|
-
config.rock_matrix.shale_fractions,
|
|
71
|
-
)
|
|
49
|
+
fractions = import_fractions(
|
|
50
|
+
root_dir=root_dir,
|
|
51
|
+
fraction_path=matrix.volume_fractions.rel_path_fractions,
|
|
52
|
+
fraction_files=matrix.volume_fractions.fractions_prop_file_names,
|
|
53
|
+
fraction_names=matrix.fraction_names,
|
|
54
|
+
grd=sim_grid,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
vsh = get_shale_fraction(
|
|
58
|
+
fractions,
|
|
59
|
+
matrix.fraction_names,
|
|
60
|
+
matrix.shale_fractions,
|
|
61
|
+
)
|
|
72
62
|
|
|
73
|
-
mineral_names =
|
|
63
|
+
mineral_names = matrix.fraction_minerals
|
|
74
64
|
eff_min_props = estimate_effective_mineral_properties(
|
|
75
|
-
mineral_names, fractions,
|
|
65
|
+
mineral_names, fractions, matrix, sim_init.poro
|
|
76
66
|
)
|
|
77
67
|
return vsh, eff_min_props
|
|
78
68
|
|
|
79
69
|
|
|
80
70
|
def estimate_effective_mineral_properties(
|
|
81
|
-
fraction_names:
|
|
82
|
-
fractions:
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
fraction_names: str | list[str],
|
|
72
|
+
fractions: np.ma.MaskedArray | list[np.ma.MaskedArray],
|
|
73
|
+
matrix_params: RockMatrixProperties,
|
|
74
|
+
porosity: np.ma.MaskedArray,
|
|
75
|
+
) -> EffectiveMineralProperties:
|
|
85
76
|
"""Estimation of effective mineral properties must be able to handle cases where
|
|
86
77
|
there is a more complex combination of minerals than the standard sand/shale case.
|
|
87
78
|
For carbonates the input can be based on minerals (e.g. calcite, dolomite, quartz,
|
|
@@ -101,12 +92,16 @@ def estimate_effective_mineral_properties(
|
|
|
101
92
|
verify_mineral_inputs(
|
|
102
93
|
fraction_names,
|
|
103
94
|
fractions,
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
matrix_params.minerals,
|
|
96
|
+
matrix_params.complement,
|
|
106
97
|
)
|
|
107
98
|
|
|
108
99
|
fraction_names, fractions = normalize_mineral_fractions(
|
|
109
|
-
fraction_names,
|
|
100
|
+
fraction_names,
|
|
101
|
+
fractions,
|
|
102
|
+
matrix_params.complement,
|
|
103
|
+
porosity,
|
|
104
|
+
matrix_params.volume_fractions.fractions_are_mineral_fraction,
|
|
110
105
|
)
|
|
111
106
|
|
|
112
107
|
mask, *fractions = filter_and_one_dim(*fractions)
|
|
@@ -114,12 +109,12 @@ def estimate_effective_mineral_properties(
|
|
|
114
109
|
mu_list = []
|
|
115
110
|
rho_list = []
|
|
116
111
|
for name in fraction_names:
|
|
117
|
-
mineral =
|
|
112
|
+
mineral = matrix_params.minerals[name]
|
|
118
113
|
k_list.append(to_masked_array(mineral.bulk_modulus, fractions[0]))
|
|
119
114
|
mu_list.append(to_masked_array(mineral.shear_modulus, fractions[0]))
|
|
120
115
|
rho_list.append(to_masked_array(mineral.density, fractions[0]))
|
|
121
116
|
|
|
122
|
-
if
|
|
117
|
+
if matrix_params.mineral_mix_model == MineralMixModel.HASHIN_SHTRIKMAN:
|
|
123
118
|
eff_k, eff_mu = multi_hashin_shtrikman(
|
|
124
119
|
*[arr for prop in zip(k_list, mu_list, fractions) for arr in prop]
|
|
125
120
|
)
|
|
@@ -134,8 +129,8 @@ def estimate_effective_mineral_properties(
|
|
|
134
129
|
eff_min_k, eff_min_mu, eff_min_rho = reverse_filter_and_restore(
|
|
135
130
|
mask, eff_k, eff_mu, eff_rho
|
|
136
131
|
)
|
|
137
|
-
return
|
|
138
|
-
bulk_modulus=eff_min_k, shear_modulus=eff_min_mu,
|
|
132
|
+
return EffectiveMineralProperties(
|
|
133
|
+
bulk_modulus=eff_min_k, shear_modulus=eff_min_mu, density=eff_min_rho
|
|
139
134
|
)
|
|
140
135
|
|
|
141
136
|
|
|
@@ -166,9 +161,14 @@ def normalize_mineral_fractions(
|
|
|
166
161
|
names: str | list[str],
|
|
167
162
|
fracs: np.ma.MaskedArray | list[np.ma.MaskedArray],
|
|
168
163
|
complement: str,
|
|
169
|
-
|
|
164
|
+
porosity: np.ma.MaskedArray,
|
|
165
|
+
mineral_fractions: bool,
|
|
166
|
+
) -> tuple[list[str], list[np.ma.MaskedArray]]:
|
|
170
167
|
"""Normalizes mineral fractions and adds complement mineral if needed.
|
|
171
168
|
|
|
169
|
+
If the fractions are volume fractions, porosity must be taken into account
|
|
170
|
+
when the fractions are normalized.
|
|
171
|
+
|
|
172
172
|
When the sum of specified mineral fractions is less than 1.0, adds the complement
|
|
173
173
|
mineral to make up the remainder. For example, if shale is 0.6 (60%) and the
|
|
174
174
|
complement mineral is quartz, then quartz will be added at 0.4 (40%) to reach 100%.
|
|
@@ -182,49 +182,52 @@ def normalize_mineral_fractions(
|
|
|
182
182
|
complement: Name of mineral to use as complement if sum < 1.0
|
|
183
183
|
|
|
184
184
|
Returns:
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
185
|
+
tuple containing:
|
|
186
|
+
- list of mineral names (with complement added if needed)
|
|
187
|
+
- list of normalized mineral fractions as masked arrays
|
|
188
188
|
"""
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
# Decide the mode of normalization - volume or mineral fractions
|
|
190
|
+
normalize_sum = 1.0 if mineral_fractions else 1.0 - porosity
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
# Check for single or list of names and fractions
|
|
193
|
+
names = [names] if isinstance(names, str) else names
|
|
194
|
+
fracs = [fracs] if isinstance(fracs, np.ma.MaskedArray) else fracs
|
|
194
195
|
|
|
196
|
+
# Demand values in the range [0.0, 1.0]
|
|
195
197
|
for i, frac in enumerate(fracs):
|
|
196
|
-
if np.any(frac
|
|
198
|
+
if np.any(frac < 0.0) or np.any(frac > 1.0):
|
|
197
199
|
warn(
|
|
198
|
-
f"
|
|
200
|
+
f"fraction {names[i]} has values outside of range 0.0 to 1.0,"
|
|
199
201
|
f"clipped to range",
|
|
200
202
|
UserWarning,
|
|
201
203
|
)
|
|
202
204
|
fracs[i] = np.ma.MaskedArray(np.ma.clip(frac, 0.0, 1.0))
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if np.any(tot_fractions[~tot_fractions.mask] > 1.0 + TOLERANCE):
|
|
206
|
+
# Adjust values so that no cells exceed normalize_sum
|
|
207
|
+
tot_fractions = sum(fracs)
|
|
208
|
+
if np.ma.any(tot_fractions > normalize_sum):
|
|
209
209
|
warn(
|
|
210
|
-
|
|
211
|
-
f"{np.sum(tot_fractions[~tot_fractions.mask] > 1.0)} cells, is "
|
|
212
|
-
f"scaled to maximum 1.0.\n"
|
|
213
|
-
f"Max value is {np.max(tot_fractions[~tot_fractions.mask])}",
|
|
210
|
+
"sum of fractions has values above limit, rescaled to range",
|
|
214
211
|
UserWarning,
|
|
215
212
|
)
|
|
213
|
+
scale_factor = np.ma.max(tot_fractions / normalize_sum)
|
|
216
214
|
for i, frac in enumerate(fracs):
|
|
217
|
-
fracs[i] /=
|
|
215
|
+
fracs[i] /= scale_factor
|
|
218
216
|
|
|
219
|
-
|
|
217
|
+
# Add a complement fraction if needed
|
|
218
|
+
comp_fraction = normalize_sum - sum(fracs)
|
|
220
219
|
if np.any(comp_fraction > 0.0):
|
|
221
220
|
names = names + [complement]
|
|
222
221
|
fracs = fracs + [comp_fraction]
|
|
223
222
|
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
# Rescale from volume fractions to mineral fractions if needed
|
|
224
|
+
if not mineral_fractions:
|
|
225
|
+
for i, frac in enumerate(fracs):
|
|
226
|
+
fracs[i] /= normalize_sum
|
|
226
227
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
# Final check that all fractions sum to 1.0 for all cells
|
|
229
|
+
try:
|
|
230
|
+
np.testing.assert_allclose(sum(fracs), 1.0, rtol=1.0e-6, atol=1.0e-6)
|
|
231
|
+
except AssertionError as e:
|
|
232
|
+
raise ValueError(f"mineral fractions do not sum to 1: {e}") from e
|
|
233
|
+
return names, fracs
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# python
|
|
2
|
+
# File: src/fmu/pem/pem_functions/pressure_sensitivity.py
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from fmu.pem.pem_utilities.enum_defs import (
|
|
11
|
+
ParameterTypes,
|
|
12
|
+
PhysicsPressureModelTypes,
|
|
13
|
+
RegressionPressureParameterTypes,
|
|
14
|
+
)
|
|
15
|
+
from fmu.pem.pem_utilities.pem_class_definitions import EffectiveMineralProperties
|
|
16
|
+
from fmu.pem.pem_utilities.rpm_models import (
|
|
17
|
+
MineralProperties,
|
|
18
|
+
PhysicsModelPressureSensitivity,
|
|
19
|
+
RegressionPressureSensitivity,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_FEATURE_NAME_MAP = {
|
|
23
|
+
ParameterTypes.VP.value: "VP",
|
|
24
|
+
ParameterTypes.VS.value: "VSX", # Model expects VSX for Vs
|
|
25
|
+
ParameterTypes.K.value: "K",
|
|
26
|
+
ParameterTypes.MU.value: "MU",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class RegressionPressureModel(Protocol):
|
|
32
|
+
"""Protocol for regression-based pressure sensitivity models."""
|
|
33
|
+
|
|
34
|
+
mode: RegressionPressureParameterTypes
|
|
35
|
+
|
|
36
|
+
def predict_elastic_properties(
|
|
37
|
+
self,
|
|
38
|
+
prop1: np.ndarray,
|
|
39
|
+
prop2: np.ndarray,
|
|
40
|
+
in_situ_press: np.ndarray,
|
|
41
|
+
depl_press: np.ndarray,
|
|
42
|
+
) -> tuple[np.ndarray, np.ndarray]: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@runtime_checkable
|
|
46
|
+
class PhysicsPressureModel(Protocol):
|
|
47
|
+
"""Protocol for physics-based pressure sensitivity models."""
|
|
48
|
+
|
|
49
|
+
model_type: PhysicsPressureModelTypes
|
|
50
|
+
|
|
51
|
+
def predict_elastic_properties(
|
|
52
|
+
self,
|
|
53
|
+
k_dry: np.ndarray,
|
|
54
|
+
mu_dry: np.ndarray,
|
|
55
|
+
poro: np.ndarray,
|
|
56
|
+
min_prop: MineralProperties,
|
|
57
|
+
in_situ_press: np.ndarray,
|
|
58
|
+
depl_press: np.ndarray,
|
|
59
|
+
cem_prop: MineralProperties | None = None,
|
|
60
|
+
) -> tuple[np.ndarray, np.ndarray]: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_array_shapes(
|
|
64
|
+
*arrays: np.ndarray,
|
|
65
|
+
names: list[str] | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Validate that all arrays have the same first dimension.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
*arrays : np.ndarray
|
|
73
|
+
Arrays to validate.
|
|
74
|
+
names : list[str] | None
|
|
75
|
+
Names for error messages. If None, uses generic labels.
|
|
76
|
+
|
|
77
|
+
Raises
|
|
78
|
+
------
|
|
79
|
+
PressureSensitivityInputError
|
|
80
|
+
If array shapes are inconsistent.
|
|
81
|
+
"""
|
|
82
|
+
if not arrays:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
expected_shape = arrays[0].shape[0]
|
|
86
|
+
names = names or [f"array_{i}" for i in range(len(arrays))]
|
|
87
|
+
|
|
88
|
+
for arr, name in zip(arrays, names):
|
|
89
|
+
if arr.shape[0] != expected_shape:
|
|
90
|
+
raise PressureSensitivityInputError(
|
|
91
|
+
f"Shape mismatch for '{name}': expected {expected_shape}, "
|
|
92
|
+
f"got {arr.shape[0]}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _validate_required_keys(
|
|
97
|
+
provided: dict[str, np.ndarray],
|
|
98
|
+
required: set[str],
|
|
99
|
+
dict_name: str,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Validate that all required keys exist in provided dictionary.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
provided : dict[str, np.ndarray]
|
|
107
|
+
Dictionary to validate.
|
|
108
|
+
required : set[str]
|
|
109
|
+
Required keys.
|
|
110
|
+
dict_name : str
|
|
111
|
+
Name for error messages.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
PressureSensitivityInputError
|
|
116
|
+
If any required key is missing.
|
|
117
|
+
"""
|
|
118
|
+
missing = required - set(provided.keys())
|
|
119
|
+
if missing:
|
|
120
|
+
raise PressureSensitivityInputError(
|
|
121
|
+
f"Missing keys {sorted(missing)} in {dict_name}; "
|
|
122
|
+
f"required={sorted(required)}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extract_input_properties(
|
|
127
|
+
in_situ_dict: dict[str, np.ndarray],
|
|
128
|
+
mode: RegressionPressureParameterTypes,
|
|
129
|
+
rho: np.ndarray,
|
|
130
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
131
|
+
"""
|
|
132
|
+
Extract or compute the two elastic properties needed for the model.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
in_situ_dict : dict[str, np.ndarray]
|
|
137
|
+
Dictionary with in-situ properties. Must contain either (vp, vs) or (k, mu).
|
|
138
|
+
mode : RegressionPressureParameterTypes
|
|
139
|
+
Model mode determining which properties are needed.
|
|
140
|
+
rho : np.ndarray
|
|
141
|
+
Density array for conversions.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
tuple[np.ndarray, np.ndarray]
|
|
146
|
+
(prop1, prop2) matching the model mode:
|
|
147
|
+
- VP_VS mode: (vp, vs)
|
|
148
|
+
- K_MU mode: (k, mu)
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
PressureSensitivityInputError
|
|
153
|
+
If required properties cannot be obtained.
|
|
154
|
+
"""
|
|
155
|
+
from rock_physics_open.equinor_utilities.std_functions import moduli, velocity
|
|
156
|
+
|
|
157
|
+
vp_key = ParameterTypes.VP.value
|
|
158
|
+
vs_key = ParameterTypes.VS.value
|
|
159
|
+
k_key = ParameterTypes.K.value
|
|
160
|
+
mu_key = ParameterTypes.MU.value
|
|
161
|
+
|
|
162
|
+
has_velocities = vp_key in in_situ_dict and vs_key in in_situ_dict
|
|
163
|
+
has_moduli = k_key in in_situ_dict and mu_key in in_situ_dict
|
|
164
|
+
|
|
165
|
+
if mode == RegressionPressureParameterTypes.VP_VS:
|
|
166
|
+
if has_velocities:
|
|
167
|
+
return in_situ_dict[vp_key], in_situ_dict[vs_key]
|
|
168
|
+
if has_moduli:
|
|
169
|
+
# Convert from moduli to velocities
|
|
170
|
+
vp, vs = velocity(in_situ_dict[k_key], in_situ_dict[mu_key], rho)[0:2]
|
|
171
|
+
return vp, vs
|
|
172
|
+
raise PressureSensitivityInputError(
|
|
173
|
+
f"For VP_VS mode, need either ({vp_key}, {vs_key}) or ({k_key}, {mu_key})"
|
|
174
|
+
)
|
|
175
|
+
# K_MU mode
|
|
176
|
+
if has_moduli:
|
|
177
|
+
return in_situ_dict[k_key], in_situ_dict[mu_key]
|
|
178
|
+
if has_velocities:
|
|
179
|
+
# Convert from velocities to moduli
|
|
180
|
+
k, mu = moduli(in_situ_dict[vp_key], in_situ_dict[vs_key], rho)
|
|
181
|
+
return k, mu
|
|
182
|
+
raise PressureSensitivityInputError(
|
|
183
|
+
f"For K_MU mode, need either ({k_key}, {mu_key}) or ({vp_key}, {vs_key})"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _compute_all_elastic_properties(
|
|
188
|
+
prop1: np.ndarray,
|
|
189
|
+
prop2: np.ndarray,
|
|
190
|
+
rho: np.ndarray,
|
|
191
|
+
mode: RegressionPressureParameterTypes,
|
|
192
|
+
) -> dict[str, np.ndarray]:
|
|
193
|
+
"""
|
|
194
|
+
Compute all four elastic properties from the two predicted ones.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
prop1 : np.ndarray
|
|
199
|
+
First predicted property (vp or k).
|
|
200
|
+
prop2 : np.ndarray
|
|
201
|
+
Second predicted property (vs or mu).
|
|
202
|
+
rho : np.ndarray
|
|
203
|
+
Density array.
|
|
204
|
+
mode : RegressionPressureParameterTypes
|
|
205
|
+
Model mode indicating which properties were predicted.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
dict[str, np.ndarray]
|
|
210
|
+
Dictionary containing vp, vs, k, mu, and rho.
|
|
211
|
+
"""
|
|
212
|
+
from rock_physics_open.equinor_utilities.std_functions import moduli, velocity
|
|
213
|
+
|
|
214
|
+
if mode == RegressionPressureParameterTypes.VP_VS:
|
|
215
|
+
vp, vs = prop1, prop2
|
|
216
|
+
k, mu = moduli(vp, vs, rho)
|
|
217
|
+
else: # K_MU mode
|
|
218
|
+
k, mu = prop1, prop2
|
|
219
|
+
vp, vs = velocity(k, mu, rho)[0:2]
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
ParameterTypes.VP.value: vp,
|
|
223
|
+
ParameterTypes.VS.value: vs,
|
|
224
|
+
ParameterTypes.K.value: k,
|
|
225
|
+
ParameterTypes.MU.value: mu,
|
|
226
|
+
ParameterTypes.RHO.value: rho,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PressureSensitivityInputError(ValueError):
|
|
231
|
+
"""Raised when required pressure sensitivity inputs are missing or inconsistent."""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _validate_required(
|
|
235
|
+
provided: dict[str, np.ndarray],
|
|
236
|
+
required: Iterable[str],
|
|
237
|
+
dict_name: str,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Validate that all required keys exist in a provided dictionary.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
provided : dict[str, np.ndarray]
|
|
245
|
+
Dictionary containing arrays for rock properties.
|
|
246
|
+
required : Iterable[str]
|
|
247
|
+
Keys that must be present.
|
|
248
|
+
dict_name : str
|
|
249
|
+
Name used in error messages.
|
|
250
|
+
|
|
251
|
+
Raises
|
|
252
|
+
------
|
|
253
|
+
PressureSensitivityInputError
|
|
254
|
+
If any required key is missing.
|
|
255
|
+
"""
|
|
256
|
+
missing = [k for k in required if k not in provided]
|
|
257
|
+
if missing:
|
|
258
|
+
raise PressureSensitivityInputError(
|
|
259
|
+
f"Missing keys {missing} in {dict_name}; required={list(required)}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _as_enum_mode(
|
|
264
|
+
mode: RegressionPressureParameterTypes | str,
|
|
265
|
+
) -> RegressionPressureParameterTypes:
|
|
266
|
+
"""
|
|
267
|
+
Normalize mode argument to RegressionPressureParameterTypes enum.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
mode : RegressionPressureParameterTypes | str
|
|
272
|
+
Mode specification ('vp_vs' or 'k_mu').
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
RegressionPressureParameterTypes
|
|
277
|
+
Normalized enum value.
|
|
278
|
+
|
|
279
|
+
Raises
|
|
280
|
+
------
|
|
281
|
+
ValueError
|
|
282
|
+
If unsupported mode supplied.
|
|
283
|
+
"""
|
|
284
|
+
if isinstance(mode, RegressionPressureParameterTypes):
|
|
285
|
+
return mode
|
|
286
|
+
try:
|
|
287
|
+
return RegressionPressureParameterTypes(mode)
|
|
288
|
+
except ValueError as exc:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"Unsupported mode '{mode}'. Expected 'vp_vs' or 'k_mu'."
|
|
291
|
+
) from exc
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def apply_dry_rock_pressure_sensitivity_model(
|
|
295
|
+
model: RegressionPressureSensitivity | PhysicsModelPressureSensitivity,
|
|
296
|
+
initial_eff_pressure: np.ndarray,
|
|
297
|
+
depleted_eff_pressure: np.ndarray,
|
|
298
|
+
in_situ_dict: dict[str, np.ndarray],
|
|
299
|
+
mineral_properties: MineralProperties | EffectiveMineralProperties | None = None,
|
|
300
|
+
cement_properties: MineralProperties | EffectiveMineralProperties | None = None,
|
|
301
|
+
) -> dict[str, np.ndarray]:
|
|
302
|
+
"""
|
|
303
|
+
Apply pressure sensitivity model to estimate depleted elastic properties.
|
|
304
|
+
|
|
305
|
+
Handles both regression-based and physics-based pressure sensitivity models
|
|
306
|
+
with their different input requirements.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
----------
|
|
310
|
+
model : RegressionPressureSensitivity | PhysicsModelPressureSensitivity
|
|
311
|
+
Pressure sensitivity model instance.
|
|
312
|
+
initial_eff_pressure : np.ndarray
|
|
313
|
+
In-situ effective (pore) pressure [Pa], shape (n,).
|
|
314
|
+
depleted_eff_pressure : np.ndarray
|
|
315
|
+
Depleted effective pressure [Pa], shape (n,).
|
|
316
|
+
in_situ_dict : dict[str, np.ndarray]
|
|
317
|
+
Dictionary with in-situ properties. Must contain 'rho'.
|
|
318
|
+
For regression models: requires ('vp', 'vs') or ('k', 'mu').
|
|
319
|
+
For physics models: requires ('k', 'mu', 'porosity').
|
|
320
|
+
mineral_properties : MineralProperties | None
|
|
321
|
+
Required for physics-based models. Mineral elastic properties.
|
|
322
|
+
cement_properties : MineralProperties | None
|
|
323
|
+
Required for patchy cement physics model.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
dict[str, np.ndarray]
|
|
328
|
+
Dictionary with 'vp', 'vs', 'k', 'mu', 'rho'.
|
|
329
|
+
|
|
330
|
+
Raises
|
|
331
|
+
------
|
|
332
|
+
PressureSensitivityInputError
|
|
333
|
+
If required inputs are missing or inconsistent.
|
|
334
|
+
"""
|
|
335
|
+
# Validate common inputs
|
|
336
|
+
_validate_required_keys(in_situ_dict, {ParameterTypes.RHO.value}, "in_situ_dict")
|
|
337
|
+
rho = in_situ_dict[ParameterTypes.RHO.value]
|
|
338
|
+
|
|
339
|
+
# Route to appropriate handler based on model type
|
|
340
|
+
if isinstance(model, RegressionPressureSensitivity):
|
|
341
|
+
return _apply_regression_model(
|
|
342
|
+
model, in_situ_dict, rho, initial_eff_pressure, depleted_eff_pressure
|
|
343
|
+
)
|
|
344
|
+
if isinstance(model, PhysicsModelPressureSensitivity):
|
|
345
|
+
return _apply_physics_model(
|
|
346
|
+
model,
|
|
347
|
+
in_situ_dict,
|
|
348
|
+
rho,
|
|
349
|
+
initial_eff_pressure,
|
|
350
|
+
depleted_eff_pressure,
|
|
351
|
+
mineral_properties,
|
|
352
|
+
cement_properties,
|
|
353
|
+
)
|
|
354
|
+
raise TypeError(f"Unsupported model type: {type(model)}")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _apply_regression_model(
|
|
358
|
+
model: RegressionPressureSensitivity,
|
|
359
|
+
in_situ_dict: dict[str, np.ndarray],
|
|
360
|
+
rho: np.ndarray,
|
|
361
|
+
pres_in_situ: np.ndarray,
|
|
362
|
+
pres_depleted: np.ndarray,
|
|
363
|
+
) -> dict[str, np.ndarray]:
|
|
364
|
+
"""Apply regression-based pressure sensitivity model."""
|
|
365
|
+
# Extract or compute input properties matching model mode
|
|
366
|
+
prop1_in_situ, prop2_in_situ = _extract_input_properties(
|
|
367
|
+
in_situ_dict, model.mode, rho
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Predict depleted properties
|
|
371
|
+
prop1_depleted, prop2_depleted = model.predict_elastic_properties(
|
|
372
|
+
prop1_in_situ, prop2_in_situ, pres_in_situ, pres_depleted
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Compute all elastic properties
|
|
376
|
+
return _compute_all_elastic_properties(
|
|
377
|
+
prop1_depleted, prop2_depleted, rho, model.mode
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _apply_physics_model(
|
|
382
|
+
model: PhysicsModelPressureSensitivity,
|
|
383
|
+
in_situ_dict: dict[str, np.ndarray],
|
|
384
|
+
rho: np.ndarray,
|
|
385
|
+
pres_in_situ: np.ndarray,
|
|
386
|
+
pres_depleted: np.ndarray,
|
|
387
|
+
mineral_properties: MineralProperties | EffectiveMineralProperties | None,
|
|
388
|
+
cement_properties: MineralProperties | EffectiveMineralProperties | None,
|
|
389
|
+
) -> dict[str, np.ndarray]:
|
|
390
|
+
"""Apply physics-based pressure sensitivity model."""
|
|
391
|
+
from rock_physics_open.equinor_utilities.std_functions import velocity
|
|
392
|
+
|
|
393
|
+
# Validate required inputs for physics models
|
|
394
|
+
if mineral_properties is None:
|
|
395
|
+
raise PressureSensitivityInputError(
|
|
396
|
+
"Physics-based models require mineral_properties"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
required_keys = {
|
|
400
|
+
ParameterTypes.K.value,
|
|
401
|
+
ParameterTypes.MU.value,
|
|
402
|
+
ParameterTypes.POROSITY.value,
|
|
403
|
+
}
|
|
404
|
+
_validate_required_keys(in_situ_dict, required_keys, "in_situ_dict")
|
|
405
|
+
|
|
406
|
+
k_dry = in_situ_dict[ParameterTypes.K.value]
|
|
407
|
+
mu_dry = in_situ_dict[ParameterTypes.MU.value]
|
|
408
|
+
poro = in_situ_dict[ParameterTypes.POROSITY.value]
|
|
409
|
+
|
|
410
|
+
# Predict depleted moduli
|
|
411
|
+
k_depleted, mu_depleted = model.predict_elastic_properties(
|
|
412
|
+
k_dry=k_dry,
|
|
413
|
+
mu_dry=mu_dry,
|
|
414
|
+
poro=poro,
|
|
415
|
+
min_prop=mineral_properties,
|
|
416
|
+
in_situ_press=pres_in_situ,
|
|
417
|
+
depl_press=pres_depleted,
|
|
418
|
+
cem_prop=cement_properties,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Convert to velocities
|
|
422
|
+
vp, vs = velocity(k_depleted, mu_depleted, rho)[0:2]
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
ParameterTypes.VP.value: vp,
|
|
426
|
+
ParameterTypes.VS.value: vs,
|
|
427
|
+
ParameterTypes.K.value: k_depleted,
|
|
428
|
+
ParameterTypes.MU.value: mu_depleted,
|
|
429
|
+
ParameterTypes.RHO.value: rho,
|
|
430
|
+
}
|