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
@@ -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")