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
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""Import Cube data via SegyIO library or XTGeo CLIB.
|
|
2
|
+
|
|
3
|
+
Data model in XTGeo illustrated by example: ncol=3, nrow=4 (non-rotated):
|
|
4
|
+
|
|
5
|
+
3 7 ^ "J, ~NORTH" direction
|
|
6
|
+
xline=1022 +--------------+--------------+11 | (if unrotated)
|
|
7
|
+
| | | |
|
|
8
|
+
| | | |
|
|
9
|
+
| | |
|
|
10
|
+
|2 |6 |10
|
|
11
|
+
xline=1020 +--------------+--------------+ Rotation is school angle of
|
|
12
|
+
| | | "I east" vs X axis
|
|
13
|
+
| | |
|
|
14
|
+
| | |
|
|
15
|
+
|1 |5 |9
|
|
16
|
+
xline=1018 +--------------+--------------+
|
|
17
|
+
| | |
|
|
18
|
+
| | |
|
|
19
|
+
| | |
|
|
20
|
+
|0 |4 |8
|
|
21
|
+
xline=1016 +--------------+--------------+ ------> "I, ~EAST" direction
|
|
22
|
+
|
|
23
|
+
iline=4400 iline=4401 iline=4402
|
|
24
|
+
|
|
25
|
+
Indices is fastest along "J" (C order), and xlines and ilines spacing may vary (2 for
|
|
26
|
+
xline, 1 for iline in this example) but shall be constant per axis
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from collections import defaultdict
|
|
34
|
+
from copy import deepcopy
|
|
35
|
+
from struct import unpack
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
from warnings import warn
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
import segyio
|
|
41
|
+
from segyio import TraceField as TF
|
|
42
|
+
|
|
43
|
+
import xtgeo.common.sys as xsys
|
|
44
|
+
from xtgeo import _cxtgeo
|
|
45
|
+
from xtgeo.common import calc
|
|
46
|
+
from xtgeo.common.constants import UNDEF
|
|
47
|
+
from xtgeo.common.log import null_logger
|
|
48
|
+
from xtgeo.common.xtgeo_dialog import XTGeoDialog
|
|
49
|
+
from xtgeo.metadata.metadata import MetaDataRegularCube
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from xtgeo.io._file import FileWrapper
|
|
53
|
+
|
|
54
|
+
xtg = XTGeoDialog()
|
|
55
|
+
logger = null_logger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def import_segy(sfile: FileWrapper) -> dict:
|
|
59
|
+
"""Import SEGY via the SegyIO library.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
sfile: File object for SEGY file
|
|
63
|
+
"""
|
|
64
|
+
sfile = sfile.file
|
|
65
|
+
|
|
66
|
+
attributes = {}
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# cube with all traces present
|
|
70
|
+
with segyio.open(sfile, "r") as segyfile:
|
|
71
|
+
attributes = _import_segy_all_traces(segyfile)
|
|
72
|
+
except ValueError as verr:
|
|
73
|
+
if any(word in str(verr) for word in ["Invalid dimensions", "inconsistent"]):
|
|
74
|
+
# cube with missing traces is now handled but his is complex, hence users
|
|
75
|
+
# shall be warned. With more experience, this warning can be removed.
|
|
76
|
+
warn(
|
|
77
|
+
"Missing or inconsistent traces in SEGY detected, xtgeo will try "
|
|
78
|
+
"to import by infilling, but please check result carefully!",
|
|
79
|
+
UserWarning,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
with segyio.open(sfile, "r", ignore_geometry=True) as segyfile:
|
|
83
|
+
attributes = _import_segy_incomplete_traces(segyfile)
|
|
84
|
+
else:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as anyerror: # catch the rest
|
|
87
|
+
raise OSError(f"Cannot parse SEGY file: {str(anyerror)}") from anyerror
|
|
88
|
+
|
|
89
|
+
if not attributes:
|
|
90
|
+
raise ValueError("Could not get attributes for segy file")
|
|
91
|
+
|
|
92
|
+
attributes["segyfile"] = sfile
|
|
93
|
+
return attributes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _import_segy_all_traces(segyfile: segyio.segy.SegyFile) -> dict:
|
|
97
|
+
"""Import a a full cube SEGY via the SegyIO library to xtgeo format spec.
|
|
98
|
+
|
|
99
|
+
Here, the segyio.tools.cube function can be applied
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
segyfile: Filehandle from segyio
|
|
103
|
+
"""
|
|
104
|
+
segyfile.mmap()
|
|
105
|
+
|
|
106
|
+
values = _process_cube_values(segyio.tools.cube(segyfile))
|
|
107
|
+
|
|
108
|
+
ilines = segyfile.ilines
|
|
109
|
+
xlines = segyfile.xlines
|
|
110
|
+
|
|
111
|
+
ncol, nrow, nlay = values.shape
|
|
112
|
+
|
|
113
|
+
# get additional but required geometries for xtgeo; xori, yori, xinc, ..., rotation
|
|
114
|
+
attrs = _segy_all_traces_attributes(segyfile, ncol, nrow, nlay)
|
|
115
|
+
|
|
116
|
+
attrs["ilines"] = ilines
|
|
117
|
+
attrs["xlines"] = xlines
|
|
118
|
+
attrs["values"] = values
|
|
119
|
+
|
|
120
|
+
return attrs
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _process_cube_values(values: np.ndarray) -> np.ndarray:
|
|
124
|
+
"""Helper function to validate/check values."""
|
|
125
|
+
if values.dtype != np.float32:
|
|
126
|
+
xtg.warnuser(f"Values are converted from {values.dtype} to float32")
|
|
127
|
+
values = values.astype(np.float32)
|
|
128
|
+
if np.any(np.isnan(values)):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"The input values: {values} contains NaN values which is currently "
|
|
131
|
+
"not handled!"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return values
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _segy_all_traces_attributes(
|
|
138
|
+
segyfile: segyio.segy.SegyFile, ncol, nrow, nlay
|
|
139
|
+
) -> dict:
|
|
140
|
+
"""Get the geometrical values xtgeo needs for a cube definition."""
|
|
141
|
+
trcode = segyio.TraceField.TraceIdentificationCode
|
|
142
|
+
traceidcodes = segyfile.attributes(trcode)[:].reshape(ncol, nrow)
|
|
143
|
+
|
|
144
|
+
# need positions in corners for making vectors to compute geometries
|
|
145
|
+
c1v = calc.ijk_to_ib(1, 1, 1, ncol, nrow, 1, forder=False)
|
|
146
|
+
c2v = calc.ijk_to_ib(ncol, 1, 1, ncol, nrow, 1, forder=False)
|
|
147
|
+
c3v = calc.ijk_to_ib(1, nrow, 1, ncol, nrow, 1, forder=False)
|
|
148
|
+
|
|
149
|
+
xori, yori, zori, zinc = _get_coordinate(segyfile, c1v)
|
|
150
|
+
point_x1, point_y1, _, _ = _get_coordinate(segyfile, c2v)
|
|
151
|
+
point_x2, point_y2, _, _ = _get_coordinate(segyfile, c3v)
|
|
152
|
+
|
|
153
|
+
slen1, _, rotation = calc.vectorinfo2(xori, point_x1, yori, point_y1)
|
|
154
|
+
xinc = slen1 / (ncol - 1)
|
|
155
|
+
|
|
156
|
+
slen2, _, _ = calc.vectorinfo2(xori, point_x2, yori, point_y2)
|
|
157
|
+
yinc = slen2 / (nrow - 1)
|
|
158
|
+
|
|
159
|
+
# find YFLIP by cross products
|
|
160
|
+
yflip = calc.find_flip(
|
|
161
|
+
(point_x1 - xori, point_y1 - yori, 0), (point_x2 - xori, point_y2 - yori, 0)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"ncol": ncol,
|
|
166
|
+
"nrow": nrow,
|
|
167
|
+
"nlay": nlay,
|
|
168
|
+
"xori": xori,
|
|
169
|
+
"xinc": xinc,
|
|
170
|
+
"yori": yori,
|
|
171
|
+
"yinc": yinc,
|
|
172
|
+
"zori": zori,
|
|
173
|
+
"zinc": zinc,
|
|
174
|
+
"rotation": rotation,
|
|
175
|
+
"yflip": yflip,
|
|
176
|
+
"traceidcodes": traceidcodes,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _import_segy_incomplete_traces(segyfile: segyio.segy.SegyFile) -> dict:
|
|
181
|
+
"""Import a a cube SEGY with incomplete traces via the SegyIO library.
|
|
182
|
+
|
|
183
|
+
Note that the undefined value will be xtgeo.UNDEF (large number)!
|
|
184
|
+
|
|
185
|
+
It is also logical to treat missing traces as dead traces, i.e. the should
|
|
186
|
+
get value 2 in traceidcodes
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
segyfile: Filehandle from segyio
|
|
190
|
+
"""
|
|
191
|
+
segyfile.mmap()
|
|
192
|
+
# get data (which will need padding later for missing traces)
|
|
193
|
+
data = segyfile.trace.raw[:]
|
|
194
|
+
|
|
195
|
+
trcode = segyio.TraceField.TraceIdentificationCode
|
|
196
|
+
traceidcodes_input = segyfile.attributes(trcode)[:]
|
|
197
|
+
|
|
198
|
+
ilines_case = np.array([h[TF.INLINE_3D] for h in segyfile.header])
|
|
199
|
+
xlines_case = np.array([h[TF.CROSSLINE_3D] for h in segyfile.header])
|
|
200
|
+
|
|
201
|
+
# detect minimum inline and xline spacing (e.g.sampling could be every second)
|
|
202
|
+
idiff = np.diff(ilines_case)
|
|
203
|
+
xdiff = np.diff(xlines_case)
|
|
204
|
+
ispacing = int(np.abs(idiff[idiff != 0]).min())
|
|
205
|
+
xspacing = int(np.abs(xdiff[xdiff != 0]).min())
|
|
206
|
+
|
|
207
|
+
ncol = int(abs(ilines_case.min() - ilines_case.max()) / ispacing) + 1
|
|
208
|
+
nrow = int(abs(xlines_case.min() - xlines_case.max()) / xspacing) + 1
|
|
209
|
+
nlay = data.shape[1]
|
|
210
|
+
|
|
211
|
+
values = np.full((ncol, nrow, nlay), UNDEF, dtype=np.float32)
|
|
212
|
+
traceidcodes = np.full((ncol, nrow), 2, dtype=np.int64)
|
|
213
|
+
|
|
214
|
+
ilines_shifted = (ilines_case / ispacing).astype(np.int64)
|
|
215
|
+
ilines_shifted -= ilines_shifted.min()
|
|
216
|
+
xlines_shifted = (xlines_case / xspacing).astype(np.int64)
|
|
217
|
+
xlines_shifted -= xlines_shifted.min()
|
|
218
|
+
|
|
219
|
+
values[ilines_shifted, xlines_shifted, :] = data
|
|
220
|
+
values = _process_cube_values(values)
|
|
221
|
+
traceidcodes[ilines_shifted, xlines_shifted] = traceidcodes_input
|
|
222
|
+
|
|
223
|
+
# generate new ilines, xlines vector with unique values
|
|
224
|
+
ilines = np.array(
|
|
225
|
+
range(ilines_case.min(), ilines_case.max() + ispacing, ispacing), dtype=np.int32
|
|
226
|
+
)
|
|
227
|
+
xlines = np.array(
|
|
228
|
+
range(xlines_case.min(), xlines_case.max() + xspacing, xspacing), dtype=np.int32
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
attrs = _geometry_incomplete_traces(
|
|
232
|
+
segyfile,
|
|
233
|
+
ncol,
|
|
234
|
+
nrow,
|
|
235
|
+
ilines,
|
|
236
|
+
xlines,
|
|
237
|
+
ilines_case,
|
|
238
|
+
xlines_case,
|
|
239
|
+
ispacing,
|
|
240
|
+
xspacing,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
attrs["ncol"] = ncol
|
|
244
|
+
attrs["nrow"] = nrow
|
|
245
|
+
attrs["nlay"] = nlay
|
|
246
|
+
attrs["ilines"] = ilines
|
|
247
|
+
attrs["xlines"] = xlines
|
|
248
|
+
attrs["values"] = values
|
|
249
|
+
attrs["traceidcodes"] = traceidcodes
|
|
250
|
+
return attrs
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _inverse_anyline_map(anylines: list[int]) -> dict:
|
|
254
|
+
"""Small helper function to get e.g. inline 2345: [0, 1, 2, .., 70].
|
|
255
|
+
|
|
256
|
+
I.e. to get a mapping between inline number and a list of possible indices
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
anyll = defaultdict(list)
|
|
260
|
+
for ind, key in enumerate(anylines):
|
|
261
|
+
anyll[key].append(ind)
|
|
262
|
+
|
|
263
|
+
return anyll
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _find_long_line(anyll: dict, nany: int) -> list:
|
|
267
|
+
"""Helper function; get index of vectors indices to be used for calculations.
|
|
268
|
+
|
|
269
|
+
Look for a "sufficiently" long inline/xline, as long distance between points
|
|
270
|
+
increases accuracy.
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
minimumlen = int(nany * 0.8) # get at least 80% length if possible
|
|
274
|
+
maxlenfound = -1
|
|
275
|
+
|
|
276
|
+
keepresult = []
|
|
277
|
+
for indices in anyll.values():
|
|
278
|
+
result = [indices[0], indices[-1]]
|
|
279
|
+
indlen = len(indices)
|
|
280
|
+
if indlen > maxlenfound:
|
|
281
|
+
maxlenfound = indlen
|
|
282
|
+
keepresult = deepcopy(result)
|
|
283
|
+
|
|
284
|
+
if indlen >= minimumlen:
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
if not keepresult or abs(keepresult[1] - keepresult[0]) == 0:
|
|
288
|
+
raise RuntimeError("Not able to get inline or xline vector for geometry")
|
|
289
|
+
return keepresult
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _geometry_incomplete_traces(
|
|
293
|
+
segyfile: segyio.segy.SegyFile,
|
|
294
|
+
ncol: int,
|
|
295
|
+
nrow: int,
|
|
296
|
+
ilines: list[int],
|
|
297
|
+
xlines: list[int],
|
|
298
|
+
ilines_case: list[int],
|
|
299
|
+
xlines_case: list[int],
|
|
300
|
+
ispacing: int,
|
|
301
|
+
xspacing: int,
|
|
302
|
+
) -> list:
|
|
303
|
+
"""Compute xtgeo attributes (mostly geometries) for incomplete trace cube."""
|
|
304
|
+
attrs = {}
|
|
305
|
+
|
|
306
|
+
ill = _inverse_anyline_map(ilines_case)
|
|
307
|
+
xll = _inverse_anyline_map(xlines_case)
|
|
308
|
+
|
|
309
|
+
# need both partial and full reverse lookup of indices vs (iline, xxline)
|
|
310
|
+
# for computing cube origin later
|
|
311
|
+
index_case = {
|
|
312
|
+
ind: (h[TF.INLINE_3D], h[TF.CROSSLINE_3D])
|
|
313
|
+
for ind, h in enumerate(segyfile.header)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reverseindex_full = {
|
|
317
|
+
(il, xl): (ind, xnd)
|
|
318
|
+
for ind, il in enumerate(ilines)
|
|
319
|
+
for xnd, xl in enumerate(xlines)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
jnd1, jnd2 = _find_long_line(ill, ncol) # 2 indices along constant iline, aka JY
|
|
323
|
+
ind1, ind2 = _find_long_line(xll, nrow) # 2 indices along constant xline, aka IX
|
|
324
|
+
|
|
325
|
+
il1x, il1y, zori, zinc = _get_coordinate(segyfile, ind1)
|
|
326
|
+
il2x, il2y, _, _ = _get_coordinate(segyfile, ind2)
|
|
327
|
+
|
|
328
|
+
jl1x, jl1y, _, _ = _get_coordinate(segyfile, jnd1)
|
|
329
|
+
jl2x, jl2y, _, _ = _get_coordinate(segyfile, jnd2)
|
|
330
|
+
|
|
331
|
+
xslen, _, rot1 = calc.vectorinfo2(il1x, il2x, il1y, il2y)
|
|
332
|
+
xinc = ispacing * xslen / (abs(ilines_case[ind1] - ilines_case[ind2]))
|
|
333
|
+
yslen, _, _ = calc.vectorinfo2(jl1x, jl2x, jl1y, jl2y)
|
|
334
|
+
yinc = xspacing * yslen / (abs(xlines_case[jnd1] - xlines_case[jnd2]))
|
|
335
|
+
|
|
336
|
+
yflip = calc.find_flip((il2x - il1x, il2y - il1y, 0), (jl2x - jl1x, jl2y - jl1y, 0))
|
|
337
|
+
|
|
338
|
+
# need to compute xori and yori from 'case' I J indices with known x y and
|
|
339
|
+
# (iline, xline); use ind1 with assosiated coordinates il1x il1y
|
|
340
|
+
i_use, j_use = reverseindex_full[index_case[ind1]]
|
|
341
|
+
xori, yori = calc.xyori_from_ij(
|
|
342
|
+
i_use, j_use, il1x, il1y, xinc, yinc, ncol, nrow, yflip, rot1
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
attrs["xori"] = xori
|
|
346
|
+
attrs["yori"] = yori
|
|
347
|
+
attrs["zori"] = zori
|
|
348
|
+
attrs["xinc"] = xinc
|
|
349
|
+
attrs["yinc"] = yinc
|
|
350
|
+
attrs["zinc"] = zinc
|
|
351
|
+
attrs["yflip"] = yflip
|
|
352
|
+
attrs["rotation"] = rot1
|
|
353
|
+
|
|
354
|
+
return attrs
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _get_coordinate(
|
|
358
|
+
segyfile: segyio.segy.SegyFile, segyindex: int
|
|
359
|
+
) -> tuple[float, float, float, float]:
|
|
360
|
+
"""Helper function to get coordinates given a index."""
|
|
361
|
+
origin = segyfile.header[segyindex][
|
|
362
|
+
segyio.su.cdpx,
|
|
363
|
+
segyio.su.cdpy,
|
|
364
|
+
segyio.su.scalco,
|
|
365
|
+
segyio.su.delrt,
|
|
366
|
+
segyio.su.dt,
|
|
367
|
+
segyio.su.iline,
|
|
368
|
+
segyio.su.xline,
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
point_x = origin[segyio.su.cdpx]
|
|
372
|
+
point_y = origin[segyio.su.cdpy]
|
|
373
|
+
scaler = origin[segyio.su.scalco]
|
|
374
|
+
if scaler < 0:
|
|
375
|
+
point_x = -1 * float(point_x) / scaler
|
|
376
|
+
point_y = -1 * float(point_y) / scaler
|
|
377
|
+
else:
|
|
378
|
+
point_x = point_x * scaler
|
|
379
|
+
point_y = point_y * scaler
|
|
380
|
+
|
|
381
|
+
zori = origin[segyio.su.delrt]
|
|
382
|
+
zinc = origin[segyio.su.dt] / 1000.0
|
|
383
|
+
|
|
384
|
+
return point_x, point_y, zori, zinc
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def import_stormcube(
|
|
388
|
+
sfile: FileWrapper,
|
|
389
|
+
) -> dict:
|
|
390
|
+
"""Import on StormCube format."""
|
|
391
|
+
# The ASCII header has all the metadata on the form:
|
|
392
|
+
# ---------------------------------------------------------------------
|
|
393
|
+
# storm_petro_binary // always
|
|
394
|
+
#
|
|
395
|
+
# 0 ModelFile -999 // zonenumber, source_of_file, undef_value
|
|
396
|
+
#
|
|
397
|
+
# UNKNOWN // name_of_parameter?
|
|
398
|
+
#
|
|
399
|
+
# 452638.45298827 6262.499 6780706.6462283 10762.4999 1800 2500 0 0
|
|
400
|
+
# 700 -0.80039470880765
|
|
401
|
+
#
|
|
402
|
+
# 501 861 140
|
|
403
|
+
# ---------------------------------------------------------------------
|
|
404
|
+
# The rest is float32 binary data, I (column fastest), then J, then K
|
|
405
|
+
# a total of ncol * nrow * nlay
|
|
406
|
+
|
|
407
|
+
# Scan the header with Python; then use CLIB for the binary data
|
|
408
|
+
sfile = str(sfile.file)
|
|
409
|
+
with open(sfile, "rb") as stf:
|
|
410
|
+
iline = 0
|
|
411
|
+
|
|
412
|
+
ncol = nrow = nlay = nlines = 1
|
|
413
|
+
xori = yori = zori = xinc = yinc = rotation = rot = 0.999
|
|
414
|
+
xlen = ylen = zlen = 0.999
|
|
415
|
+
|
|
416
|
+
for line in range(10):
|
|
417
|
+
xline = stf.readline()
|
|
418
|
+
if not xline.strip():
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
iline += 1
|
|
422
|
+
if iline == 1:
|
|
423
|
+
pass
|
|
424
|
+
elif iline == 2:
|
|
425
|
+
_, _, _ = xline.strip().split()
|
|
426
|
+
elif iline == 3:
|
|
427
|
+
pass
|
|
428
|
+
elif iline == 4:
|
|
429
|
+
(xori, xlen, yori, ylen, zori, _, _, _) = xline.strip().split()
|
|
430
|
+
elif iline == 5:
|
|
431
|
+
zlen, rot = xline.strip().split()
|
|
432
|
+
elif iline == 6:
|
|
433
|
+
ncol, nrow, nlay = xline.strip().split()
|
|
434
|
+
nlines = line + 2
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
ncol = int(ncol)
|
|
438
|
+
nrow = int(nrow)
|
|
439
|
+
nlay = int(nlay)
|
|
440
|
+
nrcl = ncol * nrow * nlay
|
|
441
|
+
|
|
442
|
+
xori = float(xori)
|
|
443
|
+
yori = float(yori)
|
|
444
|
+
zori = float(zori)
|
|
445
|
+
|
|
446
|
+
rotation = float(rot)
|
|
447
|
+
if rotation < 0:
|
|
448
|
+
rotation += 360
|
|
449
|
+
|
|
450
|
+
xinc = float(xlen) / ncol
|
|
451
|
+
yinc = float(ylen) / nrow
|
|
452
|
+
zinc = float(zlen) / nlay
|
|
453
|
+
|
|
454
|
+
yflip = 1
|
|
455
|
+
|
|
456
|
+
if yinc < 0:
|
|
457
|
+
yflip = -1
|
|
458
|
+
yinc = yinc * yflip # not sure if this will ever happen
|
|
459
|
+
|
|
460
|
+
ier, values = _cxtgeo.cube_import_storm(ncol, nrow, nlay, sfile, nlines, nrcl, 0)
|
|
461
|
+
|
|
462
|
+
if ier != 0:
|
|
463
|
+
raise RuntimeError(f"Something went wrong in {__name__}, code is {ier}")
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
"ncol": ncol,
|
|
467
|
+
"nrow": nrow,
|
|
468
|
+
"nlay": nlay,
|
|
469
|
+
"xori": xori,
|
|
470
|
+
"xinc": xinc,
|
|
471
|
+
"yori": yori,
|
|
472
|
+
"yinc": yinc,
|
|
473
|
+
"zori": zori,
|
|
474
|
+
"zinc": zinc,
|
|
475
|
+
"rotation": rotation,
|
|
476
|
+
"values": values.reshape((ncol, nrow, nlay)),
|
|
477
|
+
"yflip": yflip,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def import_xtgregcube(mfile, values=True):
|
|
482
|
+
"""Using pure python for experimental cube import, xtgregsurf format."""
|
|
483
|
+
logger.info("Importing cube on xtgregcube format...")
|
|
484
|
+
|
|
485
|
+
offset = 36
|
|
486
|
+
with open(mfile.file, "rb") as fhandle:
|
|
487
|
+
buf = fhandle.read(offset)
|
|
488
|
+
|
|
489
|
+
# unpack header
|
|
490
|
+
swap, magic, nfloat, ncol, nrow, nlay = unpack("= i i i q q q", buf)
|
|
491
|
+
|
|
492
|
+
if swap != 1 or magic != 1201:
|
|
493
|
+
raise ValueError("Invalid file format (wrong swap id or magic number).")
|
|
494
|
+
|
|
495
|
+
dtype = np.float32 if nfloat == 4 else np.float64
|
|
496
|
+
|
|
497
|
+
vals = None
|
|
498
|
+
narr = ncol * nrow * nlay
|
|
499
|
+
|
|
500
|
+
if values:
|
|
501
|
+
vals = xsys.npfromfile(mfile.file, dtype=dtype, count=narr, offset=offset)
|
|
502
|
+
|
|
503
|
+
# read metadata which will be at position offet + nfloat*narr +13
|
|
504
|
+
pos = offset + nfloat * narr + 13
|
|
505
|
+
|
|
506
|
+
with open(mfile.file, "rb") as fhandle:
|
|
507
|
+
fhandle.seek(pos)
|
|
508
|
+
jmeta = fhandle.read().decode()
|
|
509
|
+
|
|
510
|
+
meta = json.loads(jmeta, object_pairs_hook=dict)
|
|
511
|
+
req = meta["_required_"]
|
|
512
|
+
|
|
513
|
+
reqattrs = MetaDataRegularCube.REQUIRED
|
|
514
|
+
|
|
515
|
+
results = {myattr: req[myattr] for myattr in reqattrs}
|
|
516
|
+
|
|
517
|
+
# For backwards compatability, xtgeo outputs files with the undef field set
|
|
518
|
+
# although we do not support initializing with any other value.
|
|
519
|
+
# As xtgeo-format is only written/read by xtgeo as far as we know, this should
|
|
520
|
+
# be unproblematic for now.
|
|
521
|
+
if results.pop("undef", None) != UNDEF:
|
|
522
|
+
raise ValueError(
|
|
523
|
+
f"File {mfile.file} has non-standard undef, not supported by xtgeo"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# TODO: dead traces and traceidcodes
|
|
527
|
+
if values:
|
|
528
|
+
results["values"] = vals.reshape(
|
|
529
|
+
results["ncol"], results["nrow"], results["nlay"]
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
return results
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Roxar API functions for XTGeo Cube
|
|
3
|
+
|
|
4
|
+
Note on rotation:
|
|
5
|
+
|
|
6
|
+
xtgeo uses rotation of "columns" whick is xline direction counterclockwise
|
|
7
|
+
measured from X axis.
|
|
8
|
+
|
|
9
|
+
roxarapi uses rotation of inline direction (rows) relative to Y axis.
|
|
10
|
+
api < 1.4: counterclockwise "rotation"
|
|
11
|
+
api >= 1.4 clockwise "orientation"
|
|
12
|
+
|
|
13
|
+
Seems like self._rotation == roxar.orientation * -1 anyway @ reverse engineering/testing
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from xtgeo.common import XTGeoDialog, null_logger
|
|
20
|
+
from xtgeo.roxutils import RoxUtils
|
|
21
|
+
from xtgeo.roxutils._roxar_loader import roxar
|
|
22
|
+
|
|
23
|
+
xtg = XTGeoDialog()
|
|
24
|
+
|
|
25
|
+
logger = null_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def import_cube_roxapi(self, project, name, folder=None): # pragma: no cover
|
|
29
|
+
"""Import (transfer) a Cube via ROXAR API container to XTGeo.
|
|
30
|
+
|
|
31
|
+
.. versionadded:: 2.1
|
|
32
|
+
"""
|
|
33
|
+
rox = RoxUtils(project, readonly=True)
|
|
34
|
+
|
|
35
|
+
proj = rox.project
|
|
36
|
+
|
|
37
|
+
_roxapi_import_cube(self, rox, proj, name, folder)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _roxapi_import_cube(self, rox, proj, name, folder): # pragma: no cover
|
|
41
|
+
"""Short summary.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
proj (object): RMS magic project.
|
|
45
|
+
name (str): Name of cube.
|
|
46
|
+
folder (str): Cube folder in RMS.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
# note that name must be in brackets
|
|
50
|
+
path = [name]
|
|
51
|
+
if folder is not None:
|
|
52
|
+
fld = folder.split("/")
|
|
53
|
+
path = fld + path
|
|
54
|
+
|
|
55
|
+
if path not in proj.seismic.data:
|
|
56
|
+
raise ValueError(f"Path {path} is not within RMS Seismic Cube container")
|
|
57
|
+
try:
|
|
58
|
+
rcube = proj.seismic.data[path]
|
|
59
|
+
_roxapi_cube_to_xtgeo(self, rox, rcube)
|
|
60
|
+
except KeyError as emsg:
|
|
61
|
+
logger.error(emsg)
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _roxapi_cube_to_xtgeo(self, rox, rcube): # pragma: no cover
|
|
66
|
+
"""Tranforming cube from ROXAPI to XTGeo object."""
|
|
67
|
+
logger.info("Cube from roxapi to xtgeo...")
|
|
68
|
+
|
|
69
|
+
# roxrotation is cube rotation clockwise from azimuth but not consistent
|
|
70
|
+
roxrotation = rcube.orientation
|
|
71
|
+
|
|
72
|
+
roxhandedness = str(rcube.handedness)
|
|
73
|
+
|
|
74
|
+
self._xori, self._yori = rcube.origin
|
|
75
|
+
self._zori = rcube.first_z
|
|
76
|
+
self._zinc = rcube.sample_rate
|
|
77
|
+
self._ncol, self._nrow, self._nlay = rcube.dimensions
|
|
78
|
+
self._xinc, self._yinc = rcube.increment
|
|
79
|
+
|
|
80
|
+
self._rotation = roxrotation * -1
|
|
81
|
+
|
|
82
|
+
if self._rotation < 0:
|
|
83
|
+
self._rotation += 360
|
|
84
|
+
elif self._rotation > 360:
|
|
85
|
+
self._rotation -= 360
|
|
86
|
+
|
|
87
|
+
self._yflip = 1
|
|
88
|
+
if roxhandedness == "right":
|
|
89
|
+
self._yflip = -1
|
|
90
|
+
|
|
91
|
+
il_start = rcube.get_inline(0)
|
|
92
|
+
xl_start = rcube.get_crossline(0)
|
|
93
|
+
il_incr, xl_incr = rcube.inline_crossline_increment
|
|
94
|
+
il_end = (self._ncol) * il_incr + il_start
|
|
95
|
+
xl_end = (self._nrow) * xl_incr + xl_start
|
|
96
|
+
|
|
97
|
+
self._ilines = np.array(range(il_start, il_end, il_incr), dtype=np.int32)
|
|
98
|
+
self._xlines = np.array(range(xl_start, xl_end, xl_incr), dtype=np.int32)
|
|
99
|
+
|
|
100
|
+
# roxar API does not store traceid codes, assume 1
|
|
101
|
+
self._traceidcodes = np.ones((self._ncol, self._nrow), dtype=np.int32)
|
|
102
|
+
|
|
103
|
+
if rcube.is_empty:
|
|
104
|
+
xtg.warn("Cube has no data; assume 0")
|
|
105
|
+
else:
|
|
106
|
+
self.values = rcube.get_values()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def export_cube_roxapi(
|
|
110
|
+
self, project, name, folder=None, domain="time", compression=("wavelet", 5)
|
|
111
|
+
): # pragma: no cover
|
|
112
|
+
"""Export (store) a Seismic cube to RMS via ROXAR API spec."""
|
|
113
|
+
rox = RoxUtils(project, readonly=False)
|
|
114
|
+
|
|
115
|
+
logger.debug("TODO: compression %s", compression)
|
|
116
|
+
|
|
117
|
+
_roxapi_export_cube(
|
|
118
|
+
self,
|
|
119
|
+
rox.project,
|
|
120
|
+
rox,
|
|
121
|
+
name,
|
|
122
|
+
folder=folder,
|
|
123
|
+
domain=domain,
|
|
124
|
+
compression=compression,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if rox._roxexternal:
|
|
128
|
+
rox.project.save()
|
|
129
|
+
|
|
130
|
+
rox.safe_close()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _roxapi_export_cube(
|
|
134
|
+
self, proj, rox, name, folder=None, domain="time", compression=("wavelet", 5)
|
|
135
|
+
): # type: ignore # pragma: no cover
|
|
136
|
+
logger.info(
|
|
137
|
+
"There are issues with compression %s, hence it is ignored", compression
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
path = []
|
|
141
|
+
if folder is not None:
|
|
142
|
+
fld = folder.split("/")
|
|
143
|
+
path = fld + path
|
|
144
|
+
|
|
145
|
+
rcube = proj.seismic.data.create_cube(name, path=path)
|
|
146
|
+
|
|
147
|
+
# populate
|
|
148
|
+
origin = (float(self.xori), float(self.yori))
|
|
149
|
+
first_z = self.zori
|
|
150
|
+
increment = (self.xinc, self.yinc)
|
|
151
|
+
sample_rate = self.zinc
|
|
152
|
+
rotation = self.rotation
|
|
153
|
+
vertical_domain = roxar.VerticalDomain.time
|
|
154
|
+
if domain == "depth":
|
|
155
|
+
vertical_domain = roxar.VerticalDomain.depth
|
|
156
|
+
|
|
157
|
+
values = self.values.copy() # copy() needed?
|
|
158
|
+
|
|
159
|
+
handedness = roxar.Direction.left
|
|
160
|
+
if self.yflip == -1:
|
|
161
|
+
handedness = roxar.Direction.right
|
|
162
|
+
|
|
163
|
+
# inline xline vector
|
|
164
|
+
ilstart = self.ilines[0]
|
|
165
|
+
xlstart = self.xlines[0]
|
|
166
|
+
ilincr = self.ilines[1] - self.ilines[0]
|
|
167
|
+
xlincr = self.xlines[1] - self.xlines[0]
|
|
168
|
+
|
|
169
|
+
rcube.set_seismic(
|
|
170
|
+
values,
|
|
171
|
+
origin,
|
|
172
|
+
increment,
|
|
173
|
+
first_z,
|
|
174
|
+
sample_rate,
|
|
175
|
+
rotation * -1,
|
|
176
|
+
vertical_domain=vertical_domain,
|
|
177
|
+
handedness=handedness,
|
|
178
|
+
inline_crossline_start=(ilstart, xlstart),
|
|
179
|
+
inline_crossline_increment=(ilincr, xlincr),
|
|
180
|
+
)
|