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
|
@@ -2,86 +2,112 @@
|
|
|
2
2
|
Define RPM model types and their parameters
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Literal, Self
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
from pydantic
|
|
9
|
-
|
|
7
|
+
import numpy as np
|
|
8
|
+
from pydantic import (
|
|
9
|
+
BaseModel,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
Field,
|
|
12
|
+
ValidationInfo,
|
|
13
|
+
field_validator,
|
|
14
|
+
model_validator,
|
|
15
|
+
)
|
|
16
|
+
from rock_physics_open.equinor_utilities.machine_learning_utilities import (
|
|
17
|
+
ExponentialPressureModel,
|
|
18
|
+
PolynomialPressureModel,
|
|
19
|
+
)
|
|
20
|
+
from rock_physics_open.sandstone_models import (
|
|
21
|
+
friable_model_dry,
|
|
22
|
+
patchy_cement_model_dry,
|
|
23
|
+
)
|
|
10
24
|
|
|
11
|
-
from fmu.pem.pem_utilities.enum_defs import
|
|
25
|
+
from fmu.pem.pem_utilities.enum_defs import (
|
|
26
|
+
CoordinationNumberFunction,
|
|
27
|
+
ParameterTypes,
|
|
28
|
+
PhysicsPressureModelTypes,
|
|
29
|
+
RegressionPressureModelTypes,
|
|
30
|
+
RegressionPressureParameterTypes,
|
|
31
|
+
RPMType,
|
|
32
|
+
)
|
|
33
|
+
from fmu.pem.pem_utilities.pem_class_definitions import EffectiveMineralProperties
|
|
12
34
|
|
|
13
35
|
|
|
14
|
-
class
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
density: float = Field(gt=1.0e3, lt=1.0e4, description="Units: kg/m^3")
|
|
36
|
+
class OptionalField(BaseModel):
|
|
37
|
+
def __eq__(self, other):
|
|
38
|
+
return other is None
|
|
18
39
|
|
|
40
|
+
def __ne__(self, other):
|
|
41
|
+
return not self.__eq__(other)
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
default="PorBased",
|
|
23
|
-
description="Coordinate number is the number of grain-grain contacts. It is "
|
|
24
|
-
"normally assumed to be a function of porosity for friable sand",
|
|
25
|
-
)
|
|
43
|
+
def __bool__(self):
|
|
44
|
+
return False
|
|
26
45
|
|
|
46
|
+
model_config = ConfigDict(title="This field is optional")
|
|
27
47
|
|
|
28
|
-
class CoordinationNumberConstVal(BaseModel):
|
|
29
|
-
fcn: SkipJsonSchema[CoordinationNumberFunction] = Field(
|
|
30
|
-
default="ConstVal",
|
|
31
|
-
)
|
|
32
|
-
coordination_number: float = Field(
|
|
33
|
-
default=9.0,
|
|
34
|
-
gt=2.0,
|
|
35
|
-
lt=16.0,
|
|
36
|
-
description="In case of a constant value for the number of grain contacts, "
|
|
37
|
-
"a value of 8-9 is common",
|
|
38
|
-
)
|
|
39
48
|
|
|
49
|
+
class MineralProperties(BaseModel):
|
|
50
|
+
bulk_modulus: float = Field(gt=1.0e9, lt=5.0e11, description="Unit: `Pa`")
|
|
51
|
+
shear_modulus: float = Field(gt=1.0e9, lt=5.0e11, description="Unit: `Pa`")
|
|
52
|
+
density: float = Field(gt=1.0e3, lt=1.0e4, description="Unit: `kg/m³`")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FriableParams(BaseModel):
|
|
56
|
+
"""Friable sandstone model parameters."""
|
|
57
|
+
|
|
58
|
+
model_config = ConfigDict(title="Friable Model Parameters")
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
default=0.1,
|
|
44
|
-
description="There is an upper limit for the constant cement model, which "
|
|
45
|
-
"is part of the Patchy Cement model. Values higher than 0.1 can "
|
|
46
|
-
"lead to the model reaching saturation",
|
|
60
|
+
critical_porosity: float = Field(
|
|
61
|
+
ge=0.3, le=0.5, default=0.4, description="Critical porosity"
|
|
47
62
|
)
|
|
48
|
-
|
|
49
|
-
default=
|
|
50
|
-
gt=0,
|
|
51
|
-
le=0.1,
|
|
52
|
-
description="Representative cement fraction for Patchy Cement should be chosen "
|
|
53
|
-
"so that the model trend line goes through the median of the log "
|
|
54
|
-
"values",
|
|
63
|
+
coordination_number_function: CoordinationNumberFunction = Field(
|
|
64
|
+
default="PorBased", description="Coordination number function"
|
|
55
65
|
)
|
|
56
|
-
|
|
57
|
-
default=0
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
description="Critical porosity is the porosity of the sands at the time of "
|
|
61
|
-
"deposition, before any compaction",
|
|
66
|
+
coord_num: float = Field(
|
|
67
|
+
default=9.0,
|
|
68
|
+
description="Coordination number value."
|
|
69
|
+
" This is normally only used in patchy cement model",
|
|
62
70
|
)
|
|
63
71
|
shear_reduction: float = Field(
|
|
64
|
-
default=0
|
|
65
|
-
ge=0.0,
|
|
66
|
-
le=1.0,
|
|
67
|
-
description="Shear reduction is related to the fraction of tangential friction "
|
|
68
|
-
"between grains. Shear reduction of 1 means frictionless contact, "
|
|
69
|
-
"and 0 means full friction",
|
|
72
|
+
default=1.0, ge=0, le=1, description="Shear reduction factor"
|
|
70
73
|
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
description="Coordinate number is the number of grain-grain contacts. It is "
|
|
76
|
-
"normally assumed to be a function of porosity for friable sand",
|
|
74
|
+
model_max_pressure: float = Field(
|
|
75
|
+
default=40, # MPa
|
|
76
|
+
description="Maximum pressure value for the friable sandstone model used as"
|
|
77
|
+
" pressure sensitive model",
|
|
77
78
|
)
|
|
78
79
|
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
"""Convert friable parameters to dictionary."""
|
|
82
|
+
return {
|
|
83
|
+
"critical_porosity": self.critical_porosity,
|
|
84
|
+
"coordination_number_function": self.coordination_number_function,
|
|
85
|
+
"coord_num": self.coord_num,
|
|
86
|
+
"shear_reduction": self.shear_reduction,
|
|
87
|
+
"model_max_pressure": self.model_max_pressure,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PatchyCementParams(FriableParams):
|
|
92
|
+
"""Patchy cement model parameters."""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(title="Patchy Cement Parameters")
|
|
95
|
+
|
|
96
|
+
cement_fraction: float = Field(gt=0, le=0.1, description="Cement volume fraction")
|
|
97
|
+
|
|
98
|
+
def to_dict(self) -> dict[str, Any]:
|
|
99
|
+
"""Convert patchy cement parameters to dictionary."""
|
|
100
|
+
base_dict = super().to_dict()
|
|
101
|
+
base_dict["cement_fraction"] = self.cement_fraction
|
|
102
|
+
return base_dict
|
|
103
|
+
|
|
79
104
|
|
|
80
105
|
class TMatrixParams(BaseModel):
|
|
81
106
|
t_mat_model_version: Literal["PETEC", "EXP"] = Field(
|
|
107
|
+
default="PETEC",
|
|
82
108
|
description="When T Matrix model is calibrated and optimised based on well "
|
|
83
109
|
"data, a selection is made on how much information will be "
|
|
84
|
-
"available when the calibrated model is applied to a PEM model"
|
|
110
|
+
"available when the calibrated model is applied to a PEM model",
|
|
85
111
|
)
|
|
86
112
|
angle: float = Field(
|
|
87
113
|
default=90.0,
|
|
@@ -115,58 +141,87 @@ class TMatrixParams(BaseModel):
|
|
|
115
141
|
)
|
|
116
142
|
|
|
117
143
|
|
|
118
|
-
class
|
|
119
|
-
rho_weights:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
class RhoRegressionParams(BaseModel):
|
|
145
|
+
rho_weights: list[float] = Field(
|
|
146
|
+
description="\n\n".join(
|
|
147
|
+
[
|
|
148
|
+
"Matrix density is normally estimated from "
|
|
149
|
+
"mineral composition and the density of each mineral. "
|
|
150
|
+
"Selecting `RhoRegressionParams` will estimate matrix "
|
|
151
|
+
"density based on porosity alone. In that case, weights "
|
|
152
|
+
"for the polynomial expression must be provided.",
|
|
153
|
+
"Polynomial coefficients for matrix density as a function of porosity:",
|
|
154
|
+
"`rho(phi) = w1 + w2*phi + w3*phi^2 + ... + wn*phi^n`",
|
|
155
|
+
"List order: `[w1, w2, w3, ..., wn]`",
|
|
156
|
+
"where `phi` is porosity (fraction) and `rho` is in kg/m³.",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
133
159
|
)
|
|
134
160
|
|
|
135
161
|
|
|
136
|
-
class VpVsRegressionParams(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
description="
|
|
140
|
-
|
|
162
|
+
class VpVsRegressionParams(BaseModel):
|
|
163
|
+
# model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
164
|
+
vp_weights: list[float] = Field(
|
|
165
|
+
description="\n\n".join(
|
|
166
|
+
[
|
|
167
|
+
"Polynomial coefficients for vp as a function of porosity:",
|
|
168
|
+
"`vp(phi) = w1 + w2*phi + w3*phi^2 + ... + wn*phi^n`",
|
|
169
|
+
"List order: `[w1, w2, w3, ..., wn]`",
|
|
170
|
+
"where `phi` is porosity (fraction) and `vp` is in m/s.",
|
|
171
|
+
]
|
|
172
|
+
),
|
|
141
173
|
)
|
|
142
|
-
vs_weights:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
vs_weights: list[float] = Field(
|
|
175
|
+
description="\n\n".join(
|
|
176
|
+
[
|
|
177
|
+
"Polynomial coefficients for vs as a function of porosity:",
|
|
178
|
+
"`vs(phi) = w1 + w2*phi + w3*phi^2 + ... + wn*phi^n`",
|
|
179
|
+
"List order: `[w1, w2, w3, ..., wn]`",
|
|
180
|
+
"where `phi` is porosity (fraction) and `vs` is in m/s.",
|
|
181
|
+
]
|
|
182
|
+
),
|
|
146
183
|
)
|
|
147
|
-
mode:
|
|
184
|
+
mode: Literal["vp_vs"] = Field(
|
|
148
185
|
default="vp_vs",
|
|
149
|
-
description="
|
|
150
|
-
"
|
|
186
|
+
description="Mode for Vp/Vs regression. 'vp_vs' indicates that both "
|
|
187
|
+
"Vp and Vs are modeled as polynomial functions of porosity "
|
|
188
|
+
"using the provided coefficients.",
|
|
189
|
+
)
|
|
190
|
+
rho_model: OptionalField | RhoRegressionParams = Field(
|
|
191
|
+
description="Optional model for rho regression. ",
|
|
192
|
+
default_factory=OptionalField,
|
|
151
193
|
)
|
|
152
194
|
|
|
153
195
|
|
|
154
|
-
class KMuRegressionParams(
|
|
155
|
-
k_weights:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
196
|
+
class KMuRegressionParams(BaseModel):
|
|
197
|
+
k_weights: list[float] = Field(
|
|
198
|
+
description="\n\n".join(
|
|
199
|
+
[
|
|
200
|
+
"Polynomial coefficients for bulk modulus as a function of porosity:",
|
|
201
|
+
"`k(phi) = w1 + w2*phi + w3*phi^2 + ... + wn*phi^n`",
|
|
202
|
+
"List order: `[w1, w2, w3, ..., wn]`",
|
|
203
|
+
"where `phi` is porosity (fraction) and `k` is in Pa.",
|
|
204
|
+
]
|
|
205
|
+
),
|
|
159
206
|
)
|
|
160
|
-
mu_weights:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
mu_weights: list[float] = Field(
|
|
208
|
+
description="\n\n".join(
|
|
209
|
+
[
|
|
210
|
+
"Polynomial coefficients for shear modulus as a function of porosity:",
|
|
211
|
+
"`mu(phi) = w1 + w2*phi + w3*phi^2 + ... + wn*phi^n`",
|
|
212
|
+
"List order: `[w1, w2, w3, ..., wn]`",
|
|
213
|
+
"where `phi` is porosity (fraction) and `mu` is in Pa.",
|
|
214
|
+
]
|
|
215
|
+
),
|
|
164
216
|
)
|
|
165
|
-
mode:
|
|
217
|
+
mode: Literal["k_mu"] = Field(
|
|
166
218
|
default="k_mu",
|
|
167
219
|
description="Regression mode mode must be set to 'k_mu' for "
|
|
168
220
|
"estimation of bulk and shear modulus based on porosity",
|
|
169
221
|
)
|
|
222
|
+
rho_model: OptionalField | RhoRegressionParams = Field(
|
|
223
|
+
description="Optional model for rho regression. "
|
|
224
|
+
)
|
|
170
225
|
|
|
171
226
|
|
|
172
227
|
class RegressionModels(BaseModel):
|
|
@@ -180,23 +235,341 @@ class RegressionModels(BaseModel):
|
|
|
180
235
|
|
|
181
236
|
class PatchyCementRPM(BaseModel):
|
|
182
237
|
model_config = ConfigDict(title="Patchy Cement Model")
|
|
183
|
-
|
|
238
|
+
model_name: Literal[RPMType.PATCHY_CEMENT]
|
|
184
239
|
parameters: PatchyCementParams
|
|
185
240
|
|
|
186
241
|
|
|
187
242
|
class FriableRPM(BaseModel):
|
|
188
243
|
model_config = ConfigDict(title="Friable Sand Model")
|
|
189
|
-
|
|
190
|
-
parameters:
|
|
244
|
+
model_name: Literal[RPMType.FRIABLE]
|
|
245
|
+
parameters: FriableParams
|
|
191
246
|
|
|
192
247
|
|
|
193
248
|
class TMatrixRPM(BaseModel):
|
|
194
249
|
model_config = ConfigDict(title="T-Matrix Inclusion Model")
|
|
195
|
-
|
|
250
|
+
model_name: Literal[RPMType.T_MATRIX]
|
|
196
251
|
parameters: TMatrixParams
|
|
197
252
|
|
|
198
253
|
|
|
199
254
|
class RegressionRPM(BaseModel):
|
|
200
255
|
model_config = ConfigDict(title="Regression Model")
|
|
201
|
-
|
|
256
|
+
model_name: Literal[RPMType.REGRESSION]
|
|
202
257
|
parameters: RegressionModels
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ExpParams(BaseModel):
|
|
261
|
+
"""Exponential pressure model parameters."""
|
|
262
|
+
|
|
263
|
+
model_config = ConfigDict(title="Exponential Parameters")
|
|
264
|
+
|
|
265
|
+
a_factor: float = Field(description="Exponential coefficient A")
|
|
266
|
+
b_factor: float = Field(description="Exponential coefficient B")
|
|
267
|
+
|
|
268
|
+
def to_dict(self) -> dict[str, Any]:
|
|
269
|
+
"""Convert exponential parameters to dictionary."""
|
|
270
|
+
return {
|
|
271
|
+
"a_factor": self.a_factor,
|
|
272
|
+
"b_factor": self.b_factor,
|
|
273
|
+
"model_max_pressure": self.model_max_pressure,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
model_max_pressure: float = Field(
|
|
277
|
+
default=40, # MPa
|
|
278
|
+
description="Maximum pressure value for the exponential pressure"
|
|
279
|
+
" sensitive model",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class PolyParams(BaseModel):
|
|
284
|
+
"""Polynomial pressure model parameters."""
|
|
285
|
+
|
|
286
|
+
model_config = ConfigDict(title="Polynomial Parameters")
|
|
287
|
+
|
|
288
|
+
weights: list[float] = Field(description="Polynomial coefficients")
|
|
289
|
+
|
|
290
|
+
def to_dict(self) -> dict[str, Any]:
|
|
291
|
+
"""Convert polynomial parameters to dictionary."""
|
|
292
|
+
return {
|
|
293
|
+
"weights": self.weights,
|
|
294
|
+
"model_max_pressure": self.model_max_pressure,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
model_max_pressure: float = Field(
|
|
298
|
+
default=40, # MPa
|
|
299
|
+
description="Maximum pressure value for the polynomial pressure"
|
|
300
|
+
" sensitive model",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class RegressionPressureSensitivity(BaseModel):
|
|
305
|
+
"""
|
|
306
|
+
Pressure sensitivity model for rock physics modeling.
|
|
307
|
+
|
|
308
|
+
This model handles different pressure-dependent parameter types (VP/VS or K/MU)
|
|
309
|
+
using various model types (exponential, polynomial.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
model_config = ConfigDict(
|
|
313
|
+
arbitrary_types_allowed=True, title="Regression Pressure Sensitivity"
|
|
314
|
+
)
|
|
315
|
+
# Selections that cover model types Exponential/Polynomial and parameter types
|
|
316
|
+
# VP-VS or K-MU
|
|
317
|
+
model_type: RegressionPressureModelTypes = Field(
|
|
318
|
+
description="Type of pressure model"
|
|
319
|
+
)
|
|
320
|
+
mode: RegressionPressureParameterTypes = Field(
|
|
321
|
+
description="Parameter mode (VP/VS or K/MU)"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Parameter containers
|
|
325
|
+
parameters: dict[ParameterTypes, ExpParams | PolyParams]
|
|
326
|
+
|
|
327
|
+
@field_validator("model_type", mode="before")
|
|
328
|
+
@classmethod
|
|
329
|
+
def check_model_type(cls, v: str, info: ValidationInfo) -> str:
|
|
330
|
+
if v in list(RegressionPressureModelTypes):
|
|
331
|
+
return v
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"unknown physics pressure model type: {v}\n"
|
|
334
|
+
f"Should be one of {list(RegressionPressureModelTypes)}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
@field_validator("mode", mode="before")
|
|
338
|
+
@classmethod
|
|
339
|
+
def check_mode(cls, v: str, info: ValidationInfo) -> str:
|
|
340
|
+
if v in list(RegressionPressureParameterTypes):
|
|
341
|
+
return v
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"unknown physics pressure model type: {v}\n"
|
|
344
|
+
+ f"Should be one of {list(RegressionPressureParameterTypes)}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
@field_validator("parameters", mode="before")
|
|
348
|
+
@classmethod
|
|
349
|
+
def check_parameters(cls, v: dict, info: ValidationInfo) -> dict:
|
|
350
|
+
for key, value in v.items():
|
|
351
|
+
if key not in list(ParameterTypes):
|
|
352
|
+
raise ValueError(f"unknown pressure parameter: {key}")
|
|
353
|
+
if not isinstance(value, (ExpParams, PolyParams)):
|
|
354
|
+
raise ValueError(f"unknown pressure parameter type: {value}")
|
|
355
|
+
return v
|
|
356
|
+
|
|
357
|
+
@model_validator(mode="after")
|
|
358
|
+
def _validate_model_configuration(self) -> Self:
|
|
359
|
+
"""Validate model configuration and parameter consistency."""
|
|
360
|
+
if self.mode == RegressionPressureParameterTypes.VP_VS and not (
|
|
361
|
+
self.parameters.get(ParameterTypes.VP)
|
|
362
|
+
and self.parameters.get(ParameterTypes.VS)
|
|
363
|
+
):
|
|
364
|
+
raise ValueError("VP/VS mode requires both vp_parameters and vs_parameters")
|
|
365
|
+
if self.mode == RegressionPressureParameterTypes.K_MU and not (
|
|
366
|
+
self.parameters.get(ParameterTypes.K)
|
|
367
|
+
and self.parameters.get(ParameterTypes.MU)
|
|
368
|
+
):
|
|
369
|
+
raise ValueError("K/MU mode requires both k_parameters and mu_parameters")
|
|
370
|
+
"""Validate model configuration and parameter consistency."""
|
|
371
|
+
for key, value in self.parameters.items():
|
|
372
|
+
if self.model_type == RegressionPressureModelTypes.POLYNOMIAL and (
|
|
373
|
+
not isinstance(value, PolyParams)
|
|
374
|
+
):
|
|
375
|
+
raise ValueError(
|
|
376
|
+
"model parameter mismatch, expected polynomial weights"
|
|
377
|
+
)
|
|
378
|
+
if self.model_type == RegressionPressureModelTypes.EXPONENTIAL and (
|
|
379
|
+
not isinstance(value, ExpParams)
|
|
380
|
+
):
|
|
381
|
+
raise ValueError(
|
|
382
|
+
"model parameter mismatch, expected exponential parameters"
|
|
383
|
+
)
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def _get_ml_models(self):
|
|
387
|
+
ml_models = {}
|
|
388
|
+
for key, value in self.parameters.items():
|
|
389
|
+
if self.model_type == RegressionPressureModelTypes.POLYNOMIAL:
|
|
390
|
+
ml_models[key] = PolynomialPressureModel(**value.to_dict())
|
|
391
|
+
if self.model_type == RegressionPressureModelTypes.EXPONENTIAL:
|
|
392
|
+
ml_models[key] = ExponentialPressureModel(**value.to_dict())
|
|
393
|
+
return ml_models
|
|
394
|
+
|
|
395
|
+
def predict_elastic_properties(
|
|
396
|
+
self,
|
|
397
|
+
prop1: np.ndarray,
|
|
398
|
+
prop2: np.ndarray,
|
|
399
|
+
in_situ_press: np.ndarray,
|
|
400
|
+
depl_press: np.ndarray,
|
|
401
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
402
|
+
"""
|
|
403
|
+
Predict depleted elastic properties based on pressure change.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
prop1: First elastic property (K or VP depending on mode)
|
|
407
|
+
prop2: Second elastic property (MU or VS depending on mode)
|
|
408
|
+
in_situ_press: In-situ pressure array
|
|
409
|
+
depl_press: Depletion pressure array
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Tuple of depleted (prop1, prop2) arrays
|
|
413
|
+
"""
|
|
414
|
+
models = self._get_ml_models()
|
|
415
|
+
|
|
416
|
+
# Determine parameter keys based on mode
|
|
417
|
+
key1, key2 = (
|
|
418
|
+
(ParameterTypes.K, ParameterTypes.MU)
|
|
419
|
+
if self.mode == RegressionPressureParameterTypes.K_MU
|
|
420
|
+
else (ParameterTypes.VP, ParameterTypes.VS)
|
|
421
|
+
)
|
|
422
|
+
# The model has set a maximum depletion, make sure that the depleted
|
|
423
|
+
# pressure does not exceed this. Maximum depletion is given in MPa,
|
|
424
|
+
# must be converted to Pa
|
|
425
|
+
max_depl = 1.0e6 * min(
|
|
426
|
+
self.parameters[key1].model_max_pressure,
|
|
427
|
+
self.parameters[key2].model_max_pressure,
|
|
428
|
+
)
|
|
429
|
+
depl_press = in_situ_press + np.minimum(depl_press - in_situ_press, max_depl)
|
|
430
|
+
|
|
431
|
+
# Build input array for first property
|
|
432
|
+
input_array = np.concatenate(
|
|
433
|
+
(
|
|
434
|
+
prop1.reshape(-1, 1),
|
|
435
|
+
in_situ_press.reshape(-1, 1),
|
|
436
|
+
depl_press.reshape(-1, 1),
|
|
437
|
+
),
|
|
438
|
+
axis=1,
|
|
439
|
+
)
|
|
440
|
+
prop1_depl = models[key1].predict_abs(input_array, case="depl")
|
|
441
|
+
|
|
442
|
+
# Reuse array for second property
|
|
443
|
+
input_array[:, 0] = prop2
|
|
444
|
+
prop2_depl = models[key2].predict_abs(input_array, case="depl")
|
|
445
|
+
|
|
446
|
+
return prop1_depl, prop2_depl
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class PhysicsModelPressureSensitivity(BaseModel):
|
|
450
|
+
"""
|
|
451
|
+
Pressure sensitivity modelling using theoretical rock physics models.
|
|
452
|
+
|
|
453
|
+
Parameter types are determined by the model type. At this point, friable dry
|
|
454
|
+
rock model and patchy cement dry rock model are available, and both of them
|
|
455
|
+
are based on moduli (k, mu)
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
model_config = ConfigDict(
|
|
459
|
+
arbitrary_types_allowed=True, title="Physics Model Pressure Sensitivity"
|
|
460
|
+
)
|
|
461
|
+
model_type: PhysicsPressureModelTypes = Field(description="Type of pressure model")
|
|
462
|
+
parameters: PatchyCementParams | FriableParams = Field(
|
|
463
|
+
description="Dry rock model parameters"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
@field_validator("model_type", mode="before")
|
|
467
|
+
@classmethod
|
|
468
|
+
def check_model_type(cls, v: str, info: ValidationInfo) -> str:
|
|
469
|
+
if v in list(PhysicsPressureModelTypes):
|
|
470
|
+
return v
|
|
471
|
+
raise ValueError(
|
|
472
|
+
f"unknown physics pressure model type: {v}\n"
|
|
473
|
+
f"Should be one of {list(PhysicsPressureModelTypes)}"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
@model_validator(mode="after")
|
|
477
|
+
def _validate_model_configuration(self) -> Self:
|
|
478
|
+
if self.model_type == PhysicsPressureModelTypes.FRIABLE and (
|
|
479
|
+
not isinstance(self.parameters, FriableParams)
|
|
480
|
+
):
|
|
481
|
+
raise ValueError("Mismatch between Friable model and parameter set")
|
|
482
|
+
if self.model_type == PhysicsPressureModelTypes.PATCHY_CEMENT and (
|
|
483
|
+
not isinstance(self.parameters, PatchyCementParams)
|
|
484
|
+
):
|
|
485
|
+
raise ValueError("Mismatch between Patchy cement model and parameter set")
|
|
486
|
+
return self
|
|
487
|
+
|
|
488
|
+
def predict_elastic_properties(
|
|
489
|
+
self,
|
|
490
|
+
k_dry: np.ndarray,
|
|
491
|
+
mu_dry: np.ndarray,
|
|
492
|
+
poro: np.ndarray,
|
|
493
|
+
min_prop: EffectiveMineralProperties,
|
|
494
|
+
in_situ_press: np.ndarray,
|
|
495
|
+
depl_press: np.ndarray,
|
|
496
|
+
cem_prop: EffectiveMineralProperties | None = None,
|
|
497
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
498
|
+
# The only differences in inputs and parameters between friable model and
|
|
499
|
+
# patchy cement model are the parameter cement fraction and the cement
|
|
500
|
+
# mineral properties.
|
|
501
|
+
if (
|
|
502
|
+
cem_prop is None
|
|
503
|
+
and self.model_type == PhysicsPressureModelTypes.PATCHY_CEMENT
|
|
504
|
+
):
|
|
505
|
+
raise ValueError("patchy cement model requires cement mineral properties")
|
|
506
|
+
|
|
507
|
+
# To please the IDE ...
|
|
508
|
+
k_in_situ = None
|
|
509
|
+
mu_in_situ = None
|
|
510
|
+
k_depl = None
|
|
511
|
+
mu_depl = None
|
|
512
|
+
|
|
513
|
+
# The model has set a maximum depletion, make sure that the depleted pressure
|
|
514
|
+
# does not exceed this. Maximum depletion is given in MPa, must be converted
|
|
515
|
+
# to Pa
|
|
516
|
+
max_depl = 1.0e6 * self.parameters.model_max_pressure
|
|
517
|
+
depl_press = in_situ_press + np.minimum(depl_press - in_situ_press, max_depl)
|
|
518
|
+
|
|
519
|
+
if self.model_type == PhysicsPressureModelTypes.FRIABLE:
|
|
520
|
+
k_in_situ, mu_in_situ = friable_model_dry(
|
|
521
|
+
k_min=min_prop.bulk_modulus,
|
|
522
|
+
mu_min=min_prop.shear_modulus,
|
|
523
|
+
phi=poro,
|
|
524
|
+
p_eff=in_situ_press,
|
|
525
|
+
phi_c=self.parameters.critical_porosity,
|
|
526
|
+
coord_num_func=self.parameters.coordination_number_function,
|
|
527
|
+
n=self.parameters.coord_num,
|
|
528
|
+
shear_red=self.parameters.shear_reduction,
|
|
529
|
+
)
|
|
530
|
+
k_depl, mu_depl = friable_model_dry(
|
|
531
|
+
k_min=min_prop.bulk_modulus,
|
|
532
|
+
mu_min=min_prop.shear_modulus,
|
|
533
|
+
phi=poro,
|
|
534
|
+
p_eff=depl_press,
|
|
535
|
+
phi_c=self.parameters.critical_porosity,
|
|
536
|
+
coord_num_func=self.parameters.coordination_number_function,
|
|
537
|
+
n=self.parameters.coord_num,
|
|
538
|
+
shear_red=self.parameters.shear_reduction,
|
|
539
|
+
)
|
|
540
|
+
if self.model_type == PhysicsPressureModelTypes.PATCHY_CEMENT:
|
|
541
|
+
k_in_situ, mu_in_situ, _ = patchy_cement_model_dry(
|
|
542
|
+
k_min=min_prop.bulk_modulus,
|
|
543
|
+
mu_min=min_prop.shear_modulus,
|
|
544
|
+
rho_min=min_prop.density,
|
|
545
|
+
k_cem=cem_prop.bulk_modulus,
|
|
546
|
+
mu_cem=cem_prop.shear_modulus,
|
|
547
|
+
rho_cem=cem_prop.density,
|
|
548
|
+
phi=poro,
|
|
549
|
+
p_eff=in_situ_press,
|
|
550
|
+
frac_cem=self.parameters.cement_fraction,
|
|
551
|
+
phi_c=self.parameters.critical_porosity,
|
|
552
|
+
coord_num_func=self.parameters.coordination_number_function,
|
|
553
|
+
n=self.parameters.coord_num,
|
|
554
|
+
shear_red=self.parameters.shear_reduction,
|
|
555
|
+
)
|
|
556
|
+
k_depl, mu_depl, _ = patchy_cement_model_dry(
|
|
557
|
+
k_min=min_prop.bulk_modulus,
|
|
558
|
+
mu_min=min_prop.shear_modulus,
|
|
559
|
+
rho_min=min_prop.density,
|
|
560
|
+
k_cem=cem_prop.bulk_modulus,
|
|
561
|
+
mu_cem=cem_prop.shear_modulus,
|
|
562
|
+
rho_cem=cem_prop.density,
|
|
563
|
+
phi=poro,
|
|
564
|
+
p_eff=depl_press,
|
|
565
|
+
frac_cem=self.parameters.cement_fraction,
|
|
566
|
+
phi_c=self.parameters.critical_porosity,
|
|
567
|
+
coord_num_func=self.parameters.coordination_number_function,
|
|
568
|
+
n=self.parameters.coord_num,
|
|
569
|
+
shear_red=self.parameters.shear_reduction,
|
|
570
|
+
)
|
|
571
|
+
# Should not be necessary to test if there is an erroneous model type,
|
|
572
|
+
# that would have been caught in the model validation
|
|
573
|
+
k_dry = k_dry * (k_depl / k_in_situ)
|
|
574
|
+
mu_dry = mu_dry * (mu_depl / mu_in_situ)
|
|
575
|
+
return k_dry, mu_dry
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from dataclasses import
|
|
2
|
+
from dataclasses import asdict
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import xtgeo
|
|
@@ -34,7 +34,8 @@ def update_inactive_grid_cells(
|
|
|
34
34
|
init_mask = np.zeros_like(grid.actnum_array).astype(bool)
|
|
35
35
|
|
|
36
36
|
for prop in props:
|
|
37
|
-
|
|
37
|
+
# Iterate over all properties (vp, vs, density, ai, si, vpvs)
|
|
38
|
+
for prop_arr in asdict(prop).values():
|
|
38
39
|
init_mask = np.logical_or(init_mask, prop_arr.mask.astype(bool))
|
|
39
40
|
|
|
40
41
|
# To match the logic in xtgeo grid actnum, the mask must be inverted
|