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,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,273 @@
|
|
|
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
|
+
|
|
11
|
+
import xtgeo._internal as _internal # type: ignore
|
|
12
|
+
from xtgeo.common.log import null_logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from xtgeo.surface.regular_surface import RegularSurface
|
|
16
|
+
|
|
17
|
+
from . import Cube
|
|
18
|
+
|
|
19
|
+
logger = null_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
STAT_ATTRS: Final = [
|
|
23
|
+
"min",
|
|
24
|
+
"max",
|
|
25
|
+
"mean",
|
|
26
|
+
"var",
|
|
27
|
+
"rms",
|
|
28
|
+
"maxpos",
|
|
29
|
+
"maxneg",
|
|
30
|
+
"maxabs",
|
|
31
|
+
"meanpos",
|
|
32
|
+
"meanneg",
|
|
33
|
+
"meanabs",
|
|
34
|
+
]
|
|
35
|
+
SUM_ATTRS: Final = [
|
|
36
|
+
"sumpos",
|
|
37
|
+
"sumneg",
|
|
38
|
+
"sumabs",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class CubeAttrs:
|
|
44
|
+
"""Internal class for computing attributes in window between two surfaces.
|
|
45
|
+
|
|
46
|
+
Compared with the former implementation (mid September 2025), more logic is moved
|
|
47
|
+
to the C++ routine, ensuring:
|
|
48
|
+
- Significantly smaller memory overhead (e.g. 0.1 GB vs 20 GB)
|
|
49
|
+
- Much faster execution, in particularly when using multiple processers. (5-10 x)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
cube: Cube
|
|
53
|
+
upper_surface: RegularSurface | float | int
|
|
54
|
+
lower_surface: RegularSurface | float | int
|
|
55
|
+
ndiv: int = 10
|
|
56
|
+
interpolation: str = "cubic" # cf. scipy's make_interp_spline() when k=3
|
|
57
|
+
minimum_thickness: float = 0.0
|
|
58
|
+
|
|
59
|
+
# internal attributes
|
|
60
|
+
_template_surface: RegularSurface | None = None
|
|
61
|
+
_depth_array: np.ndarray | None = None
|
|
62
|
+
_outside_depth: float | None = None # detected and updated from the depth cube
|
|
63
|
+
_min_indices: int = 0 # minimum Z index for cube slicing
|
|
64
|
+
_max_indices: int = 0 # maximum Z index for cube slicing
|
|
65
|
+
_reduced_cube: Cube = None
|
|
66
|
+
_reduced_depth_array: np.ndarray | None = None
|
|
67
|
+
_refined_cube: Cube | None = None
|
|
68
|
+
_refined_depth_array: np.ndarray | None = None
|
|
69
|
+
|
|
70
|
+
_upper: RegularSurface | None = None # upper surf, resampled to cube map resolution
|
|
71
|
+
_lower: RegularSurface | None = None # lower surf, resampled to cube map resolution
|
|
72
|
+
_min_thickness_mask: RegularSurface | None = None # mask for min. thickness trunc.
|
|
73
|
+
_mask_map_by_traceidcode: RegularSurface | None = None # mask for traceidcode 2
|
|
74
|
+
|
|
75
|
+
_result_attr_maps: dict = field(default_factory=dict) # holds the resulting maps
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
self._process_upper_lower_surface()
|
|
79
|
+
self._create_depth_array()
|
|
80
|
+
self._determine_slice_indices()
|
|
81
|
+
self._compute_statistical_attribute_surfaces()
|
|
82
|
+
|
|
83
|
+
def result(self) -> dict[RegularSurface]:
|
|
84
|
+
# return the resulting attribute maps
|
|
85
|
+
return self._result_attr_maps
|
|
86
|
+
|
|
87
|
+
def _process_upper_lower_surface(self) -> None:
|
|
88
|
+
"""Extract upper and lower surface, sampled to cube resolution."""
|
|
89
|
+
|
|
90
|
+
from xtgeo import surface_from_cube # avoid circular import by having this here
|
|
91
|
+
|
|
92
|
+
logger.debug("Process upper and lower surface...")
|
|
93
|
+
|
|
94
|
+
upper = (
|
|
95
|
+
surface_from_cube(self.cube, self.upper_surface)
|
|
96
|
+
if isinstance(self.upper_surface, (float, int))
|
|
97
|
+
else self.upper_surface
|
|
98
|
+
)
|
|
99
|
+
lower = (
|
|
100
|
+
surface_from_cube(self.cube, self.lower_surface)
|
|
101
|
+
if isinstance(self.lower_surface, (float, int))
|
|
102
|
+
else self.lower_surface
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# the template surface is the topology that defines the resulting attribute maps
|
|
106
|
+
self._template_surface = (
|
|
107
|
+
upper
|
|
108
|
+
if isinstance(self.upper_surface, (float, int))
|
|
109
|
+
else self.upper_surface
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# determine which of "this" and "other" is actually upper and lower
|
|
113
|
+
if (lower - upper).values.mean() < 0:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"The upper surface is below the lower surface. "
|
|
116
|
+
"Please provide the surfaces in the correct order."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# although not an attribute, we store the upper and lower surfaces
|
|
120
|
+
self._result_attr_maps["upper"] = upper
|
|
121
|
+
self._result_attr_maps["lower"] = lower
|
|
122
|
+
|
|
123
|
+
# get the surfaces on cube resolution
|
|
124
|
+
self._upper = surface_from_cube(self.cube, self.cube.zori)
|
|
125
|
+
self._lower = surface_from_cube(self.cube, self.cube.zori)
|
|
126
|
+
self._upper.resample(upper)
|
|
127
|
+
self._lower.resample(lower)
|
|
128
|
+
|
|
129
|
+
self._upper.fill()
|
|
130
|
+
self._lower.fill()
|
|
131
|
+
|
|
132
|
+
self._min_thickness_mask = self._lower - self._upper
|
|
133
|
+
|
|
134
|
+
self._min_thickness_mask.values = np.where(
|
|
135
|
+
self._min_thickness_mask.values <= self.minimum_thickness, 0, 1
|
|
136
|
+
)
|
|
137
|
+
if np.all(self._min_thickness_mask.values == 0):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"The minimum thickness is too large, no valid data in the interval. "
|
|
140
|
+
"Perhaps surfaces are overlapping?"
|
|
141
|
+
)
|
|
142
|
+
logger.debug("Process upper and lower surface... done")
|
|
143
|
+
|
|
144
|
+
def _create_depth_array(self) -> None:
|
|
145
|
+
"""Create a 1D array where values are cube depths; to be used as filter.
|
|
146
|
+
|
|
147
|
+
Belowe and above the input surfaces (plus a buffer), the values are set to
|
|
148
|
+
a constant value self._outside_depth.
|
|
149
|
+
|
|
150
|
+
Will also issue warnings or errors if the surfaces are outside the cube,
|
|
151
|
+
depending on severity.
|
|
152
|
+
"""
|
|
153
|
+
logger.debug("Create depth array...")
|
|
154
|
+
|
|
155
|
+
self._depth_array = np.array(
|
|
156
|
+
[
|
|
157
|
+
self.cube.zori + n * self.cube.zinc
|
|
158
|
+
for n in range(self.cube.values.shape[2])
|
|
159
|
+
]
|
|
160
|
+
).astype(np.float32)
|
|
161
|
+
|
|
162
|
+
# check that surfaces are within the cube
|
|
163
|
+
if self._upper.values.min() > self._depth_array.max():
|
|
164
|
+
raise ValueError("Upper surface is fully below the cube")
|
|
165
|
+
if self._lower.values.max() < self._depth_array.min():
|
|
166
|
+
raise ValueError("Lower surface is fully above the cube")
|
|
167
|
+
if self._upper.values.max() < self._depth_array.min():
|
|
168
|
+
warnings.warn("Upper surface is fully above the cube", UserWarning)
|
|
169
|
+
if self._lower.values.min() > self._depth_array.max():
|
|
170
|
+
warnings.warn("Lower surface is fully below the cube", UserWarning)
|
|
171
|
+
|
|
172
|
+
self._outside_depth = self._depth_array.max() + 1
|
|
173
|
+
|
|
174
|
+
add_extra_depth = 2 * self.cube.zinc # add buffer on upper/lower edges
|
|
175
|
+
|
|
176
|
+
self._depth_array = np.where(
|
|
177
|
+
self._depth_array < self._upper.values.min() - add_extra_depth,
|
|
178
|
+
self._outside_depth,
|
|
179
|
+
self._depth_array,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
self._depth_array = np.where(
|
|
183
|
+
self._depth_array > self._lower.values.max() + add_extra_depth,
|
|
184
|
+
self._outside_depth,
|
|
185
|
+
self._depth_array,
|
|
186
|
+
)
|
|
187
|
+
logger.debug("Create depth array... done")
|
|
188
|
+
|
|
189
|
+
def _determine_slice_indices(self) -> None:
|
|
190
|
+
"""Create parameters for cube slicing.
|
|
191
|
+
|
|
192
|
+
The purpose is to limit the computation to the relevant volume, to save
|
|
193
|
+
CPU time. I.e. cube values above the upper surface and below the lower are
|
|
194
|
+
now excluded.
|
|
195
|
+
"""
|
|
196
|
+
logger.debug("Determine cube slice indices...")
|
|
197
|
+
|
|
198
|
+
# Create a boolean mask based on the threshold
|
|
199
|
+
mask = self._depth_array < self._outside_depth
|
|
200
|
+
|
|
201
|
+
# Find the bounding box of the true values
|
|
202
|
+
non_zero_indices = np.nonzero(mask)[0]
|
|
203
|
+
|
|
204
|
+
if len(non_zero_indices) == 0:
|
|
205
|
+
raise RuntimeError( # e.g. if cube and surfaces are at different locations
|
|
206
|
+
"No valid data found in the depth cube. Perhaps the surfaces are "
|
|
207
|
+
"outside the cube?"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
self._min_indices = int(np.min(non_zero_indices))
|
|
211
|
+
# Add 1 to include the upper bound
|
|
212
|
+
self._max_indices = int(np.max(non_zero_indices) + 1)
|
|
213
|
+
|
|
214
|
+
logger.debug("Determine cube slice indices... done")
|
|
215
|
+
logger.debug(
|
|
216
|
+
"Cube slice indices: %d to %d", self._min_indices, self._max_indices
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _add_to_attribute_map(self, attr_name: str, values: np.ndarray) -> None:
|
|
220
|
+
"""Compute the attribute map and add to result dictionary."""
|
|
221
|
+
logger.debug("Add to attribute map...")
|
|
222
|
+
attr_map = self._upper.copy()
|
|
223
|
+
attr_map.values = np.ma.masked_invalid(values)
|
|
224
|
+
|
|
225
|
+
# now resample to the original input map
|
|
226
|
+
attr_map_resampled = self._template_surface.copy()
|
|
227
|
+
attr_map_resampled.resample(attr_map)
|
|
228
|
+
|
|
229
|
+
# Use template_surface consistently for masking (it's already set correctly)
|
|
230
|
+
if hasattr(self._template_surface.values, "mask"):
|
|
231
|
+
attr_map_resampled.values = np.ma.masked_where(
|
|
232
|
+
self._template_surface.values.mask, attr_map_resampled.values
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self._result_attr_maps[attr_name] = attr_map_resampled
|
|
236
|
+
logger.debug("Add to attribute map... done")
|
|
237
|
+
|
|
238
|
+
def _compute_statistical_attribute_surfaces(self) -> None:
|
|
239
|
+
"""Compute stats very fast by using internal C++ bindings."""
|
|
240
|
+
logger.debug("Compute statistical attribute surfaces...")
|
|
241
|
+
|
|
242
|
+
# compute statistics for vertically refined cube using original cube
|
|
243
|
+
cubecpp = _internal.cube.Cube(self.cube)
|
|
244
|
+
all_attrs = cubecpp.cube_stats_along_z(
|
|
245
|
+
self._upper.values,
|
|
246
|
+
self._lower.values,
|
|
247
|
+
self._depth_array, # use original depth array
|
|
248
|
+
self.ndiv,
|
|
249
|
+
self.interpolation,
|
|
250
|
+
self.minimum_thickness,
|
|
251
|
+
self._min_indices, # pass slice indices
|
|
252
|
+
self._max_indices,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
for attr in STAT_ATTRS:
|
|
256
|
+
self._add_to_attribute_map(attr, all_attrs[attr])
|
|
257
|
+
|
|
258
|
+
# compute statistics with ndiv=1 (for sum attributes)
|
|
259
|
+
all_attrs = cubecpp.cube_stats_along_z(
|
|
260
|
+
self._upper.values,
|
|
261
|
+
self._lower.values,
|
|
262
|
+
self._depth_array, # use original depth array
|
|
263
|
+
1,
|
|
264
|
+
self.interpolation,
|
|
265
|
+
self.minimum_thickness,
|
|
266
|
+
self._min_indices, # pass slice indices
|
|
267
|
+
self._max_indices,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
for attr in SUM_ATTRS:
|
|
271
|
+
self._add_to_attribute_map(attr, all_attrs[attr])
|
|
272
|
+
|
|
273
|
+
logger.debug("Compute statistical attribute surfaces... done")
|