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