fmu-pem 0.0.1__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/__init__.py +2 -0
- fmu/pem/__init__.py +19 -0
- fmu/pem/__main__.py +53 -0
- fmu/pem/forward_models/__init__.py +7 -0
- fmu/pem/forward_models/pem_model.py +72 -0
- fmu/pem/hook_implementations/__init__.py +0 -0
- fmu/pem/hook_implementations/jobs.py +19 -0
- fmu/pem/pem_functions/__init__.py +17 -0
- fmu/pem/pem_functions/density.py +55 -0
- fmu/pem/pem_functions/effective_pressure.py +168 -0
- fmu/pem/pem_functions/estimate_saturated_rock.py +90 -0
- fmu/pem/pem_functions/fluid_properties.py +281 -0
- fmu/pem/pem_functions/mineral_properties.py +230 -0
- fmu/pem/pem_functions/regression_models.py +261 -0
- fmu/pem/pem_functions/run_friable_model.py +119 -0
- fmu/pem/pem_functions/run_patchy_cement_model.py +120 -0
- fmu/pem/pem_functions/run_t_matrix_and_pressure.py +186 -0
- fmu/pem/pem_utilities/__init__.py +66 -0
- fmu/pem/pem_utilities/cumsum_properties.py +104 -0
- fmu/pem/pem_utilities/delta_cumsum_time.py +104 -0
- fmu/pem/pem_utilities/enum_defs.py +54 -0
- fmu/pem/pem_utilities/export_routines.py +272 -0
- fmu/pem/pem_utilities/import_config.py +93 -0
- fmu/pem/pem_utilities/import_routines.py +161 -0
- fmu/pem/pem_utilities/pem_class_definitions.py +113 -0
- fmu/pem/pem_utilities/pem_config_validation.py +505 -0
- fmu/pem/pem_utilities/rpm_models.py +177 -0
- fmu/pem/pem_utilities/update_grid.py +54 -0
- fmu/pem/pem_utilities/utils.py +262 -0
- fmu/pem/run_pem.py +98 -0
- fmu/pem/version.py +21 -0
- fmu_pem-0.0.1.dist-info/METADATA +768 -0
- fmu_pem-0.0.1.dist-info/RECORD +37 -0
- fmu_pem-0.0.1.dist-info/WHEEL +5 -0
- fmu_pem-0.0.1.dist-info/entry_points.txt +5 -0
- fmu_pem-0.0.1.dist-info/licenses/LICENSE +674 -0
- fmu_pem-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import date
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Literal, Optional, Self, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
DirectoryPath,
|
|
10
|
+
Field,
|
|
11
|
+
FilePath,
|
|
12
|
+
field_validator,
|
|
13
|
+
model_validator,
|
|
14
|
+
)
|
|
15
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
16
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
17
|
+
|
|
18
|
+
from fmu.pem import INTERNAL_EQUINOR
|
|
19
|
+
|
|
20
|
+
from .enum_defs import (
|
|
21
|
+
CO2Models,
|
|
22
|
+
FluidMixModel,
|
|
23
|
+
MineralMixModel,
|
|
24
|
+
OverburdenPressure,
|
|
25
|
+
VolumeFractions,
|
|
26
|
+
)
|
|
27
|
+
from .rpm_models import MineralProperties, PatchyCementRPM, RegressionRPM, TMatrixRPM
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NTGFraction(BaseModel):
|
|
31
|
+
mode: SkipJsonSchema[Literal[VolumeFractions.NTG_SIM]]
|
|
32
|
+
from_porosity: bool = Field(
|
|
33
|
+
default=False,
|
|
34
|
+
description="If True, net-to-gross is estimated from porosity parameter in "
|
|
35
|
+
"reservoir simulator INIT file. If False, net-to-gross is read "
|
|
36
|
+
"from the NTG parameter in the INIT file.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FractionFiles(BaseModel):
|
|
41
|
+
mode: SkipJsonSchema[Literal[VolumeFractions.VOL_FRAC]]
|
|
42
|
+
rel_path_fractions: DirectoryPath = Field(
|
|
43
|
+
default=Path("../../sim2seis/input/pem"),
|
|
44
|
+
description="Directory for volume fractions",
|
|
45
|
+
)
|
|
46
|
+
fractions_grid_file_name: FilePath = Field(
|
|
47
|
+
description="Grid definition of the volume fractions"
|
|
48
|
+
)
|
|
49
|
+
fractions_prop_file_names: list[Path] = Field(description="Volume fractions")
|
|
50
|
+
fraction_is_ntg: bool = Field(
|
|
51
|
+
default=True,
|
|
52
|
+
description="In case of a single fraction, it can either be a real volume "
|
|
53
|
+
"fraction or a net-to-gross parameter. If there is more than one fraction, "
|
|
54
|
+
"they have to represent real volume fractions, and this will be ignored",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RockMatrixProperties(BaseModel):
|
|
59
|
+
"""Configuration for rock matrix properties.
|
|
60
|
+
|
|
61
|
+
Contains parameters necessary for defining matrix properties for
|
|
62
|
+
different rock types, including sandstones, carbonates,
|
|
63
|
+
and other lithologies.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
rpm: Union[PatchyCementRPM, TMatrixRPM, RegressionRPM] = Field(
|
|
67
|
+
description="Selection of parameter set for rock physics model"
|
|
68
|
+
)
|
|
69
|
+
minerals: Dict[str, MineralProperties] = Field(
|
|
70
|
+
default={
|
|
71
|
+
"shale": {
|
|
72
|
+
"bulk_modulus": 25.0e9,
|
|
73
|
+
"shear_modulus": 12.0e9,
|
|
74
|
+
"density": 2680.0,
|
|
75
|
+
},
|
|
76
|
+
"quartz": {
|
|
77
|
+
"bulk_modulus": 36.8e9,
|
|
78
|
+
"shear_modulus": 44.0e9,
|
|
79
|
+
"density": 2650.0,
|
|
80
|
+
},
|
|
81
|
+
"calcite": {
|
|
82
|
+
"bulk_modulus": 76.8e9,
|
|
83
|
+
"shear_modulus": 32.0e9,
|
|
84
|
+
"density": 2710.0,
|
|
85
|
+
},
|
|
86
|
+
"dolomite": {
|
|
87
|
+
"bulk_modulus": 94.9e9,
|
|
88
|
+
"shear_modulus": 45.0e9,
|
|
89
|
+
"density": 2870.0,
|
|
90
|
+
},
|
|
91
|
+
"stevensite": {
|
|
92
|
+
"bulk_modulus": 32.5e9,
|
|
93
|
+
"shear_modulus": 45.0e9,
|
|
94
|
+
"density": 2490.0,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
description="Standard values are set for shale, quartz, calcite, dolomite and "
|
|
98
|
+
"stevensite. All settings can be changed by re-defining them in "
|
|
99
|
+
"the parameter file",
|
|
100
|
+
)
|
|
101
|
+
volume_fractions: NTGFraction | FractionFiles = Field(
|
|
102
|
+
default=NTGFraction,
|
|
103
|
+
description=r"Choice of volume fractions based on NTG from "
|
|
104
|
+
"simulator INIT file or from grid property file ",
|
|
105
|
+
)
|
|
106
|
+
fraction_names: List[str] = Field(
|
|
107
|
+
description="Fraction names must match the names in the volume fractions file",
|
|
108
|
+
)
|
|
109
|
+
fraction_minerals: List[str] = Field(
|
|
110
|
+
description="The list of minerals matching the fractions' definition. Each "
|
|
111
|
+
"mineral must be defined in the mineral properties dictionary"
|
|
112
|
+
)
|
|
113
|
+
shale_fractions: List[str] = Field(
|
|
114
|
+
description="List the fractions that should be regarded as non-net reservoir"
|
|
115
|
+
)
|
|
116
|
+
complement: str = Field(
|
|
117
|
+
description="For grid cells where the sum of the fractions does not add "
|
|
118
|
+
"up to 1.0, the remainder is filled with the complement mineral, "
|
|
119
|
+
"e.g. when using net-to-gross instead of volume fractions"
|
|
120
|
+
)
|
|
121
|
+
pressure_sensitivity: bool = Field(
|
|
122
|
+
default=True,
|
|
123
|
+
description="For the RPM models where pressure sensitivity is not part of "
|
|
124
|
+
"the model, as for friable and patchy cement models, a separate "
|
|
125
|
+
"pressure sensitivity model, based on plug measurements is added",
|
|
126
|
+
)
|
|
127
|
+
cement: str = Field(
|
|
128
|
+
default="quartz",
|
|
129
|
+
description="For the patchy cement model, the cement mineral must be defined, "
|
|
130
|
+
"and its properties must be defined in the mineral properties' "
|
|
131
|
+
"dictionary",
|
|
132
|
+
)
|
|
133
|
+
mineral_mix_model: MineralMixModel = Field(
|
|
134
|
+
default="voigt-reuss-hill",
|
|
135
|
+
description="Effective medium model selection: either "
|
|
136
|
+
"'hashin-shtrikman-average' or 'voigt-reuss-hill'",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@field_validator("shale_fractions", mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def shale_fraction_check(cls, v: list, info: ValidationInfo) -> list:
|
|
142
|
+
for frac in v:
|
|
143
|
+
if frac not in info.data["fraction_names"]:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f'{__file__}: shale fraction "{frac}" not listed in volume '
|
|
146
|
+
f"fraction names"
|
|
147
|
+
)
|
|
148
|
+
return v
|
|
149
|
+
|
|
150
|
+
@field_validator("complement", mode="before")
|
|
151
|
+
@classmethod
|
|
152
|
+
def complement_fraction_check(cls, v: list, info: ValidationInfo) -> list:
|
|
153
|
+
if v not in info.data["minerals"]:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f'{__file__}: shale fraction mineral "{v}" not listed in fraction '
|
|
156
|
+
f"minerals"
|
|
157
|
+
)
|
|
158
|
+
return v
|
|
159
|
+
|
|
160
|
+
@field_validator("cement", mode="before")
|
|
161
|
+
def cement_check(cls, v: list, info: ValidationInfo) -> list:
|
|
162
|
+
if v not in info.data["minerals"]:
|
|
163
|
+
raise ValueError(f'{__file__}: cement mineral "{v}" not listed in minerals')
|
|
164
|
+
return v
|
|
165
|
+
|
|
166
|
+
@model_validator(mode="after")
|
|
167
|
+
def mineral_fraction_check(self):
|
|
168
|
+
for frac_min in self.fraction_minerals:
|
|
169
|
+
if frac_min not in self.minerals:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"{__file__}: volume fraction mineral {frac_min} is not defined"
|
|
172
|
+
)
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Pressure
|
|
177
|
+
class Trend(BaseModel):
|
|
178
|
+
intercept: float = Field(description="Intercept in pressure depth trend")
|
|
179
|
+
gradient: float = Field(description="Gradient in pressure depth trend")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Overburden(BaseModel):
|
|
183
|
+
type: OverburdenPressure = Field(
|
|
184
|
+
description="Selection of 'trend' or 'constant' type for overburden pressure"
|
|
185
|
+
)
|
|
186
|
+
trend: Trend = Field(
|
|
187
|
+
description="Setting of intercept and gradient for pressure trend vs. depth"
|
|
188
|
+
)
|
|
189
|
+
constant: float = Field(description="Constant overburden pressure setting")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Pressure(BaseModel):
|
|
193
|
+
overburden: Overburden
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Fluids
|
|
197
|
+
class Brine(BaseModel):
|
|
198
|
+
salinity: float = Field(
|
|
199
|
+
default=35000.0,
|
|
200
|
+
gt=0.0,
|
|
201
|
+
description="Salinity of brine, with unit ppm (parts per million)",
|
|
202
|
+
)
|
|
203
|
+
perc_na: float = Field(
|
|
204
|
+
ge=0.0,
|
|
205
|
+
le=100.0,
|
|
206
|
+
default=0.0,
|
|
207
|
+
description="Percentage of NaCl in the dissolved salts in brine",
|
|
208
|
+
)
|
|
209
|
+
perc_ca: float = Field(
|
|
210
|
+
ge=0.0,
|
|
211
|
+
le=100.0,
|
|
212
|
+
default=100.0,
|
|
213
|
+
description="Percentage of CaCl in the dissolved salts in brine",
|
|
214
|
+
)
|
|
215
|
+
perc_k: float = Field(
|
|
216
|
+
ge=0.0,
|
|
217
|
+
le=100.0,
|
|
218
|
+
default=0.0,
|
|
219
|
+
description="Percentage of KCl in the dissolved salts in brine",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@model_validator(mode="after")
|
|
223
|
+
def check_perc_sum(self):
|
|
224
|
+
perc_sum = self.perc_na + self.perc_ca + self.perc_k
|
|
225
|
+
if np.isclose(perc_sum, 100.0):
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
eps = 0.1
|
|
229
|
+
if abs(perc_sum - 100.0) < eps:
|
|
230
|
+
# silently adjust values
|
|
231
|
+
self.perc_na *= 100.0 / perc_sum
|
|
232
|
+
self.perc_ca *= 100.0 / perc_sum
|
|
233
|
+
self.perc_k *= 100.0 / perc_sum
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"sum of chloride percentages "
|
|
238
|
+
f"({perc_sum}%) differs from 100% by more than 10% "
|
|
239
|
+
"in brine parameters"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class Oil(BaseModel):
|
|
244
|
+
gas_gravity: float = Field(
|
|
245
|
+
default=0.7,
|
|
246
|
+
ge=0.55,
|
|
247
|
+
le=0.87,
|
|
248
|
+
description="Gas gravity is a ratio of gas molecular weight to that air",
|
|
249
|
+
)
|
|
250
|
+
reference_density: float = Field(
|
|
251
|
+
default=865.0,
|
|
252
|
+
ge=700,
|
|
253
|
+
le=950,
|
|
254
|
+
description="Oil density in kg/m^3 at standard conditions, i.e. 15.6 deg. C "
|
|
255
|
+
"and 101 kPa",
|
|
256
|
+
)
|
|
257
|
+
gor: float = Field(
|
|
258
|
+
default=123.0,
|
|
259
|
+
ge=0.0,
|
|
260
|
+
description="Gas-oil volume ratio in liter/liter when the oil it brought to "
|
|
261
|
+
"the surface at standard conditions",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Gas(BaseModel):
|
|
266
|
+
gas_gravity: float = Field(
|
|
267
|
+
default=0.7,
|
|
268
|
+
ge=0.55,
|
|
269
|
+
le=0.87,
|
|
270
|
+
description="Gas gravity is a ratio of gas molecular weight to that air",
|
|
271
|
+
)
|
|
272
|
+
model: str = Field(
|
|
273
|
+
default="HC2016",
|
|
274
|
+
description="Gas model is one of 'Global', 'Light', or 'HC2016' (default)",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Note that CO2 does not require a separate definition here, as it's properties only
|
|
279
|
+
# depend on temperature and pressure
|
|
280
|
+
class Fluids(BaseModel):
|
|
281
|
+
brine: Brine = Field(
|
|
282
|
+
description="Brine model parameters",
|
|
283
|
+
)
|
|
284
|
+
oil: Oil = Field(description="Oil model parameters")
|
|
285
|
+
gas: Gas = Field(description="Gas model parameters")
|
|
286
|
+
condensate: Oil | None = Field(
|
|
287
|
+
default=None,
|
|
288
|
+
description="Condensate is defined by the same set of parameters as oil, "
|
|
289
|
+
"optional setting for condensate cases",
|
|
290
|
+
)
|
|
291
|
+
mix_method: FluidMixModel = Field(
|
|
292
|
+
description="Selection between Wood's or Brie model. Wood's model gives more "
|
|
293
|
+
"radical response to adding small amounts of gas in brine or oil"
|
|
294
|
+
)
|
|
295
|
+
brie_exponent: int = Field(
|
|
296
|
+
description="Brie exponent selects the mixing curve shape, from linear mix to "
|
|
297
|
+
"harmonic mean"
|
|
298
|
+
)
|
|
299
|
+
temperature: float = Field(
|
|
300
|
+
description="In most cases it is sufficient with a constant temperature "
|
|
301
|
+
"setting for the reservoir. If temperature is modelled in the "
|
|
302
|
+
"simulation model, it is preferred to use that"
|
|
303
|
+
)
|
|
304
|
+
salinity_from_sim: bool = Field(
|
|
305
|
+
default=False,
|
|
306
|
+
description="In most cases it is sufficient with a constant salinity "
|
|
307
|
+
"setting for the reservoir, unless there is large contrast"
|
|
308
|
+
"between formation water and injected water. If salinity is "
|
|
309
|
+
"modelled in the simulation model, it is preferred to use that",
|
|
310
|
+
)
|
|
311
|
+
temperature_from_sim: bool = Field(
|
|
312
|
+
default=False,
|
|
313
|
+
description="Flag to use temperature estimate from simulation model",
|
|
314
|
+
)
|
|
315
|
+
gas_saturation_is_co2: bool = Field(
|
|
316
|
+
default=False,
|
|
317
|
+
description="Eclipse model only provides one parameter for gas saturation, "
|
|
318
|
+
"this flag sets the gas type to be CO2 instead of hydrocarbon gas",
|
|
319
|
+
)
|
|
320
|
+
calculate_condensate: bool = Field(
|
|
321
|
+
default=False,
|
|
322
|
+
description="Flag to control if gas should be modelled with condensate model, "
|
|
323
|
+
"in which case RV parameter must be present in the Eclipse model",
|
|
324
|
+
)
|
|
325
|
+
gas_z_factor: float = Field(
|
|
326
|
+
default=1.0,
|
|
327
|
+
description="Factor for deviation from an ideal gas in terms of volume change "
|
|
328
|
+
"as a function of temperature and pressure",
|
|
329
|
+
)
|
|
330
|
+
co2_model: CO2Models = Field(
|
|
331
|
+
default="span_wagner",
|
|
332
|
+
description="Selection of model for CO2 properties, 'span_wagner' equation "
|
|
333
|
+
"of state model or 'flag'. Note that access to flag model depends "
|
|
334
|
+
"on licence",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
@model_validator(mode="after")
|
|
338
|
+
def check_fluid_type(self) -> Self:
|
|
339
|
+
if self.calculate_condensate and not INTERNAL_EQUINOR:
|
|
340
|
+
raise NotImplementedError(
|
|
341
|
+
"Missing model for condensate, proprietary model required"
|
|
342
|
+
)
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def possible_date_string(date_strings: List[str]) -> bool:
|
|
347
|
+
"""
|
|
348
|
+
Validate a list of date strings in YYYYMMDD format.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
date_strings: List of strings to validate
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
bool: True if all strings are valid dates
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: If any string is not a valid date in YYYYMMDD format
|
|
358
|
+
"""
|
|
359
|
+
for date_string in date_strings:
|
|
360
|
+
if len(date_string) != 8:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"Invalid date format: '{date_string}' must be exactly 8 characters"
|
|
363
|
+
)
|
|
364
|
+
try:
|
|
365
|
+
date(
|
|
366
|
+
year=int(date_string[0:4]),
|
|
367
|
+
month=int(date_string[4:6]),
|
|
368
|
+
day=int(date_string[6:]),
|
|
369
|
+
)
|
|
370
|
+
except ValueError:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"Invalid date: '{date_string}' must be a valid date in YYYYMMDD format"
|
|
373
|
+
)
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class FromGlobal(BaseModel):
|
|
378
|
+
grid_model: str
|
|
379
|
+
seis_dates: List[str]
|
|
380
|
+
diff_dates: List[List[str]]
|
|
381
|
+
global_config: Dict[str, Any]
|
|
382
|
+
|
|
383
|
+
@field_validator("seis_dates", mode="before")
|
|
384
|
+
def check_date_string(cls, v: List[str]) -> List[str]:
|
|
385
|
+
possible_date_string(v)
|
|
386
|
+
return v
|
|
387
|
+
|
|
388
|
+
@field_validator("diff_dates", mode="before")
|
|
389
|
+
def check_diffdate_string(cls, v: List[List[str]]) -> List[List[str]]:
|
|
390
|
+
for ll in v:
|
|
391
|
+
possible_date_string(ll)
|
|
392
|
+
return v
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class PemPaths(BaseModel):
|
|
396
|
+
rel_path_mandatory_output: SkipJsonSchema[DirectoryPath] = Field(
|
|
397
|
+
default=Path("../../sim2seis/output/pem"),
|
|
398
|
+
description="Directory of PEM results that will be used as input "
|
|
399
|
+
"to seismic_forward",
|
|
400
|
+
frozen=True,
|
|
401
|
+
)
|
|
402
|
+
rel_path_output: SkipJsonSchema[DirectoryPath] = Field(
|
|
403
|
+
default=Path("../../share/results/grids"),
|
|
404
|
+
description="Directory for grid parameter results from PEM for "
|
|
405
|
+
"later visualization",
|
|
406
|
+
frozen=True,
|
|
407
|
+
)
|
|
408
|
+
rel_path_pem: SkipJsonSchema[DirectoryPath] = Field(
|
|
409
|
+
default=Path("../../sim2seis/model"),
|
|
410
|
+
description="Relative path to the directory containing the PEM's config file",
|
|
411
|
+
frozen=True,
|
|
412
|
+
)
|
|
413
|
+
rel_path_fmu_config: SkipJsonSchema[DirectoryPath] = Field(
|
|
414
|
+
default=Path("../../fmuconfig/output"),
|
|
415
|
+
description="Relative path to the directory containing the global config "
|
|
416
|
+
"file for the FMU workflow",
|
|
417
|
+
frozen=True,
|
|
418
|
+
)
|
|
419
|
+
rel_ntg_grid_path: SkipJsonSchema[DirectoryPath] = Field(
|
|
420
|
+
default=Path("../../sim2seis/input/pem"),
|
|
421
|
+
description="This is the relative path to the ntg grid file. If the "
|
|
422
|
+
"ntg_calculation_flag is False, it is disregarded, cfr. fractions",
|
|
423
|
+
)
|
|
424
|
+
rel_path_simgrid: SkipJsonSchema[DirectoryPath] = Field(
|
|
425
|
+
default=Path("../../sim2seis/input/pem"),
|
|
426
|
+
description="Directory for eclipse simulation grid",
|
|
427
|
+
)
|
|
428
|
+
rel_path_geogrid: SkipJsonSchema[DirectoryPath] = Field(
|
|
429
|
+
default=Path("../../sim2seis/input/pem"),
|
|
430
|
+
description="If the porosity property is read from geogrid instead of from "
|
|
431
|
+
"simgrid, this directory is used. At present, porosity is expected "
|
|
432
|
+
"to come from the simgrid",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class Results(BaseModel):
|
|
437
|
+
save_results_to_rms: bool = Field(
|
|
438
|
+
default=False,
|
|
439
|
+
description="When the PEM is run from RMS, the results can be saved "
|
|
440
|
+
"directly to the RMS project",
|
|
441
|
+
)
|
|
442
|
+
save_results_to_disk: bool = Field(
|
|
443
|
+
default=True,
|
|
444
|
+
description="Results must be saved to disk for use in sim2seis setting etc",
|
|
445
|
+
)
|
|
446
|
+
save_intermediate_results: bool = Field(
|
|
447
|
+
default=False,
|
|
448
|
+
description="Intermediate results can be saved to a separate directory for "
|
|
449
|
+
"QC. Intermediate results include effective mineral and fluid "
|
|
450
|
+
"properties",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class PemConfig(BaseModel):
|
|
455
|
+
paths: SkipJsonSchema[PemPaths] = Field(
|
|
456
|
+
description="Default path settings exist, it is possible to override them, "
|
|
457
|
+
"mostly relevant for input paths",
|
|
458
|
+
)
|
|
459
|
+
rock_matrix: RockMatrixProperties = Field(
|
|
460
|
+
description="Settings related to effective mineral properties and rock "
|
|
461
|
+
"physics model",
|
|
462
|
+
)
|
|
463
|
+
fluids: Fluids = Field(
|
|
464
|
+
description="Settings related to fluid composition",
|
|
465
|
+
)
|
|
466
|
+
pressure: Pressure = Field(
|
|
467
|
+
description="Definition of overburden pressure model - constant or trend",
|
|
468
|
+
)
|
|
469
|
+
results: Results = Field(
|
|
470
|
+
description="Flags for saving results of the PEM",
|
|
471
|
+
)
|
|
472
|
+
diff_calculation: Dict[str, List[Literal["ratio", "diff", "diffpercent"]]] = Field(
|
|
473
|
+
description="Difference properties of the PEM can be calculated for the dates "
|
|
474
|
+
"in the Eclipse UNRST file. The settings decide which parameters "
|
|
475
|
+
"difference properties will be generated for, and what kind of "
|
|
476
|
+
"difference calculation is run - normal difference, percent "
|
|
477
|
+
"difference or ratio"
|
|
478
|
+
)
|
|
479
|
+
global_params: Optional[FromGlobal] = None
|
|
480
|
+
|
|
481
|
+
@field_validator("paths", mode="before")
|
|
482
|
+
def check_and_create_directories(cls, v):
|
|
483
|
+
for key, path in v.items():
|
|
484
|
+
if key == "rel_path_intermed_output" or key == "rel_path_output":
|
|
485
|
+
os.makedirs(path, exist_ok=True)
|
|
486
|
+
else:
|
|
487
|
+
if not Path(path).exists():
|
|
488
|
+
raise ValueError(f"Directory {path} does not exist")
|
|
489
|
+
return v
|
|
490
|
+
|
|
491
|
+
@field_validator("diff_calculation", mode="before")
|
|
492
|
+
def to_list(cls, v: Dict) -> Dict:
|
|
493
|
+
v_keys = list(v.keys())
|
|
494
|
+
v_val = list(v.values())
|
|
495
|
+
for i, val_item in enumerate(v_val):
|
|
496
|
+
if not isinstance(val_item, list):
|
|
497
|
+
v_val[i] = [
|
|
498
|
+
val_item,
|
|
499
|
+
]
|
|
500
|
+
return dict(zip(v_keys, v_val))
|
|
501
|
+
|
|
502
|
+
# Add global parameters used in the PEM
|
|
503
|
+
def update_with_global(self, global_params: dict):
|
|
504
|
+
self.global_params = FromGlobal(**global_params)
|
|
505
|
+
return self
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Define RPM model types and their parameters
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from fmu.pem.pem_utilities.enum_defs import RPMType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
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
|
+
)
|
|
35
|
+
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",
|
|
41
|
+
)
|
|
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",
|
|
49
|
+
)
|
|
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(
|
|
56
|
+
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",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TMatrixParams(BaseModel):
|
|
63
|
+
t_mat_model_version: Literal["PETEC", "EXP"] = Field(
|
|
64
|
+
description="When T Matrix model is calibrated and optimised based on well "
|
|
65
|
+
"data, a selection is made on how much information will be "
|
|
66
|
+
"available when the calibrated model is applied to a PEM model"
|
|
67
|
+
)
|
|
68
|
+
angle: float = Field(
|
|
69
|
+
default=90.0,
|
|
70
|
+
ge=0.0,
|
|
71
|
+
lt=180.0,
|
|
72
|
+
description="Angle between axis of symmetry and horizontal plane. A standard "
|
|
73
|
+
"VTI medium will have 90 deg angle",
|
|
74
|
+
)
|
|
75
|
+
freq: float = Field(
|
|
76
|
+
default=100.0,
|
|
77
|
+
gt=0.0,
|
|
78
|
+
description="Frequency of the acoustic signal in Hz. For seismic, a standard "
|
|
79
|
+
"value of 100 is used. Sonic log or ultrasonic measurements will "
|
|
80
|
+
"require a higher value",
|
|
81
|
+
)
|
|
82
|
+
perm: float = Field(
|
|
83
|
+
default=100.0,
|
|
84
|
+
gt=0.0,
|
|
85
|
+
description="Permeability of the rock matrix in mD. A standard value of 100 mD "
|
|
86
|
+
"is commonly used",
|
|
87
|
+
)
|
|
88
|
+
visco: float = Field(
|
|
89
|
+
default=10.0,
|
|
90
|
+
gt=0.0,
|
|
91
|
+
description="Fluid viscosity in cP",
|
|
92
|
+
)
|
|
93
|
+
tau: float = Field(
|
|
94
|
+
default=1.0e-7,
|
|
95
|
+
description="A time factor for reaching equilibrium in fluid movement. Best "
|
|
96
|
+
"left alone",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
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",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
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",
|
|
120
|
+
)
|
|
121
|
+
vs_weights: List[float] = Field(
|
|
122
|
+
default=None,
|
|
123
|
+
description="List of float values for polynomial regression for Vs "
|
|
124
|
+
"based on porosity",
|
|
125
|
+
)
|
|
126
|
+
mode: Literal["vp_vs"] = Field(
|
|
127
|
+
default="vp_vs",
|
|
128
|
+
description="Regression mode mode must be set to 'vp_vs' for "
|
|
129
|
+
"estimation of Vp and Vs based on porosity",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
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",
|
|
138
|
+
)
|
|
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",
|
|
143
|
+
)
|
|
144
|
+
mode: Literal["k_mu"] = Field(
|
|
145
|
+
default="k_mu",
|
|
146
|
+
description="Regression mode mode must be set to 'k_mu' for "
|
|
147
|
+
"estimation of bulk and shear modulus based on porosity",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class RegressionModels(BaseModel):
|
|
152
|
+
sandstone: VpVsRegressionParams | KMuRegressionParams = Field(
|
|
153
|
+
description="Selection of model type for sandstone regression model"
|
|
154
|
+
)
|
|
155
|
+
shale: VpVsRegressionParams | KMuRegressionParams = Field(
|
|
156
|
+
description="Selection of model type for shale regression model"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class PatchyCementRPM(BaseModel):
|
|
161
|
+
model: Literal[RPMType.PATCHY_CEMENT]
|
|
162
|
+
parameters: PatchyCementParams
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class FriableRPM(BaseModel):
|
|
166
|
+
model: Literal[RPMType.FRIABLE]
|
|
167
|
+
parameters: PatchyCementParams
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TMatrixRPM(BaseModel):
|
|
171
|
+
model: Literal[RPMType.T_MATRIX]
|
|
172
|
+
parameters: TMatrixParams
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class RegressionRPM(BaseModel):
|
|
176
|
+
model: Literal[RPMType.REGRESSION]
|
|
177
|
+
parameters: RegressionModels
|