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/xyz/_xyz_roxapi.py
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""RMSAPI (former Roxar API) functions for XTGeo Points/Polygons, i.e. XYZ data."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Literal, Type, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from xtgeo.common._xyz_enum import _AttrName, _XYZType
|
|
13
|
+
from xtgeo.common.constants import UNDEF, UNDEF_INT, UNDEF_INT_LIMIT, UNDEF_LIMIT
|
|
14
|
+
from xtgeo.common.log import null_logger
|
|
15
|
+
from xtgeo.roxutils import RoxUtils
|
|
16
|
+
from xtgeo.roxutils._roxar_loader import RoxarType, roxar, roxar_well_picks
|
|
17
|
+
from xtgeo.xyz import points, polygons
|
|
18
|
+
|
|
19
|
+
if roxar:
|
|
20
|
+
roxwp = roxar_well_picks
|
|
21
|
+
|
|
22
|
+
logger = null_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class STYPE(str, Enum):
|
|
26
|
+
HORIZONS = "horizons"
|
|
27
|
+
ZONES = "zones"
|
|
28
|
+
CLIPBOARD = "clipboard"
|
|
29
|
+
GENERAL2D_DATA = "general2d_data"
|
|
30
|
+
FAULTS = "faults"
|
|
31
|
+
WELL_PICKS = "well_picks"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _missing_(cls: Type[STYPE], value: object) -> None:
|
|
35
|
+
valid_values = [m.value for m in cls]
|
|
36
|
+
raise ValueError(f"Invalid stype {value}. Valid entries are {valid_values}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
XYZ_COLUMNS = [_AttrName.XNAME.value, _AttrName.YNAME.value, _AttrName.ZNAME.value]
|
|
40
|
+
REQUIRED_WELL_PICK_ATTRIBUTES = [
|
|
41
|
+
_AttrName.M_MD_NAME.value,
|
|
42
|
+
_AttrName.WELLNAME.value,
|
|
43
|
+
_AttrName.TRAJECTORY.value,
|
|
44
|
+
]
|
|
45
|
+
VALID_WELL_PICK_TYPES = ["fault", "horizon"] # note, not plural as in STYPE
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_input_and_version_requirement(
|
|
49
|
+
roxutils: RoxUtils,
|
|
50
|
+
stype: STYPE,
|
|
51
|
+
category: str | list[str] | None,
|
|
52
|
+
attributes: list[str] | bool | None,
|
|
53
|
+
xyztype: _XYZType,
|
|
54
|
+
mode: Literal["set", "get"],
|
|
55
|
+
) -> None:
|
|
56
|
+
"""General check of some input values."""
|
|
57
|
+
|
|
58
|
+
if attributes is not None:
|
|
59
|
+
if mode == "get" and not isinstance(attributes, (list, bool)):
|
|
60
|
+
raise TypeError("'attributes' argument can only be of type list or bool")
|
|
61
|
+
if mode == "set" and not isinstance(attributes, bool):
|
|
62
|
+
raise TypeError("'attributes' argument can only be of type bool")
|
|
63
|
+
|
|
64
|
+
logger.info("The stype is: %s", stype.value)
|
|
65
|
+
|
|
66
|
+
# note: check of clipboard va Roxar API 1.2 is now removed as usage of
|
|
67
|
+
# such old API versions is obsolute.
|
|
68
|
+
if stype in (
|
|
69
|
+
STYPE.GENERAL2D_DATA,
|
|
70
|
+
STYPE.WELL_PICKS,
|
|
71
|
+
) and not roxutils.version_required("1.6"):
|
|
72
|
+
raise NotImplementedError(
|
|
73
|
+
f"API Support for {stype.value} is missing in this RMS version "
|
|
74
|
+
f"(current API version is {roxutils.roxversion} - required is 1.6)"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if stype == STYPE.WELL_PICKS:
|
|
78
|
+
if category not in VALID_WELL_PICK_TYPES:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid {category=}. Valid entries are {VALID_WELL_PICK_TYPES}"
|
|
81
|
+
)
|
|
82
|
+
if xyztype == _XYZType.POLYGONS.value:
|
|
83
|
+
raise ValueError(f"Polygons does not support stype={stype.value}.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_presence_in_project(
|
|
87
|
+
rox: RoxUtils,
|
|
88
|
+
name: str,
|
|
89
|
+
category: str | list[str] | None,
|
|
90
|
+
stype: STYPE,
|
|
91
|
+
realisation: int,
|
|
92
|
+
mode: Literal["set", "get"] = "get",
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Helper to check if valid placeholder' whithin RMS."""
|
|
95
|
+
|
|
96
|
+
logger.warning("Realisation %s not in use", realisation)
|
|
97
|
+
|
|
98
|
+
project_attr = getattr(rox.project, stype)
|
|
99
|
+
|
|
100
|
+
if stype in [STYPE.HORIZONS, STYPE.ZONES, STYPE.FAULTS]:
|
|
101
|
+
if name not in project_attr:
|
|
102
|
+
raise ValueError(f"Cannot access {name=} in {stype.value}")
|
|
103
|
+
if category is None:
|
|
104
|
+
raise ValueError("Need to specify category for horizons, zones and faults")
|
|
105
|
+
if isinstance(category, list) or category not in project_attr.representations:
|
|
106
|
+
raise ValueError(f"Cannot access {category=} in {stype.value}")
|
|
107
|
+
if mode == "get" and project_attr[name][category].is_empty():
|
|
108
|
+
raise RuntimeError(f"'{name}' is empty for {stype.value} {category=}")
|
|
109
|
+
|
|
110
|
+
# only need to check presence in clipboard/general2d_data/well_picks if mode = get.
|
|
111
|
+
if mode == "get":
|
|
112
|
+
if stype in [STYPE.CLIPBOARD, STYPE.GENERAL2D_DATA]:
|
|
113
|
+
folders = _get_rox_clipboard_folders(category)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
rox_folder = project_attr.folders[folders]
|
|
117
|
+
except KeyError as keyerr:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Cannot access clipboards folder (not existing?)"
|
|
120
|
+
) from keyerr
|
|
121
|
+
|
|
122
|
+
if name not in rox_folder:
|
|
123
|
+
raise ValueError(f"Name {name} is not within Clipboard...")
|
|
124
|
+
|
|
125
|
+
if stype == STYPE.WELL_PICKS and name not in project_attr.sets:
|
|
126
|
+
raise ValueError(f"Well pick set {name} is not within Well Picks.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_xyz_from_rms(
|
|
130
|
+
project: RoxarType.project,
|
|
131
|
+
name: str,
|
|
132
|
+
category: str | list[str],
|
|
133
|
+
stype: str = STYPE.HORIZONS.value,
|
|
134
|
+
realisation: int = 0,
|
|
135
|
+
attributes: list[str] | bool | None = False,
|
|
136
|
+
xyztype: str = _XYZType.POINTS.value,
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Import a Points or Polygons item via ROXAR API spec.
|
|
139
|
+
|
|
140
|
+
'Import' means transfer of data from Roxar API memory space to XTGeo memory space.
|
|
141
|
+
|
|
142
|
+
~ a part of a classmethod, and it will return the following kwargs to __init__()::
|
|
143
|
+
|
|
144
|
+
xname
|
|
145
|
+
yname
|
|
146
|
+
zname
|
|
147
|
+
pname # optional for Polygons
|
|
148
|
+
name
|
|
149
|
+
dataframe
|
|
150
|
+
values=None # since dataframe is set
|
|
151
|
+
filesrc
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
rox = RoxUtils(project, readonly=True)
|
|
155
|
+
stype = STYPE(stype.lower())
|
|
156
|
+
|
|
157
|
+
_check_input_and_version_requirement(
|
|
158
|
+
rox, stype, category, attributes, xyztype, mode="get"
|
|
159
|
+
)
|
|
160
|
+
_check_presence_in_project(rox, name, category, stype, realisation, mode="get")
|
|
161
|
+
|
|
162
|
+
if stype == STYPE.WELL_PICKS:
|
|
163
|
+
category = cast("Literal['fault', 'horizon']", category)
|
|
164
|
+
result = _load_wellpicks_from_rms(
|
|
165
|
+
rox=rox,
|
|
166
|
+
well_pick_set=name,
|
|
167
|
+
wp_category=category,
|
|
168
|
+
attributes=attributes,
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
result = _load_xyz_from_rms(
|
|
172
|
+
rox, name, category, stype, realisation, xyztype, attributes
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
rox.safe_close()
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _load_xyz_from_rms(
|
|
180
|
+
rox: RoxUtils,
|
|
181
|
+
name: str,
|
|
182
|
+
category: str | list[str] | None,
|
|
183
|
+
stype: STYPE,
|
|
184
|
+
realisation: int,
|
|
185
|
+
xyztype: str,
|
|
186
|
+
attributes: bool | list[str],
|
|
187
|
+
) -> dict[str, str | dict | pd.DataFrame]:
|
|
188
|
+
"""From RMS Points, Polygons, Picks to XTGeo"""
|
|
189
|
+
|
|
190
|
+
kwargs: dict[str, str | dict | pd.DataFrame] = {
|
|
191
|
+
"xname": _AttrName.XNAME.value,
|
|
192
|
+
"yname": _AttrName.YNAME.value,
|
|
193
|
+
"zname": _AttrName.ZNAME.value,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if xyztype == _XYZType.POLYGONS.value:
|
|
197
|
+
kwargs["pname"] = _AttrName.PNAME.value
|
|
198
|
+
|
|
199
|
+
roxxyz = _get_roxitem(
|
|
200
|
+
rox,
|
|
201
|
+
name,
|
|
202
|
+
category,
|
|
203
|
+
stype,
|
|
204
|
+
mode="get",
|
|
205
|
+
xyztype=xyztype,
|
|
206
|
+
)
|
|
207
|
+
values = _get_roxvalues(roxxyz, realisation=realisation)
|
|
208
|
+
|
|
209
|
+
dfr = _rmsapi_xyz_to_dataframe(values, xyztype)
|
|
210
|
+
|
|
211
|
+
# handling attributes for points and polygons/polylines, from Roxar API version 1.6
|
|
212
|
+
if attributes:
|
|
213
|
+
attr_names = roxxyz.get_attributes_names(realisation=realisation)
|
|
214
|
+
if isinstance(attributes, list):
|
|
215
|
+
attr_names = [a for a in attr_names if a in attributes]
|
|
216
|
+
logger.info("XYZ attribute names are: %s", attr_names)
|
|
217
|
+
attr_dict = _get_rox_attrvalues(roxxyz, attr_names, realisation=realisation)
|
|
218
|
+
|
|
219
|
+
poly_id = None if xyztype == _XYZType.POINTS.value else kwargs["pname"]
|
|
220
|
+
|
|
221
|
+
dfr, datatypes = _add_attributes_to_dataframe(dfr, attr_dict, xyztype, poly_id)
|
|
222
|
+
kwargs["attributes"] = datatypes
|
|
223
|
+
|
|
224
|
+
kwargs["values"] = dfr
|
|
225
|
+
return kwargs
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _rmsapi_xyz_to_dataframe(
|
|
229
|
+
roxxyz: list[np.ndarray] | np.ndarray, xyztype: str = _XYZType.POINTS.value
|
|
230
|
+
) -> pd.DataFrame:
|
|
231
|
+
"""Transforming some XYZ from ROXAPI/RMSAPI to a Pandas dataframe."""
|
|
232
|
+
|
|
233
|
+
# In ROXAPI, polygons/polylines are a list of numpies, while
|
|
234
|
+
# points is just a numpy array. Hence a polyg* may be identified
|
|
235
|
+
# by being a list after import
|
|
236
|
+
|
|
237
|
+
if xyztype == _XYZType.POLYGONS.value and isinstance(roxxyz, list):
|
|
238
|
+
# polylines/-gons
|
|
239
|
+
dfs = []
|
|
240
|
+
for idx, poly in enumerate(roxxyz):
|
|
241
|
+
dataset = pd.DataFrame.from_records(poly, columns=XYZ_COLUMNS)
|
|
242
|
+
dataset["POLY_ID"] = idx
|
|
243
|
+
dfs.append(dataset)
|
|
244
|
+
|
|
245
|
+
dfr = pd.concat(dfs)
|
|
246
|
+
|
|
247
|
+
elif xyztype == _XYZType.POINTS.value and isinstance(roxxyz, np.ndarray):
|
|
248
|
+
# points
|
|
249
|
+
dfr = pd.DataFrame.from_records(roxxyz, columns=XYZ_COLUMNS)
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
raise RuntimeError(f"Unknown error in getting data from RMS: {type(roxxyz)}")
|
|
253
|
+
|
|
254
|
+
dfr.reset_index(drop=True, inplace=True)
|
|
255
|
+
return dfr
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _infer_type(values: np.array) -> Literal["str", "float", "int"]:
|
|
259
|
+
"""Helper function for points/polygons attributes type."""
|
|
260
|
+
dtype = str(values.dtype)
|
|
261
|
+
if "int" in dtype:
|
|
262
|
+
return "int"
|
|
263
|
+
if "float" in dtype:
|
|
264
|
+
return "float"
|
|
265
|
+
return "str"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _add_attributes_to_dataframe(
|
|
269
|
+
dfr: pd.DataFrame,
|
|
270
|
+
attributes: dict,
|
|
271
|
+
xyztype: _XYZType,
|
|
272
|
+
poly_id_name: str = _AttrName.PNAME.value,
|
|
273
|
+
) -> tuple[pd.DataFrame, dict[str, str]]:
|
|
274
|
+
"""Add attributes to dataframe (points and polygons) for Roxar API ver 1.6+
|
|
275
|
+
|
|
276
|
+
Note that Points and Polygons behave a bit different, as for Polygons only
|
|
277
|
+
a value per POLY_ID is given in a list, while Points deals with numpy arrays.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
logger.info("Attributes adding to dataframe...")
|
|
281
|
+
newdfr = dfr.copy()
|
|
282
|
+
|
|
283
|
+
datatypes = {}
|
|
284
|
+
for name, values in attributes.items():
|
|
285
|
+
datatypes[name] = _infer_type(values)
|
|
286
|
+
|
|
287
|
+
fill_value = (
|
|
288
|
+
UNDEF_INT
|
|
289
|
+
if datatypes[name] == "int"
|
|
290
|
+
else UNDEF
|
|
291
|
+
if datatypes[name] == "float"
|
|
292
|
+
else "UNDEF"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if xyztype == _XYZType.POLYGONS.value:
|
|
296
|
+
id_to_name = {id_no: value for id_no, value in enumerate(values)} # noqa: C416
|
|
297
|
+
newdfr[name] = newdfr[poly_id_name].map(id_to_name)
|
|
298
|
+
else:
|
|
299
|
+
newdfr[name] = np.ma.filled(values, fill_value=fill_value)
|
|
300
|
+
|
|
301
|
+
return newdfr, datatypes
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def save_xyz_to_rms(
|
|
305
|
+
self: points.Points | polygons.Polygons,
|
|
306
|
+
project: RoxarType.Project,
|
|
307
|
+
name: str,
|
|
308
|
+
category: str | list[str] | None,
|
|
309
|
+
stype: str,
|
|
310
|
+
pfilter: dict[str, list],
|
|
311
|
+
realisation: int,
|
|
312
|
+
attributes: bool = False,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Export (store) a XYZ item from XTGeo to RMS via ROXAR API spec."""
|
|
315
|
+
|
|
316
|
+
rox = RoxUtils(project, readonly=False)
|
|
317
|
+
stype = STYPE(stype.lower())
|
|
318
|
+
|
|
319
|
+
_check_input_and_version_requirement(
|
|
320
|
+
rox, stype, category, attributes, self._xyztype, mode="set"
|
|
321
|
+
)
|
|
322
|
+
_check_presence_in_project(rox, name, category, stype, realisation, mode="set")
|
|
323
|
+
|
|
324
|
+
if stype == STYPE.WELL_PICKS:
|
|
325
|
+
assert isinstance(self, points.Points)
|
|
326
|
+
category = cast("Literal['fault', 'horizon']", category)
|
|
327
|
+
_save_well_picks_to_rms(self, rox, name, category, attributes, pfilter)
|
|
328
|
+
else:
|
|
329
|
+
_save_xyz_to_rms(
|
|
330
|
+
self, rox, name, category, stype, pfilter, realisation, attributes
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if rox._roxexternal:
|
|
334
|
+
rox.project.save()
|
|
335
|
+
|
|
336
|
+
rox.safe_close()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _save_xyz_to_rms(
|
|
340
|
+
self: points.Points | polygons.Polygons,
|
|
341
|
+
rox: RoxUtils,
|
|
342
|
+
name: str,
|
|
343
|
+
category: str | list[str] | None,
|
|
344
|
+
stype: STYPE,
|
|
345
|
+
pfilter: dict[str, list] | None,
|
|
346
|
+
realisation: int,
|
|
347
|
+
attributes: bool,
|
|
348
|
+
) -> None:
|
|
349
|
+
logger.warning("Realisation %s not in use", realisation)
|
|
350
|
+
|
|
351
|
+
df = self.get_dataframe()
|
|
352
|
+
if df is None or df.empty:
|
|
353
|
+
logger.warning("Empty dataframe! Skipping object update...")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
xyz_columns = [self.xname, self.yname, self.zname]
|
|
357
|
+
if not set(xyz_columns).issubset(df.columns):
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"One or all {xyz_columns=} are missing in the dataframe, "
|
|
360
|
+
f"available columns are {list(df.columns)}! "
|
|
361
|
+
"Rename your columns or update the corresponding 'xname', "
|
|
362
|
+
"'yname' and 'zname' attributes to columns in your dataframe."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
roxxyz = _get_roxitem(
|
|
366
|
+
rox,
|
|
367
|
+
name,
|
|
368
|
+
category,
|
|
369
|
+
stype,
|
|
370
|
+
mode="set",
|
|
371
|
+
xyztype=self._xyztype,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
df = _apply_pfilter_to_dataframe(df, pfilter)
|
|
375
|
+
if df.empty:
|
|
376
|
+
logger.warning("Empty dataframe after filtering! Skipping object update...")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
arrxyz = (
|
|
380
|
+
[polydf[xyz_columns].to_numpy() for _, polydf in df.groupby(self.pname)]
|
|
381
|
+
if self._xyztype == _XYZType.POLYGONS.value
|
|
382
|
+
else df[xyz_columns].to_numpy()
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
roxxyz.set_values(arrxyz)
|
|
386
|
+
|
|
387
|
+
if attributes:
|
|
388
|
+
if self._xyztype == _XYZType.POINTS.value:
|
|
389
|
+
for attr in _get_attribute_names_from_dataframe(df, xyz_columns):
|
|
390
|
+
values = _replace_undefined_values(
|
|
391
|
+
values=df[attr].values, dtype=self._attrs.get(attr), asmasked=True
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
logger.info("Store Point attribute %s to Roxar API", name)
|
|
395
|
+
roxxyz.set_attribute_values(attr, values)
|
|
396
|
+
elif self._xyztype == _XYZType.POLYGONS.value:
|
|
397
|
+
raise NotImplementedError(
|
|
398
|
+
"Setting attributes for Polygons is not implemented in RMS API"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _get_attribute_names_from_dataframe(
|
|
403
|
+
df: pd.DataFrame, xyz_columns: list[str] | None = None
|
|
404
|
+
) -> list[str]:
|
|
405
|
+
xyz_columns = xyz_columns or XYZ_COLUMNS
|
|
406
|
+
return [col for col in df.columns if col not in xyz_columns]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _get_attribute_type_from_values(values: np.ndarray) -> str:
|
|
410
|
+
if "float" in str(values.dtype):
|
|
411
|
+
return "float"
|
|
412
|
+
if "int" in str(values.dtype):
|
|
413
|
+
return "int"
|
|
414
|
+
return "str"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _replace_undefined_values(
|
|
418
|
+
values: np.ndarray,
|
|
419
|
+
dtype: str | None = None,
|
|
420
|
+
asmasked: bool = False,
|
|
421
|
+
) -> np.ndarray | np.ma.MaskedArray:
|
|
422
|
+
"""
|
|
423
|
+
Set xtgeo UNDEF values to np.nan or empty string dependent on type.
|
|
424
|
+
With option to return array with masked values instead of np.nan.
|
|
425
|
+
"""
|
|
426
|
+
values = pd.to_numeric(values, errors="ignore")
|
|
427
|
+
|
|
428
|
+
dtype = dtype or _get_attribute_type_from_values(values)
|
|
429
|
+
|
|
430
|
+
if dtype == "float":
|
|
431
|
+
if asmasked:
|
|
432
|
+
return np.ma.masked_greater(values, UNDEF_LIMIT)
|
|
433
|
+
return np.where(values > UNDEF_LIMIT, np.nan, values)
|
|
434
|
+
|
|
435
|
+
if dtype == "int":
|
|
436
|
+
if asmasked:
|
|
437
|
+
return np.ma.masked_greater(values, UNDEF_INT_LIMIT)
|
|
438
|
+
return np.where(values > UNDEF_INT_LIMIT, np.nan, values)
|
|
439
|
+
|
|
440
|
+
# string attributes does not support nan values
|
|
441
|
+
# and requires string type array returned
|
|
442
|
+
values = values.astype(str)
|
|
443
|
+
return np.where(np.isin(values, ["UNDEF", "nan"]), "", values)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _apply_pfilter_to_dataframe(
|
|
447
|
+
df: pd.DataFrame, pfilter: dict[str, list] | None
|
|
448
|
+
) -> pd.DataFrame:
|
|
449
|
+
if pfilter is not None:
|
|
450
|
+
for key, val in pfilter.items():
|
|
451
|
+
if key not in df:
|
|
452
|
+
raise KeyError(
|
|
453
|
+
f"The requested pfilter key {key} was not found in dataframe. "
|
|
454
|
+
f"Valid keys are {list(df.columns)}"
|
|
455
|
+
)
|
|
456
|
+
df = df.loc[df[key].isin(val)]
|
|
457
|
+
return df
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _get_rox_clipboard_folders(category: str | list[str] | None) -> list[str]:
|
|
461
|
+
if category is None or category == "":
|
|
462
|
+
return []
|
|
463
|
+
|
|
464
|
+
if isinstance(category, list):
|
|
465
|
+
return category
|
|
466
|
+
|
|
467
|
+
if isinstance(category, str):
|
|
468
|
+
if "|" in category:
|
|
469
|
+
return category.split("|")
|
|
470
|
+
if "/" in category:
|
|
471
|
+
return category.split("/")
|
|
472
|
+
return [category]
|
|
473
|
+
|
|
474
|
+
raise RuntimeError(f"Cannot parse category: {category}, see documentation!")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _get_roxitem(
|
|
478
|
+
rox: RoxUtils,
|
|
479
|
+
name: str,
|
|
480
|
+
category: str | list[str] | None,
|
|
481
|
+
stype: STYPE,
|
|
482
|
+
mode: Literal["set", "get"] = "set",
|
|
483
|
+
xyztype: str = _XYZType.POINTS.value,
|
|
484
|
+
) -> Any:
|
|
485
|
+
"""Get the correct rox_xyz which is some pointer to a RoxarAPI structure."""
|
|
486
|
+
|
|
487
|
+
project_attr = getattr(rox.project, stype)
|
|
488
|
+
|
|
489
|
+
if stype not in [STYPE.CLIPBOARD, STYPE.GENERAL2D_DATA]:
|
|
490
|
+
return project_attr[name][category]
|
|
491
|
+
|
|
492
|
+
folders = _get_rox_clipboard_folders(category)
|
|
493
|
+
if mode == "get":
|
|
494
|
+
return project_attr.folders[folders][name]
|
|
495
|
+
|
|
496
|
+
# clipboard folders will be created if not present, and overwritten else
|
|
497
|
+
return (
|
|
498
|
+
project_attr.create_polylines(name, folders)
|
|
499
|
+
if xyztype == _XYZType.POLYGONS.value
|
|
500
|
+
else project_attr.create_points(name, folders)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _get_roxvalues(rox_xyz: Any, realisation: int = 0) -> list[np.ndarray] | np.ndarray:
|
|
505
|
+
"""Return primary values from the Roxar API, numpy (Points) or list (Polygons)."""
|
|
506
|
+
try:
|
|
507
|
+
roxitem = rox_xyz.get_values(realisation)
|
|
508
|
+
logger.info(roxitem)
|
|
509
|
+
except KeyError as kwe:
|
|
510
|
+
logger.error(kwe)
|
|
511
|
+
|
|
512
|
+
return roxitem
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _get_rox_attrvalues(
|
|
516
|
+
rox_xyz: Any, attrnames: list[str], realisation: int = 0
|
|
517
|
+
) -> dict[str, list[np.ndarray] | np.ndarray]:
|
|
518
|
+
"""Return attributes from the Roxar API, numpy (Points) or list (Polygons)."""
|
|
519
|
+
roxitems = {}
|
|
520
|
+
for attrname in attrnames:
|
|
521
|
+
values = rox_xyz.get_attribute_values(attrname, realisation=realisation)
|
|
522
|
+
roxitems[attrname] = values
|
|
523
|
+
return roxitems
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _load_wellpicks_from_rms(
|
|
527
|
+
rox: RoxUtils,
|
|
528
|
+
well_pick_set: str,
|
|
529
|
+
wp_category: Literal["fault", "horizon"] = "horizon",
|
|
530
|
+
attributes: bool | list[str] = False,
|
|
531
|
+
) -> dict[str, str | pd.DataFrame | dict]:
|
|
532
|
+
"""From RMS to XTGeo"""
|
|
533
|
+
|
|
534
|
+
rox_wp = rox.project.well_picks
|
|
535
|
+
rox_wp_set = rox_wp.sets[well_pick_set]
|
|
536
|
+
|
|
537
|
+
well_picks = [wp for wp in rox_wp_set if wp.type.name == wp_category]
|
|
538
|
+
if len(well_picks) == 0:
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"No well picks of type '{wp_category}' found in {well_pick_set=}."
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
attribute_types = {}
|
|
544
|
+
if attributes:
|
|
545
|
+
rox_attributes = well_picks[0].attributes # first one is valid for all
|
|
546
|
+
for rox_attr in rox_attributes:
|
|
547
|
+
if isinstance(attributes, list) and rox_attr.name not in attributes:
|
|
548
|
+
continue
|
|
549
|
+
attribute_types[rox_attr.name] = rox_attr.type.name
|
|
550
|
+
|
|
551
|
+
dfr = _create_dataframe_from_wellpicks(well_picks, wp_category, attribute_types)
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
"xname": _AttrName.XNAME.value,
|
|
555
|
+
"yname": _AttrName.YNAME.value,
|
|
556
|
+
"zname": _AttrName.ZNAME.value,
|
|
557
|
+
"values": dfr,
|
|
558
|
+
"attributes": attribute_types,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _create_dataframe_from_wellpicks(
|
|
563
|
+
well_picks: list[RoxarType.well_picks.WellPick],
|
|
564
|
+
wp_category: Literal["fault", "horizon"],
|
|
565
|
+
attribute_types: dict[str, str],
|
|
566
|
+
) -> pd.DataFrame:
|
|
567
|
+
"""Create a dataframe from a well pick set, and selected attributes."""
|
|
568
|
+
|
|
569
|
+
items = []
|
|
570
|
+
for wp in well_picks:
|
|
571
|
+
wp_attributes = {attr.name: val for attr, val in wp.get_values().items()}
|
|
572
|
+
|
|
573
|
+
data = {
|
|
574
|
+
_AttrName.XNAME.value: wp_attributes["East"],
|
|
575
|
+
_AttrName.YNAME.value: wp_attributes["North"],
|
|
576
|
+
_AttrName.ZNAME.value: wp_attributes["TVD_MSL"],
|
|
577
|
+
_AttrName.M_MD_NAME.value: wp_attributes["MD"],
|
|
578
|
+
"WELLNAME": wp.trajectory.wellbore.well.name,
|
|
579
|
+
"TRAJECTORY": wp.trajectory.name,
|
|
580
|
+
wp_category.upper(): wp.intersection_object.name,
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for attr, dtype in attribute_types.items():
|
|
584
|
+
if attr in wp_attributes:
|
|
585
|
+
if wp_attributes[attr] is not None:
|
|
586
|
+
data[attr] = wp_attributes[attr]
|
|
587
|
+
else:
|
|
588
|
+
if dtype == "float":
|
|
589
|
+
data[attr] = UNDEF
|
|
590
|
+
elif dtype == "int":
|
|
591
|
+
data[attr] = UNDEF_INT
|
|
592
|
+
else:
|
|
593
|
+
data[attr] = "UNDEF"
|
|
594
|
+
|
|
595
|
+
items.append(data)
|
|
596
|
+
|
|
597
|
+
return pd.DataFrame(items)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _save_well_picks_to_rms(
|
|
601
|
+
self: points.Points,
|
|
602
|
+
rox: RoxUtils,
|
|
603
|
+
well_pick_set: str,
|
|
604
|
+
wp_category: Literal["horizon", "fault"],
|
|
605
|
+
attributes: bool,
|
|
606
|
+
pfilter: dict[str, list] | None,
|
|
607
|
+
) -> None:
|
|
608
|
+
"""
|
|
609
|
+
Export/store as RMS well picks; this is only valid if points belong to wells
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
df = self.get_dataframe()
|
|
613
|
+
if df is None or df.empty:
|
|
614
|
+
logger.warning("Empty dataframe! Skipping object update...")
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
project_attr = getattr(rox.project, f"{wp_category}s")
|
|
618
|
+
rox_wp_type = getattr(roxar.WellPickType, wp_category)
|
|
619
|
+
|
|
620
|
+
df = _apply_pfilter_to_dataframe(df, pfilter)
|
|
621
|
+
if df.empty:
|
|
622
|
+
logger.warning("Empty dataframe after filtering! Skipping object update...")
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
required_columns = REQUIRED_WELL_PICK_ATTRIBUTES + [wp_category.upper()]
|
|
626
|
+
for column in required_columns:
|
|
627
|
+
if column not in df:
|
|
628
|
+
raise ValueError(f"Required {column=} missing in the dataframe.")
|
|
629
|
+
if df[column].isnull().any():
|
|
630
|
+
raise ValueError(f"The required {column=} contains undefined values.")
|
|
631
|
+
|
|
632
|
+
if attributes:
|
|
633
|
+
attr_types = self._attrs
|
|
634
|
+
|
|
635
|
+
attr_types = {}
|
|
636
|
+
for attr in _get_attribute_names_from_dataframe(df):
|
|
637
|
+
if attr not in required_columns:
|
|
638
|
+
if attr in self._attrs:
|
|
639
|
+
attr_types[attr] = self._attrs[attr]
|
|
640
|
+
else:
|
|
641
|
+
attr_types[attr] = _get_attribute_type_from_values(df[attr])
|
|
642
|
+
|
|
643
|
+
rox_wp_attributes = _get_writeable_well_pick_attributes(
|
|
644
|
+
rox, attr_types, rox_wp_type
|
|
645
|
+
)
|
|
646
|
+
for attr in rox_wp_attributes:
|
|
647
|
+
df[attr] = _replace_undefined_values(
|
|
648
|
+
values=df[attr].values, dtype=attr_types.get(attr), asmasked=False
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
mypicks = []
|
|
652
|
+
for well, wp_df in df.groupby("WELLNAME"):
|
|
653
|
+
rox_well_traj = rox.project.wells[well].wellbore.trajectories
|
|
654
|
+
|
|
655
|
+
for _, wp_row in wp_df.iterrows():
|
|
656
|
+
intersection_object_name = wp_row[wp_category.upper()]
|
|
657
|
+
if intersection_object_name not in project_attr:
|
|
658
|
+
raise ValueError(
|
|
659
|
+
f"{wp_category} '{intersection_object_name}' not in project"
|
|
660
|
+
)
|
|
661
|
+
traj_name = wp_row["TRAJECTORY"]
|
|
662
|
+
if traj_name not in rox_well_traj:
|
|
663
|
+
raise ValueError(
|
|
664
|
+
f"Trajectory name '{traj_name}' not present for {well=}"
|
|
665
|
+
)
|
|
666
|
+
wp = roxar.well_picks.WellPick.create(
|
|
667
|
+
intersection_object=project_attr[intersection_object_name],
|
|
668
|
+
trajectory=rox_well_traj[traj_name],
|
|
669
|
+
md=wp_row[_AttrName.M_MD_NAME.value],
|
|
670
|
+
)
|
|
671
|
+
if attributes:
|
|
672
|
+
for attr, rox_attr in rox_wp_attributes.items():
|
|
673
|
+
try:
|
|
674
|
+
wp.set_values({rox_attr: wp_row[attr]})
|
|
675
|
+
except ValueError as err:
|
|
676
|
+
raise ValueError(
|
|
677
|
+
f"Could not assign value '{wp_row[attr]}' to attribute "
|
|
678
|
+
f"'{attr}'. The value type {type(wp_row[attr])} might be "
|
|
679
|
+
f"incompatible with dtype of attribute '{rox_attr.type}'"
|
|
680
|
+
) from err
|
|
681
|
+
|
|
682
|
+
mypicks.append(wp)
|
|
683
|
+
|
|
684
|
+
rox_wps = _get_well_pick_set(rox, well_pick_set, rox_wp_type)
|
|
685
|
+
rox_wps.append(mypicks)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _get_well_pick_set(
|
|
689
|
+
rox: RoxUtils, well_pick_set: str, rox_wp_type: RoxarType.WellPickType
|
|
690
|
+
) -> RoxarType.well_picks.WellPickSet:
|
|
691
|
+
"""
|
|
692
|
+
Function to retrieve a well pick set object. If the given well pick set
|
|
693
|
+
name is not present, it will be created. Otherwise the current well pick
|
|
694
|
+
set will be emptied for the given well pick type.
|
|
695
|
+
"""
|
|
696
|
+
well_pick_sets = rox.project.well_picks.sets
|
|
697
|
+
if well_pick_set not in well_pick_sets:
|
|
698
|
+
well_pick_sets.create(well_pick_set)
|
|
699
|
+
|
|
700
|
+
rox_wps = well_pick_sets[well_pick_set]
|
|
701
|
+
|
|
702
|
+
rox_wps.delete_at([idx for idx, wp in enumerate(rox_wps) if wp.type == rox_wp_type])
|
|
703
|
+
return rox_wps
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _get_writeable_well_pick_attributes(
|
|
707
|
+
rox: RoxUtils,
|
|
708
|
+
attribute_types: dict[str, str],
|
|
709
|
+
rox_wp_type: RoxarType.WellPickType,
|
|
710
|
+
) -> dict[str, RoxarType.well_picks.WellPickAttribute]:
|
|
711
|
+
"""
|
|
712
|
+
Function to retrive a dictionary of regular and user-defined
|
|
713
|
+
roxar WellPickAttribute's. Only writable attributes are
|
|
714
|
+
returned (i.e. not read_only). Attributes not present in the
|
|
715
|
+
project will be created as user-defined attributes.
|
|
716
|
+
"""
|
|
717
|
+
attributes_with_value_constraints = [
|
|
718
|
+
"Structural model",
|
|
719
|
+
"Lock",
|
|
720
|
+
"Quality",
|
|
721
|
+
"Wellpick Symbol - Horizon",
|
|
722
|
+
]
|
|
723
|
+
regular_attributes = {x.name: x for x in roxwp.WellPick.get_attributes(rox_wp_type)}
|
|
724
|
+
user_attributes = {
|
|
725
|
+
x.name: x
|
|
726
|
+
for x in rox.project.well_picks.user_attributes.get_subset(rox_wp_type)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
rox_attributes = {}
|
|
730
|
+
for attr, dtype in attribute_types.items():
|
|
731
|
+
rox_dtype = getattr(roxar.WellPickAttributeType, dtype)
|
|
732
|
+
|
|
733
|
+
if attr in regular_attributes:
|
|
734
|
+
if (
|
|
735
|
+
regular_attributes[attr].read_only
|
|
736
|
+
or attr in attributes_with_value_constraints
|
|
737
|
+
):
|
|
738
|
+
logger.debug("Skipping read-only attribute %s", attr)
|
|
739
|
+
continue
|
|
740
|
+
rox_attributes[attr] = regular_attributes[attr]
|
|
741
|
+
|
|
742
|
+
elif attr in user_attributes:
|
|
743
|
+
if user_attributes[attr].type != rox_dtype:
|
|
744
|
+
raise ValueError(
|
|
745
|
+
f"Attribute type provided for '{attr}': {dtype}, is different "
|
|
746
|
+
f" from existing type in project: {user_attributes[attr].type}.\n"
|
|
747
|
+
"Either delete user defined attribute up-front, "
|
|
748
|
+
"or rename to a new unique attribute name."
|
|
749
|
+
)
|
|
750
|
+
rox_attributes[attr] = user_attributes[attr]
|
|
751
|
+
|
|
752
|
+
else:
|
|
753
|
+
# roxar only supports creating string or float attributes
|
|
754
|
+
if dtype not in ["str", "float"]:
|
|
755
|
+
raise ValueError(
|
|
756
|
+
"Only 'float' or 'str' are valid options for user-defined "
|
|
757
|
+
f"attributes. Found type {dtype} for attribute '{attr}'."
|
|
758
|
+
)
|
|
759
|
+
logger.info("Creating user-defined attribute %s", attr)
|
|
760
|
+
rox_attributes[attr] = rox.project.well_picks.user_attributes.create(
|
|
761
|
+
name=attr,
|
|
762
|
+
pick_type=rox_wp_type,
|
|
763
|
+
data_type=rox_dtype,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
return rox_attributes
|