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,3043 @@
1
+ """Module/class for regular surfaces with XTGeo.
2
+
3
+ Regular surfaces have a constant distance between nodes (xinc, yinc),
4
+ and this simplifies computations a lot. A regular surface is
5
+ defined by an origin (xori, yori)
6
+ in UTM, a number of columns (along X axis, if no rotation), a number of
7
+ rows (along Y axis if no rotation), and increment (distance between nodes).
8
+
9
+ The map itself is an array of values.
10
+
11
+ Rotation is allowed and is measured in degrees, anticlock from X axis.
12
+
13
+ Note that an instance of a regular surface can be made directly with::
14
+
15
+ >>> import xtgeo
16
+ >>> mysurf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
17
+
18
+ or::
19
+
20
+ mysurf = xtgeo.surface_from_rms('some_rms_project', 'TopX', 'DepthSurface')
21
+
22
+ """
23
+
24
+ # --------------------------------------------------------------------------------------
25
+ # Comment on 'asmasked' vs 'activeonly:
26
+ # 'asmasked'=True will return a np.ma array, with some fill_value if
27
+ # if asmasked = False
28
+ #
29
+ # while 'activeonly' will filter
30
+ # out maked entries, or use np.nan if 'activeonly' is False
31
+ #
32
+ # For functions with mask=... ,the should be replaced with asmasked=...
33
+ # --------------------------------------------------------------------------------------
34
+
35
+ from __future__ import annotations
36
+
37
+ import functools
38
+ import math
39
+ import numbers
40
+ import warnings
41
+ from copy import deepcopy
42
+ from types import FunctionType
43
+ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Type, Union
44
+
45
+ import numpy as np
46
+ import numpy.ma as ma
47
+ import pandas as pd
48
+ import scipy.ndimage
49
+
50
+ import xtgeo.interfaces.rms
51
+ from xtgeo.common.constants import (
52
+ UNDEF,
53
+ UNDEF_LIMIT,
54
+ VERYLARGENEGATIVE,
55
+ VERYLARGEPOSITIVE,
56
+ )
57
+ from xtgeo.common.exceptions import InvalidFileFormatError
58
+ from xtgeo.common.log import null_logger
59
+ from xtgeo.common.sys import generic_hash
60
+ from xtgeo.common.xtgeo_dialog import XTGDescription, XTGeoDialog
61
+ from xtgeo.io._file import FileFormat, FileWrapper
62
+ from xtgeo.metadata.metadata import MetaDataRegularSurface
63
+ from xtgeo.xyz.points import Points
64
+
65
+ from . import (
66
+ _regsurf_boundary,
67
+ _regsurf_cube,
68
+ _regsurf_cube_window,
69
+ _regsurf_export,
70
+ _regsurf_grid3d,
71
+ _regsurf_gridding,
72
+ _regsurf_import,
73
+ _regsurf_oper,
74
+ _regsurf_utils,
75
+ )
76
+
77
+ if TYPE_CHECKING:
78
+ import io
79
+ import pathlib
80
+
81
+ from xtgeo.cube.cube1 import Cube
82
+ from xtgeo.grid3d.grid import Grid, GridProperty
83
+
84
+
85
+ xtg = XTGeoDialog()
86
+ logger = null_logger(__name__)
87
+
88
+ # valid argumentts for seismic attributes
89
+ ValidAttrs = Literal[
90
+ "all",
91
+ "max",
92
+ "min",
93
+ "rms",
94
+ "mean",
95
+ "var",
96
+ "maxpos",
97
+ "maxneg",
98
+ "sumpos",
99
+ "sumneg",
100
+ "meanabs",
101
+ "meanpos",
102
+ "meanneg",
103
+ ]
104
+
105
+ # ======================================================================================
106
+ # METHODS as wrappers to class init + import
107
+
108
+
109
+ def surface_from_file(
110
+ mfile,
111
+ fformat=None,
112
+ template=None,
113
+ values=True,
114
+ engine: Optional[str] = None,
115
+ dtype: Union[Type[np.float64], Type[np.float32]] = np.float64,
116
+ ):
117
+ """Make an instance of a RegularSurface directly from file import.
118
+
119
+ Args:
120
+ mfile (str): Name of file
121
+ fformat: File format, None/guess/irap_binary/irap_ascii/ijxyz/petromod/
122
+ zmap_ascii/xtg/hdf is currently supported. If None or guess, the file
123
+ 'signature' is used to guess format first, then file extension.
124
+ template: Only valid if ``ijxyz`` format, where an existing Cube or
125
+ RegularSurface instance is applied to get correct topology.
126
+ values (bool): If True (default), surface values will be read (Irap binary only)
127
+ engine (str): Key indended for developers initially, now deprecated and have no
128
+ effect. To be removed in future versions.
129
+ dtype: Requsted numpy dtype of values; default is float64, alternatively float32
130
+
131
+ Example::
132
+
133
+ >>> import xtgeo
134
+ >>> mysurf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
135
+
136
+ .. versionchanged:: 2.1
137
+ Key "values" for Irap binary maps added
138
+
139
+ .. versionchanged:: 2.13 Key "engine" added
140
+ """
141
+
142
+ return RegularSurface._read_file(
143
+ mfile,
144
+ fformat=fformat,
145
+ load_values=values,
146
+ template=template,
147
+ dtype=dtype,
148
+ )
149
+
150
+
151
+ def surface_from_rms(
152
+ project,
153
+ name,
154
+ category,
155
+ stype="horizons",
156
+ realisation=0,
157
+ dtype: Union[Type[np.float64], Type[np.float32]] = np.float64,
158
+ ):
159
+ """This makes an instance of a RegularSurface directly from RMS input.
160
+
161
+ Args:
162
+ project (str or special): Name of project (as folder) if
163
+ outside RMS, og just use the magic ``project`` word if within RMS.
164
+ name (str): Name of surface/map
165
+ category (str): For horizons/zones or clipboard/general2d_data:
166
+ for example 'DS_extracted'. For clipboard/general2d_data this can
167
+ be empty or None, or use '/' for multiple folder levels (e.g. 'fld/subfld').
168
+ For 'trends', the category is not applied.
169
+ stype (str): RMS folder type, 'horizons' (default), 'zones', 'clipboard',
170
+ 'general2d_data' or 'trends'
171
+ realisation (int): Realisation number, default is 0
172
+ dtype: Requested numpy dtype for array; default is 64 bit float. Note that down-
173
+ casting may give loss of precision.
174
+
175
+ Example::
176
+
177
+ # inside RMS:
178
+ import xtgeo
179
+ mysurf = xtgeo.surface_from_rms(project, 'TopEtive', 'DepthSurface')
180
+
181
+ Note::
182
+
183
+ When dealing with surfaces to and from ``stype="trends"``, the surface must
184
+ exist in advance, i.e. the Roxar API do not allow creating new surfaces.
185
+ Actually trends are read only, but a workaround using ``load()`` in Roxar
186
+ API makes it possible to overwrite existing surface trends. In addition,
187
+ ``realisation`` is not applied in trends.
188
+
189
+ """
190
+
191
+ regsurf_rms = xtgeo.interfaces.rms.RegularSurfaceReader(
192
+ project_or_path=project,
193
+ name=name,
194
+ category=category,
195
+ stype=stype,
196
+ realisation=realisation,
197
+ ).load()
198
+
199
+ return RegularSurface(
200
+ name=regsurf_rms.name,
201
+ ncol=regsurf_rms.ncol,
202
+ nrow=regsurf_rms.nrow,
203
+ xori=regsurf_rms.xori,
204
+ yori=regsurf_rms.yori,
205
+ xinc=regsurf_rms.xinc,
206
+ yinc=regsurf_rms.yinc,
207
+ rotation=regsurf_rms.rotation,
208
+ values=regsurf_rms.values,
209
+ dtype=dtype, # handled by constructor
210
+ )
211
+
212
+
213
+ def surface_from_roxar(
214
+ project,
215
+ name,
216
+ category,
217
+ stype="horizons",
218
+ realisation=0,
219
+ dtype: Union[Type[np.float64], Type[np.float32]] = np.float64,
220
+ ):
221
+ """This makes an instance of a RegularSurface directly from roxar (i.e. RMS) input.
222
+
223
+ .. deprecated::
224
+ The `surface_from_roxar` function is deprecated and will be removed in a future
225
+ version. Use `surface_from_rms` instead.
226
+
227
+ For parameters and usage details, see :func:`surface_from_rms`.
228
+
229
+ """
230
+
231
+ # A pending deprecation warning will not show up in RMS for normal users
232
+ warnings.warn(
233
+ "The 'surface_from_roxar' function is deprecated and will be removed in a "
234
+ "future version. Use 'surface_from_rms' instead.",
235
+ PendingDeprecationWarning,
236
+ stacklevel=2,
237
+ )
238
+
239
+ return surface_from_rms(
240
+ project=project,
241
+ name=name,
242
+ category=category,
243
+ stype=stype,
244
+ realisation=realisation,
245
+ dtype=dtype,
246
+ )
247
+
248
+
249
+ def surface_from_cube(cube, value):
250
+ """Make RegularSurface directly from a cube instance with a constant value.
251
+
252
+ The surface geometry will be exactly the same as for the Cube.
253
+
254
+ Args:
255
+ cube(xtgeo.cube.Cube): A Cube instance
256
+ value (float): A constant value for the surface
257
+
258
+ Example::
259
+
260
+ >>> import xtgeo
261
+ >>> mycube = xtgeo.cube_from_file(cube_dir + "/ib_test_cube2.segy")
262
+ >>> mymap = xtgeo.surface_from_cube(mycube, 2700)
263
+
264
+ """
265
+ return RegularSurface._read_cube(cube, value)
266
+
267
+
268
+ def surface_from_grid3d(
269
+ grid: Grid,
270
+ template: RegularSurface | str | None = None,
271
+ where: str | int = "top",
272
+ property: str | GridProperty = "depth",
273
+ rfactor: float = 1.0,
274
+ index_position: Literal["center", "top", "base"] = "center",
275
+ **kwargs,
276
+ ) -> RegularSurface | List[np.ndarray]:
277
+ """This makes an instance of a RegularSurface directly from a Grid() instance.
278
+
279
+ Args:
280
+ grid: XTGeo 3D Grid instance, describing the corner point grid geometry
281
+ template: Optional, to use an existing surface as
282
+ template for map geometry, or by using "native" which returns a surface that
283
+ approximate the 3D grid layout (same number of rows and columns, and same
284
+ rotation). If None (default) a non-rotated surface will be made
285
+ based on a refined estimation from the current grid
286
+ resolution (see ``rfactor``).
287
+ where: Cell layer number, or if property is "depth", use "top" or "base" to get
288
+ a surface sampled from the very top or very base of the grid (including
289
+ inactive cells!). Otherwise use the syntax "2_top" where 2
290
+ is layer no. 2 and _top indicates top of cell, while "_base"
291
+ indicates base of cell. Cell layer numbering starts from 1. Default position
292
+ in a cell layer is "top" if layer is given as pure number and "depth" is
293
+ the property. If a grid property is given, the position is always found
294
+ the center depth within in a cell.
295
+ property: Which property to return. Choices are "depth", "i"
296
+ (columns) or "j" (rows) or, more generic, a GridProperty instance
297
+ which belongs to the given grid geometry. Alle these returns a
298
+ RegularSurface. A special variant is "raw" which
299
+ returns a list of 2D numpy arrays. See details in the Note section.
300
+ rfactor: Note this setting will only apply if ``template`` is None.
301
+ Determines how fine the extracted map is; higher values
302
+ for finer map (but computing time will increase slightly).
303
+ The default is 1.0, which in effect will make a surface approximentaly
304
+ twice as fine as the average resolution estimated from the 3D grid.
305
+ index_position: Default is "center" which means that the index is taken
306
+ from the Z center of the cell. If "top" is given, the index is taken from
307
+ the top of the cell, and if "base" is given, the index is taken from the
308
+ base of the cell. This is only valid for index properties "i" and "j".
309
+
310
+ Note::
311
+ The keyword ``mode`` is deprecated and will be removed in XTGeo version 5,
312
+ use keyword ``property`` instead. If both are given, ``property`` will be used.
313
+
314
+ Note::
315
+ For ``property`` "depth", "i" and "j", all cells in a layer will be used
316
+ (including inactive 3D cells), while for a GridProperty, only active cells
317
+ will be used. Hence the extent of the resulting surfaces may differ.
318
+
319
+ Note::
320
+ For ``property`` "raw", the return is a list of 2D arrays, where the first
321
+ array is the i-index (int), the second is the j-index (int), the third is the
322
+ top depth (float64), the fourth is the bottom depth (float64), and the
323
+ fifth is a mask (boolean) for inactive cells. For the index arrays, -1
324
+ indicates that the cell is outside any grid cell (projected from above; i.e.
325
+ could also be within a fault). For the depth arrays, the value is NaN
326
+ for inactive cells. The inactive mask is True for inactive cells. The index
327
+ arrays and mask is derived from the Z midpoints of the 3D cells. The "raw"
328
+ option is useful for further processing in Python, e.g. when a combination
329
+ of properties is needed.
330
+
331
+ .. versionadded:: 2.1
332
+ .. versionchanged:: 4.2 Changed ``mode`` to ``property`` to add support for
333
+ a GridProperty. The ``where`` arg. can now be an integer.
334
+ Added option ``activeonly``.
335
+ .. versionchanged:: 4.3 Added option ``raw`` to get data for further processing.
336
+ and add ``index_position`` for "i" and "j" properties.
337
+ """
338
+ mode = kwargs.get("mode")
339
+ if mode is not None:
340
+ warnings.warn(
341
+ "The 'mode' argument is deprecated, use 'property' instead",
342
+ DeprecationWarning,
343
+ stacklevel=2,
344
+ )
345
+ property = mode
346
+
347
+ if isinstance(property, str) and property == "raw":
348
+ args = _regsurf_grid3d.from_grid3d(
349
+ grid, template, where, property, rfactor, index_position="center"
350
+ )
351
+ return args["values"]
352
+
353
+ return RegularSurface._read_grid3d(
354
+ grid, template, where, property, rfactor, index_position
355
+ )
356
+
357
+
358
+ def _data_reader_factory(file_format: FileFormat):
359
+ if file_format == FileFormat.IRAP_BINARY:
360
+ return _regsurf_import.import_irap_binary
361
+ if file_format == FileFormat.IRAP_ASCII:
362
+ return _regsurf_import.import_irap_ascii
363
+ if file_format == FileFormat.IJXYZ:
364
+ return _regsurf_import.import_ijxyz
365
+ if file_format == FileFormat.PETROMOD:
366
+ return _regsurf_import.import_petromod
367
+ if file_format == FileFormat.ZMAP_ASCII:
368
+ return _regsurf_import.import_zmap_ascii
369
+ if file_format == FileFormat.XTG:
370
+ return _regsurf_import.import_xtg
371
+ if file_format == FileFormat.HDF:
372
+ return _regsurf_import.import_hdf5_regsurf
373
+
374
+ extensions = FileFormat.extensions_string(
375
+ [
376
+ FileFormat.IRAP_BINARY,
377
+ FileFormat.IRAP_ASCII,
378
+ FileFormat.IJXYZ,
379
+ FileFormat.PETROMOD,
380
+ FileFormat.ZMAP_ASCII,
381
+ FileFormat.XTG,
382
+ FileFormat.HDF,
383
+ ]
384
+ )
385
+ raise InvalidFileFormatError(
386
+ f"File format {file_format} is invalid for type RegularSurface. "
387
+ f"Supported formats are {extensions}."
388
+ )
389
+
390
+
391
+ class RegularSurface:
392
+ """Class for a regular surface in the XTGeo framework.
393
+
394
+ The values can as default be accessed by the user as a 2D masked numpy
395
+ (ncol, nrow) float64 array, but also other representations or views are
396
+ possible (e.g. as 1D ordinary numpy).
397
+
398
+ """
399
+
400
+ def __init__(
401
+ self,
402
+ ncol: int,
403
+ nrow: int,
404
+ xinc: float,
405
+ yinc: float,
406
+ xori: Optional[float] = 0.0,
407
+ yori: Optional[float] = 0.0,
408
+ yflip: Optional[int] = 1,
409
+ rotation: Optional[float] = 0.0,
410
+ values: Optional[Union[List[float], float]] = None,
411
+ ilines: Optional[List[float]] = None,
412
+ xlines: Optional[List[float]] = None,
413
+ masked: Optional[bool] = True,
414
+ name: Optional[str] = "unknown",
415
+ filesrc: Optional[str] = None,
416
+ fformat: Optional[str] = None,
417
+ undef: Optional[float] = UNDEF,
418
+ dtype: Union[Type[np.float64], Type[np.float32]] = np.float64,
419
+ ):
420
+ """Instantiating a RegularSurface::
421
+
422
+ vals = np.zeros(30 * 50)
423
+ surf = xtgeo.RegularSurface(
424
+ ncol=30, nrow=50, xori=1234.5, yori=4321.0, xinc=30.0, yinc=50.0,
425
+ rotation=30.0, values=vals, yflip=1,
426
+ )
427
+
428
+ Args:
429
+ ncol: Integer for number of X direction columns.
430
+ nrow: Integer for number of Y direction rows.
431
+ xori: X (East) origon coordinate.
432
+ yori: Y (North) origin coordinate.
433
+ xinc: X increment.
434
+ yinc: Y increment.
435
+ yflip: If 1, the map grid is left-handed (assuming depth downwards),
436
+ otherwise -1 means that Y axis is flipped (right-handed).
437
+ rotation: rotation in degrees, anticlock from X axis between 0, 360.
438
+ values: A scalar (for constant values) or a "array-like" input that has
439
+ ncol * nrow elements. As result, a 2D (masked) numpy array of shape
440
+ (ncol, nrow), C order will be made.
441
+ masked: Indicating if numpy array shall be masked or not. Default is True.
442
+ name: A given name for the surface, default is file name root or
443
+ 'unknown' if constructed from scratch.
444
+
445
+ Examples:
446
+ The instance can be made by specification::
447
+
448
+ >>> surface = RegularSurface(
449
+ ... ncol=20,
450
+ ... nrow=10,
451
+ ... xori=2000.0,
452
+ ... yori=2000.0,
453
+ ... rotation=0.0,
454
+ ... xinc=25.0,
455
+ ... yinc=25.0,
456
+ ... values=np.zeros((20,10))
457
+ ... )
458
+
459
+
460
+ """
461
+ logger.info("Start __init__ method for RegularSurface object %s", id(self))
462
+ self._ncol = ncol
463
+ self._nrow = nrow
464
+ self._xori = xori
465
+ self._yori = yori
466
+ self._xinc = xinc
467
+ self._yinc = yinc
468
+ self._rotation = rotation
469
+ self._yflip = yflip
470
+ self._name = name
471
+
472
+ self._undef = undef
473
+ self._undef_limit = UNDEF_LIMIT
474
+
475
+ self._filesrc = filesrc # Name of original input file or stream, if any
476
+
477
+ self._fformat = fformat # current fileformat, useful for load()
478
+ self._metadata = MetaDataRegularSurface()
479
+
480
+ self._values = None
481
+
482
+ if values is None:
483
+ values = np.ma.zeros((self._ncol, self._nrow))
484
+ self._isloaded = False
485
+ else:
486
+ self._isloaded = True
487
+ self._ensure_correct_values(values, force_dtype=dtype)
488
+
489
+ if ilines is None:
490
+ self._ilines = np.array(range(1, self._ncol + 1), dtype=np.int32)
491
+ self._xlines = np.array(range(1, self._nrow + 1), dtype=np.int32)
492
+ else:
493
+ self._ilines = ilines
494
+ self._xlines = xlines
495
+
496
+ self._masked = masked # TODO: check usecase
497
+ self._metadata.required = self
498
+
499
+ @classmethod
500
+ def _read_zmap_ascii(cls, mfile, values):
501
+ mfile = FileWrapper(mfile)
502
+ args = _data_reader_factory(FileFormat.ZMAP_ASCII)(mfile, values=values)
503
+ return cls(**args)
504
+
505
+ @classmethod
506
+ def _read_ijxyz(cls, mfile: FileWrapper, template: RegularSurface | Cube | None):
507
+ mfile = FileWrapper(mfile)
508
+ args = _data_reader_factory(FileFormat.IJXYZ)(mfile, template=template)
509
+ return cls(**args)
510
+
511
+ def __repr__(self):
512
+ """Magic method __repr__."""
513
+ return (
514
+ f"{self.__class__.__name__} (xori={self._xori!r}, yori={self._yori!r}, "
515
+ f"xinc={self._xinc!r}, yinc={self._yinc!r}, ncol={self._ncol!r}, "
516
+ f"nrow={self._nrow!r}, rotation={self._rotation!r}, yflip={self._yflip!r}, "
517
+ f"masked={self._masked!r}, filesrc={self._filesrc!r}, "
518
+ f"name={self._name!r}, ilines={self.ilines.shape!r}, "
519
+ f"xlines={self.xlines.shape!r}, values={self.values.shape!r}) "
520
+ f"ID={id(self)}."
521
+ )
522
+
523
+ def __str__(self):
524
+ """Magic method __str__ for user friendly print."""
525
+ return self.describe(flush=False)
526
+
527
+ def __getitem__(self, index):
528
+ """Magic method."""
529
+ col, row = index
530
+ return self._values[col, row]
531
+
532
+ def __add__(self, other):
533
+ """Magic method."""
534
+ news = self.copy()
535
+ _regsurf_oper.operations_two(news, other, oper="add")
536
+
537
+ return news
538
+
539
+ def __iadd__(self, other):
540
+ """Magic method."""
541
+ _regsurf_oper.operations_two(self, other, oper="iadd")
542
+ return self
543
+
544
+ def __sub__(self, other):
545
+ """Magic method."""
546
+ news = self.copy()
547
+ _regsurf_oper.operations_two(news, other, oper="sub")
548
+
549
+ return news
550
+
551
+ def __isub__(self, other):
552
+ """Magic method."""
553
+ _regsurf_oper.operations_two(self, other, oper="isub")
554
+ return self
555
+
556
+ def __mul__(self, other):
557
+ """Magic method."""
558
+ news = self.copy()
559
+ _regsurf_oper.operations_two(news, other, oper="mul")
560
+
561
+ return news
562
+
563
+ def __imul__(self, other):
564
+ """Magic method."""
565
+ _regsurf_oper.operations_two(self, other, oper="imul")
566
+ return self
567
+
568
+ def __truediv__(self, other):
569
+ """Magic method."""
570
+ news = self.copy()
571
+ _regsurf_oper.operations_two(news, other, oper="div")
572
+
573
+ return news
574
+
575
+ def __idiv__(self, other):
576
+ """Magic method."""
577
+ _regsurf_oper.operations_two(self, other, oper="idiv")
578
+ return self
579
+
580
+ # comparison operators, return boolean arrays
581
+
582
+ def __lt__(self, other):
583
+ """Magic method."""
584
+ return _regsurf_oper.operations_two(self, other, oper="lt")
585
+
586
+ def __gt__(self, other):
587
+ """Magic method."""
588
+ return _regsurf_oper.operations_two(self, other, oper="gt")
589
+
590
+ def __le__(self, other):
591
+ """Magic method."""
592
+ return _regsurf_oper.operations_two(self, other, oper="le")
593
+
594
+ def __ge__(self, other):
595
+ """Magic method."""
596
+ return _regsurf_oper.operations_two(self, other, oper="ge")
597
+
598
+ def __eq__(self, other):
599
+ """Magic method."""
600
+ return _regsurf_oper.operations_two(self, other, oper="eq")
601
+
602
+ def __ne__(self, other):
603
+ """Magic method."""
604
+ return _regsurf_oper.operations_two(self, other, oper="ne")
605
+
606
+ # ==================================================================================
607
+ # Class and special methods
608
+ # ==================================================================================
609
+
610
+ @classmethod
611
+ def methods(cls):
612
+ """Returns the names of the methods in the class.
613
+
614
+ >>> print(RegularSurface.methods())
615
+ METHODS for RegularSurface():
616
+ ======================
617
+ __init__
618
+ __repr__
619
+ ...
620
+
621
+ """
622
+ mets = [x for x, y in cls.__dict__.items() if isinstance(y, FunctionType)]
623
+
624
+ txt = "METHODS for RegularSurface():\n======================\n"
625
+ for met in mets:
626
+ txt += str(met) + "\n"
627
+
628
+ return txt
629
+
630
+ # ==================================================================================
631
+ # Properties
632
+ # ==================================================================================
633
+
634
+ @property
635
+ def metadata(self):
636
+ """Return metadata object instance of type MetaDataRegularSurface."""
637
+ return self._metadata
638
+
639
+ @metadata.setter
640
+ def metadata(self, obj):
641
+ # The current metadata object can be replaced. A bit dangerous so further
642
+ # check must be done to validate. TODO.
643
+ if not isinstance(obj, MetaDataRegularSurface):
644
+ raise ValueError("Input obj not an instance of MetaDataRegularSurface")
645
+
646
+ self._metadata = obj # checking is currently missing! TODO
647
+
648
+ @property
649
+ def ncol(self):
650
+ """The NCOL (NX or N-Idir) number, as property (read only)."""
651
+ return self._ncol
652
+
653
+ @property
654
+ def nrow(self):
655
+ """The NROW (NY or N-Jdir) number, as property (read only)."""
656
+ return self._nrow
657
+
658
+ @property
659
+ def dimensions(self):
660
+ """2-tuple: The surface dimensions as a tuple of 2 integers (read only)."""
661
+ return (self._ncol, self._nrow)
662
+
663
+ @property
664
+ def nactive(self):
665
+ """Number of active map nodes (read only)."""
666
+ if self._isloaded:
667
+ return self._values.count()
668
+ return None
669
+
670
+ @property
671
+ def rotation(self):
672
+ """The rotation, anticlock from X axis, in degrees [0..360>."""
673
+ return self._rotation
674
+
675
+ @rotation.setter
676
+ def rotation(self, rota):
677
+ if 0 <= rota < 360:
678
+ self._rotation = rota
679
+ else:
680
+ raise ValueError("Rotation must be in interval [0, 360>")
681
+
682
+ @property
683
+ def xinc(self):
684
+ """The X increment (or I dir increment)."""
685
+ return self._xinc
686
+
687
+ @property
688
+ def yinc(self):
689
+ """The Y increment (or I dir increment)."""
690
+ return self._yinc
691
+
692
+ @property
693
+ def yflip(self):
694
+ """The Y flip (handedness) indicator 1, or -1 (read only).
695
+
696
+ The value 1 (default) means a left-handed system if depth values are
697
+ positive downwards. Assume -1 is rare, but may happen when
698
+ surface is derived from seismic cube.
699
+ """
700
+ return self._yflip
701
+
702
+ @property
703
+ def xori(self):
704
+ """The X coordinate origin of the map."""
705
+ return self._xori
706
+
707
+ @xori.setter
708
+ def xori(self, xnew):
709
+ self._xori = xnew
710
+
711
+ @property
712
+ def yori(self):
713
+ """The Y coordinate origin of the map."""
714
+ return self._yori
715
+
716
+ @yori.setter
717
+ def yori(self, ynew):
718
+ self._yori = ynew
719
+
720
+ @property
721
+ def ilines(self):
722
+ """The inlines numbering vector (read only)."""
723
+ return self._ilines
724
+
725
+ @ilines.setter
726
+ def ilines(self, values):
727
+ if isinstance(values, np.ndarray) and values.shape[0] == self._ncol:
728
+ self._ilines = values
729
+
730
+ @property
731
+ def xlines(self):
732
+ """The xlines numbering vector (read only)."""
733
+ return self._xlines
734
+
735
+ @xlines.setter
736
+ def xlines(self, values):
737
+ if isinstance(values, np.ndarray) and values.shape[0] == self._nrow:
738
+ self._xlines = values
739
+
740
+ @property
741
+ def xmin(self):
742
+ """The minimim X coordinate (read only)."""
743
+ corners = self.get_map_xycorners()
744
+
745
+ xmin = VERYLARGEPOSITIVE
746
+ for corner in corners:
747
+ if corner[0] < xmin:
748
+ xmin = corner[0]
749
+ return xmin
750
+
751
+ @property
752
+ def xmax(self):
753
+ """The maximum X coordinate (read only)."""
754
+ corners = self.get_map_xycorners()
755
+
756
+ xmax = VERYLARGENEGATIVE
757
+ for corner in corners:
758
+ if corner[0] > xmax:
759
+ xmax = corner[0]
760
+ return xmax
761
+
762
+ @property
763
+ def ymin(self):
764
+ """The minimim Y coordinate (read only)."""
765
+ corners = self.get_map_xycorners()
766
+
767
+ ymin = VERYLARGEPOSITIVE
768
+ for corner in corners:
769
+ if corner[1] < ymin:
770
+ ymin = corner[1]
771
+ return ymin
772
+
773
+ @property
774
+ def ymax(self):
775
+ """The maximum Y xoordinate (read only)."""
776
+ corners = self.get_map_xycorners()
777
+
778
+ ymax = VERYLARGENEGATIVE
779
+ for corner in corners:
780
+ if corner[1] > ymax:
781
+ ymax = corner[1]
782
+ return ymax
783
+
784
+ @property
785
+ def dtype(self) -> np.dtype:
786
+ """Getting the dtype of the values (np.array); float64 or float32"""
787
+ # this is not stored as state varible, but derived from the actual values
788
+ try:
789
+ infer_dtype = self._values.dtype
790
+ except AttributeError:
791
+ infer_dtype = np.float64
792
+ return infer_dtype
793
+
794
+ @dtype.setter
795
+ def dtype(self, wanted_dtype: Union[Type[np.float64], Type[np.float32]]):
796
+ """Setting the dtype of the values (np.array); float64 or float32"""
797
+ try:
798
+ self._values = self._values.astype(wanted_dtype)
799
+ except TypeError as msg:
800
+ warnings.warn(f"Cannot change dtype: {msg}. Will keep current", UserWarning)
801
+ return
802
+
803
+ @property
804
+ def values(self):
805
+ """The map values, as 2D masked numpy (float64/32), shape (ncol, nrow).
806
+
807
+ When setting values as a scalar, the current mask will be preserved.
808
+
809
+ When setting values, list-like input (lists, tuples) is also accepted, as
810
+ long as the length is correct and the entries are number-like.
811
+
812
+ In order to specify undefined values, you can specify the ``undef`` attribute
813
+ in the list, or use ``float("nan")``.
814
+
815
+ Example::
816
+
817
+ # list like input where nrow=3 and ncol=5 (15 entries)
818
+ newvalues = list(range(15))
819
+ newvalues[2] = srf.undef
820
+ srf.values = newvalues # here, entry 2 will be undefined
821
+ """
822
+ return self._values
823
+
824
+ @values.setter
825
+ def values(self, values):
826
+ self._ensure_correct_values(values)
827
+
828
+ @property
829
+ def values1d(self):
830
+ """(Read only) Map values, as 1D numpy masked, normally a numpy view(?).
831
+
832
+ Example::
833
+
834
+ map = xtgeo.surface_from_file('myfile.gri')
835
+ map.values1d
836
+ """
837
+ return self.get_values1d(asmasked=True)
838
+
839
+ @property
840
+ def npvalues1d(self):
841
+ """(Read only) Map values, as 1D numpy (not masked), undef as np.nan.
842
+
843
+ In most cases this will be a copy of the values.
844
+
845
+ Example::
846
+
847
+ >>> import xtgeo
848
+ >>> map = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
849
+ >>> values = map.npvalues1d
850
+ >>> mean = np.nanmean(values)
851
+ >>> values[values <= 0] = np.nan
852
+ >>> print(values)
853
+ [nan nan ... nan]
854
+ """
855
+ return self.get_values1d(asmasked=False, fill_value=np.nan)
856
+
857
+ @property
858
+ def name(self):
859
+ """A free form name for the surface, to be used in display etc."""
860
+ return self._name
861
+
862
+ @name.setter
863
+ def name(self, newname):
864
+ if isinstance(newname, str):
865
+ self._name = newname
866
+
867
+ @property
868
+ def undef(self):
869
+ """Returns the undef map value (read only)."""
870
+ return self._undef
871
+
872
+ @property
873
+ def undef_limit(self):
874
+ """Returns the undef_limit map value (read only)."""
875
+ return self._undef_limit
876
+
877
+ @property
878
+ def filesrc(self):
879
+ """Gives the name of the file source (if any)."""
880
+ return self._filesrc
881
+
882
+ @filesrc.setter
883
+ def filesrc(self, name):
884
+ self._filesrc = name # checking is currently missing
885
+
886
+ # ==================================================================================
887
+ # Describe, import and export
888
+ # ==================================================================================
889
+
890
+ def generate_hash(self, hashmethod="md5"):
891
+ """Return a unique hash ID for current instance.
892
+
893
+ See :meth:`~xtgeo.common.sys.generic_hash()` for documentation.
894
+
895
+ .. versionadded:: 2.14
896
+ """
897
+ required = (
898
+ "ncol",
899
+ "nrow",
900
+ "xori",
901
+ "yori",
902
+ "xinc",
903
+ "yinc",
904
+ "yflip",
905
+ "rotation",
906
+ )
907
+ gid = ""
908
+ for req in required:
909
+ gid += f"{getattr(self, '_' + req)}"
910
+ # Ignore the mask
911
+ gid += self._values.data.tobytes().hex()
912
+
913
+ return generic_hash(gid, hashmethod=hashmethod)
914
+
915
+ def describe(self, flush=True):
916
+ """Describe an instance by printing to stdout."""
917
+ #
918
+ dsc = XTGDescription()
919
+ dsc.title("Description of RegularSurface instance")
920
+ dsc.txt("Object ID", id(self))
921
+ dsc.txt("File source", self._filesrc)
922
+ dsc.txt("Shape: NCOL, NROW", self.ncol, self.nrow)
923
+ dsc.txt("Active cells vs total", self.nactive, self.nrow * self.ncol)
924
+ dsc.txt("Origins XORI, YORI", self.xori, self.yori)
925
+ dsc.txt("Increments XINC YINC", self.xinc, self.yinc)
926
+ dsc.txt("Rotation (anti-clock from X)", self.rotation)
927
+ dsc.txt("YFLIP flag", self._yflip)
928
+ np.set_printoptions(threshold=16)
929
+ dsc.txt("Inlines vector", self._ilines)
930
+ dsc.txt("Xlines vector", self._xlines)
931
+ np.set_printoptions(threshold=1000)
932
+ if self._isloaded:
933
+ dsc.txt("Values", self._values.reshape(-1), self._values.dtype)
934
+ dsc.txt(
935
+ "Values: mean, stdev, minimum, maximum",
936
+ self.values.mean(),
937
+ self.values.std(),
938
+ self.values.min(),
939
+ self.values.max(),
940
+ )
941
+ msize = float(self.values.size * 8) / (1024 * 1024 * 1024)
942
+ dsc.txt("Minimum memory usage of array (GB)", msize)
943
+ else:
944
+ dsc.txt("Values:", "Not loaded")
945
+
946
+ if flush:
947
+ dsc.flush()
948
+ return None
949
+
950
+ return dsc.astext()
951
+
952
+ @classmethod
953
+ def _read_file(
954
+ cls,
955
+ mfile: Union[str, pathlib.Path, io.BytesIO],
956
+ fformat: Optional[str] = None,
957
+ load_values: bool = True,
958
+ **kwargs,
959
+ ):
960
+ """Import surface (regular map) from file.
961
+
962
+ Note that the ``fformat=None`` or ``guess`` option will guess format by
963
+ looking at the file or stream signature or file extension.
964
+ For the signature, the first bytes are scanned for 'patterns'. If that
965
+ does not work (and input is not a memory stream), it will try to use
966
+ file extension where e.g. "gri" will assume irap_binary and "fgr"
967
+ assume Irap Ascii. If file extension is missing, Irap binary is assumed.
968
+
969
+ The ``ijxyz`` format is the typical seismic format, on the form
970
+ (ILINE, XLINE, X, Y, VALUE) as a table of points. Map values are
971
+ estimated from the given values, or by using an existing map or
972
+ cube as template, and match by ILINE/XLINE numbering.
973
+
974
+ BytesIO input is supported for Irap binary, Irap Ascii, ZMAP ascii.
975
+
976
+ Args:
977
+ mfile: File-like or memory stream instance.
978
+ fformat: File format, None/guess/irap_binary/irap_ascii/ijxyz
979
+ is currently supported. If None or guess, the file 'signature' is
980
+ used to guess format first, then file extension.
981
+ load_values: If True (default), then full array is read, if False
982
+ only metadata will be read. Valid for Irap binary only. This allows
983
+ lazy loading in e.g. ensembles.
984
+ kwargs: some readers allow additonal options
985
+
986
+ Keyword Args:
987
+ template: Only valid if ``ijxyz`` format, where an
988
+ existing Cube or RegularSurface instance is applied to
989
+ get correct topology.
990
+ engine: Deprecated keyword with no function, will be removed in version 5.
991
+
992
+ Returns:
993
+ Object instance.
994
+
995
+ Example::
996
+
997
+ >>> surf = RegularSurface._read_file(surface_dir + "/topreek_rota.gri")
998
+
999
+ .. versionadded:: 2.14
1000
+
1001
+ """
1002
+ mfile = FileWrapper(mfile)
1003
+ mfile.check_file(raiseerror=ValueError)
1004
+ fmt = mfile.fileformat(fformat)
1005
+
1006
+ new_kwargs = _data_reader_factory(fmt)(mfile, values=load_values, **kwargs)
1007
+ new_kwargs["filesrc"] = mfile.file
1008
+ new_kwargs["fformat"] = fmt
1009
+ new_kwargs["dtype"] = kwargs.get("dtype", np.float64)
1010
+ return cls(**new_kwargs)
1011
+
1012
+ def load_values(self):
1013
+ """Import surface values in cases where metadata only is loaded.
1014
+
1015
+ Currently, only Irap binary format is supported.
1016
+
1017
+ Example::
1018
+
1019
+ surfs = []
1020
+ for i in range(1000):
1021
+ surfs.append(xtgeo.surface_from_file(f"myfile{i}.gri", values=False))
1022
+
1023
+ # load values in number 88:
1024
+ surfs[88].load_values()
1025
+
1026
+ .. versionadded:: 2.1
1027
+ """
1028
+
1029
+ if not self._isloaded:
1030
+ if self.filesrc is None:
1031
+ raise ValueError(
1032
+ "Can only load values into object initialised from file"
1033
+ )
1034
+
1035
+ with warnings.catch_warnings():
1036
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
1037
+ mfile = FileWrapper(self.filesrc)
1038
+ kwargs = _data_reader_factory(self._fformat)(mfile, values=True)
1039
+ self.values = kwargs.get("values", self._values)
1040
+
1041
+ self._isloaded = True
1042
+
1043
+ def to_file(
1044
+ self,
1045
+ mfile: Union[str, pathlib.Path, io.BytesIO],
1046
+ fformat: Optional[str] = "irap_binary",
1047
+ pmd_dataunits: Optional[Tuple[int, int]] = (15, 10),
1048
+ engine: Optional[str] = None,
1049
+ error_if_near_empty: bool = False,
1050
+ ):
1051
+ """Export a surface (map) to file.
1052
+
1053
+ Note, for zmap_ascii and storm_binary an unrotation will be done
1054
+ automatically. The sampling will be somewhat finer than the
1055
+ original map in order to prevent aliasing. See :func:`unrotate`.
1056
+
1057
+ Args:
1058
+ mfile: Name of file,
1059
+ Path instance or IOBytestream instance. An alias can be e.g.
1060
+ "%md5sum%" or "%fmu-v1%" with string or Path() input.
1061
+ fformat: File format, irap_binary/irap_ascii/zmap_ascii/
1062
+ storm_binary/ijxyz/petromod/xtg*. Default is irap_binary.
1063
+ pmd_dataunits: A tuple of length 2 for petromod format,
1064
+ spesifying metadata for units (DataUnitDistance, DataUnitZ).
1065
+ engine: This was mainly a developer setting! The 'engine' is now deprecated.
1066
+ error_if_near_empty: Default is False. If True, raise a RuntimeError if
1067
+ number of map nodes is less than 4. Otherwise, if False and number of
1068
+ nodes are less than 4, a UserWarning will be given.
1069
+
1070
+ Returns:
1071
+ ofile (pathlib.Path): The actual file instance, or None if io.Bytestream
1072
+
1073
+ Examples::
1074
+
1075
+ >>> # read and write to ordinary file
1076
+ >>> surf = xtgeo.surface_from_file(
1077
+ ... surface_dir + '/topreek_rota.fgr',
1078
+ ... fformat = 'irap_ascii'
1079
+ ... )
1080
+ >>> surf.values = surf.values + 300
1081
+ >>> filename = surf.to_file(
1082
+ ... outdir + '/topreek_rota300.fgr',
1083
+ ... fformat = 'irap_ascii'
1084
+ ... )
1085
+
1086
+ >>> # writing to io.BytesIO:
1087
+ >>> stream = io.BytesIO()
1088
+ >>> surf.to_file(stream, fformat="irap_binary")
1089
+
1090
+ >>> # read from memory stream:
1091
+ >>> _ = stream.seek(0)
1092
+ >>> newsurf = xtgeo.surface_from_file(stream, fformat = 'irap_binary')
1093
+
1094
+ .. versionchanged:: 2.5 Added support for BytesIO
1095
+ .. versionchanged:: 2.13 Improved support for BytesIO
1096
+ .. versionchanged:: 2.14 Support for alias file name and return value
1097
+ .. versionchanged:: 3.8 Add key ``error_if_near_empty``
1098
+ .. versionchanged:: 4.6 The ``engine`` keyword (for developers) is deprecated
1099
+ and will be removed in version 5
1100
+ """
1101
+ if engine:
1102
+ warnings.warn(
1103
+ "The 'engine' keyword is deprecated and will be removed in XTGeo 5"
1104
+ "(Note that this key was intended as a developer setting.)"
1105
+ "Please remove this key from your code.",
1106
+ DeprecationWarning,
1107
+ stacklevel=2,
1108
+ )
1109
+
1110
+ logger.info("Export RegularSurface to file or memstream...")
1111
+ if self.nactive is None or self.nactive < 4:
1112
+ msg = (
1113
+ f"Number of maps nodes are {self.nactive}. Exporting regular "
1114
+ "surfaces with fewer than 4 nodes will not provide any "
1115
+ "usable result. The map may also be not loaded if nodes are None."
1116
+ )
1117
+
1118
+ if error_if_near_empty:
1119
+ raise RuntimeError(msg)
1120
+ warnings.warn(msg, UserWarning)
1121
+
1122
+ mfile = FileWrapper(mfile, mode="wb", obj=self)
1123
+ mfile.check_folder(raiseerror=OSError)
1124
+
1125
+ if fformat in FileFormat.IRAP_ASCII.value:
1126
+ _regsurf_export.export_irap_ascii(self, mfile)
1127
+
1128
+ elif fformat in FileFormat.IRAP_BINARY.value:
1129
+ _regsurf_export.export_irap_binary(self, mfile)
1130
+
1131
+ elif fformat in FileFormat.ZMAP_ASCII.value:
1132
+ _regsurf_export.export_zmap_ascii(self, mfile)
1133
+
1134
+ elif fformat in FileFormat.STORM.value:
1135
+ _regsurf_export.export_storm_binary(self, mfile)
1136
+
1137
+ elif fformat in FileFormat.PETROMOD.value:
1138
+ _regsurf_export.export_petromod_binary(self, mfile, pmd_dataunits)
1139
+
1140
+ elif fformat in FileFormat.IJXYZ.value:
1141
+ _regsurf_export.export_ijxyz_ascii(self, mfile)
1142
+
1143
+ elif fformat == "xtgregsurf":
1144
+ _regsurf_export.export_xtgregsurf(self, mfile)
1145
+
1146
+ else:
1147
+ extensions = FileFormat.extensions_string(
1148
+ [
1149
+ FileFormat.IRAP_BINARY,
1150
+ FileFormat.IRAP_ASCII,
1151
+ FileFormat.IJXYZ,
1152
+ FileFormat.PETROMOD,
1153
+ FileFormat.ZMAP_ASCII,
1154
+ FileFormat.XTG,
1155
+ FileFormat.HDF,
1156
+ ]
1157
+ )
1158
+ raise InvalidFileFormatError(
1159
+ f"File format {fformat} is invalid for type RegularSurface. "
1160
+ f"Supported formats are {extensions}."
1161
+ )
1162
+
1163
+ logger.info("Export RegularSurface to file or memstream... done")
1164
+
1165
+ if mfile.memstream:
1166
+ return None
1167
+ return mfile.file
1168
+
1169
+ def to_hdf(
1170
+ self,
1171
+ mfile: Union[str, pathlib.Path, io.BytesIO],
1172
+ compression: Optional[str] = "lzf",
1173
+ ) -> pathlib.Path:
1174
+ """Export a surface (map) with metadata to a HDF5 file.
1175
+
1176
+ Warning:
1177
+ This implementation is currently experimental and only recommended
1178
+ for testing.
1179
+
1180
+ The file extension shall be '.hdf'
1181
+
1182
+ Args:
1183
+ mfile: Name of file, Path instance or BytesIO instance. An alias can
1184
+ be e.g. ``$md5sum.hdf``, ``$fmu-v1.hdf`` with string or Path() input.
1185
+ compression: Compression method, None, lzf (default), blosc
1186
+
1187
+ Returns:
1188
+ pathlib.Path: The actual file instance, or None if io.Bytestream
1189
+
1190
+ Example:
1191
+ >>> import xtgeo
1192
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
1193
+ >>> filepath = surf.to_hdf(outdir + "/topreek_rota.hdf")
1194
+
1195
+ .. versionadded:: 2.14
1196
+
1197
+ """
1198
+ # developing, in prep and experimental!
1199
+ mfile = FileWrapper(mfile, mode="wb", obj=self)
1200
+
1201
+ mfile.check_folder(raiseerror=OSError)
1202
+
1203
+ _regsurf_export.export_hdf5_regsurf(self, mfile, compression=compression)
1204
+ return mfile.file
1205
+
1206
+ def to_rms(
1207
+ self: RegularSurface,
1208
+ project: object,
1209
+ name: str,
1210
+ category: str,
1211
+ stype: Literal[
1212
+ "horizons", "zones", "clipboard", "general2d_data", "trends"
1213
+ ] = "horizons",
1214
+ realisation: int = 0,
1215
+ domain: Literal["depth", "time", "unknown"] = "depth",
1216
+ ) -> None: # pragma: no cover
1217
+ """Store (export/save) a regular surface to a Roxar RMS project via the RMSAPI.
1218
+
1219
+ The export to the RMS project can be done either within the project
1220
+ or outside the project. The storing can be done to various storage types in
1221
+ RMS, such as 'Horizons', 'Zones', 'Clipboard', 'General2D_data' and
1222
+ 'Trends'.
1223
+
1224
+ Note:
1225
+ For stype = 'horizons' or 'zones', the horizon or zone name and category
1226
+ must exists in advance, otherwise an Exception will be raised. Items on
1227
+ 'clipboard' and 'general2d_data' will be created if not already present,
1228
+ and overwritten if they do.
1229
+
1230
+ When project is a file path (direct access, outside RMS) then
1231
+ ``to_rms()`` will implicitly do a project save. Otherwise, the project
1232
+ will not be saved until the user do an explicit project save action.
1233
+
1234
+ Args:
1235
+ project: Name of project (as a folder string) if
1236
+ outside RMS, or just use the magic ``project`` word if within RMS.
1237
+ name: Name of surface/map
1238
+ category: Required for horizons/zones: e.g. 'DS_extracted'. For
1239
+ clipboard/general2d_data represents the folder(s), where "" or None
1240
+ means no folder, while e.g. "myfolder/subfolder" means that folders
1241
+ myfolder/subfolder will be created if not already present. For
1242
+ stype = 'trends', the category will not be applied
1243
+ stype: RMS storage type, 'horizons' (default), 'zones', 'clipboard'
1244
+ 'general2d_data', 'trends'
1245
+ realisation: Realisation number, default is 0
1246
+ domain: Default is 'depth', but 'time', 'unknown' is also possible.
1247
+ Note that domain only applies to stypes 'clipboard' and
1248
+ 'general2d_data', since the domain for horizons/zones is defined
1249
+ more rigidly when the horizon/zone category is created in RMS.
1250
+ Trends are always 'unknown' domain.
1251
+
1252
+ Raises:
1253
+ ValueError: If name or category does not exist in the project
1254
+ RuntimeError: If Roxar RMS API cannot store the surface for various reasons
1255
+
1256
+ Example::
1257
+
1258
+ Here the from_rms method is used to initiate the object
1259
+ directly::
1260
+
1261
+ import xtgeo
1262
+ topupperreek = xtgeo.surface_from_rms(project, 'TopUpperReek',
1263
+ 'DS_extracted')
1264
+ topupperreek.values += 200
1265
+
1266
+ # export to file:
1267
+ topupperreek.to_file('topupperreek.gri')
1268
+
1269
+ # store in project
1270
+ topupperreek.to_rms(project, 'TopUpperReek', 'DS_something')
1271
+
1272
+ Note:
1273
+ The ``realisation`` number is not applied in trends.
1274
+
1275
+
1276
+ .. versionadded:: 2.1 clipboard support
1277
+ .. versionadded:: 2.19 general2d_data and trends support
1278
+ .. versionadded:: 4.14 Add domain keyword
1279
+
1280
+ """
1281
+ use_srf = self
1282
+ if self.yflip == -1:
1283
+ use_srf = self.copy()
1284
+ use_srf.make_lefthanded()
1285
+
1286
+ data = xtgeo.interfaces.rms.RegularSurfaceDataRms(
1287
+ name=name,
1288
+ xori=use_srf.xori,
1289
+ yori=use_srf.yori,
1290
+ ncol=use_srf.ncol,
1291
+ nrow=use_srf.nrow,
1292
+ xinc=use_srf.xinc,
1293
+ yinc=use_srf.yinc,
1294
+ rotation=use_srf.rotation,
1295
+ values=use_srf.values, # exporter will enforce float64 and sanitize
1296
+ )
1297
+
1298
+ writer = xtgeo.interfaces.rms.RegularSurfaceWriter(
1299
+ project_or_path=project,
1300
+ name=name,
1301
+ category=category,
1302
+ stype=stype,
1303
+ realisation=realisation,
1304
+ domain=domain,
1305
+ )
1306
+ writer.save(data)
1307
+
1308
+ def to_roxar(
1309
+ self: RegularSurface,
1310
+ project: object,
1311
+ name: str,
1312
+ category: str,
1313
+ stype: str = "horizons",
1314
+ realisation: int = 0,
1315
+ domain: Literal["depth", "time", "unknown"] = "depth",
1316
+ ) -> None: # pragma: no cover
1317
+ """Deprecated: Use to_rms() instead.
1318
+
1319
+ .. deprecated:: 4.15
1320
+ ``to_roxar()`` is deprecated and will be removed in a future version.
1321
+ Use :meth:`to_rms()` instead.
1322
+ """
1323
+ # a PendingDeprecationWarning will not show up for RMS users, which is intended
1324
+ # since they should not be 'disturbed' by this change at this point.
1325
+ warnings.warn(
1326
+ "The 'to_roxar()' method is deprecated and will be removed in a later "
1327
+ "xtgeo version. Use 'to_rms()' instead.",
1328
+ PendingDeprecationWarning,
1329
+ stacklevel=2,
1330
+ )
1331
+ self.to_rms(
1332
+ project=project,
1333
+ name=name,
1334
+ category=category,
1335
+ stype=stype,
1336
+ realisation=realisation,
1337
+ domain=domain,
1338
+ )
1339
+
1340
+ @classmethod
1341
+ def _read_cube(cls, cube, zlevel):
1342
+ """Make a constant surface from a Cube, at a given time/depth level.
1343
+
1344
+ The surface instance will have exactly the same origins and increments
1345
+ as the cube.
1346
+
1347
+ Args:
1348
+ cube (Cube): XTGeo Cube instance
1349
+ zlevel (float): Depth or Time (or whatever) value of the surface
1350
+
1351
+ Returns:
1352
+ Object instance updated
1353
+
1354
+ Example:
1355
+ Here the from_roxar method is used to initiate the object
1356
+ directly::
1357
+
1358
+ >>> mycube = xtgeo.cube_from_file(cube_dir + "/ib_test_cube2.segy")
1359
+ >>> mymap = RegularSurface._read_cube(mycube, 2700)
1360
+
1361
+ """
1362
+ props = [
1363
+ "ncol",
1364
+ "nrow",
1365
+ "xori",
1366
+ "yori",
1367
+ "xinc",
1368
+ "yinc",
1369
+ "rotation",
1370
+ "ilines",
1371
+ "xlines",
1372
+ "yflip",
1373
+ ]
1374
+
1375
+ input_dict = {key: deepcopy(getattr(cube, key)) for key in props}
1376
+
1377
+ input_dict["values"] = ma.array(
1378
+ np.full((input_dict["ncol"], input_dict["nrow"]), zlevel, dtype=np.float64)
1379
+ )
1380
+ return cls(**input_dict)
1381
+
1382
+ @classmethod
1383
+ def _read_grid3d(
1384
+ cls,
1385
+ grid: Grid,
1386
+ template: RegularSurface | str | None = None,
1387
+ where: str | int = "top",
1388
+ property: str | GridProperty = "depth",
1389
+ rfactor: int = 1,
1390
+ index_position: str = "center",
1391
+ ):
1392
+ """Private class method to extract a surface from a 3D grid."""
1393
+ args = _regsurf_grid3d.from_grid3d(
1394
+ grid, template, where, property, rfactor, index_position
1395
+ )
1396
+ return cls(**args)
1397
+
1398
+ def copy(self):
1399
+ """Deep copy of a RegularSurface object to another instance.
1400
+
1401
+ Example::
1402
+
1403
+ >>> mymap = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
1404
+ >>> mymapcopy = mymap.copy()
1405
+
1406
+ """
1407
+
1408
+ xsurf = RegularSurface(
1409
+ ncol=self.ncol,
1410
+ nrow=self.nrow,
1411
+ xinc=self.xinc,
1412
+ yinc=self.yinc,
1413
+ xori=self.xori,
1414
+ yori=self.yori,
1415
+ rotation=self.rotation,
1416
+ yflip=self.yflip,
1417
+ )
1418
+
1419
+ xsurf._values = self._values.copy()
1420
+ xsurf._isloaded = self._isloaded
1421
+
1422
+ xsurf.ilines = self._ilines.copy()
1423
+ xsurf.xlines = self._xlines.copy()
1424
+ xsurf.filesrc = self._filesrc
1425
+ xsurf.metadata.required = xsurf
1426
+
1427
+ return xsurf
1428
+
1429
+ def get_values1d(
1430
+ self, order="C", asmasked=False, fill_value=UNDEF, activeonly=False
1431
+ ):
1432
+ """Get a 1D numpy or masked array of the map values.
1433
+
1434
+ Args:
1435
+ order (str): Flatteting is in C (default) or F order
1436
+ asmasked (bool): If true, return as MaskedArray, other as standard
1437
+ numpy ndarray with undef as np.nan or fill_value
1438
+ fill_value (str): Relevent only if asmasked is False, this
1439
+ will be the value of undef entries
1440
+ activeonly (bool): If True, only active cells. Keys 'asmasked' and
1441
+ 'fill_value' are not revelant.
1442
+
1443
+ Returns:
1444
+ A numpy 1D array or MaskedArray
1445
+
1446
+ """
1447
+ val = self.values.copy()
1448
+
1449
+ if order == "F":
1450
+ val = ma.array(val.data, mask=val.mask, order="F")
1451
+
1452
+ val = val.ravel(order=order)
1453
+
1454
+ if activeonly:
1455
+ val = val[~val.mask]
1456
+
1457
+ if not asmasked and not activeonly:
1458
+ val = ma.filled(val, fill_value=fill_value)
1459
+
1460
+ return val
1461
+
1462
+ def set_values1d(self, val, order="C"):
1463
+ """Update the values attribute based on a 1D input, multiple options.
1464
+
1465
+ If values are np.nan or values are > UNDEF_LIMIT, they will be
1466
+ masked.
1467
+
1468
+ Args:
1469
+ val (list-like): Set values as a 1D array
1470
+ order (str): Input is C (default) or F order
1471
+ """
1472
+ if order == "F":
1473
+ val = np.copy(val, order="C")
1474
+
1475
+ val = val.reshape((self.ncol, self.nrow))
1476
+
1477
+ if not isinstance(val, ma.MaskedArray):
1478
+ val = ma.array(val)
1479
+
1480
+ val = ma.masked_greater(val, self.undef_limit)
1481
+ val = ma.masked_invalid(val)
1482
+
1483
+ self.values = val
1484
+
1485
+ def get_rotation(self):
1486
+ """Returns the surface roation, in degrees, from X, anti-clock."""
1487
+ return self._rotation
1488
+
1489
+ def get_nx(self):
1490
+ """Same as ncol (nx) (for backward compatibility)."""
1491
+ return self._ncol
1492
+
1493
+ def get_ny(self):
1494
+ """Same as nrow (ny) (for backward compatibility)."""
1495
+ return self._nrow
1496
+
1497
+ def get_xori(self):
1498
+ """Same as property xori (for backward compatibility)."""
1499
+ return self._xori
1500
+
1501
+ def get_yori(self):
1502
+ """Same as property yori (for backward compatibility)."""
1503
+ return self._yori
1504
+
1505
+ def get_xinc(self):
1506
+ """Same as property xinc (for backward compatibility)."""
1507
+ return self._xinc
1508
+
1509
+ def get_yinc(self):
1510
+ """Same as property yinc (for backward compatibility)."""
1511
+ return self._yinc
1512
+
1513
+ def similarity_index(self, other):
1514
+ """Report the degree of similarity between two maps, by comparing mean.
1515
+
1516
+ The method computes the average per surface, and the similarity
1517
+ is difference in mean divided on mean of self. I.e. values close
1518
+ to 0.0 mean small difference.
1519
+
1520
+ Args:
1521
+ other (surface object): The other surface to compare with
1522
+
1523
+ """
1524
+ ovalues = other.values
1525
+ svalues = self.values
1526
+
1527
+ diff = math.pow(svalues.mean() - ovalues.mean(), 2)
1528
+ diff = math.sqrt(diff)
1529
+
1530
+ try:
1531
+ diff = diff / svalues.mean()
1532
+ except ZeroDivisionError:
1533
+ diff = -999
1534
+
1535
+ return diff
1536
+
1537
+ def compare_topology(self, other: RegularSurface, strict: bool = True) -> bool:
1538
+ """Check that two object has the same topology, i.e. map definitions.
1539
+
1540
+ Map definitions such as origin, dimensions, number of defined cells...
1541
+
1542
+ Args:
1543
+ other: The other surface to compare with
1544
+ strict (bool): If false, the masks are not compared
1545
+
1546
+ Returns:
1547
+ True of same topology, False if not
1548
+ """
1549
+ tstatus = True
1550
+
1551
+ # consider refactor to getattr() instead!
1552
+ chklist = {
1553
+ "_ncol",
1554
+ "_nrow",
1555
+ "_xori",
1556
+ "_yori",
1557
+ "_xinc",
1558
+ "_yinc",
1559
+ "_rotation",
1560
+ }
1561
+ for skey, sval in self.__dict__.items():
1562
+ if skey in chklist:
1563
+ for okey, oval in other.__dict__.items():
1564
+ if skey == okey and sval != oval:
1565
+ logger.info("CMP %s: %s vs %s", skey, sval, oval)
1566
+ tstatus = False
1567
+
1568
+ if not tstatus:
1569
+ return False
1570
+
1571
+ # check that masks are equal
1572
+ mas1 = ma.getmaskarray(self.values)
1573
+ mas2 = ma.getmaskarray(other.values)
1574
+ if (
1575
+ strict
1576
+ and isinstance(mas1, np.ndarray)
1577
+ and isinstance(mas2, np.ndarray)
1578
+ and not np.array_equal(mas1, mas2)
1579
+ ):
1580
+ logger.warning("Masks differ, not consistent with 'strict'")
1581
+ return False
1582
+ return True
1583
+
1584
+ def swapaxes(self):
1585
+ """Swap (flip) the axes columns vs rows, keep origin but reverse yflip."""
1586
+ _regsurf_utils.swapaxes(self)
1587
+
1588
+ def make_lefthanded(self) -> None:
1589
+ """Makes the surface lefthanded in case yflip is -1. This will change origin.
1590
+
1591
+ Lefthanded regular maps are common in subsurface data, where I is to east, J is
1592
+ to north and Z axis is positive down for depth and time data.
1593
+
1594
+ The instance is changed in-place. The method is a no-op if yflip already is 1.
1595
+
1596
+ .. versionadded:: 4.2
1597
+ """
1598
+ _regsurf_utils.make_lefthanded(self)
1599
+
1600
+ def make_righthanded(self) -> None:
1601
+ """Makes the surface righthanded in case yflip is 1. This will change origin.
1602
+
1603
+ Righthanded regular maps are less common in subsurface data, where I is to
1604
+ east, J is to north and Z axis is positive down for depth and time data. This
1605
+ method is mainly for consistency since make_lefthanded() is present.
1606
+
1607
+ The instance is changed in-place. The method is a no-op if yflip already is -1.
1608
+
1609
+ .. versionadded:: 4.5
1610
+ """
1611
+ _regsurf_utils.make_righthanded(self)
1612
+
1613
+ def get_map_xycorners(self):
1614
+ """Get the X and Y coordinates of the map corners.
1615
+
1616
+ Returns a tuple on the form
1617
+ ((x0, y0), (x1, y1), (x2, y2), (x3, y3)) where
1618
+ (if unrotated and normal flip) 0 is the lower left
1619
+ corner, 1 is the right, 2 is the upper left, 3 is the upper right.
1620
+ """
1621
+ rot1 = self._rotation * math.pi / 180
1622
+ rot2 = rot1 + (math.pi / 2.0)
1623
+
1624
+ xc0 = self._xori
1625
+ yc0 = self._yori
1626
+
1627
+ xc1 = self._xori + (self.ncol - 1) * math.cos(rot1) * self._xinc
1628
+ yc1 = self._yori + (self.ncol - 1) * math.sin(rot1) * self._xinc
1629
+
1630
+ xc2 = self._xori + (self.nrow - 1) * math.cos(rot2) * self._yinc * self._yflip
1631
+ yc2 = self._yori + (self.nrow - 1) * math.sin(rot2) * self._yinc * self._yflip
1632
+
1633
+ xc3 = xc2 + (self.ncol - 1) * math.cos(rot1) * self._xinc
1634
+ yc3 = yc2 + (self.ncol - 1) * math.sin(rot1) * self._xinc
1635
+
1636
+ return ((xc0, yc0), (xc1, yc1), (xc2, yc2), (xc3, yc3))
1637
+
1638
+ def get_value_from_xy(self, point=(0.0, 0.0), sampling="bilinear"):
1639
+ """Return the map value given a X Y point.
1640
+
1641
+ Args:
1642
+ point (float tuple): Position of X and Y coordinate
1643
+ sampling (str): Sampling method, either "bilinear" for bilinear
1644
+ interpolation, or "nearest" for nearest node sampling (e.g. facies maps)
1645
+
1646
+ Returns:
1647
+ The map value (interpolated). None if XY is outside defined map
1648
+
1649
+ Example::
1650
+ mvalue = map.get_value_from_xy(point=(539291.12, 6788228.2))
1651
+
1652
+
1653
+ .. versionchanged:: 2.14 Added keyword option `sampling`
1654
+ """
1655
+ return _regsurf_oper.get_value_from_xy(self, point=point, sampling=sampling)
1656
+
1657
+ def get_xy_value_from_ij(self, iloc, jloc, zvalues=None):
1658
+ """Returns x, y, z(value) from a single i j location.
1659
+
1660
+ Args:
1661
+ iloc (int): I (col) location (base is 1)
1662
+ jloc (int): J (row) location (base is 1)
1663
+ zvalues (ndarray). If this is used in a loop it is wise
1664
+ to precompute the numpy surface once in the caller,
1665
+ and submit the numpy array (use surf.get_values1d()).
1666
+
1667
+ Returns:
1668
+ The x, y, z values at location iloc, jloc
1669
+ """
1670
+ xval, yval, value = _regsurf_oper.get_xy_value_from_ij(
1671
+ self, iloc, jloc, zvalues=zvalues
1672
+ )
1673
+
1674
+ return xval, yval, value
1675
+
1676
+ def get_ij_values(self, zero_based=False, asmasked=False, order="C"):
1677
+ """Return I J numpy 2D arrays, optionally as masked arrays.
1678
+
1679
+ Args:
1680
+ zero_based (bool): If False, first number is 1, not 0
1681
+ asmasked (bool): If True, UNDEF map nodes are skipped
1682
+ order (str): 'C' (default) or 'F' order (row vs column major)
1683
+ """
1684
+ return _regsurf_oper.get_ij_values(
1685
+ self, zero_based=zero_based, asmasked=asmasked, order=order
1686
+ )
1687
+
1688
+ def get_ij_values1d(self, zero_based=False, activeonly=True, order="C"):
1689
+ """Return I J numpy as 1D arrays.
1690
+
1691
+ Args:
1692
+ zero_based (bool): If False, first number is 1, not 0
1693
+ activeonly (bool): If True, UNDEF map nodes are skipped
1694
+ order (str): 'C' (default) or 'F' order (row vs column major)
1695
+ """
1696
+ return _regsurf_oper.get_ij_values1d(
1697
+ self, zero_based=zero_based, activeonly=activeonly, order=order
1698
+ )
1699
+
1700
+ def get_xy_values(self, order="C", asmasked=True):
1701
+ """Return coordinates for X and Y as numpy (masked) 2D arrays.
1702
+
1703
+ Args:
1704
+ order (str): 'C' (default) or 'F' order (row major vs column major)
1705
+ asmasked (bool): If True , inactive nodes are masked.
1706
+ """
1707
+ xvals, yvals = _regsurf_oper.get_xy_values(self, order=order, asmasked=asmasked)
1708
+
1709
+ return xvals, yvals
1710
+
1711
+ def get_xy_values1d(self, order="C", activeonly=True):
1712
+ """Return coordinates for X and Y as numpy 1D arrays.
1713
+
1714
+ Args:
1715
+ order (str): 'C' (default) or 'F' order (row major vs column major)
1716
+ activeonly (bool): Only active cells are returned.
1717
+ """
1718
+ xvals, yvals = _regsurf_oper.get_xy_values1d(
1719
+ self, order=order, activeonly=activeonly
1720
+ )
1721
+
1722
+ return xvals, yvals
1723
+
1724
+ def get_xyz_values(self):
1725
+ """Return coordinates for X Y and Z (values) as numpy (masked) 2D arrays."""
1726
+ xcoord, ycoord = self.get_xy_values(asmasked=True)
1727
+
1728
+ values = self.values.copy()
1729
+
1730
+ return xcoord, ycoord, values
1731
+
1732
+ def get_xyz_values1d(self, order="C", activeonly=True, fill_value=np.nan):
1733
+ """Return coordinates for X Y and Z (values) as numpy 1D arrays.
1734
+
1735
+ Args:
1736
+ order (str): 'C' (default) or 'F' order (row major vs column major)
1737
+ activeonly (bool): Only active cells are returned.
1738
+ fill_value (float): If activeonly is False, value of inactive nodes
1739
+ """
1740
+ xcoord, ycoord = self.get_xy_values1d(order=order, activeonly=activeonly)
1741
+
1742
+ values = self.get_values1d(
1743
+ order=order, asmasked=False, fill_value=fill_value, activeonly=activeonly
1744
+ )
1745
+
1746
+ return xcoord, ycoord, values
1747
+
1748
+ def get_dataframe(
1749
+ self, ijcolumns=False, ij=False, order="C", activeonly=True, fill_value=np.nan
1750
+ ):
1751
+ """Return a Pandas dataframe object, with columns X_UTME, Y_UTMN, VALUES.
1752
+
1753
+ Args:
1754
+ ijcolumns (bool): If True, and IX and JY indices will be
1755
+ added as dataframe columns. Redundant, use "ij" instead.
1756
+ ij (bool): Same as ijcolumns. If True, and IX and JY indices will be
1757
+ added as dataframe columns. Preferred syntax
1758
+ order (str): 'C' (default) for C order (row fastest), or 'F'
1759
+ for Fortran order (column fastest)
1760
+ activeonly (bool): If True, only active nodes are listed. If
1761
+ False, the values will have fill_value default None = NaN
1762
+ as values
1763
+ fill_value (float): Value of inactive nodes if activeonly is False
1764
+
1765
+ Example::
1766
+
1767
+ >>> import xtgeo
1768
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
1769
+ >>> dfr = surf.get_dataframe()
1770
+ >>> dfr.to_csv('somecsv.csv')
1771
+
1772
+ Returns:
1773
+ A Pandas dataframe object.
1774
+ """
1775
+ xcoord, ycoord, values = self.get_xyz_values1d(
1776
+ order=order, activeonly=activeonly, fill_value=fill_value
1777
+ )
1778
+
1779
+ entry = {}
1780
+
1781
+ if ijcolumns or ij:
1782
+ ixn, jyn = self.get_ij_values1d(order=order, activeonly=activeonly)
1783
+ entry["IX"] = ixn
1784
+ entry["JY"] = jyn
1785
+
1786
+ entry.update([("X_UTME", xcoord), ("Y_UTMN", ycoord), ("VALUES", values)])
1787
+
1788
+ return pd.DataFrame(entry)
1789
+
1790
+ def dataframe(self, **kwargs):
1791
+ """Deprecated; see method get_dataframe()."""
1792
+ warnings.warn(
1793
+ "The dataframe() is deprecated and will be removed in xtgeo "
1794
+ "version 5. Use get_dataframe() instead",
1795
+ PendingDeprecationWarning,
1796
+ )
1797
+
1798
+ return self.get_dataframe(**kwargs)
1799
+
1800
+ def get_xy_value_lists(self, lformat="webportal", xyfmt=None, valuefmt=None):
1801
+ """Returns two lists for coordinates (x, y) and values.
1802
+
1803
+ For lformat = 'webportal' (default):
1804
+
1805
+ The lists are returned as xylist and valuelist, where xylist
1806
+ is on the format:
1807
+
1808
+ [(x1, y1), (x2, y2) ...] (a list of x, y tuples)
1809
+
1810
+ and valuelist is one the format
1811
+
1812
+ [v1, v2, ...]
1813
+
1814
+ Inactive cells will be ignored.
1815
+
1816
+ Args:
1817
+ lformat (string): List return format ('webportal' is default,
1818
+ other options later)
1819
+ xyfmt (string): The formatter for xy numbers, e.g. '12.2f'
1820
+ (default None). Note no checks on valid input.
1821
+ valuefmt (string): The formatter for values e.g. '8.4f'
1822
+ (default None). Note no checks on valid input.
1823
+
1824
+ Returns:
1825
+ xylist, valuelist
1826
+
1827
+ Example:
1828
+
1829
+ >>> import xtgeo
1830
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
1831
+ >>> xylist, valuelist = surf.get_xy_value_lists(valuefmt='6.2f')
1832
+ """
1833
+ xylist = []
1834
+ valuelist = []
1835
+
1836
+ zvalues = self.get_values1d()
1837
+
1838
+ if lformat != "webportal":
1839
+ raise ValueError("Unsupported lformat")
1840
+
1841
+ for jnum in range(self.nrow):
1842
+ for inum in range(self.ncol):
1843
+ xcv, ycv, vcv = self.get_xy_value_from_ij(
1844
+ inum + 1, jnum + 1, zvalues=zvalues
1845
+ )
1846
+
1847
+ if vcv is not None:
1848
+ if xyfmt is not None:
1849
+ xcv = float(f"{xcv:{xyfmt}}")
1850
+ ycv = float(f"{ycv:{xyfmt}}")
1851
+ if valuefmt is not None:
1852
+ vcv = float(f"{vcv:{valuefmt}}")
1853
+ valuelist.append(vcv)
1854
+ xylist.append((xcv, ycv))
1855
+
1856
+ return xylist, valuelist
1857
+
1858
+ # ==================================================================================
1859
+ # Crop, interpolation, smooth or fill of values (possibly many methods here)
1860
+ # ==================================================================================
1861
+
1862
+ def autocrop(self):
1863
+ """Automatic cropping of the surface to minimize undefined areas.
1864
+
1865
+ This method is simply removing undefined "white areas". The
1866
+ instance will be updated with new values for xori, yori, ncol, etc. Rotation
1867
+ will never change
1868
+
1869
+ Returns:
1870
+ RegularSurface instance is updated in-place
1871
+
1872
+ .. versionadded:: 2.12
1873
+ """
1874
+ _regsurf_utils.autocrop(self)
1875
+
1876
+ def fill(self, fill_value=None):
1877
+ """Fast infilling of undefined values.
1878
+
1879
+ Note that minimum and maximum values will not change.
1880
+
1881
+ Algorithm if `fill_value` is not set is based on a nearest node extrapolation.
1882
+ Technically, ``scipy.ndimage.distance_transform_edt`` is applied. If fill_value
1883
+ is set by a scalar, that (constant) value be be applied
1884
+
1885
+ Args:
1886
+ fill_value (float): If defined, fills all undefined cells with that value.
1887
+
1888
+ Returns:
1889
+ RegularSurface instance is updated in-place
1890
+
1891
+ .. versionadded:: 2.1
1892
+ .. versionchanged:: 2.6 Added option key `fill_value`
1893
+ """
1894
+ _regsurf_gridding.surf_fill(self, fill_value=fill_value)
1895
+
1896
+ def smooth(
1897
+ self,
1898
+ method: Literal["median", "gaussian"] = "median",
1899
+ iterations: int = 1,
1900
+ width: float = 1,
1901
+ ) -> None:
1902
+ """Various smoothing methods for surfaces.
1903
+
1904
+ Args:
1905
+ method: Smoothing method (median or gaussian)
1906
+ iterations: Number of iterations
1907
+ width:
1908
+ - If method is 'median' range of influence is in nodes.
1909
+ - If method is 'gaussian' range of influence is standard
1910
+ deviation of the Gaussian kernel.
1911
+
1912
+ .. versionadded:: 2.1
1913
+ """
1914
+
1915
+ if method == "median":
1916
+ _regsurf_gridding._smooth(
1917
+ self,
1918
+ window_function=functools.partial(
1919
+ scipy.ndimage.median_filter, size=int(width)
1920
+ ),
1921
+ iterations=iterations,
1922
+ )
1923
+ elif method == "gaussian":
1924
+ _regsurf_gridding._smooth(
1925
+ self,
1926
+ window_function=functools.partial(
1927
+ scipy.ndimage.gaussian_filter, sigma=width
1928
+ ),
1929
+ iterations=iterations,
1930
+ )
1931
+ else:
1932
+ raise ValueError("Unsupported method for smoothing")
1933
+
1934
+ # ==================================================================================
1935
+ # Operation on map values (list to be extended)
1936
+ # ==================================================================================
1937
+
1938
+ def operation(self, opname, value):
1939
+ """Do operation on map values.
1940
+
1941
+ Do operations on the current map values. Valid operations are:
1942
+
1943
+ * 'elilt' or 'eliminatelessthan': Eliminate less than <value>
1944
+
1945
+ * 'elile' or 'eliminatelessequal': Eliminate less or equal than <value>
1946
+
1947
+ Args:
1948
+ opname (str): Name of operation. See list above.
1949
+ value (*): A scalar number (float) or a tuple of two floats,
1950
+ dependent on operation opname.
1951
+
1952
+ Examples::
1953
+
1954
+ surf.operation('elilt', 200) # set all values < 200 as undef
1955
+ """
1956
+ if opname in ("elilt", "eliminatelessthan"):
1957
+ self._values = ma.masked_less(self._values, value)
1958
+ elif opname in ("elile", "eliminatelessequal"):
1959
+ self._values = ma.masked_less_equal(self._values, value)
1960
+ else:
1961
+ raise ValueError("Invalid operation name")
1962
+
1963
+ # ==================================================================================
1964
+ # Operations restricted to inside/outside polygons
1965
+ # ==================================================================================
1966
+
1967
+ def operation_polygons(self, poly, value, opname="add", inside=True):
1968
+ """A generic function for map operations inside or outside polygon(s).
1969
+
1970
+ Args:
1971
+ poly (Polygons): A XTGeo Polygons instance
1972
+ value(float or RegularSurface): Value to add, subtract etc
1973
+ opname (str): Name of operation... 'add', 'sub', etc
1974
+ inside (bool): If True do operation inside polygons; else outside.
1975
+ _version (int): Algorithm version, 2 will be much faster when many points
1976
+ on polygons (this key will be removed in later versions and shall not
1977
+ be applied)
1978
+ """
1979
+ _regsurf_oper.operation_polygons(
1980
+ self, poly, value, opname=opname, inside=inside
1981
+ )
1982
+
1983
+ # shortforms
1984
+ def add_inside(self, poly, value):
1985
+ """Add a value (scalar or other map) inside polygons."""
1986
+ self.operation_polygons(poly, value, opname="add", inside=True)
1987
+
1988
+ def add_outside(self, poly, value):
1989
+ """Add a value (scalar or other map) outside polygons."""
1990
+ self.operation_polygons(poly, value, opname="add", inside=False)
1991
+
1992
+ def sub_inside(self, poly, value):
1993
+ """Subtract a value (scalar or other map) inside polygons."""
1994
+ self.operation_polygons(poly, value, opname="sub", inside=True)
1995
+
1996
+ def sub_outside(self, poly, value):
1997
+ """Subtract a value (scalar or other map) outside polygons."""
1998
+ self.operation_polygons(poly, value, opname="sub", inside=False)
1999
+
2000
+ def mul_inside(self, poly, value):
2001
+ """Multiply a value (scalar or other map) inside polygons."""
2002
+ self.operation_polygons(poly, value, opname="mul", inside=True)
2003
+
2004
+ def mul_outside(self, poly, value):
2005
+ """Multiply a value (scalar or other map) outside polygons."""
2006
+ self.operation_polygons(poly, value, opname="mul", inside=False)
2007
+
2008
+ def div_inside(self, poly, value):
2009
+ """Divide a value (scalar or other map) inside polygons."""
2010
+ self.operation_polygons(poly, value, opname="div", inside=True)
2011
+
2012
+ def div_outside(self, poly, value):
2013
+ """Divide a value (scalar or other map) outside polygons."""
2014
+ self.operation_polygons(poly, value, opname="div", inside=False)
2015
+
2016
+ def set_inside(self, poly, value):
2017
+ """Set a value (scalar or other map) inside polygons."""
2018
+ self.operation_polygons(poly, value, opname="set", inside=True)
2019
+
2020
+ def set_outside(self, poly, value):
2021
+ """Set a value (scalar or other map) outside polygons."""
2022
+ self.operation_polygons(poly, value, opname="set", inside=False)
2023
+
2024
+ def eli_inside(self, poly):
2025
+ """Eliminate current map values inside polygons."""
2026
+ self.operation_polygons(poly, 0, opname="eli", inside=True)
2027
+
2028
+ def eli_outside(self, poly):
2029
+ """Eliminate current map values outside polygons."""
2030
+ self.operation_polygons(poly, 0, opname="eli", inside=False)
2031
+
2032
+ # ==================================================================================
2033
+ # Operation with secondary map
2034
+ # ==================================================================================
2035
+
2036
+ def add(self, other):
2037
+ """Add another map to current map."""
2038
+ _regsurf_oper.operations_two(self, other, oper="add")
2039
+
2040
+ def subtract(self, other):
2041
+ """Subtract another map from current map."""
2042
+ _regsurf_oper.operations_two(self, other, oper="sub")
2043
+
2044
+ def multiply(self, other):
2045
+ """Multiply another map and current map."""
2046
+ _regsurf_oper.operations_two(self, other, oper="mul")
2047
+
2048
+ def divide(self, other):
2049
+ """Divide current map with another map."""
2050
+ _regsurf_oper.operations_two(self, other, oper="div")
2051
+
2052
+ # ==================================================================================
2053
+ # Interacion with points
2054
+ # ==================================================================================
2055
+
2056
+ def gridding(self, points, method="linear", coarsen=1):
2057
+ """Grid a surface from points.
2058
+
2059
+ Args:
2060
+ points(Points): XTGeo Points instance.
2061
+ method (str): Gridding method option: linear / cubic / nearest
2062
+ coarsen (int): Coarsen factor, to speed up gridding, but will
2063
+ give poorer result.
2064
+
2065
+ Example::
2066
+
2067
+ >>> import xtgeo
2068
+ >>> mypoints = xtgeo.Points(points_dir + '/pointset2.poi')
2069
+ >>> mysurf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
2070
+
2071
+ >>> # update the surface by gridding the points
2072
+ >>> mysurf.gridding(mypoints)
2073
+
2074
+ Raises:
2075
+ RuntimeError: If not possible to grid for some reason
2076
+ ValueError: If invalid input
2077
+
2078
+ """
2079
+ if not isinstance(points, Points):
2080
+ raise ValueError("Argument not a Points instance")
2081
+
2082
+ logger.info("Do gridding...")
2083
+
2084
+ _regsurf_gridding.points_gridding(self, points, coarsen=coarsen, method=method)
2085
+
2086
+ # ==================================================================================
2087
+ # Interacion with other surface
2088
+ # ==================================================================================
2089
+
2090
+ def resample(self, other, mask=True, sampling="bilinear"):
2091
+ """Resample an instance surface values from another surface instance.
2092
+
2093
+ Note that there may be some 'loss' of nodes at the edges of the
2094
+ updated map, as only the 'inside' nodes in the updated map
2095
+ versus the input map are applied.
2096
+
2097
+ The interpolation algorithm in resample is bilinear interpolation. The
2098
+ topolopogy of the surface (map definitions, rotation, ...) will not change,
2099
+ only the map values. Areas with undefined nodes in ``other`` will become
2100
+ undefined in the instance if mask is True; othewise they will be kept as is.
2101
+
2102
+ Args:
2103
+ other (RegularSurface): Surface to resample from.
2104
+ mask (bool): If True (default) nodes outside will be made undefined,
2105
+ if False then values will be kept as original
2106
+ sampling (str): Either 'bilinear' interpolation (default) or, 'nearest' for
2107
+ nearest node. The latter can be useful for resampling discrete maps.
2108
+
2109
+ Example::
2110
+
2111
+ # map with 230x210 columns, rotation 20
2112
+ surf1 = xtgeo.surface_from_file("some1.gri")
2113
+ # map with 270x190 columns, rotation 0
2114
+ surf2 = xtgeo.surface_from_file("some2.gri")
2115
+ # will sample (interpolate) surf2's values to surf1
2116
+ surf1.resample(surf2)
2117
+
2118
+ Returns:
2119
+ Instance's surface values will be updated in-place.
2120
+
2121
+
2122
+ .. versionchanged:: 2.9
2123
+ Added ``mask`` keyword, default is True for backward compatibility.
2124
+
2125
+ .. versionchanged:: 2.21
2126
+ Added ``sampling`` keyword option.
2127
+
2128
+ """
2129
+ if not isinstance(other, RegularSurface):
2130
+ raise ValueError("Argument not a RegularSurface instance")
2131
+
2132
+ logger.info("Do resampling...")
2133
+
2134
+ _regsurf_oper.resample(self, other, mask=mask, sampling=sampling)
2135
+
2136
+ # ==================================================================================
2137
+ # Change a surface more fundamentally
2138
+ # ==================================================================================
2139
+
2140
+ def unrotate(self, factor=2):
2141
+ r"""Unrotete a map instance, and this will also change nrow, ncol, xinc, etc.
2142
+
2143
+ The default sampling (factor=2) makes a finer grid in order to
2144
+ avoid artifacts, and this default can be used in most cases.
2145
+
2146
+ If an even finer grid is wanted, increase the factor. Theoretically the
2147
+ new increment for factor=N is between :math:`\\frac{1}{N}` and
2148
+ :math:`\\frac{1}{N}\\sqrt{2}` of the original increment,
2149
+ dependent on the rotation of the original surface.
2150
+
2151
+ If the current instance already is unrotated, nothing is done.
2152
+
2153
+ Args:
2154
+ factor (int): Refinement factor (>= 1)
2155
+
2156
+ """
2157
+ if abs(self.rotation) < 0.00001:
2158
+ logger.info("Surface has no rotation, nothing is done")
2159
+ return
2160
+
2161
+ if factor < 1:
2162
+ raise ValueError("Unrotate refinement factor cannot be be less than 1")
2163
+
2164
+ if not isinstance(factor, int):
2165
+ raise ValueError("Refinementfactor must an integer")
2166
+
2167
+ scopy = self
2168
+ if scopy._yflip < 0:
2169
+ scopy = self.copy()
2170
+ scopy.swapaxes()
2171
+
2172
+ xlen = scopy.xmax - scopy.xmin
2173
+ ylen = scopy.ymax - scopy.ymin
2174
+ ncol = scopy.ncol * factor
2175
+ nrow = scopy.nrow * factor
2176
+ xinc = xlen / (ncol - 1) # node based, not cell center based
2177
+ yinc = ylen / (nrow - 1)
2178
+ vals = ma.zeros((ncol, nrow), order="C")
2179
+
2180
+ nonrot = RegularSurface(
2181
+ xori=scopy.xmin,
2182
+ yori=scopy.ymin,
2183
+ xinc=xinc,
2184
+ yinc=yinc,
2185
+ ncol=ncol,
2186
+ nrow=nrow,
2187
+ values=vals,
2188
+ yflip=1,
2189
+ )
2190
+ nonrot.resample(scopy)
2191
+
2192
+ self._values = nonrot.values
2193
+ self._nrow = nonrot.nrow
2194
+ self._ncol = nonrot.ncol
2195
+ self._rotation = nonrot.rotation
2196
+ self._xori = nonrot.xori
2197
+ self._yori = nonrot.yori
2198
+ self._xinc = nonrot.xinc
2199
+ self._yinc = nonrot.yinc
2200
+ self._yflip = nonrot.yflip
2201
+ self._ilines = nonrot.ilines
2202
+ self._xlines = nonrot.xlines
2203
+
2204
+ def refine(self, factor):
2205
+ """Refine a surface with a factor.
2206
+
2207
+ Range for factor is 2 to 10.
2208
+
2209
+ Note that there may be some 'loss' of nodes at the edges of the
2210
+ updated map, as only the 'inside' nodes in the updated map
2211
+ versus the input map are applied.
2212
+
2213
+ Args:
2214
+ factor (int): Refinement factor
2215
+ """
2216
+ logger.info("Do refining...")
2217
+
2218
+ if not isinstance(factor, int):
2219
+ raise ValueError("Argument not a, Integer")
2220
+
2221
+ if factor < 2 or factor >= 10:
2222
+ raise ValueError("Argument exceeds range 2 .. 10")
2223
+
2224
+ xlen = self._xinc * (self._ncol - 1)
2225
+ ylen = self._yinc * (self._nrow - 1)
2226
+
2227
+ proxy = self.copy()
2228
+ self._ncol = proxy.ncol * factor
2229
+ self._nrow = proxy.nrow * factor
2230
+ self._xinc = xlen / (self._ncol - 1)
2231
+ self._yinc = ylen / (self._nrow - 1)
2232
+
2233
+ self._values = ma.zeros((self._ncol, self._nrow))
2234
+
2235
+ self._ilines = np.array(range(1, self._ncol + 1), dtype=np.int32)
2236
+ self._xlines = np.array(range(1, self._nrow + 1), dtype=np.int32)
2237
+
2238
+ self.resample(proxy)
2239
+
2240
+ del proxy
2241
+ logger.info("Do refining... done")
2242
+
2243
+ def coarsen(self, factor):
2244
+ """Coarsen a surface with a factor.
2245
+
2246
+ Range for coarsening is 2 to 10, where e.g. 2 meaning half the number of
2247
+ columns and rows.
2248
+
2249
+ Note that there may be some 'loss' of nodes at the edges of the
2250
+ updated map, as only the 'inside' nodes in the updated map
2251
+ versus the input map are applied.
2252
+
2253
+ Args:
2254
+ factor (int): Coarsen factor (2 .. 10)
2255
+
2256
+ Raises:
2257
+ ValueError: Coarsen is too large, giving too few nodes in result
2258
+ """
2259
+ logger.info("Do coarsening...")
2260
+ if not isinstance(factor, int):
2261
+ raise ValueError("Argument not a, Integer")
2262
+
2263
+ if factor < 2 or factor >= 10:
2264
+ raise ValueError("Argument exceeds range 2 .. 10")
2265
+
2266
+ proxy = self.copy()
2267
+ xlen = self._xinc * (self._ncol - 1)
2268
+ ylen = self._yinc * (self._nrow - 1)
2269
+
2270
+ ncol = int(round(proxy._ncol / factor))
2271
+ nrow = int(round(proxy._nrow / factor))
2272
+
2273
+ if ncol < 4 or nrow < 4:
2274
+ raise ValueError(
2275
+ "Coarsen is too large, giving ncol or nrow less than 4 nodes"
2276
+ )
2277
+
2278
+ self._ncol = ncol
2279
+ self._nrow = nrow
2280
+
2281
+ self._xinc = xlen / (self._ncol - 1)
2282
+ self._yinc = ylen / (self._nrow - 1)
2283
+
2284
+ self._values = ma.zeros((self._ncol, self._nrow))
2285
+
2286
+ self._ilines = np.array(range(1, self._ncol + 1), dtype=np.int32)
2287
+ self._xlines = np.array(range(1, self._nrow + 1), dtype=np.int32)
2288
+
2289
+ self.resample(proxy)
2290
+
2291
+ del proxy
2292
+ logger.info("Do coarsening... done")
2293
+
2294
+ # ==================================================================================
2295
+ # Interacion with a grid3d
2296
+ # ==================================================================================
2297
+
2298
+ def slice_grid3d(self, grid, prop, zsurf=None, sbuffer=1):
2299
+ """Slice the grid property and update the instance surface to sampled values.
2300
+
2301
+ Args:
2302
+ grid (Grid): Instance of a Grid.
2303
+ prop (GridProperty): Instance of a GridProperty, belongs to grid
2304
+ zsurf (surface object): Instance of map, which is used a slicer.
2305
+ If None, then the surface instance itself is used a slice
2306
+ criteria. Note that zsurf must have same map defs as the
2307
+ surface instance.
2308
+ sbuffer (int): Default is 1; if "holes" after sampling
2309
+ extend this to e.g. 3
2310
+ Example::
2311
+
2312
+ >>> import xtgeo
2313
+ >>> grd = xtgeo.grid_from_file(reek_dir + '/REEK.EGRID')
2314
+ >>> prop = xtgeo.gridproperty_from_file(
2315
+ ... reek_dir + '/REEK.UNRST',
2316
+ ... name='PRESSURE',
2317
+ ... date="first",
2318
+ ... grid=grd,
2319
+ ... )
2320
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
2321
+ >>> # update surf to sample the 3D grid property:
2322
+ >>> surf.slice_grid3d(grd, prop)
2323
+
2324
+ Raises:
2325
+ Exception if maps have different definitions (topology)
2326
+ """
2327
+ # TODO: Remove this when circular dependency untangled
2328
+ from xtgeo.grid3d.grid import Grid
2329
+
2330
+ if not isinstance(grid, Grid):
2331
+ raise ValueError("First argument must be a grid instance")
2332
+
2333
+ ier = _regsurf_grid3d.slice_grid3d(
2334
+ self, grid, prop, zsurf=zsurf, sbuffer=sbuffer
2335
+ )
2336
+
2337
+ if ier != 0:
2338
+ raise RuntimeError(
2339
+ "Wrong status from routine; something went wrong. Contact the author"
2340
+ )
2341
+
2342
+ # ==================================================================================
2343
+ # Interacion with a cube
2344
+ # ==================================================================================
2345
+
2346
+ def slice_cube(
2347
+ self,
2348
+ cube,
2349
+ zsurf=None,
2350
+ sampling="nearest",
2351
+ mask=True,
2352
+ snapxy=False,
2353
+ deadtraces=True,
2354
+ algorithm=2,
2355
+ ):
2356
+ """Slice the cube and update the instance surface to sampled cube values.
2357
+
2358
+ Args:
2359
+ cube (object): Instance of a Cube()
2360
+ zsurf (surface object): Instance of a depth (or time) map, which
2361
+ is the depth or time map (or...) that is used a slicer.
2362
+ If None, then the surface instance itself is used a slice
2363
+ criteria. Note that zsurf must have same map defs as the
2364
+ surface instance.
2365
+ sampling (str): 'nearest' for nearest node (default), or
2366
+ 'trilinear' for trilinear interpolation.
2367
+ mask (bool): If True (default), then the map values outside
2368
+ the cube will be undef. Otherwise, map will be kept as is.
2369
+ snapxy (bool): If True (optional), then the map values will get
2370
+ values at nearest Cube XY location. Only relevant to use if
2371
+ surface is derived from seismic coordinates (e.g. Auto4D).
2372
+ deadtraces (bool): If True (default) then dead cube traces
2373
+ (given as value 2 in SEGY trace headers), are treated as
2374
+ undefined, and map will become undefined at dead trace location.
2375
+ algorithm (int): 1 for legacy method, 2 (default from 2.9) for
2376
+ new method available in xtgeo from version 2.9
2377
+
2378
+ Example::
2379
+
2380
+ >>> import xtgeo
2381
+ >>> cube = xtgeo.cube_from_file(cube_dir + "/ib_test_cube2.segy")
2382
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
2383
+ >>> # update surf to sample cube values:
2384
+ >>> surf.slice_cube(cube)
2385
+
2386
+ Raises:
2387
+ Exception if maps have different definitions (topology)
2388
+ RuntimeWarning if number of sampled nodes is less than 10%
2389
+
2390
+ .. versionchanged:: 2.9 Added ``algorithm`` keyword, default is 2
2391
+ """
2392
+ scube = surface_from_cube(cube, 0)
2393
+ ier = _regsurf_cube.slice_cube(
2394
+ self,
2395
+ cube,
2396
+ scube,
2397
+ zsurf=zsurf,
2398
+ sampling=sampling,
2399
+ mask=mask,
2400
+ snapxy=snapxy,
2401
+ deadtraces=deadtraces,
2402
+ algorithm=algorithm,
2403
+ )
2404
+
2405
+ if ier == -4:
2406
+ xtg.warnuser("Number of sampled surface nodes < 10 percent of Cube nodes")
2407
+ print("Number of sampled surface nodes < 10 percent of Cube nodes")
2408
+ elif ier == -5:
2409
+ xtg.warn("No nodes sampled: map is 100 percent outside of cube?")
2410
+
2411
+ def slice_cube_window(
2412
+ self,
2413
+ cube: Cube,
2414
+ zsurf: Optional[RegularSurface] = None,
2415
+ other: Optional[RegularSurface] = None,
2416
+ other_position: str = "below",
2417
+ sampling: Literal["nearest", "cube", "trilinear"] = "nearest",
2418
+ mask: bool = True,
2419
+ zrange: Optional[float] = None,
2420
+ ndiv: Optional[int] = None,
2421
+ attribute: Union[List[ValidAttrs], ValidAttrs] = "max",
2422
+ maskthreshold: float = 0.1,
2423
+ snapxy: bool = False,
2424
+ showprogress: bool = False,
2425
+ deadtraces: bool = True,
2426
+ algorithm: Literal[1, 2, 3] = 2,
2427
+ ) -> Optional[Dict[RegularSurface]]:
2428
+ """Slice the cube within a vertical window and get the statistical attrubutes.
2429
+
2430
+ The statistical attributes can be min, max etc. Attributes are:
2431
+
2432
+ * 'max' for maximum
2433
+
2434
+ * 'min' for minimum
2435
+
2436
+ * 'rms' for root mean square
2437
+
2438
+ * 'mean' for expected value
2439
+
2440
+ * 'var' for variance (population var; https://en.wikipedia.org/wiki/Variance)
2441
+
2442
+ * 'maxpos' for maximum of positive values
2443
+
2444
+ * 'maxneg' for negative maximum of negative values
2445
+
2446
+ * 'maxabs' for maximum of absolute values
2447
+
2448
+ * 'sumpos' for sum of positive values using cube sampling resolution
2449
+
2450
+ * 'sumneg' for sum of negative values using cube sampling resolution
2451
+
2452
+ * 'meanabs' for mean of absolute values
2453
+
2454
+ * 'meanpos' for mean of positive values
2455
+
2456
+ * 'meanneg' for mean of negative values
2457
+
2458
+ Note that 'all' can be used to select all attributes that are currently
2459
+ available.
2460
+
2461
+ Args:
2462
+ cube: Instance of a Cube() here
2463
+ zsurf: Instance of a depth (or time) map, which
2464
+ is the depth or time map (or...) that is used a slicer.
2465
+ If None, then the surface instance itself is used a slice
2466
+ criteria. Note that zsurf must have same map defs as the
2467
+ surface instance.
2468
+ other: Instance of other surface if window is
2469
+ between surfaces instead of a static window. The zrange
2470
+ input is then not applied.
2471
+ sampling: 'nearest'/'trilinear'/'cube' for nearest node (default),
2472
+ or 'trilinear' for trilinear interpolation. The 'cube' option is
2473
+ only available with algorithm = 2 and will overrule ndiv and sample
2474
+ at the cube's Z increment resolution.
2475
+ mask: If True (default), then the map values outside
2476
+ the cube will be undef, otherwise map will be kept as-is
2477
+ zrange: The one-sided "radius" range of the window, e.g. 10
2478
+ (10 is default) units (e.g. meters if in depth mode).
2479
+ The full window is +- zrange (i.e. diameter).
2480
+ If other surface is present, zrange is computed based on those
2481
+ two surfaces instead.
2482
+ ndiv: Number of intervals for sampling within zrange. None
2483
+ means 'auto' sampling, using 0.5 of cube Z increment as basis. If
2484
+ algorithm = 2/3 and sampling is 'cube', the cube Z increment
2485
+ will be used.
2486
+ attribute: The requested attribute(s), e.g.
2487
+ 'max' value. May also be a list of attributes, e.g.
2488
+ ['min', 'rms', 'max']. By such, a dict of surface objects is
2489
+ returned. Note 'all' will make a list of all possible attributes.
2490
+ maskthreshold (float): Only if two surface; if isochore is less
2491
+ than given value, the result will be masked.
2492
+ snapxy: If True (optional), then the map values will get
2493
+ values at nearest Cube XY location, and the resulting surfaces layout
2494
+ map will be defined by the seismic layout. Quite relevant to use if
2495
+ surface is derived from seismic coordinates (e.g. Auto4D), but can be
2496
+ useful in other cases also, as long as one notes that map definition
2497
+ may change from input.
2498
+ showprogress: If True, then a progress is printed to stdout.
2499
+ deadtraces: If True (default) then dead cube traces
2500
+ (given as value 2 in SEGY trace headers), are treated as
2501
+ undefined, and map will be undefined at dead trace location.
2502
+ algorithm: 1 for legacy method, 2 (default) for new faster
2503
+ and more precise method available from xtgeo version 2.9, and
2504
+ algorithm 3 as new implementation from Sept. 2023 (v3.4)
2505
+
2506
+ Example::
2507
+
2508
+ >>> import xtgeo
2509
+ >>> cube = xtgeo.cube_from_file(cube_dir + "/ib_test_cube2.segy")
2510
+ >>> surf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
2511
+ >>> # update surf to sample cube values in a total range of 30 m:
2512
+ >>> surf.slice_cube_window(cube, attribute='min', zrange=15.0)
2513
+
2514
+ >>> # Here a list is given instead:
2515
+ >>> alst = ['min', 'max', 'rms']
2516
+
2517
+ >>> myattrs = surf.slice_cube_window(cube, attribute=alst, zrange=15.0)
2518
+ >>> for attr in myattrs.keys():
2519
+ ... _ = myattrs[attr].to_file(
2520
+ ... outdir + '/myfile_' + attr + '.gri'
2521
+ ... )
2522
+
2523
+ Raises:
2524
+ Exception if maps have different definitions (topology)
2525
+ ValueError if attribute is invalid.
2526
+
2527
+ Returns:
2528
+ If `attribute` is a string, then the instance is updated and
2529
+ None is returned. If `attribute` is a list, then a dictionary
2530
+ of surface objects is returned.
2531
+
2532
+ Note:
2533
+ This method is now deprecated and will be removed in xtgeo version 5.
2534
+ Please replace with :meth:`Cube().compute_attributes_in_window()` as soon
2535
+ as possible.
2536
+
2537
+
2538
+ .. versionchanged:: 2.9 Added ``algorithm`` keyword, default is now 2,
2539
+ while 1 is the legacy version
2540
+
2541
+ .. versionchanged:: 3.4 Added ``algorithm`` 3 which is more robust and
2542
+ hence recommended!
2543
+
2544
+ .. versionchanged:: 4.1 Flagged as deprecated.
2545
+
2546
+ """
2547
+
2548
+ warnings.warn(
2549
+ "This method is deprecated and will be removed in xtgeo version 5. "
2550
+ "It is strongly recommended to use `Cube().compute_attributes_in_window()` "
2551
+ "instead!",
2552
+ FutureWarning,
2553
+ )
2554
+
2555
+ if other is None and zrange is None:
2556
+ zrange = 10
2557
+
2558
+ if algorithm != 3:
2559
+ warnings.warn(
2560
+ "Other algorithms than no. 3 is not recommended, and will be "
2561
+ "removed in near future.",
2562
+ DeprecationWarning,
2563
+ )
2564
+
2565
+ scube = surface_from_cube(cube, 0)
2566
+ return _regsurf_cube_window.slice_cube_window(
2567
+ self,
2568
+ cube,
2569
+ scube,
2570
+ zsurf=zsurf,
2571
+ other=other,
2572
+ other_position=other_position,
2573
+ sampling=sampling,
2574
+ mask=mask,
2575
+ zrange=zrange,
2576
+ ndiv=ndiv,
2577
+ attribute=attribute,
2578
+ maskthreshold=maskthreshold,
2579
+ snapxy=snapxy,
2580
+ showprogress=showprogress,
2581
+ deadtraces=deadtraces,
2582
+ algorithm=algorithm,
2583
+ )
2584
+
2585
+ # ==================================================================================
2586
+ # Special methods
2587
+ # ==================================================================================
2588
+
2589
+ def get_boundary_polygons(
2590
+ self,
2591
+ alpha_factor: Optional[float] = 1.0,
2592
+ convex: Optional[bool] = False,
2593
+ simplify: Optional[bool] = True,
2594
+ ):
2595
+ """Extract boundary polygons from the surface.
2596
+
2597
+ A regular surface may often contain areas of undefined (masked) entries which
2598
+ makes the surface appear 'ragged' and/or 'patchy'.
2599
+
2600
+ This method extracts boundaries around the surface patches, and the
2601
+ precision depends on the keyword settings. As default, the ``alpha_factor``
2602
+ of 1 makes a precise boundary, while a larger alpha_factor makes more rough
2603
+ polygons.
2604
+
2605
+ .. image:: images/regsurf_boundary_polygons.png
2606
+ :width: 600
2607
+ :align: center
2608
+
2609
+ |
2610
+
2611
+ Args:
2612
+ alpha_factor: An alpha multiplier, where lowest allowed value is 1.0.
2613
+ A higher number will produce smoother and less accurate polygons. Not
2614
+ applied if convex is set to True.
2615
+ convex: The default is False, which means that a "concave hull" algorithm
2616
+ is used. If convex is True, the alpha factor is overridden to a large
2617
+ number, producing a 'convex' shape boundary instead.
2618
+ simplify: If True, a simplification is done in order to reduce the number
2619
+ of points in the polygons, where tolerance is 0.1. Another
2620
+ alternative to True is to input a Dict on the form
2621
+ ``{"tolerance": 2.0, "preserve_topology": True}``, cf. the
2622
+ :func:`Polygons.simplify()` method. For details on e.g. tolerance, see
2623
+ Shapely's simplify() method.
2624
+
2625
+ Returns:
2626
+ A XTGeo Polygons instance
2627
+
2628
+ Example::
2629
+
2630
+ surf = xtgeo.surface_from_file("mytop.gri")
2631
+ # eliminate all values below a depth, e.g. a fluid contact
2632
+ surf.values = np.ma.masked_greater(surf.values, 2100.0)
2633
+ boundary = surf.get_boundary_polygons()
2634
+ # the boundary may contain several smaller polygons; keep only the
2635
+ # largest (first) polygon which is number 0:
2636
+ boundary.filter_byid([0]) # polygon is updated in-place
2637
+
2638
+ See also:
2639
+ The :func:`Polygons.boundary_from_points()` class method.
2640
+
2641
+ .. versionadded:: 3.1
2642
+ """
2643
+ return _regsurf_boundary.create_boundary(self, alpha_factor, convex, simplify)
2644
+
2645
+ def get_fence(
2646
+ self, xyfence: np.ndarray, sampling: Optional[str] = "bilinear"
2647
+ ) -> np.ma.MaskedArray:
2648
+ """Sample the surface along X and Y positions (numpy arrays) and get Z.
2649
+
2650
+ .. versionchanged:: 2.14 Added keyword option `sampling`
2651
+
2652
+ Returns a masked numpy 2D array similar as input, but with updated
2653
+ Z values, which are masked if undefined.
2654
+
2655
+ Args:
2656
+ xyfence: A 2D numpy array with shape (N, 3) where columns
2657
+ are (X, Y, Z). The Z will be updated to the map.
2658
+ sampling: Use "bilinear" (default) for interpolation or "nearest" for
2659
+ snapping to nearest node.
2660
+
2661
+ """
2662
+ return _regsurf_oper.get_fence(self, xyfence, sampling=sampling)
2663
+
2664
+ def get_randomline(
2665
+ self,
2666
+ fencespec: Union[np.ndarray, object],
2667
+ hincrement: Optional[Union[bool, float]] = None,
2668
+ atleast: Optional[int] = 5,
2669
+ nextend: Optional[int] = 2,
2670
+ sampling: Optional[str] = "bilinear",
2671
+ ) -> np.ndarray:
2672
+ """Extract a line along a fencespec.
2673
+
2674
+ .. versionadded:: 2.1
2675
+ .. versionchanged:: 2.14 Added keyword option `sampling`
2676
+
2677
+ Here, horizontal axis is "length" and vertical axis is sampled depth, and
2678
+ this is used for fence plots.
2679
+
2680
+ The input fencespec is either a 2D numpy where each row is X, Y, Z, HLEN,
2681
+ where X, Y are UTM coordinates, Z is depth/time, and HLEN is a
2682
+ length along the fence, or a Polygons instance.
2683
+
2684
+ If input fencspec is a numpy 2D, it is important that the HLEN array
2685
+ has a constant increment and ideally a sampling that is less than the
2686
+ map resolution. If a Polygons() instance, this is automated if hincrement is
2687
+ None, and ignored if hincrement is False.
2688
+
2689
+ Returns a ndarray with shape (:, 2).
2690
+
2691
+ Args:
2692
+ fencespec:
2693
+ 2D numpy with X, Y, Z, HLEN as rows or a xtgeo Polygons() object.
2694
+ hincrement: Resampling horizontally. This applies only
2695
+ if the fencespec is a Polygons() instance. If None (default),
2696
+ the distance will be deduced automatically. If False, then it assumes
2697
+ the Polygons can be used as-is.
2698
+ atleast: Minimum number of horizontal samples (only if
2699
+ fencespec is a Polygons instance and hincrement != False)
2700
+ nextend: Extend with nextend * hincrement in both ends (only if
2701
+ fencespec is a Polygons instance and hincrement != False)
2702
+ sampling: Use "bilinear" (default) for interpolation or "nearest" for
2703
+ snapping to nearest node.
2704
+
2705
+
2706
+ Example::
2707
+
2708
+ fence = xtgeo.polygons_from_file("somefile.pol")
2709
+ fspec = fence.get_fence(distance=20, nextend=5, asnumpy=True)
2710
+ surf = xtgeo.surface_from_file("somefile.gri")
2711
+
2712
+ arr = surf.get_randomline(fspec)
2713
+
2714
+ distance = arr[:, 0]
2715
+ zval = arr[:, 1]
2716
+ # matplotlib...
2717
+ plt.plot(distance, zval)
2718
+
2719
+ .. seealso::
2720
+ Class :class:`~xtgeo.xyz.polygons.Polygons`
2721
+ The method :meth:`~xtgeo.xyz.polygons.Polygons.get_fence()` which can be
2722
+ used to pregenerate `fencespec`
2723
+ """
2724
+ return _regsurf_oper.get_randomline(
2725
+ self,
2726
+ fencespec,
2727
+ hincrement=hincrement,
2728
+ atleast=atleast,
2729
+ nextend=nextend,
2730
+ sampling=sampling,
2731
+ )
2732
+
2733
+ def hc_thickness_from_3dprops(
2734
+ self,
2735
+ xprop=None,
2736
+ yprop=None,
2737
+ hcpfzprop=None,
2738
+ zoneprop=None,
2739
+ zone_minmax=None,
2740
+ dzprop=None,
2741
+ zone_avg=False,
2742
+ coarsen=1,
2743
+ mask_outside=False,
2744
+ ):
2745
+ """Make a thickness weighted HC thickness map.
2746
+
2747
+ Make a HC thickness map based on numpy arrays of properties
2748
+ from a 3D grid. The numpy arrays here shall be ndarray,
2749
+ not masked numpies (MaskedArray).
2750
+
2751
+ Note that the input hcpfzprop is hydrocarbon fraction multiplied
2752
+ with thickness, which can be achieved by e.g.:
2753
+ cpfz = dz*poro*ntg*shc or by hcpfz = dz*hcpv/vbulk
2754
+
2755
+ Args:
2756
+ xprop (ndarray): 3D numpy array of X coordinates
2757
+ yprop (ndarray): 3D numpy array of Y coordinates
2758
+ hcpfzprop (ndarray): 3D numpy array of HC fraction multiplied
2759
+ with DZ per cell.
2760
+ zoneprop (ndarray): 3D numpy array indicating zonation
2761
+ property, where 1 is the lowest (0 values can be used to
2762
+ exclude parts of the grid)
2763
+ dzprop (ndarray): 3D numpy array holding DZ thickness. Will
2764
+ be applied in weighting if zone_avg is active.
2765
+ zone_minmax (tuple): (optional) 2 element list indicating start
2766
+ and stop zonation (both start and end spec are included)
2767
+ zone_avg (bool): A zone averaging is done prior to map gridding.
2768
+ This may speed up the process a lot, but result will be less
2769
+ precise. Default is False.
2770
+ coarsen (int): Select every N'th X Y point in the gridding. Will
2771
+ speed up process, but less precise result. Default=1
2772
+ mask_outside (bool): Will mask the result map undef where sum of DZ
2773
+ is zero. Default is False as it costs some extra CPU.
2774
+
2775
+ Returns:
2776
+ True if operation went OK (but check result!), False if not
2777
+ """
2778
+ for inum, myprop in enumerate([xprop, yprop, hcpfzprop, zoneprop]):
2779
+ if isinstance(myprop, ma.MaskedArray):
2780
+ raise ValueError(
2781
+ f"Property input {inum} with avg {myprop.mean()} to {__name__} "
2782
+ "is a masked array, not a plain numpy ndarray"
2783
+ )
2784
+
2785
+ status = _regsurf_gridding.avgsum_from_3dprops_gridding(
2786
+ self,
2787
+ summing=True,
2788
+ xprop=xprop,
2789
+ yprop=yprop,
2790
+ mprop=hcpfzprop,
2791
+ dzprop=dzprop,
2792
+ zoneprop=zoneprop,
2793
+ zone_minmax=zone_minmax,
2794
+ zone_avg=zone_avg,
2795
+ coarsen=coarsen,
2796
+ mask_outside=mask_outside,
2797
+ )
2798
+
2799
+ if status is False:
2800
+ raise RuntimeError("Failure from hc thickness calculation")
2801
+
2802
+ def avg_from_3dprop(
2803
+ self,
2804
+ xprop=None,
2805
+ yprop=None,
2806
+ mprop=None,
2807
+ dzprop=None,
2808
+ truncate_le=None,
2809
+ zoneprop=None,
2810
+ zone_minmax=None,
2811
+ coarsen=1,
2812
+ zone_avg=False,
2813
+ ):
2814
+ """Average map (DZ weighted) based on numpy arrays of properties from a 3D grid.
2815
+
2816
+ The 3D arrays mush be ordinary numpies of size (nx,ny,nz). Undef
2817
+ entries must be given weights 0 by using DZ=0
2818
+
2819
+ Args:
2820
+ xprop: 3D numpy of all X coordinates (also inactive cells)
2821
+ yprop: 3D numpy of all Y coordinates (also inactive cells)
2822
+ mprop: 3D numpy of requested property (e.g. porosity) all
2823
+ dzprop: 3D numpy of dz values (for weighting)
2824
+ NB zero for undef cells
2825
+ truncate_le (float): Optional. Truncate value (mask) if
2826
+ value is less
2827
+ zoneprop: 3D numpy to a zone property
2828
+ zone_minmax: a tuple with from-to zones to combine
2829
+ (e.g. (1,3))
2830
+
2831
+ Returns:
2832
+ Nothing explicit, but updates the surface object.
2833
+ """
2834
+ for inum, myprop in enumerate([xprop, yprop, mprop, dzprop, zoneprop]):
2835
+ if isinstance(myprop, ma.MaskedArray):
2836
+ raise ValueError(
2837
+ f"Property input {inum} with avg {myprop.mean()} to {__name__} "
2838
+ "is a masked array, not a plain numpy ndarray"
2839
+ )
2840
+
2841
+ _regsurf_gridding.avgsum_from_3dprops_gridding(
2842
+ self,
2843
+ summing=False,
2844
+ xprop=xprop,
2845
+ yprop=yprop,
2846
+ mprop=mprop,
2847
+ dzprop=dzprop,
2848
+ truncate_le=truncate_le,
2849
+ zoneprop=zoneprop,
2850
+ zone_minmax=zone_minmax,
2851
+ coarsen=coarsen,
2852
+ zone_avg=zone_avg,
2853
+ )
2854
+
2855
+ def quickplot(
2856
+ self,
2857
+ filename=None,
2858
+ title="QuickPlot for Surfaces",
2859
+ subtitle=None,
2860
+ infotext=None,
2861
+ minmax=(None, None),
2862
+ xlabelrotation=None,
2863
+ colormap="rainbow",
2864
+ faults=None,
2865
+ logarithmic=False,
2866
+ ):
2867
+ """Fast surface plot of maps using matplotlib.
2868
+
2869
+ Args:
2870
+ filename (str): Name of plot file; None will plot to screen.
2871
+ title (str): Title of plot
2872
+ subtitle (str): Subtitle of plot
2873
+ infotext (str): Additonal info on plot.
2874
+ minmax (tuple): Tuple of min and max values to be plotted. Note
2875
+ that values outside range will be set equal to range limits
2876
+ xlabelrotation (float): Rotation in degrees of X labels.
2877
+ colormap (str): Name of matplotlib or RMS file or XTGeo
2878
+ colormap. Default is matplotlib's 'rainbow'
2879
+ faults (dict): If fault plot is wanted, a dictionary on the
2880
+ form => {'faults': XTGeo Polygons object, 'color': 'k'}
2881
+ logarithmic (bool): If True, a logarithmic contouring color scale
2882
+ will be used.
2883
+
2884
+ """
2885
+ # This is using the more versatile Map class in XTGeo. Most kwargs
2886
+ # is just passed as is. Prefer using Map() directly in apps?
2887
+
2888
+ ncount = self.values.count()
2889
+ if ncount < 5:
2890
+ xtg.warn(f"None or too few map nodes for plotting. Skip output {filename}!")
2891
+ return
2892
+
2893
+ import xtgeoviz.plot
2894
+
2895
+ mymap = xtgeoviz.plot.Map()
2896
+
2897
+ logger.info("Infotext is <%s>", infotext)
2898
+ mymap.canvas(title=title, subtitle=subtitle, infotext=infotext)
2899
+
2900
+ minvalue = minmax[0]
2901
+ maxvalue = minmax[1]
2902
+
2903
+ mymap.colormap = colormap
2904
+
2905
+ mymap.plot_surface(
2906
+ self,
2907
+ minvalue=minvalue,
2908
+ maxvalue=maxvalue,
2909
+ xlabelrotation=xlabelrotation,
2910
+ logarithmic=logarithmic,
2911
+ )
2912
+ if faults:
2913
+ poly = faults.pop("faults")
2914
+ mymap.plot_faults(poly, **faults)
2915
+
2916
+ if filename is None:
2917
+ mymap.show()
2918
+ else:
2919
+ mymap.savefig(filename)
2920
+
2921
+ def distance_from_point(self, point=(0, 0), azimuth=0.0):
2922
+ """Make map values as horizontal distance from a point with azimuth direction.
2923
+
2924
+ Args:
2925
+ point (tuple): Point to measure from
2926
+ azimuth (float): Angle from North (clockwise) in degrees
2927
+
2928
+ """
2929
+ _regsurf_oper.distance_from_point(self, point=point, azimuth=azimuth)
2930
+
2931
+ def translate_coordinates(self, translate=(0, 0, 0)):
2932
+ """Translate a map in X Y VALUE space.
2933
+
2934
+ Args:
2935
+ translate (tuple): Translate (shift) distance in X Y Z
2936
+
2937
+ Example::
2938
+
2939
+ >>> import xtgeo
2940
+ >>> mysurf = xtgeo.surface_from_file(surface_dir + '/topreek_rota.gri')
2941
+ >>> print(mysurf.xori, mysurf.yori)
2942
+ 468895.125 5932889.5
2943
+ >>> mysurf.translate_coordinates((300,500,0))
2944
+ >>> print(mysurf.xori, mysurf.yori)
2945
+ 469195.125 5933389.5
2946
+
2947
+ """
2948
+ xshift, yshift, zshift = translate
2949
+
2950
+ # just shift the xori and yori
2951
+ self.xori = self.xori + xshift
2952
+ self.yori = self.yori + yshift
2953
+
2954
+ # note the Z coordinates are perhaps not depth
2955
+ # numpy operation:
2956
+ self.values = self.values + zshift
2957
+
2958
+ # ==================================================================================
2959
+ # Private
2960
+ # ==================================================================================
2961
+
2962
+ def _ensure_correct_values(
2963
+ self,
2964
+ values,
2965
+ force_dtype=None,
2966
+ ):
2967
+ """Ensures that values is a 2D masked numpy (ncol, nrow), C order.
2968
+
2969
+ This is an improved but private version over ensure_correct_values
2970
+
2971
+ Args:
2972
+ values (array-like or scalar): Values to process.
2973
+ force_dtype (numpy dtype or None): If not None, try to derive dtype from
2974
+ current values
2975
+
2976
+ Return:
2977
+ Nothing, self._values will be updated inplace
2978
+
2979
+ """
2980
+ apply_dtype = force_dtype if force_dtype else self.dtype
2981
+
2982
+ currentmask = None
2983
+ if isinstance(self.values, ma.MaskedArray):
2984
+ currentmask = ma.getmaskarray(self.values)
2985
+
2986
+ if isinstance(values, ma.MaskedArray):
2987
+ newmask = ma.getmaskarray(values)
2988
+ vals = values.astype(np.float64)
2989
+ vals = ma.masked_greater(vals, self.undef_limit)
2990
+ vals = ma.masked_invalid(vals)
2991
+ if (
2992
+ currentmask is not None
2993
+ and np.array_equal(currentmask, newmask)
2994
+ and self.values.shape == values.shape
2995
+ and values.flags.c_contiguous is True
2996
+ ):
2997
+ self._values *= 0
2998
+ self._values += vals
2999
+ else:
3000
+ vals = vals.reshape((self._ncol, self._nrow))
3001
+ if not vals.flags.c_contiguous:
3002
+ mask = ma.getmaskarray(values)
3003
+ mask = np.asanyarray(mask, order="C")
3004
+ vals = np.asanyarray(vals, order="C")
3005
+ vals = ma.array(vals, mask=mask, order="C")
3006
+ self._values = vals
3007
+
3008
+ elif isinstance(values, numbers.Number):
3009
+ if currentmask is not None:
3010
+ vals = np.ones(self.dimensions, dtype=apply_dtype) * values
3011
+ vals = np.ma.array(vals, mask=currentmask)
3012
+
3013
+ # there maybe cases where values scalar input is some kind of UNDEF
3014
+ # which will change the mask
3015
+ vals = ma.masked_greater(vals, self.undef_limit, copy=False)
3016
+ vals = ma.masked_invalid(vals, copy=False)
3017
+ self._values *= 0
3018
+ self._values += vals
3019
+ else:
3020
+ vals = ma.zeros((self.ncol, self.nrow), order="C", dtype=apply_dtype)
3021
+ self._values = vals + float(values)
3022
+
3023
+ elif isinstance(values, (list, tuple, np.ndarray)): # ie values ~ list-like
3024
+ vals = ma.array(values, order="C", dtype=apply_dtype)
3025
+ vals = ma.masked_greater(vals, self.undef_limit, copy=True)
3026
+ vals = ma.masked_invalid(vals, copy=True)
3027
+
3028
+ if vals.shape != (self.ncol, self.nrow):
3029
+ try:
3030
+ vals = ma.reshape(vals, (self.ncol, self.nrow), order="C")
3031
+ except ValueError as emsg:
3032
+ raise ValueError(f"Cannot reshape array: {values}") from emsg
3033
+
3034
+ self._values = vals
3035
+
3036
+ else:
3037
+ raise ValueError(f"Input values are in invalid format: {values}")
3038
+
3039
+ if self._values.mask is ma.nomask:
3040
+ self._values = ma.array(self._values, mask=ma.getmaskarray(self._values))
3041
+
3042
+ # ensure dtype; avoid allocation and ID change if possible by setting copy=False
3043
+ self._values = self._values.astype(apply_dtype, copy=False)