fmu-pem 0.0.2__py3-none-any.whl → 0.0.4__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/__init__.py +2 -0
- fmu/pem/__main__.py +72 -19
- fmu/pem/forward_models/pem_model.py +21 -26
- fmu/pem/pem_functions/__init__.py +2 -2
- fmu/pem/pem_functions/density.py +32 -38
- fmu/pem/pem_functions/effective_pressure.py +153 -49
- fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
- fmu/pem/pem_functions/fluid_properties.py +447 -245
- 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 +30 -10
- 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 +65 -8
- 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 +76 -50
- fmu/pem/pem_utilities/import_routines.py +57 -69
- fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
- fmu/pem/pem_utilities/pem_config_validation.py +364 -172
- fmu/pem/pem_utilities/rpm_models.py +473 -100
- fmu/pem/pem_utilities/update_grid.py +3 -2
- fmu/pem/pem_utilities/utils.py +90 -38
- fmu/pem/run_pem.py +66 -48
- fmu/pem/version.py +16 -3
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/METADATA +19 -11
- fmu_pem-0.0.4.dist-info/RECORD +39 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/WHEEL +1 -1
- fmu_pem-0.0.2.dist-info/RECORD +0 -37
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/entry_points.txt +0 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/licenses/LICENSE +0 -0
- {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/top_level.txt +0 -0
|
@@ -9,66 +9,92 @@ from .pem_config_validation import PemConfig
|
|
|
9
9
|
from .utils import restore_dir
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def find_key_first(d: dict, key: str) -> str | None:
|
|
13
|
+
"""Recursively search for the first occurrence of a key in nested dicts.
|
|
14
|
+
|
|
15
|
+
The search now prioritizes keys at the current dictionary level before
|
|
16
|
+
descending into nested dictionaries, ensuring top-level occurrences win when
|
|
17
|
+
duplicates exist deeper in the structure.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
d: A potentially nested mapping structure where values may themselves be
|
|
21
|
+
dictionaries. Typically a ``dict`` originating from parsed YAML/JSON.
|
|
22
|
+
key: The key to search for in ``d`` and any nested dictionaries.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The value associated with the first occurrence of ``key`` encountered during
|
|
26
|
+
the depth-first search, or ``None`` if the key is not present.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> data = {"a": 1, "b": {"target": 2, "c": {"target": 3}}}
|
|
30
|
+
>>> find_key_first(data, "target")
|
|
31
|
+
2
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(d, dict):
|
|
34
|
+
return None
|
|
35
|
+
if key in d:
|
|
36
|
+
return d[key]
|
|
37
|
+
for v in d.values():
|
|
38
|
+
if isinstance(v, dict):
|
|
39
|
+
result = find_key_first(v, key)
|
|
40
|
+
if result is not None:
|
|
41
|
+
return result
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_global_params_and_dates(
|
|
46
|
+
global_config_dir: Path,
|
|
47
|
+
global_conf_file: Path,
|
|
48
|
+
mod_prefix: str | None = None,
|
|
49
|
+
obs_prefix: str | None = None,
|
|
50
|
+
) -> dict:
|
|
13
51
|
"""Read global configuration parameters, simulation model dates and seismic dates
|
|
14
52
|
for difference calculation
|
|
15
53
|
|
|
16
54
|
Args:
|
|
17
|
-
|
|
18
|
-
|
|
55
|
+
global_config_dir: directory path for the global config file
|
|
56
|
+
global_conf_file: name of the global config file
|
|
19
57
|
|
|
20
58
|
Returns:
|
|
21
59
|
global parameter configuration dict, list of strings for simulation dates,
|
|
22
60
|
list of tuples with
|
|
23
61
|
strings of dates to calculate difference properties
|
|
24
62
|
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
except KeyError:
|
|
61
|
-
pass
|
|
62
|
-
if not found_grid_name:
|
|
63
|
-
raise ValueError(
|
|
64
|
-
f"{__file__}: no value for ECLGRIDNAME_PEM in global config file"
|
|
65
|
-
)
|
|
66
|
-
return {
|
|
67
|
-
"grid_model": grid_model_name,
|
|
68
|
-
"seis_dates": seismic_dates,
|
|
69
|
-
"diff_dates": diff_dates,
|
|
70
|
-
"global_config": global_config_par,
|
|
71
|
-
}
|
|
63
|
+
global_config_par = yaml_load(
|
|
64
|
+
str(global_config_dir / global_conf_file),
|
|
65
|
+
)
|
|
66
|
+
grid_model_name = find_key_first(global_config_par["global"], "ECLGRIDNAME_PEM")
|
|
67
|
+
if grid_model_name is None:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"{__file__}: no value for ECLGRIDNAME_PEM in global config file"
|
|
70
|
+
)
|
|
71
|
+
# Find the correct seismic dates references
|
|
72
|
+
dates_config = global_config_par["global"]["dates"]
|
|
73
|
+
return_dict = {
|
|
74
|
+
"global_config": global_config_par,
|
|
75
|
+
"grid_model": grid_model_name,
|
|
76
|
+
"seismic": global_config_par["global"]["seismic"],
|
|
77
|
+
}
|
|
78
|
+
if mod_prefix:
|
|
79
|
+
return_dict.update(
|
|
80
|
+
{
|
|
81
|
+
"mod_dates": dates_config.get(f"SEISMIC_{mod_prefix}_DATES", None),
|
|
82
|
+
"mod_diffdates": dates_config.get(
|
|
83
|
+
f"SEISMIC_{mod_prefix}_DIFFDATES", None
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
if obs_prefix:
|
|
88
|
+
return_dict.update(
|
|
89
|
+
{
|
|
90
|
+
"obs_dates": dates_config.get(f"SEISMIC_{obs_prefix}_DATES", None),
|
|
91
|
+
"obs_diffdates": dates_config.get(
|
|
92
|
+
f"SEISMIC_{obs_prefix}_DIFFDATES", None
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return return_dict
|
|
72
98
|
|
|
73
99
|
|
|
74
100
|
def read_pem_config(yaml_file: Path) -> PemConfig:
|
|
@@ -1,61 +1,47 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import List, Tuple
|
|
3
2
|
|
|
4
|
-
import numpy as np
|
|
5
3
|
import xtgeo
|
|
6
4
|
|
|
7
5
|
from .pem_class_definitions import SimInitProperties, SimRstProperties
|
|
8
|
-
from .
|
|
9
|
-
from .utils import restore_dir
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def read_geogrid(root_dir: Path, config: PemConfig) -> dict:
|
|
13
|
-
"""Not in use? Read porosity from geo-grid
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
root_dir: start dir for PEM script run
|
|
17
|
-
config: PEM specific parameters
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
Dict object with porosity
|
|
21
|
-
"""
|
|
22
|
-
with restore_dir(root_dir.joinpath(config.paths.rel_path_geogrid)):
|
|
23
|
-
return {"poro": xtgeo.gridproperty_from_file("geogrid--phit.roff").values}
|
|
6
|
+
from .utils import bar_to_pa, restore_dir
|
|
24
7
|
|
|
25
8
|
|
|
26
9
|
def read_init_properties(
|
|
27
|
-
property_file: Path,
|
|
10
|
+
property_file: Path,
|
|
11
|
+
sim_grid: xtgeo.Grid,
|
|
12
|
+
fipnum_param: str,
|
|
28
13
|
) -> SimInitProperties:
|
|
29
14
|
"""Read initial properties from INIT file
|
|
30
15
|
Args:
|
|
31
16
|
property_file: Full path to the .INIT file
|
|
32
17
|
sim_grid: The simulation grid to use for reading properties
|
|
18
|
+
fipnum_param: Name for zone/region parameter, normally 'FIPNUM'
|
|
33
19
|
Returns:
|
|
34
20
|
SimInitProperties: The loaded initial grid properties
|
|
35
21
|
"""
|
|
36
|
-
|
|
22
|
+
init_props = ["PORO", "DEPTH", "PVTNUM"] + [fipnum_param]
|
|
37
23
|
sim_init_props = xtgeo.gridproperties_from_file(
|
|
38
|
-
property_file, fformat="init", names=
|
|
24
|
+
property_file, fformat="init", names=init_props, grid=sim_grid
|
|
39
25
|
)
|
|
40
26
|
props_dict = {
|
|
41
27
|
sim_init_props[name].name.lower(): sim_init_props[name].values
|
|
42
|
-
for name in
|
|
28
|
+
for name in init_props
|
|
43
29
|
}
|
|
44
30
|
return SimInitProperties(**props_dict)
|
|
45
31
|
|
|
46
32
|
|
|
47
33
|
def create_rst_list(
|
|
48
34
|
rst_props: xtgeo.GridProperties,
|
|
49
|
-
seis_dates:
|
|
50
|
-
rst_prop_names:
|
|
51
|
-
) ->
|
|
35
|
+
seis_dates: list[str],
|
|
36
|
+
rst_prop_names: list[str],
|
|
37
|
+
) -> list[SimRstProperties]:
|
|
52
38
|
"""Create list of SimRstProperties from raw restart properties
|
|
53
39
|
Args:
|
|
54
40
|
rst_props: Raw restart properties
|
|
55
|
-
seis_dates:
|
|
56
|
-
rst_prop_names:
|
|
41
|
+
seis_dates: list of dates to process
|
|
42
|
+
rst_prop_names: list of property names to include
|
|
57
43
|
Returns:
|
|
58
|
-
|
|
44
|
+
list[SimRstProperties]: list of processed restart properties by date
|
|
59
45
|
"""
|
|
60
46
|
return [
|
|
61
47
|
SimRstProperties(
|
|
@@ -70,93 +56,95 @@ def create_rst_list(
|
|
|
70
56
|
|
|
71
57
|
|
|
72
58
|
def read_sim_grid_props(
|
|
59
|
+
rel_dir_sim_files: Path,
|
|
73
60
|
egrid_file: Path,
|
|
74
61
|
init_property_file: Path,
|
|
75
62
|
restart_property_file: Path,
|
|
76
|
-
seis_dates:
|
|
77
|
-
|
|
63
|
+
seis_dates: list[str],
|
|
64
|
+
fipnum_name: str = "FIPNUM",
|
|
65
|
+
) -> tuple[xtgeo.Grid, SimInitProperties, list[SimRstProperties]]:
|
|
78
66
|
"""Read grid and properties from simulation run, both initial and restart properties
|
|
79
67
|
|
|
80
68
|
Args:
|
|
69
|
+
rel_dir_sim_files: start dir for PEM script run
|
|
81
70
|
egrid_file: Path to the EGRID file
|
|
82
71
|
init_property_file: Path to the INIT file
|
|
83
72
|
restart_property_file: Path to the UNRST file
|
|
84
|
-
seis_dates:
|
|
73
|
+
seis_dates: list of dates for which to read restart properties
|
|
85
74
|
|
|
86
75
|
Returns:
|
|
87
76
|
sim_grid: grid definition for eclipse input
|
|
88
77
|
init_props: object with initial properties of simulation grid
|
|
89
78
|
rst_list: list with time-dependent simulation properties
|
|
90
79
|
"""
|
|
91
|
-
sim_grid = xtgeo.grid_from_file(egrid_file)
|
|
80
|
+
sim_grid = xtgeo.grid_from_file(rel_dir_sim_files / egrid_file)
|
|
92
81
|
|
|
93
|
-
init_props = read_init_properties(
|
|
82
|
+
init_props = read_init_properties(
|
|
83
|
+
rel_dir_sim_files / init_property_file, sim_grid, fipnum_name
|
|
84
|
+
)
|
|
94
85
|
|
|
95
86
|
# TEMP will only be available for eclipse-300
|
|
96
|
-
|
|
87
|
+
rst_props_names = ["SWAT", "SGAS", "SOIL", "RS", "RV", "PRESSURE", "SALT", "TEMP"]
|
|
97
88
|
|
|
98
89
|
# Restart properties - set strict to False, False in case RV is not included in
|
|
99
|
-
# the UNRST file
|
|
90
|
+
# the UNRST file. NB: This has the effect that other missing parameters will not
|
|
91
|
+
# raise an error here, but that is handled by the following try-except statement.
|
|
100
92
|
rst_props = xtgeo.gridproperties_from_file(
|
|
101
|
-
restart_property_file,
|
|
93
|
+
rel_dir_sim_files / restart_property_file,
|
|
102
94
|
fformat="unrst",
|
|
103
|
-
names=
|
|
95
|
+
names=rst_props_names,
|
|
104
96
|
dates=seis_dates,
|
|
105
97
|
grid=sim_grid,
|
|
106
98
|
strict=(False, False),
|
|
107
99
|
)
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
# Formation pressure has unit `bar` in eclipse, but in the PEM models, unit
|
|
102
|
+
# `Pa` is expected. Perform unit conversion before class objects are populated
|
|
103
|
+
for date in seis_dates:
|
|
104
|
+
rst_props["PRESSURE" + "_" + date].values = bar_to_pa(
|
|
105
|
+
rst_props["PRESSURE" + "_" + date].values
|
|
106
|
+
)
|
|
112
107
|
|
|
108
|
+
try:
|
|
109
|
+
rst_list = create_rst_list(rst_props, seis_dates, rst_props_names)
|
|
110
|
+
except (AttributeError, TypeError) as e:
|
|
111
|
+
raise ValueError(f"eclipse simulator restart file is missing parameters: {e}")
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
"""Read PEM specific NTG property
|
|
116
|
-
Args:
|
|
117
|
-
ntg_grid_file: path to the NTG grid file
|
|
118
|
-
Returns:
|
|
119
|
-
net to gross property from simgrid adapted to PEM definition
|
|
120
|
-
"""
|
|
121
|
-
return xtgeo.gridproperty_from_file(ntg_grid_file).values
|
|
113
|
+
return sim_grid, init_props, rst_list
|
|
122
114
|
|
|
123
115
|
|
|
124
|
-
def import_fractions(
|
|
116
|
+
def import_fractions(
|
|
117
|
+
root_dir: Path,
|
|
118
|
+
fraction_path: Path,
|
|
119
|
+
fraction_files: list[Path],
|
|
120
|
+
fraction_names: list[str],
|
|
121
|
+
grd: xtgeo.Grid,
|
|
122
|
+
) -> list:
|
|
125
123
|
"""Import volume fractions
|
|
126
124
|
|
|
127
125
|
Args:
|
|
128
126
|
root_dir (str): model directory, relative paths refer to it
|
|
129
|
-
|
|
127
|
+
fraction_path: path to the fractions files
|
|
128
|
+
fraction_files: list of fraction files
|
|
129
|
+
fraction_names: list of parameter names in fraction files
|
|
130
|
+
grd (xtgeo.Grid): model grid
|
|
130
131
|
|
|
131
132
|
Returns:
|
|
132
133
|
list: fraction properties
|
|
133
134
|
"""
|
|
134
|
-
with restore_dir(
|
|
135
|
-
root_dir.joinpath(config.rock_matrix.volume_fractions.rel_path_fractions)
|
|
136
|
-
):
|
|
137
|
-
try:
|
|
138
|
-
grd = xtgeo.grid_from_file(
|
|
139
|
-
config.rock_matrix.volume_fractions.fractions_grid_file_name,
|
|
140
|
-
)
|
|
141
|
-
except ValueError as exc:
|
|
142
|
-
raise ImportError(
|
|
143
|
-
f"{__file__}: failed to import volume fractions file "
|
|
144
|
-
f"{config.rock_matrix.volume_fractions.fractions_grid_file_name}"
|
|
145
|
-
) from exc
|
|
135
|
+
with restore_dir(root_dir / fraction_path):
|
|
146
136
|
try:
|
|
147
|
-
fracs = config.rock_matrix.fraction_names
|
|
148
137
|
grid_props = [
|
|
149
138
|
xtgeo.gridproperty_from_file(
|
|
150
139
|
file,
|
|
151
|
-
name=name,
|
|
152
|
-
grid=grd,
|
|
140
|
+
name=name, # type: ignore
|
|
141
|
+
grid=grd, # type: ignore
|
|
153
142
|
)
|
|
154
|
-
for name in
|
|
155
|
-
for file in
|
|
143
|
+
for name in fraction_names
|
|
144
|
+
for file in fraction_files
|
|
156
145
|
]
|
|
157
146
|
except ValueError as exc:
|
|
158
147
|
raise ImportError(
|
|
159
|
-
f"{__file__}: failed to import volume fractions files "
|
|
160
|
-
f"{config.rock_matrix.volume_fractions.fractions_prop_file_names}"
|
|
148
|
+
f"{__file__}: failed to import volume fractions files {fraction_files}"
|
|
161
149
|
) from exc
|
|
162
150
|
return [grid_prop.values for grid_prop in grid_props]
|
|
@@ -1,17 +1,52 @@
|
|
|
1
|
-
from dataclasses import dataclass,
|
|
2
|
-
from typing import
|
|
1
|
+
from dataclasses import dataclass, fields
|
|
2
|
+
from typing import Self
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from numpy.ma import MaskedArray
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class PropertiesSubgridMasked:
|
|
9
|
+
"""
|
|
10
|
+
Class to derive object properties in a subgrid. The mask is assumed to
|
|
11
|
+
come from a numpy masked array.
|
|
12
|
+
|
|
13
|
+
In a numpy masked array, True means masked, False means not masked
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def masked_where(self: Self, mask: np.ndarray, invert_mask: bool = True) -> Self:
|
|
17
|
+
"""
|
|
18
|
+
Method to derive object properties in a subgrid. The mask is assumed to
|
|
19
|
+
come from a numpy masked array.
|
|
20
|
+
|
|
21
|
+
In a numpy masked array, True means masked, False means not masked
|
|
22
|
+
Args:
|
|
23
|
+
self: object with np.ndarray or np.ma.MaskedArray attributes
|
|
24
|
+
mask: Boolean mask to apply
|
|
25
|
+
invert_mask: If True, invert the mask with ~mask
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
New instance of the same type with masked arrays
|
|
29
|
+
"""
|
|
30
|
+
actual_mask = ~mask if invert_mask else mask
|
|
31
|
+
|
|
32
|
+
field_values = {}
|
|
33
|
+
for field in fields(self):
|
|
34
|
+
value = getattr(self, field.name)
|
|
35
|
+
if value is None:
|
|
36
|
+
field_values[field.name] = None
|
|
37
|
+
else:
|
|
38
|
+
field_values[field.name] = np.ma.masked_where(actual_mask, value.data)
|
|
39
|
+
return type(self)(**field_values)
|
|
40
|
+
|
|
41
|
+
|
|
8
42
|
# Eclipse simulator file classes - SimInitProperties and time step SimRstProperties
|
|
9
43
|
@dataclass
|
|
10
|
-
class SimInitProperties:
|
|
44
|
+
class SimInitProperties(PropertiesSubgridMasked):
|
|
11
45
|
poro: MaskedArray
|
|
12
46
|
depth: MaskedArray
|
|
13
|
-
|
|
14
|
-
|
|
47
|
+
vsh_pem: MaskedArray | None = None
|
|
48
|
+
pvtnum: MaskedArray | None = None
|
|
49
|
+
fipnum: MaskedArray | None = None
|
|
15
50
|
|
|
16
51
|
@property
|
|
17
52
|
def delta_z(self) -> MaskedArray:
|
|
@@ -40,7 +75,7 @@ class SimInitProperties:
|
|
|
40
75
|
|
|
41
76
|
|
|
42
77
|
@dataclass
|
|
43
|
-
class SimRstProperties:
|
|
78
|
+
class SimRstProperties(PropertiesSubgridMasked):
|
|
44
79
|
swat: MaskedArray
|
|
45
80
|
sgas: MaskedArray
|
|
46
81
|
soil: MaskedArray
|
|
@@ -53,35 +88,45 @@ class SimRstProperties:
|
|
|
53
88
|
|
|
54
89
|
# Elastic properties for matrix, i.e. mixed minerals and volume fractions
|
|
55
90
|
@dataclass
|
|
56
|
-
class
|
|
57
|
-
bulk_modulus: MaskedArray
|
|
58
|
-
shear_modulus: MaskedArray
|
|
59
|
-
|
|
91
|
+
class EffectiveMineralProperties(PropertiesSubgridMasked):
|
|
92
|
+
bulk_modulus: MaskedArray | np.ndarray
|
|
93
|
+
shear_modulus: MaskedArray | np.ndarray
|
|
94
|
+
density: MaskedArray | np.ndarray
|
|
95
|
+
|
|
96
|
+
def __post_init__(self):
|
|
97
|
+
self.vs = np.sqrt(self.shear_modulus * self.density)
|
|
98
|
+
self.vp = np.sqrt(
|
|
99
|
+
(self.bulk_modulus + 4 / 3 * self.shear_modulus) / self.density
|
|
100
|
+
)
|
|
60
101
|
|
|
61
102
|
|
|
62
|
-
# Separate class for dry rock,
|
|
103
|
+
# Separate class for dry rock, can use MatrixProperties as base
|
|
63
104
|
# class
|
|
64
105
|
@dataclass
|
|
65
|
-
class DryRockProperties(
|
|
106
|
+
class DryRockProperties(EffectiveMineralProperties):
|
|
66
107
|
pass
|
|
67
108
|
|
|
68
109
|
|
|
69
110
|
# Acoustic properties for mixed fluids. If non-Newtonian fluids are to be considered,
|
|
70
111
|
# shear modulus and vs must be added
|
|
71
112
|
@dataclass
|
|
72
|
-
class EffectiveFluidProperties:
|
|
113
|
+
class EffectiveFluidProperties(PropertiesSubgridMasked):
|
|
73
114
|
bulk_modulus: MaskedArray
|
|
74
|
-
|
|
115
|
+
density: MaskedArray
|
|
75
116
|
|
|
76
117
|
@property
|
|
77
118
|
def vp(self):
|
|
78
|
-
return np.sqrt(self.bulk_modulus / self.
|
|
119
|
+
return np.sqrt(self.bulk_modulus / self.density)
|
|
79
120
|
|
|
80
121
|
|
|
81
122
|
# Pressure properties - overburden, formation and effective (strictly speaking
|
|
82
123
|
# differential) pressure
|
|
83
124
|
@dataclass
|
|
84
|
-
class PressureProperties:
|
|
125
|
+
class PressureProperties(PropertiesSubgridMasked):
|
|
126
|
+
"""
|
|
127
|
+
All attributes shall have unit Pa
|
|
128
|
+
"""
|
|
129
|
+
|
|
85
130
|
formation_pressure: MaskedArray
|
|
86
131
|
effective_pressure: MaskedArray
|
|
87
132
|
overburden_pressure: MaskedArray
|
|
@@ -99,15 +144,28 @@ class TwoWayTime:
|
|
|
99
144
|
# to be defined, others can be derived from them, but this construction is needed
|
|
100
145
|
# to have all properties recognised by dataclasses.asdict()
|
|
101
146
|
@dataclass
|
|
102
|
-
class SaturatedRockProperties:
|
|
147
|
+
class SaturatedRockProperties(PropertiesSubgridMasked):
|
|
103
148
|
vp: MaskedArray
|
|
104
149
|
vs: MaskedArray
|
|
105
|
-
|
|
106
|
-
ai: MaskedArray =
|
|
107
|
-
si: MaskedArray =
|
|
108
|
-
vpvs: MaskedArray =
|
|
150
|
+
density: MaskedArray
|
|
151
|
+
ai: MaskedArray | None = None
|
|
152
|
+
si: MaskedArray | None = None
|
|
153
|
+
vpvs: MaskedArray | None = None
|
|
109
154
|
|
|
110
155
|
def __post_init__(self):
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
"""Calculate derived properties from independent variables.
|
|
157
|
+
|
|
158
|
+
This runs both at initialization and can be called manually after
|
|
159
|
+
updating vp/vs/density arrays (e.g., after zone merging).
|
|
160
|
+
"""
|
|
161
|
+
self.recalculate_derived()
|
|
162
|
+
|
|
163
|
+
def recalculate_derived(self):
|
|
164
|
+
"""Recalculate derived properties (ai, si, vpvs) from current vp, vs, density.
|
|
165
|
+
|
|
166
|
+
Call this method after modifying vp, vs, or density arrays to update
|
|
167
|
+
the derived properties.
|
|
168
|
+
"""
|
|
169
|
+
self.ai = self.vp * self.density
|
|
170
|
+
self.si = self.vs * self.density
|
|
113
171
|
self.vpvs = self.vp / self.vs
|