fmu-pem 0.0.1__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 -48
- fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
- fmu/pem/pem_functions/fluid_properties.py +453 -246
- 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 +77 -4
- 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 +58 -69
- fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
- fmu/pem/pem_utilities/pem_config_validation.py +374 -149
- fmu/pem/pem_utilities/rpm_models.py +481 -83
- 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.1.dist-info → fmu_pem-0.0.3.dist-info}/METADATA +33 -28
- fmu_pem-0.0.3.dist-info/RECORD +39 -0
- fmu_pem-0.0.1.dist-info/RECORD +0 -37
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/WHEEL +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/entry_points.txt +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,90 +1,282 @@
|
|
|
1
|
-
from
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
2
5
|
|
|
3
6
|
from fmu.pem.pem_utilities import (
|
|
7
|
+
DryRockProperties,
|
|
4
8
|
EffectiveFluidProperties,
|
|
5
|
-
|
|
6
|
-
PemConfig,
|
|
9
|
+
EffectiveMineralProperties,
|
|
7
10
|
PressureProperties,
|
|
11
|
+
RockMatrixProperties,
|
|
8
12
|
SaturatedRockProperties,
|
|
9
13
|
SimInitProperties,
|
|
10
14
|
estimate_cement,
|
|
15
|
+
get_masked_array_mask,
|
|
16
|
+
set_mask,
|
|
17
|
+
to_masked_array,
|
|
18
|
+
)
|
|
19
|
+
from fmu.pem.pem_utilities.fipnum_pvtnum_utilities import (
|
|
20
|
+
input_num_string_to_list,
|
|
21
|
+
validate_zone_coverage,
|
|
11
22
|
)
|
|
12
23
|
from fmu.pem.pem_utilities.rpm_models import (
|
|
13
24
|
FriableRPM,
|
|
14
25
|
PatchyCementRPM,
|
|
15
|
-
|
|
26
|
+
RegressionRPM,
|
|
27
|
+
TMatrixRPM,
|
|
16
28
|
)
|
|
17
29
|
|
|
18
30
|
from .regression_models import run_regression_models
|
|
19
31
|
from .run_friable_model import run_friable
|
|
20
32
|
from .run_patchy_cement_model import run_patchy_cement
|
|
21
|
-
from .
|
|
33
|
+
from .run_t_matrix_model import run_t_matrix_model
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
def estimate_saturated_rock(
|
|
25
|
-
|
|
37
|
+
rock_matrix: RockMatrixProperties,
|
|
26
38
|
sim_init: SimInitProperties,
|
|
27
|
-
|
|
28
|
-
matrix_props:
|
|
39
|
+
press_props: list[PressureProperties],
|
|
40
|
+
matrix_props: EffectiveMineralProperties,
|
|
29
41
|
fluid_props: list[EffectiveFluidProperties],
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
model_directory: Path,
|
|
43
|
+
fipnum_param: np.ma.MaskedArray,
|
|
44
|
+
) -> tuple[list[SaturatedRockProperties], list[DryRockProperties]]:
|
|
45
|
+
"""Estimate saturated rock properties with zone-specific RPM selection.
|
|
46
|
+
|
|
47
|
+
Each FIPNUM zone (string specification allowing lists/ranges/wildcards) can have
|
|
48
|
+
its own rock physics model (Friable, Patchy Cement, Regression, T-Matrix). The
|
|
49
|
+
workflow per zone is:
|
|
50
|
+
1. Create zone-masked inputs (mask outside zone cells only for filtering).
|
|
51
|
+
2. Call the appropriate RPM wrapper which internally flattens inputs using
|
|
52
|
+
filter_and_one_dim, runs the physics, and restores to 3D using
|
|
53
|
+
reverse_filter_and_restore.
|
|
54
|
+
3. Copy computed data values back into the global 3D result grids for cells in
|
|
55
|
+
this zone. The original simulation mask (inactive cells) is preserved; no
|
|
56
|
+
per‑zone mask overwrites occur.
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- Intermediate zone masks are not propagated to final outputs; only the original
|
|
60
|
+
reservoir inactive mask (fipnum_param.mask) remains.
|
|
61
|
+
- NaN handling (invalid physics results) is deferred to later processing, not
|
|
62
|
+
adjusted here.
|
|
32
63
|
|
|
33
64
|
Args:
|
|
34
|
-
|
|
35
|
-
sim_init: initial properties
|
|
36
|
-
|
|
37
|
-
matrix_props:
|
|
38
|
-
fluid_props: effective fluid properties
|
|
65
|
+
rock_matrix: zone-aware rock matrix configuration
|
|
66
|
+
sim_init: simulation model initial properties (contains porosity, vsh, etc.)
|
|
67
|
+
press_props: effective / formation / overburden pressure objects per time step
|
|
68
|
+
matrix_props: effective mineral properties (already estimated upstream)
|
|
69
|
+
fluid_props: effective fluid properties per time step
|
|
70
|
+
model_directory: directory for model-specific parameter files (T-Matrix)
|
|
71
|
+
fipnum_param: FIPNUM grid partition descriptor
|
|
39
72
|
|
|
40
73
|
Returns:
|
|
41
|
-
|
|
74
|
+
List of SaturatedRockProperties (one per time step)
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: invalid zone coverage or unknown RPM type
|
|
42
78
|
"""
|
|
43
|
-
|
|
79
|
+
# Validate zone coverage
|
|
80
|
+
fipnum_strings: list[str] = [zone.fipnum for zone in rock_matrix.zone_regions]
|
|
81
|
+
validate_zone_coverage(fipnum_strings, fipnum_param, zone_name="FIPNUM")
|
|
82
|
+
|
|
83
|
+
# Get FIPNUM grid data and mask
|
|
84
|
+
fipnum_data = fipnum_param.data
|
|
85
|
+
fipnum_mask = get_masked_array_mask(fipnum_param)
|
|
86
|
+
|
|
87
|
+
# Initialize grids for each time step
|
|
88
|
+
sat_rock_props_list = [
|
|
89
|
+
SaturatedRockProperties(
|
|
90
|
+
vp=to_masked_array(np.nan, fipnum_param),
|
|
91
|
+
vs=to_masked_array(np.nan, fipnum_param),
|
|
92
|
+
density=to_masked_array(np.nan, fipnum_param),
|
|
93
|
+
)
|
|
94
|
+
for _ in fluid_props
|
|
95
|
+
]
|
|
96
|
+
dry_rock_props_list = [
|
|
97
|
+
DryRockProperties(
|
|
98
|
+
bulk_modulus=to_masked_array(np.nan, fipnum_param),
|
|
99
|
+
shear_modulus=to_masked_array(np.nan, fipnum_param),
|
|
100
|
+
density=to_masked_array(np.nan, fipnum_param),
|
|
101
|
+
)
|
|
102
|
+
for _ in fluid_props
|
|
103
|
+
]
|
|
104
|
+
# Process each zone with its specific rock physics model
|
|
105
|
+
# Get actual FIPNUM values present in grid for use with input_num_string_to_list
|
|
106
|
+
actual_fipnum_values = list(np.unique(fipnum_data[~fipnum_mask]).astype(int))
|
|
107
|
+
|
|
108
|
+
# Process each unique zone (may contain multiple FIPNUMs)
|
|
109
|
+
for zone_region in rock_matrix.zone_regions:
|
|
110
|
+
# Get all FIPNUM values for this zone using input_num_string_to_list
|
|
111
|
+
fipnum_values = input_num_string_to_list(
|
|
112
|
+
zone_region.fipnum, actual_fipnum_values
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Build combined mask for all FIPNUMs in this zone using vectorized operation
|
|
116
|
+
zone_mask = np.isin(fipnum_data, fipnum_values) & ~fipnum_mask
|
|
117
|
+
|
|
118
|
+
# Create zone-specific masked arrays by masking cells OUTSIDE the zone
|
|
119
|
+
# The RPM functions will call filter_and_one_dim internally to flatten arrays
|
|
120
|
+
# and remove masked values before calling rock_physics_open library
|
|
121
|
+
zone_porosity = np.ma.masked_where(~zone_mask, sim_init.poro)
|
|
122
|
+
|
|
123
|
+
zone_matrix_props = matrix_props.masked_where(zone_mask)
|
|
124
|
+
zone_fluid_props = [
|
|
125
|
+
fluid_date.masked_where(zone_mask) for fluid_date in fluid_props
|
|
126
|
+
]
|
|
127
|
+
zone_eff_pres = [pres_date.masked_where(zone_mask) for pres_date in press_props]
|
|
128
|
+
|
|
129
|
+
# Call the appropriate rock physics model for this zone
|
|
130
|
+
zone_sat_props, zone_dry_props = _call_zone_rpm_model(
|
|
131
|
+
zone_region=zone_region,
|
|
132
|
+
rock_matrix=rock_matrix,
|
|
133
|
+
sim_init=sim_init,
|
|
134
|
+
zone_porosity=zone_porosity,
|
|
135
|
+
zone_matrix_props=zone_matrix_props,
|
|
136
|
+
zone_fluid_props=zone_fluid_props,
|
|
137
|
+
zone_eff_pres=zone_eff_pres,
|
|
138
|
+
model_directory=model_directory,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Merge zone results into the full grid for each time step (data only;
|
|
142
|
+
# mask preserved)
|
|
143
|
+
for time_idx, (zone_props, dry_props) in enumerate(
|
|
144
|
+
zip(zone_sat_props, zone_dry_props)
|
|
145
|
+
):
|
|
146
|
+
sat_rock_props_list[time_idx].vp.data[zone_mask] = zone_props.vp.data[
|
|
147
|
+
zone_mask
|
|
148
|
+
]
|
|
149
|
+
sat_rock_props_list[time_idx].vs.data[zone_mask] = zone_props.vs.data[
|
|
150
|
+
zone_mask
|
|
151
|
+
]
|
|
152
|
+
sat_rock_props_list[time_idx].density.data[zone_mask] = (
|
|
153
|
+
zone_props.density.data[zone_mask]
|
|
154
|
+
)
|
|
155
|
+
dry_rock_props_list[time_idx].bulk_modulus.data[zone_mask] = (
|
|
156
|
+
dry_props.bulk_modulus.data[zone_mask]
|
|
157
|
+
)
|
|
158
|
+
dry_rock_props_list[time_idx].shear_modulus.data[zone_mask] = (
|
|
159
|
+
dry_props.shear_modulus.data[zone_mask]
|
|
160
|
+
)
|
|
161
|
+
dry_rock_props_list[time_idx].density.data[zone_mask] = (
|
|
162
|
+
dry_props.density.data[zone_mask]
|
|
163
|
+
)
|
|
164
|
+
# Recalculate derived properties (ai, si, vpvs) after all zones have been
|
|
165
|
+
# merged
|
|
166
|
+
for sat_props in sat_rock_props_list:
|
|
167
|
+
sat_props.recalculate_derived()
|
|
168
|
+
|
|
169
|
+
return sat_rock_props_list, dry_rock_props_list
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _call_zone_rpm_model(
|
|
173
|
+
zone_region,
|
|
174
|
+
rock_matrix: RockMatrixProperties,
|
|
175
|
+
sim_init: SimInitProperties,
|
|
176
|
+
zone_porosity: np.ma.MaskedArray,
|
|
177
|
+
zone_matrix_props: EffectiveMineralProperties,
|
|
178
|
+
zone_fluid_props: list[EffectiveFluidProperties],
|
|
179
|
+
zone_eff_pres: list[PressureProperties],
|
|
180
|
+
model_directory: Path,
|
|
181
|
+
) -> tuple[list[SaturatedRockProperties], list[DryRockProperties]]:
|
|
182
|
+
"""Call the appropriate rock physics model for a specific zone.
|
|
183
|
+
|
|
184
|
+
This helper function dispatches to the correct RPM model (Patchy Cement, Friable,
|
|
185
|
+
Regression, or T-Matrix) based on the zone's configuration. It creates a temporary
|
|
186
|
+
RockMatrixProperties object with zone-specific model parameters.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
zone_region: zone-specific rock matrix parameters
|
|
190
|
+
rock_matrix: full rock matrix properties (for minerals and other shared config)
|
|
191
|
+
sim_init: initial simulation properties
|
|
192
|
+
zone_porosity: porosity for this zone only
|
|
193
|
+
zone_matrix_props: effective mineral properties for this zone
|
|
194
|
+
zone_fluid_props: effective fluid properties for this zone (per time step)
|
|
195
|
+
zone_eff_pres: effective pressure properties for this zone (per time step)
|
|
196
|
+
model_directory: directory for model files
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of SaturatedRockProperties for each time step for this zone
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If unknown rock physics model type is encountered
|
|
203
|
+
"""
|
|
204
|
+
# Create a simple object with zone-specific model attributes
|
|
205
|
+
# We can't instantiate RockMatrixProperties with already-instantiated zone_region
|
|
206
|
+
# because Pydantic validators expect dict input. Instead, create a namespace object
|
|
207
|
+
# with the attributes that RPM functions need.
|
|
208
|
+
|
|
209
|
+
# SimpleNamespace causes confusion for IDE linter, type is ignored below
|
|
210
|
+
zone_rock_matrix = SimpleNamespace(
|
|
211
|
+
model=zone_region.model,
|
|
212
|
+
pressure_sensitivity=zone_region.pressure_sensitivity,
|
|
213
|
+
pressure_sensitivity_model=zone_region.pressure_sensitivity_model,
|
|
214
|
+
minerals=rock_matrix.minerals,
|
|
215
|
+
cement=rock_matrix.cement,
|
|
216
|
+
volume_fractions=rock_matrix.volume_fractions,
|
|
217
|
+
fraction_names=rock_matrix.fraction_names,
|
|
218
|
+
fraction_minerals=rock_matrix.fraction_minerals,
|
|
219
|
+
shale_fractions=rock_matrix.shale_fractions,
|
|
220
|
+
complement=rock_matrix.complement,
|
|
221
|
+
mineral_mix_model=rock_matrix.mineral_mix_model,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if isinstance(zone_region.model, PatchyCementRPM):
|
|
44
225
|
# Patchy cement model
|
|
45
|
-
cement =
|
|
226
|
+
cement = rock_matrix.minerals[rock_matrix.cement]
|
|
46
227
|
cement_properties = estimate_cement(
|
|
47
228
|
density=cement.density,
|
|
48
229
|
bulk_modulus=cement.bulk_modulus,
|
|
49
230
|
shear_modulus=cement.shear_modulus,
|
|
50
|
-
grid=
|
|
231
|
+
grid=zone_porosity,
|
|
51
232
|
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
cement_properties,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
233
|
+
zone_sat_props, zone_dry_props = run_patchy_cement(
|
|
234
|
+
mineral=zone_matrix_props,
|
|
235
|
+
fluid=zone_fluid_props,
|
|
236
|
+
cement=cement_properties,
|
|
237
|
+
porosity=zone_porosity,
|
|
238
|
+
pressure=zone_eff_pres,
|
|
239
|
+
rock_matrix_props=zone_rock_matrix, # type: ignore
|
|
59
240
|
)
|
|
60
|
-
elif isinstance(
|
|
241
|
+
elif isinstance(zone_region.model, FriableRPM):
|
|
61
242
|
# Friable sandstone model
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
243
|
+
zone_sat_props, zone_dry_props = run_friable(
|
|
244
|
+
mineral=zone_matrix_props,
|
|
245
|
+
fluid=zone_fluid_props,
|
|
246
|
+
porosity=zone_porosity,
|
|
247
|
+
pressure=zone_eff_pres,
|
|
248
|
+
rock_matrix=zone_rock_matrix, # type: ignore
|
|
68
249
|
)
|
|
69
|
-
elif isinstance(
|
|
250
|
+
elif isinstance(zone_region.model, RegressionRPM):
|
|
70
251
|
# Regression models for dry rock properties, saturation by Gassmann
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
sim_init.poro,
|
|
75
|
-
eff_pres,
|
|
76
|
-
config,
|
|
77
|
-
vsh=sim_init.ntg_pem,
|
|
252
|
+
zone_vsh = set_mask(
|
|
253
|
+
masked_template=zone_porosity,
|
|
254
|
+
prop_array=sim_init.vsh_pem,
|
|
78
255
|
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
sim_init.ntg_pem,
|
|
87
|
-
eff_pres,
|
|
88
|
-
config,
|
|
256
|
+
zone_sat_props, zone_dry_props = run_regression_models(
|
|
257
|
+
matrix=zone_matrix_props,
|
|
258
|
+
fluid_properties=zone_fluid_props,
|
|
259
|
+
porosity=zone_porosity,
|
|
260
|
+
pressure=zone_eff_pres,
|
|
261
|
+
rock_matrix=zone_rock_matrix, # type: ignore
|
|
262
|
+
vsh=zone_vsh,
|
|
89
263
|
)
|
|
90
|
-
|
|
264
|
+
elif isinstance(zone_region.model, TMatrixRPM):
|
|
265
|
+
# T-Matrix model - estimates dry rock and saturated rock in one integrated model
|
|
266
|
+
zone_vsh = set_mask(
|
|
267
|
+
masked_template=zone_porosity,
|
|
268
|
+
prop_array=sim_init.vsh_pem,
|
|
269
|
+
)
|
|
270
|
+
zone_sat_props, zone_dry_props = run_t_matrix_model(
|
|
271
|
+
mineral_properties=zone_matrix_props,
|
|
272
|
+
fluid_properties=zone_fluid_props,
|
|
273
|
+
porosity=zone_porosity,
|
|
274
|
+
vsh=zone_vsh,
|
|
275
|
+
pressure=zone_eff_pres,
|
|
276
|
+
rock_matrix=zone_rock_matrix, # type: ignore
|
|
277
|
+
model_directory=model_directory,
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
raise ValueError(f"Unknown rock model type: {zone_region.model}")
|
|
281
|
+
|
|
282
|
+
return zone_sat_props, zone_dry_props
|