fmu-pem 0.0.1__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/__init__.py +2 -0
- fmu/pem/__init__.py +19 -0
- fmu/pem/__main__.py +53 -0
- fmu/pem/forward_models/__init__.py +7 -0
- fmu/pem/forward_models/pem_model.py +72 -0
- fmu/pem/hook_implementations/__init__.py +0 -0
- fmu/pem/hook_implementations/jobs.py +19 -0
- fmu/pem/pem_functions/__init__.py +17 -0
- fmu/pem/pem_functions/density.py +55 -0
- fmu/pem/pem_functions/effective_pressure.py +168 -0
- fmu/pem/pem_functions/estimate_saturated_rock.py +90 -0
- fmu/pem/pem_functions/fluid_properties.py +281 -0
- fmu/pem/pem_functions/mineral_properties.py +230 -0
- fmu/pem/pem_functions/regression_models.py +261 -0
- fmu/pem/pem_functions/run_friable_model.py +119 -0
- fmu/pem/pem_functions/run_patchy_cement_model.py +120 -0
- fmu/pem/pem_functions/run_t_matrix_and_pressure.py +186 -0
- fmu/pem/pem_utilities/__init__.py +66 -0
- fmu/pem/pem_utilities/cumsum_properties.py +104 -0
- fmu/pem/pem_utilities/delta_cumsum_time.py +104 -0
- fmu/pem/pem_utilities/enum_defs.py +54 -0
- fmu/pem/pem_utilities/export_routines.py +272 -0
- fmu/pem/pem_utilities/import_config.py +93 -0
- fmu/pem/pem_utilities/import_routines.py +161 -0
- fmu/pem/pem_utilities/pem_class_definitions.py +113 -0
- fmu/pem/pem_utilities/pem_config_validation.py +505 -0
- fmu/pem/pem_utilities/rpm_models.py +177 -0
- fmu/pem/pem_utilities/update_grid.py +54 -0
- fmu/pem/pem_utilities/utils.py +262 -0
- fmu/pem/run_pem.py +98 -0
- fmu/pem/version.py +21 -0
- fmu_pem-0.0.1.dist-info/METADATA +768 -0
- fmu_pem-0.0.1.dist-info/RECORD +37 -0
- fmu_pem-0.0.1.dist-info/WHEEL +5 -0
- fmu_pem-0.0.1.dist-info/entry_points.txt +5 -0
- fmu_pem-0.0.1.dist-info/licenses/LICENSE +674 -0
- fmu_pem-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Regression models for saturated sandstones
|
|
2
|
+
|
|
3
|
+
Regression based on polynomial models for saturated sandstones. The models are
|
|
4
|
+
based on porosity polynomials to estimate either Vp and Vs, or K and Mu.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import numpy.typing as npt
|
|
12
|
+
from rock_physics_open.equinor_utilities.std_functions import (
|
|
13
|
+
gassmann,
|
|
14
|
+
hashin_shtrikman_average,
|
|
15
|
+
moduli,
|
|
16
|
+
velocity,
|
|
17
|
+
voigt_reuss_hill,
|
|
18
|
+
)
|
|
19
|
+
from rock_physics_open.t_matrix_models import carbonate_pressure_model
|
|
20
|
+
|
|
21
|
+
from fmu.pem.pem_utilities import (
|
|
22
|
+
DryRockProperties,
|
|
23
|
+
EffectiveFluidProperties,
|
|
24
|
+
MatrixProperties,
|
|
25
|
+
PemConfig,
|
|
26
|
+
PressureProperties,
|
|
27
|
+
SaturatedRockProperties,
|
|
28
|
+
filter_and_one_dim,
|
|
29
|
+
reverse_filter_and_restore,
|
|
30
|
+
)
|
|
31
|
+
from fmu.pem.pem_utilities.enum_defs import MineralMixModel
|
|
32
|
+
from fmu.pem.pem_utilities.rpm_models import (
|
|
33
|
+
KMuRegressionParams,
|
|
34
|
+
VpVsRegressionParams,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def gen_regression(
|
|
39
|
+
porosity: np.ndarray, polynom_weights: list | np.ndarray
|
|
40
|
+
) -> np.ndarray:
|
|
41
|
+
"""Regression model for Vp
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
porosity: porosity [fraction]
|
|
45
|
+
polynom_weights: weights for the polynomial regression model
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Vp [m/s]
|
|
49
|
+
"""
|
|
50
|
+
pol = np.polynomial.Polynomial(polynom_weights)
|
|
51
|
+
return pol(porosity)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def dry_rock_regression(
|
|
55
|
+
porosity: npt.NDArray[np.float64] | np.ma.MaskedArray,
|
|
56
|
+
rho_min: npt.NDArray[np.float64] | np.ma.MaskedArray,
|
|
57
|
+
params: VpVsRegressionParams | KMuRegressionParams,
|
|
58
|
+
) -> DryRockProperties:
|
|
59
|
+
"""
|
|
60
|
+
Calculates dry rock properties based on porosity and polynomial weights.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
porosity: An array of porosity values.
|
|
64
|
+
rho_min: An array of mineral density values.
|
|
65
|
+
params: PemConfig object containing the regression model parameters.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A dictionary with keys corresponding to the calculated properties and
|
|
69
|
+
their values.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If an invalid mode is provided or necessary weights for
|
|
73
|
+
the selected mode are missing.
|
|
74
|
+
"""
|
|
75
|
+
if not params.rho_regression:
|
|
76
|
+
# rho_regression = False, use mineral density
|
|
77
|
+
rho_dry = rho_min * (1 - porosity)
|
|
78
|
+
else:
|
|
79
|
+
rho_dry = gen_regression(porosity, params.rho_weights)
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
params.mode == "vp_vs"
|
|
83
|
+
and params.vp_weights is not None
|
|
84
|
+
and params.vs_weights is not None
|
|
85
|
+
):
|
|
86
|
+
vp_dry = gen_regression(porosity, params.vp_weights)
|
|
87
|
+
vs_dry = gen_regression(porosity, params.vs_weights)
|
|
88
|
+
k_dry, mu = moduli(vp_dry, vs_dry, rho_dry)
|
|
89
|
+
elif (
|
|
90
|
+
params.mode == "k_mu"
|
|
91
|
+
and params.k_weights is not None
|
|
92
|
+
and params.mu_weights is not None
|
|
93
|
+
):
|
|
94
|
+
k_dry = gen_regression(porosity, params.k_weights)
|
|
95
|
+
mu = gen_regression(porosity, params.mu_weights)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("Invalid mode or missing weights for the selected mode.")
|
|
98
|
+
|
|
99
|
+
return DryRockProperties(
|
|
100
|
+
bulk_modulus=np.ma.MaskedArray(k_dry),
|
|
101
|
+
shear_modulus=np.ma.MaskedArray(mu),
|
|
102
|
+
dens=np.ma.MaskedArray(rho_dry),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_regression_models(
|
|
107
|
+
mineral: MatrixProperties,
|
|
108
|
+
fluid_properties: list[EffectiveFluidProperties],
|
|
109
|
+
porosity: np.ma.MaskedArray,
|
|
110
|
+
pressure: list[PressureProperties],
|
|
111
|
+
config: PemConfig,
|
|
112
|
+
vsh: Union[np.ma.MaskedArray, None] = None,
|
|
113
|
+
pres_model_vp: Path = Path("carbonate_pressure_model_vp_exp.pkl"),
|
|
114
|
+
pres_model_vs: Path = Path("carbonate_pressure_model_vs_exp.pkl"),
|
|
115
|
+
) -> List[SaturatedRockProperties]:
|
|
116
|
+
"""Run regression models for saturated rock properties.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
mineral: Mineral properties containing bulk modulus (k) [Pa],
|
|
120
|
+
shear modulus (mu) [Pa] and density (rho_sat) [kg/m3]
|
|
121
|
+
fluid_properties: List of fluid properties,
|
|
122
|
+
each containing bulk modulus (k) [Pa] and density (rho_sat) [kg/m3]
|
|
123
|
+
porosity: Porosity as a masked array [fraction]
|
|
124
|
+
pressure: List of pressure properties containing effective pressure values
|
|
125
|
+
[bar] following Eclipse reservoir simulator convention
|
|
126
|
+
config: Parameters for the PEM
|
|
127
|
+
vsh: Volume of shale as a masked array [fraction], optional
|
|
128
|
+
pres_model_vp: Path to the pressure sensitivity model file for P-wave velocity
|
|
129
|
+
(default: "carbonate_pressure_model_vp_exp_2023.pkl")
|
|
130
|
+
pres_model_vs: Path to the pressure sensitivity model file for S-wave velocity
|
|
131
|
+
(default: "carbonate_pressure_model_vs_exp_2023.pkl")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List[SaturatedRockProperties]: Saturated rock properties for each time step.
|
|
136
|
+
Only fluid properties change between time steps in this model.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
saturated_props = []
|
|
140
|
+
tmp_pres_over = None
|
|
141
|
+
tmp_pres_form = None
|
|
142
|
+
tmp_pres_depl = None
|
|
143
|
+
|
|
144
|
+
if vsh is None:
|
|
145
|
+
vsh = np.ma.MaskedArray(
|
|
146
|
+
data=np.zeros_like(porosity), mask=np.zeros_like(porosity).astype(bool)
|
|
147
|
+
)
|
|
148
|
+
multiple_lithologies = False
|
|
149
|
+
else:
|
|
150
|
+
multiple_lithologies = True
|
|
151
|
+
# Convert pressure from bar to Pa
|
|
152
|
+
pres_ovb = pressure[0].overburden_pressure * 1.0e5
|
|
153
|
+
pres_form = pressure[0].formation_pressure * 1.0e5
|
|
154
|
+
for time_step, fl_prop in enumerate(fluid_properties):
|
|
155
|
+
if time_step > 0 and config.rock_matrix.pressure_sensitivity:
|
|
156
|
+
pres_depl = pressure[time_step].formation_pressure * 1.0e5
|
|
157
|
+
(
|
|
158
|
+
mask,
|
|
159
|
+
tmp_min_k,
|
|
160
|
+
tmp_min_mu,
|
|
161
|
+
tmp_min_rho,
|
|
162
|
+
tmp_fl_prop_k,
|
|
163
|
+
tmp_fl_prop_rho,
|
|
164
|
+
tmp_por,
|
|
165
|
+
tmp_vsh,
|
|
166
|
+
tmp_pres_over,
|
|
167
|
+
tmp_pres_form,
|
|
168
|
+
tmp_pres_depl,
|
|
169
|
+
) = filter_and_one_dim(
|
|
170
|
+
mineral.bulk_modulus,
|
|
171
|
+
mineral.shear_modulus,
|
|
172
|
+
mineral.dens,
|
|
173
|
+
fl_prop.bulk_modulus,
|
|
174
|
+
fl_prop.dens,
|
|
175
|
+
porosity,
|
|
176
|
+
vsh,
|
|
177
|
+
pres_ovb,
|
|
178
|
+
pres_form,
|
|
179
|
+
pres_depl,
|
|
180
|
+
return_numpy_array=True,
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
(
|
|
184
|
+
mask,
|
|
185
|
+
tmp_min_k,
|
|
186
|
+
tmp_min_mu,
|
|
187
|
+
tmp_min_rho,
|
|
188
|
+
tmp_fl_prop_k,
|
|
189
|
+
tmp_fl_prop_rho,
|
|
190
|
+
tmp_por,
|
|
191
|
+
tmp_vsh,
|
|
192
|
+
) = filter_and_one_dim(
|
|
193
|
+
mineral.bulk_modulus,
|
|
194
|
+
mineral.shear_modulus,
|
|
195
|
+
mineral.dens,
|
|
196
|
+
fl_prop.bulk_modulus,
|
|
197
|
+
fl_prop.dens,
|
|
198
|
+
porosity,
|
|
199
|
+
vsh,
|
|
200
|
+
)
|
|
201
|
+
if not multiple_lithologies:
|
|
202
|
+
dry_props = dry_rock_regression(
|
|
203
|
+
tmp_por, tmp_min_rho, config.rock_matrix.rpm.parameters.sandstone
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
dry_props_sand = dry_rock_regression(
|
|
207
|
+
tmp_por, tmp_min_rho, config.rock_matrix.rpm.parameters.sandstone
|
|
208
|
+
)
|
|
209
|
+
dry_props_shale = dry_rock_regression(
|
|
210
|
+
tmp_por, tmp_min_rho, config.rock_matrix.rpm.parameters.shale
|
|
211
|
+
)
|
|
212
|
+
dry_rho = (
|
|
213
|
+
dry_props_sand.dens * (1.0 - tmp_vsh) + dry_props_shale.dens * tmp_vsh
|
|
214
|
+
)
|
|
215
|
+
if config.rock_matrix.mineral_mix_model == MineralMixModel.HASHIN_SHTRIKMAN:
|
|
216
|
+
k_dry, mu = hashin_shtrikman_average(
|
|
217
|
+
dry_props_sand.bulk_modulus,
|
|
218
|
+
dry_props_shale.bulk_modulus,
|
|
219
|
+
dry_props_sand.shear_modulus,
|
|
220
|
+
dry_props_shale.shear_modulus,
|
|
221
|
+
(1.0 - tmp_vsh),
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
k_dry, mu = voigt_reuss_hill(
|
|
225
|
+
dry_props_sand.bulk_modulus,
|
|
226
|
+
dry_props_shale.bulk_modulus,
|
|
227
|
+
dry_props_sand.shear_modulus,
|
|
228
|
+
dry_props_shale.shear_modulus,
|
|
229
|
+
(1.0 - tmp_vsh),
|
|
230
|
+
)
|
|
231
|
+
dry_props = DryRockProperties(
|
|
232
|
+
bulk_modulus=k_dry, shear_modulus=mu, dens=dry_rho
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
k_sat = gassmann(dry_props.bulk_modulus, tmp_por, tmp_fl_prop_k, tmp_min_k)
|
|
236
|
+
rho_sat = dry_props.dens + tmp_por * tmp_fl_prop_rho
|
|
237
|
+
vp, vs = velocity(k_sat, dry_props.shear_modulus, rho_sat)[0:2]
|
|
238
|
+
if time_step > 0 and config.rock_matrix.pressure_sensitivity:
|
|
239
|
+
# Inputs must be numpy arrays, not masked arrays
|
|
240
|
+
vp, vs, rho_sat, _, _ = carbonate_pressure_model(
|
|
241
|
+
tmp_fl_prop_rho,
|
|
242
|
+
vp.data,
|
|
243
|
+
vs.data,
|
|
244
|
+
rho_sat,
|
|
245
|
+
vp.data,
|
|
246
|
+
vs.data,
|
|
247
|
+
rho_sat,
|
|
248
|
+
tmp_por,
|
|
249
|
+
tmp_pres_over,
|
|
250
|
+
tmp_pres_form,
|
|
251
|
+
tmp_pres_depl,
|
|
252
|
+
pres_model_vp,
|
|
253
|
+
pres_model_vs,
|
|
254
|
+
config.paths.rel_path_pem.absolute(),
|
|
255
|
+
False,
|
|
256
|
+
)
|
|
257
|
+
vp, vs, rho_sat = reverse_filter_and_restore(mask, vp, vs, rho_sat)
|
|
258
|
+
props = SaturatedRockProperties(vp=vp, vs=vs, dens=rho_sat)
|
|
259
|
+
saturated_props.append(props)
|
|
260
|
+
|
|
261
|
+
return saturated_props
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from rock_physics_open.sandstone_models import friable_model
|
|
6
|
+
|
|
7
|
+
from fmu.pem.pem_utilities import (
|
|
8
|
+
EffectiveFluidProperties,
|
|
9
|
+
MatrixProperties,
|
|
10
|
+
PemConfig,
|
|
11
|
+
PressureProperties,
|
|
12
|
+
SaturatedRockProperties,
|
|
13
|
+
filter_and_one_dim,
|
|
14
|
+
reverse_filter_and_restore,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_friable(
|
|
19
|
+
mineral: MatrixProperties,
|
|
20
|
+
fluid: Union[list[EffectiveFluidProperties], EffectiveFluidProperties],
|
|
21
|
+
porosity: np.ma.MaskedArray,
|
|
22
|
+
pressure: Union[list[PressureProperties], PressureProperties],
|
|
23
|
+
config: PemConfig,
|
|
24
|
+
) -> List[SaturatedRockProperties]:
|
|
25
|
+
"""
|
|
26
|
+
Prepare inputs and parameters for running the Friable sandstone model
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
mineral: mineral properties containing k [Pa], mu [Pa] and rho [kg/m3]
|
|
30
|
+
fluid: fluid properties containing k [Pa] and rho [kg/m3], can be several fluid
|
|
31
|
+
properties in a list
|
|
32
|
+
porosity: porosity fraction
|
|
33
|
+
pressure: steps in effective pressure in [bar] due to Eclipse standard
|
|
34
|
+
config: parameters for the PEM
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
saturated rock properties with vp [m/s], vs [m/s], density [kg/m^3], ai
|
|
38
|
+
(vp * density), si (vs * density), vpvs (vp / vs)
|
|
39
|
+
"""
|
|
40
|
+
# Mineral and porosity are assumed to be single objects, fluid and
|
|
41
|
+
# effective_pressure can be lists
|
|
42
|
+
fluid, pressure = _verify_inputs(fluid, pressure)
|
|
43
|
+
saturated_props = []
|
|
44
|
+
friable_params = config.rock_matrix.rpm.parameters
|
|
45
|
+
for fl_prop, pres in zip(fluid, pressure):
|
|
46
|
+
(
|
|
47
|
+
mask,
|
|
48
|
+
tmp_min_k,
|
|
49
|
+
tmp_min_mu,
|
|
50
|
+
tmp_min_rho,
|
|
51
|
+
tmp_fl_prop_k,
|
|
52
|
+
tmp_fl_prop_rho,
|
|
53
|
+
tmp_por,
|
|
54
|
+
tmp_pres,
|
|
55
|
+
) = filter_and_one_dim(
|
|
56
|
+
mineral.bulk_modulus,
|
|
57
|
+
mineral.shear_modulus,
|
|
58
|
+
mineral.dens,
|
|
59
|
+
fl_prop.bulk_modulus,
|
|
60
|
+
fl_prop.dens,
|
|
61
|
+
porosity,
|
|
62
|
+
pres.effective_pressure * 1.0e5,
|
|
63
|
+
)
|
|
64
|
+
"""Estimation of effective mineral properties must be able to handle cases where
|
|
65
|
+
there is a more complex combination of minerals than the standard sand/shale
|
|
66
|
+
case. For carbonates the input can be based on minerals (e.g. calcite,
|
|
67
|
+
dolomite, quartz, smectite, ...) or PRTs (petrophysical rock types) that each
|
|
68
|
+
have been assigned elastic properties to."""
|
|
69
|
+
vp, vs, rho, _, _ = friable_model(
|
|
70
|
+
tmp_min_k,
|
|
71
|
+
tmp_min_mu,
|
|
72
|
+
tmp_min_rho,
|
|
73
|
+
tmp_fl_prop_k,
|
|
74
|
+
tmp_fl_prop_rho,
|
|
75
|
+
tmp_por,
|
|
76
|
+
tmp_pres,
|
|
77
|
+
friable_params.critical_porosity,
|
|
78
|
+
friable_params.coord_num_function,
|
|
79
|
+
friable_params.coordination_number,
|
|
80
|
+
friable_params.shear_reduction,
|
|
81
|
+
)
|
|
82
|
+
vp, vs, rho = reverse_filter_and_restore(mask, vp, vs, rho)
|
|
83
|
+
props = SaturatedRockProperties(vp=vp, vs=vs, dens=rho)
|
|
84
|
+
saturated_props.append(props)
|
|
85
|
+
return saturated_props
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _verify_inputs(fl_prop, pres_prop):
|
|
89
|
+
# Ensure that properties of input objects are masked arrays
|
|
90
|
+
def check_masked_arrays(inp_obj):
|
|
91
|
+
for value in inp_obj.values():
|
|
92
|
+
if not isinstance(value, np.ma.MaskedArray):
|
|
93
|
+
raise ValueError("Expected all input object values to be masked arrays")
|
|
94
|
+
|
|
95
|
+
if isinstance(fl_prop, list) and isinstance(pres_prop, list):
|
|
96
|
+
if not len(fl_prop) == len(pres_prop):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"{__file__}: unequal steps in fluid properties and pressure: "
|
|
99
|
+
f"{len(fl_prop)} vs. {len(pres_prop)}"
|
|
100
|
+
)
|
|
101
|
+
for item in fl_prop + pres_prop:
|
|
102
|
+
check_masked_arrays(asdict(item))
|
|
103
|
+
return fl_prop, pres_prop
|
|
104
|
+
if isinstance(fl_prop, EffectiveFluidProperties) and isinstance(
|
|
105
|
+
pres_prop, PressureProperties
|
|
106
|
+
):
|
|
107
|
+
check_masked_arrays(asdict(fl_prop))
|
|
108
|
+
check_masked_arrays(asdict(pres_prop))
|
|
109
|
+
return [
|
|
110
|
+
fl_prop,
|
|
111
|
+
], [
|
|
112
|
+
pres_prop,
|
|
113
|
+
]
|
|
114
|
+
# else:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"{__file__}: mismatch between fluid and pressure objects, both should "
|
|
117
|
+
f"either be lists or class objects, are {type(fl_prop)} and "
|
|
118
|
+
f"{type(pres_prop)}"
|
|
119
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from rock_physics_open.sandstone_models import (
|
|
5
|
+
patchy_cement_model_cem_frac as patchy_cement,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from fmu.pem.pem_utilities import (
|
|
9
|
+
EffectiveFluidProperties,
|
|
10
|
+
MatrixProperties,
|
|
11
|
+
PemConfig,
|
|
12
|
+
PressureProperties,
|
|
13
|
+
SaturatedRockProperties,
|
|
14
|
+
filter_and_one_dim,
|
|
15
|
+
reverse_filter_and_restore,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_patchy_cement(
|
|
20
|
+
mineral: MatrixProperties,
|
|
21
|
+
fluid: Union[list[EffectiveFluidProperties], EffectiveFluidProperties],
|
|
22
|
+
cement: MatrixProperties,
|
|
23
|
+
porosity: np.ma.MaskedArray,
|
|
24
|
+
pressure: Union[list[PressureProperties], PressureProperties],
|
|
25
|
+
config: PemConfig,
|
|
26
|
+
) -> List[SaturatedRockProperties]:
|
|
27
|
+
"""Prepare inputs and parameters for running the Patchy Cement model
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
mineral: mineral properties containing k [Pa], mu [Pa] and rho [kg/m3]
|
|
31
|
+
fluid: fluid properties containing k [Pa] and rho [kg/m3], can be several fluid
|
|
32
|
+
properties in a list
|
|
33
|
+
cement: cement properties containing k [Pa], mu [Pa] and rho [kg/m3]
|
|
34
|
+
porosity: porosity fraction
|
|
35
|
+
pressure: steps in effective pressure in [bar] due to Eclipse standard
|
|
36
|
+
config: parameters for the PEM
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
saturated rock properties with vp [m/s], vs [m/s], density [kg/m^3], ai
|
|
40
|
+
(vp * density), si (vs * density), vpvs (vp / vs)
|
|
41
|
+
"""
|
|
42
|
+
# Mineral and porosity are assumed to be single objects, fluid and
|
|
43
|
+
# effective_pressure can be lists
|
|
44
|
+
fluid, pressure = _verify_inputs(fluid, pressure)
|
|
45
|
+
saturated_props = []
|
|
46
|
+
pat_cem_params = config.rock_matrix.rpm.parameters
|
|
47
|
+
for fl_prop, pres in zip(fluid, pressure):
|
|
48
|
+
(
|
|
49
|
+
mask,
|
|
50
|
+
tmp_min_k,
|
|
51
|
+
tmp_min_mu,
|
|
52
|
+
tmp_min_rho,
|
|
53
|
+
tmp_cem_k,
|
|
54
|
+
tmp_cem_mu,
|
|
55
|
+
tmp_cem_rho,
|
|
56
|
+
tmp_fl_prop_k,
|
|
57
|
+
tmp_fl_prop_rho,
|
|
58
|
+
tmp_por,
|
|
59
|
+
tmp_pres,
|
|
60
|
+
) = filter_and_one_dim(
|
|
61
|
+
mineral.bulk_modulus,
|
|
62
|
+
mineral.shear_modulus,
|
|
63
|
+
mineral.dens,
|
|
64
|
+
cement.bulk_modulus,
|
|
65
|
+
cement.shear_modulus,
|
|
66
|
+
cement.dens,
|
|
67
|
+
fl_prop.bulk_modulus,
|
|
68
|
+
fl_prop.dens,
|
|
69
|
+
porosity,
|
|
70
|
+
pres.effective_pressure * 1.0e5,
|
|
71
|
+
return_numpy_array=True,
|
|
72
|
+
)
|
|
73
|
+
"""Estimation of effective mineral properties must be able to handle cases where
|
|
74
|
+
there is a more complex combination of minerals than the standard sand/shale
|
|
75
|
+
case. For carbonates the input can be based on minerals (e.g. calcite,
|
|
76
|
+
dolomite, quartz, smectite, ...) or PRTs (petrophysical rock types) that each
|
|
77
|
+
have been assigned elastic properties to."""
|
|
78
|
+
vp, vs, rho, _, _ = patchy_cement(
|
|
79
|
+
tmp_min_k,
|
|
80
|
+
tmp_min_mu,
|
|
81
|
+
tmp_min_rho,
|
|
82
|
+
tmp_cem_k,
|
|
83
|
+
tmp_cem_mu,
|
|
84
|
+
tmp_cem_rho,
|
|
85
|
+
tmp_fl_prop_k,
|
|
86
|
+
tmp_fl_prop_rho,
|
|
87
|
+
tmp_por,
|
|
88
|
+
tmp_pres,
|
|
89
|
+
pat_cem_params.cement_fraction,
|
|
90
|
+
pat_cem_params.critical_porosity,
|
|
91
|
+
pat_cem_params.coord_num_function,
|
|
92
|
+
pat_cem_params.coordination_number,
|
|
93
|
+
pat_cem_params.shear_reduction,
|
|
94
|
+
)
|
|
95
|
+
vp, vs, rho = reverse_filter_and_restore(mask, vp, vs, rho)
|
|
96
|
+
props = SaturatedRockProperties(vp=vp, vs=vs, dens=rho)
|
|
97
|
+
saturated_props.append(props)
|
|
98
|
+
return saturated_props
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _verify_inputs(fl_prop, pres_prop):
|
|
102
|
+
if isinstance(fl_prop, list) and isinstance(pres_prop, list):
|
|
103
|
+
if not len(fl_prop) == len(pres_prop):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"{__file__}: unequal steps in fluid properties and pressure: "
|
|
106
|
+
f"{len(fl_prop)} vs. {len(pres_prop)}"
|
|
107
|
+
)
|
|
108
|
+
return fl_prop, pres_prop
|
|
109
|
+
if isinstance(fl_prop, EffectiveFluidProperties) and (
|
|
110
|
+
isinstance(pres_prop, PressureProperties)
|
|
111
|
+
):
|
|
112
|
+
return [
|
|
113
|
+
fl_prop,
|
|
114
|
+
], [
|
|
115
|
+
pres_prop,
|
|
116
|
+
]
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"{__file__}: mismatch between fluid and pressure objects, both should either "
|
|
119
|
+
f"be lists or single objects, are {type(fl_prop)} and {type(pres_prop)}"
|
|
120
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from rock_physics_open.t_matrix_models import (
|
|
6
|
+
carbonate_pressure_model,
|
|
7
|
+
run_t_matrix_forward_model_with_opt_params_exp,
|
|
8
|
+
run_t_matrix_forward_model_with_opt_params_petec,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from fmu.pem.pem_utilities import (
|
|
12
|
+
EffectiveFluidProperties,
|
|
13
|
+
MatrixProperties,
|
|
14
|
+
PemConfig,
|
|
15
|
+
PressureProperties,
|
|
16
|
+
SaturatedRockProperties,
|
|
17
|
+
filter_and_one_dim,
|
|
18
|
+
reverse_filter_and_restore,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .run_patchy_cement_model import _verify_inputs
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_t_matrix_and_pressure_models(
|
|
25
|
+
mineral_properties: MatrixProperties,
|
|
26
|
+
fluid_properties: Union[list[EffectiveFluidProperties], EffectiveFluidProperties],
|
|
27
|
+
porosity: np.ma.MaskedArray,
|
|
28
|
+
vsh: Union[None, np.ma.MaskedArray],
|
|
29
|
+
pressure: Union[list[PressureProperties], PressureProperties],
|
|
30
|
+
config: PemConfig,
|
|
31
|
+
petec_parameter_file: Path = Path("t_mat_params_petec.pkl"),
|
|
32
|
+
exp_parameter_file: Path = Path("t_mat_params_exp.pkl"),
|
|
33
|
+
pres_model_vp: Path = Path("carbonate_pressure_model_vp_exp.pkl"),
|
|
34
|
+
pres_model_vs: Path = Path("carbonate_pressure_model_vs_exp.pkl"),
|
|
35
|
+
) -> List[SaturatedRockProperties]:
|
|
36
|
+
"""Model for carbonate rock with possibility to estimate changes due to
|
|
37
|
+
production, i.e., saturation changes, changes in the fluids due to pore pressure
|
|
38
|
+
change and also changes in the properties of the matrix by increase in effective
|
|
39
|
+
pressure.
|
|
40
|
+
|
|
41
|
+
The first timestep is regarded as in situ conditions for the pressure
|
|
42
|
+
substitution, any later timestep also takes into account the changes in effective
|
|
43
|
+
pressure from the initial one.
|
|
44
|
+
|
|
45
|
+
Calibration of parameters for the T-Matrix model must be made before running the
|
|
46
|
+
forward model, this is only possible in RokDoc with 1D match to logs.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
mineral_properties: bulk modulus (k) [Pa], shear modulus (mu) [Pa],
|
|
50
|
+
density [kg/m^3]
|
|
51
|
+
fluid_properties: bulk modulus (k) [Pa] and density [kg/m^3] for each restart
|
|
52
|
+
date
|
|
53
|
+
porosity: porosity [fraction]
|
|
54
|
+
vsh: shale volume [fraction]
|
|
55
|
+
pressure: overburden, effective and formation (pore) pressure per restart date
|
|
56
|
+
config: configuration parameters
|
|
57
|
+
petec_parameter_file: additional parameters for the T-Matrix model, determined
|
|
58
|
+
through optimisation to well logs
|
|
59
|
+
exp_parameter_file: additional parameters for the T-Matrix model, determined
|
|
60
|
+
through optimisation to well logs
|
|
61
|
+
pres_model_vp: pressure sensitivity model for vp
|
|
62
|
+
pres_model_vs: pressure sensitivity model for vs
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
saturated rock properties with vp [m/s], vs [m/s], density [kg/m^3],
|
|
66
|
+
ai (vp * density), si (vs * density), vpvs (vp / vs)
|
|
67
|
+
"""
|
|
68
|
+
fluid_properties, pressure = _verify_inputs(fluid_properties, pressure)
|
|
69
|
+
|
|
70
|
+
# All model files are gathered with the config file for the PEM model,
|
|
71
|
+
# i.e. in ../sim2seis/model
|
|
72
|
+
petec_parameter_file = config.paths.rel_path_pem / petec_parameter_file
|
|
73
|
+
exp_parameter_file = config.paths.rel_path_pem / exp_parameter_file
|
|
74
|
+
|
|
75
|
+
saturated_props = []
|
|
76
|
+
t_mat_params = config.rock_matrix.rpm.parameters
|
|
77
|
+
|
|
78
|
+
if vsh is None and t_mat_params.t_mat_model_version == "EXP":
|
|
79
|
+
raise ValueError("Shale volume must be provided for EXP model")
|
|
80
|
+
if vsh is None:
|
|
81
|
+
# For simplicity in the filtering, if no shale volume is provided, it is set
|
|
82
|
+
# to zero
|
|
83
|
+
vsh = np.ma.array(
|
|
84
|
+
np.zeros_like(porosity), mask=np.zeros_like(porosity).astype(bool)
|
|
85
|
+
)
|
|
86
|
+
# Change unit from bar to Pa
|
|
87
|
+
pres_ovb = pressure[0].overburden_pressure * 1.0e5
|
|
88
|
+
pres_form = pressure[0].formation_pressure * 1.0e5
|
|
89
|
+
|
|
90
|
+
for time_step, fl_prop in enumerate(fluid_properties):
|
|
91
|
+
if time_step > 0:
|
|
92
|
+
pres_depl = pressure[time_step].formation_pressure * 1.0e5
|
|
93
|
+
(
|
|
94
|
+
mask,
|
|
95
|
+
tmp_min_k,
|
|
96
|
+
tmp_min_mu,
|
|
97
|
+
tmp_min_rho,
|
|
98
|
+
tmp_fl_k,
|
|
99
|
+
tmp_fl_rho,
|
|
100
|
+
tmp_por,
|
|
101
|
+
tmp_vsh,
|
|
102
|
+
tmp_pres_over,
|
|
103
|
+
tmp_pres_form,
|
|
104
|
+
tmp_pres_depl,
|
|
105
|
+
) = filter_and_one_dim(
|
|
106
|
+
mineral_properties.bulk_modulus,
|
|
107
|
+
mineral_properties.shear_modulus,
|
|
108
|
+
mineral_properties.dens,
|
|
109
|
+
fl_prop.bulk_modulus,
|
|
110
|
+
fl_prop.dens,
|
|
111
|
+
porosity,
|
|
112
|
+
vsh,
|
|
113
|
+
pres_ovb,
|
|
114
|
+
pres_form,
|
|
115
|
+
pres_depl,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
tmp_pres_over, tmp_pres_form, tmp_pres_depl = (None, None, None)
|
|
119
|
+
(
|
|
120
|
+
mask,
|
|
121
|
+
tmp_min_k,
|
|
122
|
+
tmp_min_mu,
|
|
123
|
+
tmp_min_rho,
|
|
124
|
+
tmp_fl_k,
|
|
125
|
+
tmp_fl_rho,
|
|
126
|
+
tmp_por,
|
|
127
|
+
tmp_vsh,
|
|
128
|
+
) = filter_and_one_dim(
|
|
129
|
+
mineral_properties.bulk_modulus,
|
|
130
|
+
mineral_properties.shear_modulus,
|
|
131
|
+
mineral_properties.dens,
|
|
132
|
+
fl_prop.bulk_modulus,
|
|
133
|
+
fl_prop.dens,
|
|
134
|
+
porosity,
|
|
135
|
+
vsh,
|
|
136
|
+
)
|
|
137
|
+
if t_mat_params.t_mat_model_version == "PETEC":
|
|
138
|
+
vp, vs, rho, k, mu = run_t_matrix_forward_model_with_opt_params_petec(
|
|
139
|
+
tmp_min_k,
|
|
140
|
+
tmp_min_mu,
|
|
141
|
+
tmp_min_rho,
|
|
142
|
+
tmp_fl_k,
|
|
143
|
+
tmp_fl_rho,
|
|
144
|
+
tmp_por,
|
|
145
|
+
t_mat_params.angle,
|
|
146
|
+
t_mat_params.perm,
|
|
147
|
+
t_mat_params.visco,
|
|
148
|
+
t_mat_params.tau,
|
|
149
|
+
t_mat_params.freq,
|
|
150
|
+
str(petec_parameter_file),
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
vp, vs, rho, k, mu = run_t_matrix_forward_model_with_opt_params_exp(
|
|
154
|
+
tmp_fl_k,
|
|
155
|
+
tmp_fl_rho,
|
|
156
|
+
tmp_por,
|
|
157
|
+
tmp_vsh,
|
|
158
|
+
t_mat_params.angle,
|
|
159
|
+
t_mat_params.perm,
|
|
160
|
+
t_mat_params.visco,
|
|
161
|
+
t_mat_params.tau,
|
|
162
|
+
t_mat_params.freq,
|
|
163
|
+
str(exp_parameter_file),
|
|
164
|
+
)
|
|
165
|
+
if time_step > 0:
|
|
166
|
+
vp, vs, rho, _, _ = carbonate_pressure_model(
|
|
167
|
+
tmp_fl_rho,
|
|
168
|
+
vp,
|
|
169
|
+
vs,
|
|
170
|
+
rho,
|
|
171
|
+
vp,
|
|
172
|
+
vs,
|
|
173
|
+
rho,
|
|
174
|
+
tmp_por,
|
|
175
|
+
tmp_pres_over,
|
|
176
|
+
tmp_pres_form,
|
|
177
|
+
tmp_pres_depl,
|
|
178
|
+
pres_model_vp,
|
|
179
|
+
pres_model_vs,
|
|
180
|
+
config.paths.rel_path_pem.absolute(),
|
|
181
|
+
False,
|
|
182
|
+
)
|
|
183
|
+
vp, vs, rho = reverse_filter_and_restore(mask, vp, vs, rho)
|
|
184
|
+
props = SaturatedRockProperties(vp=vp, vs=vs, dens=rho)
|
|
185
|
+
saturated_props.append(props)
|
|
186
|
+
return saturated_props
|