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.
Files changed (35) hide show
  1. fmu/pem/__main__.py +32 -16
  2. fmu/pem/forward_models/pem_model.py +19 -27
  3. fmu/pem/pem_functions/__init__.py +2 -2
  4. fmu/pem/pem_functions/density.py +32 -38
  5. fmu/pem/pem_functions/effective_pressure.py +153 -48
  6. fmu/pem/pem_functions/estimate_saturated_rock.py +244 -52
  7. fmu/pem/pem_functions/fluid_properties.py +453 -246
  8. fmu/pem/pem_functions/mineral_properties.py +77 -74
  9. fmu/pem/pem_functions/pressure_sensitivity.py +430 -0
  10. fmu/pem/pem_functions/regression_models.py +129 -97
  11. fmu/pem/pem_functions/run_friable_model.py +106 -37
  12. fmu/pem/pem_functions/run_patchy_cement_model.py +107 -45
  13. fmu/pem/pem_functions/{run_t_matrix_and_pressure.py → run_t_matrix_model.py} +48 -27
  14. fmu/pem/pem_utilities/__init__.py +31 -9
  15. fmu/pem/pem_utilities/cumsum_properties.py +29 -37
  16. fmu/pem/pem_utilities/delta_cumsum_time.py +8 -13
  17. fmu/pem/pem_utilities/enum_defs.py +77 -4
  18. fmu/pem/pem_utilities/export_routines.py +84 -72
  19. fmu/pem/pem_utilities/fipnum_pvtnum_utilities.py +217 -0
  20. fmu/pem/pem_utilities/import_config.py +64 -46
  21. fmu/pem/pem_utilities/import_routines.py +58 -69
  22. fmu/pem/pem_utilities/pem_class_definitions.py +81 -23
  23. fmu/pem/pem_utilities/pem_config_validation.py +374 -149
  24. fmu/pem/pem_utilities/rpm_models.py +481 -83
  25. fmu/pem/pem_utilities/update_grid.py +3 -2
  26. fmu/pem/pem_utilities/utils.py +90 -38
  27. fmu/pem/run_pem.py +70 -39
  28. fmu/pem/version.py +16 -3
  29. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/METADATA +33 -28
  30. fmu_pem-0.0.3.dist-info/RECORD +39 -0
  31. fmu_pem-0.0.1.dist-info/RECORD +0 -37
  32. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/WHEEL +0 -0
  33. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/entry_points.txt +0 -0
  34. {fmu_pem-0.0.1.dist-info → fmu_pem-0.0.3.dist-info}/licenses/LICENSE +0 -0
  35. {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 enum import Enum
6
- from typing import List, Literal
5
+ from typing import Any, Literal, Self
7
6
 
8
- from pydantic import BaseModel, Field
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 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
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: Annotated[float, Field(gt=1.0e9)]
16
- shear_modulus: Annotated[float, Field(gt=1.0e9)]
17
- density: Annotated[float, Field(gt=1.0e3)]
18
-
19
-
20
- class PatchyCementParams(BaseModel):
21
- upper_bound_cement_fraction: float = Field(
22
- default=0.1,
23
- description="There is an upper limit for the constant cement model, which "
24
- "is part of the Patchy Cement model. Values higher than 0.1 can "
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
- shear_reduction: float = Field(
43
- default=0.5,
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
- coord_num_function: Literal["ConstVal", "PorBased"] = Field(
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="In case of a constant value for the number of grain contacts, "
58
- "a value of 8-9 is common",
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 RhoRegressionMixin(BaseModel):
101
- rho_weights: List[float] | None = Field(
102
- default=None,
103
- description="List of float values for polynomial regression for density "
104
- "based on porosity",
105
- )
106
- rho_regression: bool = Field(
107
- default=False,
108
- description="Matrix density is normally estimated from "
109
- "mineral composition and the density of each mineral. "
110
- "Setting this to True will estimate matrix "
111
- "density based on porosity alone",
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(RhoRegressionMixin):
116
- vp_weights: List[float] = Field(
117
- default=None,
118
- description="List of float values for polynomial regression for Vp "
119
- "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
+ ),
120
173
  )
121
- vs_weights: List[float] = Field(
122
- default=None,
123
- description="List of float values for polynomial regression for Vs "
124
- "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
+ ),
125
183
  )
126
184
  mode: Literal["vp_vs"] = Field(
127
185
  default="vp_vs",
128
- description="Regression mode mode must be set to 'vp_vs' for "
129
- "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,
130
193
  )
131
194
 
132
195
 
133
- class KMuRegressionParams(RhoRegressionMixin):
134
- k_weights: List[float] = Field(
135
- default=None,
136
- description="List of float values for polynomial regression for bulk modulus "
137
- "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
+ ),
138
206
  )
139
- mu_weights: List[float] = Field(
140
- default=None,
141
- description="List of float values for polynomial regression for shear modulus "
142
- "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
+ ),
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
- model: Literal[RPMType.PATCHY_CEMENT]
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
- model: Literal[RPMType.FRIABLE]
167
- parameters: PatchyCementParams
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
- model: Literal[RPMType.T_MATRIX]
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
- model: Literal[RPMType.REGRESSION]
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 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