xtgeo 4.8.0__cp313-cp313-macosx_11_0_arm64.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 xtgeo might be problematic. Click here for more details.
- cxtgeo.py +582 -0
- cxtgeoPYTHON_wrap.c +20938 -0
- xtgeo/__init__.py +246 -0
- xtgeo/_cxtgeo.cpython-313-darwin.so +0 -0
- xtgeo/_internal.cpython-313-darwin.so +0 -0
- xtgeo/common/__init__.py +19 -0
- xtgeo/common/_angles.py +29 -0
- xtgeo/common/_xyz_enum.py +50 -0
- xtgeo/common/calc.py +396 -0
- xtgeo/common/constants.py +30 -0
- xtgeo/common/exceptions.py +42 -0
- xtgeo/common/log.py +93 -0
- xtgeo/common/sys.py +166 -0
- xtgeo/common/types.py +18 -0
- xtgeo/common/version.py +21 -0
- xtgeo/common/xtgeo_dialog.py +604 -0
- xtgeo/cube/__init__.py +9 -0
- xtgeo/cube/_cube_export.py +214 -0
- xtgeo/cube/_cube_import.py +532 -0
- xtgeo/cube/_cube_roxapi.py +180 -0
- xtgeo/cube/_cube_utils.py +287 -0
- xtgeo/cube/_cube_window_attributes.py +340 -0
- xtgeo/cube/cube1.py +1023 -0
- xtgeo/grid3d/__init__.py +15 -0
- xtgeo/grid3d/_ecl_grid.py +774 -0
- xtgeo/grid3d/_ecl_inte_head.py +148 -0
- xtgeo/grid3d/_ecl_logi_head.py +71 -0
- xtgeo/grid3d/_ecl_output_file.py +81 -0
- xtgeo/grid3d/_egrid.py +1004 -0
- xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
- xtgeo/grid3d/_grdecl_format.py +266 -0
- xtgeo/grid3d/_grdecl_grid.py +388 -0
- xtgeo/grid3d/_grid3d.py +29 -0
- xtgeo/grid3d/_grid3d_fence.py +181 -0
- xtgeo/grid3d/_grid3d_utils.py +228 -0
- xtgeo/grid3d/_grid_boundary.py +76 -0
- xtgeo/grid3d/_grid_etc1.py +1566 -0
- xtgeo/grid3d/_grid_export.py +221 -0
- xtgeo/grid3d/_grid_hybrid.py +66 -0
- xtgeo/grid3d/_grid_import.py +79 -0
- xtgeo/grid3d/_grid_import_ecl.py +101 -0
- xtgeo/grid3d/_grid_import_roff.py +135 -0
- xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
- xtgeo/grid3d/_grid_refine.py +125 -0
- xtgeo/grid3d/_grid_roxapi.py +292 -0
- xtgeo/grid3d/_grid_wellzone.py +165 -0
- xtgeo/grid3d/_gridprop_export.py +178 -0
- xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
- xtgeo/grid3d/_gridprop_import_grdecl.py +130 -0
- xtgeo/grid3d/_gridprop_import_roff.py +52 -0
- xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
- xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
- xtgeo/grid3d/_gridprop_op1.py +174 -0
- xtgeo/grid3d/_gridprop_roxapi.py +239 -0
- xtgeo/grid3d/_gridprop_value_init.py +140 -0
- xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
- xtgeo/grid3d/_gridprops_import_roff.py +83 -0
- xtgeo/grid3d/_roff_grid.py +469 -0
- xtgeo/grid3d/_roff_parameter.py +303 -0
- xtgeo/grid3d/grid.py +2537 -0
- xtgeo/grid3d/grid_properties.py +699 -0
- xtgeo/grid3d/grid_property.py +1341 -0
- xtgeo/grid3d/types.py +15 -0
- xtgeo/io/__init__.py +1 -0
- xtgeo/io/_file.py +592 -0
- xtgeo/metadata/__init__.py +17 -0
- xtgeo/metadata/metadata.py +431 -0
- xtgeo/roxutils/__init__.py +7 -0
- xtgeo/roxutils/_roxar_loader.py +54 -0
- xtgeo/roxutils/_roxutils_etc.py +122 -0
- xtgeo/roxutils/roxutils.py +207 -0
- xtgeo/surface/__init__.py +18 -0
- xtgeo/surface/_regsurf_boundary.py +26 -0
- xtgeo/surface/_regsurf_cube.py +210 -0
- xtgeo/surface/_regsurf_cube_window.py +391 -0
- xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
- xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
- xtgeo/surface/_regsurf_export.py +388 -0
- xtgeo/surface/_regsurf_grid3d.py +271 -0
- xtgeo/surface/_regsurf_gridding.py +347 -0
- xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
- xtgeo/surface/_regsurf_import.py +347 -0
- xtgeo/surface/_regsurf_lowlevel.py +122 -0
- xtgeo/surface/_regsurf_oper.py +631 -0
- xtgeo/surface/_regsurf_roxapi.py +241 -0
- xtgeo/surface/_regsurf_utils.py +81 -0
- xtgeo/surface/_surfs_import.py +43 -0
- xtgeo/surface/_zmap_parser.py +138 -0
- xtgeo/surface/regular_surface.py +2967 -0
- xtgeo/surface/surfaces.py +276 -0
- xtgeo/well/__init__.py +24 -0
- xtgeo/well/_blockedwell_roxapi.py +221 -0
- xtgeo/well/_blockedwells_roxapi.py +68 -0
- xtgeo/well/_well_aux.py +30 -0
- xtgeo/well/_well_io.py +327 -0
- xtgeo/well/_well_oper.py +574 -0
- xtgeo/well/_well_roxapi.py +304 -0
- xtgeo/well/_wellmarkers.py +486 -0
- xtgeo/well/_wells_utils.py +158 -0
- xtgeo/well/blocked_well.py +216 -0
- xtgeo/well/blocked_wells.py +122 -0
- xtgeo/well/well1.py +1514 -0
- xtgeo/well/wells.py +211 -0
- xtgeo/xyz/__init__.py +6 -0
- xtgeo/xyz/_polygons_oper.py +272 -0
- xtgeo/xyz/_xyz.py +741 -0
- xtgeo/xyz/_xyz_data.py +646 -0
- xtgeo/xyz/_xyz_io.py +490 -0
- xtgeo/xyz/_xyz_lowlevel.py +42 -0
- xtgeo/xyz/_xyz_oper.py +613 -0
- xtgeo/xyz/_xyz_roxapi.py +766 -0
- xtgeo/xyz/points.py +681 -0
- xtgeo/xyz/polygons.py +811 -0
- xtgeo-4.8.0.dist-info/METADATA +145 -0
- xtgeo-4.8.0.dist-info/RECORD +117 -0
- xtgeo-4.8.0.dist-info/WHEEL +6 -0
- xtgeo-4.8.0.dist-info/licenses/LICENSE.md +165 -0
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
"""Private module, Grid ETC 1 methods, info/modify/report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from math import atan2, degrees
|
|
8
|
+
from typing import TYPE_CHECKING, Literal, no_type_check
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from packaging.version import parse as versionparse
|
|
13
|
+
|
|
14
|
+
import xtgeo._internal as _internal # type: ignore
|
|
15
|
+
from xtgeo import _cxtgeo
|
|
16
|
+
from xtgeo.common.calc import find_flip
|
|
17
|
+
from xtgeo.common.constants import UNDEF_INT, UNDEF_LIMIT
|
|
18
|
+
from xtgeo.common.log import null_logger
|
|
19
|
+
from xtgeo.common.types import Dimensions
|
|
20
|
+
from xtgeo.grid3d.grid_properties import GridProperties
|
|
21
|
+
from xtgeo.xyz.polygons import Polygons
|
|
22
|
+
|
|
23
|
+
from . import _gridprop_lowlevel
|
|
24
|
+
from ._grid3d_fence import _update_tmpvars
|
|
25
|
+
from .grid_property import GridProperty
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from xtgeo.grid3d import Grid
|
|
29
|
+
from xtgeo.grid3d.types import METRIC
|
|
30
|
+
from xtgeo.surface.regular_surface import RegularSurface
|
|
31
|
+
from xtgeo.surface.surfaces import Surfaces
|
|
32
|
+
from xtgeo.xyz.points import Points
|
|
33
|
+
|
|
34
|
+
logger = null_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_box(
|
|
38
|
+
dimension: Dimensions,
|
|
39
|
+
origin: tuple[float, float, float],
|
|
40
|
+
oricenter: bool,
|
|
41
|
+
increment: tuple[int, int, int],
|
|
42
|
+
rotation: float,
|
|
43
|
+
flip: Literal[1, -1],
|
|
44
|
+
) -> dict[str, np.ndarray]:
|
|
45
|
+
"""Create a shoebox grid from cubi'sh spec, xtgformat=2."""
|
|
46
|
+
|
|
47
|
+
from xtgeo.cube.cube1 import Cube
|
|
48
|
+
|
|
49
|
+
ncol, nrow, nlay = dimension
|
|
50
|
+
nncol = ncol + 1
|
|
51
|
+
nnrow = nrow + 1
|
|
52
|
+
nnlay = nlay + 1
|
|
53
|
+
|
|
54
|
+
coordsv = np.zeros((nncol, nnrow, 6), dtype=np.float64)
|
|
55
|
+
zcornsv = np.zeros((nncol, nnrow, nnlay, 4), dtype=np.float32)
|
|
56
|
+
actnumsv = np.zeros((ncol, nrow, nlay), dtype=np.int32)
|
|
57
|
+
|
|
58
|
+
cube = Cube(
|
|
59
|
+
ncol=ncol,
|
|
60
|
+
nrow=nrow,
|
|
61
|
+
nlay=nlay,
|
|
62
|
+
xinc=increment[0],
|
|
63
|
+
yinc=increment[1],
|
|
64
|
+
zinc=increment[2],
|
|
65
|
+
xori=origin[0],
|
|
66
|
+
yori=origin[1],
|
|
67
|
+
zori=origin[2],
|
|
68
|
+
rotation=rotation,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
cubecpp = _internal.cube.Cube(cube)
|
|
72
|
+
logger.debug("Calling CPP internal 'create_grid_from_cube'...")
|
|
73
|
+
coordsv, zcornsv, actnumsv = _internal.grid3d.create_grid_from_cube(
|
|
74
|
+
cubecpp, oricenter, flip
|
|
75
|
+
)
|
|
76
|
+
return {
|
|
77
|
+
"coordsv": coordsv,
|
|
78
|
+
"zcornsv": zcornsv,
|
|
79
|
+
"actnumsv": actnumsv.astype(np.int32),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_grid_from_surfaces(
|
|
84
|
+
srfs: Surfaces,
|
|
85
|
+
ij_dimension: tuple[int, int] | None = None,
|
|
86
|
+
ij_origin: tuple[float, float] | None = None,
|
|
87
|
+
ij_increment: tuple[float, float] | None = None,
|
|
88
|
+
rotation: float | None = None,
|
|
89
|
+
tolerance: float = _internal.numerics.TOLERANCE,
|
|
90
|
+
) -> Grid:
|
|
91
|
+
"""Use a stack of surfaces to create a nonfaulted grid.
|
|
92
|
+
|
|
93
|
+
Technically, a shoebox grid is made first, then the layers are adjusted to follow
|
|
94
|
+
surfaces.
|
|
95
|
+
"""
|
|
96
|
+
from xtgeo.grid3d.grid import create_box_grid
|
|
97
|
+
|
|
98
|
+
n_surfaces = len(srfs.surfaces)
|
|
99
|
+
|
|
100
|
+
# ensure that surfaces are consistent
|
|
101
|
+
if not srfs.is_depth_consistent():
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"Surfaces are not depth consistent, they must not cross is depth"
|
|
104
|
+
)
|
|
105
|
+
top = srfs.surfaces[0]
|
|
106
|
+
base = srfs.surfaces[-1]
|
|
107
|
+
|
|
108
|
+
zinc = (base.values.mean() - top.values.mean()) / (n_surfaces - 1)
|
|
109
|
+
kdim: int = n_surfaces - 1
|
|
110
|
+
zori = top.values.mean()
|
|
111
|
+
ncol: int = top.ncol - 1 # since surface are nodes while grid is cell centered
|
|
112
|
+
nrow: int = top.nrow - 1
|
|
113
|
+
|
|
114
|
+
if ij_dimension: # mypy needs this:
|
|
115
|
+
dimension = Dimensions(int(ij_dimension[0]), int(ij_dimension[1]), kdim)
|
|
116
|
+
else:
|
|
117
|
+
dimension = Dimensions(ncol, nrow, kdim)
|
|
118
|
+
|
|
119
|
+
increment = (*ij_increment, zinc) if ij_increment else (top.xinc, top.yinc, zinc)
|
|
120
|
+
origin = (*ij_origin, zori) if ij_origin else (top.xori, top.yori, zori)
|
|
121
|
+
rotation = rotation if rotation is not None else top.rotation
|
|
122
|
+
|
|
123
|
+
grd = create_box_grid(
|
|
124
|
+
dimension=dimension,
|
|
125
|
+
origin=origin,
|
|
126
|
+
increment=increment,
|
|
127
|
+
rotation=rotation,
|
|
128
|
+
oricenter=False,
|
|
129
|
+
flip=1,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# now adjust the grid to surfaces
|
|
133
|
+
surf_list = []
|
|
134
|
+
for surf in srfs.surfaces:
|
|
135
|
+
cpp_surf = _internal.regsurf.RegularSurface(surf)
|
|
136
|
+
surf_list.append(cpp_surf)
|
|
137
|
+
|
|
138
|
+
grd_cpp = _internal.grid3d.Grid(grd)
|
|
139
|
+
new_zcorns, new_actnum = grd_cpp.adjust_boxgrid_layers_from_regsurfs(
|
|
140
|
+
surf_list, tolerance
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
grd._zcornsv = new_zcorns
|
|
144
|
+
grd._actnumsv = new_actnum
|
|
145
|
+
|
|
146
|
+
# set the subgrid index (zones)
|
|
147
|
+
subgrids = {f"zone_{i + 1}": 1 for i in range(n_surfaces - 1)}
|
|
148
|
+
grd.set_subgrids(subgrids)
|
|
149
|
+
|
|
150
|
+
return grd
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
method_factory = {
|
|
154
|
+
"euclid": _cxtgeo.euclid_length,
|
|
155
|
+
"horizontal": _cxtgeo.horizontal_length,
|
|
156
|
+
"east west vertical": _cxtgeo.east_west_vertical_length,
|
|
157
|
+
"north south vertical": _cxtgeo.north_south_vertical_length,
|
|
158
|
+
"x projection": _cxtgeo.x_projection,
|
|
159
|
+
"y projection": _cxtgeo.y_projection,
|
|
160
|
+
"z projection": _cxtgeo.z_projection,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_dz(
|
|
165
|
+
self: Grid,
|
|
166
|
+
name: str = "dZ",
|
|
167
|
+
flip: bool = True,
|
|
168
|
+
asmasked: bool = True,
|
|
169
|
+
metric: METRIC = "z projection",
|
|
170
|
+
) -> GridProperty:
|
|
171
|
+
"""Get average cell height (dz) as property.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
flip (bool): whether to flip the z direction, ie. increasing z is
|
|
175
|
+
increasing depth (defaults to True)
|
|
176
|
+
asmasked (bool): Whether to mask property by whether
|
|
177
|
+
name (str): Name of resulting grid property, defaults to "dZ".
|
|
178
|
+
"""
|
|
179
|
+
if metric not in method_factory:
|
|
180
|
+
raise ValueError(f"Unknown metric {metric}")
|
|
181
|
+
metric_fun = method_factory[metric]
|
|
182
|
+
|
|
183
|
+
self._xtgformat2()
|
|
184
|
+
nx, ny, nz = self.dimensions
|
|
185
|
+
result = np.zeros(nx * ny * nz)
|
|
186
|
+
_cxtgeo.grdcp3d_calc_dz(
|
|
187
|
+
self._ncol,
|
|
188
|
+
self._nrow,
|
|
189
|
+
self._nlay,
|
|
190
|
+
self._coordsv.ravel(),
|
|
191
|
+
self._zcornsv.ravel(),
|
|
192
|
+
result,
|
|
193
|
+
metric_fun,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not flip:
|
|
197
|
+
result *= -1
|
|
198
|
+
|
|
199
|
+
result = np.ma.masked_array(result, self._actnumsv == 0 if asmasked else False)
|
|
200
|
+
|
|
201
|
+
return GridProperty(
|
|
202
|
+
ncol=self._ncol,
|
|
203
|
+
nrow=self._nrow,
|
|
204
|
+
nlay=self._nlay,
|
|
205
|
+
values=result.ravel(),
|
|
206
|
+
name=name,
|
|
207
|
+
discrete=False,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@lru_cache(maxsize=1)
|
|
212
|
+
def get_dx(
|
|
213
|
+
self: Grid, name: str = "dX", asmasked: bool = False, metric: METRIC = "horizontal"
|
|
214
|
+
) -> GridProperty:
|
|
215
|
+
if metric not in method_factory:
|
|
216
|
+
raise ValueError(f"Unknown metric {metric}")
|
|
217
|
+
metric_fun = method_factory[metric]
|
|
218
|
+
|
|
219
|
+
self._xtgformat2()
|
|
220
|
+
nx, ny, nz = self.dimensions
|
|
221
|
+
result = np.zeros(nx * ny * nz)
|
|
222
|
+
_cxtgeo.grdcp3d_calc_dx(
|
|
223
|
+
self._ncol,
|
|
224
|
+
self._nrow,
|
|
225
|
+
self._nlay,
|
|
226
|
+
self._coordsv.ravel(),
|
|
227
|
+
self._zcornsv.ravel(),
|
|
228
|
+
result,
|
|
229
|
+
metric_fun,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
result = np.ma.masked_array(result, self._actnumsv == 0 if asmasked else False)
|
|
233
|
+
|
|
234
|
+
return GridProperty(
|
|
235
|
+
ncol=self._ncol,
|
|
236
|
+
nrow=self._nrow,
|
|
237
|
+
nlay=self._nlay,
|
|
238
|
+
values=result.reshape((nx, ny, nz)),
|
|
239
|
+
name=name,
|
|
240
|
+
discrete=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@lru_cache(maxsize=1)
|
|
245
|
+
def get_dy(
|
|
246
|
+
self: Grid, name: str = "dY", asmasked: bool = False, metric: METRIC = "horizontal"
|
|
247
|
+
) -> GridProperty:
|
|
248
|
+
if metric not in method_factory:
|
|
249
|
+
raise ValueError(f"Unknown metric {metric}")
|
|
250
|
+
metric_fun = method_factory[metric]
|
|
251
|
+
|
|
252
|
+
self._xtgformat2()
|
|
253
|
+
nx, ny, nz = self.dimensions
|
|
254
|
+
result = np.zeros(nx * ny * nz)
|
|
255
|
+
_cxtgeo.grdcp3d_calc_dy(
|
|
256
|
+
self._ncol,
|
|
257
|
+
self._nrow,
|
|
258
|
+
self._nlay,
|
|
259
|
+
self._coordsv.ravel(),
|
|
260
|
+
self._zcornsv.ravel(),
|
|
261
|
+
result,
|
|
262
|
+
metric_fun,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
result = np.ma.masked_array(result, self._actnumsv == 0 if asmasked else False)
|
|
266
|
+
|
|
267
|
+
return GridProperty(
|
|
268
|
+
ncol=self._ncol,
|
|
269
|
+
nrow=self._nrow,
|
|
270
|
+
nlay=self._nlay,
|
|
271
|
+
values=result.reshape((nx, ny, nz)),
|
|
272
|
+
name=name,
|
|
273
|
+
discrete=False,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_bulk_volume(
|
|
278
|
+
grid: Grid,
|
|
279
|
+
name: str = "bulkvol",
|
|
280
|
+
asmasked: bool = True,
|
|
281
|
+
precision: Literal[1, 2, 4] = 2,
|
|
282
|
+
) -> GridProperty:
|
|
283
|
+
"""Get cell bulk volume as a GridProperty() instance."""
|
|
284
|
+
if precision not in (1, 2, 4):
|
|
285
|
+
raise ValueError("The precision key has an invalid entry, use 1, 2, or 4")
|
|
286
|
+
grid._xtgformat2()
|
|
287
|
+
|
|
288
|
+
grid_cpp = _internal.grid3d.Grid(grid)
|
|
289
|
+
|
|
290
|
+
bulk_values = grid_cpp.get_cell_volumes(precision, asmasked)
|
|
291
|
+
if asmasked:
|
|
292
|
+
bulk_values = np.ma.masked_greater(bulk_values, UNDEF_LIMIT)
|
|
293
|
+
|
|
294
|
+
return GridProperty(
|
|
295
|
+
ncol=grid.ncol,
|
|
296
|
+
nrow=grid.nrow,
|
|
297
|
+
nlay=grid.nlay,
|
|
298
|
+
name=name,
|
|
299
|
+
values=bulk_values,
|
|
300
|
+
discrete=False,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_heights_above_ffl(
|
|
305
|
+
grid: Grid,
|
|
306
|
+
ffl: GridProperty,
|
|
307
|
+
option: Literal[
|
|
308
|
+
"cell_center_above_ffl", "cell_corners_above_ffl"
|
|
309
|
+
] = "cell_center_above_ffl",
|
|
310
|
+
) -> tuple[GridProperty, GridProperty, GridProperty]:
|
|
311
|
+
"""Compute delta heights for cell top, bottom and midpoints above a given level."""
|
|
312
|
+
|
|
313
|
+
valid_options = ("cell_center_above_ffl", "cell_corners_above_ffl")
|
|
314
|
+
if option not in valid_options:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"The option key <{option}> is invalid, must be one of {valid_options}"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
grid._xtgformat2()
|
|
320
|
+
|
|
321
|
+
grid_cpp = _internal.grid3d.Grid(grid)
|
|
322
|
+
htop_arr, hbot_arr, hmid_arr = grid_cpp.get_height_above_ffl(
|
|
323
|
+
ffl.values.ravel(),
|
|
324
|
+
1 if option == "cell_center_above_ffl" else 2,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
htop = GridProperty(
|
|
328
|
+
ncol=grid.ncol,
|
|
329
|
+
nrow=grid.nrow,
|
|
330
|
+
nlay=grid.nlay,
|
|
331
|
+
name="htop",
|
|
332
|
+
values=htop_arr,
|
|
333
|
+
discrete=False,
|
|
334
|
+
)
|
|
335
|
+
hbot = GridProperty(
|
|
336
|
+
ncol=grid.ncol,
|
|
337
|
+
nrow=grid.nrow,
|
|
338
|
+
nlay=grid.nlay,
|
|
339
|
+
name="hbot",
|
|
340
|
+
values=hbot_arr,
|
|
341
|
+
discrete=False,
|
|
342
|
+
)
|
|
343
|
+
hmid = GridProperty(
|
|
344
|
+
ncol=grid.ncol,
|
|
345
|
+
nrow=grid.nrow,
|
|
346
|
+
nlay=grid.nlay,
|
|
347
|
+
name="hmid",
|
|
348
|
+
values=hmid_arr,
|
|
349
|
+
discrete=False,
|
|
350
|
+
)
|
|
351
|
+
return htop, hbot, hmid
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def get_property_between_surfaces(
|
|
355
|
+
grid: Grid,
|
|
356
|
+
top: RegularSurface,
|
|
357
|
+
base: RegularSurface,
|
|
358
|
+
value: int = 1,
|
|
359
|
+
name: str = "between_surfaces",
|
|
360
|
+
) -> GridProperty:
|
|
361
|
+
"""For a grid, create a grid property with value <value> between two surfaces.
|
|
362
|
+
|
|
363
|
+
The value would be zero elsewhere, or if surfaces has inactive nodes.
|
|
364
|
+
"""
|
|
365
|
+
if not isinstance(value, int) or value < 1:
|
|
366
|
+
raise ValueError(f"Value (integer) must be positive, >= 1, got: {value}")
|
|
367
|
+
|
|
368
|
+
grid._xtgformat2()
|
|
369
|
+
logger.debug("Creating property between surfaces...")
|
|
370
|
+
|
|
371
|
+
grid_cpp = _internal.grid3d.Grid(grid)
|
|
372
|
+
|
|
373
|
+
top_ = top
|
|
374
|
+
base_ = base
|
|
375
|
+
if top.yflip == -1:
|
|
376
|
+
top_ = top.copy()
|
|
377
|
+
top_.make_lefthanded()
|
|
378
|
+
logger.debug("Top surface is right-handed, flipping a copy prior to operation")
|
|
379
|
+
if base.yflip == -1:
|
|
380
|
+
base_ = base.copy()
|
|
381
|
+
base_.make_lefthanded()
|
|
382
|
+
logger.debug("Base surface is right-handed, flipping a copy prior to operation")
|
|
383
|
+
|
|
384
|
+
diff = base_ - top_
|
|
385
|
+
if (diff.values).all() <= 0:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
"Top surface must be equal or above base surface for all nodes"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# array is always 0, 1 integer
|
|
391
|
+
array = grid_cpp.get_gridprop_value_between_surfaces(
|
|
392
|
+
_internal.regsurf.RegularSurface(top_),
|
|
393
|
+
_internal.regsurf.RegularSurface(base_),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
logger.debug("Creating property between surfaces... done")
|
|
397
|
+
|
|
398
|
+
return GridProperty(
|
|
399
|
+
ncol=grid.ncol,
|
|
400
|
+
nrow=grid.nrow,
|
|
401
|
+
nlay=grid.nlay,
|
|
402
|
+
name=name,
|
|
403
|
+
values=array * value,
|
|
404
|
+
discrete=True,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_ijk(
|
|
409
|
+
self: Grid,
|
|
410
|
+
names: tuple[str, str, str] = ("IX", "JY", "KZ"),
|
|
411
|
+
asmasked: bool = True,
|
|
412
|
+
zerobased: bool = False,
|
|
413
|
+
) -> tuple[GridProperty, GridProperty, GridProperty]:
|
|
414
|
+
"""Get I J K as properties."""
|
|
415
|
+
ashape = self.dimensions
|
|
416
|
+
|
|
417
|
+
ix, jy, kz = np.indices(ashape)
|
|
418
|
+
|
|
419
|
+
ix = ix.ravel()
|
|
420
|
+
jy = jy.ravel()
|
|
421
|
+
kz = kz.ravel()
|
|
422
|
+
|
|
423
|
+
if asmasked:
|
|
424
|
+
actnum = self.get_actnum()
|
|
425
|
+
|
|
426
|
+
ix = np.ma.masked_where(actnum.values1d == 0, ix)
|
|
427
|
+
jy = np.ma.masked_where(actnum.values1d == 0, jy)
|
|
428
|
+
kz = np.ma.masked_where(actnum.values1d == 0, kz)
|
|
429
|
+
|
|
430
|
+
if not zerobased:
|
|
431
|
+
ix += 1
|
|
432
|
+
jy += 1
|
|
433
|
+
kz += 1
|
|
434
|
+
|
|
435
|
+
ix = GridProperty(
|
|
436
|
+
ncol=self._ncol,
|
|
437
|
+
nrow=self._nrow,
|
|
438
|
+
nlay=self._nlay,
|
|
439
|
+
values=ix.reshape(ashape),
|
|
440
|
+
name=names[0],
|
|
441
|
+
discrete=True,
|
|
442
|
+
)
|
|
443
|
+
jy = GridProperty(
|
|
444
|
+
ncol=self._ncol,
|
|
445
|
+
nrow=self._nrow,
|
|
446
|
+
nlay=self._nlay,
|
|
447
|
+
values=jy.reshape(ashape),
|
|
448
|
+
name=names[1],
|
|
449
|
+
discrete=True,
|
|
450
|
+
)
|
|
451
|
+
kz = GridProperty(
|
|
452
|
+
ncol=self._ncol,
|
|
453
|
+
nrow=self._nrow,
|
|
454
|
+
nlay=self._nlay,
|
|
455
|
+
values=kz.reshape(ashape),
|
|
456
|
+
name=names[2],
|
|
457
|
+
discrete=True,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# return the objects
|
|
461
|
+
return ix, jy, kz
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_ijk_from_points(
|
|
465
|
+
self: Grid,
|
|
466
|
+
points: Points,
|
|
467
|
+
activeonly: bool = True,
|
|
468
|
+
zerobased: bool = False,
|
|
469
|
+
dataframe: bool = True,
|
|
470
|
+
includepoints: bool = True,
|
|
471
|
+
columnnames: tuple[str, str, str] = ("IX", "JY", "KZ"),
|
|
472
|
+
fmt: Literal["int", "float"] = "int",
|
|
473
|
+
undef: int = -1,
|
|
474
|
+
) -> pd.DataFrame | list:
|
|
475
|
+
"""Get I J K indices as a list of tuples or a dataframe.
|
|
476
|
+
|
|
477
|
+
It is here tried to get fast execution. This requires a preprosessing
|
|
478
|
+
of the grid to store a onlayer version, and maps with IJ positions
|
|
479
|
+
"""
|
|
480
|
+
logger.info("Getting IJK indices from Points...")
|
|
481
|
+
|
|
482
|
+
actnumoption = 1 if activeonly else 0
|
|
483
|
+
|
|
484
|
+
self._xtgformat1()
|
|
485
|
+
_update_tmpvars(self, force=True)
|
|
486
|
+
|
|
487
|
+
points_df = points.get_dataframe(copy=False)
|
|
488
|
+
arrsize = points_df[points.xname].values.size
|
|
489
|
+
|
|
490
|
+
useflip = -1 if self.ijk_handedness == "left" else 1
|
|
491
|
+
|
|
492
|
+
logger.info("Grid FLIP for C code is %s", useflip)
|
|
493
|
+
self._tmp["onegrid"]._xtgformat1() # to be sure...
|
|
494
|
+
|
|
495
|
+
logger.info("Running C routine...")
|
|
496
|
+
|
|
497
|
+
_, iarr, jarr, karr = _cxtgeo.grd3d_points_ijk_cells(
|
|
498
|
+
points_df[points.xname].values,
|
|
499
|
+
points_df[points.yname].values,
|
|
500
|
+
points_df[points.zname].values,
|
|
501
|
+
self._tmp["topd"].ncol,
|
|
502
|
+
self._tmp["topd"].nrow,
|
|
503
|
+
self._tmp["topd"].xori,
|
|
504
|
+
self._tmp["topd"].yori,
|
|
505
|
+
self._tmp["topd"].xinc,
|
|
506
|
+
self._tmp["topd"].yinc,
|
|
507
|
+
self._tmp["topd"].rotation,
|
|
508
|
+
self._tmp["topd"].yflip,
|
|
509
|
+
self._tmp["topi_carr"],
|
|
510
|
+
self._tmp["topj_carr"],
|
|
511
|
+
self._tmp["basi_carr"],
|
|
512
|
+
self._tmp["basj_carr"],
|
|
513
|
+
self.ncol,
|
|
514
|
+
self.nrow,
|
|
515
|
+
self.nlay,
|
|
516
|
+
self._coordsv.ravel(),
|
|
517
|
+
self._zcornsv.ravel(),
|
|
518
|
+
self._actnumsv.ravel(),
|
|
519
|
+
self._tmp["onegrid"]._zcornsv,
|
|
520
|
+
actnumoption,
|
|
521
|
+
arrsize,
|
|
522
|
+
arrsize,
|
|
523
|
+
arrsize,
|
|
524
|
+
)
|
|
525
|
+
logger.info("Running C routine... DONE")
|
|
526
|
+
|
|
527
|
+
if zerobased:
|
|
528
|
+
# zero based cell indexing
|
|
529
|
+
iarr -= 1
|
|
530
|
+
jarr -= 1
|
|
531
|
+
karr -= 1
|
|
532
|
+
|
|
533
|
+
proplist = {}
|
|
534
|
+
if includepoints:
|
|
535
|
+
proplist["X_UTME"] = points_df[points.xname].values
|
|
536
|
+
proplist["Y_UTME"] = points_df[points.yname].values
|
|
537
|
+
proplist["Z_TVDSS"] = points_df[points.zname].values
|
|
538
|
+
|
|
539
|
+
proplist[columnnames[0]] = iarr
|
|
540
|
+
proplist[columnnames[1]] = jarr
|
|
541
|
+
proplist[columnnames[2]] = karr
|
|
542
|
+
|
|
543
|
+
mydataframe = pd.DataFrame.from_dict(proplist)
|
|
544
|
+
mydataframe = mydataframe.replace(UNDEF_INT, -1)
|
|
545
|
+
|
|
546
|
+
if fmt == "float":
|
|
547
|
+
mydataframe[columnnames[0]] = mydataframe[columnnames[0]].astype("float")
|
|
548
|
+
mydataframe[columnnames[1]] = mydataframe[columnnames[1]].astype("float")
|
|
549
|
+
mydataframe[columnnames[2]] = mydataframe[columnnames[2]].astype("float")
|
|
550
|
+
|
|
551
|
+
if undef != -1:
|
|
552
|
+
mydataframe[columnnames[0]] = mydataframe[columnnames[0]].replace(-1, undef)
|
|
553
|
+
mydataframe[columnnames[1]] = mydataframe[columnnames[1]].replace(-1, undef)
|
|
554
|
+
mydataframe[columnnames[2]] = mydataframe[columnnames[2]].replace(-1, undef)
|
|
555
|
+
|
|
556
|
+
if dataframe:
|
|
557
|
+
return mydataframe
|
|
558
|
+
|
|
559
|
+
return list(mydataframe.itertuples(index=False, name=None))
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@lru_cache(maxsize=1)
|
|
563
|
+
def get_xyz(
|
|
564
|
+
self: Grid,
|
|
565
|
+
names: tuple[str, str, str] = ("X_UTME", "Y_UTMN", "Z_TVDSS"),
|
|
566
|
+
asmasked: bool = True,
|
|
567
|
+
) -> tuple[GridProperty, GridProperty, GridProperty]:
|
|
568
|
+
"""Get X Y Z as properties."""
|
|
569
|
+
|
|
570
|
+
self._xtgformat2()
|
|
571
|
+
|
|
572
|
+
# note: using _internal here is 2-3 times faster than using the former cxtgeo!
|
|
573
|
+
grid_cpp = _internal.grid3d.Grid(self)
|
|
574
|
+
xv, yv, zv = grid_cpp.get_cell_centers(asmasked)
|
|
575
|
+
|
|
576
|
+
xv = np.ma.masked_invalid(xv)
|
|
577
|
+
yv = np.ma.masked_invalid(yv)
|
|
578
|
+
zv = np.ma.masked_invalid(zv)
|
|
579
|
+
|
|
580
|
+
xo = GridProperty(
|
|
581
|
+
ncol=self._ncol,
|
|
582
|
+
nrow=self._nrow,
|
|
583
|
+
nlay=self._nlay,
|
|
584
|
+
values=xv,
|
|
585
|
+
name=names[0],
|
|
586
|
+
discrete=False,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
yo = GridProperty(
|
|
590
|
+
ncol=self._ncol,
|
|
591
|
+
nrow=self._nrow,
|
|
592
|
+
nlay=self._nlay,
|
|
593
|
+
values=yv,
|
|
594
|
+
name=names[1],
|
|
595
|
+
discrete=False,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
zo = GridProperty(
|
|
599
|
+
ncol=self._ncol,
|
|
600
|
+
nrow=self._nrow,
|
|
601
|
+
nlay=self._nlay,
|
|
602
|
+
values=zv,
|
|
603
|
+
name=names[2],
|
|
604
|
+
discrete=False,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# return the objects
|
|
608
|
+
return xo, yo, zo
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def get_xyz_cell_corners(
|
|
612
|
+
grid: Grid,
|
|
613
|
+
ijk: tuple[int, int, int] = (1, 1, 1),
|
|
614
|
+
activeonly: bool = True,
|
|
615
|
+
zerobased: bool = False,
|
|
616
|
+
) -> tuple[int, ...] | None:
|
|
617
|
+
"""Get X Y Z cell corners for one cell."""
|
|
618
|
+
grid._xtgformat2()
|
|
619
|
+
i, j, k = ijk
|
|
620
|
+
shift = 1 if zerobased else 0
|
|
621
|
+
|
|
622
|
+
if activeonly:
|
|
623
|
+
actnum = grid.get_actnum()
|
|
624
|
+
iact = actnum.values[i - 1 + shift, j - 1 + shift, k - 1 + shift]
|
|
625
|
+
if np.all(iact == 0):
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
corners = _internal.grid3d.Grid(grid).get_cell_corners_from_ijk(
|
|
629
|
+
i + shift - 1,
|
|
630
|
+
j + shift - 1,
|
|
631
|
+
k + shift - 1,
|
|
632
|
+
)
|
|
633
|
+
# Existing functionality relies on the grid being in xtgformat1 after this
|
|
634
|
+
# function returns. Most probably as a result of some invocation of
|
|
635
|
+
# `estimate_flip` or `ijk_handedness` or `reverse_row_axis` somewhere.
|
|
636
|
+
grid._xtgformat1()
|
|
637
|
+
|
|
638
|
+
corners = corners.to_numpy().flatten().tolist()
|
|
639
|
+
|
|
640
|
+
return tuple(corners)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def get_xyz_corners(
|
|
644
|
+
self: Grid, names: tuple[str, str, str] = ("X_UTME", "Y_UTMN", "Z_TVDSS")
|
|
645
|
+
) -> tuple[GridProperty, ...]:
|
|
646
|
+
"""Get X Y Z cell corners for all cells (as 24 GridProperty objects)."""
|
|
647
|
+
self._xtgformat1()
|
|
648
|
+
|
|
649
|
+
ntot = self.dimensions
|
|
650
|
+
|
|
651
|
+
grid_props = []
|
|
652
|
+
|
|
653
|
+
for i in range(8):
|
|
654
|
+
xname = names[0] + str(i)
|
|
655
|
+
yname = names[1] + str(i)
|
|
656
|
+
zname = names[2] + str(i)
|
|
657
|
+
x = GridProperty(
|
|
658
|
+
ncol=self._ncol,
|
|
659
|
+
nrow=self._nrow,
|
|
660
|
+
nlay=self._nlay,
|
|
661
|
+
values=np.zeros(ntot, dtype=np.float64),
|
|
662
|
+
name=xname,
|
|
663
|
+
discrete=False,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
y = GridProperty(
|
|
667
|
+
ncol=self._ncol,
|
|
668
|
+
nrow=self._nrow,
|
|
669
|
+
nlay=self._nlay,
|
|
670
|
+
values=np.zeros(ntot, dtype=np.float64),
|
|
671
|
+
name=yname,
|
|
672
|
+
discrete=False,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
z = GridProperty(
|
|
676
|
+
ncol=self._ncol,
|
|
677
|
+
nrow=self._nrow,
|
|
678
|
+
nlay=self._nlay,
|
|
679
|
+
values=np.zeros(ntot, dtype=np.float64),
|
|
680
|
+
name=zname,
|
|
681
|
+
discrete=False,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
grid_props.append(x)
|
|
685
|
+
grid_props.append(y)
|
|
686
|
+
grid_props.append(z)
|
|
687
|
+
|
|
688
|
+
ptr_coord = []
|
|
689
|
+
for i in range(24):
|
|
690
|
+
some = _cxtgeo.new_doublearray(self.ntotal)
|
|
691
|
+
ptr_coord.append(some)
|
|
692
|
+
logger.debug("SWIG object %s %s", i, some)
|
|
693
|
+
|
|
694
|
+
option = 0
|
|
695
|
+
|
|
696
|
+
# note, fool the argument list to unpack ptr_coord with * ...
|
|
697
|
+
_cxtgeo.grd3d_get_all_corners(
|
|
698
|
+
self._ncol,
|
|
699
|
+
self._nrow,
|
|
700
|
+
self._nlay,
|
|
701
|
+
self._coordsv,
|
|
702
|
+
self._zcornsv,
|
|
703
|
+
self._actnumsv,
|
|
704
|
+
*(ptr_coord + [option]),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
for i in range(0, 24, 3):
|
|
708
|
+
_gridprop_lowlevel.update_values_from_carray(
|
|
709
|
+
grid_props[i], ptr_coord[i], np.float64, delete=True
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
_gridprop_lowlevel.update_values_from_carray(
|
|
713
|
+
grid_props[i + 1], ptr_coord[i + 1], np.float64, delete=True
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
_gridprop_lowlevel.update_values_from_carray(
|
|
717
|
+
grid_props[i + 2], ptr_coord[i + 2], np.float64, delete=True
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# return the 24 objects (x1, y1, z1, ... x8, y8, z8)
|
|
721
|
+
return tuple(grid_props)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def get_vtk_esg_geometry_data(
|
|
725
|
+
self: Grid,
|
|
726
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
727
|
+
"""Get geometry data consisting of vertices and cell connectivities suitable for
|
|
728
|
+
use with VTK's vtkExplicitStructuredGrid.
|
|
729
|
+
|
|
730
|
+
Returned tuple contains:
|
|
731
|
+
- numpy array with dimensions in terms of points (not cells)
|
|
732
|
+
- vertex array, numpy array with vertex coordinates
|
|
733
|
+
- connectivity array for all the cells, numpy array with integer indices
|
|
734
|
+
- inactive cell indices, numpy array with integer indices
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
self._xtgformat2()
|
|
738
|
+
|
|
739
|
+
# Number of elements to allocate in the vertex and connectivity arrays
|
|
740
|
+
num_cells = self.ncol * self.nrow * self.nlay
|
|
741
|
+
n_vertex_arr = 3 * 8 * num_cells
|
|
742
|
+
n_conn_arr = 8 * num_cells
|
|
743
|
+
|
|
744
|
+
# Note first value in return tuple which is the actual number of vertices that
|
|
745
|
+
# was written into vertex_arr and which we'll use to shrink the array.
|
|
746
|
+
vertex_count, vertex_arr, conn_arr = _cxtgeo.grdcp3d_get_vtk_esg_geometry_data(
|
|
747
|
+
self.ncol,
|
|
748
|
+
self.nrow,
|
|
749
|
+
self.nlay,
|
|
750
|
+
self._coordsv,
|
|
751
|
+
self._zcornsv,
|
|
752
|
+
n_vertex_arr,
|
|
753
|
+
n_conn_arr,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Need to shrink the vertex array
|
|
757
|
+
vertex_arr = np.resize(vertex_arr, 3 * vertex_count)
|
|
758
|
+
vertex_arr = vertex_arr.reshape(-1, 3)
|
|
759
|
+
|
|
760
|
+
point_dims = np.asarray((self.ncol, self.nrow, self.nlay)) + 1
|
|
761
|
+
inact_indices = self.get_actnum_indices(order="F", inverse=True)
|
|
762
|
+
|
|
763
|
+
return point_dims, vertex_arr, conn_arr, inact_indices
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def get_vtk_geometries(self: Grid) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
767
|
+
"""Return actnum, corners and dims arrays for VTK ExplicitStructuredGrid usage."""
|
|
768
|
+
self._xtgformat2()
|
|
769
|
+
|
|
770
|
+
narr = 8 * self.ncol * self.nrow * self.nlay
|
|
771
|
+
xarr, yarr, zarr = _cxtgeo.grdcp3d_get_vtk_grid_arrays(
|
|
772
|
+
self.ncol,
|
|
773
|
+
self.nrow,
|
|
774
|
+
self.nlay,
|
|
775
|
+
self._coordsv,
|
|
776
|
+
self._zcornsv,
|
|
777
|
+
narr,
|
|
778
|
+
narr,
|
|
779
|
+
narr,
|
|
780
|
+
)
|
|
781
|
+
corners = np.stack((xarr, yarr, zarr))
|
|
782
|
+
corners = corners.transpose()
|
|
783
|
+
|
|
784
|
+
dims = np.asarray((self.ncol, self.nrow, self.nlay)) + 1
|
|
785
|
+
|
|
786
|
+
actindices = self.get_actnum_indices(order="F", inverse=True)
|
|
787
|
+
|
|
788
|
+
return dims, corners, actindices
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def get_cell_volume(
|
|
792
|
+
grid: Grid,
|
|
793
|
+
ijk: tuple[int, int, int] = (1, 1, 1),
|
|
794
|
+
activeonly: bool = True,
|
|
795
|
+
zerobased: bool = False,
|
|
796
|
+
precision: Literal[1, 2, 4] = 2,
|
|
797
|
+
) -> float | None:
|
|
798
|
+
"""Get bulk cell volume for one cell."""
|
|
799
|
+
if precision not in (1, 2, 4):
|
|
800
|
+
raise ValueError("The precision key has an invalid entry, use 1, 2, or 4")
|
|
801
|
+
grid._xtgformat2()
|
|
802
|
+
|
|
803
|
+
i, j, k = ijk
|
|
804
|
+
shift = 1 if zerobased else 0
|
|
805
|
+
|
|
806
|
+
if activeonly:
|
|
807
|
+
actnum = grid.get_actnum()
|
|
808
|
+
iact = actnum.values[i - 1 + shift, j - 1 + shift, k - 1 + shift]
|
|
809
|
+
if np.all(iact == 0):
|
|
810
|
+
return None
|
|
811
|
+
|
|
812
|
+
corners = _internal.grid3d.Grid(grid).get_cell_corners_from_ijk(
|
|
813
|
+
i + shift - 1,
|
|
814
|
+
j + shift - 1,
|
|
815
|
+
k + shift - 1,
|
|
816
|
+
)
|
|
817
|
+
return _internal.geometry.hexahedron_volume(corners, precision)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def get_layer_slice(
|
|
821
|
+
self: Grid, layer: int, top: bool = True, activeonly: bool = True
|
|
822
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
823
|
+
"""Get X Y cell corners (XY per cell; 5 per cell) as array."""
|
|
824
|
+
self._xtgformat1()
|
|
825
|
+
ntot = self._ncol * self._nrow * self._nlay
|
|
826
|
+
|
|
827
|
+
opt1 = 0 if top else 1
|
|
828
|
+
opt2 = 1 if activeonly else 0
|
|
829
|
+
|
|
830
|
+
icn, lay_array, ic_array = _cxtgeo.grd3d_get_lay_slice(
|
|
831
|
+
self._ncol,
|
|
832
|
+
self._nrow,
|
|
833
|
+
self._nlay,
|
|
834
|
+
self._coordsv,
|
|
835
|
+
self._zcornsv,
|
|
836
|
+
self._actnumsv,
|
|
837
|
+
layer,
|
|
838
|
+
opt1,
|
|
839
|
+
opt2,
|
|
840
|
+
10 * ntot,
|
|
841
|
+
ntot,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
lay_array = lay_array[: 10 * icn]
|
|
845
|
+
ic_array = ic_array[:icn]
|
|
846
|
+
|
|
847
|
+
lay_array = lay_array.reshape((icn, 5, 2))
|
|
848
|
+
|
|
849
|
+
return lay_array, ic_array
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def get_geometrics(
|
|
853
|
+
self: Grid,
|
|
854
|
+
allcells: bool = False,
|
|
855
|
+
cellcenter: bool = True,
|
|
856
|
+
return_dict: bool = False,
|
|
857
|
+
_ver: Literal[1, 2] = 1,
|
|
858
|
+
) -> dict | tuple:
|
|
859
|
+
"""Getting cell geometrics."""
|
|
860
|
+
self._xtgformat1()
|
|
861
|
+
|
|
862
|
+
geom_function = _get_geometrics_v1 if _ver == 1 else _get_geometrics_v2
|
|
863
|
+
return geom_function(
|
|
864
|
+
self, allcells=allcells, cellcenter=cellcenter, return_dict=return_dict
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _get_geometrics_v1(
|
|
869
|
+
self: Grid,
|
|
870
|
+
allcells: bool = False,
|
|
871
|
+
cellcenter: bool = True,
|
|
872
|
+
return_dict: bool = False,
|
|
873
|
+
) -> dict | tuple:
|
|
874
|
+
ptr_x = [_cxtgeo.new_doublepointer() for i in range(13)]
|
|
875
|
+
|
|
876
|
+
option1 = 0 if allcells else 1
|
|
877
|
+
option2 = 1 if cellcenter else 0
|
|
878
|
+
|
|
879
|
+
quality = _cxtgeo.grd3d_geometrics(
|
|
880
|
+
self._ncol,
|
|
881
|
+
self._nrow,
|
|
882
|
+
self._nlay,
|
|
883
|
+
self._coordsv,
|
|
884
|
+
self._zcornsv,
|
|
885
|
+
self._actnumsv,
|
|
886
|
+
ptr_x[0],
|
|
887
|
+
ptr_x[1],
|
|
888
|
+
ptr_x[2],
|
|
889
|
+
ptr_x[3],
|
|
890
|
+
ptr_x[4],
|
|
891
|
+
ptr_x[5],
|
|
892
|
+
ptr_x[6],
|
|
893
|
+
ptr_x[7],
|
|
894
|
+
ptr_x[8],
|
|
895
|
+
ptr_x[9],
|
|
896
|
+
ptr_x[10],
|
|
897
|
+
ptr_x[11],
|
|
898
|
+
ptr_x[12],
|
|
899
|
+
option1,
|
|
900
|
+
option2,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
glist = [_cxtgeo.doublepointer_value(item) for item in ptr_x]
|
|
904
|
+
glist.append(quality)
|
|
905
|
+
|
|
906
|
+
logger.info("Cell geometrics done")
|
|
907
|
+
|
|
908
|
+
if not return_dict:
|
|
909
|
+
return tuple(glist)
|
|
910
|
+
|
|
911
|
+
gkeys = [
|
|
912
|
+
"xori",
|
|
913
|
+
"yori",
|
|
914
|
+
"zori",
|
|
915
|
+
"xmin",
|
|
916
|
+
"xmax",
|
|
917
|
+
"ymin",
|
|
918
|
+
"ymax",
|
|
919
|
+
"zmin",
|
|
920
|
+
"zmax",
|
|
921
|
+
"avg_rotation",
|
|
922
|
+
"avg_dx",
|
|
923
|
+
"avg_dy",
|
|
924
|
+
"avg_dz",
|
|
925
|
+
"grid_regularity_flag",
|
|
926
|
+
]
|
|
927
|
+
return dict(zip(gkeys, glist))
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _get_geometrics_v2(
|
|
931
|
+
self: Grid,
|
|
932
|
+
allcells: bool = False,
|
|
933
|
+
cellcenter: bool = True,
|
|
934
|
+
return_dict: bool = False,
|
|
935
|
+
) -> dict | tuple:
|
|
936
|
+
# Currently a workaround as there seems to be bugs in v1
|
|
937
|
+
# Will only work with allcells False and cellcenter True
|
|
938
|
+
|
|
939
|
+
glist = []
|
|
940
|
+
if cellcenter and allcells:
|
|
941
|
+
xcor, ycor, zcor = self.get_xyz(asmasked=False)
|
|
942
|
+
glist.append(xcor.values[0, 0, 0])
|
|
943
|
+
glist.append(ycor.values[0, 0, 0])
|
|
944
|
+
glist.append(zcor.values[0, 0, 0])
|
|
945
|
+
glist.append(xcor.values.min())
|
|
946
|
+
glist.append(xcor.values.max())
|
|
947
|
+
glist.append(ycor.values.min())
|
|
948
|
+
glist.append(ycor.values.max())
|
|
949
|
+
glist.append(zcor.values.min())
|
|
950
|
+
glist.append(zcor.values.max())
|
|
951
|
+
|
|
952
|
+
# rotation (approx) for mid column
|
|
953
|
+
midcol = int(self.nrow / 2)
|
|
954
|
+
midlay = int(self.nlay / 2)
|
|
955
|
+
x0 = xcor.values[0, midcol, midlay]
|
|
956
|
+
y0 = ycor.values[0, midcol, midlay]
|
|
957
|
+
x1 = xcor.values[self.ncol - 1, midcol, midlay]
|
|
958
|
+
y1 = ycor.values[self.ncol - 1, midcol, midlay]
|
|
959
|
+
glist.append(degrees(atan2(y1 - y0, x1 - x0)))
|
|
960
|
+
|
|
961
|
+
dx = self.get_dx(asmasked=False)
|
|
962
|
+
dy = self.get_dy(asmasked=False)
|
|
963
|
+
dz = self.get_dz(asmasked=False)
|
|
964
|
+
glist.append(dx.values.mean())
|
|
965
|
+
glist.append(dy.values.mean())
|
|
966
|
+
glist.append(dz.values.mean())
|
|
967
|
+
glist.append(1)
|
|
968
|
+
|
|
969
|
+
if not return_dict:
|
|
970
|
+
return tuple(glist)
|
|
971
|
+
|
|
972
|
+
gkeys = [
|
|
973
|
+
"xori",
|
|
974
|
+
"yori",
|
|
975
|
+
"zori",
|
|
976
|
+
"xmin",
|
|
977
|
+
"xmax",
|
|
978
|
+
"ymin",
|
|
979
|
+
"ymax",
|
|
980
|
+
"zmin",
|
|
981
|
+
"zmax",
|
|
982
|
+
"avg_rotation",
|
|
983
|
+
"avg_dx",
|
|
984
|
+
"avg_dy",
|
|
985
|
+
"avg_dz",
|
|
986
|
+
"grid_regularity_flag",
|
|
987
|
+
]
|
|
988
|
+
return dict(zip(gkeys, glist))
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def inactivate_by_dz(self: Grid, threshold: float, flip: bool = True) -> None:
|
|
992
|
+
"""Set cell to inactive if dz does not exceed threshold.
|
|
993
|
+
Args:
|
|
994
|
+
threshold (float): The threshold for which the absolute value
|
|
995
|
+
of dz should exceed.
|
|
996
|
+
flip (bool): Whether the z-direction should be flipped.
|
|
997
|
+
|
|
998
|
+
"""
|
|
999
|
+
self._xtgformat2()
|
|
1000
|
+
dz_values = self.get_dz(asmasked=False, flip=flip).values
|
|
1001
|
+
self._actnumsv[dz_values.reshape(self._actnumsv.shape) < threshold] = 0
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def make_zconsistent(self: Grid, zsep: float | int) -> None:
|
|
1005
|
+
"""Make consistent in z."""
|
|
1006
|
+
self._xtgformat1()
|
|
1007
|
+
|
|
1008
|
+
if isinstance(zsep, int):
|
|
1009
|
+
zsep = float(zsep)
|
|
1010
|
+
|
|
1011
|
+
if not isinstance(zsep, float):
|
|
1012
|
+
raise ValueError('The "zsep" is not a float or int')
|
|
1013
|
+
|
|
1014
|
+
_cxtgeo.grd3d_make_z_consistent(
|
|
1015
|
+
self.ncol,
|
|
1016
|
+
self.nrow,
|
|
1017
|
+
self.nlay,
|
|
1018
|
+
self._zcornsv,
|
|
1019
|
+
zsep,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def inactivate_inside(
|
|
1024
|
+
self: Grid,
|
|
1025
|
+
poly: Polygons,
|
|
1026
|
+
layer_range: tuple[int, int] | None = None,
|
|
1027
|
+
inside: bool = True,
|
|
1028
|
+
force_close: bool = False,
|
|
1029
|
+
) -> None:
|
|
1030
|
+
"""Inactivate inside a polygon (or outside)."""
|
|
1031
|
+
self._xtgformat1()
|
|
1032
|
+
|
|
1033
|
+
if not isinstance(poly, Polygons):
|
|
1034
|
+
raise ValueError("Input polygon not a XTGeo Polygons instance")
|
|
1035
|
+
|
|
1036
|
+
if layer_range is not None:
|
|
1037
|
+
k1, k2 = layer_range
|
|
1038
|
+
else:
|
|
1039
|
+
k1, k2 = 1, self.nlay
|
|
1040
|
+
|
|
1041
|
+
method = 0 if inside else 1
|
|
1042
|
+
iforce = 0 if not force_close else 1
|
|
1043
|
+
|
|
1044
|
+
# get dataframe where each polygon is ended by a 999 value
|
|
1045
|
+
dfxyz = poly.get_xyz_dataframe()
|
|
1046
|
+
|
|
1047
|
+
xc = dfxyz["X_UTME"].values.copy()
|
|
1048
|
+
yc = dfxyz["Y_UTMN"].values.copy()
|
|
1049
|
+
|
|
1050
|
+
ier = _cxtgeo.grd3d_inact_outside_pol(
|
|
1051
|
+
xc,
|
|
1052
|
+
yc,
|
|
1053
|
+
self.ncol,
|
|
1054
|
+
self.nrow,
|
|
1055
|
+
self.nlay,
|
|
1056
|
+
self._coordsv,
|
|
1057
|
+
self._zcornsv,
|
|
1058
|
+
self._actnumsv, # is modified!
|
|
1059
|
+
k1,
|
|
1060
|
+
k2,
|
|
1061
|
+
iforce,
|
|
1062
|
+
method,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
if ier == 1:
|
|
1066
|
+
raise RuntimeError("Problems with one or more polygons. Not closed?")
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def collapse_inactive_cells(self: Grid) -> None:
|
|
1070
|
+
"""Collapse inactive cells."""
|
|
1071
|
+
self._xtgformat1()
|
|
1072
|
+
|
|
1073
|
+
_cxtgeo.grd3d_collapse_inact(
|
|
1074
|
+
self.ncol, self.nrow, self.nlay, self._zcornsv, self._actnumsv
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def copy(self: Grid) -> Grid:
|
|
1079
|
+
"""Copy a grid instance (C pointers) and other props.
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
A new instance (attached grid properties will also be unique)
|
|
1083
|
+
"""
|
|
1084
|
+
self._xtgformat2()
|
|
1085
|
+
|
|
1086
|
+
copy_tag = " (copy)"
|
|
1087
|
+
|
|
1088
|
+
filesrc = str(self._filesrc)
|
|
1089
|
+
if filesrc is not None and copy_tag not in filesrc:
|
|
1090
|
+
filesrc += copy_tag
|
|
1091
|
+
|
|
1092
|
+
return self.__class__(
|
|
1093
|
+
coordsv=self._coordsv.copy(),
|
|
1094
|
+
zcornsv=self._zcornsv.copy(),
|
|
1095
|
+
actnumsv=self._actnumsv.copy(),
|
|
1096
|
+
subgrids=deepcopy(self.subgrids),
|
|
1097
|
+
dualporo=self.dualporo,
|
|
1098
|
+
dualperm=self.dualperm,
|
|
1099
|
+
name=self.name + copy_tag if self.name else None,
|
|
1100
|
+
roxgrid=self.roxgrid,
|
|
1101
|
+
roxindexer=self.roxindexer,
|
|
1102
|
+
props=self._props.copy() if self._props else None,
|
|
1103
|
+
filesrc=filesrc,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
@no_type_check # due to some hard-to-solve issues with mypy
|
|
1108
|
+
def crop(
|
|
1109
|
+
self: Grid,
|
|
1110
|
+
spec: tuple[tuple[int, int], tuple[int, int], tuple[int, int]],
|
|
1111
|
+
props: Literal["all"] | list[GridProperty] | None = None,
|
|
1112
|
+
) -> None:
|
|
1113
|
+
"""Do cropping of geometry (and properties).
|
|
1114
|
+
|
|
1115
|
+
If props is 'all' then all properties assosiated (linked) to then
|
|
1116
|
+
grid are also cropped, and the instances are updated.
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
spec (tuple): A nested tuple on the form ((i1, i2), (j1, j2), (k1, k2))
|
|
1120
|
+
where 1 represents start number, and 2 reperesent end. The range
|
|
1121
|
+
is inclusive for both ends, and the number start index is 1 based.
|
|
1122
|
+
props (list or str): None is default, while properties can be listed.
|
|
1123
|
+
If 'all', then all GridProperty objects which are linked to the
|
|
1124
|
+
Grid instance are updated.
|
|
1125
|
+
|
|
1126
|
+
Returns:
|
|
1127
|
+
The instance is updated (cropped)
|
|
1128
|
+
"""
|
|
1129
|
+
self._xtgformat1()
|
|
1130
|
+
|
|
1131
|
+
(ic1, ic2), (jc1, jc2), (kc1, kc2) = spec
|
|
1132
|
+
|
|
1133
|
+
if (
|
|
1134
|
+
ic1 < 1
|
|
1135
|
+
or ic2 > self.ncol
|
|
1136
|
+
or jc1 < 1
|
|
1137
|
+
or jc2 > self.nrow
|
|
1138
|
+
or kc1 < 1
|
|
1139
|
+
or kc2 > self.nlay
|
|
1140
|
+
):
|
|
1141
|
+
raise ValueError("Boundary for tuples not matching grid NCOL, NROW, NLAY")
|
|
1142
|
+
|
|
1143
|
+
oldnlay = self._nlay
|
|
1144
|
+
|
|
1145
|
+
# compute size of new cropped grid
|
|
1146
|
+
nncol = ic2 - ic1 + 1
|
|
1147
|
+
nnrow = jc2 - jc1 + 1
|
|
1148
|
+
nnlay = kc2 - kc1 + 1
|
|
1149
|
+
|
|
1150
|
+
ntot = nncol * nnrow * nnlay
|
|
1151
|
+
ncoord = (nncol + 1) * (nnrow + 1) * 2 * 3
|
|
1152
|
+
nzcorn = nncol * nnrow * (nnlay + 1) * 4
|
|
1153
|
+
|
|
1154
|
+
new_num_act = _cxtgeo.new_intpointer()
|
|
1155
|
+
new_coordsv = np.zeros(ncoord, dtype=np.float64)
|
|
1156
|
+
new_zcornsv = np.zeros(nzcorn, dtype=np.float64)
|
|
1157
|
+
new_actnumsv = np.zeros(ntot, dtype=np.int32)
|
|
1158
|
+
|
|
1159
|
+
_cxtgeo.grd3d_crop_geometry(
|
|
1160
|
+
self.ncol,
|
|
1161
|
+
self.nrow,
|
|
1162
|
+
self.nlay,
|
|
1163
|
+
self._coordsv,
|
|
1164
|
+
self._zcornsv,
|
|
1165
|
+
self._actnumsv,
|
|
1166
|
+
new_coordsv,
|
|
1167
|
+
new_zcornsv,
|
|
1168
|
+
new_actnumsv,
|
|
1169
|
+
ic1,
|
|
1170
|
+
ic2,
|
|
1171
|
+
jc1,
|
|
1172
|
+
jc2,
|
|
1173
|
+
kc1,
|
|
1174
|
+
kc2,
|
|
1175
|
+
new_num_act,
|
|
1176
|
+
0,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
self._coordsv = new_coordsv
|
|
1180
|
+
self._zcornsv = new_zcornsv
|
|
1181
|
+
self._actnumsv = new_actnumsv
|
|
1182
|
+
|
|
1183
|
+
self._ncol = nncol
|
|
1184
|
+
self._nrow = nnrow
|
|
1185
|
+
self._nlay = nnlay
|
|
1186
|
+
|
|
1187
|
+
if isinstance(self.subgrids, dict):
|
|
1188
|
+
newsub = {}
|
|
1189
|
+
# easier to work with numpies than lists
|
|
1190
|
+
newarr = np.array(range(1, oldnlay + 1))
|
|
1191
|
+
newarr[newarr < kc1] = 0
|
|
1192
|
+
newarr[newarr > kc2] = 0
|
|
1193
|
+
newaxx = newarr.copy() - kc1 + 1
|
|
1194
|
+
for sub, arr in self.subgrids.items():
|
|
1195
|
+
arrx = np.array(arr)
|
|
1196
|
+
arrxmap = newaxx[arrx[0] - 1 : arrx[-1]]
|
|
1197
|
+
arrxmap = arrxmap[arrxmap > 0]
|
|
1198
|
+
if arrxmap.size > 0:
|
|
1199
|
+
newsub[sub] = arrxmap.astype(np.int32).tolist()
|
|
1200
|
+
|
|
1201
|
+
self.subgrids = newsub
|
|
1202
|
+
|
|
1203
|
+
# crop properties
|
|
1204
|
+
props = self.props if props == "all" else props
|
|
1205
|
+
if props is not None:
|
|
1206
|
+
for prop in props:
|
|
1207
|
+
logger.info("Crop %s", prop.name)
|
|
1208
|
+
prop.crop(spec)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def reduce_to_one_layer(self: Grid) -> None:
|
|
1212
|
+
"""Reduce the grid to one single layer.
|
|
1213
|
+
|
|
1214
|
+
This can be useful for algorithms that need to test if a point is within
|
|
1215
|
+
the full grid.
|
|
1216
|
+
|
|
1217
|
+
Example::
|
|
1218
|
+
|
|
1219
|
+
>>> import xtgeo
|
|
1220
|
+
>>> grid = xtgeo.grid_from_file(reek_dir + "/REEK.EGRID")
|
|
1221
|
+
>>> grid.nlay
|
|
1222
|
+
14
|
|
1223
|
+
>>> grid.reduce_to_one_layer()
|
|
1224
|
+
>>> grid.nlay
|
|
1225
|
+
1
|
|
1226
|
+
|
|
1227
|
+
"""
|
|
1228
|
+
# need new pointers in C (not for coord)
|
|
1229
|
+
# Note this could probably be done with pure numpy operations
|
|
1230
|
+
self._xtgformat1()
|
|
1231
|
+
|
|
1232
|
+
ptr_new_num_act = _cxtgeo.new_intpointer()
|
|
1233
|
+
|
|
1234
|
+
nnum = (1 + 1) * 4
|
|
1235
|
+
|
|
1236
|
+
new_zcorn = np.zeros(self.ncol * self.nrow * nnum, dtype=np.float64)
|
|
1237
|
+
new_actnum = np.zeros(self.ncol * self.nrow * 1, dtype=np.int32)
|
|
1238
|
+
|
|
1239
|
+
_cxtgeo.grd3d_reduce_onelayer(
|
|
1240
|
+
self.ncol,
|
|
1241
|
+
self.nrow,
|
|
1242
|
+
self.nlay,
|
|
1243
|
+
self._zcornsv,
|
|
1244
|
+
new_zcorn,
|
|
1245
|
+
self._actnumsv,
|
|
1246
|
+
new_actnum,
|
|
1247
|
+
ptr_new_num_act,
|
|
1248
|
+
0,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
self._nlay = 1
|
|
1252
|
+
self._zcornsv = new_zcorn
|
|
1253
|
+
self._actnumsv = new_actnum
|
|
1254
|
+
self._props = None
|
|
1255
|
+
self._subgrids = None
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def translate_coordinates(
|
|
1259
|
+
self: Grid,
|
|
1260
|
+
translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
1261
|
+
flip: tuple[int, int, int] = (1, 1, 1),
|
|
1262
|
+
) -> None:
|
|
1263
|
+
"""Translate grid coordinates."""
|
|
1264
|
+
self._xtgformat1()
|
|
1265
|
+
|
|
1266
|
+
tx, ty, tz = translate
|
|
1267
|
+
fx, fy, fz = flip
|
|
1268
|
+
|
|
1269
|
+
ier = _cxtgeo.grd3d_translate(
|
|
1270
|
+
self._ncol,
|
|
1271
|
+
self._nrow,
|
|
1272
|
+
self._nlay,
|
|
1273
|
+
fx,
|
|
1274
|
+
fy,
|
|
1275
|
+
fz,
|
|
1276
|
+
tx,
|
|
1277
|
+
ty,
|
|
1278
|
+
tz,
|
|
1279
|
+
self._coordsv,
|
|
1280
|
+
self._zcornsv,
|
|
1281
|
+
)
|
|
1282
|
+
if ier != 0:
|
|
1283
|
+
raise RuntimeError(f"Something went wrong in translate, code: {ier}")
|
|
1284
|
+
|
|
1285
|
+
logger.info("Translation of coords done")
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def reverse_row_axis(
|
|
1289
|
+
self: Grid, ijk_handedness: Literal["left", "right"] | None = None
|
|
1290
|
+
) -> None:
|
|
1291
|
+
"""Reverse rows (aka flip) for geometry and assosiated properties."""
|
|
1292
|
+
if ijk_handedness == self.ijk_handedness:
|
|
1293
|
+
return
|
|
1294
|
+
|
|
1295
|
+
self._xtgformat1()
|
|
1296
|
+
|
|
1297
|
+
ier = _cxtgeo.grd3d_reverse_jrows(
|
|
1298
|
+
self._ncol,
|
|
1299
|
+
self._nrow,
|
|
1300
|
+
self._nlay,
|
|
1301
|
+
self._coordsv.ravel(),
|
|
1302
|
+
self._zcornsv.ravel(),
|
|
1303
|
+
self._actnumsv.ravel(),
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
if ier != 0:
|
|
1307
|
+
raise RuntimeError(f"Something went wrong in jswapping, code: {ier}")
|
|
1308
|
+
|
|
1309
|
+
if self._props is None:
|
|
1310
|
+
return
|
|
1311
|
+
|
|
1312
|
+
# do it for properties
|
|
1313
|
+
if self._props.props:
|
|
1314
|
+
for prp in self._props.props:
|
|
1315
|
+
prp.values = prp.values[:, ::-1, :]
|
|
1316
|
+
|
|
1317
|
+
logger.info("Reversing of rows done")
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def get_adjacent_cells(
|
|
1321
|
+
self: Grid,
|
|
1322
|
+
prop: GridProperty,
|
|
1323
|
+
val1: int,
|
|
1324
|
+
val2: int,
|
|
1325
|
+
activeonly: bool = True,
|
|
1326
|
+
) -> GridProperty:
|
|
1327
|
+
"""Get adjacents cells."""
|
|
1328
|
+
self._xtgformat1()
|
|
1329
|
+
|
|
1330
|
+
if not isinstance(prop, GridProperty):
|
|
1331
|
+
raise ValueError("The argument prop is not a xtgeo.GridPropery")
|
|
1332
|
+
|
|
1333
|
+
if prop.isdiscrete is False:
|
|
1334
|
+
raise ValueError("The argument prop is not a discrete property")
|
|
1335
|
+
|
|
1336
|
+
result = GridProperty(
|
|
1337
|
+
ncol=self._ncol,
|
|
1338
|
+
nrow=self._nrow,
|
|
1339
|
+
nlay=self._nlay,
|
|
1340
|
+
values=np.zeros(self.ntotal, dtype=np.int32),
|
|
1341
|
+
name="ADJ_CELLS",
|
|
1342
|
+
discrete=True,
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
p_prop1 = _gridprop_lowlevel.update_carray(prop)
|
|
1346
|
+
p_prop2 = _cxtgeo.new_intarray(self.ntotal)
|
|
1347
|
+
|
|
1348
|
+
iflag1 = 0 if activeonly else 1
|
|
1349
|
+
iflag2 = 1
|
|
1350
|
+
|
|
1351
|
+
_cxtgeo.grd3d_adj_cells(
|
|
1352
|
+
self._ncol,
|
|
1353
|
+
self._nrow,
|
|
1354
|
+
self._nlay,
|
|
1355
|
+
self._coordsv,
|
|
1356
|
+
self._zcornsv,
|
|
1357
|
+
self._actnumsv,
|
|
1358
|
+
p_prop1,
|
|
1359
|
+
self.ntotal,
|
|
1360
|
+
val1,
|
|
1361
|
+
val2,
|
|
1362
|
+
p_prop2,
|
|
1363
|
+
self.ntotal,
|
|
1364
|
+
iflag1,
|
|
1365
|
+
iflag2,
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
_gridprop_lowlevel.update_values_from_carray(result, p_prop2, np.int32, delete=True)
|
|
1369
|
+
# return the property object
|
|
1370
|
+
return result
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
def estimate_design(
|
|
1374
|
+
self: Grid,
|
|
1375
|
+
nsubname: str | None = None,
|
|
1376
|
+
) -> dict[str, str | float]:
|
|
1377
|
+
"""Estimate (guess) (sub)grid design by examing DZ in median thickness column."""
|
|
1378
|
+
actv = self.get_actnum().values
|
|
1379
|
+
|
|
1380
|
+
dzv = self.get_dz(asmasked=False).values
|
|
1381
|
+
|
|
1382
|
+
# treat inactive thicknesses as zero
|
|
1383
|
+
dzv[actv == 0] = 0.0
|
|
1384
|
+
|
|
1385
|
+
if nsubname is None:
|
|
1386
|
+
vrange = np.array(range(self.nlay))
|
|
1387
|
+
else:
|
|
1388
|
+
assert self.subgrids is not None
|
|
1389
|
+
vrange = np.array(list(self.subgrids[nsubname])) - 1
|
|
1390
|
+
|
|
1391
|
+
# find the dz for the actual subzone
|
|
1392
|
+
dzv = dzv[:, :, vrange]
|
|
1393
|
+
|
|
1394
|
+
# find cumulative thickness as a 2D array
|
|
1395
|
+
dzcum: np.ndarray = np.sum(dzv, axis=2, keepdims=False)
|
|
1396
|
+
|
|
1397
|
+
# find the average thickness for nonzero thicknesses
|
|
1398
|
+
dzcum2 = dzcum.copy()
|
|
1399
|
+
dzcum2[dzcum == 0.0] = np.nan
|
|
1400
|
+
dzavg = np.nanmean(dzcum2) / dzv.shape[2]
|
|
1401
|
+
|
|
1402
|
+
# find the I J indices for the median value
|
|
1403
|
+
if versionparse(np.__version__) < versionparse("1.22"):
|
|
1404
|
+
median_value = np.percentile(dzcum, 50, interpolation="nearest") # type: ignore
|
|
1405
|
+
else:
|
|
1406
|
+
median_value = np.percentile(dzcum, 50, method="nearest")
|
|
1407
|
+
|
|
1408
|
+
argmed = np.stack(np.nonzero(dzcum == median_value), axis=1)
|
|
1409
|
+
|
|
1410
|
+
im, jm = argmed[0]
|
|
1411
|
+
# find the dz stack of the median
|
|
1412
|
+
dzmedian = dzv[im, jm, :]
|
|
1413
|
+
logger.info("DZ median column is %s", dzmedian)
|
|
1414
|
+
|
|
1415
|
+
# to compare thicknesses with (divide on 2 to assure)
|
|
1416
|
+
target = dzcum[im, jm] / (dzmedian.shape[0] * 2)
|
|
1417
|
+
eps = target / 100.0
|
|
1418
|
+
|
|
1419
|
+
logger.info("Target and EPS values are %s, %s", target, eps)
|
|
1420
|
+
|
|
1421
|
+
status = "X" # unknown or cannot determine
|
|
1422
|
+
|
|
1423
|
+
if dzmedian[0] > target and dzmedian[-1] <= eps:
|
|
1424
|
+
status = "T"
|
|
1425
|
+
dzavg = dzmedian[0]
|
|
1426
|
+
elif dzmedian[0] < eps and dzmedian[-1] > target:
|
|
1427
|
+
status = "B"
|
|
1428
|
+
dzavg = dzmedian[-1]
|
|
1429
|
+
elif dzmedian[0] > target and dzmedian[-1] > target:
|
|
1430
|
+
ratio = dzmedian[0] / dzmedian[-1]
|
|
1431
|
+
if 0.5 < ratio < 1.5:
|
|
1432
|
+
status = "P"
|
|
1433
|
+
elif dzmedian[0] < eps and dzmedian[-1] < eps:
|
|
1434
|
+
status = "M"
|
|
1435
|
+
middleindex = int(dzmedian.shape[0] / 2)
|
|
1436
|
+
dzavg = dzmedian[middleindex]
|
|
1437
|
+
|
|
1438
|
+
return {"design": status, "dzsimbox": dzavg}
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def estimate_flip(self: Grid) -> Literal[-1, 1]:
|
|
1442
|
+
"""Estimate if grid is left or right handed."""
|
|
1443
|
+
corners = self.get_xyz_cell_corners(activeonly=False) # for cell 1, 1, 1
|
|
1444
|
+
|
|
1445
|
+
v1 = (corners[3] - corners[0], corners[4] - corners[1], 0.0)
|
|
1446
|
+
v2 = (corners[6] - corners[0], corners[7] - corners[1], 0.0)
|
|
1447
|
+
|
|
1448
|
+
return find_flip(v1, v2)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _convert_xtgformat2to1(self: Grid) -> None:
|
|
1452
|
+
"""Convert arrays from new structure xtgformat=2 to legacy xtgformat=1."""
|
|
1453
|
+
if self._xtgformat == 1:
|
|
1454
|
+
logger.info("No conversion, format is already xtgformat == 1 or unset")
|
|
1455
|
+
return
|
|
1456
|
+
|
|
1457
|
+
logger.info("Convert grid from new xtgformat to legacy format...")
|
|
1458
|
+
|
|
1459
|
+
newcoordsv = np.zeros(((self._ncol + 1) * (self._nrow + 1) * 6), dtype=np.float64)
|
|
1460
|
+
newzcornsv = np.zeros(
|
|
1461
|
+
(self._ncol * self._nrow * (self._nlay + 1) * 4), dtype=np.float64
|
|
1462
|
+
)
|
|
1463
|
+
newactnumsv = np.zeros((self._ncol * self._nrow * self._nlay), dtype=np.int32)
|
|
1464
|
+
|
|
1465
|
+
_cxtgeo.grd3cp3d_xtgformat2to1_geom(
|
|
1466
|
+
self._ncol,
|
|
1467
|
+
self._nrow,
|
|
1468
|
+
self._nlay,
|
|
1469
|
+
newcoordsv,
|
|
1470
|
+
self._coordsv,
|
|
1471
|
+
newzcornsv,
|
|
1472
|
+
self._zcornsv,
|
|
1473
|
+
newactnumsv,
|
|
1474
|
+
self._actnumsv,
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
self._coordsv = newcoordsv
|
|
1478
|
+
self._zcornsv = newzcornsv
|
|
1479
|
+
self._actnumsv = newactnumsv
|
|
1480
|
+
self._xtgformat = 1
|
|
1481
|
+
|
|
1482
|
+
logger.info("Convert grid from new xtgformat to legacy format... done")
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def _convert_xtgformat1to2(self: Grid) -> None:
|
|
1486
|
+
"""Convert arrays from old structure xtgformat=1 to new xtgformat=2."""
|
|
1487
|
+
if self._xtgformat == 2 or self._coordsv is None:
|
|
1488
|
+
logger.info("No conversion, format is already xtgformat == 2 or unset")
|
|
1489
|
+
return
|
|
1490
|
+
|
|
1491
|
+
logger.info("Convert grid from legacy xtgformat to new format...")
|
|
1492
|
+
|
|
1493
|
+
newcoordsv = np.zeros((self._ncol + 1, self._nrow + 1, 6), dtype=np.float64)
|
|
1494
|
+
newzcornsv = np.zeros(
|
|
1495
|
+
(self._ncol + 1, self._nrow + 1, self._nlay + 1, 4), dtype=np.float32
|
|
1496
|
+
)
|
|
1497
|
+
newactnumsv = np.zeros((self._ncol, self._nrow, self._nlay), dtype=np.int32)
|
|
1498
|
+
|
|
1499
|
+
_cxtgeo.grd3cp3d_xtgformat1to2_geom(
|
|
1500
|
+
self._ncol,
|
|
1501
|
+
self._nrow,
|
|
1502
|
+
self._nlay,
|
|
1503
|
+
self._coordsv,
|
|
1504
|
+
newcoordsv,
|
|
1505
|
+
self._zcornsv,
|
|
1506
|
+
newzcornsv,
|
|
1507
|
+
self._actnumsv,
|
|
1508
|
+
newactnumsv,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
self._coordsv = newcoordsv
|
|
1512
|
+
self._zcornsv = newzcornsv
|
|
1513
|
+
self._actnumsv = newactnumsv
|
|
1514
|
+
self._xtgformat = 2
|
|
1515
|
+
|
|
1516
|
+
logger.info("Convert grid from legacy xtgformat to new format... done")
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
def get_gridquality_properties(self: Grid) -> GridProperties:
|
|
1520
|
+
"""Get the grid quality properties."""
|
|
1521
|
+
self._xtgformat2()
|
|
1522
|
+
|
|
1523
|
+
qcnames = {
|
|
1524
|
+
0: "minangle_topbase",
|
|
1525
|
+
1: "maxangle_topbase",
|
|
1526
|
+
2: "minangle_topbase_proj",
|
|
1527
|
+
3: "maxangle_topbase_proj",
|
|
1528
|
+
4: "minangle_sides",
|
|
1529
|
+
5: "maxangle_sides",
|
|
1530
|
+
6: "collapsed",
|
|
1531
|
+
7: "faulted",
|
|
1532
|
+
8: "negative_thickness",
|
|
1533
|
+
9: "concave_proj",
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
# some of the properties shall be discrete:
|
|
1537
|
+
qcdiscrete = [6, 7, 8, 9]
|
|
1538
|
+
|
|
1539
|
+
fresults = np.ones(
|
|
1540
|
+
(len(qcnames), self.ncol * self.nrow * self.nlay), dtype=np.float32
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
_cxtgeo.grdcp3d_quality_indicators(
|
|
1544
|
+
self.ncol,
|
|
1545
|
+
self.nrow,
|
|
1546
|
+
self.nlay,
|
|
1547
|
+
self._coordsv,
|
|
1548
|
+
self._zcornsv,
|
|
1549
|
+
self._actnumsv,
|
|
1550
|
+
fresults,
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
grdprops = GridProperties()
|
|
1554
|
+
|
|
1555
|
+
for num, name in qcnames.items():
|
|
1556
|
+
discrete = num in qcdiscrete
|
|
1557
|
+
prop = GridProperty(
|
|
1558
|
+
self,
|
|
1559
|
+
name=name,
|
|
1560
|
+
discrete=discrete,
|
|
1561
|
+
values=fresults[num, :].astype(np.int32 if discrete else np.float32),
|
|
1562
|
+
codes={0: "None", 1: name} if discrete else None,
|
|
1563
|
+
)
|
|
1564
|
+
grdprops.append_props([prop])
|
|
1565
|
+
|
|
1566
|
+
return grdprops
|