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
xtgeo/well/well1.py ADDED
@@ -0,0 +1,1516 @@
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, Any
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(
633
+ self,
634
+ project: Any,
635
+ wname: str,
636
+ lognames: str | list[str] = "all",
637
+ realisation: int = 0,
638
+ trajectory: str = "Drilled trajectory",
639
+ logrun: str = "log",
640
+ update_option: str = None,
641
+ ):
642
+ """Export (save/store) a well to a roxar project.
643
+
644
+ Note this method works only when inside RMS, or when RMS license is
645
+ activated in terminal.
646
+
647
+ The current implementation will either update the existing well
648
+ (then well log array size must not change), or it will make a new well in RMS.
649
+
650
+
651
+ Args:
652
+ project: Magic string 'project' or file path to project
653
+ wname: Name of well, as shown in RMS.
654
+ lognames: List of lognames to save, or
655
+ use simply 'all' for current logs for this well. Default is 'all'
656
+ realisation: Currently inactive
657
+ trajectory: Name of trajectory in RMS, default is "Drilled trajectory"
658
+ logrun: Name of logrun in RMS, default is "log"
659
+ update_option (str): None | "overwrite" | "append". This only applies
660
+ when the well (wname) exists in RMS, and rules are based on name
661
+ matching. Default is None which means that all well logs in
662
+ RMS are emptied and then replaced with the content from xtgeo.
663
+ The "overwrite" option will replace logs in RMS with logs from xtgeo,
664
+ and append new if they do not exist in RMS. The
665
+ "append" option will only append logs if name does not exist in RMS
666
+ already. Reading only a subset of logs and then use "overwrite" or
667
+ "append" may speed up execution significantly.
668
+
669
+ Note:
670
+ When project is file path (direct access, outside RMS) then
671
+ ``to_roxar()`` will implicitly do a project save. Otherwise, the project
672
+ will not be saved until the user do an explicit project save action.
673
+
674
+ Example::
675
+
676
+ # assume that existing logs in RMS are ["PORO", "PERMH", "GR", "DT", "FAC"]
677
+ # read only one existing log (faster)
678
+
679
+ wll = xtgeo.well_from_roxar(project, "WELL1", lognames=["PORO"])
680
+ dfr = wll.get_dataframe()
681
+ dfr["PORO"] += 0.2 # add 0.2 to PORO log
682
+ wll.set_dataframe(dfr)
683
+ wll.create_log("NEW", value=0.333) # create a new log with constant value
684
+
685
+ # the "option" is a variable... for output, ``lognames="all"`` is default
686
+ if option is None:
687
+ # remove all current logs in RMS; only logs will be PORO and NEW
688
+ wll.to_roxar(project, "WELL1", update_option=option)
689
+ elif option == "overwrite":
690
+ # keep all original logs but update PORO and add NEW
691
+ wll.to_roxar(project, "WELL1", update_option=option)
692
+ elif option == "append":
693
+ # keep all original logs as they were (incl. PORO) and add NEW
694
+ wll.to_roxar(project, "WELL1", update_option=option)
695
+
696
+ Note:
697
+ The keywords ``lognames`` and ``update_option`` will interact
698
+
699
+ .. versionadded:: 2.12
700
+ .. versionchanged:: 2.15
701
+ Saving to new wells enabled (earlier only modifying existing)
702
+ .. versionchanged:: 3.5
703
+ Add key ``update_option``
704
+ """
705
+ logger.debug("Not in use: realisation %s", realisation)
706
+
707
+ _well_roxapi.export_well_roxapi(
708
+ self,
709
+ project,
710
+ wname,
711
+ lognames=lognames,
712
+ trajectory=trajectory,
713
+ logrun=logrun,
714
+ realisation=realisation,
715
+ update_option=update_option,
716
+ )
717
+
718
+ def get_lognames(self):
719
+ """Get the lognames for all logs."""
720
+ return list(self._wdata.data)
721
+
722
+ def get_wlogs(self) -> dict:
723
+ """Get a compound dictionary with well log metadata.
724
+
725
+ The result will be an dict on the form:
726
+
727
+ ``{"X_UTME": ["CONT", None], ... "Facies": ["DISC", {1: "BG", 2: "SAND"}]}``
728
+ """
729
+ res = {}
730
+
731
+ for key in self.get_lognames():
732
+ wtype = _AttrType.CONT.value
733
+ wrecord = None
734
+ if key in self._wdata.attr_types:
735
+ wtype = self._wdata.attr_types[key].name
736
+ if key in self._wdata.attr_records:
737
+ wrecord = self._wdata.attr_records[key]
738
+
739
+ res[key] = [wtype, wrecord]
740
+
741
+ return res
742
+
743
+ def set_wlogs(self, wlogs: dict):
744
+ """Set a compound dictionary with well log metadata.
745
+
746
+ This operation is somewhat risky as it may lead to inconsistency, so use with
747
+ care! Typically, one will use :meth:`get_wlogs` first and then modify some
748
+ attributes.
749
+
750
+ Args:
751
+ wlogs: Input data dictionary
752
+
753
+ Raises:
754
+ ValueError: Invalid log type found in input:
755
+ ValueError: Invalid log record found in input:
756
+ ValueError: Invalid input key found:
757
+ ValueError: Invalid log record found in input:
758
+
759
+ """
760
+ for key in self.get_lognames():
761
+ if key in wlogs:
762
+ typ, rec = wlogs[key]
763
+ self._wdata.set_attr_type(key, typ)
764
+ self._wdata.set_attr_record(key, deepcopy(rec))
765
+
766
+ self._ensure_consistency()
767
+
768
+ def isdiscrete(self, logname):
769
+ """Return True of log is discrete, otherwise False.
770
+
771
+ Args:
772
+ logname (str): Name of log to check if discrete or not
773
+
774
+ .. versionadded:: 2.2.0
775
+ """
776
+ return (
777
+ logname in self.get_lognames()
778
+ and self.get_logtype(logname) == _AttrType.DISC.value
779
+ )
780
+
781
+ def copy(self):
782
+ """Copy a Well instance to a new unique Well instance."""
783
+ return Well(
784
+ self.rkb,
785
+ self.xpos,
786
+ self.ypos,
787
+ self.wname,
788
+ self._wdata.data.copy(),
789
+ self.mdlogname,
790
+ self.zonelogname,
791
+ self.wlogtypes,
792
+ self.wlogrecords,
793
+ self._filesrc,
794
+ )
795
+
796
+ def rename_log(self, lname, newname):
797
+ """Rename a log, e.g. Poro to PORO."""
798
+ self._wdata.rename_attr(lname, newname)
799
+
800
+ if self._mdlogname == lname:
801
+ self._mdlogname = newname
802
+
803
+ if self._zonelogname == lname:
804
+ self._zonelogname = newname
805
+
806
+ def create_log(
807
+ self,
808
+ lname: str,
809
+ logtype: str = _AttrType.CONT.value,
810
+ logrecord: dict | None = None,
811
+ value: float = 0.0,
812
+ force: bool = True,
813
+ ) -> bool:
814
+ """Create a new log with initial values.
815
+
816
+ If the logname already exists, it will be silently overwritten, unless
817
+ the option force=False.
818
+
819
+ Args:
820
+ lname: name of new log
821
+ logtype: Must be 'CONT' (default) or 'DISC' (discrete)
822
+ logrecord: A dictionary of key: values for 'DISC' logs
823
+ value: initial value to set
824
+ force: If True, and lname exists, it will be overwritten, if
825
+ False, no new log will be made. Will return False.
826
+
827
+ Returns:
828
+ True ff a new log is made (either new or force overwrite an
829
+ existing) or False if the new log already exists,
830
+ and ``force=False``.
831
+
832
+ Note::
833
+
834
+ A new log can also be created by adding it to the dataframe directly, but
835
+ with less control over e.g. logrecord
836
+
837
+ """
838
+ return self._wdata.create_attr(lname, logtype, logrecord, value, force)
839
+
840
+ def copy_log(
841
+ self,
842
+ lname: str,
843
+ newname: str,
844
+ force: bool = True,
845
+ ) -> bool:
846
+ """Copy a log from an existing to a name
847
+
848
+ If the new log already exists, it will be silently overwritten, unless
849
+ the option force=False.
850
+
851
+ Args:
852
+ lname: name of existing log
853
+ newname: name of new log
854
+
855
+ Returns:
856
+ True if a new log is made (either new or force overwrite an
857
+ existing) or False if the new log already exists,
858
+ and ``force=False``.
859
+
860
+ Note::
861
+
862
+ A copy can also be done directly in the dataframe, but with less
863
+ consistency checks; hence this method is recommended
864
+
865
+ """
866
+ return self._wdata.copy_attr(lname, newname, force)
867
+
868
+ def delete_log(self, lname: str | list[str]) -> int:
869
+ """Delete/remove an existing log, or list of logs.
870
+
871
+ Will continue silently if a log does not exist.
872
+
873
+ Args:
874
+ lname: A logname or a list of lognames
875
+
876
+ Returns:
877
+ Number of logs deleted
878
+
879
+ Note::
880
+
881
+ A log can also be deleted by simply removing it from the dataframe.
882
+
883
+ """
884
+ logger.debug("Deleting log(s) %s...", lname)
885
+ return self._wdata.delete_attr(lname)
886
+
887
+ delete_logs = delete_log # alias function
888
+
889
+ def get_logtype(self, lname) -> str | None:
890
+ """Returns the type of a given log (e.g. DISC or CONT), None if not present."""
891
+ if lname in self._wdata.attr_types:
892
+ return self._wdata.attr_types[lname].name
893
+ return None
894
+
895
+ def set_logtype(self, lname, ltype):
896
+ """Sets the type of a give log (e.g. DISC or CONT)."""
897
+ self._wdata.set_attr_type(lname, ltype)
898
+
899
+ def get_logrecord(self, lname):
900
+ """Returns the record (dict) of a given log name, None if not exists."""
901
+
902
+ return self._wdata.get_attr_record(lname)
903
+
904
+ def set_logrecord(self, lname, newdict):
905
+ """Sets the record (dict) of a given discrete log."""
906
+ self._wdata.set_attr_record(lname, newdict)
907
+
908
+ def get_logrecord_codename(self, lname, key):
909
+ """Returns the name entry of a log record, for a given key.
910
+
911
+ Example::
912
+
913
+ # get the name for zonelog entry no 4:
914
+ zname = well.get_logrecord_codename('ZONELOG', 4)
915
+ """
916
+ zlogdict = self.get_logrecord(lname)
917
+ if key in zlogdict:
918
+ return zlogdict[key]
919
+
920
+ return None
921
+
922
+ def get_dataframe(self, copy: bool = True):
923
+ """Get a copy (default) or a view of the dataframe.
924
+
925
+ Args:
926
+ copy: If True, return a deep copy. A view (copy=False) will be faster and
927
+ more memory efficient, but less "safe" for some cases when manipulating
928
+ dataframes.
929
+
930
+ .. versionchanged:: 3.7 Added `copy` keyword
931
+ """
932
+ return self._wdata.get_dataframe(copy=copy)
933
+
934
+ def get_filled_dataframe(self, fill_value=UNDEF, fill_value_int=UNDEF_INT):
935
+ """Fill the Nan's in the dataframe with real UNDEF values.
936
+
937
+ This module returns a copy of the dataframe in the object; it
938
+ does not change the instance.
939
+
940
+ Note that DISC logs will be casted to columns with integer
941
+ as datatype.
942
+
943
+ Returns:
944
+ A pandas dataframe where Nan er replaces with preset
945
+ high XTGeo UNDEF values, or user defined values.
946
+
947
+ """
948
+ return self._wdata.get_dataframe_copy(
949
+ infer_dtype=True,
950
+ filled=True,
951
+ fill_value=fill_value,
952
+ fill_value_int=fill_value_int,
953
+ )
954
+
955
+ def set_dataframe(self, dfr):
956
+ """Set the dataframe."""
957
+ self._wdata.set_dataframe(dfr)
958
+
959
+ def create_relative_hlen(self):
960
+ """Make a relative length of a well, as a log.
961
+
962
+ The first well og entry defines zero, then the horizontal length
963
+ is computed relative to that by simple geometric methods.
964
+ """
965
+ self._wdata.create_relative_hlen()
966
+
967
+ def geometrics(self):
968
+ """Compute some well geometrical arrays MD, INCL, AZI, as logs.
969
+
970
+ These are kind of quasi measurements hence the logs will named
971
+ with a Q in front as Q_MDEPTH, Q_INCL, and Q_AZI.
972
+
973
+ These logs will be added to the dataframe. If the mdlogname
974
+ attribute does not exist in advance, it will be set to 'Q_MDEPTH'.
975
+
976
+ Returns:
977
+ False if geometrics cannot be computed
978
+
979
+ """
980
+ rvalue = self._wdata.geometrics()
981
+
982
+ if not self._mdlogname:
983
+ self._mdlogname = "Q_MDEPTH"
984
+
985
+ return rvalue
986
+
987
+ def truncate_parallel_path(
988
+ self, other, xtol=None, ytol=None, ztol=None, itol=None, atol=None
989
+ ):
990
+ """Truncate the part of the well trajectory that is ~parallel with other.
991
+
992
+ Args:
993
+ other (Well): Other well to compare with
994
+ xtol (float): Tolerance in X (East) coord for measuring unit
995
+ ytol (float): Tolerance in Y (North) coord for measuring unit
996
+ ztol (float): Tolerance in Z (TVD) coord for measuring unit
997
+ itol (float): Tolerance in inclination (degrees)
998
+ atol (float): Tolerance in azimuth (degrees)
999
+ """
1000
+ if xtol is None:
1001
+ xtol = 0.0
1002
+ if ytol is None:
1003
+ ytol = 0.0
1004
+ if ztol is None:
1005
+ ztol = 0.0
1006
+ if itol is None:
1007
+ itol = 0.0
1008
+ if atol is None:
1009
+ atol = 0.0
1010
+
1011
+ this_df = self.get_dataframe()
1012
+ other_df = other.get_dataframe()
1013
+
1014
+ if this_df.shape[0] < 3 or other_df.shape[0] < 3:
1015
+ raise ValueError(
1016
+ f"Too few points to truncate parallel path, was "
1017
+ f"{this_df.size} and {other_df.size}, must be >3"
1018
+ )
1019
+
1020
+ # extract numpies from XYZ trajectory logs
1021
+ xv1 = self._wdata.data[self.xname].values
1022
+ yv1 = self._wdata.data[self.yname].values
1023
+ zv1 = self._wdata.data[self.zname].values
1024
+
1025
+ xv2 = other_df[self.xname].values
1026
+ yv2 = other_df[self.yname].values
1027
+ zv2 = other_df[self.zname].values
1028
+
1029
+ ier = _cxtgeo.well_trunc_parallel(
1030
+ xv1, yv1, zv1, xv2, yv2, zv2, xtol, ytol, ztol, itol, atol, 0
1031
+ )
1032
+
1033
+ if ier != 0:
1034
+ raise RuntimeError("Unexpected error")
1035
+
1036
+ dfr = self.get_dataframe()
1037
+ dfr = dfr[dfr[self.xname] < UNDEF_LIMIT]
1038
+ self.set_dataframe(dfr)
1039
+
1040
+ def may_overlap(self, other):
1041
+ """Consider if well overlap in X Y coordinates with other well, True/False."""
1042
+ dataframe = self.get_dataframe()
1043
+ other_dataframe = other.get_dataframe()
1044
+
1045
+ if dataframe.size < 2 or other_dataframe.size < 2:
1046
+ return False
1047
+
1048
+ # extract numpies from XYZ trajectory logs
1049
+ xmin1 = np.nanmin(dataframe[self.xname].values)
1050
+ xmax1 = np.nanmax(dataframe[self.xname].values)
1051
+ ymin1 = np.nanmin(dataframe[self.yname].values)
1052
+ ymax1 = np.nanmax(dataframe[self.yname].values)
1053
+
1054
+ xmin2 = np.nanmin(other_dataframe[self.xname].values)
1055
+ xmax2 = np.nanmax(other_dataframe[self.xname].values)
1056
+ ymin2 = np.nanmin(other_dataframe[self.yname].values)
1057
+ ymax2 = np.nanmax(other_dataframe[self.yname].values)
1058
+
1059
+ if xmin1 > xmax2 or ymin1 > ymax2:
1060
+ return False
1061
+ return not (xmin2 > xmax1 or ymin2 > ymax1)
1062
+
1063
+ def limit_tvd(self, tvdmin, tvdmax):
1064
+ """Truncate the part of the well that is outside tvdmin, tvdmax.
1065
+
1066
+ Range will be in tvdmin <= tvd <= tvdmax.
1067
+
1068
+ Args:
1069
+ tvdmin (float): Minimum TVD
1070
+ tvdmax (float): Maximum TVD
1071
+ """
1072
+ dfr = self.get_dataframe()
1073
+ dfr = dfr[dfr[self.zname] >= tvdmin]
1074
+ dfr = dfr[dfr[self.zname] <= tvdmax]
1075
+ self.set_dataframe(dfr)
1076
+
1077
+ def downsample(self, interval=4, keeplast=True):
1078
+ """Downsample by sampling every N'th element (coarsen only).
1079
+
1080
+ Args:
1081
+ interval (int): Sampling interval.
1082
+ keeplast (bool): If True, the last element from the original
1083
+ dataframe is kept, to avoid that the well is shortened.
1084
+ """
1085
+ dataframe = self.get_dataframe()
1086
+
1087
+ if dataframe.size < 2 * interval:
1088
+ return
1089
+
1090
+ dfr = dataframe[::interval].copy()
1091
+
1092
+ if keeplast:
1093
+ dfr = pd.concat([dfr, dataframe.iloc[-1:]], ignore_index=True)
1094
+
1095
+ self.set_dataframe(dfr.reset_index(drop=True))
1096
+
1097
+ def rescale(self, delta=0.15, tvdrange=None):
1098
+ """Rescale (refine or coarse) by sampling a delta along the trajectory, in MD.
1099
+
1100
+ Args:
1101
+ delta (float): Step length
1102
+ tvdrange (tuple of floats): Resampling can be limited to TVD interval
1103
+
1104
+ .. versionchanged:: 2.2 Added tvdrange
1105
+ """
1106
+ _well_oper.rescale(self, delta=delta, tvdrange=tvdrange)
1107
+
1108
+ def get_polygons(self, skipname=False):
1109
+ """Return a Polygons object from the well trajectory.
1110
+
1111
+ Args:
1112
+ skipname (bool): If True then name column is omitted
1113
+
1114
+ .. versionadded:: 2.1
1115
+ .. versionchanged:: 2.13 Added `skipname` key
1116
+ """
1117
+ dfr = self._wdata.data.copy()
1118
+
1119
+ keep = (self.xname, self.yname, self.zname)
1120
+ for col in dfr.columns:
1121
+ if col not in keep:
1122
+ dfr.drop(labels=col, axis=1, inplace=True)
1123
+ dfr["POLY_ID"] = 1
1124
+
1125
+ if not skipname:
1126
+ dfr["NAME"] = self.xwellname
1127
+ poly = Polygons()
1128
+ poly.set_dataframe(dfr)
1129
+ poly.name = self.xwellname
1130
+
1131
+ return poly
1132
+
1133
+ def get_fence_polyline(self, sampling=20, nextend=2, tvdmin=None, asnumpy=True):
1134
+ """Return a fence polyline as a numpy array or a Polygons object.
1135
+
1136
+ The result will aim for a regular sampling interval, useful for extracting
1137
+ fence plots (cross-sections).
1138
+
1139
+ Args:
1140
+ sampling (float): Sampling interval i.e. horizonal distance (input)
1141
+ nextend (int): Number if sampling to extend; e.g. 2 * 20
1142
+ tvdmin (float): Minimum TVD starting point.
1143
+ as_numpy (bool): If True, a numpy array, otherwise a Polygons
1144
+ object with 5 columns where the 2 last are HLEN and POLY_ID
1145
+ and the POLY_ID will be set to 0.
1146
+
1147
+ Returns:
1148
+ A numpy array of shape (NLEN, 5) in F order,
1149
+ Or a Polygons object with 5 columns
1150
+ If not possible, return False
1151
+
1152
+ .. versionchanged:: 2.1 improved algorithm
1153
+ """
1154
+ poly = self.get_polygons()
1155
+
1156
+ if tvdmin is not None:
1157
+ poly_df = poly.get_dataframe()
1158
+ poly_df = poly_df[poly_df[poly.zname] >= tvdmin]
1159
+ poly_df.reset_index(drop=True, inplace=True)
1160
+ poly.set_dataframe(poly_df)
1161
+
1162
+ return poly.get_fence(distance=sampling, nextend=nextend, asnumpy=asnumpy)
1163
+
1164
+ def create_surf_distance_log(
1165
+ self,
1166
+ surf: object,
1167
+ name: str | None = "DIST_SURF",
1168
+ ):
1169
+ """Make a log that is vertical distance to a regular surface.
1170
+
1171
+ If the trajectory is above the surface (i.e. more shallow), then the
1172
+ distance sign is positive.
1173
+
1174
+ Args:
1175
+ surf: The RegularSurface instance.
1176
+ name: The name of the new log. If it exists it will be overwritten.
1177
+
1178
+ Example::
1179
+
1180
+ mywell.rescale() # optional
1181
+ thesurf = xtgeo.surface_from_file("some.gri")
1182
+ mywell.create_surf_distance_log(thesurf, name="sdiff")
1183
+
1184
+ """
1185
+ _well_oper.create_surf_distance_log(self, surf, name)
1186
+
1187
+ def report_zonation_holes(self, threshold=5):
1188
+ """Reports if well has holes in zonation, less or equal to N samples.
1189
+
1190
+ Zonation may have holes due to various reasons, and
1191
+ usually a few undef samples indicates that something is wrong.
1192
+ This method reports well and start interval of the "holes"
1193
+
1194
+ The well shall have zonelog from import (via zonelogname attribute) and
1195
+ preferly a MD log (via mdlogname attribute); however if the
1196
+ latter is not present, a report withou MD values will be present.
1197
+
1198
+ Args:
1199
+ threshold (int): Number of samples (max.) that defines a hole, e.g.
1200
+ 5 means that undef samples in the range [1, 5] (including 5) is
1201
+ applied
1202
+
1203
+ Returns:
1204
+ A Pandas dataframe as a report. None if no list is made.
1205
+
1206
+ Raises:
1207
+ RuntimeError if zonelog is not present
1208
+ """
1209
+ return _well_oper.report_zonation_holes(self, threshold=threshold)
1210
+
1211
+ def get_zonation_points(
1212
+ self, tops=True, incl_limit=80, top_prefix="Top", zonelist=None, use_undef=False
1213
+ ):
1214
+ """Extract zonation points from Zonelog and make a marker list.
1215
+
1216
+ Currently it is either 'Tops' or 'Zone' (thicknesses); default
1217
+ is tops (i.e. tops=True).
1218
+
1219
+ The `zonelist` can be a list of zones, or a tuple with two members specifying
1220
+ first and last member. Note however that the zonation shall be without jumps
1221
+ and increasing. E.g.::
1222
+
1223
+ zonelist=(1, 5) # meaning [1, 2, 3, 4, 5]
1224
+ # or
1225
+ zonelist=[1, 2, 3, 4]
1226
+ # while _not_ legal:
1227
+ zonelist=[1, 4, 8]
1228
+
1229
+ Zone numbers less than 0 are not accepted
1230
+
1231
+ Args:
1232
+ tops (bool): If True then compute tops, else (thickness) points.
1233
+ incl_limit (float): If given, and usezone is True, the max
1234
+ angle of inclination to be used as input to zonation points.
1235
+ top_prefix (str): As well logs usually have isochore (zone) name,
1236
+ this prefix could be Top, e.g. 'SO43' --> 'TopSO43'
1237
+ zonelist (list of int or tuple): Zones to use
1238
+ use_undef (bool): If True, then transition from UNDEF is also
1239
+ used.
1240
+
1241
+
1242
+ Returns:
1243
+ A pandas dataframe (ready for the xyz/Points class), None
1244
+ if a zonelog is missing
1245
+ """
1246
+
1247
+ return _wellmarkers.get_zonation_points(
1248
+ self, tops, incl_limit, top_prefix, zonelist, use_undef
1249
+ )
1250
+
1251
+ def get_zone_interval(self, zonevalue, resample=1, extralogs=None):
1252
+ """Extract the X Y Z ID line (polyline) segment for a given zonevalue.
1253
+
1254
+ Args:
1255
+ zonevalue (int): The zone value to extract
1256
+ resample (int): If given, downsample every N'th sample to make
1257
+ polylines smaller in terms of bit and bytes.
1258
+ 1 = No downsampling.
1259
+ extralogs (list of str): List of extra log names to include
1260
+
1261
+
1262
+ Returns:
1263
+ A pandas dataframe X Y Z ID (ready for the xyz/Polygon class),
1264
+ None if a zonelog is missing or actual zone does dot
1265
+ exist in the well.
1266
+ """
1267
+ if resample < 1 or not isinstance(resample, int):
1268
+ raise KeyError("Key resample of wrong type (must be int >= 1)")
1269
+
1270
+ dff = self.get_filled_dataframe()
1271
+
1272
+ if self.zonelogname not in dff.columns:
1273
+ return None
1274
+
1275
+ # the technical solution here is to make a tmp column which
1276
+ # will add one number for each time the actual segment is repeated,
1277
+ # not straightforward... (thanks to H. Berland for tip)
1278
+
1279
+ dff["ztmp"] = dff[self.zonelogname]
1280
+ dff["ztmp"] = (dff[self.zonelogname] != zonevalue).astype(int)
1281
+
1282
+ dff["ztmp"] = (dff.ztmp != dff.ztmp.shift()).cumsum()
1283
+
1284
+ dff = dff[dff[self.zonelogname] == zonevalue]
1285
+
1286
+ m1v = dff["ztmp"].min()
1287
+ m2v = dff["ztmp"].max()
1288
+ if np.isnan(m1v):
1289
+ logger.debug("Returns (no data)")
1290
+ return None
1291
+
1292
+ df2 = dff.copy()
1293
+
1294
+ dflist = []
1295
+ for mvv in range(m1v, m2v + 1):
1296
+ dff9 = df2.copy()
1297
+ dff9 = df2[df2["ztmp"] == mvv]
1298
+ if dff9.index.shape[0] > 0:
1299
+ dflist.append(dff9)
1300
+
1301
+ dxlist = []
1302
+
1303
+ useloglist = [self.xname, self.yname, self.zname, "POLY_ID"]
1304
+ if extralogs is not None:
1305
+ useloglist.extend(extralogs)
1306
+
1307
+ for ivv in range(len(dflist)):
1308
+ dxf = dflist[ivv]
1309
+ dxf = dxf.rename(columns={"ztmp": "POLY_ID"})
1310
+ cols = [xxx for xxx in dxf.columns if xxx not in useloglist]
1311
+
1312
+ dxf = dxf.drop(cols, axis=1)
1313
+
1314
+ # now (down) resample every N'th
1315
+ if resample > 1:
1316
+ dxf = pd.concat([dxf.iloc[::resample, :], dxf.tail(1)])
1317
+
1318
+ dxlist.append(dxf)
1319
+
1320
+ dff = pd.concat(dxlist)
1321
+ dff.reset_index(inplace=True, drop=True)
1322
+
1323
+ logger.debug("Dataframe from well:\n%s", dff)
1324
+ return dff
1325
+
1326
+ def get_fraction_per_zone(
1327
+ self,
1328
+ dlogname,
1329
+ dcodes,
1330
+ zonelist=None,
1331
+ incl_limit=80,
1332
+ count_limit=3,
1333
+ zonelogname=None,
1334
+ ):
1335
+ """Get fraction of a discrete parameter, e.g. a facies, per zone.
1336
+
1337
+ It can be constrained by an inclination.
1338
+
1339
+ Also, it needs to be evaluated only of ZONE is complete; either
1340
+ INCREASE or DECREASE ; hence a quality flag is made and applied.
1341
+
1342
+ Args:
1343
+ dlogname (str): Name of discrete log, e.g. 'FACIES'
1344
+ dnames (list of int): Codes of facies (or similar) to report for
1345
+ zonelist (list of int): Zones to use
1346
+ incl_limit (float): Inclination limit for well path.
1347
+ count_limit (int): Minimum number of counts required per segment
1348
+ for valid calculations
1349
+ zonelogname (str). If None, the Well().zonelogname attribute is
1350
+ applied
1351
+
1352
+ Returns:
1353
+ A pandas dataframe (ready for the xyz/Points class), None
1354
+ if a zonelog is missing or or dlogname is missing,
1355
+ list is zero length for any reason.
1356
+ """
1357
+ return _wellmarkers.get_fraction_per_zone(
1358
+ self,
1359
+ dlogname,
1360
+ dcodes,
1361
+ zonelist=zonelist,
1362
+ incl_limit=incl_limit,
1363
+ count_limit=count_limit,
1364
+ zonelogname=zonelogname,
1365
+ )
1366
+
1367
+ def mask_shoulderbeds(
1368
+ self,
1369
+ inputlogs: list[str],
1370
+ targetlogs: list[str],
1371
+ nsamples: int | dict[str, float] | None = 2,
1372
+ strict: bool | None = False,
1373
+ ) -> bool:
1374
+ """Mask data around zone boundaries or other discrete log boundaries.
1375
+
1376
+ This operates on number of samples, hence the actual distance which is masked
1377
+ depends on the sampling interval (ie. count) or on distance measures.
1378
+ Distance measures are TVD (true vertical depth) or MD (measured depth).
1379
+
1380
+ .. image:: images/wells-mask-shoulderbeds.png
1381
+ :width: 300
1382
+ :align: center
1383
+
1384
+ Args:
1385
+ inputlogs: List of input logs, must be of discrete type.
1386
+ targetlogs: List of logs where mask is applied.
1387
+ nsamples: Number of samples around boundaries to filter, per side, i.e.
1388
+ value 2 means 2 above and 2 below, in total 4 samples.
1389
+ As alternative specify nsamples indirectly with a relative distance,
1390
+ as a dictionary with one record, as {"tvd": 0.5} or {"md": 0.7}.
1391
+ strict: If True, will raise Exception of any of the input or target log
1392
+ names are missing.
1393
+
1394
+ Returns:
1395
+ True if any operation has been done. False in case nothing has been done,
1396
+ e.g. no targetlogs for this particular well and ``strict`` is False.
1397
+
1398
+ Raises:
1399
+ ValueError: Various messages when wrong or inconsistent input.
1400
+
1401
+ Example:
1402
+ >>> mywell1 = Well(well_dir + '/OP_1.w')
1403
+ >>> mywell2 = Well(well_dir + '/OP_2.w')
1404
+ >>> did_succeed = mywell1.mask_shoulderbeds(["Zonelog", "Facies"], ["Perm"])
1405
+ >>> did_succeed = mywell2.mask_shoulderbeds(
1406
+ ... ["Zonelog"],
1407
+ ... ["Perm"],
1408
+ ... nsamples={"tvd": 0.8}
1409
+ ... )
1410
+
1411
+ """
1412
+ return _well_oper.mask_shoulderbeds(
1413
+ self, inputlogs, targetlogs, nsamples, strict
1414
+ )
1415
+
1416
+ def get_surface_picks(self, surf):
1417
+ """Return :class:`.Points` obj where well crosses the surface (horizon picks).
1418
+
1419
+ There may be several points in the Points() dataframe attribute.
1420
+ Also a ``DIRECTION`` column will show 1 if surface is penetrated from
1421
+ above, and -1 if penetrated from below.
1422
+
1423
+ Args:
1424
+ surf (RegularSurface): The surface instance
1425
+
1426
+ Returns:
1427
+ A :class:`.Points` instance, or None if no crossing points
1428
+
1429
+ .. versionadded:: 2.8
1430
+
1431
+ """
1432
+ return _wellmarkers.get_surface_picks(self, surf)
1433
+
1434
+ def make_ijk_from_grid(self, grid, grid_id="", activeonly=True, **kwargs):
1435
+ """Look through a Grid and add grid I J K as discrete logs.
1436
+
1437
+ Note that the the grid counting has base 1 (first row is 1 etc).
1438
+
1439
+ By default, log (i.e. column names in the dataframe) will be
1440
+ ICELL, JCELL, KCELL, but you can add a tag (ID) to that name.
1441
+
1442
+ Args:
1443
+ grid (Grid): A XTGeo Grid instance
1444
+ grid_id (str): Add a tag (optional) to the current log name
1445
+ activeonly (bool): If True, only active cells are applied (algorithm 2 only)
1446
+
1447
+ Raises:
1448
+ RuntimeError: 'Error from C routine, code is ...'
1449
+
1450
+ .. versionchanged:: 2.9 Added keys for and `activeonly`
1451
+ """
1452
+ algorithm = kwargs.get("algorithm")
1453
+ if algorithm:
1454
+ warnings.warn(
1455
+ "Keyword 'algorithm': Is not in use anymore, please remove it",
1456
+ UserWarning,
1457
+ )
1458
+
1459
+ _well_oper.make_ijk_from_grid(
1460
+ self, grid, grid_id=grid_id, activeonly=activeonly
1461
+ )
1462
+
1463
+ def make_zone_qual_log(self, zqname):
1464
+ """Create a zone quality/indicator (flag) log.
1465
+
1466
+ This routine looks through to zone log and flag intervals according
1467
+ to neighbouring zones:
1468
+
1469
+ * 0: Undetermined flag
1470
+
1471
+ * 1: Zonelog interval numbering increases,
1472
+ e.g. for zone 2: 1 1 1 1 2 2 2 2 2 5 5 5 5 5
1473
+
1474
+ * 2: Zonelog interval numbering decreases,
1475
+ e.g. for zone 2: 6 6 6 2 2 2 2 1 1 1
1476
+
1477
+ * 3: Interval is a U turning point, e.g. 0 0 0 2 2 2 1 1 1
1478
+
1479
+ * 4: Interval is a inverse U turning point, 3 3 3 2 2 2 5 5
1480
+
1481
+ * 9: Interval is bounded by one or more missing sections,
1482
+ e.g. 1 1 1 2 2 2 -999 -999
1483
+
1484
+ If a log with the name exists, it will be silently replaced
1485
+
1486
+ Args:
1487
+ zqname (str): Name of quality log
1488
+ """
1489
+ _well_oper.make_zone_qual_log(self, zqname)
1490
+
1491
+ def get_gridproperties(
1492
+ self, gridprops, grid=("ICELL", "JCELL", "KCELL"), prop_id="_model"
1493
+ ):
1494
+ """Look through a Grid and add a set of grid properties as logs.
1495
+
1496
+ The name of the logs will ...
1497
+
1498
+ This can be done to sample model properties along a well.
1499
+
1500
+ Args:
1501
+ gridprops (Grid): A XTGeo GridProperties instance (a collection
1502
+ of properties) or a single GridProperty instance
1503
+ grid (Grid or tuple): A XTGeo Grid instance or a reference
1504
+ via tuple. If this is tuple with log names,
1505
+ it states that these logs already contains
1506
+ the gridcell IJK numbering.
1507
+ prop_id (str): Add a tag (optional) to the current log name, e.g
1508
+ as PORO_model, where _model is the tag.
1509
+
1510
+ Raises:
1511
+ None
1512
+
1513
+ .. versionadded:: 2.1
1514
+
1515
+ """
1516
+ _well_oper.get_gridproperties(self, gridprops, grid=grid, prop_id=prop_id)