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.
- fmu/pem/__main__.py +32 -16
- fmu/pem/forward_models/pem_model.py +19 -27
- fmu/pem/pem_functions/__init__.py +2 -2
- fmu/pem/pem_functions/density.py +32 -38
- fmu/pem/pem_functions/effective_pressure.py +153 -48
- fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
- fmu/pem/pem_functions/fluid_properties.py +453 -246
- 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 +31 -9
- 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 +77 -4
- 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 +64 -46
- fmu/pem/pem_utilities/import_routines.py +58 -69
- fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
- fmu/pem/pem_utilities/pem_config_validation.py +374 -149
- fmu/pem/pem_utilities/rpm_models.py +481 -83
- fmu/pem/pem_utilities/update_grid.py +3 -2
- fmu/pem/pem_utilities/utils.py +90 -38
- fmu/pem/run_pem.py +70 -39
- fmu/pem/version.py +16 -3
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/METADATA +33 -28
- fmu_pem-0.0.3.dist-info/RECORD +39 -0
- fmu_pem-0.0.1.dist-info/RECORD +0 -37
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/WHEEL +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/entry_points.txt +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -2,68 +2,112 @@
|
|
|
2
2
|
Define RPM model types and their parameters
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from typing import List, Literal
|
|
5
|
+
from typing import Any, Literal, Self
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
from
|
|
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
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OptionalField(BaseModel):
|
|
37
|
+
def __eq__(self, other):
|
|
38
|
+
return other is None
|
|
39
|
+
|
|
40
|
+
def __ne__(self, other):
|
|
41
|
+
return not self.__eq__(other)
|
|
42
|
+
|
|
43
|
+
def __bool__(self):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(title="This field is optional")
|
|
12
47
|
|
|
13
48
|
|
|
14
49
|
class MineralProperties(BaseModel):
|
|
15
|
-
bulk_modulus:
|
|
16
|
-
shear_modulus:
|
|
17
|
-
density:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"lead to the model reaching saturation",
|
|
26
|
-
)
|
|
27
|
-
cement_fraction: float = Field(
|
|
28
|
-
default=0.04,
|
|
29
|
-
gt=0,
|
|
30
|
-
le=0.1,
|
|
31
|
-
description="Representative cement fraction for Patchy Cement should be chosen "
|
|
32
|
-
"so that the model trend line goes through the median of the log "
|
|
33
|
-
"values",
|
|
34
|
-
)
|
|
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")
|
|
59
|
+
|
|
35
60
|
critical_porosity: float = Field(
|
|
36
|
-
default=0.4,
|
|
37
|
-
ge=0.3,
|
|
38
|
-
le=0.5,
|
|
39
|
-
description="Critical porosity is the porosity of the sands at the time of "
|
|
40
|
-
"deposition, before any compaction",
|
|
61
|
+
ge=0.3, le=0.5, default=0.4, description="Critical porosity"
|
|
41
62
|
)
|
|
42
|
-
|
|
43
|
-
default=
|
|
44
|
-
ge=0.0,
|
|
45
|
-
le=1.0,
|
|
46
|
-
description="Shear reduction is related to the fraction of tangential friction "
|
|
47
|
-
"between grains. Shear reduction of 1 means frictionless contact, "
|
|
48
|
-
"and 0 means full friction",
|
|
63
|
+
coordination_number_function: CoordinationNumberFunction = Field(
|
|
64
|
+
default="PorBased", description="Coordination number function"
|
|
49
65
|
)
|
|
50
|
-
|
|
51
|
-
default="PorBased",
|
|
52
|
-
description="Coordinate number is the number of grain-grain contacts. It is "
|
|
53
|
-
"normally assumed to be a function of porosity for friable sand",
|
|
54
|
-
)
|
|
55
|
-
coordination_number: float = Field(
|
|
66
|
+
coord_num: float = Field(
|
|
56
67
|
default=9.0,
|
|
57
|
-
description="
|
|
58
|
-
"
|
|
68
|
+
description="Coordination number value."
|
|
69
|
+
" This is normally only used in patchy cement model",
|
|
59
70
|
)
|
|
71
|
+
shear_reduction: float = Field(
|
|
72
|
+
default=1.0, ge=0, le=1, description="Shear reduction factor"
|
|
73
|
+
)
|
|
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",
|
|
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
|
|
60
103
|
|
|
61
104
|
|
|
62
105
|
class TMatrixParams(BaseModel):
|
|
63
106
|
t_mat_model_version: Literal["PETEC", "EXP"] = Field(
|
|
107
|
+
default="PETEC",
|
|
64
108
|
description="When T Matrix model is calibrated and optimised based on well "
|
|
65
109
|
"data, a selection is made on how much information will be "
|
|
66
|
-
"available when the calibrated model is applied to a PEM model"
|
|
110
|
+
"available when the calibrated model is applied to a PEM model",
|
|
67
111
|
)
|
|
68
112
|
angle: float = Field(
|
|
69
113
|
default=90.0,
|
|
@@ -97,55 +141,87 @@ class TMatrixParams(BaseModel):
|
|
|
97
141
|
)
|
|
98
142
|
|
|
99
143
|
|
|
100
|
-
class
|
|
101
|
-
rho_weights:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
)
|
|
112
159
|
)
|
|
113
160
|
|
|
114
161
|
|
|
115
|
-
class VpVsRegressionParams(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
description="
|
|
119
|
-
|
|
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
|
+
),
|
|
120
173
|
)
|
|
121
|
-
vs_weights:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
),
|
|
125
183
|
)
|
|
126
184
|
mode: Literal["vp_vs"] = Field(
|
|
127
185
|
default="vp_vs",
|
|
128
|
-
description="
|
|
129
|
-
"
|
|
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,
|
|
130
193
|
)
|
|
131
194
|
|
|
132
195
|
|
|
133
|
-
class KMuRegressionParams(
|
|
134
|
-
k_weights:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
),
|
|
138
206
|
)
|
|
139
|
-
mu_weights:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
),
|
|
143
216
|
)
|
|
144
217
|
mode: Literal["k_mu"] = Field(
|
|
145
218
|
default="k_mu",
|
|
146
219
|
description="Regression mode mode must be set to 'k_mu' for "
|
|
147
220
|
"estimation of bulk and shear modulus based on porosity",
|
|
148
221
|
)
|
|
222
|
+
rho_model: OptionalField | RhoRegressionParams = Field(
|
|
223
|
+
description="Optional model for rho regression. "
|
|
224
|
+
)
|
|
149
225
|
|
|
150
226
|
|
|
151
227
|
class RegressionModels(BaseModel):
|
|
@@ -158,20 +234,342 @@ class RegressionModels(BaseModel):
|
|
|
158
234
|
|
|
159
235
|
|
|
160
236
|
class PatchyCementRPM(BaseModel):
|
|
161
|
-
|
|
237
|
+
model_config = ConfigDict(title="Patchy Cement Model")
|
|
238
|
+
model_name: Literal[RPMType.PATCHY_CEMENT]
|
|
162
239
|
parameters: PatchyCementParams
|
|
163
240
|
|
|
164
241
|
|
|
165
242
|
class FriableRPM(BaseModel):
|
|
166
|
-
|
|
167
|
-
|
|
243
|
+
model_config = ConfigDict(title="Friable Sand Model")
|
|
244
|
+
model_name: Literal[RPMType.FRIABLE]
|
|
245
|
+
parameters: FriableParams
|
|
168
246
|
|
|
169
247
|
|
|
170
248
|
class TMatrixRPM(BaseModel):
|
|
171
|
-
|
|
249
|
+
model_config = ConfigDict(title="T-Matrix Inclusion Model")
|
|
250
|
+
model_name: Literal[RPMType.T_MATRIX]
|
|
172
251
|
parameters: TMatrixParams
|
|
173
252
|
|
|
174
253
|
|
|
175
254
|
class RegressionRPM(BaseModel):
|
|
176
|
-
|
|
255
|
+
model_config = ConfigDict(title="Regression Model")
|
|
256
|
+
model_name: Literal[RPMType.REGRESSION]
|
|
177
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
|