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.

@@ -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
+ ]