xtgeo 4.8.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xtgeo might be problematic. Click here for more details.

Files changed (117) hide show
  1. cxtgeo.py +582 -0
  2. cxtgeoPYTHON_wrap.c +20938 -0
  3. xtgeo/__init__.py +246 -0
  4. xtgeo/_cxtgeo.cp313-win_amd64.pyd +0 -0
  5. xtgeo/_internal.cp313-win_amd64.pyd +0 -0
  6. xtgeo/common/__init__.py +19 -0
  7. xtgeo/common/_angles.py +29 -0
  8. xtgeo/common/_xyz_enum.py +50 -0
  9. xtgeo/common/calc.py +396 -0
  10. xtgeo/common/constants.py +30 -0
  11. xtgeo/common/exceptions.py +42 -0
  12. xtgeo/common/log.py +93 -0
  13. xtgeo/common/sys.py +166 -0
  14. xtgeo/common/types.py +18 -0
  15. xtgeo/common/version.py +21 -0
  16. xtgeo/common/xtgeo_dialog.py +604 -0
  17. xtgeo/cube/__init__.py +9 -0
  18. xtgeo/cube/_cube_export.py +214 -0
  19. xtgeo/cube/_cube_import.py +532 -0
  20. xtgeo/cube/_cube_roxapi.py +180 -0
  21. xtgeo/cube/_cube_utils.py +287 -0
  22. xtgeo/cube/_cube_window_attributes.py +340 -0
  23. xtgeo/cube/cube1.py +1023 -0
  24. xtgeo/grid3d/__init__.py +15 -0
  25. xtgeo/grid3d/_ecl_grid.py +774 -0
  26. xtgeo/grid3d/_ecl_inte_head.py +148 -0
  27. xtgeo/grid3d/_ecl_logi_head.py +71 -0
  28. xtgeo/grid3d/_ecl_output_file.py +81 -0
  29. xtgeo/grid3d/_egrid.py +1004 -0
  30. xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
  31. xtgeo/grid3d/_grdecl_format.py +266 -0
  32. xtgeo/grid3d/_grdecl_grid.py +388 -0
  33. xtgeo/grid3d/_grid3d.py +29 -0
  34. xtgeo/grid3d/_grid3d_fence.py +181 -0
  35. xtgeo/grid3d/_grid3d_utils.py +228 -0
  36. xtgeo/grid3d/_grid_boundary.py +76 -0
  37. xtgeo/grid3d/_grid_etc1.py +1566 -0
  38. xtgeo/grid3d/_grid_export.py +221 -0
  39. xtgeo/grid3d/_grid_hybrid.py +66 -0
  40. xtgeo/grid3d/_grid_import.py +79 -0
  41. xtgeo/grid3d/_grid_import_ecl.py +101 -0
  42. xtgeo/grid3d/_grid_import_roff.py +135 -0
  43. xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
  44. xtgeo/grid3d/_grid_refine.py +125 -0
  45. xtgeo/grid3d/_grid_roxapi.py +292 -0
  46. xtgeo/grid3d/_grid_wellzone.py +165 -0
  47. xtgeo/grid3d/_gridprop_export.py +178 -0
  48. xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
  49. xtgeo/grid3d/_gridprop_import_grdecl.py +130 -0
  50. xtgeo/grid3d/_gridprop_import_roff.py +52 -0
  51. xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
  52. xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
  53. xtgeo/grid3d/_gridprop_op1.py +174 -0
  54. xtgeo/grid3d/_gridprop_roxapi.py +239 -0
  55. xtgeo/grid3d/_gridprop_value_init.py +140 -0
  56. xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
  57. xtgeo/grid3d/_gridprops_import_roff.py +83 -0
  58. xtgeo/grid3d/_roff_grid.py +469 -0
  59. xtgeo/grid3d/_roff_parameter.py +303 -0
  60. xtgeo/grid3d/grid.py +2537 -0
  61. xtgeo/grid3d/grid_properties.py +699 -0
  62. xtgeo/grid3d/grid_property.py +1341 -0
  63. xtgeo/grid3d/types.py +15 -0
  64. xtgeo/io/__init__.py +1 -0
  65. xtgeo/io/_file.py +592 -0
  66. xtgeo/metadata/__init__.py +17 -0
  67. xtgeo/metadata/metadata.py +431 -0
  68. xtgeo/roxutils/__init__.py +7 -0
  69. xtgeo/roxutils/_roxar_loader.py +54 -0
  70. xtgeo/roxutils/_roxutils_etc.py +122 -0
  71. xtgeo/roxutils/roxutils.py +207 -0
  72. xtgeo/surface/__init__.py +18 -0
  73. xtgeo/surface/_regsurf_boundary.py +26 -0
  74. xtgeo/surface/_regsurf_cube.py +210 -0
  75. xtgeo/surface/_regsurf_cube_window.py +391 -0
  76. xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
  77. xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
  78. xtgeo/surface/_regsurf_export.py +388 -0
  79. xtgeo/surface/_regsurf_grid3d.py +271 -0
  80. xtgeo/surface/_regsurf_gridding.py +347 -0
  81. xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
  82. xtgeo/surface/_regsurf_import.py +347 -0
  83. xtgeo/surface/_regsurf_lowlevel.py +122 -0
  84. xtgeo/surface/_regsurf_oper.py +631 -0
  85. xtgeo/surface/_regsurf_roxapi.py +241 -0
  86. xtgeo/surface/_regsurf_utils.py +81 -0
  87. xtgeo/surface/_surfs_import.py +43 -0
  88. xtgeo/surface/_zmap_parser.py +138 -0
  89. xtgeo/surface/regular_surface.py +2967 -0
  90. xtgeo/surface/surfaces.py +276 -0
  91. xtgeo/well/__init__.py +24 -0
  92. xtgeo/well/_blockedwell_roxapi.py +221 -0
  93. xtgeo/well/_blockedwells_roxapi.py +68 -0
  94. xtgeo/well/_well_aux.py +30 -0
  95. xtgeo/well/_well_io.py +327 -0
  96. xtgeo/well/_well_oper.py +574 -0
  97. xtgeo/well/_well_roxapi.py +304 -0
  98. xtgeo/well/_wellmarkers.py +486 -0
  99. xtgeo/well/_wells_utils.py +158 -0
  100. xtgeo/well/blocked_well.py +216 -0
  101. xtgeo/well/blocked_wells.py +122 -0
  102. xtgeo/well/well1.py +1514 -0
  103. xtgeo/well/wells.py +211 -0
  104. xtgeo/xyz/__init__.py +6 -0
  105. xtgeo/xyz/_polygons_oper.py +272 -0
  106. xtgeo/xyz/_xyz.py +741 -0
  107. xtgeo/xyz/_xyz_data.py +646 -0
  108. xtgeo/xyz/_xyz_io.py +490 -0
  109. xtgeo/xyz/_xyz_lowlevel.py +42 -0
  110. xtgeo/xyz/_xyz_oper.py +613 -0
  111. xtgeo/xyz/_xyz_roxapi.py +766 -0
  112. xtgeo/xyz/points.py +681 -0
  113. xtgeo/xyz/polygons.py +811 -0
  114. xtgeo-4.8.0.dist-info/METADATA +145 -0
  115. xtgeo-4.8.0.dist-info/RECORD +117 -0
  116. xtgeo-4.8.0.dist-info/WHEEL +5 -0
  117. xtgeo-4.8.0.dist-info/licenses/LICENSE.md +165 -0
