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.
- cxtgeo.py +558 -0
- cxtgeoPYTHON_wrap.c +19537 -0
- xtgeo/__init__.py +248 -0
- xtgeo/_cxtgeo.cp313-win_amd64.pyd +0 -0
- xtgeo/_internal.cp313-win_amd64.pyd +0 -0
- xtgeo/common/__init__.py +19 -0
- xtgeo/common/_angles.py +29 -0
- xtgeo/common/_xyz_enum.py +50 -0
- xtgeo/common/calc.py +396 -0
- xtgeo/common/constants.py +30 -0
- xtgeo/common/exceptions.py +42 -0
- xtgeo/common/log.py +93 -0
- xtgeo/common/sys.py +166 -0
- xtgeo/common/types.py +18 -0
- xtgeo/common/version.py +34 -0
- xtgeo/common/xtgeo_dialog.py +604 -0
- xtgeo/cube/__init__.py +9 -0
- xtgeo/cube/_cube_export.py +214 -0
- xtgeo/cube/_cube_import.py +532 -0
- xtgeo/cube/_cube_roxapi.py +180 -0
- xtgeo/cube/_cube_utils.py +287 -0
- xtgeo/cube/_cube_window_attributes.py +273 -0
- xtgeo/cube/cube1.py +1023 -0
- xtgeo/grid3d/__init__.py +15 -0
- xtgeo/grid3d/_ecl_grid.py +778 -0
- xtgeo/grid3d/_ecl_inte_head.py +152 -0
- xtgeo/grid3d/_ecl_logi_head.py +71 -0
- xtgeo/grid3d/_ecl_output_file.py +81 -0
- xtgeo/grid3d/_egrid.py +1004 -0
- xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
- xtgeo/grid3d/_grdecl_format.py +309 -0
- xtgeo/grid3d/_grdecl_grid.py +400 -0
- xtgeo/grid3d/_grid3d.py +29 -0
- xtgeo/grid3d/_grid3d_fence.py +284 -0
- xtgeo/grid3d/_grid3d_utils.py +228 -0
- xtgeo/grid3d/_grid_boundary.py +76 -0
- xtgeo/grid3d/_grid_etc1.py +1683 -0
- xtgeo/grid3d/_grid_export.py +222 -0
- xtgeo/grid3d/_grid_hybrid.py +50 -0
- xtgeo/grid3d/_grid_import.py +79 -0
- xtgeo/grid3d/_grid_import_ecl.py +101 -0
- xtgeo/grid3d/_grid_import_roff.py +135 -0
- xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
- xtgeo/grid3d/_grid_refine.py +258 -0
- xtgeo/grid3d/_grid_roxapi.py +292 -0
- xtgeo/grid3d/_grid_translate_coords.py +154 -0
- xtgeo/grid3d/_grid_wellzone.py +165 -0
- xtgeo/grid3d/_gridprop_export.py +202 -0
- xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
- xtgeo/grid3d/_gridprop_import_grdecl.py +132 -0
- xtgeo/grid3d/_gridprop_import_roff.py +52 -0
- xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
- xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
- xtgeo/grid3d/_gridprop_op1.py +272 -0
- xtgeo/grid3d/_gridprop_roxapi.py +301 -0
- xtgeo/grid3d/_gridprop_value_init.py +140 -0
- xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
- xtgeo/grid3d/_gridprops_import_roff.py +83 -0
- xtgeo/grid3d/_roff_grid.py +470 -0
- xtgeo/grid3d/_roff_parameter.py +303 -0
- xtgeo/grid3d/grid.py +3010 -0
- xtgeo/grid3d/grid_properties.py +699 -0
- xtgeo/grid3d/grid_property.py +1313 -0
- xtgeo/grid3d/types.py +15 -0
- xtgeo/interfaces/rms/__init__.py +18 -0
- xtgeo/interfaces/rms/_regular_surface.py +460 -0
- xtgeo/interfaces/rms/_rms_base.py +100 -0
- xtgeo/interfaces/rms/_rmsapi_package.py +69 -0
- xtgeo/interfaces/rms/rmsapi_utils.py +438 -0
- xtgeo/io/__init__.py +1 -0
- xtgeo/io/_file.py +603 -0
- xtgeo/metadata/__init__.py +17 -0
- xtgeo/metadata/metadata.py +435 -0
- xtgeo/roxutils/__init__.py +7 -0
- xtgeo/roxutils/_roxar_loader.py +54 -0
- xtgeo/roxutils/_roxutils_etc.py +122 -0
- xtgeo/roxutils/roxutils.py +207 -0
- xtgeo/surface/__init__.py +20 -0
- xtgeo/surface/_regsurf_boundary.py +26 -0
- xtgeo/surface/_regsurf_cube.py +210 -0
- xtgeo/surface/_regsurf_cube_window.py +391 -0
- xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
- xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
- xtgeo/surface/_regsurf_export.py +388 -0
- xtgeo/surface/_regsurf_grid3d.py +275 -0
- xtgeo/surface/_regsurf_gridding.py +347 -0
- xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
- xtgeo/surface/_regsurf_import.py +347 -0
- xtgeo/surface/_regsurf_lowlevel.py +122 -0
- xtgeo/surface/_regsurf_oper.py +538 -0
- xtgeo/surface/_regsurf_utils.py +81 -0
- xtgeo/surface/_surfs_import.py +43 -0
- xtgeo/surface/_zmap_parser.py +138 -0
- xtgeo/surface/regular_surface.py +3043 -0
- xtgeo/surface/surfaces.py +276 -0
- xtgeo/well/__init__.py +24 -0
- xtgeo/well/_blockedwell_roxapi.py +241 -0
- xtgeo/well/_blockedwells_roxapi.py +68 -0
- xtgeo/well/_well_aux.py +30 -0
- xtgeo/well/_well_io.py +327 -0
- xtgeo/well/_well_oper.py +483 -0
- xtgeo/well/_well_roxapi.py +304 -0
- xtgeo/well/_wellmarkers.py +486 -0
- xtgeo/well/_wells_utils.py +158 -0
- xtgeo/well/blocked_well.py +220 -0
- xtgeo/well/blocked_wells.py +134 -0
- xtgeo/well/well1.py +1516 -0
- xtgeo/well/wells.py +211 -0
- xtgeo/xyz/__init__.py +6 -0
- xtgeo/xyz/_polygons_oper.py +272 -0
- xtgeo/xyz/_xyz.py +758 -0
- xtgeo/xyz/_xyz_data.py +646 -0
- xtgeo/xyz/_xyz_io.py +737 -0
- xtgeo/xyz/_xyz_lowlevel.py +42 -0
- xtgeo/xyz/_xyz_oper.py +613 -0
- xtgeo/xyz/_xyz_roxapi.py +766 -0
- xtgeo/xyz/points.py +698 -0
- xtgeo/xyz/polygons.py +827 -0
- xtgeo-4.14.1.dist-info/METADATA +146 -0
- xtgeo-4.14.1.dist-info/RECORD +122 -0
- xtgeo-4.14.1.dist-info/WHEEL +5 -0
- 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)
|