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.
Files changed (35) hide show
  1. fmu/pem/__main__.py +32 -16
  2. fmu/pem/forward_models/pem_model.py +19 -27
  3. fmu/pem/pem_functions/__init__.py +2 -2
  4. fmu/pem/pem_functions/density.py +32 -38
  5. fmu/pem/pem_functions/effective_pressure.py +153 -48
  6. fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
  7. fmu/pem/pem_functions/fluid_properties.py +453 -246
  8. fmu/pem/pem_functions/mineral_properties.py +77 -74
  9. fmu/pem/pem_functions/pressure_sensitivity.py +430 -0
  10. fmu/pem/pem_functions/regression_models.py +129 -97
  11. fmu/pem/pem_functions/run_friable_model.py +106 -37
  12. fmu/pem/pem_functions/run_patchy_cement_model.py +107 -45
  13. fmu/pem/pem_functions/{run_t_matrix_and_pressure.py → run_t_matrix_model.py} +48 -27
  14. fmu/pem/pem_utilities/__init__.py +31 -9
  15. fmu/pem/pem_utilities/cumsum_properties.py +29 -37
  16. fmu/pem/pem_utilities/delta_cumsum_time.py +8 -13
  17. fmu/pem/pem_utilities/enum_defs.py +77 -4
  18. fmu/pem/pem_utilities/export_routines.py +84 -72
  19. fmu/pem/pem_utilities/fipnum_pvtnum_utilities.py +217 -0
  20. fmu/pem/pem_utilities/import_config.py +64 -46
  21. fmu/pem/pem_utilities/import_routines.py +58 -69
  22. fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
  23. fmu/pem/pem_utilities/pem_config_validation.py +374 -149
  24. fmu/pem/pem_utilities/rpm_models.py +481 -83
  25. fmu/pem/pem_utilities/update_grid.py +3 -2
  26. fmu/pem/pem_utilities/utils.py +90 -38
  27. fmu/pem/run_pem.py +70 -39
  28. fmu/pem/version.py +16 -3
  29. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/METADATA +33 -28
  30. fmu_pem-0.0.3.dist-info/RECORD +39 -0
  31. fmu_pem-0.0.1.dist-info/RECORD +0 -37
  32. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/WHEEL +0 -0
  33. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/entry_points.txt +0 -0
  34. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/licenses/LICENSE +0 -0
  35. {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 typing import List
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
- MatrixProperties,
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
- RegressionModels,
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 .run_t_matrix_and_pressure import run_t_matrix_and_pressure_models
33
+ from .run_t_matrix_model import run_t_matrix_model
22
34
 
23
35
 
24
36
  def estimate_saturated_rock(
25
- config: PemConfig,
37
+ rock_matrix: RockMatrixProperties,
26
38
  sim_init: SimInitProperties,
27
- eff_pres: list[PressureProperties],
28
- matrix_props: MatrixProperties,
39
+ press_props: list[PressureProperties],
40
+ matrix_props: EffectiveMineralProperties,
29
41
  fluid_props: list[EffectiveFluidProperties],
30
- ) -> List[SaturatedRockProperties]:
31
- """Wrapper to call rock physics model
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
- config: PEM configuration parameters
35
- sim_init: initial properties from simulation model
36
- eff_pres: restart properties from simulation model
37
- matrix_props: rock properties (mineral and fluids)
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
- saturated rock properties per restart date
74
+ List of SaturatedRockProperties (one per time step)
75
+
76
+ Raises:
77
+ ValueError: invalid zone coverage or unknown RPM type
42
78
  """
43
- if isinstance(config.rock_matrix.rpm, PatchyCementRPM):
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 = config.rock_matrix.minerals[config.rock_matrix.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=sim_init.poro,
231
+ grid=zone_porosity,
51
232
  )
52
- sat_rock_props = run_patchy_cement(
53
- matrix_props,
54
- fluid_props,
55
- cement_properties,
56
- sim_init.poro,
57
- eff_pres,
58
- config,
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(config.rock_matrix.rpm, FriableRPM):
241
+ elif isinstance(zone_region.model, FriableRPM):
61
242
  # Friable sandstone model
62
- sat_rock_props = run_friable(
63
- matrix_props,
64
- fluid_props,
65
- sim_init.poro,
66
- eff_pres,
67
- config,
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(config.rock_matrix.rpm, RegressionModels):
250
+ elif isinstance(zone_region.model, RegressionRPM):
70
251
  # Regression models for dry rock properties, saturation by Gassmann
71
- sat_rock_props = run_regression_models(
72
- matrix_props,
73
- fluid_props,
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
- else:
80
- # Using default values for T-Matrix parameter file and vp and vs pressure
81
- # model files
82
- sat_rock_props = run_t_matrix_and_pressure_models(
83
- matrix_props,
84
- fluid_props,
85
- sim_init.poro,
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
- return sat_rock_props
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