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.
Files changed (37) hide show
  1. fmu/__init__.py +2 -0
  2. fmu/pem/__init__.py +19 -0
  3. fmu/pem/__main__.py +53 -0
  4. fmu/pem/forward_models/__init__.py +7 -0
  5. fmu/pem/forward_models/pem_model.py +72 -0
  6. fmu/pem/hook_implementations/__init__.py +0 -0
  7. fmu/pem/hook_implementations/jobs.py +19 -0
  8. fmu/pem/pem_functions/__init__.py +17 -0
  9. fmu/pem/pem_functions/density.py +55 -0
  10. fmu/pem/pem_functions/effective_pressure.py +168 -0
  11. fmu/pem/pem_functions/estimate_saturated_rock.py +90 -0
  12. fmu/pem/pem_functions/fluid_properties.py +281 -0
  13. fmu/pem/pem_functions/mineral_properties.py +230 -0
  14. fmu/pem/pem_functions/regression_models.py +261 -0
  15. fmu/pem/pem_functions/run_friable_model.py +119 -0
  16. fmu/pem/pem_functions/run_patchy_cement_model.py +120 -0
  17. fmu/pem/pem_functions/run_t_matrix_and_pressure.py +186 -0
  18. fmu/pem/pem_utilities/__init__.py +66 -0
  19. fmu/pem/pem_utilities/cumsum_properties.py +104 -0
  20. fmu/pem/pem_utilities/delta_cumsum_time.py +104 -0
  21. fmu/pem/pem_utilities/enum_defs.py +54 -0
  22. fmu/pem/pem_utilities/export_routines.py +272 -0
  23. fmu/pem/pem_utilities/import_config.py +93 -0
  24. fmu/pem/pem_utilities/import_routines.py +161 -0
  25. fmu/pem/pem_utilities/pem_class_definitions.py +113 -0
  26. fmu/pem/pem_utilities/pem_config_validation.py +505 -0
  27. fmu/pem/pem_utilities/rpm_models.py +177 -0
  28. fmu/pem/pem_utilities/update_grid.py +54 -0
  29. fmu/pem/pem_utilities/utils.py +262 -0
  30. fmu/pem/run_pem.py +98 -0
  31. fmu/pem/version.py +21 -0
  32. fmu_pem-0.0.1.dist-info/METADATA +768 -0
  33. fmu_pem-0.0.1.dist-info/RECORD +37 -0
  34. fmu_pem-0.0.1.dist-info/WHEEL +5 -0
  35. fmu_pem-0.0.1.dist-info/entry_points.txt +5 -0
  36. fmu_pem-0.0.1.dist-info/licenses/LICENSE +674 -0
  37. fmu_pem-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,54 @@
