resfo-utilities 0.3.0b0__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.
Potentially problematic release.
This version of resfo-utilities might be problematic. Click here for more details.
- resfo_utilities/__init__.py +29 -0
- resfo_utilities/_cornerpoint_grid.py +569 -0
- resfo_utilities/_summary_keys.py +404 -0
- resfo_utilities/_summary_reader.py +594 -0
- resfo_utilities/testing/__init__.py +88 -0
- resfo_utilities/testing/_egrid_generator.py +422 -0
- resfo_utilities/testing/_summary_generator.py +568 -0
- resfo_utilities-0.3.0b0.dist-info/METADATA +74 -0
- resfo_utilities-0.3.0b0.dist-info/RECORD +12 -0
- resfo_utilities-0.3.0b0.dist-info/WHEEL +5 -0
- resfo_utilities-0.3.0b0.dist-info/licenses/LICENSE.md +166 -0
- resfo_utilities-0.3.0b0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The egrid fileformat is a file used by reservoir simulators such as opm
|
|
3
|
+
flow containing the grid geometry.
|
|
4
|
+
|
|
5
|
+
For details about the data format see https://resfo.readthedocs.io/en/latest/the_file_format.html
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
The basic usage is to use the ``egrids`` generator::
|
|
9
|
+
|
|
10
|
+
from hypothesis import given
|
|
11
|
+
from resfo_utilities import egrids, EGrid
|
|
12
|
+
|
|
13
|
+
@given(egrids)
|
|
14
|
+
def test_egrid(egrid: EGrid):
|
|
15
|
+
print(egrid.shape) # tuple ni,nj,nk
|
|
16
|
+
egrid.to_file("MY_CASE.EGRID")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
egrid files contain tuples of keywords and list of data values
|
|
20
|
+
of one type (An array with a name). The enums in this file generally describe
|
|
21
|
+
a range of values for a position in one of these lists, the dataclasses describe
|
|
22
|
+
the values of one keyword or a collection of those, named a file section.
|
|
23
|
+
|
|
24
|
+
The following egrid file contents (as keyword/array pairs)::
|
|
25
|
+
|
|
26
|
+
("FILEHEAD", [2001,3,0,3,0,0,0])
|
|
27
|
+
("GRIDUNIT", "METRES ")
|
|
28
|
+
|
|
29
|
+
is represented by::
|
|
30
|
+
|
|
31
|
+
EGrid(
|
|
32
|
+
Filehead(2001,3,3,TypeOfGrid.CORNER_POINT,RockModel(0),GridFormat(0)),
|
|
33
|
+
GridUnit("METRES ")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
Where ``EGrid`` is a section of the file, ``Filehead`` and ``GridUnit`` are
|
|
37
|
+
keywords.
|
|
38
|
+
|
|
39
|
+
Generally, the data layout of these objects map 1-to-1 with some section of an
|
|
40
|
+
valid egrid file.
|
|
41
|
+
|
|
42
|
+
keywords implement the `to_egrid` that convert from the object representation
|
|
43
|
+
to the in file representation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from dataclasses import astuple, dataclass
|
|
47
|
+
from enum import Enum, auto, unique
|
|
48
|
+
from typing import Any, assert_never, IO
|
|
49
|
+
from os import PathLike
|
|
50
|
+
|
|
51
|
+
import hypothesis.strategies as st
|
|
52
|
+
import numpy as np
|
|
53
|
+
import resfo
|
|
54
|
+
from hypothesis.extra.numpy import arrays
|
|
55
|
+
import numpy.typing as npt
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@unique
|
|
59
|
+
class Units(Enum):
|
|
60
|
+
"""The Grids distance units."""
|
|
61
|
+
|
|
62
|
+
METRES = auto()
|
|
63
|
+
CM = auto()
|
|
64
|
+
FEET = auto()
|
|
65
|
+
|
|
66
|
+
def to_egrid(self) -> str:
|
|
67
|
+
return self.name.ljust(8)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@unique
|
|
71
|
+
class GridRelative(Enum):
|
|
72
|
+
"""GridRelative is the second value given GRIDUNIT keyword.
|
|
73
|
+
|
|
74
|
+
MAP means map relative units, while
|
|
75
|
+
leaving it blank means relative to the origin given by the
|
|
76
|
+
MAPAXES keyword.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
MAP = auto()
|
|
80
|
+
ORIGIN = auto()
|
|
81
|
+
|
|
82
|
+
def to_egrid(self) -> str:
|
|
83
|
+
if self == GridRelative.MAP:
|
|
84
|
+
return "MAP".ljust(8)
|
|
85
|
+
else:
|
|
86
|
+
return "".ljust(8)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class GrdeclKeyword:
|
|
91
|
+
"""An abstract grdecl keyword.
|
|
92
|
+
|
|
93
|
+
Gives a general implementation of to/from grdecl which recurses on
|
|
94
|
+
fields. Ie. a dataclass such as
|
|
95
|
+
>>> class A(GrdeclKeyword):
|
|
96
|
+
... ...
|
|
97
|
+
>>> class B(GrdeclKeyword):
|
|
98
|
+
... ...
|
|
99
|
+
|
|
100
|
+
>>> @dataclass
|
|
101
|
+
... class MyKeyword(GrdeclKeyword):
|
|
102
|
+
... field1: A
|
|
103
|
+
... field2: B
|
|
104
|
+
|
|
105
|
+
will have a to_egrid method that will be similar to
|
|
106
|
+
|
|
107
|
+
>>> def to_egrid(self):
|
|
108
|
+
... return [self.field1.to_egrid(), self.field2.to_egrid]
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def to_egrid(self) -> list[Any]:
|
|
112
|
+
return [value.to_egrid() for value in astuple(self)]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class GridUnit(GrdeclKeyword):
|
|
117
|
+
"""Defines the units used for grid dimensions.
|
|
118
|
+
|
|
119
|
+
The first value is a string describing the units used, defaults to METRES,
|
|
120
|
+
known accepted other units are FIELD and LAB. The last value describes
|
|
121
|
+
whether the measurements are relative to the map or to the origin of
|
|
122
|
+
MAPAXES.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
unit: Units = Units.METRES
|
|
126
|
+
grid_relative: GridRelative = GridRelative.ORIGIN
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@unique
|
|
130
|
+
class CoordinateType(Enum):
|
|
131
|
+
"""The coordinate system type given in the SPECGRID keyword.
|
|
132
|
+
|
|
133
|
+
This is given by either T or F in the last value of SPECGRID, meaning
|
|
134
|
+
either cylindrical or cartesian coordinates respectively.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
CARTESIAN = auto()
|
|
138
|
+
CYLINDRICAL = auto()
|
|
139
|
+
|
|
140
|
+
def to_egrid(self) -> int:
|
|
141
|
+
if self == CoordinateType.CARTESIAN:
|
|
142
|
+
return 0
|
|
143
|
+
else:
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@unique
|
|
148
|
+
class TypeOfGrid(Enum):
|
|
149
|
+
"""
|
|
150
|
+
A Grid has three possible data layout formats, UNSTRUCTURED, CORNER_POINT,
|
|
151
|
+
BLOCK_CENTER and COMPOSITE (meaning combination of the two former).
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
COMPOSITE = 0
|
|
155
|
+
CORNER_POINT = 1
|
|
156
|
+
UNSTRUCTURED = 2
|
|
157
|
+
BLOCK_CENTER = 3
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def alternate_value(self) -> int:
|
|
161
|
+
"""Inverse of alternate_code."""
|
|
162
|
+
alternate_value = 0
|
|
163
|
+
match self:
|
|
164
|
+
case TypeOfGrid.CORNER_POINT:
|
|
165
|
+
alternate_value = 0
|
|
166
|
+
case TypeOfGrid.UNSTRUCTURED:
|
|
167
|
+
alternate_value = 1
|
|
168
|
+
case TypeOfGrid.COMPOSITE:
|
|
169
|
+
alternate_value = 2
|
|
170
|
+
case TypeOfGrid.BLOCK_CENTER:
|
|
171
|
+
alternate_value = 3
|
|
172
|
+
|
|
173
|
+
case default:
|
|
174
|
+
assert_never(default)
|
|
175
|
+
return alternate_value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@unique
|
|
179
|
+
class RockModel(Enum):
|
|
180
|
+
"""Type of rock model."""
|
|
181
|
+
|
|
182
|
+
SINGLE_PERMEABILITY_POROSITY = 0
|
|
183
|
+
DUAL_POROSITY = 1
|
|
184
|
+
DUAL_PERMEABILITY = 2
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@unique
|
|
188
|
+
class GridFormat(Enum):
|
|
189
|
+
"""
|
|
190
|
+
The format of the "original grid", ie., what
|
|
191
|
+
method was used to construct the values in the file.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
UNKNOWN = 0
|
|
195
|
+
IRREGULAR_CORNER_POINT = 1
|
|
196
|
+
REGULAR_CARTESIAN = 2
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class Filehead:
|
|
201
|
+
"""
|
|
202
|
+
The first keyword in an egrid file is the FILEHEAD
|
|
203
|
+
keyword, containing metadata about the file and its
|
|
204
|
+
content.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
version_number: np.int32
|
|
208
|
+
year: np.int32
|
|
209
|
+
version_bound: np.int32
|
|
210
|
+
type_of_grid: TypeOfGrid
|
|
211
|
+
rock_model: RockModel
|
|
212
|
+
grid_format: GridFormat
|
|
213
|
+
|
|
214
|
+
def to_egrid(self) -> np.ndarray:
|
|
215
|
+
"""
|
|
216
|
+
Returns:
|
|
217
|
+
List of values, as layed out after the FILEHEAD keyword for
|
|
218
|
+
the given filehead.
|
|
219
|
+
"""
|
|
220
|
+
# The data is expected to consist of
|
|
221
|
+
# 100 integers, but only a subset is used.
|
|
222
|
+
result = np.zeros((100,), dtype=np.int32)
|
|
223
|
+
result[0] = self.version_number
|
|
224
|
+
result[1] = self.year
|
|
225
|
+
result[3] = self.version_bound
|
|
226
|
+
result[4] = self.type_of_grid.alternate_value
|
|
227
|
+
result[5] = self.rock_model.value
|
|
228
|
+
result[6] = self.grid_format.value
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class GridHead:
|
|
234
|
+
"""
|
|
235
|
+
Both for lgr (see LGRSection) and the global grid (see GlobalGrid)
|
|
236
|
+
the GRIDHEAD keyword indicates the start of the grid layout for that
|
|
237
|
+
section.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
type_of_grid: TypeOfGrid
|
|
241
|
+
num_x: np.int32
|
|
242
|
+
num_y: np.int32
|
|
243
|
+
num_z: np.int32
|
|
244
|
+
grid_reference_number: np.int32
|
|
245
|
+
numres: np.int32
|
|
246
|
+
nseg: np.int32
|
|
247
|
+
coordinate_type: CoordinateType
|
|
248
|
+
lgr_start: tuple[np.int32, np.int32, np.int32]
|
|
249
|
+
lgr_end: tuple[np.int32, np.int32, np.int32]
|
|
250
|
+
|
|
251
|
+
def to_egrid(self) -> np.ndarray:
|
|
252
|
+
# The data is expected to consist of
|
|
253
|
+
# 100 integers, but only a subset is used.
|
|
254
|
+
result = np.zeros((100,), dtype=np.int32)
|
|
255
|
+
result[0] = self.type_of_grid.value
|
|
256
|
+
result[1] = self.num_x
|
|
257
|
+
result[2] = self.num_y
|
|
258
|
+
result[3] = self.num_z
|
|
259
|
+
result[4] = self.grid_reference_number
|
|
260
|
+
result[24] = self.numres
|
|
261
|
+
result[25] = self.nseg
|
|
262
|
+
result[26] = self.coordinate_type.to_egrid()
|
|
263
|
+
result[[27, 28, 29]] = np.array(self.lgr_start)
|
|
264
|
+
result[[30, 31, 32]] = np.array(self.lgr_end)
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass
|
|
269
|
+
class GlobalGrid:
|
|
270
|
+
"""
|
|
271
|
+
The global grid contains the layout of the grid before
|
|
272
|
+
refinements, and the sectioning into grid coarsening
|
|
273
|
+
through the optional corsnum keyword.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
grid_head: GridHead
|
|
277
|
+
coord: np.ndarray
|
|
278
|
+
zcorn: np.ndarray
|
|
279
|
+
actnum: np.ndarray | None = None
|
|
280
|
+
|
|
281
|
+
def __eq__(self, other: object) -> bool:
|
|
282
|
+
if not isinstance(other, GlobalGrid):
|
|
283
|
+
return False
|
|
284
|
+
if self.actnum is None:
|
|
285
|
+
return other.actnum is None
|
|
286
|
+
if other.actnum is None:
|
|
287
|
+
return self.actnum is None
|
|
288
|
+
return (
|
|
289
|
+
self.grid_head == other.grid_head
|
|
290
|
+
and np.array_equal(self.actnum, other.actnum)
|
|
291
|
+
and np.array_equal(self.coord, other.coord)
|
|
292
|
+
and np.array_equal(self.zcorn, other.zcorn)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def to_egrid(self) -> list[tuple[str, Any]]:
|
|
296
|
+
result = [
|
|
297
|
+
("GRIDHEAD", self.grid_head.to_egrid()),
|
|
298
|
+
("COORD ", self.coord.astype(np.float32)),
|
|
299
|
+
("ZCORN ", self.zcorn.astype(np.float32)),
|
|
300
|
+
]
|
|
301
|
+
if self.actnum is not None:
|
|
302
|
+
result.append(("ACTNUM ", self.actnum.astype(np.int32)))
|
|
303
|
+
result.append(("ENDGRID ", np.array([], dtype=np.int32)))
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class EGrid:
|
|
309
|
+
"""Contains the data of an EGRID file.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
file_head:
|
|
313
|
+
The file header starting with the FILEHEAD keyword
|
|
314
|
+
global_grid:
|
|
315
|
+
The global grid
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
file_head: Filehead
|
|
319
|
+
grid_unit: GridUnit
|
|
320
|
+
global_grid: GlobalGrid
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def shape(self) -> tuple[np.int32, np.int32, np.int32]:
|
|
324
|
+
grid_head = self.global_grid.grid_head
|
|
325
|
+
return (grid_head.num_x, grid_head.num_y, grid_head.num_z)
|
|
326
|
+
|
|
327
|
+
def to_file(self, filelike: str | PathLike[str] | IO[Any]) -> None:
|
|
328
|
+
"""write the EGrid to file.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
filelike (str,Path,stream): The egrid file to write to.
|
|
332
|
+
"""
|
|
333
|
+
contents = []
|
|
334
|
+
contents.append(("FILEHEAD", self.file_head.to_egrid()))
|
|
335
|
+
contents.append(("GRIDUNIT", self.grid_unit.to_egrid())) # type: ignore
|
|
336
|
+
contents += self.global_grid.to_egrid()
|
|
337
|
+
resfo.write(filelike, contents)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
_finites = st.floats(
|
|
341
|
+
min_value=-100.0, max_value=100.0, allow_nan=False, allow_infinity=False, width=32
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
_indices = st.integers(min_value=1, max_value=4)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _zcorns(dims: tuple[int, int, int]) -> st.SearchStrategy[npt.NDArray[Any]]:
|
|
348
|
+
return arrays(
|
|
349
|
+
shape=8 * dims[0] * dims[1] * dims[2],
|
|
350
|
+
dtype=np.float32,
|
|
351
|
+
elements=_finites,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_types_of_grid = st.just(TypeOfGrid.CORNER_POINT)
|
|
356
|
+
_file_heads = st.builds(
|
|
357
|
+
Filehead,
|
|
358
|
+
st.integers(min_value=0, max_value=5),
|
|
359
|
+
st.integers(min_value=2000, max_value=2022),
|
|
360
|
+
st.integers(min_value=0, max_value=5),
|
|
361
|
+
_types_of_grid,
|
|
362
|
+
grid_format=st.just(GridFormat.IRREGULAR_CORNER_POINT),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
_grid_heads = st.builds(
|
|
366
|
+
GridHead,
|
|
367
|
+
_types_of_grid,
|
|
368
|
+
_indices,
|
|
369
|
+
_indices,
|
|
370
|
+
_indices,
|
|
371
|
+
_indices,
|
|
372
|
+
st.just(1),
|
|
373
|
+
st.just(1),
|
|
374
|
+
coordinate_type=st.just(CoordinateType.CARTESIAN),
|
|
375
|
+
lgr_start=st.tuples(_indices, _indices, _indices),
|
|
376
|
+
lgr_end=st.tuples(_indices, _indices, _indices),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@st.composite
|
|
381
|
+
def _global_grids(draw: st.DrawFn) -> GlobalGrid:
|
|
382
|
+
grid_head = draw(_grid_heads)
|
|
383
|
+
dims = (int(grid_head.num_x), int(grid_head.num_y), int(grid_head.num_z))
|
|
384
|
+
corner_size = (dims[0] + 1) * (dims[1] + 1) * 6
|
|
385
|
+
coord = arrays(
|
|
386
|
+
shape=corner_size,
|
|
387
|
+
dtype=np.float32,
|
|
388
|
+
elements=_finites,
|
|
389
|
+
)
|
|
390
|
+
actnum = st.one_of(
|
|
391
|
+
st.just(None),
|
|
392
|
+
arrays(
|
|
393
|
+
shape=dims[0] * dims[1] * dims[2],
|
|
394
|
+
dtype=np.int32,
|
|
395
|
+
elements=st.integers(min_value=0, max_value=3),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
return GlobalGrid(
|
|
399
|
+
coord=draw(coord),
|
|
400
|
+
zcorn=draw(_zcorns(dims)),
|
|
401
|
+
actnum=draw(actnum),
|
|
402
|
+
grid_head=grid_head,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
egrids = st.builds(EGrid, _file_heads, global_grid=_global_grids())
|
|
407
|
+
|
|
408
|
+
__all__ = [
|
|
409
|
+
"GrdeclKeyword",
|
|
410
|
+
"Units",
|
|
411
|
+
"GridRelative",
|
|
412
|
+
"GridUnit",
|
|
413
|
+
"CoordinateType",
|
|
414
|
+
"TypeOfGrid",
|
|
415
|
+
"RockModel",
|
|
416
|
+
"GridFormat",
|
|
417
|
+
"Filehead",
|
|
418
|
+
"GridHead",
|
|
419
|
+
"GlobalGrid",
|
|
420
|
+
"EGrid",
|
|
421
|
+
"egrids",
|
|
422
|
+
]
|