xtgeo 4.14.1__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. cxtgeo.py +558 -0
  2. cxtgeoPYTHON_wrap.c +19537 -0
  3. xtgeo/__init__.py +248 -0
  4. xtgeo/_cxtgeo.cp313-win_amd64.pyd +0 -0
  5. xtgeo/_internal.cp313-win_amd64.pyd +0 -0
  6. xtgeo/common/__init__.py +19 -0
  7. xtgeo/common/_angles.py +29 -0
  8. xtgeo/common/_xyz_enum.py +50 -0
  9. xtgeo/common/calc.py +396 -0
  10. xtgeo/common/constants.py +30 -0
  11. xtgeo/common/exceptions.py +42 -0
  12. xtgeo/common/log.py +93 -0
  13. xtgeo/common/sys.py +166 -0
  14. xtgeo/common/types.py +18 -0
  15. xtgeo/common/version.py +34 -0
  16. xtgeo/common/xtgeo_dialog.py +604 -0
  17. xtgeo/cube/__init__.py +9 -0
  18. xtgeo/cube/_cube_export.py +214 -0
  19. xtgeo/cube/_cube_import.py +532 -0
  20. xtgeo/cube/_cube_roxapi.py +180 -0
  21. xtgeo/cube/_cube_utils.py +287 -0
  22. xtgeo/cube/_cube_window_attributes.py +273 -0
  23. xtgeo/cube/cube1.py +1023 -0
  24. xtgeo/grid3d/__init__.py +15 -0
  25. xtgeo/grid3d/_ecl_grid.py +778 -0
  26. xtgeo/grid3d/_ecl_inte_head.py +152 -0
  27. xtgeo/grid3d/_ecl_logi_head.py +71 -0
  28. xtgeo/grid3d/_ecl_output_file.py +81 -0
  29. xtgeo/grid3d/_egrid.py +1004 -0
  30. xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
  31. xtgeo/grid3d/_grdecl_format.py +309 -0
  32. xtgeo/grid3d/_grdecl_grid.py +400 -0
  33. xtgeo/grid3d/_grid3d.py +29 -0
  34. xtgeo/grid3d/_grid3d_fence.py +284 -0
  35. xtgeo/grid3d/_grid3d_utils.py +228 -0
  36. xtgeo/grid3d/_grid_boundary.py +76 -0
  37. xtgeo/grid3d/_grid_etc1.py +1683 -0
  38. xtgeo/grid3d/_grid_export.py +222 -0
  39. xtgeo/grid3d/_grid_hybrid.py +50 -0
  40. xtgeo/grid3d/_grid_import.py +79 -0
  41. xtgeo/grid3d/_grid_import_ecl.py +101 -0
  42. xtgeo/grid3d/_grid_import_roff.py +135 -0
  43. xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
  44. xtgeo/grid3d/_grid_refine.py +258 -0
  45. xtgeo/grid3d/_grid_roxapi.py +292 -0
  46. xtgeo/grid3d/_grid_translate_coords.py +154 -0
  47. xtgeo/grid3d/_grid_wellzone.py +165 -0
  48. xtgeo/grid3d/_gridprop_export.py +202 -0
  49. xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
  50. xtgeo/grid3d/_gridprop_import_grdecl.py +132 -0
  51. xtgeo/grid3d/_gridprop_import_roff.py +52 -0
  52. xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
  53. xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
  54. xtgeo/grid3d/_gridprop_op1.py +272 -0
  55. xtgeo/grid3d/_gridprop_roxapi.py +301 -0
  56. xtgeo/grid3d/_gridprop_value_init.py +140 -0
  57. xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
  58. xtgeo/grid3d/_gridprops_import_roff.py +83 -0
  59. xtgeo/grid3d/_roff_grid.py +470 -0
  60. xtgeo/grid3d/_roff_parameter.py +303 -0
  61. xtgeo/grid3d/grid.py +3010 -0
  62. xtgeo/grid3d/grid_properties.py +699 -0
  63. xtgeo/grid3d/grid_property.py +1313 -0
  64. xtgeo/grid3d/types.py +15 -0
  65. xtgeo/interfaces/rms/__init__.py +18 -0
  66. xtgeo/interfaces/rms/_regular_surface.py +460 -0
  67. xtgeo/interfaces/rms/_rms_base.py +100 -0
  68. xtgeo/interfaces/rms/_rmsapi_package.py +69 -0
  69. xtgeo/interfaces/rms/rmsapi_utils.py +438 -0
  70. xtgeo/io/__init__.py +1 -0
  71. xtgeo/io/_file.py +603 -0
  72. xtgeo/metadata/__init__.py +17 -0
  73. xtgeo/metadata/metadata.py +435 -0
  74. xtgeo/roxutils/__init__.py +7 -0
  75. xtgeo/roxutils/_roxar_loader.py +54 -0
  76. xtgeo/roxutils/_roxutils_etc.py +122 -0
  77. xtgeo/roxutils/roxutils.py +207 -0
  78. xtgeo/surface/__init__.py +20 -0
  79. xtgeo/surface/_regsurf_boundary.py +26 -0
  80. xtgeo/surface/_regsurf_cube.py +210 -0
  81. xtgeo/surface/_regsurf_cube_window.py +391 -0
  82. xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
  83. xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
  84. xtgeo/surface/_regsurf_export.py +388 -0
  85. xtgeo/surface/_regsurf_grid3d.py +275 -0
  86. xtgeo/surface/_regsurf_gridding.py +347 -0
  87. xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
  88. xtgeo/surface/_regsurf_import.py +347 -0
  89. xtgeo/surface/_regsurf_lowlevel.py +122 -0
  90. xtgeo/surface/_regsurf_oper.py +538 -0
  91. xtgeo/surface/_regsurf_utils.py +81 -0
  92. xtgeo/surface/_surfs_import.py +43 -0
  93. xtgeo/surface/_zmap_parser.py +138 -0
  94. xtgeo/surface/regular_surface.py +3043 -0
  95. xtgeo/surface/surfaces.py +276 -0
  96. xtgeo/well/__init__.py +24 -0
  97. xtgeo/well/_blockedwell_roxapi.py +241 -0
  98. xtgeo/well/_blockedwells_roxapi.py +68 -0
  99. xtgeo/well/_well_aux.py +30 -0
  100. xtgeo/well/_well_io.py +327 -0
  101. xtgeo/well/_well_oper.py +483 -0
  102. xtgeo/well/_well_roxapi.py +304 -0
  103. xtgeo/well/_wellmarkers.py +486 -0
  104. xtgeo/well/_wells_utils.py +158 -0
  105. xtgeo/well/blocked_well.py +220 -0
  106. xtgeo/well/blocked_wells.py +134 -0
  107. xtgeo/well/well1.py +1516 -0
  108. xtgeo/well/wells.py +211 -0
  109. xtgeo/xyz/__init__.py +6 -0
  110. xtgeo/xyz/_polygons_oper.py +272 -0
  111. xtgeo/xyz/_xyz.py +758 -0
  112. xtgeo/xyz/_xyz_data.py +646 -0
  113. xtgeo/xyz/_xyz_io.py +737 -0
  114. xtgeo/xyz/_xyz_lowlevel.py +42 -0
  115. xtgeo/xyz/_xyz_oper.py +613 -0
  116. xtgeo/xyz/_xyz_roxapi.py +766 -0
  117. xtgeo/xyz/points.py +698 -0
  118. xtgeo/xyz/polygons.py +827 -0
  119. xtgeo-4.14.1.dist-info/METADATA +146 -0
  120. xtgeo-4.14.1.dist-info/RECORD +122 -0
  121. xtgeo-4.14.1.dist-info/WHEEL +5 -0
  122. xtgeo-4.14.1.dist-info/licenses/LICENSE.md +165 -0
