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.
Files changed (36) hide show
  1. fmu/pem/__init__.py +2 -0
  2. fmu/pem/__main__.py +72 -19
  3. fmu/pem/forward_models/pem_model.py +21 -26
  4. fmu/pem/pem_functions/__init__.py +2 -2
  5. fmu/pem/pem_functions/density.py +32 -38
  6. fmu/pem/pem_functions/effective_pressure.py +153 -49
  7. fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
  8. fmu/pem/pem_functions/fluid_properties.py +447 -245
  9. fmu/pem/pem_functions/mineral_properties.py +77 -74
  10. fmu/pem/pem_functions/pressure_sensitivity.py +430 -0
  11. fmu/pem/pem_functions/regression_models.py +129 -97
  12. fmu/pem/pem_functions/run_friable_model.py +106 -37
  13. fmu/pem/pem_functions/run_patchy_cement_model.py +107 -45
  14. fmu/pem/pem_functions/{run_t_matrix_and_pressure.py → run_t_matrix_model.py} +48 -27
  15. fmu/pem/pem_utilities/__init__.py +30 -10
  16. fmu/pem/pem_utilities/cumsum_properties.py +29 -37
  17. fmu/pem/pem_utilities/delta_cumsum_time.py +8 -13
  18. fmu/pem/pem_utilities/enum_defs.py +65 -8
  19. fmu/pem/pem_utilities/export_routines.py +84 -72
  20. fmu/pem/pem_utilities/fipnum_pvtnum_utilities.py +217 -0
  21. fmu/pem/pem_utilities/import_config.py +76 -50
  22. fmu/pem/pem_utilities/import_routines.py +57 -69
  23. fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
  24. fmu/pem/pem_utilities/pem_config_validation.py +364 -172
  25. fmu/pem/pem_utilities/rpm_models.py +473 -100
  26. fmu/pem/pem_utilities/update_grid.py +3 -2
  27. fmu/pem/pem_utilities/utils.py +90 -38
  28. fmu/pem/run_pem.py +66 -48
  29. fmu/pem/version.py +16 -3
  30. {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/METADATA +19 -11
  31. fmu_pem-0.0.4.dist-info/RECORD +39 -0
  32. {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/WHEEL +1 -1
  33. fmu_pem-0.0.2.dist-info/RECORD +0 -37
  34. {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/entry_points.txt +0 -0
  35. {fmu_pem-0.0.2.dist-info → fmu_pem-0.0.4.dist-info}/licenses/LICENSE +0 -0
  36. {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 List, Literal
5
+ from typing import Any, Literal, Self
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field
8
- from pydantic.json_schema import SkipJsonSchema
9
- from typing_extensions import Annotated
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 CoordinationNumberFunction, RPMType
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 MineralProperties(BaseModel):
15
- bulk_modulus: float = Field(gt=1.0e9, lt=5.0e11, description="Units: Pa")
16
- shear_modulus: float = Field(gt=1.0e9, lt=5.0e11, description="Units: Pa")
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
- class CoordinationNumberPorBased(BaseModel):
21
- fcn: SkipJsonSchema[CoordinationNumberFunction] = Field(
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
- class PatchyCementParams(BaseModel):
42
- upper_bound_cement_fraction: float = Field(
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
- cement_fraction: float = Field(
49
- default=0.04,
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
- critical_porosity: float = Field(
57
- default=0.4,
58
- ge=0.3,
59
- le=0.5,
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.5,
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
- coordination_number_function: (
72
- CoordinationNumberPorBased | CoordinationNumberConstVal
73
- ) = Field(
74
- default_factory=CoordinationNumberPorBased,
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 RhoRegressionMixin(BaseModel):
119
- rho_weights: List[float] = Field(
120
- default=[
121
- 1.0,
122
- ],
123
- description="List of float values for polynomial regression for density "
124
- "based on porosity",
125
- )
126
- rho_regression: bool = Field(
127
- default=False,
128
- description="Matrix density is normally estimated from "
129
- "mineral composition and the density of each mineral. "
130
- "Setting this to True will estimate matrix "
131
- "density based on porosity alone. In that case, check the "
132
- "rho regression parameters",
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(RhoRegressionMixin):
137
- vp_weights: List[float] = Field(
138
- default=None,
139
- description="List of float values for polynomial regression for Vp "
140
- "based on porosity",
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: List[float] = Field(
143
- default=None,
144
- description="List of float values for polynomial regression for Vs "
145
- "based on porosity",
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: SkipJsonSchema[Literal["vp_vs"]] = Field(
184
+ mode: Literal["vp_vs"] = Field(
148
185
  default="vp_vs",
149
- description="Regression mode mode must be set to 'vp_vs' for "
150
- "estimation of Vp and Vs based on porosity",
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(RhoRegressionMixin):
155
- k_weights: List[float] = Field(
156
- default=None,
157
- description="List of float values for polynomial regression for bulk modulus "
158
- "based on porosity",
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: List[float] = Field(
161
- default=None,
162
- description="List of float values for polynomial regression for shear modulus "
163
- "based on porosity",
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: SkipJsonSchema[Literal["k_mu"]] = Field(
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
- model: SkipJsonSchema[Literal[RPMType.PATCHY_CEMENT]]
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
- model: SkipJsonSchema[Literal[RPMType.FRIABLE]]
190
- parameters: PatchyCementParams
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
- model: SkipJsonSchema[Literal[RPMType.T_MATRIX]]
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
- model: SkipJsonSchema[Literal[RPMType.REGRESSION]]
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 astuple
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
- for prop_arr in astuple(prop): # noqa: type
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