xtgeo 4.8.0__cp313-cp313-win_amd64.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.

Files changed (117) hide show
  1. cxtgeo.py +582 -0
  2. cxtgeoPYTHON_wrap.c +20938 -0
  3. xtgeo/__init__.py +246 -0
  4. xtgeo/_cxtgeo.cp313-win_amd64.pyd +0 -0
  5. xtgeo/_internal.cp313-win_amd64.pyd +0 -0
  6. xtgeo/common/__init__.py +19 -0
  7. xtgeo/common/_angles.py +29 -0
  8. xtgeo/common/_xyz_enum.py +50 -0
  9. xtgeo/common/calc.py +396 -0
  10. xtgeo/common/constants.py +30 -0
  11. xtgeo/common/exceptions.py +42 -0
  12. xtgeo/common/log.py +93 -0
  13. xtgeo/common/sys.py +166 -0
  14. xtgeo/common/types.py +18 -0
  15. xtgeo/common/version.py +21 -0
  16. xtgeo/common/xtgeo_dialog.py +604 -0
  17. xtgeo/cube/__init__.py +9 -0
  18. xtgeo/cube/_cube_export.py +214 -0
  19. xtgeo/cube/_cube_import.py +532 -0
  20. xtgeo/cube/_cube_roxapi.py +180 -0
  21. xtgeo/cube/_cube_utils.py +287 -0
  22. xtgeo/cube/_cube_window_attributes.py +340 -0
  23. xtgeo/cube/cube1.py +1023 -0
  24. xtgeo/grid3d/__init__.py +15 -0
  25. xtgeo/grid3d/_ecl_grid.py +774 -0
  26. xtgeo/grid3d/_ecl_inte_head.py +148 -0
  27. xtgeo/grid3d/_ecl_logi_head.py +71 -0
  28. xtgeo/grid3d/_ecl_output_file.py +81 -0
  29. xtgeo/grid3d/_egrid.py +1004 -0
  30. xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
  31. xtgeo/grid3d/_grdecl_format.py +266 -0
  32. xtgeo/grid3d/_grdecl_grid.py +388 -0
  33. xtgeo/grid3d/_grid3d.py +29 -0
  34. xtgeo/grid3d/_grid3d_fence.py +181 -0
  35. xtgeo/grid3d/_grid3d_utils.py +228 -0
  36. xtgeo/grid3d/_grid_boundary.py +76 -0
  37. xtgeo/grid3d/_grid_etc1.py +1566 -0
  38. xtgeo/grid3d/_grid_export.py +221 -0
  39. xtgeo/grid3d/_grid_hybrid.py +66 -0
  40. xtgeo/grid3d/_grid_import.py +79 -0
  41. xtgeo/grid3d/_grid_import_ecl.py +101 -0
  42. xtgeo/grid3d/_grid_import_roff.py +135 -0
  43. xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
  44. xtgeo/grid3d/_grid_refine.py +125 -0
  45. xtgeo/grid3d/_grid_roxapi.py +292 -0
  46. xtgeo/grid3d/_grid_wellzone.py +165 -0
  47. xtgeo/grid3d/_gridprop_export.py +178 -0
  48. xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
  49. xtgeo/grid3d/_gridprop_import_grdecl.py +130 -0
  50. xtgeo/grid3d/_gridprop_import_roff.py +52 -0
  51. xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
  52. xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
  53. xtgeo/grid3d/_gridprop_op1.py +174 -0
  54. xtgeo/grid3d/_gridprop_roxapi.py +239 -0
  55. xtgeo/grid3d/_gridprop_value_init.py +140 -0
  56. xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
  57. xtgeo/grid3d/_gridprops_import_roff.py +83 -0
  58. xtgeo/grid3d/_roff_grid.py +469 -0
  59. xtgeo/grid3d/_roff_parameter.py +303 -0
  60. xtgeo/grid3d/grid.py +2537 -0
  61. xtgeo/grid3d/grid_properties.py +699 -0
  62. xtgeo/grid3d/grid_property.py +1341 -0
  63. xtgeo/grid3d/types.py +15 -0
  64. xtgeo/io/__init__.py +1 -0
  65. xtgeo/io/_file.py +592 -0
  66. xtgeo/metadata/__init__.py +17 -0
  67. xtgeo/metadata/metadata.py +431 -0
  68. xtgeo/roxutils/__init__.py +7 -0
  69. xtgeo/roxutils/_roxar_loader.py +54 -0
  70. xtgeo/roxutils/_roxutils_etc.py +122 -0
  71. xtgeo/roxutils/roxutils.py +207 -0
  72. xtgeo/surface/__init__.py +18 -0
  73. xtgeo/surface/_regsurf_boundary.py +26 -0
  74. xtgeo/surface/_regsurf_cube.py +210 -0
  75. xtgeo/surface/_regsurf_cube_window.py +391 -0
  76. xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
  77. xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
  78. xtgeo/surface/_regsurf_export.py +388 -0
  79. xtgeo/surface/_regsurf_grid3d.py +271 -0
  80. xtgeo/surface/_regsurf_gridding.py +347 -0
  81. xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
  82. xtgeo/surface/_regsurf_import.py +347 -0
  83. xtgeo/surface/_regsurf_lowlevel.py +122 -0
  84. xtgeo/surface/_regsurf_oper.py +631 -0
  85. xtgeo/surface/_regsurf_roxapi.py +241 -0
  86. xtgeo/surface/_regsurf_utils.py +81 -0
  87. xtgeo/surface/_surfs_import.py +43 -0
  88. xtgeo/surface/_zmap_parser.py +138 -0
  89. xtgeo/surface/regular_surface.py +2967 -0
  90. xtgeo/surface/surfaces.py +276 -0
  91. xtgeo/well/__init__.py +24 -0
  92. xtgeo/well/_blockedwell_roxapi.py +221 -0
  93. xtgeo/well/_blockedwells_roxapi.py +68 -0
  94. xtgeo/well/_well_aux.py +30 -0
  95. xtgeo/well/_well_io.py +327 -0
  96. xtgeo/well/_well_oper.py +574 -0
  97. xtgeo/well/_well_roxapi.py +304 -0
  98. xtgeo/well/_wellmarkers.py +486 -0
  99. xtgeo/well/_wells_utils.py +158 -0
  100. xtgeo/well/blocked_well.py +216 -0
  101. xtgeo/well/blocked_wells.py +122 -0
  102. xtgeo/well/well1.py +1514 -0
  103. xtgeo/well/wells.py +211 -0
  104. xtgeo/xyz/__init__.py +6 -0
  105. xtgeo/xyz/_polygons_oper.py +272 -0
  106. xtgeo/xyz/_xyz.py +741 -0
  107. xtgeo/xyz/_xyz_data.py +646 -0
  108. xtgeo/xyz/_xyz_io.py +490 -0
  109. xtgeo/xyz/_xyz_lowlevel.py +42 -0
  110. xtgeo/xyz/_xyz_oper.py +613 -0
  111. xtgeo/xyz/_xyz_roxapi.py +766 -0
  112. xtgeo/xyz/points.py +681 -0
  113. xtgeo/xyz/polygons.py +811 -0
  114. xtgeo-4.8.0.dist-info/METADATA +145 -0
  115. xtgeo-4.8.0.dist-info/RECORD +117 -0
  116. xtgeo-4.8.0.dist-info/WHEEL +5 -0
  117. 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