1
+ import warnings
2
+ from dataclasses import astuple
3
+
4
+ import numpy as np
5
+ import xtgeo
6
+
7
+ from .pem_class_definitions import SaturatedRockProperties
8
+
9
+
10
+ def update_inactive_grid_cells(
11
+ grid: xtgeo.grid3d.Grid,
12
+ props: list[SaturatedRockProperties],
13
+ ) -> xtgeo.grid3d.Grid:
14
+ """
15
+ Update the grid mask based on the mask of the properties
16
+
17
+ Args:
18
+ grid: original grid
19
+ props: list of saturated rock properties
20
+
21
+ Returns:
22
+ Grid with the same geometry, but with updated mask for inactive cells
23
+ """
24
+ # Make sure that the 'props' are of type SaturatedRockProperties
25
+ for prop in props:
26
+ if not isinstance(prop, SaturatedRockProperties):
27
+ raise ValueError(
28
+ f"Expected 'props' to be of type SaturatedRockProperties, got "
29
+ f"{type(prop)}"
30
+ )
31
+
32
+ grid_mask = grid.get_actnum(asmasked=True)
33
+
34
+ init_mask = np.zeros_like(grid.actnum_array).astype(bool)
35
+
36
+ for prop in props:
37
+ for prop_arr in astuple(prop): # noqa: type
38
+ init_mask = np.logical_or(init_mask, prop_arr.mask.astype(bool))
39
+
40
+ # To match the logic in xtgeo grid actnum, the mask must be inverted
41
+ init_mask = np.logical_not(init_mask)
42
+
43
+ if not np.all(init_mask == grid.actnum_array.astype(bool)):
44
+ warnings.warn(
45
+ f"There are undefined values in PEM results: "
46
+ f"{np.sum(np.logical_xor(init_mask, grid.actnum_array.astype(bool)))} "
47
+ f"cells are added to the model's inactive cells. \nPlease investigate the "
48
+ f"PEM's intermediate and final results for a cause."
49
+ )
50
+ init_mask = np.logical_and(init_mask, grid.actnum_array.astype(bool))
51
+ grid_mask.values = init_mask
52
+ grid.set_actnum(grid_mask)
53
+
54
+ return grid
@@ -0,0 +1,262 @@
1
+ import os
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+ from typing import List, Optional, Tuple, Union
5
+
6
+ import numpy as np
7
+ import xtgeo
8
+
9
+ from .pem_class_definitions import MatrixProperties
10
+ from .pem_config_validation import PemConfig
11
+
12
+
13
+ @contextmanager
14
+ def restore_dir(path: Path) -> None:
15
+ """restore_dir run block of code from a given path, restore original path
16
+
17
+ Args:
18
+ path: path where the call is made from
19
+
20
+ Returns:
21
+ None
22
+ """
23
+ old_pwd = Path.cwd()
24
+ os.chdir(path)
25
+ try:
26
+ yield
27
+ finally:
28
+ os.chdir(old_pwd)
29
+
30
+
31
+ def to_masked_array(
32
+ value: Union[float, int], masked_array: np.ma.MaskedArray
33
+ ) -> np.ma.MaskedArray:
34
+ """Create a masked array with a constant value from an int or float and a template
35
+ masked array
36
+
37
+ Args:
38
+ value: constant value for the returned masked array
39
+ masked_array: template for shape and mask of returned masked array
40
+
41
+ Returns:
42
+ constant value masked array
43
+ """
44
+ return np.ma.MaskedArray(value * np.ones_like(masked_array), mask=masked_array.mask)
45
+
46
+
47
+ def filter_and_one_dim(
48
+ *args: np.ma.MaskedArray, return_numpy_array: bool = False
49
+ ) -> tuple[np.ma.MaskedArray, ...]:
50
+ """Filters multiple masked arrays by removing masked values and flattens them to 1D.
51
+
52
+ Typically used in preparation for calling the rock-physics library.
53
+
54
+ Args:
55
+ *args: One or more masked arrays of identical shape. Each array contains data
56
+ with some values potentially masked as invalid.
57
+ return_numpy_array: If True, returns regular numpy arrays instead of
58
+ masked arrays for the filtered data. Defaults to False.
59
+
60
+ Returns:
61
+ tuple containing:
62
+ - mask: Boolean array of the same shape as inputs where True indicates
63
+ positions that were masked in any of the input arrays
64
+ - filtered arrays: One or more 1D arrays containing only the unmasked values
65
+ from each input array, in their original order
66
+ """
67
+ if not np.all([isinstance(arg, np.ma.MaskedArray) for arg in args]):
68
+ raise ValueError(f"{__file__}: all inputs should be numpy masked arrays")
69
+ mask = args[0].mask
70
+ for i in range(1, len(args)):
71
+ mask = np.logical_or(mask, args[i].mask)
72
+ if return_numpy_array:
73
+ out_args = [arg[~mask].data for arg in args]
74
+ else:
75
+ out_args = [arg[~mask] for arg in args]
76
+ return mask, *out_args
77
+
78
+
79
+ def reverse_filter_and_restore(
80
+ mask: np.ndarray, *args: np.ndarray
81
+ ) -> Tuple[np.ma.MaskedArray, ...]:
82
+ """Restores 1D filtered arrays back to their original shape with masking.
83
+
84
+ Typically called with results returned from the rock-physics library.
85
+
86
+ Args:
87
+ mask: Boolean array where True indicates positions that should be masked
88
+ in the restored arrays.
89
+ *args: One or more 1D numpy arrays containing the filtered values to be
90
+ restored. Each array should contain exactly enough values to fill
91
+ the unmasked positions in the mask.
92
+
93
+ Returns:
94
+ tuple of masked arrays where:
95
+ - Each array has the same shape as the input mask
96
+ - Unmasked positions contain values from the input args
97
+ - Masked positions (where mask is True) contain zeros and are masked
98
+ - All returned arrays share the same mask
99
+ """
100
+ out_args: list[np.ma.MaskedArray] = []
101
+ for arg in args:
102
+ tmp = np.zeros(mask.shape)
103
+ tmp[~mask] = arg
104
+ out_args.append(np.ma.MaskedArray(tmp, mask=mask))
105
+
106
+ return tuple(out_args)
107
+
108
+
109
+ def _verify_export_inputs(props, grid, dates, file_format=None):
110
+ if file_format is not None and file_format not in ["roff", "grdecl"]:
111
+ raise ValueError(
112
+ f'{__file__}: output file format must be one of "roff", "grdecl", is '
113
+ f"{file_format}"
114
+ )
115
+ if not isinstance(grid, xtgeo.grid3d.Grid):
116
+ raise ValueError(
117
+ f"{__file__}: model grid is not an xtgeo 3D grid, type: {type(grid)}"
118
+ )
119
+ if isinstance(props, list):
120
+ if isinstance(dates, list):
121
+ if len(props) == len(dates):
122
+ return props, dates
123
+ raise ValueError(
124
+ f"{__file__}: length of property list does not match the number of "
125
+ f"simulation model "
126
+ f"dates: {len(props)} vs. {len(dates)}"
127
+ )
128
+ if dates is None:
129
+ return props, [""] * len(props)
130
+ raise ValueError(
131
+ f"{__file__}: unknown input type, time_steps should be None or list, is "
132
+ f"{type(dates)}"
133
+ )
134
+ if isinstance(props, dict):
135
+ props = [
136
+ props,
137
+ ]
138
+ if dates is None:
139
+ return props, [
140
+ "",
141
+ ]
142
+ if isinstance(dates, list) and len(dates) == 1:
143
+ return props, dates
144
+ raise ValueError(
145
+ f"{__file__}: single length property list does not match the number of "
146
+ f"simulation model "
147
+ f"dates: {len(dates)}"
148
+ )
149
+ raise ValueError(
150
+ f"{__file__}: unknown input types, result_props should be list or dict, is "
151
+ f"{type(props)}, time_steps should be None or list, is {type(dates)}"
152
+ )
153
+
154
+
155
+ def ntg_to_shale_fraction(
156
+ ntg: np.ma.MaskedArray, por: np.ma.MaskedArray
157
+ ) -> np.ma.MaskedArray:
158
+ """Calculate sand and shale fraction from N/G property
159
+
160
+ Args:
161
+ ntg: net-to-gross property [fraction]
162
+ por: total porosity [fraction]
163
+
164
+ Returns:
165
+ shale fraction
166
+ """
167
+ clip_ntg: np.ma.MaskedArray = np.ma.clip(np.ma.MaskedArray(ntg), 0.0, 1.0) # type: ignore[assignment]
168
+ vsh: np.ma.MaskedArray = np.ma.MaskedArray(1.0 - clip_ntg)
169
+ return (vsh / (1.0 - por)).clip(0.0, 1.0)
170
+
171
+
172
+ def get_shale_fraction(
173
+ vol_fractions: List[np.ma.MaskedArray],
174
+ fraction_names: list[str],
175
+ shale_fraction_names: Optional[str | list[str]],
176
+ ) -> Optional[np.ma.MaskedArray]:
177
+ """
178
+
179
+ Args:
180
+ vol_fractions: volume fractions, already verified that there is consistency
181
+ between named fractions and available fractions in property file
182
+ fraction_names: names of the volume fractions
183
+ shale_fraction_names: Names of fractions that should be considered shale
184
+
185
+ Returns:
186
+ sum of volume fractions that are defined as shale, None if there are no defined
187
+ shale fractions
188
+ """
189
+
190
+ if not shale_fraction_names:
191
+ return None
192
+
193
+ if isinstance(shale_fraction_names, str):
194
+ shale_fraction_names = [shale_fraction_names]
195
+
196
+ sh_list: list[np.ma.MaskedArray] = []
197
+ for shale_name in shale_fraction_names:
198
+ try:
199
+ idx = fraction_names.index(shale_name)
200
+ sh_list.append(vol_fractions[idx])
201
+ except ValueError:
202
+ raise ValueError(f"unknown shale fraction: {shale_name}")
203
+
204
+ # Note that masked elements are set to 0 internally.
205
+ return np.ma.sum(sh_list, axis=0)
206
+
207
+
208
+ def estimate_cement(
209
+ bulk_modulus: float | int,
210
+ shear_modulus: float | int,
211
+ density: float | int,
212
+ grid: np.ma.MaskedArray,
213
+ ) -> MatrixProperties:
214
+ """Creates masked arrays filled with constant cement properties, matching the shape
215
+ and mask of the input grid.
216
+
217
+ Args:
218
+ bulk_modulus: Bulk modulus of the cement
219
+ shear_modulus: Shear modulus of the cement
220
+ density: Density of the cement
221
+ grid: Template array that defines the shape and mask for the output arrays
222
+
223
+ Returns:
224
+ cement properties as MatrixProperties containing constant-valued masked arrays
225
+ """
226
+ cement_k = to_masked_array(bulk_modulus, grid)
227
+ cement_mu = to_masked_array(shear_modulus, grid)
228
+ cement_rho = to_masked_array(density, grid)
229
+ return MatrixProperties(
230
+ bulk_modulus=cement_k, shear_modulus=cement_mu, dens=cement_rho
231
+ )
232
+
233
+
234
+ def update_dict_list(base_list: List[dict], add_list: List[dict]) -> List[dict]:
235
+ """Update/add new key/value pairs to dicts in list
236
+
237
+ Args:
238
+ base_list: original list of dicts
239
+ add_list: list of dicts to be added
240
+
241
+ Returns:
242
+ combined list of dicts
243
+ """
244
+ _verify_update_inputs(base_list, add_list)
245
+ for i, item in enumerate(add_list):
246
+ base_list[i].update(item)
247
+ return base_list
248
+
249
+
250
+ def _verify_update_inputs(base, add_list):
251
+ if not isinstance(base, list) and isinstance(add_list, list):
252
+ raise TypeError(f"{__file__}: inputs are not lists")
253
+ if not len(base) == len(add_list):
254
+ raise ValueError(
255
+ f"{__file__}: mismatch in list lengths: base list: {len(base)} vs. added "
256
+ f"list: {len(add_list)}"
257
+ )
258
+ if not (
259
+ all(isinstance(item, dict) for item in base)
260
+ and all(isinstance(item, dict) for item in add_list)
261
+ ):
262
+ raise TypeError(f"{__file__}: all items in input lists are not dict")
fmu/pem/run_pem.py ADDED
@@ -0,0 +1,98 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from fmu.pem import pem_functions as pem_fcns
5
+ from fmu.pem import pem_utilities as pem_utils
6
+
7
+
8
+ def pem_fcn(
9
+ start_dir: Path,
10
+ rel_path_pem: Path,
11
+ pem_config_file_name: Path,
12
+ run_from_rms=False,
13
+ proj=None,
14
+ ) -> None:
15
+ """
16
+ Run script for extended petro elastic module within sim2seis. Parameters in
17
+ yaml-file control the selections made in the PEM.
18
+
19
+ """
20
+ # Read and validate all PEM parameters
21
+ config = pem_utils.read_pem_config(
22
+ start_dir.joinpath(rel_path_pem, pem_config_file_name)
23
+ )
24
+
25
+ # Read necessary part of global configurations and parameters
26
+ config.update_with_global(
27
+ pem_utils.get_global_params_and_dates(
28
+ start_dir, config.paths.rel_path_fmu_config
29
+ )
30
+ )
31
+
32
+ # Import Eclipse simulation grid - INIT and RESTART
33
+ egrid_file = start_dir / config.paths.rel_path_simgrid / "ECLIPSE.EGRID"
34
+ init_property_file = start_dir / config.paths.rel_path_simgrid / "ECLIPSE.INIT"
35
+ restart_property_file = start_dir / config.paths.rel_path_simgrid / "ECLIPSE.UNRST"
36
+
37
+ sim_grid, constant_props, time_step_props = pem_utils.read_sim_grid_props(
38
+ egrid_file,
39
+ init_property_file,
40
+ restart_property_file,
41
+ config.global_params.seis_dates,
42
+ )
43
+
44
+ # Calculate rock properties - fluids and minerals
45
+ # Fluid properties calculated for all time-steps
46
+ fluid_properties = pem_fcns.effective_fluid_properties(
47
+ time_step_props, config.fluids
48
+ )
49
+
50
+ # Effective mineral (matrix) properties - one set valid for all time-steps
51
+ vsh, matrix_properties = pem_fcns.effective_mineral_properties(
52
+ start_dir, config, constant_props
53
+ )
54
+ # VSH is exported with other constant results, add it to the constant properties
55
+ constant_props.ntg_pem = vsh
56
+
57
+ # Estimate effective pressure
58
+ eff_pres = pem_fcns.estimate_pressure(
59
+ config, constant_props, time_step_props, matrix_properties, fluid_properties
60
+ )
61
+
62
+ # Estimate saturated rock properties
63
+ sat_rock_props = pem_fcns.estimate_saturated_rock(
64
+ config, constant_props, eff_pres, matrix_properties, fluid_properties
65
+ )
66
+
67
+ # Delta and cumulative time estimates (only TWT properties are kept)
68
+ sum_delta_time = pem_utils.delta_cumsum_time.estimate_sum_delta_time(
69
+ constant_props, sat_rock_props, config
70
+ )
71
+
72
+ # Calculate difference properties. Possible properties are all that vary with time
73
+ diff_props, diff_date_strs = pem_utils.calculate_diff_properties(
74
+ [time_step_props, eff_pres, sat_rock_props, sum_delta_time], config
75
+ )
76
+
77
+ # As a precaution, update the grid mask for inactive cells, based on the saturated
78
+ # rock properties
79
+ sim_grid = pem_utils.update_inactive_grid_cells(sim_grid, sat_rock_props)
80
+
81
+ # Save results to disk or RMS project according to settings in the PEM config
82
+ pem_utils.save_results(
83
+ start_dir,
84
+ run_from_rms,
85
+ config,
86
+ proj,
87
+ sim_grid,
88
+ eff_pres,
89
+ sat_rock_props,
90
+ diff_props,
91
+ diff_date_strs,
92
+ matrix_properties,
93
+ fluid_properties,
94
+ )
95
+
96
+ # Restore original path
97
+ os.chdir(start_dir)
98
+ return
fmu/pem/version.py ADDED
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.0.1'
21
+ __version_tuple__ = version_tuple = (0, 0, 1)