xtgeo/well/well1.py ADDED
@@ -0,0 +1,1514 @@
1
+ """XTGeo well module, working with one single well."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from copy import deepcopy
7
+ from typing import TYPE_CHECKING
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from xtgeo import _cxtgeo
13
+ from xtgeo.common._xyz_enum import _AttrType
14
+ from xtgeo.common.constants import UNDEF, UNDEF_INT, UNDEF_LIMIT
15
+ from xtgeo.common.exceptions import InvalidFileFormatError
16
+ from xtgeo.common.log import null_logger
17
+ from xtgeo.common.xtgeo_dialog import XTGDescription
18
+ from xtgeo.io._file import FileFormat, FileWrapper
19
+ from xtgeo.metadata.metadata import MetaDataWell
20
+ from xtgeo.xyz import _xyz_data
21
+ from xtgeo.xyz.polygons import Polygons
22
+
23
+ from . import _well_aux, _well_io, _well_oper, _well_roxapi, _wellmarkers
24
+
25
+ if TYPE_CHECKING:
26
+ import io
27
+ from pathlib import Path
28
+
29
+ logger = null_logger(__name__)
30
+
31
+ # ======================================================================================
32
+ # Functions, as wrappers to class methods
33
+
34
+
35
+ def well_from_file(
36
+ wfile: str | Path,
37
+ fformat: str | None = "rms_ascii",
38
+ mdlogname: str | None = None,
39
+ zonelogname: str | None = None,
40
+ lognames: str | list[str] | None = "all",
41
+ lognames_strict: bool | None = False,
42
+ strict: bool | None = False,
43
+ ) -> Well:
44
+ """Make an instance of a Well directly from file import.
45
+
46
+ Args:
47
+ wfile: File path for well, either a string or a pathlib.Path instance
48
+ fformat: "rms_ascii" or "hdf5"
49
+ mdlogname: Name of Measured Depth log, if any
50
+ zonelogname: Name of Zonelog, if any
51
+ lognames: Name or list of lognames to import, default is "all"
52
+ lognames_strict: If True, all lognames must be present.
53
+ strict: If True, then import will fail if zonelogname or mdlogname are asked
54
+ for but those names are not present in wells.
55
+
56
+ Example::
57
+
58
+ >>> import xtgeo
59
+ >>> import pathlib
60
+ >>> welldir = pathlib.Path("../foo")
61
+ >>> mywell = xtgeo.well_from_file(welldir / "OP_1.w")
62
+
63
+ .. versionchanged:: 2.1 Added ``lognames`` and ``lognames_strict``
64
+ .. versionchanged:: 2.1 ``strict`` now defaults to False
65
+ """
66
+ return Well._read_file(
67
+ wfile,
68
+ fformat=fformat,
69
+ mdlogname=mdlogname,
70
+ zonelogname=zonelogname,
71
+ strict=strict,
72
+ lognames=lognames,
73
+ lognames_strict=lognames_strict,
74
+ )
75
+
76
+
77
+ def well_from_roxar(
78
+ project: str | object,
79
+ name: str,
80
+ trajectory: str | None = "Drilled trajectory",
81
+ logrun: str | None = "log",
82
+ lognames: str | list[str] | None = "all",
83
+ lognames_strict: bool | None = False,
84
+ inclmd: bool | None = False,
85
+ inclsurvey: bool | None = False,
86
+ ) -> Well:
87
+ """This makes an instance of a Well directly from Roxar RMS.
88
+
89
+ Note this method works only when inside RMS, or when RMS license is
90
+ activated (through the roxar environment).
91
+
92
+ Args:
93
+ project: Path to project or magic the ``project`` variable in RMS.
94
+ name: Name of Well, as shown in RMS.
95
+ trajectory: Name of trajectory in RMS.
96
+ logrun: Name of logrun in RMS.
97
+ lognames: List of lognames to import, or use 'all' for all present logs
98
+ lognames_strict: If True and log is not in lognames is a list, an Exception will
99
+ be raised.
100
+ inclmd: If True, a Measured Depth log will be included.
101
+ inclsurvey: If True, logs for azimuth and deviation will be included.
102
+
103
+ Returns:
104
+ Well instance.
105
+
106
+ Example::
107
+
108
+ # inside RMS:
109
+ import xtgeo
110
+ mylogs = ['ZONELOG', 'GR', 'Facies']
111
+ mywell = xtgeo.well_from_roxar(
112
+ project, "31_3-1", trajectory="Drilled", logrun="log", lognames=mylogs
113
+ )
114
+
115
+ .. versionchanged:: 2.1 lognames defaults to "all", not None
116
+ """
117
+ # TODO - mdlogname and zonelogname
118
+ return Well._read_roxar(
119
+ project,
120
+ name,
121
+ trajectory=trajectory,
122
+ logrun=logrun,
123
+ lognames=lognames,
124
+ lognames_strict=lognames_strict,
125
+ inclmd=inclmd,
126
+ inclsurvey=inclsurvey,
127
+ )
128
+
129
+
130
+ class Well:
131
+ """Class for a single well in the XTGeo framework.
132
+
133
+ The well logs are stored in a Pandas dataframe, which make manipulation
134
+ easy and fast.
135
+
136
+ The well trajectory are here represented as first 3 columns in the dataframe,
137
+ and XYZ have pre-defined names: ``X_UTME``, ``Y_UTMN``, ``Z_TVDSS``.
138
+
139
+ Other geometry logs may has also 'semi-defined' names, but this is not a strict
140
+ rule:
141
+
142
+ ``M_MDEPTH`` or ``Q_MDEPTH``: Measured depth, either real/true (M_xx) or
143
+ quasi computed/estimated (Q_xx). The Quasi may be incorrect for
144
+ all uses, but sufficient for some computations.
145
+
146
+ Similar for ``M_INCL``, ``Q_INCL``, ``M_AZI``, ``Q_ASI``.
147
+
148
+ All Pandas values (yes, discrete also!) are currently stored as float64
149
+ format, and undefined values are Nan. Integers are stored as Float due
150
+ to the (historic) lacking support for 'Integer Nan'.
151
+
152
+ Note there is a method that can return a dataframe (copy) with Integer
153
+ and Float columns, see :meth:`get_filled_dataframe`.
154
+
155
+ The instance can be made either from file or by specification::
156
+
157
+ >>> well1 = xtgeo.well_from_file(well_dir + '/OP_1.w')
158
+ >>> well2 = xtgeo.Well(rkb=32.0, xpos=1234.0, ypos=4567.0, wname="Foo",
159
+ df: mydataframe, ...)
160
+
161
+ Args:
162
+ rkb: Well RKB height
163
+ xpos: Well head X pos
164
+ ypos: Well head Y pos
165
+ wname: well name
166
+ df: A pandas dataframe with log values, expects columns to include
167
+ 'X_UTME', 'Y_UTMN', 'Z_TVDSS' for x, y and z coordinates.
168
+ Other columns should be log values.
169
+ filesrc: source file if any
170
+ mdlogname: Name of Measured Depth log, if any.
171
+ zonelogname: Name of Zonelog, if any
172
+ wlogtypes: dictionary of log types, 'DISC' (discrete) or 'CONT' (continuous),
173
+ defaults to to 'CONT'.
174
+ wlogrecords: dictionary of codes for 'DISC' logs, None for no codes given,
175
+ defaults to None.
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ rkb: float = 0.0,
181
+ xpos: float = 0.0,
182
+ ypos: float = 0.0,
183
+ wname: str = "",
184
+ df: pd.DataFrame | None = None,
185
+ mdlogname: str | None = None,
186
+ zonelogname: str | None = None,
187
+ wlogtypes: dict[str, str] | None = None,
188
+ wlogrecords: dict[str, str] | None = None,
189
+ filesrc: str | Path | None = None,
190
+ ):
191
+ # state variables from args
192
+ self._rkb = rkb
193
+ self._xpos = xpos
194
+ self._ypos = ypos
195
+ self._wname = wname
196
+ self._filesrc = filesrc
197
+ self._mdlogname = mdlogname
198
+ self._zonelogname = zonelogname
199
+
200
+ self._wdata = _xyz_data._XYZData(df, wlogtypes, wlogrecords)
201
+
202
+ self._ensure_consistency()
203
+
204
+ # additional state variables
205
+ self._metadata = MetaDataWell()
206
+ self._metadata.required = self
207
+
208
+ def __repr__(self): # noqa: D105
209
+ # should (in theory...) be able to newobject = eval(repr(thisobject))
210
+ return (
211
+ f"{self.__class__.__name__} (rkb={self._rkb}, xpos={self._xpos}, "
212
+ f"ypos={self._ypos}, wname='{self._wname}', "
213
+ f"filesrc='{self._filesrc}', mdlogname='{self._mdlogname}', "
214
+ f"zonelogname='{self._zonelogname}', \n"
215
+ f"wlogtypes='{self._wdata.attr_types}', "
216
+ f"\nwlogrecords='{self._wdata.attr_records}', "
217
+ f"df=\n{repr(self._wdata.data)}))"
218
+ )
219
+
220
+ def __str__(self): # noqa: D105
221
+ # user friendly print
222
+ return self.describe(flush=False)
223
+
224
+ def _ensure_consistency(self):
225
+ """Ensure consistency"""
226
+ self._wdata.ensure_consistency()
227
+
228
+ if self._mdlogname not in self._wdata.data:
229
+ self._mdlogname = None
230
+
231
+ if self._zonelogname not in self._wdata.data:
232
+ self._zonelogname = None
233
+
234
+ def ensure_consistency(self):
235
+ """Ensure consistency for the instance.
236
+
237
+ .. versionadded:: 3.5
238
+ """
239
+ # public version, added oct-23
240
+ self._ensure_consistency()
241
+
242
+ # ==================================================================================
243
+ # Properties
244
+ # ==================================================================================
245
+
246
+ @property
247
+ def xname(self):
248
+ """Return or set name of X coordinate column."""
249
+ return self._wdata.xname
250
+
251
+ @xname.setter
252
+ def xname(self, new_xname: str):
253
+ self._wdata.xname = new_xname
254
+
255
+ @property
256
+ def yname(self):
257
+ """Return or set name of Y coordinate column."""
258
+ return self._wdata.yname
259
+
260
+ @yname.setter
261
+ def yname(self, new_yname: str):
262
+ self._wdata.yname = new_yname
263
+
264
+ @property
265
+ def zname(self):
266
+ """Return or set name of Z coordinate column."""
267
+ return self._wdata.zname
268
+
269
+ @zname.setter
270
+ def zname(self, new_zname: str):
271
+ self._wdata.zname = new_zname
272
+
273
+ @property
274
+ def metadata(self):
275
+ """Return metadata object instance of type MetaDataRegularSurface."""
276
+ return self._metadata
277
+
278
+ @metadata.setter
279
+ def metadata(self, obj):
280
+ # The current metadata object can be replaced. This is a bit dangerous so
281
+ # further check must be done to validate. TODO.
282
+ if not isinstance(obj, MetaDataWell):
283
+ raise ValueError("Input obj not an instance of MetaDataRegularCube")
284
+
285
+ self._metadata = obj
286
+
287
+ @property
288
+ def rkb(self):
289
+ """Returns RKB height for the well (read only)."""
290
+ return self._rkb
291
+
292
+ @property
293
+ def xpos(self):
294
+ """Returns well header X position (read only)."""
295
+ return self._xpos
296
+
297
+ @property
298
+ def ypos(self) -> float:
299
+ """Returns well header Y position (read only)."""
300
+ return self._ypos
301
+
302
+ @property
303
+ def wellname(self):
304
+ """str: Returns well name, read only."""
305
+ return self._wname
306
+
307
+ @property
308
+ def name(self):
309
+ """Returns or set (rename) a well name."""
310
+ return self._wname
311
+
312
+ @name.setter
313
+ def name(self, newname):
314
+ self._wname = newname
315
+
316
+ # alias
317
+ wname = name
318
+
319
+ @property
320
+ def safewellname(self):
321
+ """Get well name on syntax safe form; '/' and spaces replaced with '_'."""
322
+ xname = self._wname
323
+ xname = xname.replace("/", "_")
324
+ return xname.replace(" ", "_")
325
+
326
+ @property
327
+ def xwellname(self):
328
+ """See safewellname."""
329
+ return self.safewellname
330
+
331
+ @property
332
+ def shortwellname(self):
333
+ """str: Well name on a short form where blockname/spaces removed (read only).
334
+
335
+ This should cope with both North Sea style and Haltenbanken style.
336
+
337
+ E.g.: '31/2-G-5 AH' -> 'G-5AH', '6472_11-F-23_AH_T2' -> 'F-23AHT2'
338
+
339
+ """
340
+ return self.get_short_wellname(self.wellname)
341
+
342
+ @property
343
+ def truewellname(self):
344
+ """Returns well name on the assummed form aka '31/2-E-4 AH2'."""
345
+ xname = self.xwellname
346
+ if "/" not in xname:
347
+ xname = xname.replace("_", "/", 1)
348
+ xname = xname.replace("_", " ")
349
+ return xname
350
+
351
+ @property
352
+ def mdlogname(self):
353
+ """str: Returns name of MD log, if any (None if missing)."""
354
+ return self._mdlogname
355
+
356
+ @mdlogname.setter
357
+ def mdlogname(self, mname):
358
+ if mname in self.get_lognames():
359
+ self._mdlogname = mname
360
+ else:
361
+ self._mdlogname = None
362
+
363
+ @property
364
+ def zonelogname(self):
365
+ """str: Returns or sets name of zone log, return None if missing."""
366
+ return self._zonelogname
367
+
368
+ @zonelogname.setter
369
+ def zonelogname(self, zname):
370
+ if zname in self.get_lognames():
371
+ self._zonelogname = zname
372
+ else:
373
+ self._zonelogname = None
374
+
375
+ @property
376
+ def dataframe(self):
377
+ """Returns or set the Pandas dataframe object for all logs."""
378
+ warnings.warn(
379
+ "Direct access to the dataframe property in Well class will be deprecated "
380
+ "in xtgeo 5.0. Use `get_dataframe()` instead.",
381
+ PendingDeprecationWarning,
382
+ )
383
+ return self._wdata.get_dataframe(copy=False) # get a view, for backward compat.
384
+
385
+ @dataframe.setter
386
+ def dataframe(self, dfr):
387
+ warnings.warn(
388
+ "Direct access to the dataframe property in Well class will be deprecated "
389
+ "in xtgeo 5.0. Use `set_dataframe()` instead.",
390
+ PendingDeprecationWarning,
391
+ )
392
+ self.set_dataframe(dfr) # this will include consistency checking!
393
+
394
+ @property
395
+ def nrow(self):
396
+ """int: Returns the Pandas dataframe object number of rows."""
397
+ return len(self._wdata.data.index)
398
+
399
+ @property
400
+ def ncol(self):
401
+ """int: Returns the Pandas dataframe object number of columns."""
402
+ return len(self._wdata.data.columns)
403
+
404
+ @property
405
+ def nlogs(self):
406
+ """int: Returns the Pandas dataframe object number of columns."""
407
+ return len(self._wdata.data.columns) - 3
408
+
409
+ @property
410
+ def lognames_all(self):
411
+ """list: Returns dataframe column names as list, including mandatory coords."""
412
+ return self.get_lognames()
413
+
414
+ @property
415
+ def lognames(self):
416
+ """list: Returns the Pandas dataframe column as list excluding coords."""
417
+ return list(self._wdata.data)[3:]
418
+
419
+ @property
420
+ def wlogtypes(self):
421
+ """Returns wlogtypes"""
422
+ return {name: atype.name for name, atype in self._wdata.attr_types.items()}
423
+
424
+ @property
425
+ def wlogrecords(self):
426
+ """Returns wlogrecords"""
427
+ return deepcopy(self._wdata.attr_records)
428
+
429
+ # ==================================================================================
430
+ # Methods
431
+ # ==================================================================================
432
+
433
+ @staticmethod
434
+ def get_short_wellname(wellname):
435
+ """Well name on a short name form where blockname and spaces are removed.
436
+
437
+ This should cope with both North Sea style and Haltenbanken style.
438
+ E.g.: '31/2-G-5 AH' -> 'G-5AH', '6472_11-F-23_AH_T2' -> 'F-23AHT2'
439
+ """
440
+ newname = []
441
+ first1 = False
442
+ first2 = False
443
+ for letter in wellname:
444
+ if first1 and first2:
445
+ newname.append(letter)
446
+ continue
447
+ if letter in ("_", "/"):
448
+ first1 = True
449
+ continue
450
+ if first1 and letter == "-":
451
+ first2 = True
452
+ continue
453
+
454
+ xname = "".join(newname)
455
+ xname = xname.replace("_", "")
456
+ return xname.replace(" ", "")
457
+
458
+ def describe(self, flush=True):
459
+ """Describe an instance by printing to stdout."""
460
+ dsc = XTGDescription()
461
+
462
+ dsc.title("Description of Well instance")
463
+ dsc.txt("Object ID", id(self))
464
+ dsc.txt("File source", self._filesrc)
465
+ dsc.txt("Well name", self._wname)
466
+ dsc.txt("RKB", self._rkb)
467
+ dsc.txt("Well head", self._xpos, self._ypos)
468
+ dsc.txt("Name of all columns", self.lognames_all)
469
+ dsc.txt("Name of log columns", self.lognames)
470
+ for wlog in self.lognames:
471
+ rec = self.get_logrecord(wlog)
472
+ if rec is not None and len(rec) > 3:
473
+ string = "("
474
+ nlen = len(rec)
475
+ for idx, (code, val) in enumerate(rec.items()):
476
+ if idx < 2:
477
+ string += f"{code}: {val} "
478
+ elif idx == nlen - 1:
479
+ string += f"... {code}: {val})"
480
+ else:
481
+ string = f"{rec}"
482
+ dsc.txt("Logname", wlog, self.get_logtype(wlog), string)
483
+
484
+ if flush:
485
+ dsc.flush()
486
+ return None
487
+
488
+ return dsc.astext()
489
+
490
+ @classmethod
491
+ def _read_file(
492
+ cls,
493
+ wfile: str | Path,
494
+ fformat: str | None = "rms_ascii",
495
+ **kwargs,
496
+ ):
497
+ """Import well from file.
498
+
499
+ Args:
500
+ wfile (str): Name of file as string or pathlib.Path
501
+ fformat (str): File format, rms_ascii (rms well) is
502
+ currently supported and default format.
503
+ mdlogname (str): Name of measured depth log, if any
504
+ zonelogname (str): Name of zonation log, if any
505
+ strict (bool): If True, then import will fail if
506
+ zonelogname or mdlogname are asked for but not present
507
+ in wells. If False, and e.g. zonelogname is not present, the
508
+ attribute ``zonelogname`` will be set to None.
509
+ lognames (str or list): Name or list of lognames to import, default is "all"
510
+ lognames_strict (bool): Flag to require all logs in lognames (unless "all")
511
+ or to just accept that subset that is present. Default is `False`.
512
+
513
+
514
+ Returns:
515
+ Object instance (optionally)
516
+
517
+ Example:
518
+ Here the from_file method is used to initiate the object
519
+ directly::
520
+
521
+ >>> mywell = Well().from_file(well_dir + '/OP_1.w')
522
+
523
+ .. versionchanged:: 2.1 ``lognames`` and ``lognames_strict`` added
524
+ .. versionchanged:: 2.1 ``strict`` now defaults to False
525
+ """
526
+
527
+ wfile = FileWrapper(wfile)
528
+ fmt = wfile.fileformat(fformat)
529
+
530
+ kwargs = _well_aux._data_reader_factory(fmt)(wfile, **kwargs)
531
+ return cls(**kwargs)
532
+
533
+ def to_file(
534
+ self,
535
+ wfile: str | Path | io.BytesIO,
536
+ fformat: str | None = "rms_ascii",
537
+ ):
538
+ """Export well to file or memory stream.
539
+
540
+ Args:
541
+ wfile: File name or stream.
542
+ fformat: File format ('rms_ascii'/'rmswell', 'hdf/hdf5/h5').
543
+
544
+ Example::
545
+
546
+ >>> xwell = Well(well_dir + '/OP_1.w')
547
+ >>> dfr = xwell.get_dataframe()
548
+ >>> dfr['Poro'] += 0.1
549
+ >>> xwell.set_dataframe(dfr)
550
+ >>> filename = xwell.to_file(outdir + "/somefile_copy.rmswell")
551
+
552
+ """
553
+ wfile = FileWrapper(wfile, mode="wb", obj=self)
554
+
555
+ wfile.check_folder(raiseerror=OSError)
556
+
557
+ self._ensure_consistency()
558
+
559
+ if not fformat or fformat in (
560
+ None,
561
+ "rms_ascii",
562
+ "rms_asc",
563
+ "rmsasc",
564
+ "rmswell",
565
+ ):
566
+ _well_io.export_rms_ascii(self, wfile.name)
567
+
568
+ elif fformat in FileFormat.HD5.value:
569
+ self.to_hdf(wfile)
570
+
571
+ else:
572
+ extensions = FileFormat.extensions_string([FileFormat.HDF])
573
+ raise InvalidFileFormatError(
574
+ f"File format {fformat} is invalid for a well type. "
575
+ f"Supported formats are {extensions}, 'rms_ascii', 'rms_asc', "
576
+ "'rmsasc', 'rmswell'."
577
+ )
578
+
579
+ return wfile.file
580
+
581
+ def to_hdf(
582
+ self,
583
+ wfile: str | Path,
584
+ compression: str | None = "lzf",
585
+ ) -> Path:
586
+ """Export well to HDF based file.
587
+
588
+ Warning:
589
+ This implementation is currently experimental and only recommended
590
+ for testing.
591
+
592
+ Args:
593
+ wfile: HDF File name to write to export to.
594
+
595
+ Returns:
596
+ A Path instance to actual file applied.
597
+
598
+ .. versionadded:: 2.14
599
+ """
600
+ wfile = FileWrapper(wfile, mode="wb", obj=self)
601
+
602
+ wfile.check_folder(raiseerror=OSError)
603
+
604
+ _well_io.export_hdf5_well(self, wfile, compression=compression)
605
+
606
+ return wfile.file
607
+
608
+ @classmethod
609
+ def _read_roxar(
610
+ cls,
611
+ project: str | object,
612
+ name: str,
613
+ trajectory: str | None = "Drilled trajectory",
614
+ logrun: str | None = "log",
615
+ lognames: str | list[str] | None = "all",
616
+ lognames_strict: bool | None = False,
617
+ inclmd: bool | None = False,
618
+ inclsurvey: bool | None = False,
619
+ ):
620
+ kwargs = _well_roxapi.import_well_roxapi(
621
+ project,
622
+ name,
623
+ trajectory=trajectory,
624
+ logrun=logrun,
625
+ lognames=lognames,
626
+ lognames_strict=lognames_strict,
627
+ inclmd=inclmd,
628
+ inclsurvey=inclsurvey,
629
+ )
630
+ return cls(**kwargs)
631
+
632
+ def to_roxar(self, *args, **kwargs):
633
+ """Export (save/store) a well to a roxar project.
634
+
635
+ Note this method works only when inside RMS, or when RMS license is
636
+ activated in terminal.
637
+
638
+ The current implementation will either update the existing well
639
+ (then well log array size must not change), or it will make a new well in RMS.
640
+
641
+
642
+ Args:
643
+ project (str, object): Magic string 'project' or file path to project
644
+ wname (str): Name of well, as shown in RMS.
645
+ lognames (:obj:list or :obj:str): List of lognames to save, or
646
+ use simply 'all' for current logs for this well. Default is 'all'
647
+ realisation (int): Currently inactive
648
+ trajectory (str): Name of trajectory in RMS, default is "Drilled trajectory"
649
+ logrun (str): Name of logrun in RMS, defaault is "log"
650
+ update_option (str): None | "overwrite" | "append". This only applies
651
+ when the well (wname) exists in RMS, and rules are based on name
652
+ matching. Default is None which means that all well logs in
653
+ RMS are emptied and then replaced with the content from xtgeo.
654
+ The "overwrite" option will replace logs in RMS with logs from xtgeo,
655
+ and append new if they do not exist in RMS. The
656
+ "append" option will only append logs if name does not exist in RMS
657
+ already. Reading only a subset of logs and then use "overwrite" or
658
+ "append" may speed up execution significantly.
659
+
660
+ Note:
661
+ When project is file path (direct access, outside RMS) then
662
+ ``to_roxar()`` will implicitly do a project save. Otherwise, the project
663
+ will not be saved until the user do an explicit project save action.
664
+
665
+ Example::
666
+
667
+ # assume that existing logs in RMS are ["PORO", "PERMH", "GR", "DT", "FAC"]
668
+ # read only one existing log (faster)
669
+
670
+ wll = xtgeo.well_from_roxar(project, "WELL1", lognames=["PORO"])
671
+ dfr = wll.get_dataframe()
672
+ dfr["PORO"] += 0.2 # add 0.2 to PORO log
673
+ wll.set_dataframe(dfr)
674
+ wll.create_log("NEW", value=0.333) # create a new log with constant value
675
+
676
+ # the "option" is a variable... for output, ``lognames="all"`` is default
677
+ if option is None:
678
+ # remove all current logs in RMS; only logs will be PORO and NEW
679
+ wll.to_roxar(project, "WELL1", update_option=option)
680
+ elif option == "overwrite":
681
+ # keep all original logs but update PORO and add NEW
682
+ wll.to_roxar(project, "WELL1", update_option=option)
683
+ elif option == "append":
684
+ # keep all original logs as they were (incl. PORO) and add NEW
685
+ wll.to_roxar(project, "WELL1", update_option=option)
686
+
687
+ Note:
688
+ The keywords ``lognames`` and ``update_option`` will interact
689
+
690
+ .. versionadded:: 2.12
691
+ .. versionchanged:: 2.15
692
+ Saving to new wells enabled (earlier only modifying existing)
693
+ .. versionchanged:: 3.5
694
+ Add key ``update_option``
695
+ """
696
+ # use *args, **kwargs since this method is overrided in blocked_well, and
697
+ # signature should be the same (TODO: change this to keywords; think this is
698
+ # a python 2.7 relict?)
699
+
700
+ project = args[0]
701
+ wname = args[1]
702
+ lognames = kwargs.get("lognames", "all")
703
+ trajectory = kwargs.get("trajectory", "Drilled trajectory")
704
+ logrun = kwargs.get("logrun", "log")
705
+ realisation = kwargs.get("realisation", 0)
706
+ update_option = kwargs.get("update_option")
707
+
708
+ logger.debug("Not in use: realisation %s", realisation)
709
+
710
+ _well_roxapi.export_well_roxapi(
711
+ self,
712
+ project,
713
+ wname,
714
+ lognames=lognames,
715
+ trajectory=trajectory,
716
+ logrun=logrun,
717
+ realisation=realisation,
718
+ update_option=update_option,
719
+ )
720
+
721
+ def get_lognames(self):
722
+ """Get the lognames for all logs."""
723
+ return list(self._wdata.data)
724
+
725
+ def get_wlogs(self) -> dict:
726
+ """Get a compound dictionary with well log metadata.
727
+
728
+ The result will be an dict on the form:
729
+
730
+ ``{"X_UTME": ["CONT", None], ... "Facies": ["DISC", {1: "BG", 2: "SAND"}]}``
731
+ """
732
+ res = {}
733
+
734
+ for key in self.get_lognames():
735
+ wtype = _AttrType.CONT.value
736
+ wrecord = None
737
+ if key in self._wdata.attr_types:
738
+ wtype = self._wdata.attr_types[key].name
739
+ if key in self._wdata.attr_records:
740
+ wrecord = self._wdata.attr_records[key]
741
+
742
+ res[key] = [wtype, wrecord]
743
+
744
+ return res
745
+
746
+ def set_wlogs(self, wlogs: dict):
747
+ """Set a compound dictionary with well log metadata.
748
+
749
+ This operation is somewhat risky as it may lead to inconsistency, so use with
750
+ care! Typically, one will use :meth:`get_wlogs` first and then modify some
751
+ attributes.
752
+
753
+ Args:
754
+ wlogs: Input data dictionary
755
+
756
+ Raises:
757
+ ValueError: Invalid log type found in input:
758
+ ValueError: Invalid log record found in input:
759
+ ValueError: Invalid input key found:
760
+ ValueError: Invalid log record found in input:
761
+
762
+ """
763
+ for key in self.get_lognames():
764
+ if key in wlogs:
765
+ typ, rec = wlogs[key]
766
+ self._wdata.set_attr_type(key, typ)
767
+ self._wdata.set_attr_record(key, deepcopy(rec))
768
+
769
+ self._ensure_consistency()
770
+
771
+ def isdiscrete(self, logname):
772
+ """Return True of log is discrete, otherwise False.
773
+
774
+ Args:
775
+ logname (str): Name of log to check if discrete or not
776
+
777
+ .. versionadded:: 2.2.0
778
+ """
779
+ return (
780
+ logname in self.get_lognames()
781
+ and self.get_logtype(logname) == _AttrType.DISC.value
782
+ )
783
+
784
+ def copy(self):
785
+ """Copy a Well instance to a new unique Well instance."""
786
+ return Well(
787
+ self.rkb,
788
+ self.xpos,
789
+ self.ypos,
790
+ self.wname,
791
+ self._wdata.data.copy(),
792
+ self.mdlogname,
793
+ self.zonelogname,
794
+ self.wlogtypes,
795
+ self.wlogrecords,
796
+ self._filesrc,
797
+ )
798
+
799
+ def rename_log(self, lname, newname):
800
+ """Rename a log, e.g. Poro to PORO."""
801
+ self._wdata.rename_attr(lname, newname)
802
+
803
+ if self._mdlogname == lname:
804
+ self._mdlogname = newname
805
+
806
+ if self._zonelogname == lname:
807
+ self._zonelogname = newname
808
+
809
+ def create_log(
810
+ self,
811
+ lname: str,
812
+ logtype: str = _AttrType.CONT.value,
813
+ logrecord: dict | None = None,
814
+ value: float = 0.0,
815
+ force: bool = True,
816
+ ) -> bool:
817
+ """Create a new log with initial values.
818
+
819
+ If the logname already exists, it will be silently overwritten, unless
820
+ the option force=False.
821
+
822
+ Args:
823
+ lname: name of new log
824
+ logtype: Must be 'CONT' (default) or 'DISC' (discrete)
825
+ logrecord: A dictionary of key: values for 'DISC' logs
826
+ value: initial value to set
827
+ force: If True, and lname exists, it will be overwritten, if
828
+ False, no new log will be made. Will return False.
829
+
830
+ Returns:
831
+ True ff a new log is made (either new or force overwrite an
832
+ existing) or False if the new log already exists,
833
+ and ``force=False``.
834
+
835
+ Note::
836
+
837
+ A new log can also be created by adding it to the dataframe directly, but
838
+ with less control over e.g. logrecord
839
+
840
+ """
841
+ return self._wdata.create_attr(lname, logtype, logrecord, value, force)
842
+
843
+ def copy_log(
844
+ self,
845
+ lname: str,
846
+ newname: str,
847
+ force: bool = True,
848
+ ) -> bool:
849
+ """Copy a log from an existing to a name
850
+
851
+ If the new log already exists, it will be silently overwritten, unless
852
+ the option force=False.
853
+
854
+ Args:
855
+ lname: name of existing log
856
+ newname: name of new log
857
+
858
+ Returns:
859
+ True if a new log is made (either new or force overwrite an
860
+ existing) or False if the new log already exists,
861
+ and ``force=False``.
862
+
863
+ Note::
864
+
865
+ A copy can also be done directly in the dataframe, but with less
866
+ consistency checks; hence this method is recommended
867
+
868
+ """
869
+ return self._wdata.copy_attr(lname, newname, force)
870
+
871
+ def delete_log(self, lname: str | list[str]) -> int:
872
+ """Delete/remove an existing log, or list of logs.
873
+
874
+ Will continue silently if a log does not exist.
875
+
876
+ Args:
877
+ lname: A logname or a list of lognames
878
+
879
+ Returns:
880
+ Number of logs deleted
881
+
882
+ Note::
883
+
884
+ A log can also be deleted by simply removing it from the dataframe.
885
+
886
+ """
887
+ logger.debug("Deleting log(s) %s...", lname)
888
+ return self._wdata.delete_attr(lname)
889
+
890
+ delete_logs = delete_log # alias function
891
+
892
+ def get_logtype(self, lname) -> str | None:
893
+ """Returns the type of a given log (e.g. DISC or CONT), None if not present."""
894
+ if lname in self._wdata.attr_types:
895
+ return self._wdata.attr_types[lname].name
896
+ return None
897
+
898
+ def set_logtype(self, lname, ltype):
899
+ """Sets the type of a give log (e.g. DISC or CONT)."""
900
+ self._wdata.set_attr_type(lname, ltype)
901
+
902
+ def get_logrecord(self, lname):
903
+ """Returns the record (dict) of a given log name, None if not exists."""
904
+
905
+ return self._wdata.get_attr_record(lname)
906
+
907
+ def set_logrecord(self, lname, newdict):
908
+ """Sets the record (dict) of a given discrete log."""
909
+ self._wdata.set_attr_record(lname, newdict)
910
+
911
+ def get_logrecord_codename(self, lname, key):
912
+ """Returns the name entry of a log record, for a given key.
913
+
914
+ Example::
915
+
916
+ # get the name for zonelog entry no 4:
917
+ zname = well.get_logrecord_codename('ZONELOG', 4)
918
+ """
919
+ zlogdict = self.get_logrecord(lname)
920
+ if key in zlogdict:
921
+ return zlogdict[key]
922
+
923
+ return None
924
+
925
+ def get_dataframe(self, copy: bool = True):
926
+ """Get a copy (default) or a view of the dataframe.
927
+
928
+ Args:
929
+ copy: If True, return a deep copy. A view (copy=False) will be faster and
930
+ more memory efficient, but less "safe" for some cases when manipulating
931
+ dataframes.
932
+
933
+ .. versionchanged:: 3.7 Added `copy` keyword
934
+ """
935
+ return self._wdata.get_dataframe(copy=copy)
936
+
937
+ def get_filled_dataframe(self, fill_value=UNDEF, fill_value_int=UNDEF_INT):
938
+ """Fill the Nan's in the dataframe with real UNDEF values.
939
+
940
+ This module returns a copy of the dataframe in the object; it
941
+ does not change the instance.
942
+
943
+ Note that DISC logs will be casted to columns with integer
944
+ as datatype.
945
+
946
+ Returns:
947
+ A pandas dataframe where Nan er replaces with preset
948
+ high XTGeo UNDEF values, or user defined values.
949
+
950
+ """
951
+ return self._wdata.get_dataframe_copy(
952
+ infer_dtype=True,
953
+ filled=True,
954
+ fill_value=fill_value,
955
+ fill_value_int=fill_value_int,
956
+ )
957
+
958
+ def set_dataframe(self, dfr):
959
+ """Set the dataframe."""
960
+ self._wdata.set_dataframe(dfr)
961
+
962
+ def create_relative_hlen(self):
963
+ """Make a relative length of a well, as a log.
964
+
965
+ The first well og entry defines zero, then the horizontal length
966
+ is computed relative to that by simple geometric methods.
967
+ """
968
+ self._wdata.create_relative_hlen()
969
+
970
+ def geometrics(self):
971
+ """Compute some well geometrical arrays MD, INCL, AZI, as logs.
972
+
973
+ These are kind of quasi measurements hence the logs will named
974
+ with a Q in front as Q_MDEPTH, Q_INCL, and Q_AZI.
975
+
976
+ These logs will be added to the dataframe. If the mdlogname
977
+ attribute does not exist in advance, it will be set to 'Q_MDEPTH'.
978
+
979
+ Returns:
980
+ False if geometrics cannot be computed
981
+
982
+ """
983
+ rvalue = self._wdata.geometrics()
984
+
985
+ if not self._mdlogname:
986
+ self._mdlogname = "Q_MDEPTH"
987
+
988
+ return rvalue
989
+
990
+ def truncate_parallel_path(
991
+ self, other, xtol=None, ytol=None, ztol=None, itol=None, atol=None
992
+ ):
993
+ """Truncate the part of the well trajectory that is ~parallel with other.
994
+
995
+ Args:
996
+ other (Well): Other well to compare with
997
+ xtol (float): Tolerance in X (East) coord for measuring unit
998
+ ytol (float): Tolerance in Y (North) coord for measuring unit
999
+ ztol (float): Tolerance in Z (TVD) coord for measuring unit
1000
+ itol (float): Tolerance in inclination (degrees)
1001
+ atol (float): Tolerance in azimuth (degrees)
1002
+ """
1003
+ if xtol is None:
1004
+ xtol = 0.0
1005
+ if ytol is None:
1006
+ ytol = 0.0
1007
+ if ztol is None:
1008
+ ztol = 0.0
1009
+ if itol is None:
1010
+ itol = 0.0
1011
+ if atol is None:
1012
+ atol = 0.0
1013
+
1014
+ this_df = self.get_dataframe()
1015
+ other_df = other.get_dataframe()
1016
+
1017
+ if this_df.shape[0] < 3 or other_df.shape[0] < 3:
1018
+ raise ValueError(
1019
+ f"Too few points to truncate parallel path, was "
1020
+ f"{this_df.size} and {other_df.size}, must be >3"
1021
+ )
1022
+
1023
+ # extract numpies from XYZ trajectory logs
1024
+ xv1 = self._wdata.data[self.xname].values
1025
+ yv1 = self._wdata.data[self.yname].values
1026
+ zv1 = self._wdata.data[self.zname].values
1027
+
1028
+ xv2 = other_df[self.xname].values
1029
+ yv2 = other_df[self.yname].values
1030
+ zv2 = other_df[self.zname].values
1031
+
1032
+ ier = _cxtgeo.well_trunc_parallel(
1033
+ xv1, yv1, zv1, xv2, yv2, zv2, xtol, ytol, ztol, itol, atol, 0
1034
+ )
1035
+
1036
+ if ier != 0:
1037
+ raise RuntimeError("Unexpected error")
1038
+
1039
+ dfr = self.get_dataframe()
1040
+ dfr = dfr[dfr[self.xname] < UNDEF_LIMIT]
1041
+ self.set_dataframe(dfr)
1042
+
1043
+ def may_overlap(self, other):
1044
+ """Consider if well overlap in X Y coordinates with other well, True/False."""
1045
+ dataframe = self.get_dataframe()
1046
+ other_dataframe = other.get_dataframe()
1047
+
1048
+ if dataframe.size < 2 or other_dataframe.size < 2:
1049
+ return False
1050
+
1051
+ # extract numpies from XYZ trajectory logs
1052
+ xmin1 = np.nanmin(dataframe[self.xname].values)
1053
+ xmax1 = np.nanmax(dataframe[self.xname].values)
1054
+ ymin1 = np.nanmin(dataframe[self.yname].values)
1055
+ ymax1 = np.nanmax(dataframe[self.yname].values)
1056
+
1057
+ xmin2 = np.nanmin(other_dataframe[self.xname].values)
1058
+ xmax2 = np.nanmax(other_dataframe[self.xname].values)
1059
+ ymin2 = np.nanmin(other_dataframe[self.yname].values)
1060
+ ymax2 = np.nanmax(other_dataframe[self.yname].values)
1061
+
1062
+ if xmin1 > xmax2 or ymin1 > ymax2:
1063
+ return False
1064
+ return not (xmin2 > xmax1 or ymin2 > ymax1)
1065
+
1066
+ def limit_tvd(self, tvdmin, tvdmax):
1067
+ """Truncate the part of the well that is outside tvdmin, tvdmax.
1068
+
1069
+ Range will be in tvdmin <= tvd <= tvdmax.
1070
+
1071
+ Args:
1072
+ tvdmin (float): Minimum TVD
1073
+ tvdmax (float): Maximum TVD
1074
+ """
1075
+ dfr = self.get_dataframe()
1076
+ dfr = dfr[dfr[self.zname] >= tvdmin]
1077
+ dfr = dfr[dfr[self.zname] <= tvdmax]
1078
+ self.set_dataframe(dfr)
1079
+
1080
+ def downsample(self, interval=4, keeplast=True):
1081
+ """Downsample by sampling every N'th element (coarsen only).
1082
+
1083
+ Args:
1084
+ interval (int): Sampling interval.
1085
+ keeplast (bool): If True, the last element from the original
1086
+ dataframe is kept, to avoid that the well is shortened.
1087
+ """
1088
+ dataframe = self.get_dataframe()
1089
+
1090
+ if dataframe.size < 2 * interval:
1091
+ return
1092
+
1093
+ dfr = dataframe[::interval].copy()
1094
+
1095
+ if keeplast:
1096
+ dfr = pd.concat([dfr, dataframe.iloc[-1:]], ignore_index=True)
1097
+
1098
+ self.set_dataframe(dfr.reset_index(drop=True))
1099
+
1100
+ def rescale(self, delta=0.15, tvdrange=None):
1101
+ """Rescale (refine or coarse) by sampling a delta along the trajectory, in MD.
1102
+
1103
+ Args:
1104
+ delta (float): Step length
1105
+ tvdrange (tuple of floats): Resampling can be limited to TVD interval
1106
+
1107
+ .. versionchanged:: 2.2 Added tvdrange
1108
+ """
1109
+ _well_oper.rescale(self, delta=delta, tvdrange=tvdrange)
1110
+
1111
+ def get_polygons(self, skipname=False):
1112
+ """Return a Polygons object from the well trajectory.
1113
+
1114
+ Args:
1115
+ skipname (bool): If True then name column is omitted
1116
+
1117
+ .. versionadded:: 2.1
1118
+ .. versionchanged:: 2.13 Added `skipname` key
1119
+ """
1120
+ dfr = self._wdata.data.copy()
1121
+
1122
+ keep = (self.xname, self.yname, self.zname)
1123
+ for col in dfr.columns:
1124
+ if col not in keep:
1125
+ dfr.drop(labels=col, axis=1, inplace=True)
1126
+ dfr["POLY_ID"] = 1
1127
+
1128
+ if not skipname:
1129
+ dfr["NAME"] = self.xwellname
1130
+ poly = Polygons()
1131
+ poly.set_dataframe(dfr)
1132
+ poly.name = self.xwellname
1133
+
1134
+ return poly
1135
+
1136
+ def get_fence_polyline(self, sampling=20, nextend=2, tvdmin=None, asnumpy=True):
1137
+ """Return a fence polyline as a numpy array or a Polygons object.
1138
+
1139
+ The result will aim for a regular sampling interval, useful for extracting
1140
+ fence plots (cross-sections).
1141
+
1142
+ Args:
1143
+ sampling (float): Sampling interval i.e. horizonal distance (input)
1144
+ nextend (int): Number if sampling to extend; e.g. 2 * 20
1145
+ tvdmin (float): Minimum TVD starting point.
1146
+ as_numpy (bool): If True, a numpy array, otherwise a Polygons
1147
+ object with 5 columns where the 2 last are HLEN and POLY_ID
1148
+ and the POLY_ID will be set to 0.
1149
+
1150
+ Returns:
1151
+ A numpy array of shape (NLEN, 5) in F order,
1152
+ Or a Polygons object with 5 columns
1153
+ If not possible, return False
1154
+
1155
+ .. versionchanged:: 2.1 improved algorithm
1156
+ """
1157
+ poly = self.get_polygons()
1158
+
1159
+ if tvdmin is not None:
1160
+ poly_df = poly.get_dataframe()
1161
+ poly_df = poly_df[poly_df[poly.zname] >= tvdmin]
1162
+ poly_df.reset_index(drop=True, inplace=True)
1163
+ poly.set_dataframe(poly_df)
1164
+
1165
+ return poly.get_fence(distance=sampling, nextend=nextend, asnumpy=asnumpy)
1166
+
1167
+ def create_surf_distance_log(
1168
+ self,
1169
+ surf: object,
1170
+ name: str | None = "DIST_SURF",
1171
+ ):
1172
+ """Make a log that is vertical distance to a regular surface.
1173
+
1174
+ If the trajectory is above the surface (i.e. more shallow), then the
1175
+ distance sign is positive.
1176
+
1177
+ Args:
1178
+ surf: The RegularSurface instance.
1179
+ name: The name of the new log. If it exists it will be overwritten.
1180
+
1181
+ Example::
1182
+
1183
+ mywell.rescale() # optional
1184
+ thesurf = xtgeo.surface_from_file("some.gri")
1185
+ mywell.create_surf_distance_log(thesurf, name="sdiff")
1186
+
1187
+ """
1188
+ _well_oper.create_surf_distance_log(self, surf, name)
1189
+
1190
+ def report_zonation_holes(self, threshold=5):
1191
+ """Reports if well has holes in zonation, less or equal to N samples.
1192
+
1193
+ Zonation may have holes due to various reasons, and
1194
+ usually a few undef samples indicates that something is wrong.
1195
+ This method reports well and start interval of the "holes"
1196
+
1197
+ The well shall have zonelog from import (via zonelogname attribute) and
1198
+ preferly a MD log (via mdlogname attribute); however if the
1199
+ latter is not present, a report withou MD values will be present.
1200
+
1201
+ Args:
1202
+ threshold (int): Number of samples (max.) that defines a hole, e.g.
1203
+ 5 means that undef samples in the range [1, 5] (including 5) is
1204
+ applied
1205
+
1206
+ Returns:
1207
+ A Pandas dataframe as a report. None if no list is made.
1208
+
1209
+ Raises:
1210
+ RuntimeError if zonelog is not present
1211
+ """
1212
+ return _well_oper.report_zonation_holes(self, threshold=threshold)
1213
+
1214
+ def get_zonation_points(
1215
+ self, tops=True, incl_limit=80, top_prefix="Top", zonelist=None, use_undef=False
1216
+ ):
1217
+ """Extract zonation points from Zonelog and make a marker list.
1218
+
1219
+ Currently it is either 'Tops' or 'Zone' (thicknesses); default
1220
+ is tops (i.e. tops=True).
1221
+
1222
+ The `zonelist` can be a list of zones, or a tuple with two members specifying
1223
+ first and last member. Note however that the zonation shall be without jumps
1224
+ and increasing. E.g.::
1225
+
1226
+ zonelist=(1, 5) # meaning [1, 2, 3, 4, 5]
1227
+ # or
1228
+ zonelist=[1, 2, 3, 4]
1229
+ # while _not_ legal:
1230
+ zonelist=[1, 4, 8]
1231
+
1232
+ Zone numbers less than 0 are not accepted
1233
+
1234
+ Args:
1235
+ tops (bool): If True then compute tops, else (thickness) points.
1236
+ incl_limit (float): If given, and usezone is True, the max
1237
+ angle of inclination to be used as input to zonation points.
1238
+ top_prefix (str): As well logs usually have isochore (zone) name,
1239
+ this prefix could be Top, e.g. 'SO43' --> 'TopSO43'
1240
+ zonelist (list of int or tuple): Zones to use
1241
+ use_undef (bool): If True, then transition from UNDEF is also
1242
+ used.
1243
+
1244
+
1245
+ Returns:
1246
+ A pandas dataframe (ready for the xyz/Points class), None
1247
+ if a zonelog is missing
1248
+ """
1249
+
1250
+ return _wellmarkers.get_zonation_points(
1251
+ self, tops, incl_limit, top_prefix, zonelist, use_undef
1252
+ )
1253
+
1254
+ def get_zone_interval(self, zonevalue, resample=1, extralogs=None):
1255
+ """Extract the X Y Z ID line (polyline) segment for a given zonevalue.
1256
+
1257
+ Args:
1258
+ zonevalue (int): The zone value to extract
1259
+ resample (int): If given, downsample every N'th sample to make
1260
+ polylines smaller in terms of bit and bytes.
1261
+ 1 = No downsampling.
1262
+ extralogs (list of str): List of extra log names to include
1263
+
1264
+
1265
+ Returns:
1266
+ A pandas dataframe X Y Z ID (ready for the xyz/Polygon class),
1267
+ None if a zonelog is missing or actual zone does dot
1268
+ exist in the well.
1269
+ """
1270
+ if resample < 1 or not isinstance(resample, int):
1271
+ raise KeyError("Key resample of wrong type (must be int >= 1)")
1272
+
1273
+ dff = self.get_filled_dataframe()
1274
+
1275
+ if self.zonelogname not in dff.columns:
1276
+ return None
1277
+
1278
+ # the technical solution here is to make a tmp column which
1279
+ # will add one number for each time the actual segment is repeated,
1280
+ # not straightforward... (thanks to H. Berland for tip)
1281
+
1282
+ dff["ztmp"] = dff[self.zonelogname]
1283
+ dff["ztmp"] = (dff[self.zonelogname] != zonevalue).astype(int)
1284
+
1285
+ dff["ztmp"] = (dff.ztmp != dff.ztmp.shift()).cumsum()
1286
+
1287
+ dff = dff[dff[self.zonelogname] == zonevalue]
1288
+
1289
+ m1v = dff["ztmp"].min()
1290
+ m2v = dff["ztmp"].max()
1291
+ if np.isnan(m1v):
1292
+ logger.debug("Returns (no data)")
1293
+ return None
1294
+
1295
+ df2 = dff.copy()
1296
+
1297
+ dflist = []
1298
+ for mvv in range(m1v, m2v + 1):
1299
+ dff9 = df2.copy()
1300
+ dff9 = df2[df2["ztmp"] == mvv]
1301
+ if dff9.index.shape[0] > 0:
1302
+ dflist.append(dff9)
1303
+
1304
+ dxlist = []
1305
+
1306
+ useloglist = [self.xname, self.yname, self.zname, "POLY_ID"]
1307
+ if extralogs is not None:
1308
+ useloglist.extend(extralogs)
1309
+
1310
+ for ivv in range(len(dflist)):
1311
+ dxf = dflist[ivv]
1312
+ dxf = dxf.rename(columns={"ztmp": "POLY_ID"})
1313
+ cols = [xxx for xxx in dxf.columns if xxx not in useloglist]
1314
+
1315
+ dxf = dxf.drop(cols, axis=1)
1316
+
1317
+ # now (down) resample every N'th
1318
+ if resample > 1:
1319
+ dxf = pd.concat([dxf.iloc[::resample, :], dxf.tail(1)])
1320
+
1321
+ dxlist.append(dxf)
1322
+
1323
+ dff = pd.concat(dxlist)
1324
+ dff.reset_index(inplace=True, drop=True)
1325
+
1326
+ logger.debug("Dataframe from well:\n%s", dff)
1327
+ return dff
1328
+
1329
+ def get_fraction_per_zone(
1330
+ self,
1331
+ dlogname,
1332
+ dcodes,
1333
+ zonelist=None,
1334
+ incl_limit=80,
1335
+ count_limit=3,
1336
+ zonelogname=None,
1337
+ ):
1338
+ """Get fraction of a discrete parameter, e.g. a facies, per zone.
1339
+
1340
+ It can be constrained by an inclination.
1341
+
1342
+ Also, it needs to be evaluated only of ZONE is complete; either
1343
+ INCREASE or DECREASE ; hence a quality flag is made and applied.
1344
+
1345
+ Args:
1346
+ dlogname (str): Name of discrete log, e.g. 'FACIES'
1347
+ dnames (list of int): Codes of facies (or similar) to report for
1348
+ zonelist (list of int): Zones to use
1349
+ incl_limit (float): Inclination limit for well path.
1350
+ count_limit (int): Minimum number of counts required per segment
1351
+ for valid calculations
1352
+ zonelogname (str). If None, the Well().zonelogname attribute is
1353
+ applied
1354
+
1355
+ Returns:
1356
+ A pandas dataframe (ready for the xyz/Points class), None
1357
+ if a zonelog is missing or or dlogname is missing,
1358
+ list is zero length for any reason.
1359
+ """
1360
+ return _wellmarkers.get_fraction_per_zone(
1361
+ self,
1362
+ dlogname,
1363
+ dcodes,
1364
+ zonelist=zonelist,
1365
+ incl_limit=incl_limit,
1366
+ count_limit=count_limit,
1367
+ zonelogname=zonelogname,
1368
+ )
1369
+
1370
+ def mask_shoulderbeds(
1371
+ self,
1372
+ inputlogs: list[str],
1373
+ targetlogs: list[str],
1374
+ nsamples: int | dict[str, float] | None = 2,
1375
+ strict: bool | None = False,
1376
+ ) -> bool:
1377
+ """Mask data around zone boundaries or other discrete log boundaries.
1378
+
1379
+ This operates on number of samples, hence the actual distance which is masked
1380
+ depends on the sampling interval (ie. count) or on distance measures.
1381
+ Distance measures are TVD (true vertical depth) or MD (measured depth).
1382
+
1383
+ .. image:: images/wells-mask-shoulderbeds.png
1384
+ :width: 300
1385
+ :align: center
1386
+
1387
+ Args:
1388
+ inputlogs: List of input logs, must be of discrete type.
1389
+ targetlogs: List of logs where mask is applied.
1390
+ nsamples: Number of samples around boundaries to filter, per side, i.e.
1391
+ value 2 means 2 above and 2 below, in total 4 samples.
1392
+ As alternative specify nsamples indirectly with a relative distance,
1393
+ as a dictionary with one record, as {"tvd": 0.5} or {"md": 0.7}.
1394
+ strict: If True, will raise Exception of any of the input or target log
1395
+ names are missing.
1396
+
1397
+ Returns:
1398
+ True if any operation has been done. False in case nothing has been done,
1399
+ e.g. no targetlogs for this particular well and ``strict`` is False.
1400
+
1401
+ Raises:
1402
+ ValueError: Various messages when wrong or inconsistent input.
1403
+
1404
+ Example:
1405
+ >>> mywell1 = Well(well_dir + '/OP_1.w')
1406
+ >>> mywell2 = Well(well_dir + '/OP_2.w')
1407
+ >>> did_succeed = mywell1.mask_shoulderbeds(["Zonelog", "Facies"], ["Perm"])
1408
+ >>> did_succeed = mywell2.mask_shoulderbeds(
1409
+ ... ["Zonelog"],
1410
+ ... ["Perm"],
1411
+ ... nsamples={"tvd": 0.8}
1412
+ ... )
1413
+
1414
+ """
1415
+ return _well_oper.mask_shoulderbeds(
1416
+ self, inputlogs, targetlogs, nsamples, strict
1417
+ )
1418
+
1419
+ def get_surface_picks(self, surf):
1420
+ """Return :class:`.Points` obj where well crosses the surface (horizon picks).
1421
+
1422
+ There may be several points in the Points() dataframe attribute.
1423
+ Also a ``DIRECTION`` column will show 1 if surface is penetrated from
1424
+ above, and -1 if penetrated from below.
1425
+
1426
+ Args:
1427
+ surf (RegularSurface): The surface instance
1428
+
1429
+ Returns:
1430
+ A :class:`.Points` instance, or None if no crossing points
1431
+
1432
+ .. versionadded:: 2.8
1433
+
1434
+ """
1435
+ return _wellmarkers.get_surface_picks(self, surf)
1436
+
1437
+ def make_ijk_from_grid(self, grid, grid_id="", algorithm=2, activeonly=True):
1438
+ """Look through a Grid and add grid I J K as discrete logs.
1439
+
1440
+ Note that the the grid counting has base 1 (first row is 1 etc).
1441
+
1442
+ By default, log (i.e. column names in the dataframe) will be
1443
+ ICELL, JCELL, KCELL, but you can add a tag (ID) to that name.
1444
+
1445
+ Args:
1446
+ grid (Grid): A XTGeo Grid instance
1447
+ grid_id (str): Add a tag (optional) to the current log name
1448
+ algorithm (int): Which interbal algorithm to use, default is 2 (expert
1449
+ setting)
1450
+ activeonly (bool): If True, only active cells are applied (algorithm 2 only)
1451
+
1452
+ Raises:
1453
+ RuntimeError: 'Error from C routine, code is ...'
1454
+
1455
+ .. versionchanged:: 2.9 Added keys for and `activeonly`
1456
+ """
1457
+ _well_oper.make_ijk_from_grid(
1458
+ self, grid, grid_id=grid_id, algorithm=algorithm, activeonly=activeonly
1459
+ )
1460
+
1461
+ def make_zone_qual_log(self, zqname):
1462
+ """Create a zone quality/indicator (flag) log.
1463
+
1464
+ This routine looks through to zone log and flag intervals according
1465
+ to neighbouring zones:
1466
+
1467
+ * 0: Undetermined flag
1468
+
1469
+ * 1: Zonelog interval numbering increases,
1470
+ e.g. for zone 2: 1 1 1 1 2 2 2 2 2 5 5 5 5 5
1471
+
1472
+ * 2: Zonelog interval numbering decreases,
1473
+ e.g. for zone 2: 6 6 6 2 2 2 2 1 1 1
1474
+
1475
+ * 3: Interval is a U turning point, e.g. 0 0 0 2 2 2 1 1 1
1476
+
1477
+ * 4: Interval is a inverse U turning point, 3 3 3 2 2 2 5 5
1478
+
1479
+ * 9: Interval is bounded by one or more missing sections,
1480
+ e.g. 1 1 1 2 2 2 -999 -999
1481
+
1482
+ If a log with the name exists, it will be silently replaced
1483
+
1484
+ Args:
1485
+ zqname (str): Name of quality log
1486
+ """
1487
+ _well_oper.make_zone_qual_log(self, zqname)
1488
+
1489
+ def get_gridproperties(
1490
+ self, gridprops, grid=("ICELL", "JCELL", "KCELL"), prop_id="_model"
1491
+ ):
1492
+ """Look through a Grid and add a set of grid properties as logs.
1493
+
1494
+ The name of the logs will ...
1495
+
1496
+ This can be done to sample model properties along a well.
1497
+
1498
+ Args:
1499
+ gridprops (Grid): A XTGeo GridProperties instance (a collection
1500
+ of properties) or a single GridProperty instance
1501
+ grid (Grid or tuple): A XTGeo Grid instance or a reference
1502
+ via tuple. If this is tuple with log names,
1503
+ it states that these logs already contains
1504
+ the gridcell IJK numbering.
1505
+ prop_id (str): Add a tag (optional) to the current log name, e.g
1506
+ as PORO_model, where _model is the tag.
1507
+
1508
+ Raises:
1509
+ None
1510
+
1511
+ .. versionadded:: 2.1
1512
+
1513
+ """
1514
+ _well_oper.get_gridproperties(self, gridprops, grid=grid, prop_id=prop_id)