xtgeo/xyz/_xyz_data.py ADDED
@@ -0,0 +1,646 @@
1
+ """Module for private _XYZData class.
2
+
3
+ Note that that the design of this targets Well and general XYZ data (Points/Polygons),
4
+ hence the intentions is to let this work as a general 'engine' for dataframe'ish data
5
+ in xtgeo, at least Well, Points, Polygons. (But in the first round, it is implemented
6
+ for Wells only). Dataframes looks like:
7
+
8
+ X_UTME Y_UTMN Z_TVDSS MDepth PHIT KLOGH Sw
9
+ 0 463256.911 5930542.294 -49.0000 0.0000 NaN NaN NaN ...
10
+ 1 463256.912 5930542.295 -48.2859 0.5000 NaN NaN NaN ...
11
+ 2 463256.913 5930542.296 -47.5735 1.0000 NaN NaN NaN ...
12
+ 3 463256.914 5930542.299 -46.8626 1.5000 NaN NaN NaN ...
13
+ 4 463256.916 5930542.302 -46.1533 2.0000 NaN NaN NaN ...
14
+ ... ... ... ... ... ... ...
15
+
16
+ Where each attr (log) has a attr_types dictionary, telling if the columns are treated
17
+ as discrete (DISC) or continuous (CONT). In addition there is a attr_records
18
+ dict, storing the unit+scale for continuous logs/attr (defaulted to tuple ("", "")) or a
19
+ dictionary of codes (defaulted to {}, if the column if DISC type (this is optional,
20
+ and perhaps only relevant for Well data).
21
+
22
+ The 3 first columns are the XYZ coordinates or XY coordinates + value:
23
+ X, Y, Z or X, Y, V. An optional fourth column as also possible as polygon_id.
24
+ All the rest are free 'attributes', which for wells will be well logs. Hence:
25
+
26
+ attr_types ~ refer to attr_types for XYZ and Well data
27
+ attr_records ~ refer to attr_records for Well data and possibly Points/Polygons
28
+
29
+ If a column is added to the dataframe, then the methods here will try to guess the
30
+ attr_type and attr_record, and add those; similarly of a column is removed, the
31
+ corresponding entries in attr_types and attr_records will be deleted.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import math
37
+ from copy import deepcopy
38
+ from typing import TYPE_CHECKING, Literal
39
+
40
+ import numpy as np
41
+ import pandas as pd
42
+ from joblib import hash as jhash
43
+
44
+ from xtgeo import _cxtgeo
45
+ from xtgeo._cxtgeo import XTGeoCLibError
46
+ from xtgeo.common._xyz_enum import _AttrName, _AttrType, _XYZType
47
+ from xtgeo.common.constants import UNDEF_CONT, UNDEF_DISC
48
+ from xtgeo.common.log import null_logger
49
+ from xtgeo.common.sys import _convert_carr_double_np, _get_carray
50
+
51
+ if TYPE_CHECKING:
52
+ from collections.abc import Sequence
53
+
54
+
55
+ logger = null_logger(__name__)
56
+
57
+
58
+ CONT_DEFAULT_RECORD = ("", "") # unit and scale, where emptry string indicates ~unknown
59
+
60
+
61
+ class _XYZData:
62
+ """Private class for the XYZ and Well log data, where a Pandas dataframe is core.
63
+
64
+ The data are stored in pandas dataframes, and by default, all columns are float, and
65
+ np.nan defines undefined values. Even if they are DISC. The reason for this is
66
+ restrictions in older versions of Pandas.
67
+
68
+ All values in the dataframe shall be numbers.
69
+
70
+ The attr_types is on form {"PHIT": CONT, "FACIES": DISC, ...}
71
+
72
+ The attr_records is somewhat heterogeneous, on form:
73
+ {"PHIT": ("unit", "scale"), "FACIES": {0:BG, 2: "SST", 4: "CALC"}}
74
+ Hence the CONT logs hold a tuple or list with 2 str members, or None, while DISC
75
+ log holds a dict where the key is an int and the value is a string.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ dataframe: pd.DataFrame,
81
+ attr_types: dict[str, str] | None = None,
82
+ attr_records: dict[str, dict[int, str] | Sequence[str]] | None = None,
83
+ xname: str = _AttrName.XNAME.value,
84
+ yname: str = _AttrName.YNAME.value,
85
+ zname: str = _AttrName.ZNAME.value,
86
+ idname: str | None = None, # Well, Polygon, ...
87
+ undef: float | Sequence[float, float] = -999.0,
88
+ xyztype: Literal["well", "points", "polygons"] = "well",
89
+ floatbits: Literal["float32", "float64"] = "float64",
90
+ ):
91
+ logger.info("Running init for: %s", __name__)
92
+ self._df = dataframe
93
+
94
+ self._attr_types = {}
95
+ if isinstance(attr_types, dict):
96
+ for name, atype in attr_types.items():
97
+ use_atype = "DISC" if atype.upper() in ("DISC", "INT") else "CONT"
98
+ self._attr_types[name] = _AttrType[use_atype]
99
+
100
+ self._attr_records = attr_records if attr_records is not None else {}
101
+ self._xname = xname
102
+ self._yname = yname
103
+ self._zname = zname
104
+ self._idname = idname
105
+ self._floatbits = (
106
+ floatbits if floatbits in ["float32", "float64"] else "float64"
107
+ )
108
+
109
+ # undefined data are given by a value, that may be different for cont vs disc
110
+ if isinstance(undef, list):
111
+ self._undef_disc = undef[0]
112
+ self._undef_cont = undef[1]
113
+ else:
114
+ self._undef_disc = undef
115
+ self._undef_cont = undef
116
+
117
+ if xyztype == "well":
118
+ self._xyztype = _XYZType.WELL
119
+
120
+ self._hash = ("0", "0", "0")
121
+
122
+ logger.debug("Initial _attr_types: %s", self._attr_types)
123
+ logger.debug("Initial _attr_records: %s", self._attr_records)
124
+ self.ensure_consistency()
125
+ logger.debug("Initial after consistency chk _attr_types: %s", self._attr_types)
126
+ logger.debug(
127
+ "Initial after consistency chk _attr_records: %s", self._attr_records
128
+ )
129
+
130
+ @property
131
+ def dataframe(self):
132
+ return self._df
133
+
134
+ data = dataframe # alias
135
+
136
+ @property
137
+ def attr_types(self):
138
+ return self._attr_types
139
+
140
+ @property
141
+ def attr_records(self):
142
+ return self._attr_records
143
+
144
+ @property
145
+ def xname(self):
146
+ return self._xname
147
+
148
+ @xname.setter
149
+ def xname(self, name: str):
150
+ if isinstance(name, str):
151
+ self._xname = name
152
+ else:
153
+ raise ValueError(f"Input name is not a string: {name}")
154
+
155
+ @property
156
+ def yname(self):
157
+ return self._yname
158
+
159
+ @yname.setter
160
+ def yname(self, name: str):
161
+ if isinstance(name, str):
162
+ self._yname = name
163
+ else:
164
+ raise ValueError(f"Input name is not a string: {name}")
165
+
166
+ @property
167
+ def zname(self):
168
+ return self._zname
169
+
170
+ @zname.setter
171
+ def zname(self, name: str):
172
+ if isinstance(name, str):
173
+ self._zname = name
174
+ else:
175
+ raise ValueError(f"Input name is not a string: {name}")
176
+
177
+ def _infer_attr_dtypes(self):
178
+ """Return as dict on form {"X_UTME": _AttrType.CONT, "FACIES": _AttrType.DISC}.
179
+
180
+ There are some important restrictions:
181
+ * The first 3 columns (X Y Z) are always CONT, even if input appears as DISC.
182
+ * A check is made towards existing attr_types; if the key,value pair exists
183
+ already, this function will *not* force a change but keep as is.
184
+ """
185
+
186
+ # pandas function that e.g. will convert integer'ish floats to int:
187
+ new_df = self._df.convert_dtypes()
188
+
189
+ dlist = new_df.dtypes.to_dict()
190
+ logger.debug("Initial attr_type: %s", self._attr_types)
191
+
192
+ datatypes = {}
193
+ for name, dtype in dlist.items():
194
+ if name in self._attr_types:
195
+ # do not change already set attr_types
196
+ datatypes[name] = self._attr_types[name]
197
+ continue
198
+
199
+ if name in (self._xname, self._yname, self._zname):
200
+ # force coordinates, first 3 columns, to be CONT
201
+ datatypes[name] = _AttrType.CONT
202
+ continue
203
+
204
+ if "float" in str(dtype).lower():
205
+ datatypes[name] = _AttrType.CONT
206
+ elif "int" in str(dtype).lower():
207
+ # although it looks like int, we keep as float since it is not
208
+ # _explicitly_ set, to preserve backward compatibility.
209
+ datatypes[name] = _AttrType.CONT # CONT being INTENTIONAL!
210
+ else:
211
+ raise RuntimeError(
212
+ "Log type seems to be something else than float or int for "
213
+ f"{name}: {dtype}"
214
+ )
215
+ self._attr_types = datatypes
216
+ logger.debug("Processed attr_type: %s", self._attr_types)
217
+
218
+ def _ensure_consistency_attr_types(self):
219
+ """Ensure that dataframe and attr_types are consistent.
220
+
221
+ attr_types are on form {"GR": "CONT", "ZONES": "DISC", ...}
222
+
223
+ The column data in the dataframe takes precedence; i.e. if a column is removed
224
+ in a pandas operation, then attr_types are adapted silently by removing the item
225
+ from the dict.
226
+ """
227
+ # check first if an attr. is removed in dataframe (e.g. by pandas operations)
228
+ logger.debug("Ensure consistency attr_types...")
229
+ for attr_name in list(self._attr_types.keys()):
230
+ if attr_name not in self._df.columns[3:]:
231
+ del self._attr_types[attr_name]
232
+
233
+ self._infer_attr_dtypes()
234
+
235
+ def _infer_automatic_record(self, attr_name: str):
236
+ """Establish automatic record from name, type and values as first attempt."""
237
+ if self.get_attr_type(attr_name) == _AttrType.CONT.value:
238
+ self._attr_records[attr_name] = CONT_DEFAULT_RECORD
239
+ else:
240
+ # it is a discrete log with missing record; try to find
241
+ # a default one based on current values...
242
+ lvalues = self._df[attr_name].to_numpy().round(decimals=0)
243
+ lvalues = lvalues[~np.isnan(lvalues)] # remove Nans
244
+
245
+ if len(lvalues) > 0:
246
+ lvalues = lvalues.astype("int")
247
+ unique = np.unique(lvalues).tolist()
248
+ codes = {value: str(value) for value in unique}
249
+ if self._undef_disc in codes:
250
+ del codes[self._undef_disc]
251
+ if UNDEF_DISC in codes:
252
+ del codes[UNDEF_DISC]
253
+ else:
254
+ codes = None
255
+
256
+ self._attr_records[attr_name] = codes
257
+
258
+ def _ensure_consistency_attr_records(self):
259
+ """Ensure that data and attr_records are consistent; cf attr_types.
260
+
261
+ Important that input attr_types are correct; i.e. run
262
+ _ensure_consistency_attr_types() first!
263
+ """
264
+ for attr_name, dtype in self._attr_types.items():
265
+ logger.debug("attr_name: %s, and dtype: %s", attr_name, dtype)
266
+ if attr_name not in self._attr_records or not isinstance(
267
+ self._attr_records[attr_name],
268
+ (dict, list, tuple),
269
+ ):
270
+ self._infer_automatic_record(attr_name)
271
+
272
+ # correct when attr_types is CONT but attr_records for that entry is a dict
273
+ if (
274
+ attr_name in self._attr_records
275
+ and self._attr_types[attr_name] == _AttrType.CONT
276
+ and isinstance(self._attr_records[attr_name], dict)
277
+ ):
278
+ self._attr_records[attr_name] = CONT_DEFAULT_RECORD
279
+
280
+ def _ensure_consistency_df_dtypes(self):
281
+ """Ensure that dataframe float32/64 for all logs, except for XYZ -> float64.
282
+
283
+ Whether it is float32 or float64 is set by self._floatbits. Float32 will save
284
+ memory but loose some precision. For backward compatibility, float64 is default.
285
+ """
286
+
287
+ col = list(self._df)
288
+ logger.debug("columns: %s", col)
289
+
290
+ coords_dtypes = [str(entry) for entry in self._df[col[0:3]].dtypes]
291
+
292
+ if not all("float64" in entry for entry in coords_dtypes):
293
+ self._df[col[0:3]] = self._df.iloc[:, 0:3].astype("float64")
294
+
295
+ attr_dtypes = [str(entry) for entry in self._df[col[3:]].dtypes]
296
+
297
+ if not all(self._floatbits in entry for entry in attr_dtypes):
298
+ self._df[col[3:]] = self._df.iloc[:, 3:].astype(self._floatbits)
299
+
300
+ for name, attr_type in self._attr_types.items():
301
+ if attr_type == _AttrType.CONT.value:
302
+ logger.debug("Replacing CONT undef...")
303
+ self._df.loc[:, name] = self._df[name].replace(
304
+ self._undef_cont,
305
+ np.float64(UNDEF_CONT).astype(self._floatbits),
306
+ )
307
+ else:
308
+ logger.debug("Replacing INT undef...")
309
+ self._df.loc[:, name] = self._df[name].replace(
310
+ self._undef_disc, np.int32(UNDEF_DISC)
311
+ )
312
+ logger.info("Processed dataframe: %s", list(self._df.dtypes))
313
+
314
+ def ensure_consistency(self) -> bool:
315
+ """Ensure that data and attr* are consistent.
316
+
317
+ This is important for many operations on the dataframe, an should keep
318
+ attr_types and attr_records 'in sync' with the dataframe.
319
+
320
+ * When adding one or columns to the dataframe
321
+ * When removing one or more columns from the dataframe
322
+ * ...
323
+
324
+ Returns True is consistency is ran, while False means that no changes have
325
+ occured, hence no consistency checks are done
326
+ """
327
+
328
+ # the purpose of this hash check is to avoid spending time on consistency
329
+ # checks if no changes
330
+ hash_proposed = (
331
+ jhash(self._df),
332
+ jhash(self._attr_types),
333
+ jhash(self._attr_records),
334
+ )
335
+ if self._hash == hash_proposed:
336
+ return False
337
+
338
+ if list(self._df.columns[:3]) != [self._xname, self._yname, self._zname]:
339
+ raise ValueError(
340
+ f"Dataframe must include '{self._xname}', '{self._yname}' "
341
+ f"and '{self._zname}', got {list(self._df.columns[:3])}"
342
+ )
343
+
344
+ # order matters:
345
+ self._ensure_consistency_attr_types()
346
+ self._ensure_consistency_attr_records()
347
+ self._ensure_consistency_df_dtypes()
348
+ self._df.reset_index(drop=True, inplace=True)
349
+
350
+ self._hash = (
351
+ jhash(self._df),
352
+ jhash(self._attr_types),
353
+ jhash(self._attr_records),
354
+ )
355
+
356
+ return True
357
+
358
+ def get_attr_type(self, name: str) -> str:
359
+ """Get the attr_type as string"""
360
+ return self._attr_types[name].name
361
+
362
+ def set_attr_type(self, name: str, attrtype: str) -> None:
363
+ """Set a type (DISC, CONT) for a named attribute.
364
+
365
+ A bit flexibility is added for attrtype, e.g. allowing "float*" for CONT
366
+ etc, and allow lowercase "cont" for CONT
367
+
368
+ """
369
+ logger.debug("Set the attribute type for %s as %s", name, attrtype)
370
+ apply_attrtype = attrtype.upper()
371
+
372
+ # allow for optionally using INT and FLOAT in addation to DISC and CONT
373
+ if "FLOAT" in apply_attrtype:
374
+ apply_attrtype = _AttrType.CONT.value
375
+ if "INT" in apply_attrtype:
376
+ apply_attrtype = _AttrType.DISC.value
377
+
378
+ if name not in self._attr_types:
379
+ raise ValueError(f"No such log name present: {name}")
380
+
381
+ if self.get_attr_type(name) == apply_attrtype:
382
+ logger.debug("Same attr_type as existing, return")
383
+ return
384
+
385
+ if apply_attrtype in _AttrType.__members__:
386
+ self._attr_types[name] = _AttrType[apply_attrtype]
387
+ else:
388
+ raise ValueError(
389
+ f"Cannot set wlogtype as {attrtype}, not in "
390
+ f"{list(_AttrType.__members__)}"
391
+ )
392
+
393
+ # need to update records with defaults
394
+ self._infer_automatic_record(name)
395
+
396
+ self.ensure_consistency()
397
+
398
+ def get_attr_record(self, name: str):
399
+ """Get a record for a named attribute."""
400
+ return self._attr_records[name]
401
+
402
+ def set_attr_record(self, name: str, record: dict | None) -> None:
403
+ """Set a record for a named log."""
404
+
405
+ if name not in self._attr_types:
406
+ raise ValueError(f"No such attr_name: {name}")
407
+
408
+ if record is None and self._attr_types[name] == _AttrType.DISC:
409
+ record = {}
410
+ elif record is None and self._attr_types[name] == _AttrType.CONT:
411
+ record = CONT_DEFAULT_RECORD
412
+
413
+ if self._attr_types[name] == _AttrType.CONT and isinstance(
414
+ record, (list, tuple)
415
+ ):
416
+ if len(record) == 2:
417
+ self._attr_records[name] = tuple(record) # prefer as tuple
418
+ elif self._attr_types[name] == _AttrType.CONT and isinstance(record, dict):
419
+ raise ValueError(
420
+ "Cannot set a log record for a continuous log: input record is "
421
+ "dictionary, not a list or tuple"
422
+ )
423
+ elif self._attr_types[name] == _AttrType.DISC and isinstance(record, dict):
424
+ self._attr_records[name] = record
425
+ elif self._attr_types[name] == _AttrType.DISC and not isinstance(record, dict):
426
+ raise ValueError(
427
+ "Input is not a dictionary. Cannot set a log record for a discrete log"
428
+ )
429
+ else:
430
+ raise ValueError(
431
+ "Something went wrong when setting logrecord: "
432
+ f"({self._attr_types[name]} {type(record)})."
433
+ )
434
+
435
+ self.ensure_consistency()
436
+
437
+ def get_dataframe_copy(
438
+ self,
439
+ infer_dtype: bool = False,
440
+ filled=False,
441
+ fill_value=UNDEF_CONT,
442
+ fill_value_int=UNDEF_DISC,
443
+ ):
444
+ """Get a deep copy of the dataframe, with options.
445
+
446
+ If infer_dtype is True, then DISC columns will be of "int32" type, but
447
+ since int32 do not support np.nan, the value for undefined values will be
448
+ ``fill_value_int``
449
+ """
450
+ dfr = self._df.copy(deep=True)
451
+ if infer_dtype:
452
+ for name, attrtype in self._attr_types.items():
453
+ if attrtype.name == _AttrType.DISC.value:
454
+ dfr[name] = dfr[name].fillna(fill_value_int)
455
+ dfr[name] = dfr[name].astype("int32")
456
+
457
+ if filled:
458
+ dfill = {}
459
+ for attrname in self._df:
460
+ if self._attr_types[attrname] == _AttrType.DISC:
461
+ dfill[attrname] = fill_value_int
462
+ else:
463
+ dfill[attrname] = fill_value
464
+
465
+ dfr = dfr.fillna(dfill)
466
+
467
+ return dfr
468
+
469
+ def get_dataframe(self, copy=True):
470
+ """Get the dataframe, as view or deep copy."""
471
+ if copy:
472
+ return self._df.copy(deep=True)
473
+
474
+ return self._df
475
+
476
+ def set_dataframe(self, dfr: pd.DataFrame):
477
+ """Set the dataframe in a controlled manner, shall be used"""
478
+ # TODO: more checks, and possibly acceptance of lists, dicts?
479
+ if isinstance(dfr, pd.DataFrame):
480
+ self._df = dfr
481
+ else:
482
+ raise ValueError("Input dfr is not a pandas dataframe")
483
+ self.ensure_consistency()
484
+
485
+ def rename_attr(self, attrname: str, newname: str):
486
+ """Rename a attribute, e.g. Poro to PORO."""
487
+
488
+ if attrname not in list(self._df):
489
+ raise ValueError("Input log does not exist")
490
+
491
+ if newname in list(self._df):
492
+ raise ValueError("New log name exists already")
493
+
494
+ # rename in dataframe
495
+ self._df.rename(index=str, columns={attrname: newname}, inplace=True)
496
+
497
+ self._attr_types[newname] = self._attr_types.pop(attrname)
498
+ self._attr_records[newname] = self._attr_records.pop(attrname)
499
+
500
+ self.ensure_consistency()
501
+
502
+ def create_attr(
503
+ self,
504
+ attrname: str,
505
+ attr_type: str = Literal[
506
+ _AttrType.CONT.value, # type: ignore
507
+ _AttrType.DISC.value, # type: ignore
508
+ ],
509
+ attr_record: dict | None = None,
510
+ value: float = 0.0,
511
+ force: bool = True,
512
+ force_reserved: bool = False,
513
+ ) -> bool:
514
+ """Create a new attribute, e.g. a log."""
515
+
516
+ if attrname in list(self._df) and force is False:
517
+ return False
518
+
519
+ if attrname in _AttrName.list() and not force_reserved:
520
+ raise ValueError(
521
+ f"The proposed name {attrname} is a reserved name; try another or "
522
+ "set keyword ``force_reserved`` to True ."
523
+ f"Note that the follwoing names are reserved: {_AttrName.list()}"
524
+ )
525
+
526
+ self._attr_types[attrname] = _AttrType[attr_type]
527
+ self._attr_records[attrname] = attr_record
528
+
529
+ # make a new column
530
+ self._df[attrname] = float(value)
531
+ self.ensure_consistency()
532
+ return True
533
+
534
+ def copy_attr(self, attrname: str, new_attrname: str, force: bool = True) -> bool:
535
+ """Copy a attribute to a new name."""
536
+
537
+ if new_attrname in list(self._df) and force is False:
538
+ return False
539
+
540
+ self._attr_types[new_attrname] = deepcopy(self._attr_types[attrname])
541
+ self._attr_records[new_attrname] = deepcopy(self._attr_records[attrname])
542
+
543
+ # make a new column
544
+ self._df[new_attrname] = self._df[attrname].copy()
545
+ self.ensure_consistency()
546
+ return True
547
+
548
+ def delete_attr(self, attrname: str | list[str]) -> int:
549
+ """Delete/remove an existing attribute, or list of attributes.
550
+
551
+ Returns number of logs deleted
552
+ """
553
+ if not isinstance(attrname, list):
554
+ attrname = [attrname]
555
+
556
+ lcount = 0
557
+ for logn in attrname:
558
+ if logn not in list(self._df):
559
+ continue
560
+
561
+ lcount += 1
562
+ logger.debug("Actually deleting %s", logn)
563
+ self._df.drop(logn, axis=1, inplace=True)
564
+
565
+ self.ensure_consistency()
566
+
567
+ return lcount
568
+
569
+ def create_relative_hlen(self):
570
+ """Make a relative length of e.g. a well, as a attribute (log)."""
571
+ # extract numpies from XYZ trajectory logs
572
+ xv = self._df[self._xname].values
573
+ yv = self._df[self._yname].values
574
+
575
+ distance = []
576
+ previous_x, previous_y = xv[0], yv[0]
577
+ for _, (x, y) in enumerate(zip(xv, yv)):
578
+ distance.append(math.hypot((previous_x - x), (y - previous_y)))
579
+ previous_x, previous_y = x, y
580
+
581
+ self._df.loc[:, _AttrName.R_HLEN_NAME.value] = pd.Series(
582
+ np.cumsum(distance), index=self._df.index
583
+ )
584
+ self.ensure_consistency()
585
+
586
+ def geometrics(self):
587
+ """Compute geometrical arrays MD, INCL, AZI, as attributes (logs) (~well data).
588
+
589
+ These are kind of quasi measurements hence the attributes (logs) will named
590
+ with a Q in front as Q_MDEPTH, Q_INCL, and Q_AZI.
591
+
592
+ These attributes will be added to the dataframe.
593
+
594
+ TODO: If the mdlogname
595
+ attribute does not exist in advance, it will be set to 'Q_MDEPTH'.
596
+
597
+ Returns:
598
+ False if geometrics cannot be computed
599
+
600
+ """
601
+ # TODO: rewrite in pure python?
602
+ if self._df.shape[0] < 3:
603
+ raise ValueError(
604
+ f"Cannot compute geometrics. Not enough "
605
+ f"trajectory points (need >3, have: {self._df.shape[0]})"
606
+ )
607
+
608
+ # extract numpies from XYZ trajetory logs
609
+ ptr_xv = _get_carray(self._df, self._attr_types, self._xname)
610
+ ptr_yv = _get_carray(self._df, self._attr_types, self._yname)
611
+ ptr_zv = _get_carray(self._df, self._attr_types, self._zname)
612
+
613
+ # get number of rows in pandas
614
+ nlen = len(self._df)
615
+
616
+ ptr_md = _cxtgeo.new_doublearray(nlen)
617
+ ptr_incl = _cxtgeo.new_doublearray(nlen)
618
+ ptr_az = _cxtgeo.new_doublearray(nlen)
619
+
620
+ ier = _cxtgeo.well_geometrics(
621
+ nlen, ptr_xv, ptr_yv, ptr_zv, ptr_md, ptr_incl, ptr_az, 0
622
+ )
623
+
624
+ if ier != 0:
625
+ raise XTGeoCLibError(f"XYZ/well_geometrics failed with error code: {ier}")
626
+
627
+ dnumpy = _convert_carr_double_np(len(self._df), ptr_md)
628
+ self._df[_AttrName.Q_MD_NAME.value] = pd.Series(dnumpy, index=self._df.index)
629
+
630
+ dnumpy = _convert_carr_double_np(len(self._df), ptr_incl)
631
+ self._df[_AttrName.Q_INCL_NAME.value] = pd.Series(dnumpy, index=self._df.index)
632
+
633
+ dnumpy = _convert_carr_double_np(len(self._df), ptr_az)
634
+ self._df[_AttrName.Q_AZI_NAME.value] = pd.Series(dnumpy, index=self._df.index)
635
+
636
+ # delete tmp pointers
637
+ _cxtgeo.delete_doublearray(ptr_xv)
638
+ _cxtgeo.delete_doublearray(ptr_yv)
639
+ _cxtgeo.delete_doublearray(ptr_zv)
640
+ _cxtgeo.delete_doublearray(ptr_md)
641
+ _cxtgeo.delete_doublearray(ptr_incl)
642
+ _cxtgeo.delete_doublearray(ptr_az)
643
+
644
+ self.ensure_consistency()
645
+
646
+ return True