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,168 @@
|
|
|
1
|
+
"""GridProperty import function of xtgcpprop format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from io import BytesIO, StringIO
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from struct import unpack
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
import xtgeo.common.sys as xsys
|
|
15
|
+
from xtgeo.common import null_logger
|
|
16
|
+
from xtgeo.common.constants import UNDEF, UNDEF_INT
|
|
17
|
+
from xtgeo.metadata.metadata import MetaDataCPProperty
|
|
18
|
+
|
|
19
|
+
logger = null_logger(__name__)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
|
|
24
|
+
from numpy.typing import DTypeLike
|
|
25
|
+
|
|
26
|
+
from xtgeo.io._file import FileWrapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def _read_from_stream(
|
|
31
|
+
stream: BytesIO | StringIO,
|
|
32
|
+
size: int | None,
|
|
33
|
+
seek: int | None,
|
|
34
|
+
) -> Generator[bytes, None, None]:
|
|
35
|
+
"""Helper function to read from a stream with optional seeking."""
|
|
36
|
+
was_at = stream.tell()
|
|
37
|
+
if seek is not None:
|
|
38
|
+
stream.seek(seek)
|
|
39
|
+
try:
|
|
40
|
+
data = stream.read(size)
|
|
41
|
+
yield data if isinstance(data, bytes) else data.encode()
|
|
42
|
+
finally:
|
|
43
|
+
stream.seek(was_at)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@contextmanager
|
|
47
|
+
def _read_filelike(
|
|
48
|
+
filelike: Path | BytesIO | StringIO,
|
|
49
|
+
size: int | None = None,
|
|
50
|
+
seek: int | None = None,
|
|
51
|
+
) -> Generator[bytes, None, None]:
|
|
52
|
+
"""Context manager for reading a specified number of bytes from a file-like object.
|
|
53
|
+
|
|
54
|
+
Accepts either a Path, BytesIO, or StringIO object as input. The function reads
|
|
55
|
+
up to 'offset' bytes from the file-like object and yields these bytes.
|
|
56
|
+
|
|
57
|
+
For BytesIO and StringIO, the read operation preserves the original
|
|
58
|
+
file cursor position.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
if isinstance(filelike, Path):
|
|
62
|
+
with filelike.open("rb") as f:
|
|
63
|
+
if seek is not None:
|
|
64
|
+
f.seek(seek)
|
|
65
|
+
yield f.read(size)
|
|
66
|
+
elif isinstance(filelike, (BytesIO, StringIO)):
|
|
67
|
+
with _read_from_stream(stream=filelike, size=size, seek=seek) as f:
|
|
68
|
+
yield f
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError("Filelike must be one of: Path, BytesIO or StringIO.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def import_xtgcpprop(
|
|
74
|
+
mfile: FileWrapper,
|
|
75
|
+
ijrange: Sequence[int] | None = None,
|
|
76
|
+
zerobased: bool = False,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Using pure python for experimental xtgcpprop import.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
mfile (FileWrapper): Input file reference
|
|
82
|
+
ijrange (list-like): List or tuple with 4 members [i_from, i_to, j_from, j_to]
|
|
83
|
+
where cell indices are zero based (starts with 0)
|
|
84
|
+
zerobased (bool): If ijrange basis is zero or one.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
offset = 36
|
|
88
|
+
|
|
89
|
+
with _read_filelike(mfile.file, size=offset) as header:
|
|
90
|
+
# unpack header
|
|
91
|
+
swap, magic, nbyte, ncol, nrow, nlay = unpack("= i i i q q q", header)
|
|
92
|
+
|
|
93
|
+
if swap != 1 or magic not in (1351, 1352):
|
|
94
|
+
raise ValueError("Invalid file format (wrong swap id or magic number).")
|
|
95
|
+
|
|
96
|
+
if magic == 1351:
|
|
97
|
+
dtype: DTypeLike = np.float32 if nbyte == 4 else np.float64
|
|
98
|
+
else:
|
|
99
|
+
dtype = f"int{nbyte * 8}"
|
|
100
|
+
|
|
101
|
+
narr = ncol * nrow * nlay
|
|
102
|
+
|
|
103
|
+
ncolnew = nrownew = 0
|
|
104
|
+
|
|
105
|
+
if ijrange:
|
|
106
|
+
vals, ncolnew, nrownew = _import_xtgcpprop_partial(
|
|
107
|
+
mfile, nbyte, dtype, offset, ijrange, zerobased, ncol, nrow, nlay
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
vals = xsys.npfromfile(mfile.file, dtype=dtype, count=narr, offset=offset)
|
|
112
|
+
|
|
113
|
+
# read metadata which will be at position offet + nfloat*narr +13
|
|
114
|
+
with _read_filelike(
|
|
115
|
+
mfile.file,
|
|
116
|
+
seek=offset + nbyte * narr + 13,
|
|
117
|
+
) as _meta:
|
|
118
|
+
meta = json.loads(_meta, object_pairs_hook=dict)
|
|
119
|
+
|
|
120
|
+
req = meta["_required_"]
|
|
121
|
+
|
|
122
|
+
result = {att: req[att] for att in MetaDataCPProperty.REQUIRED}
|
|
123
|
+
|
|
124
|
+
if ijrange:
|
|
125
|
+
result["ncol"] = ncolnew
|
|
126
|
+
result["nrow"] = nrownew
|
|
127
|
+
|
|
128
|
+
result["values"] = np.ma.masked_equal(
|
|
129
|
+
vals.reshape((result["ncol"], result["nrow"], result["nlay"])),
|
|
130
|
+
UNDEF_INT if result["discrete"] else UNDEF,
|
|
131
|
+
)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _import_xtgcpprop_partial(
|
|
136
|
+
mfile: FileWrapper,
|
|
137
|
+
nbyte: int,
|
|
138
|
+
dtype: DTypeLike,
|
|
139
|
+
offset: int,
|
|
140
|
+
ijrange: Sequence[int],
|
|
141
|
+
zerobased: bool,
|
|
142
|
+
ncol: int,
|
|
143
|
+
nrow: int,
|
|
144
|
+
nlay: int,
|
|
145
|
+
) -> tuple[np.ndarray, int, int]:
|
|
146
|
+
"""Partial import of a property."""
|
|
147
|
+
i1, i2, j1, j2 = ijrange
|
|
148
|
+
if not zerobased:
|
|
149
|
+
i1 -= 1
|
|
150
|
+
i2 -= 1
|
|
151
|
+
j1 -= 1
|
|
152
|
+
j2 -= 1
|
|
153
|
+
|
|
154
|
+
ncolnew = i2 - i1 + 1
|
|
155
|
+
nrownew = j2 - j1 + 1
|
|
156
|
+
|
|
157
|
+
if ncolnew < 1 or ncolnew > ncol or nrownew < 1 or nrownew > nrow:
|
|
158
|
+
raise ValueError("The ijrange spesification is invalid.")
|
|
159
|
+
|
|
160
|
+
vals = np.zeros(ncolnew * nrownew * nlay, dtype=dtype)
|
|
161
|
+
|
|
162
|
+
for newnum, inum in enumerate(range(i1, i2 + 1)):
|
|
163
|
+
newpos = offset + (inum * nrow * nlay + j1 * nlay) * nbyte
|
|
164
|
+
ncount = nrownew * nlay
|
|
165
|
+
xvals = xsys.npfromfile(mfile.file, dtype=dtype, count=ncount, offset=newpos)
|
|
166
|
+
vals[newnum * ncount : newnum * ncount + ncount] = xvals
|
|
167
|
+
|
|
168
|
+
return vals, ncolnew, nrownew
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""GridProperty (not GridProperies) low level functions"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.ma as ma
|
|
9
|
+
|
|
10
|
+
from xtgeo import _cxtgeo
|
|
11
|
+
from xtgeo.common import null_logger
|
|
12
|
+
from xtgeo.common.constants import UNDEF, UNDEF_INT
|
|
13
|
+
|
|
14
|
+
logger = null_logger(__name__)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ctypes import Array as cArray
|
|
18
|
+
|
|
19
|
+
from numpy.typing import DTypeLike
|
|
20
|
+
|
|
21
|
+
from xtgeo.grid3d import Grid, GridProperty
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def f2c_order(obj: Grid | GridProperty, values1d: np.ndarray) -> np.ndarray:
|
|
25
|
+
"""Convert values1d from Fortran to C order, obj can be a Grid() or GridProperty()
|
|
26
|
+
instance
|
|
27
|
+
"""
|
|
28
|
+
val = np.reshape(values1d, (obj.ncol, obj.nrow, obj.nlay), order="F")
|
|
29
|
+
val = np.asanyarray(val, order="C")
|
|
30
|
+
return val.ravel()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def c2f_order(obj: Grid | GridProperty, values1d: np.ndarray) -> np.ndarray:
|
|
34
|
+
"""Convert values1d from C to F order, obj can be a Grid() or GridProperty()
|
|
35
|
+
instance
|
|
36
|
+
"""
|
|
37
|
+
val = np.reshape(values1d, (obj.ncol, obj.nrow, obj.nlay), order="C")
|
|
38
|
+
val = np.asanyarray(val, order="F")
|
|
39
|
+
return val.ravel(order="F")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def update_values_from_carray(
|
|
43
|
+
self: GridProperty,
|
|
44
|
+
carray: cArray,
|
|
45
|
+
dtype: DTypeLike,
|
|
46
|
+
delete: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Transfer values from SWIG 1D carray to numpy, 3D array"""
|
|
49
|
+
|
|
50
|
+
logger.debug("Update numpy from C array values")
|
|
51
|
+
|
|
52
|
+
nv = self.ntotal
|
|
53
|
+
|
|
54
|
+
self._isdiscrete = False
|
|
55
|
+
|
|
56
|
+
if dtype == np.float64:
|
|
57
|
+
logger.info("Entering conversion to numpy (float64) ...")
|
|
58
|
+
values1d = _cxtgeo.swig_carr_to_numpy_1d(nv, carray)
|
|
59
|
+
else:
|
|
60
|
+
logger.info("Entering conversion to numpy (int32) ...")
|
|
61
|
+
values1d = _cxtgeo.swig_carr_to_numpy_i1d(nv, carray)
|
|
62
|
+
self._isdiscrete = True
|
|
63
|
+
|
|
64
|
+
values = np.reshape(values1d, (self._ncol, self._nrow, self._nlay), order="F")
|
|
65
|
+
|
|
66
|
+
# make into C order as this is standard Python order...
|
|
67
|
+
values = np.asanyarray(values, order="C")
|
|
68
|
+
|
|
69
|
+
# make it float64 or whatever(?) and mask it
|
|
70
|
+
self.values = values # type: ignore
|
|
71
|
+
self.mask_undef()
|
|
72
|
+
|
|
73
|
+
# optionally delete the C array if needed
|
|
74
|
+
if delete:
|
|
75
|
+
delete_carray(self, carray)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def update_carray(
|
|
79
|
+
self: GridProperty,
|
|
80
|
+
undef: int | float | None = None,
|
|
81
|
+
discrete: bool | None = None,
|
|
82
|
+
dtype: DTypeLike = None,
|
|
83
|
+
order: Literal["C", "F", "A", "K"] = "F",
|
|
84
|
+
) -> cArray:
|
|
85
|
+
"""Copy (update) values from numpy to SWIG, 1D array, returns a pointer
|
|
86
|
+
to SWIG C array. If discrete is defined as True or False, force
|
|
87
|
+
the SWIG array to be of that kind.
|
|
88
|
+
|
|
89
|
+
Note that dtype will "override" current datatype if set. The resulting
|
|
90
|
+
carray will be in Fortran order, unless order is specified as 'C'
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
dstatus = self._isdiscrete
|
|
94
|
+
if discrete is not None:
|
|
95
|
+
dstatus = bool(discrete)
|
|
96
|
+
|
|
97
|
+
if undef is None:
|
|
98
|
+
undef = UNDEF
|
|
99
|
+
if dstatus:
|
|
100
|
+
undef = UNDEF_INT
|
|
101
|
+
|
|
102
|
+
logger.debug("Entering conversion from numpy to C array ...")
|
|
103
|
+
|
|
104
|
+
values = self.values.copy()
|
|
105
|
+
|
|
106
|
+
if not dtype:
|
|
107
|
+
values = values.astype(np.int32) if dstatus else values.astype(np.float64)
|
|
108
|
+
else:
|
|
109
|
+
values = values.astype(dtype)
|
|
110
|
+
|
|
111
|
+
values = ma.filled(values, undef)
|
|
112
|
+
values = np.asfortranarray(values)
|
|
113
|
+
|
|
114
|
+
if order == "F":
|
|
115
|
+
values = np.asfortranarray(values)
|
|
116
|
+
|
|
117
|
+
values1d = np.ravel(values, order=order)
|
|
118
|
+
|
|
119
|
+
if values1d.dtype == "float64" and dstatus and not dtype:
|
|
120
|
+
values1d = values1d.astype("int32")
|
|
121
|
+
logger.debug("Casting has been done")
|
|
122
|
+
|
|
123
|
+
if values1d.dtype == "float64":
|
|
124
|
+
logger.debug("Convert to carray (double)")
|
|
125
|
+
carray = _cxtgeo.new_doublearray(self.ntotal)
|
|
126
|
+
_cxtgeo.swig_numpy_to_carr_1d(values1d, carray)
|
|
127
|
+
elif values1d.dtype == "float32":
|
|
128
|
+
logger.debug("Convert to carray (float)")
|
|
129
|
+
carray = _cxtgeo.new_floatarray(self.ntotal)
|
|
130
|
+
_cxtgeo.swig_numpy_to_carr_f1d(values1d, carray)
|
|
131
|
+
elif values1d.dtype == "int32":
|
|
132
|
+
logger.debug("Convert to carray (int32)")
|
|
133
|
+
carray = _cxtgeo.new_intarray(self.ntotal)
|
|
134
|
+
_cxtgeo.swig_numpy_to_carr_i1d(values1d, carray)
|
|
135
|
+
else:
|
|
136
|
+
raise RuntimeError(f"Unsupported dtype, probable bug in {__name__}")
|
|
137
|
+
return carray
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def delete_carray(self: GridProperty, carray: cArray) -> None:
|
|
141
|
+
"""Delete carray SWIG C pointer, return carray as None"""
|
|
142
|
+
|
|
143
|
+
logger.debug("Enter delete carray values method for %d", id(self))
|
|
144
|
+
if carray is None:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if "int" in str(carray):
|
|
148
|
+
_cxtgeo.delete_intarray(carray)
|
|
149
|
+
return
|
|
150
|
+
if "float" in str(carray):
|
|
151
|
+
_cxtgeo.delete_floatarray(carray)
|
|
152
|
+
return
|
|
153
|
+
if "double" in str(carray):
|
|
154
|
+
_cxtgeo.delete_doublearray(carray)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
raise RuntimeError("BUG?")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def check_shape_ok(self: GridProperty, values: np.ndarray) -> bool:
|
|
161
|
+
"""Check if chape of values is OK"""
|
|
162
|
+
if values.shape == (self._ncol, self._nrow, self._nlay):
|
|
163
|
+
return True
|
|
164
|
+
logger.error(
|
|
165
|
+
"Wrong shape: Dimens of values %s %s %svs %s %s %s",
|
|
166
|
+
*values.shape,
|
|
167
|
+
self._ncol,
|
|
168
|
+
self._nrow,
|
|
169
|
+
self._nlay,
|
|
170
|
+
)
|
|
171
|
+
return False
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Various grid property operations"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
import xtgeo
|
|
10
|
+
from xtgeo import _cxtgeo
|
|
11
|
+
from xtgeo.common import XTGeoDialog, null_logger
|
|
12
|
+
from xtgeo.grid3d import _gridprop_lowlevel as gl
|
|
13
|
+
|
|
14
|
+
xtg = XTGeoDialog()
|
|
15
|
+
logger = null_logger(__name__)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from xtgeo.grid3d import Grid, GridProperty
|
|
19
|
+
from xtgeo.xyz import Polygons
|
|
20
|
+
|
|
21
|
+
_CoordOrValue = Union[float, int]
|
|
22
|
+
_XYCoordinate = Tuple[_CoordOrValue, _CoordOrValue]
|
|
23
|
+
_XYCoordList = List[List[List[_XYCoordinate]]]
|
|
24
|
+
_ValueList = List[List[_CoordOrValue]]
|
|
25
|
+
XYValueLists = Tuple[_XYCoordList, _ValueList]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_cast_range(values: np.ndarray, target_dtype: np.dtype) -> None:
|
|
29
|
+
"""Validate that values fit in target dtype range."""
|
|
30
|
+
if np.issubdtype(target_dtype, np.integer):
|
|
31
|
+
info = np.iinfo(target_dtype)
|
|
32
|
+
min_val, max_val = info.min, info.max
|
|
33
|
+
|
|
34
|
+
# Check actual value ranges
|
|
35
|
+
actual_min = np.nanmin(values)
|
|
36
|
+
actual_max = np.nanmax(values)
|
|
37
|
+
|
|
38
|
+
if actual_min < min_val or actual_max > max_val:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Values [{actual_min}, {actual_max}] exceed {target_dtype} "
|
|
41
|
+
f"range [{min_val}, {max_val}]"
|
|
42
|
+
)
|
|
43
|
+
elif np.issubdtype(target_dtype, np.floating):
|
|
44
|
+
info = np.finfo(target_dtype)
|
|
45
|
+
# Check for overflow/underflow
|
|
46
|
+
if np.any(np.abs(values[np.isfinite(values)]) > info.max):
|
|
47
|
+
raise ValueError(f"Values too large for {target_dtype}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def discrete_to_continuous(self: GridProperty) -> None:
|
|
51
|
+
"""Convert from discrete to continuous values."""
|
|
52
|
+
if not self.isdiscrete:
|
|
53
|
+
logger.debug("No need to convert, already continuous")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
logger.debug("Converting to continuous ...")
|
|
57
|
+
val = self._values.copy()
|
|
58
|
+
val = val.astype(np.float64)
|
|
59
|
+
self._values = val
|
|
60
|
+
self._isdiscrete = False
|
|
61
|
+
self._codes = {}
|
|
62
|
+
self.roxar_dtype = np.float32
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def continuous_to_discrete(self) -> None:
|
|
66
|
+
"""Convert from continuous to discrete values."""
|
|
67
|
+
if self.isdiscrete:
|
|
68
|
+
logger.debug("No need to convert, already discrete")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
logger.debug("Converting to discrete ...")
|
|
72
|
+
val = self._values.copy()
|
|
73
|
+
|
|
74
|
+
# Validate finite values only
|
|
75
|
+
finite_vals = (
|
|
76
|
+
val[np.isfinite(val)]
|
|
77
|
+
if not isinstance(val, np.ma.MaskedArray)
|
|
78
|
+
else val.compressed()
|
|
79
|
+
)
|
|
80
|
+
if len(finite_vals) > 0:
|
|
81
|
+
_validate_cast_range(finite_vals, np.int32)
|
|
82
|
+
|
|
83
|
+
# Round and cast, suppressing the NaN warning
|
|
84
|
+
val = np.round(val)
|
|
85
|
+
with np.errstate(invalid="ignore"):
|
|
86
|
+
val = val.astype(np.int32)
|
|
87
|
+
|
|
88
|
+
self._values = val
|
|
89
|
+
self._isdiscrete = True
|
|
90
|
+
|
|
91
|
+
self._codes = {
|
|
92
|
+
v: str(v)
|
|
93
|
+
for v in np.ma.unique(val)
|
|
94
|
+
if np.isfinite(v) and not isinstance(v, np.ma.core.MaskedConstant)
|
|
95
|
+
}
|
|
96
|
+
self.roxar_dtype = np.uint16
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_xy_value_lists(
|
|
100
|
+
self: GridProperty,
|
|
101
|
+
grid: Optional[Grid | GridProperty] = None,
|
|
102
|
+
mask: Optional[bool] = None,
|
|
103
|
+
) -> XYValueLists:
|
|
104
|
+
"""Get values for webportal format
|
|
105
|
+
|
|
106
|
+
Two cells:
|
|
107
|
+
[[[(x1,y1), (x2,y2), (x3,y3), (x4,y4)],
|
|
108
|
+
[(x5,y5), (x6,y6), (x7,y7), (x8,y8)]]]
|
|
109
|
+
|
|
110
|
+
If mask is True then inactive cells are ommited from the lists,
|
|
111
|
+
else the active cells corners will be present while the property
|
|
112
|
+
will have a -999 value.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
if grid is None:
|
|
116
|
+
raise RuntimeError("Missing grid object")
|
|
117
|
+
|
|
118
|
+
if not isinstance(grid, xtgeo.grid3d.Grid):
|
|
119
|
+
raise RuntimeError("The input grid is not a XTGeo Grid instance")
|
|
120
|
+
|
|
121
|
+
if not isinstance(self, xtgeo.grid3d.GridProperty):
|
|
122
|
+
raise RuntimeError("The property is not a XTGeo GridProperty instance")
|
|
123
|
+
|
|
124
|
+
clist = grid.get_xyz_corners()
|
|
125
|
+
actnum = grid.get_actnum()
|
|
126
|
+
|
|
127
|
+
# set value 0 if actnum is 0 to facilitate later operations
|
|
128
|
+
if mask:
|
|
129
|
+
for cli in clist:
|
|
130
|
+
cli.values[actnum.values == 0] = 0
|
|
131
|
+
|
|
132
|
+
# now some numpy operations (coffee?, any?)
|
|
133
|
+
xy0 = np.column_stack((clist[0].values1d, clist[1].values1d))
|
|
134
|
+
xy1 = np.column_stack((clist[3].values1d, clist[4].values1d))
|
|
135
|
+
xy2 = np.column_stack((clist[6].values1d, clist[7].values1d))
|
|
136
|
+
xy3 = np.column_stack((clist[9].values1d, clist[10].values1d))
|
|
137
|
+
|
|
138
|
+
xyc = np.column_stack((xy0, xy1, xy2, xy3))
|
|
139
|
+
xyc = xyc.reshape(grid.nlay, grid.ncol * grid.nrow, 4, 2)
|
|
140
|
+
|
|
141
|
+
coordlist = xyc.tolist()
|
|
142
|
+
|
|
143
|
+
# remove cells that are undefined ("marked" as coordinate [0, 0] if mask)
|
|
144
|
+
coordlist = [
|
|
145
|
+
[[tuple(xy) for xy in cell if xy[0] > 0] for cell in lay] for lay in coordlist
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
coordlist = [[cell for cell in lay if len(cell) > 1] for lay in coordlist]
|
|
149
|
+
|
|
150
|
+
pval = self.values1d.reshape((grid.nlay, grid.ncol * grid.nrow))
|
|
151
|
+
valuelist = pval.tolist(fill_value=-999.0)
|
|
152
|
+
if mask:
|
|
153
|
+
valuelist = [[val for val in lay if val != -999.0] for lay in valuelist]
|
|
154
|
+
|
|
155
|
+
return coordlist, valuelist
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def operation_polygons(
|
|
159
|
+
self: GridProperty,
|
|
160
|
+
poly: Polygons,
|
|
161
|
+
value: float | int,
|
|
162
|
+
opname: Literal["add", "sub", "mul", "div", "set"] = "add",
|
|
163
|
+
inside: bool = True,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""A generic function for doing operations restricted to inside
|
|
166
|
+
or outside polygon(s).
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
grid = self.geometry
|
|
170
|
+
grid._set_xtgformat1()
|
|
171
|
+
|
|
172
|
+
if not isinstance(poly, xtgeo.xyz.Polygons):
|
|
173
|
+
raise ValueError("The poly input is not a Polygons instance")
|
|
174
|
+
|
|
175
|
+
# make a copy of the array which is used a "filter" or "proxy"
|
|
176
|
+
# value will be 1 inside polygons, 0 outside. Undef cells are kept as is
|
|
177
|
+
dtype = self.dtype
|
|
178
|
+
|
|
179
|
+
proxy = self.copy()
|
|
180
|
+
proxy.discrete_to_continuous()
|
|
181
|
+
|
|
182
|
+
proxy.values *= 0.0
|
|
183
|
+
cvals = gl.update_carray(proxy)
|
|
184
|
+
|
|
185
|
+
idgroups = poly.get_dataframe(copy=False).groupby(poly.pname)
|
|
186
|
+
|
|
187
|
+
for id_, grp in idgroups:
|
|
188
|
+
xcor = grp[poly.xname].values
|
|
189
|
+
ycor = grp[poly.yname].values
|
|
190
|
+
|
|
191
|
+
ier = _cxtgeo.grd3d_setval_poly(
|
|
192
|
+
xcor,
|
|
193
|
+
ycor,
|
|
194
|
+
self.ncol,
|
|
195
|
+
self.nrow,
|
|
196
|
+
self.nlay,
|
|
197
|
+
grid._coordsv,
|
|
198
|
+
grid._zcornsv,
|
|
199
|
+
grid._actnumsv,
|
|
200
|
+
cvals,
|
|
201
|
+
1,
|
|
202
|
+
)
|
|
203
|
+
if ier == -9:
|
|
204
|
+
logger.warning(f"Polygon no {id_ + 1} is not closed")
|
|
205
|
+
|
|
206
|
+
gl.update_values_from_carray(proxy, cvals, np.float64, delete=True)
|
|
207
|
+
|
|
208
|
+
if opname == "add":
|
|
209
|
+
tmp = self.values.copy() + value
|
|
210
|
+
elif opname == "sub":
|
|
211
|
+
tmp = self.values.copy() - value
|
|
212
|
+
elif opname == "mul":
|
|
213
|
+
tmp = self.values.copy() * value
|
|
214
|
+
elif opname == "div":
|
|
215
|
+
# Dividing a map of zero is always a hazzle; try to obtain 0.0
|
|
216
|
+
# as result in these cases
|
|
217
|
+
if np.isclose(value, 0.0):
|
|
218
|
+
xtg.warn(
|
|
219
|
+
"Dividing a surface with value or surface with zero "
|
|
220
|
+
"elements; may get unexpected results, try to "
|
|
221
|
+
"achieve zero values as result!"
|
|
222
|
+
)
|
|
223
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
224
|
+
this = np.ma.filled(self.values, fill_value=1.0)
|
|
225
|
+
that = np.ma.filled(value, fill_value=1.0)
|
|
226
|
+
mask = np.ma.getmaskarray(self.values)
|
|
227
|
+
tmp = np.true_divide(this, that)
|
|
228
|
+
tmp = np.where(np.isinf(tmp), 0, tmp)
|
|
229
|
+
tmp = np.nan_to_num(tmp)
|
|
230
|
+
tmp = np.ma.array(tmp, mask=mask)
|
|
231
|
+
|
|
232
|
+
elif opname == "set":
|
|
233
|
+
tmp = self.values.copy() * 0 + value
|
|
234
|
+
|
|
235
|
+
# convert tmp back to correct dtype
|
|
236
|
+
tmp = tmp.astype(dtype)
|
|
237
|
+
|
|
238
|
+
mask = proxy.values == 1 if inside else proxy.values == 0
|
|
239
|
+
|
|
240
|
+
self.values[mask] = tmp[mask]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def refine(
|
|
244
|
+
self: GridProperty,
|
|
245
|
+
refine_col: int | list[int],
|
|
246
|
+
refine_row: int | list[int],
|
|
247
|
+
refine_layer: int | list[int],
|
|
248
|
+
) -> None:
|
|
249
|
+
"""
|
|
250
|
+
Refines the grid property values along specified columns, rows, and layers.
|
|
251
|
+
This method increases the resolution of the grid property by repeating
|
|
252
|
+
the values along the specified axes. The number of repetitions for each
|
|
253
|
+
axis can be provided as either an integer or a list of integers.
|
|
254
|
+
Args:
|
|
255
|
+
refine_col (int | list[int]): Number of times to repeat values along the
|
|
256
|
+
column axis. Can be a single integer or a list of integers specifying
|
|
257
|
+
repetitions for each column.
|
|
258
|
+
refine_row (int | list[int]): Number of times to repeat values along the row
|
|
259
|
+
axis. Can be a single integer or a list of integers specifying
|
|
260
|
+
repetitions for each row.
|
|
261
|
+
refine_layer (int | list[int]): Number of times to repeat values along the
|
|
262
|
+
layer axis. Can be a single integer or a list of integers
|
|
263
|
+
specifying repetitions for each layer.
|
|
264
|
+
Returns:
|
|
265
|
+
None: The method updates the internal grid property values in place.
|
|
266
|
+
"""
|
|
267
|
+
mask = np.ma.getmaskarray(self._values)
|
|
268
|
+
values = self._values.data
|
|
269
|
+
for i, val in enumerate([refine_col, refine_row, refine_layer]):
|
|
270
|
+
values = np.repeat(values, val, axis=i)
|
|
271
|
+
mask = np.repeat(mask, val, axis=i)
|
|
272
|
+
self._values = np.ma.array(values, mask=mask)
|