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
fmu/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # This line ONLY:
2
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
fmu/pem/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+ The INTERNAL_EQUINOR property means that fmu-pem is run within the Equinor organisation
3
+ where there is access to more elaborate fluid models and other rock physics
4
+ models.
5
+ """
6
+
7
+ try: # noqa: SIM105
8
+ import init_rock_physics
9
+ except (ImportError, ModuleNotFoundError):
10
+ INTERNAL_EQUINOR = False
11
+ else:
12
+ INTERNAL_EQUINOR = True
13
+
14
+ from .run_pem import pem_fcn
15
+
16
+ __all__ = [
17
+ "pem_fcn",
18
+ "INTERNAL_EQUINOR",
19
+ ]
fmu/pem/__main__.py ADDED
@@ -0,0 +1,53 @@
1
+ # pylint: disable=missing-module-docstring
2
+ import argparse
3
+ from pathlib import Path
4
+ from warnings import warn
5
+
6
+ from .pem_utilities import restore_dir
7
+ from .run_pem import pem_fcn
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(__file__)
12
+ parser.add_argument(
13
+ "-s",
14
+ "--startdir",
15
+ type=Path,
16
+ required=True,
17
+ help="Start directory for running script (required)",
18
+ )
19
+ parser.add_argument(
20
+ "-c",
21
+ "--configdir",
22
+ type=Path,
23
+ required=True,
24
+ help="Path to config file (required)",
25
+ )
26
+ parser.add_argument(
27
+ "-f",
28
+ "--configfile",
29
+ type=Path,
30
+ required=True,
31
+ help="Configuration yaml file name (required)",
32
+ )
33
+ args = parser.parse_args()
34
+ cwd = args.startdir
35
+ if str(cwd).endswith("rms/model"):
36
+ run_folder = cwd
37
+ else:
38
+ try:
39
+ run_folder = cwd.joinpath("rms/model")
40
+ assert run_folder.exists() and run_folder.is_dir()
41
+ except AssertionError as e:
42
+ warn(f"PEM model should be run from the rms/model folder. {e}")
43
+ run_folder = cwd
44
+ with restore_dir(run_folder):
45
+ pem_fcn(
46
+ start_dir=args.startdir,
47
+ rel_path_pem=args.configdir,
48
+ pem_config_file_name=args.configfile,
49
+ )
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .pem_model import PetroElasticModel
4
+
5
+ __all__ = [
6
+ "PetroElasticModel",
7
+ ]
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from ert import (
4
+ ForwardModelStepDocumentation,
5
+ ForwardModelStepJSON,
6
+ ForwardModelStepPlugin,
7
+ ForwardModelStepValidationError,
8
+ )
9
+
10
+
11
+ class PetroElasticModel(ForwardModelStepPlugin):
12
+ def __init__(self) -> None:
13
+ super().__init__(
14
+ name="PEM",
15
+ command=[
16
+ "pem",
17
+ "--startdir",
18
+ "<START_DIR>",
19
+ "--configdir",
20
+ "<CONFIG_DIR>",
21
+ "--configfile",
22
+ "<CONFIG_FILE>",
23
+ ],
24
+ )
25
+
26
+ def validate_pre_realization_run(
27
+ self, fm_step_json: ForwardModelStepJSON
28
+ ) -> ForwardModelStepJSON:
29
+ return fm_step_json
30
+
31
+ def validate_pre_experiment(self, fm_step_json: ForwardModelStepJSON) -> None:
32
+ import os
33
+ from pathlib import Path
34
+
35
+ from fmu.pem.pem_utilities import read_pem_config
36
+
37
+ model_dir_env = os.environ.get("PEM_MODEL_DIR")
38
+ if model_dir_env is None:
39
+ raise ValueError(
40
+ 'environment variable "PEM_MODEL_DIR" must be set to '
41
+ "validate PEM model pre-experiment"
42
+ )
43
+ model_dir = Path(model_dir_env)
44
+
45
+ runpath_config_dir = Path(fm_step_json["argList"][3])
46
+ config_dir = (
47
+ model_dir
48
+ / "../.."
49
+ / runpath_config_dir.parent.name
50
+ / runpath_config_dir.name
51
+ ).resolve()
52
+ config_file = fm_step_json["argList"][5]
53
+ try:
54
+ os.chdir(model_dir)
55
+ _ = read_pem_config(config_dir / config_file)
56
+ except Exception as e:
57
+ raise ForwardModelStepValidationError(f"pem validation failed:\n {str(e)}")
58
+
59
+ @staticmethod
60
+ def documentation() -> ForwardModelStepDocumentation | None:
61
+ return ForwardModelStepDocumentation(
62
+ category="modelling.reservoir",
63
+ source_package="fmu.pem",
64
+ source_function_name="PetroElasticModel",
65
+ description="",
66
+ examples="""
67
+ .. code-block:: console
68
+
69
+ FORWARD_MODEL PEM(<START_DIR>=../../rms/model, <CONFIG_DIR>=../../sim2seis/model, <CONFIG_FILE>=new_pem.yml)
70
+
71
+ """, # noqa: E501,
72
+ )
File without changes
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import ert
4
+
5
+ from fmu.pem.forward_models import PetroElasticModel
6
+
7
+ PLUGIN_NAME = "pem"
8
+
9
+
10
+ @ert.plugin(name=PLUGIN_NAME)
11
+ def installable_workflow_jobs() -> dict[str, str]:
12
+ return {}
13
+
14
+
15
+ @ert.plugin(name=PLUGIN_NAME)
16
+ def installable_forward_model_steps() -> list[ert.ForwardModelStepPlugin]:
17
+ return [ # type: ignore
18
+ PetroElasticModel,
19
+ ]
@@ -0,0 +1,17 @@
1
+ from .density import estimate_bulk_density
2
+ from .effective_pressure import estimate_pressure
3
+ from .estimate_saturated_rock import estimate_saturated_rock
4
+ from .fluid_properties import effective_fluid_properties
5
+ from .mineral_properties import (
6
+ effective_mineral_properties,
7
+ estimate_effective_mineral_properties,
8
+ )
9
+
10
+ __all__ = [
11
+ "effective_fluid_properties",
12
+ "effective_mineral_properties",
13
+ "estimate_bulk_density",
14
+ "estimate_effective_mineral_properties",
15
+ "estimate_pressure",
16
+ "estimate_saturated_rock",
17
+ ]
@@ -0,0 +1,55 @@
1
+ from typing import List
2
+
3
+ from rock_physics_open.equinor_utilities.std_functions import rho_b
4
+
5
+ from fmu.pem.pem_utilities import (
6
+ EffectiveFluidProperties,
7
+ MatrixProperties,
8
+ PemConfig,
9
+ SimInitProperties,
10
+ estimate_cement,
11
+ )
12
+ from fmu.pem.pem_utilities.rpm_models import PatchyCementRPM
13
+
14
+
15
+ def estimate_bulk_density(
16
+ config: PemConfig,
17
+ init_prop: SimInitProperties,
18
+ fluid_props: List[EffectiveFluidProperties],
19
+ mineral_props: MatrixProperties,
20
+ ) -> List:
21
+ """
22
+ Estimate the bulk density per restart date.
23
+
24
+ Args:
25
+ config: Parameter settings.
26
+ init_prop: Constant properties, here using porosity.
27
+ fluid_props: List of EffectiveFluidProperties objects representing the effective
28
+ fluid properties per restart date.
29
+ mineral_props: EffectiveMineralProperties object representing the effective
30
+ properties.
31
+
32
+ Returns:
33
+ List of bulk densities per restart date.
34
+
35
+ Raises:
36
+ ValueError: If fluid_props is an empty list.
37
+ """
38
+ if config.rock_matrix.rpm == PatchyCementRPM:
39
+ # Get cement mineral properties
40
+ cement_mineral = config.rock_matrix.cement
41
+ mineral = config.rock_matrix.minerals[cement_mineral]
42
+ # Cement properties
43
+ cement_properties = estimate_cement(
44
+ mineral.bulk_modulus, mineral.shear_modulus, mineral.density, init_prop.poro
45
+ )
46
+ rel_frac_cem = (
47
+ config.rock_matrix.rpm.parameters.cement_fraction / init_prop.poro
48
+ )
49
+ rho_m = (
50
+ rel_frac_cem * cement_properties.dens
51
+ + (1 - rel_frac_cem) * mineral_props.dens
52
+ )
53
+ else:
54
+ rho_m = mineral_props.dens
55
+ return [rho_b(init_prop.poro, fluid.dens, rho_m) for fluid in fluid_props]
@@ -0,0 +1,168 @@
1
+ from typing import List, Union
2
+
3
+ import numpy as np
4
+
5
+ from fmu.pem.pem_utilities import (
6
+ EffectiveFluidProperties,
7
+ MatrixProperties,
8
+ PemConfig,
9
+ PressureProperties,
10
+ SimInitProperties,
11
+ SimRstProperties,
12
+ to_masked_array,
13
+ )
14
+ from fmu.pem.pem_utilities.enum_defs import OverburdenPressure
15
+
16
+ from .density import estimate_bulk_density
17
+
18
+
19
+ def estimate_pressure(
20
+ config: PemConfig,
21
+ sim_init: SimInitProperties,
22
+ sim_rst: List[SimRstProperties],
23
+ matrix_props: MatrixProperties,
24
+ fluid_props: list[EffectiveFluidProperties],
25
+ ) -> List[PressureProperties]:
26
+ """Estimate effective and overburden pressure.
27
+ Effective pressure is defined as overburden pressure minus formation (or pore)
28
+ pressure multiplied with the Biot factor
29
+
30
+ Args:
31
+ config: configuration parameters
32
+ sim_init: initial properties from simulation model
33
+ sim_rst: restart properties from the simulation model
34
+ matrix_props: rock properties
35
+ fluid_props: effective fluid properties
36
+
37
+ Returns:
38
+ effective pressure [bar], overburden_pressure [bar]
39
+
40
+ Raises:
41
+ ValueError: If sim_rst is an empty list.
42
+ """
43
+ # Effective pressure, get formation pressures and convert to Pa from bar
44
+ # Saturated rock bulk density bulk
45
+ bulk_density = estimate_bulk_density(config, sim_init, fluid_props, matrix_props)
46
+
47
+ ovb = config.pressure.overburden
48
+ if ovb.type == OverburdenPressure.CONSTANT:
49
+ eff_pres = [
50
+ estimate_effective_pressure(
51
+ formation_pressure=sim_date.pressure * 1.0e5,
52
+ bulk_density=dens,
53
+ reference_overburden_pressure=ovb.constant,
54
+ )
55
+ for (sim_date, dens) in zip(sim_rst, bulk_density)
56
+ ]
57
+ else: # ovb.type == 'trend':
58
+ eff_pres = [
59
+ estimate_effective_pressure(
60
+ formation_pressure=sim_date.pressure * 1.0e5,
61
+ bulk_density=dens,
62
+ reference_overburden_pressure=overburden_pressure_from_trend(
63
+ inter=ovb.trend.intercept,
64
+ grad=ovb.trend.gradient,
65
+ depth=sim_init.depth,
66
+ ),
67
+ )
68
+ for (sim_date, dens) in zip(sim_rst, bulk_density)
69
+ ]
70
+ # Sanity check on results - effective pressure should not be negative
71
+ for i, pres in enumerate(eff_pres):
72
+ if np.any(pres.effective_pressure < 0.0):
73
+ raise ValueError(
74
+ f"effective pressure calculation: formation pressure exceeds "
75
+ f"overburden pressure for date {config.global_params.seis_dates[i]}, \n"
76
+ f"minimum effective pressure is {np.min(pres.effective_pressure):.2f} "
77
+ f"bar, the number of cells with negative effective pressure is "
78
+ f"{np.sum(pres.effective_pressure < 0.0)}"
79
+ )
80
+ # Add effective pressure to RST properties
81
+ return eff_pres
82
+
83
+
84
+ def estimate_effective_pressure(
85
+ formation_pressure: np.ma.MaskedArray,
86
+ bulk_density: np.ma.MaskedArray,
87
+ reference_overburden_pressure: Union[np.ma.MaskedArray, float],
88
+ biot_coeff: float = 1.0,
89
+ ) -> PressureProperties:
90
+ """Estimate effective pressure from reference overburden pressure, formation
91
+ pressure, depth and bulk density
92
+
93
+ Args:
94
+ formation_pressure: formation pressure [Pa]
95
+ bulk_density: bulk density [kg/m3]
96
+ reference_overburden_pressure: constant or one-layer array with reference
97
+ biot_coeff: Biot coefficient, in the range [0.0, 1.0] [unitless]
98
+ .mineral
99
+ Returns:
100
+ PressureProperties object with formation pressure [bar], effective pressure
101
+ [bar], overburden_pressure [bar]
102
+
103
+ Raises:
104
+ ValueError: If reference overburden pressure is not of type float or numpy
105
+ masked array, or if reference overburden pressure is not of the same
106
+ dimension as comparable grids, or if reference overburden pressure does
107
+ not have the same shape as comparable grids.
108
+ """
109
+ reference_overburden_pressure = _verify_ovb_press(
110
+ reference_overburden_pressure, bulk_density
111
+ )
112
+ effective_pressure = reference_overburden_pressure - biot_coeff * formation_pressure
113
+ return PressureProperties(
114
+ formation_pressure=formation_pressure * 1.0e-5,
115
+ effective_pressure=effective_pressure * 1.0e-5,
116
+ overburden_pressure=reference_overburden_pressure * 1.0e-5,
117
+ )
118
+
119
+
120
+ def _verify_ovb_press(
121
+ ref_pres: Union[float, np.ma.MaskedArray], reference_cube: np.ma.MaskedArray
122
+ ) -> np.ma.MaskedArray:
123
+ if isinstance(ref_pres, float):
124
+ return to_masked_array(ref_pres, reference_cube)
125
+ if not isinstance(ref_pres, np.ma.MaskedArray):
126
+ raise ValueError(
127
+ f"{__file__}: Reference overburden pressure is not of type float or numpy "
128
+ "masked array"
129
+ )
130
+ if not ref_pres.ndim == reference_cube.ndim:
131
+ raise ValueError(
132
+ f"{__file__}: Reference overburden pressure is not of the same dimension "
133
+ "as comparable grids"
134
+ )
135
+ if not np.all(ref_pres.shape[0:-1] == reference_cube.shape[0:-1]):
136
+ raise ValueError(
137
+ f"{__file__}: Reference overburden pressure does not have the same shape "
138
+ "as comparable grids"
139
+ )
140
+ return ref_pres
141
+
142
+
143
+ def overburden_pressure_from_trend(
144
+ inter: float, grad: float, depth: np.ma.MaskedArray
145
+ ) -> np.ma.MaskedArray:
146
+ """Calculate overburden pressure from depth trend for top of depth grid
147
+
148
+ Args:
149
+ inter: intercept in trend
150
+ grad: gradient in trend
151
+ depth: depth cube [m]
152
+
153
+ Returns:
154
+ overburden pressure [Pa]
155
+
156
+ Raises:
157
+ ValueError: If unable to calculate overburden pressure due to non-numeric
158
+ intercept or gradient.
159
+ """
160
+ try:
161
+ a = float(inter)
162
+ b = float(grad)
163
+ ovb_pres = a + b * depth
164
+ except ValueError as err:
165
+ raise ValueError(
166
+ f"{__file__}: unable to calculate overburden pressure:"
167
+ ) from err
168
+ return ovb_pres
@@ -0,0 +1,90 @@
1
+ from typing import List
2
+
3
+ from fmu.pem.pem_utilities import (
4
+ EffectiveFluidProperties,
5
+ MatrixProperties,
6
+ PemConfig,
7
+ PressureProperties,
8
+ SaturatedRockProperties,
9
+ SimInitProperties,
10
+ estimate_cement,
11
+ )
12
+ from fmu.pem.pem_utilities.rpm_models import (
13
+ FriableRPM,
14
+ PatchyCementRPM,
15
+ RegressionModels,
16
+ )
17
+
18
+ from .regression_models import run_regression_models
19
+ from .run_friable_model import run_friable
20
+ from .run_patchy_cement_model import run_patchy_cement
21
+ from .run_t_matrix_and_pressure import run_t_matrix_and_pressure_models
22
+
23
+
24
+ def estimate_saturated_rock(
25
+ config: PemConfig,
26
+ sim_init: SimInitProperties,
27
+ eff_pres: list[PressureProperties],
28
+ matrix_props: MatrixProperties,
29
+ fluid_props: list[EffectiveFluidProperties],
30
+ ) -> List[SaturatedRockProperties]:
31
+ """Wrapper to call rock physics model
32
+
33
+ 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
39
+
40
+ Returns:
41
+ saturated rock properties per restart date
42
+ """
43
+ if isinstance(config.rock_matrix.rpm, PatchyCementRPM):
44
+ # Patchy cement model
45
+ cement = config.rock_matrix.minerals[config.rock_matrix.cement]
46
+ cement_properties = estimate_cement(
47
+ density=cement.density,
48
+ bulk_modulus=cement.bulk_modulus,
49
+ shear_modulus=cement.shear_modulus,
50
+ grid=sim_init.poro,
51
+ )
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,
59
+ )
60
+ elif isinstance(config.rock_matrix.rpm, FriableRPM):
61
+ # Friable sandstone model
62
+ sat_rock_props = run_friable(
63
+ matrix_props,
64
+ fluid_props,
65
+ sim_init.poro,
66
+ eff_pres,
67
+ config,
68
+ )
69
+ elif isinstance(config.rock_matrix.rpm, RegressionModels):
70
+ # 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,
78
+ )
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,
89
+ )
90
+ return sat_rock_props