xtgeo 4.8.0__cp313-cp313-macosx_11_0_arm64.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.
Potentially problematic release.
This version of xtgeo might be problematic. Click here for more details.
- cxtgeo.py +582 -0
- cxtgeoPYTHON_wrap.c +20938 -0
- xtgeo/__init__.py +246 -0
- xtgeo/_cxtgeo.cpython-313-darwin.so +0 -0
- xtgeo/_internal.cpython-313-darwin.so +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 +21 -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 +340 -0
- xtgeo/cube/cube1.py +1023 -0
- xtgeo/grid3d/__init__.py +15 -0
- xtgeo/grid3d/_ecl_grid.py +774 -0
- xtgeo/grid3d/_ecl_inte_head.py +148 -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 +266 -0
- xtgeo/grid3d/_grdecl_grid.py +388 -0
- xtgeo/grid3d/_grid3d.py +29 -0
- xtgeo/grid3d/_grid3d_fence.py +181 -0
- xtgeo/grid3d/_grid3d_utils.py +228 -0
- xtgeo/grid3d/_grid_boundary.py +76 -0
- xtgeo/grid3d/_grid_etc1.py +1566 -0
- xtgeo/grid3d/_grid_export.py +221 -0
- xtgeo/grid3d/_grid_hybrid.py +66 -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 +125 -0
- xtgeo/grid3d/_grid_roxapi.py +292 -0
- xtgeo/grid3d/_grid_wellzone.py +165 -0
- xtgeo/grid3d/_gridprop_export.py +178 -0
- xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
- xtgeo/grid3d/_gridprop_import_grdecl.py +130 -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 +174 -0
- xtgeo/grid3d/_gridprop_roxapi.py +239 -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 +469 -0
- xtgeo/grid3d/_roff_parameter.py +303 -0
- xtgeo/grid3d/grid.py +2537 -0
- xtgeo/grid3d/grid_properties.py +699 -0
- xtgeo/grid3d/grid_property.py +1341 -0
- xtgeo/grid3d/types.py +15 -0
- xtgeo/io/__init__.py +1 -0
- xtgeo/io/_file.py +592 -0
- xtgeo/metadata/__init__.py +17 -0
- xtgeo/metadata/metadata.py +431 -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 +18 -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 +271 -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 +631 -0
- xtgeo/surface/_regsurf_roxapi.py +241 -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 +2967 -0
- xtgeo/surface/surfaces.py +276 -0
- xtgeo/well/__init__.py +24 -0
- xtgeo/well/_blockedwell_roxapi.py +221 -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 +574 -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 +216 -0
- xtgeo/well/blocked_wells.py +122 -0
- xtgeo/well/well1.py +1514 -0
- xtgeo/well/wells.py +211 -0
- xtgeo/xyz/__init__.py +6 -0
- xtgeo/xyz/_polygons_oper.py +272 -0
- xtgeo/xyz/_xyz.py +741 -0
- xtgeo/xyz/_xyz_data.py +646 -0
- xtgeo/xyz/_xyz_io.py +490 -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 +681 -0
- xtgeo/xyz/polygons.py +811 -0
- xtgeo-4.8.0.dist-info/METADATA +145 -0
- xtgeo-4.8.0.dist-info/RECORD +117 -0
- xtgeo-4.8.0.dist-info/WHEEL +6 -0
- xtgeo-4.8.0.dist-info/licenses/LICENSE.md +165 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Cube utilities (basic low level)"""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from xtgeo import _cxtgeo
|
|
8
|
+
from xtgeo._cxtgeo import XTGeoCLibError
|
|
9
|
+
from xtgeo.common.calc import _swap_axes
|
|
10
|
+
from xtgeo.common.constants import UNDEF_LIMIT
|
|
11
|
+
from xtgeo.common.log import null_logger
|
|
12
|
+
from xtgeo.xyz.polygons import Polygons
|
|
13
|
+
|
|
14
|
+
logger = null_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def swapaxes(self):
|
|
18
|
+
"""Pure numpy/python version"""
|
|
19
|
+
self._rotation, self._yflip, swapped_values = _swap_axes(
|
|
20
|
+
self._rotation,
|
|
21
|
+
self._yflip,
|
|
22
|
+
values=self._values,
|
|
23
|
+
traceidcodes=self._traceidcodes,
|
|
24
|
+
)
|
|
25
|
+
self._ncol, self._nrow = self._nrow, self._ncol
|
|
26
|
+
self._xinc, self._yinc = self._yinc, self._xinc
|
|
27
|
+
self.values = swapped_values["values"]
|
|
28
|
+
self._traceidcodes = swapped_values["traceidcodes"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def thinning(self, icol, jrow, klay):
|
|
32
|
+
inputs = [icol, jrow, klay]
|
|
33
|
+
ranges = [self.nrow, self.ncol, self.nlay]
|
|
34
|
+
|
|
35
|
+
for inum, ixc in enumerate(inputs):
|
|
36
|
+
if not isinstance(ixc, int):
|
|
37
|
+
raise ValueError(f"Some input is not integer: {inputs}")
|
|
38
|
+
if ixc > ranges[inum] / 2:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Input numbers <{inputs}> are too large compared to existing "
|
|
41
|
+
f"ranges <{ranges}>"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# just simple numpy operations, and changing some cube props
|
|
45
|
+
|
|
46
|
+
val = self.values.copy()
|
|
47
|
+
|
|
48
|
+
val = val[::icol, ::jrow, ::klay]
|
|
49
|
+
self._ncol = val.shape[0]
|
|
50
|
+
self._nrow = val.shape[1]
|
|
51
|
+
self._nlay = val.shape[2]
|
|
52
|
+
self._xinc *= icol
|
|
53
|
+
self._yinc *= jrow
|
|
54
|
+
self._zinc *= klay
|
|
55
|
+
self._ilines = self._ilines[::icol]
|
|
56
|
+
self._xlines = self._xlines[::jrow]
|
|
57
|
+
self._traceidcodes = self._traceidcodes[::icol, ::jrow]
|
|
58
|
+
|
|
59
|
+
self.values = val
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cropping(self, icols, jrows, klays):
|
|
63
|
+
"""Cropping, where inputs are tuples"""
|
|
64
|
+
|
|
65
|
+
icol1, icol2 = icols
|
|
66
|
+
jrow1, jrow2 = jrows
|
|
67
|
+
klay1, klay2 = klays
|
|
68
|
+
|
|
69
|
+
val = self.values.copy()
|
|
70
|
+
ncol = self.ncol
|
|
71
|
+
nrow = self.nrow
|
|
72
|
+
nlay = self.nlay
|
|
73
|
+
|
|
74
|
+
val = val[
|
|
75
|
+
0 + icol1 : ncol - icol2, 0 + jrow1 : nrow - jrow2, 0 + klay1 : nlay - klay2
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
self._ncol = val.shape[0]
|
|
79
|
+
self._nrow = val.shape[1]
|
|
80
|
+
self._nlay = val.shape[2]
|
|
81
|
+
|
|
82
|
+
self._ilines = self._ilines[0 + icol1 : ncol - icol2]
|
|
83
|
+
self._xlines = self._xlines[0 + jrow1 : nrow - jrow2]
|
|
84
|
+
self.traceidcodes = self.traceidcodes[
|
|
85
|
+
0 + icol1 : ncol - icol2, 0 + jrow1 : nrow - jrow2
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# 1 + .., since the following routine as 1 as base for i j
|
|
89
|
+
ier, xpp, ypp = _cxtgeo.cube_xy_from_ij(
|
|
90
|
+
1 + icol1,
|
|
91
|
+
1 + jrow1,
|
|
92
|
+
self.xori,
|
|
93
|
+
self.xinc,
|
|
94
|
+
self.yori,
|
|
95
|
+
self.yinc,
|
|
96
|
+
ncol,
|
|
97
|
+
nrow,
|
|
98
|
+
self.yflip,
|
|
99
|
+
self.rotation,
|
|
100
|
+
0,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if ier != 0:
|
|
104
|
+
raise RuntimeError(f"Unexpected error, code is {ier}")
|
|
105
|
+
|
|
106
|
+
# get new X Y origins
|
|
107
|
+
self._xori = xpp
|
|
108
|
+
self._yori = ypp
|
|
109
|
+
self._zori = self.zori + klay1 * self.zinc
|
|
110
|
+
|
|
111
|
+
self.values = val
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def resample(self, other, sampling="nearest", outside_value=None):
|
|
115
|
+
"""Resample another cube to the current self"""
|
|
116
|
+
# TODO: traceidcodes
|
|
117
|
+
|
|
118
|
+
values1a = self.values.reshape(-1)
|
|
119
|
+
values2a = other.values.reshape(-1)
|
|
120
|
+
|
|
121
|
+
logger.info("Resampling, using %s...", sampling)
|
|
122
|
+
|
|
123
|
+
ier = _cxtgeo.cube_resample_cube(
|
|
124
|
+
self.ncol,
|
|
125
|
+
self.nrow,
|
|
126
|
+
self.nlay,
|
|
127
|
+
self.xori,
|
|
128
|
+
self.xinc,
|
|
129
|
+
self.yori,
|
|
130
|
+
self.yinc,
|
|
131
|
+
self.zori,
|
|
132
|
+
self.zinc,
|
|
133
|
+
self.rotation,
|
|
134
|
+
self.yflip,
|
|
135
|
+
values1a,
|
|
136
|
+
other.ncol,
|
|
137
|
+
other.nrow,
|
|
138
|
+
other.nlay,
|
|
139
|
+
other.xori,
|
|
140
|
+
other.xinc,
|
|
141
|
+
other.yori,
|
|
142
|
+
other.yinc,
|
|
143
|
+
other.zori,
|
|
144
|
+
other.zinc,
|
|
145
|
+
other.rotation,
|
|
146
|
+
other.yflip,
|
|
147
|
+
values2a,
|
|
148
|
+
1 if sampling == "trilinear" else 0,
|
|
149
|
+
0 if outside_value is None else 1,
|
|
150
|
+
0 if outside_value is None else outside_value,
|
|
151
|
+
)
|
|
152
|
+
if ier == -4:
|
|
153
|
+
warnings.warn("Less than 10% of origonal cube sampled", RuntimeWarning)
|
|
154
|
+
elif ier != 0:
|
|
155
|
+
raise XTGeoCLibError("cube_resample_cube failed to complete")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_xy_value_from_ij(self, iloc, jloc, ixline=False, zerobased=False):
|
|
159
|
+
"""Find X Y value from I J index, or corresponding inline/xline"""
|
|
160
|
+
# assumes that inline follows I and xlines follows J
|
|
161
|
+
|
|
162
|
+
iuse = iloc
|
|
163
|
+
juse = jloc
|
|
164
|
+
|
|
165
|
+
if zerobased:
|
|
166
|
+
iuse = iuse + 1
|
|
167
|
+
juse = juse + 1
|
|
168
|
+
|
|
169
|
+
if ixline:
|
|
170
|
+
ilst = self.ilines.tolist()
|
|
171
|
+
jlst = self.xlines.tolist()
|
|
172
|
+
iuse = ilst.index(iloc) + 1
|
|
173
|
+
juse = jlst.index(jloc) + 1
|
|
174
|
+
|
|
175
|
+
if 1 <= iuse <= self.ncol and 1 <= juse <= self.nrow:
|
|
176
|
+
ier, xval, yval = _cxtgeo.cube_xy_from_ij(
|
|
177
|
+
iuse,
|
|
178
|
+
juse,
|
|
179
|
+
self.xori,
|
|
180
|
+
self.xinc,
|
|
181
|
+
self.yori,
|
|
182
|
+
self.yinc,
|
|
183
|
+
self.ncol,
|
|
184
|
+
self.nrow,
|
|
185
|
+
self._yflip,
|
|
186
|
+
self.rotation,
|
|
187
|
+
0,
|
|
188
|
+
)
|
|
189
|
+
if ier != 0:
|
|
190
|
+
raise XTGeoCLibError(f"cube_xy_from_ij failed with error code: {ier}")
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError("Index i and/or j out of bounds")
|
|
194
|
+
|
|
195
|
+
return xval, yval
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_randomline(
|
|
199
|
+
self,
|
|
200
|
+
fencespec,
|
|
201
|
+
zmin=None,
|
|
202
|
+
zmax=None,
|
|
203
|
+
zincrement=None,
|
|
204
|
+
hincrement=None,
|
|
205
|
+
atleast=5,
|
|
206
|
+
nextend=2,
|
|
207
|
+
sampling="nearest",
|
|
208
|
+
):
|
|
209
|
+
"""Get a random line from a fence spesification"""
|
|
210
|
+
|
|
211
|
+
if isinstance(fencespec, Polygons):
|
|
212
|
+
logger.info("Estimate hincrement from Polygons instance...")
|
|
213
|
+
fencespec = _get_randomline_fence(self, fencespec, hincrement, atleast, nextend)
|
|
214
|
+
logger.info("Estimate hincrement from Polygons instance... DONE")
|
|
215
|
+
|
|
216
|
+
if not len(fencespec.shape) == 2:
|
|
217
|
+
raise ValueError("Fence is not a 2D numpy")
|
|
218
|
+
|
|
219
|
+
xcoords = fencespec[:, 0]
|
|
220
|
+
ycoords = fencespec[:, 1]
|
|
221
|
+
hcoords = fencespec[:, 3]
|
|
222
|
+
|
|
223
|
+
for ino in range(hcoords.shape[0] - 1):
|
|
224
|
+
dhv = hcoords[ino + 1] - hcoords[ino]
|
|
225
|
+
logger.info("Delta H along well path: %s", dhv)
|
|
226
|
+
|
|
227
|
+
zcubemax = self._zori + (self._nlay - 1) * self._zinc
|
|
228
|
+
if zmin is None or zmin < self._zori:
|
|
229
|
+
zmin = self._zori
|
|
230
|
+
|
|
231
|
+
if zmax is None or zmax > zcubemax:
|
|
232
|
+
zmax = zcubemax
|
|
233
|
+
|
|
234
|
+
if zincrement is None:
|
|
235
|
+
zincrement = self._zinc / 2.0
|
|
236
|
+
|
|
237
|
+
nzsam = int((zmax - zmin) / zincrement) + 1
|
|
238
|
+
|
|
239
|
+
nsamples = xcoords.shape[0] * nzsam
|
|
240
|
+
|
|
241
|
+
option = 0
|
|
242
|
+
if sampling == "trilinear":
|
|
243
|
+
option = 1
|
|
244
|
+
|
|
245
|
+
_ier, values = _cxtgeo.cube_get_randomline(
|
|
246
|
+
xcoords,
|
|
247
|
+
ycoords,
|
|
248
|
+
zmin,
|
|
249
|
+
zmax,
|
|
250
|
+
nzsam,
|
|
251
|
+
self._xori,
|
|
252
|
+
self._xinc,
|
|
253
|
+
self._yori,
|
|
254
|
+
self._yinc,
|
|
255
|
+
self._zori,
|
|
256
|
+
self._zinc,
|
|
257
|
+
self._rotation,
|
|
258
|
+
self._yflip,
|
|
259
|
+
self._ncol,
|
|
260
|
+
self._nrow,
|
|
261
|
+
self._nlay,
|
|
262
|
+
self._values.reshape(-1),
|
|
263
|
+
nsamples,
|
|
264
|
+
option,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
values[values > UNDEF_LIMIT] = np.nan
|
|
268
|
+
arr = values.reshape((xcoords.shape[0], nzsam)).T
|
|
269
|
+
|
|
270
|
+
return (hcoords[0], hcoords[-1], zmin, zmax, arr)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _get_randomline_fence(self, fencespec, hincrement, atleast, nextend):
|
|
274
|
+
"""Compute a resampled fence from a Polygons instance"""
|
|
275
|
+
|
|
276
|
+
if hincrement is None:
|
|
277
|
+
avgdxdy = 0.5 * (self.xinc + self.yinc)
|
|
278
|
+
distance = 0.5 * avgdxdy
|
|
279
|
+
else:
|
|
280
|
+
distance = hincrement
|
|
281
|
+
|
|
282
|
+
logger.info("Getting fence from a Polygons instance...")
|
|
283
|
+
fspec = fencespec.get_fence(
|
|
284
|
+
distance=distance, atleast=atleast, nextend=nextend, asnumpy=True
|
|
285
|
+
)
|
|
286
|
+
logger.info("Getting fence from a Polygons instance... DONE")
|
|
287
|
+
return fspec
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Attributes for a Cube to maps (surfaces), slice an interval, in pure numpy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING, Final
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.interpolate import make_interp_spline
|
|
11
|
+
|
|
12
|
+
import xtgeo._internal as _internal # type: ignore
|
|
13
|
+
from xtgeo.common.log import null_logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from xtgeo.surface.regular_surface import RegularSurface
|
|
17
|
+
|
|
18
|
+
from . import Cube
|
|
19
|
+
|
|
20
|
+
logger = null_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
STAT_ATTRS: Final = [
|
|
24
|
+
"min",
|
|
25
|
+
"max",
|
|
26
|
+
"mean",
|
|
27
|
+
"var",
|
|
28
|
+
"rms",
|
|
29
|
+
"maxpos",
|
|
30
|
+
"maxneg",
|
|
31
|
+
"maxabs",
|
|
32
|
+
"meanpos",
|
|
33
|
+
"meanneg",
|
|
34
|
+
"meanabs",
|
|
35
|
+
]
|
|
36
|
+
SUM_ATTRS: Final = [
|
|
37
|
+
"sumpos",
|
|
38
|
+
"sumneg",
|
|
39
|
+
"sumabs",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CubeAttrs:
|
|
45
|
+
"""Internal class for computing attributes in window between two surfaces."""
|
|
46
|
+
|
|
47
|
+
cube: Cube
|
|
48
|
+
upper_surface: RegularSurface | float | int
|
|
49
|
+
lower_surface: RegularSurface | float | int
|
|
50
|
+
ndiv: int = 10
|
|
51
|
+
interpolation: str = "cubic" # cf. scipy's make_interp_spline() when k=3
|
|
52
|
+
minimum_thickness: float = 0.0
|
|
53
|
+
|
|
54
|
+
# internal attributes
|
|
55
|
+
_template_surface: RegularSurface | None = None
|
|
56
|
+
_depth_array: np.ndarray | None = None
|
|
57
|
+
_outside_depth: float | None = None # detected and updated from the depth cube
|
|
58
|
+
_reduced_cube: Cube = None
|
|
59
|
+
_reduced_depth_array: np.ndarray | None = None
|
|
60
|
+
_refined_cube: Cube | None = None
|
|
61
|
+
_refined_depth_array: np.ndarray | None = None
|
|
62
|
+
|
|
63
|
+
_upper: RegularSurface | None = None # upper surf, resampled to cube map resolution
|
|
64
|
+
_lower: RegularSurface | None = None # lower surf, resampled to cube map resolution
|
|
65
|
+
_min_thickness_mask: RegularSurface | None = None # mask for min. thickness trunc.
|
|
66
|
+
|
|
67
|
+
_result_attr_maps: dict = field(default_factory=dict) # holds the resulting maps
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
self._process_upper_lower_surface()
|
|
71
|
+
self._create_depth_array()
|
|
72
|
+
self._create_reduced_cube()
|
|
73
|
+
self._refine_interpolate()
|
|
74
|
+
self._depth_mask()
|
|
75
|
+
self._compute_statistical_attribute_surfaces()
|
|
76
|
+
|
|
77
|
+
def result(self) -> dict[RegularSurface]:
|
|
78
|
+
# return the resulting attribute maps
|
|
79
|
+
return self._result_attr_maps
|
|
80
|
+
|
|
81
|
+
def _process_upper_lower_surface(self) -> None:
|
|
82
|
+
"""Extract upper and lower surface, sampled to cube resolution."""
|
|
83
|
+
|
|
84
|
+
from xtgeo import surface_from_cube # avoid circular import by having this here
|
|
85
|
+
|
|
86
|
+
upper = (
|
|
87
|
+
surface_from_cube(self.cube, self.upper_surface)
|
|
88
|
+
if isinstance(self.upper_surface, (float, int))
|
|
89
|
+
else self.upper_surface
|
|
90
|
+
)
|
|
91
|
+
lower = (
|
|
92
|
+
surface_from_cube(self.cube, self.lower_surface)
|
|
93
|
+
if isinstance(self.lower_surface, (float, int))
|
|
94
|
+
else self.lower_surface
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._template_surface = (
|
|
98
|
+
upper
|
|
99
|
+
if isinstance(self.upper_surface, (float, int))
|
|
100
|
+
else self.upper_surface
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# determine which of "this" and "other" is actually upper and lower
|
|
104
|
+
if (lower - upper).values.mean() < 0:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"The upper surface is below the lower surface. "
|
|
107
|
+
"Please provide the surfaces in the correct order."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# although not an attribute, we store the upper and lower surfaces
|
|
111
|
+
self._result_attr_maps["upper"] = upper
|
|
112
|
+
self._result_attr_maps["lower"] = lower
|
|
113
|
+
|
|
114
|
+
# get the surfaces on cube resolution
|
|
115
|
+
self._upper = surface_from_cube(self.cube, self.cube.zori)
|
|
116
|
+
self._lower = surface_from_cube(self.cube, self.cube.zori)
|
|
117
|
+
self._upper.resample(upper)
|
|
118
|
+
self._lower.resample(lower)
|
|
119
|
+
|
|
120
|
+
self._upper.fill()
|
|
121
|
+
self._lower.fill()
|
|
122
|
+
|
|
123
|
+
self._min_thickness_mask = self._lower - self._upper
|
|
124
|
+
|
|
125
|
+
self._min_thickness_mask.values = np.where(
|
|
126
|
+
self._min_thickness_mask.values <= self.minimum_thickness, 0, 1
|
|
127
|
+
)
|
|
128
|
+
if np.all(self._min_thickness_mask.values == 0):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"The minimum thickness is too large, no valid data in the interval. "
|
|
131
|
+
"Perhaps surfaces are overlapping?"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _create_depth_array(self) -> None:
|
|
135
|
+
"""Create a 1D array where values are cube depths; to be used as filter.
|
|
136
|
+
|
|
137
|
+
Belowe and above the input surfaces (plus a buffer), the values are set to
|
|
138
|
+
a constant value self._outside_depth.
|
|
139
|
+
|
|
140
|
+
Will also issue warnings or errors if the surfaces are outside the cube,
|
|
141
|
+
depending on severity.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
self._depth_array = np.array(
|
|
145
|
+
[
|
|
146
|
+
self.cube.zori + n * self.cube.zinc
|
|
147
|
+
for n in range(self.cube.values.shape[2])
|
|
148
|
+
]
|
|
149
|
+
).astype(np.float32)
|
|
150
|
+
|
|
151
|
+
# check that surfaces are within the cube
|
|
152
|
+
if self._upper.values.min() > self._depth_array.max():
|
|
153
|
+
raise ValueError("Upper surface is fully below the cube")
|
|
154
|
+
if self._lower.values.max() < self._depth_array.min():
|
|
155
|
+
raise ValueError("Lower surface is fully above the cube")
|
|
156
|
+
if self._upper.values.max() < self._depth_array.min():
|
|
157
|
+
warnings.warn("Upper surface is fully above the cube", UserWarning)
|
|
158
|
+
if self._lower.values.min() > self._depth_array.max():
|
|
159
|
+
warnings.warn("Lower surface is fully below the cube", UserWarning)
|
|
160
|
+
|
|
161
|
+
self._outside_depth = self._depth_array.max() + 1
|
|
162
|
+
|
|
163
|
+
add_extra_depth = 2 * self.cube.zinc # add buffer on upper/lower edges
|
|
164
|
+
|
|
165
|
+
self._depth_array = np.where(
|
|
166
|
+
self._depth_array < self._upper.values.min() - add_extra_depth,
|
|
167
|
+
self._outside_depth,
|
|
168
|
+
self._depth_array,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._depth_array = np.where(
|
|
172
|
+
self._depth_array > self._lower.values.max() + add_extra_depth,
|
|
173
|
+
self._outside_depth,
|
|
174
|
+
self._depth_array,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _create_reduced_cube(self) -> None:
|
|
178
|
+
"""Create a smaller cube based on the depth cube filter.
|
|
179
|
+
|
|
180
|
+
The purpose is to limit the computation to the relevant volume, to save
|
|
181
|
+
CPU time. I.e. cube values above the upper surface and below the lower are
|
|
182
|
+
now excluded.
|
|
183
|
+
"""
|
|
184
|
+
from xtgeo import Cube # avoid circular import by having this here
|
|
185
|
+
|
|
186
|
+
cubev = self.cube.values.copy() # copy, so we don't change the input instance
|
|
187
|
+
cubev[self.cube.traceidcodes == 2] = np.nan # set dead traces to nan
|
|
188
|
+
|
|
189
|
+
# Create a boolean mask based on the threshold
|
|
190
|
+
mask = self._depth_array < self._outside_depth
|
|
191
|
+
|
|
192
|
+
# Find the bounding box of the true values
|
|
193
|
+
non_zero_indices = np.nonzero(mask)[0]
|
|
194
|
+
|
|
195
|
+
if len(non_zero_indices) == 0:
|
|
196
|
+
raise RuntimeError( # e.g. if cube and surfaces are at different locations
|
|
197
|
+
"No valid data found in the depth cube. Perhaps the surfaces are "
|
|
198
|
+
"outside the cube?"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
min_indices = np.min(non_zero_indices)
|
|
202
|
+
max_indices = np.max(non_zero_indices) + 1 # Add 1 to include the upper bound
|
|
203
|
+
|
|
204
|
+
# Extract the reduced cube using slicing
|
|
205
|
+
reduced = cubev[:, :, min_indices:max_indices]
|
|
206
|
+
|
|
207
|
+
zori = float(self._depth_array.min())
|
|
208
|
+
|
|
209
|
+
self._reduced_cube = Cube(
|
|
210
|
+
ncol=reduced.shape[0],
|
|
211
|
+
nrow=reduced.shape[1],
|
|
212
|
+
nlay=reduced.shape[2],
|
|
213
|
+
xinc=self.cube.xinc,
|
|
214
|
+
yinc=self.cube.yinc,
|
|
215
|
+
zinc=self.cube.zinc,
|
|
216
|
+
xori=self.cube.xori,
|
|
217
|
+
yori=self.cube.yori,
|
|
218
|
+
zori=zori,
|
|
219
|
+
rotation=self.cube.rotation,
|
|
220
|
+
yflip=self.cube.yflip,
|
|
221
|
+
values=reduced.astype(np.float32),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._reduced_depth_array = self._depth_array[min_indices:max_indices]
|
|
225
|
+
|
|
226
|
+
logger.debug("Reduced cubes created %s", self._reduced_cube.values.shape)
|
|
227
|
+
|
|
228
|
+
def _refine_interpolate(self) -> None:
|
|
229
|
+
"""Apply reduced cubes and interpolate to a finer grid vertically.
|
|
230
|
+
|
|
231
|
+
This is done to get a more accurate representation of the cube values.
|
|
232
|
+
"""
|
|
233
|
+
from xtgeo import Cube
|
|
234
|
+
|
|
235
|
+
logger.debug("Refine cubes and interpolate...")
|
|
236
|
+
arr = self._reduced_cube.values
|
|
237
|
+
ndiv = self.ndiv
|
|
238
|
+
|
|
239
|
+
# Create linear interpolation function along the last axis
|
|
240
|
+
fdepth = make_interp_spline(
|
|
241
|
+
np.arange(arr.shape[2]),
|
|
242
|
+
self._reduced_depth_array,
|
|
243
|
+
axis=0,
|
|
244
|
+
k=1,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Create interpolation function along the last axis
|
|
248
|
+
if self.interpolation not in ["cubic", "linear"]:
|
|
249
|
+
raise ValueError("Interpolation must be either 'cubic' or 'linear'")
|
|
250
|
+
|
|
251
|
+
fcube = make_interp_spline(
|
|
252
|
+
np.arange(arr.shape[2]),
|
|
253
|
+
arr,
|
|
254
|
+
axis=2,
|
|
255
|
+
k=3 if self.interpolation == "cubic" else 1,
|
|
256
|
+
)
|
|
257
|
+
# Define new sampling points along the last axis
|
|
258
|
+
new_z = np.linspace(0, arr.shape[2] - 1, arr.shape[2] * ndiv)
|
|
259
|
+
|
|
260
|
+
# Resample the cube array
|
|
261
|
+
resampled_arr = fcube(new_z)
|
|
262
|
+
|
|
263
|
+
# Resample the depth array (always linear)
|
|
264
|
+
self._refined_depth_array = new_depth = fdepth(new_z)
|
|
265
|
+
new_zinc = (new_depth.max() - new_depth.min()) / (new_depth.shape[0] - 1)
|
|
266
|
+
|
|
267
|
+
self._refined_cube = Cube(
|
|
268
|
+
ncol=resampled_arr.shape[0],
|
|
269
|
+
nrow=resampled_arr.shape[1],
|
|
270
|
+
nlay=resampled_arr.shape[2],
|
|
271
|
+
xinc=self.cube.xinc,
|
|
272
|
+
yinc=self.cube.yinc,
|
|
273
|
+
zinc=new_zinc,
|
|
274
|
+
xori=self.cube.xori,
|
|
275
|
+
yori=self.cube.yori,
|
|
276
|
+
zori=self._refined_depth_array.min(),
|
|
277
|
+
rotation=self._reduced_cube.rotation,
|
|
278
|
+
yflip=self._reduced_cube.yflip,
|
|
279
|
+
values=resampled_arr.astype(np.float32),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _depth_mask(self) -> None:
|
|
283
|
+
"""Set nan values outside the interval defined by the upper + lower surface.
|
|
284
|
+
|
|
285
|
+
In addition, set nan values where the thickness is less than the minimum.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
darry = np.expand_dims(self._refined_depth_array, axis=(0, 1))
|
|
290
|
+
upper_exp = np.expand_dims(self._upper.values, 2)
|
|
291
|
+
lower_exp = np.expand_dims(self._lower.values, 2)
|
|
292
|
+
mask_2d_exp = np.expand_dims(self._min_thickness_mask.values, 2)
|
|
293
|
+
|
|
294
|
+
self._refined_cube.values = np.where(
|
|
295
|
+
(darry < upper_exp) | (darry > lower_exp) | (mask_2d_exp == 0),
|
|
296
|
+
np.nan,
|
|
297
|
+
self._refined_cube.values,
|
|
298
|
+
).astype(np.float32)
|
|
299
|
+
|
|
300
|
+
# similar for reduced cubes with original resolution
|
|
301
|
+
darry = np.expand_dims(self._reduced_depth_array, axis=(0, 1))
|
|
302
|
+
|
|
303
|
+
self._reduced_cube.values = np.where(
|
|
304
|
+
(darry < upper_exp) | (darry > lower_exp) | (mask_2d_exp == 0),
|
|
305
|
+
np.nan,
|
|
306
|
+
self._reduced_cube.values,
|
|
307
|
+
).astype(np.float32)
|
|
308
|
+
|
|
309
|
+
def _add_to_attribute_map(self, attr_name: str, values: np.ndarray) -> None:
|
|
310
|
+
"""Compute the attribute map and add to result dictionary."""
|
|
311
|
+
attr_map = self._upper.copy()
|
|
312
|
+
attr_map.values = np.ma.masked_invalid(values)
|
|
313
|
+
|
|
314
|
+
# now resample to the original input map
|
|
315
|
+
attr_map_resampled = self._template_surface.copy()
|
|
316
|
+
attr_map_resampled.resample(attr_map)
|
|
317
|
+
|
|
318
|
+
attr_map_resampled.values = np.ma.masked_where(
|
|
319
|
+
self.upper_surface.values.mask, attr_map_resampled.values
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
self._result_attr_maps[attr_name] = attr_map_resampled
|
|
323
|
+
|
|
324
|
+
def _compute_statistical_attribute_surfaces(self) -> None:
|
|
325
|
+
"""Compute stats very fast by using internal C++ bindings."""
|
|
326
|
+
|
|
327
|
+
# compute statistics for vertically refined cube
|
|
328
|
+
cubecpp = _internal.cube.Cube(self._refined_cube)
|
|
329
|
+
all_attrs = cubecpp.cube_stats_along_z()
|
|
330
|
+
|
|
331
|
+
for attr in STAT_ATTRS:
|
|
332
|
+
self._add_to_attribute_map(attr, all_attrs[attr])
|
|
333
|
+
|
|
334
|
+
# compute statistics for reduced cube (for sum attributes)
|
|
335
|
+
cubecpp = _internal.cube.Cube(self._reduced_cube)
|
|
336
|
+
all_attrs = cubecpp.cube_stats_along_z()
|
|
337
|
+
|
|
338
|
+
# add sum attributes which are the last 3 in the list
|
|
339
|
+
for attr in SUM_ATTRS:
|
|
340
|
+
self._add_to_attribute_map(attr, all_attrs[attr])
|