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,241 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""Roxar API functions for XTGeo RegularSurface."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from xtgeo.common.log import null_logger
|
|
10
|
+
from xtgeo.roxutils import RoxUtils
|
|
11
|
+
from xtgeo.roxutils._roxar_loader import roxar
|
|
12
|
+
|
|
13
|
+
logger = null_logger(__name__)
|
|
14
|
+
|
|
15
|
+
VALID_STYPES = ["horizons", "zones", "clipboard", "general2d_data", "trends"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _check_stypes_names_category(roxutils, stype, name, category):
|
|
19
|
+
"""General check of some input values."""
|
|
20
|
+
stype = stype.lower()
|
|
21
|
+
|
|
22
|
+
if stype not in VALID_STYPES:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Given stype {stype} is not supported, legal stypes are: {VALID_STYPES}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if not name:
|
|
28
|
+
raise ValueError("The name is missing or empty.")
|
|
29
|
+
|
|
30
|
+
if stype in ("horizons", "zones") and (name is None or not category):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"Need to spesify both name and category for horizons and zones"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if stype == "general2d_data" and not roxutils.version_required("1.6"):
|
|
36
|
+
raise NotImplementedError(
|
|
37
|
+
"API Support for general2d_data is missing in this RMS version"
|
|
38
|
+
f"(current API version is {roxutils.roxversion} - required is 1.6"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def import_horizon_roxapi(
|
|
43
|
+
project, name, category, stype, realisation
|
|
44
|
+
): # pragma: no cover
|
|
45
|
+
"""Import a Horizon surface via ROXAR API spec. to xtgeo."""
|
|
46
|
+
roxutils = RoxUtils(project, readonly=True)
|
|
47
|
+
|
|
48
|
+
_check_stypes_names_category(roxutils, stype, name, category)
|
|
49
|
+
|
|
50
|
+
proj = roxutils.project
|
|
51
|
+
args = _roxapi_import_surface(proj, name, category, stype, realisation)
|
|
52
|
+
|
|
53
|
+
roxutils.safe_close()
|
|
54
|
+
return args
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _roxapi_import_surface(
|
|
58
|
+
proj, name, category, stype, realisation
|
|
59
|
+
): # pragma: no cover
|
|
60
|
+
args = {}
|
|
61
|
+
args["name"] = name
|
|
62
|
+
|
|
63
|
+
if stype == "horizons":
|
|
64
|
+
if name not in proj.horizons:
|
|
65
|
+
raise ValueError(f"Name {name} is not within Horizons")
|
|
66
|
+
if category not in proj.horizons.representations:
|
|
67
|
+
raise ValueError(f"Category {category} is not within Horizons categories")
|
|
68
|
+
try:
|
|
69
|
+
rox = proj.horizons[name][category].get_grid(realisation)
|
|
70
|
+
args.update(_roxapi_horizon_to_xtgeo(rox))
|
|
71
|
+
except KeyError as kwe:
|
|
72
|
+
logger.error(kwe)
|
|
73
|
+
|
|
74
|
+
elif stype == "zones":
|
|
75
|
+
if name not in proj.zones:
|
|
76
|
+
raise ValueError(f"Name {name} is not within Zones")
|
|
77
|
+
if category not in proj.zones.representations:
|
|
78
|
+
raise ValueError(f"Category {category} is not within Zones categories")
|
|
79
|
+
try:
|
|
80
|
+
rox = proj.zones[name][category].get_grid(realisation)
|
|
81
|
+
args.update(_roxapi_horizon_to_xtgeo(rox))
|
|
82
|
+
except KeyError as kwe:
|
|
83
|
+
logger.error(kwe)
|
|
84
|
+
|
|
85
|
+
elif stype in ("clipboard", "general2d_data"):
|
|
86
|
+
styperef = getattr(proj, stype)
|
|
87
|
+
if category:
|
|
88
|
+
folders = category.split("|" if "|" in category else "/")
|
|
89
|
+
rox = styperef.folders[folders]
|
|
90
|
+
else:
|
|
91
|
+
rox = styperef
|
|
92
|
+
|
|
93
|
+
roxsurf = rox[name].get_grid(realisation)
|
|
94
|
+
args.update(_roxapi_horizon_to_xtgeo(roxsurf))
|
|
95
|
+
|
|
96
|
+
elif stype == "trends":
|
|
97
|
+
if name not in proj.trends.surfaces:
|
|
98
|
+
logger.info("Name %s is not present in trends", name)
|
|
99
|
+
raise ValueError(f"Name {name} is not within Trends")
|
|
100
|
+
rox = proj.trends.surfaces[name]
|
|
101
|
+
|
|
102
|
+
roxsurf = rox.get_grid(realisation)
|
|
103
|
+
args.update(_roxapi_horizon_to_xtgeo(roxsurf))
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Invalid stype given: {stype}") # should never reach here
|
|
107
|
+
return args
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _roxapi_horizon_to_xtgeo(rox): # pragma: no cover
|
|
111
|
+
"""Tranforming surfaces from ROXAPI to XTGeo object."""
|
|
112
|
+
# local function
|
|
113
|
+
args = {}
|
|
114
|
+
logger.info("Surface from roxapi to xtgeo...")
|
|
115
|
+
args["xori"], args["yori"] = rox.origin
|
|
116
|
+
args["ncol"], args["nrow"] = rox.dimensions
|
|
117
|
+
args["xinc"], args["yinc"] = rox.increment
|
|
118
|
+
args["rotation"] = rox.rotation
|
|
119
|
+
|
|
120
|
+
args["values"] = rox.get_values()
|
|
121
|
+
logger.info("Surface from roxapi to xtgeo... DONE")
|
|
122
|
+
return args
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def export_horizon_roxapi(
|
|
126
|
+
self, project, name, category, stype, realisation
|
|
127
|
+
): # pragma: no cover
|
|
128
|
+
"""Export (store) a Horizon surface to RMS via ROXAR API spec."""
|
|
129
|
+
roxutils = RoxUtils(project, readonly=False)
|
|
130
|
+
|
|
131
|
+
_check_stypes_names_category(roxutils, stype, name, category)
|
|
132
|
+
|
|
133
|
+
logger.info("Surface from xtgeo to roxapi...")
|
|
134
|
+
use_srf = self.copy() # avoid modifying the original instance
|
|
135
|
+
if self.yflip == -1:
|
|
136
|
+
# roxar API cannot handle negative increments
|
|
137
|
+
use_srf.swapaxes()
|
|
138
|
+
|
|
139
|
+
use_srf.values = use_srf.values.astype(np.float64)
|
|
140
|
+
|
|
141
|
+
# Note that the RMS api does NOT accepts NaNs or Infs, even if behind the mask(!),
|
|
142
|
+
# so we need to replace them with some other value
|
|
143
|
+
if np.isnan(use_srf.values.data).any() or np.isinf(use_srf.values.data).any():
|
|
144
|
+
logger.warning(
|
|
145
|
+
"NaNs or Infs detected in the surface, replacing fill_value "
|
|
146
|
+
"prior to RMS API usage."
|
|
147
|
+
)
|
|
148
|
+
applied_fill_value = np.finfo(np.float64).max # aka 1.7976931348623157e+308
|
|
149
|
+
all_values = np.ma.filled(use_srf.values, fill_value=applied_fill_value)
|
|
150
|
+
# replace nan in all_values with applied_fill_value
|
|
151
|
+
all_values = np.where(
|
|
152
|
+
np.isnan(all_values) | np.isinf(all_values), applied_fill_value, all_values
|
|
153
|
+
)
|
|
154
|
+
# now remask the array; NB! use _values to avoid the mask being reset
|
|
155
|
+
use_srf._values = np.ma.masked_equal(all_values, applied_fill_value)
|
|
156
|
+
|
|
157
|
+
_roxapi_export_surface(
|
|
158
|
+
use_srf, roxutils.project, name, category, stype, realisation
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if roxutils._roxexternal:
|
|
162
|
+
roxutils.project.save()
|
|
163
|
+
|
|
164
|
+
logger.info("Surface from xtgeo to roxapi... DONE")
|
|
165
|
+
roxutils.safe_close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _roxapi_export_surface(
|
|
169
|
+
self, proj, name, category, stype, realisation
|
|
170
|
+
): # pragma: no cover
|
|
171
|
+
if stype == "horizons":
|
|
172
|
+
if name not in proj.horizons:
|
|
173
|
+
raise ValueError(f"Name {name} is not within Horizons")
|
|
174
|
+
if category not in proj.horizons.representations:
|
|
175
|
+
raise ValueError(f"Category {category} is not within Horizons categories")
|
|
176
|
+
try:
|
|
177
|
+
roxroot = proj.horizons[name][category]
|
|
178
|
+
roxg = _xtgeo_to_roxapi_grid(self)
|
|
179
|
+
roxg.set_values(self.values)
|
|
180
|
+
roxroot.set_grid(roxg, realisation=realisation)
|
|
181
|
+
except KeyError as kwe:
|
|
182
|
+
logger.error(kwe)
|
|
183
|
+
|
|
184
|
+
elif stype == "zones":
|
|
185
|
+
if name not in proj.zones:
|
|
186
|
+
raise ValueError(f"Name {name} is not within Zones")
|
|
187
|
+
if category not in proj.zones.representations:
|
|
188
|
+
raise ValueError(f"Category {category} is not within Zones categories")
|
|
189
|
+
try:
|
|
190
|
+
roxroot = proj.zones[name][category]
|
|
191
|
+
roxg = _xtgeo_to_roxapi_grid(self)
|
|
192
|
+
roxg.set_values(self.values)
|
|
193
|
+
roxroot.set_grid(roxg)
|
|
194
|
+
except KeyError as kwe:
|
|
195
|
+
logger.error(kwe)
|
|
196
|
+
|
|
197
|
+
elif stype in ("clipboard", "general2d_data"):
|
|
198
|
+
folders = []
|
|
199
|
+
if category:
|
|
200
|
+
folders = category.split("|" if "|" in category else "/")
|
|
201
|
+
styperef = getattr(proj, stype)
|
|
202
|
+
if folders:
|
|
203
|
+
styperef.folders.create(folders)
|
|
204
|
+
|
|
205
|
+
roxroot = styperef.create_surface(name, folders)
|
|
206
|
+
roxg = _xtgeo_to_roxapi_grid(self)
|
|
207
|
+
roxg.set_values(self.values)
|
|
208
|
+
roxroot.set_grid(roxg)
|
|
209
|
+
|
|
210
|
+
elif stype == "trends":
|
|
211
|
+
if name not in proj.trends.surfaces:
|
|
212
|
+
logger.info("Name %s is not present in trends", name)
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Name {name} is not within Trends (it must exist in advance!)"
|
|
215
|
+
)
|
|
216
|
+
# here a workound; trends.surfaces are read-only in Roxar API, but is seems
|
|
217
|
+
# that load() in RMS is an (undocumented?) workaround...
|
|
218
|
+
|
|
219
|
+
roxsurf = proj.trends.surfaces[name]
|
|
220
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
221
|
+
logger.info("Made a tmp folder: %s", tmpdir)
|
|
222
|
+
self.to_file(os.path.join(tmpdir, "gxx.gri"), fformat="irap_binary")
|
|
223
|
+
|
|
224
|
+
roxsurf.load(os.path.join(tmpdir, "gxx.gri"), roxar.FileFormat.ROXAR_BINARY)
|
|
225
|
+
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError(f"Invalid stype given: {stype}") # should never reach here
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _xtgeo_to_roxapi_grid(self): # pragma: no cover
|
|
231
|
+
# Create a 2D grid
|
|
232
|
+
|
|
233
|
+
return roxar.RegularGrid2D.create(
|
|
234
|
+
x_origin=self.xori,
|
|
235
|
+
y_origin=self.yori,
|
|
236
|
+
i_inc=self.xinc,
|
|
237
|
+
j_inc=self.yinc,
|
|
238
|
+
ni=self.ncol,
|
|
239
|
+
nj=self.nrow,
|
|
240
|
+
rotation=self.rotation,
|
|
241
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""RegularSurface utilities"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from xtgeo.common.calc import _swap_axes
|
|
6
|
+
from xtgeo.common.constants import UNDEF
|
|
7
|
+
from xtgeo.common.log import null_logger
|
|
8
|
+
|
|
9
|
+
logger = null_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def swapaxes(self):
|
|
13
|
+
"""Swap the axes columns vs rows, keep origin. Will change yflip."""
|
|
14
|
+
self._rotation, self._yflip, swapped_values = _swap_axes(
|
|
15
|
+
self._rotation,
|
|
16
|
+
self._yflip,
|
|
17
|
+
values=self.values.filled(UNDEF),
|
|
18
|
+
)
|
|
19
|
+
self._ncol, self._nrow = self._nrow, self._ncol
|
|
20
|
+
self._xinc, self._yinc = self._yinc, self._xinc
|
|
21
|
+
self.values = swapped_values["values"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_lefthanded(self):
|
|
25
|
+
"""Will rearrange in case of right-handed system, ie. yflip = -1.
|
|
26
|
+
|
|
27
|
+
In contrasts to swapaxes(), this will change the origin.
|
|
28
|
+
"""
|
|
29
|
+
if self.yflip == -1 or self.yinc < 0:
|
|
30
|
+
self._xori, self._yori, _ = self.get_xy_value_from_ij(1, self.nrow)
|
|
31
|
+
self._yflip = 1
|
|
32
|
+
self._yinc = abs(self.yinc)
|
|
33
|
+
|
|
34
|
+
# the values shall be reversed along the last axis
|
|
35
|
+
self._values = self._values[:, ::-1]
|
|
36
|
+
self._xlines = self._xlines[::-1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_righthanded(self):
|
|
40
|
+
"""Will rearrange in case of left-handed system, ie. yflip = 1.
|
|
41
|
+
|
|
42
|
+
In contrasts to swapaxes(), this will change the origin.
|
|
43
|
+
"""
|
|
44
|
+
if self.yflip == 1 or self.yinc > 0:
|
|
45
|
+
self._xori, self._yori, _ = self.get_xy_value_from_ij(1, self.nrow)
|
|
46
|
+
self._yflip = -1
|
|
47
|
+
self._yinc = -1 * abs(self.yinc)
|
|
48
|
+
|
|
49
|
+
# the values shall be reversed along the last axis
|
|
50
|
+
self._values = self._values[:, ::-1]
|
|
51
|
+
self._xlines = self._xlines[::-1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def autocrop(self):
|
|
55
|
+
"""Crop surface by looking at undefined areas, update instance"""
|
|
56
|
+
|
|
57
|
+
minvalue = self.values.min()
|
|
58
|
+
|
|
59
|
+
if np.isnan(minvalue):
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
arrx, arry = np.ma.where(self.values >= minvalue)
|
|
63
|
+
|
|
64
|
+
imin = int(arrx.min())
|
|
65
|
+
imax = int(arrx.max())
|
|
66
|
+
|
|
67
|
+
jmin = int(arry.min())
|
|
68
|
+
jmax = int(arry.max())
|
|
69
|
+
|
|
70
|
+
xori, yori, _dummy = self.get_xy_value_from_ij(imin + 1, jmin + 1)
|
|
71
|
+
|
|
72
|
+
ncol = imax - imin + 1
|
|
73
|
+
nrow = jmax - jmin + 1
|
|
74
|
+
|
|
75
|
+
self._values = self.values[imin : imax + 1, jmin : jmax + 1]
|
|
76
|
+
self._ilines = self.ilines[imin : imax + 1]
|
|
77
|
+
self._xlines = self.xlines[jmin : jmax + 1]
|
|
78
|
+
self._ncol = ncol
|
|
79
|
+
self._nrow = nrow
|
|
80
|
+
self._xori = xori
|
|
81
|
+
self._yori = yori
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Import multiple surfaces"""
|
|
2
|
+
|
|
3
|
+
from xtgeo.common.log import null_logger
|
|
4
|
+
|
|
5
|
+
from .regular_surface import surface_from_grid3d
|
|
6
|
+
|
|
7
|
+
logger = null_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def from_grid3d(grid, subgrids, rfactor):
|
|
11
|
+
"""Get surfaces, subtype and order from 3D grid, including subgrids"""
|
|
12
|
+
|
|
13
|
+
logger.info("Extracting surface from 3D grid...")
|
|
14
|
+
|
|
15
|
+
# determine layers
|
|
16
|
+
layers = []
|
|
17
|
+
names = []
|
|
18
|
+
if subgrids and grid.subgrids is not None:
|
|
19
|
+
last = ""
|
|
20
|
+
for sgrd, srange in grid.subgrids.items():
|
|
21
|
+
layers.append(str(srange[0]) + "_top")
|
|
22
|
+
names.append(sgrd + "_top")
|
|
23
|
+
last = str(srange[-1])
|
|
24
|
+
lastname = sgrd
|
|
25
|
+
# base of last layer
|
|
26
|
+
layers.append(last + "_base")
|
|
27
|
+
names.append(lastname + "_base")
|
|
28
|
+
else:
|
|
29
|
+
layers.append("top")
|
|
30
|
+
names.append("top")
|
|
31
|
+
layers.append("base")
|
|
32
|
+
names.append("base")
|
|
33
|
+
|
|
34
|
+
# next extract these layers
|
|
35
|
+
layerstack = []
|
|
36
|
+
for inum, lay in enumerate(layers):
|
|
37
|
+
layer = surface_from_grid3d(grid, template=None, where=lay, rfactor=rfactor)
|
|
38
|
+
layer.name = names[inum]
|
|
39
|
+
layerstack.append(layer)
|
|
40
|
+
|
|
41
|
+
logger.info("Extracting surface from 3D grid... DONE")
|
|
42
|
+
|
|
43
|
+
return layerstack, "tops", "stratigraphic"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""ZMAP plus parsing.
|
|
2
|
+
|
|
3
|
+
cf https://saurabhkukade.com/posts/2020/07/understanding-zmap-file-format/
|
|
4
|
+
|
|
5
|
+
Note also from example here:
|
|
6
|
+
https://raw.githubusercontent.com/abduhbm/zmapio/main/examples/NSLCU.dat
|
|
7
|
+
that header lines may end with trailing comma!
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import inspect
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass
|
|
20
|
+
class ZMAPSurface:
|
|
21
|
+
nrow: int
|
|
22
|
+
ncol: int
|
|
23
|
+
xmin: float
|
|
24
|
+
xmax: float
|
|
25
|
+
ymin: float
|
|
26
|
+
ymax: float
|
|
27
|
+
node_width: int
|
|
28
|
+
precision: int
|
|
29
|
+
start_column: int
|
|
30
|
+
nan_value: float
|
|
31
|
+
nr_nodes_per_line: int
|
|
32
|
+
values: Optional[np.array] = None
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
for field in dataclasses.fields(self):
|
|
36
|
+
value = getattr(self, field.name)
|
|
37
|
+
if field.type in (int, float) and not isinstance(value, field.type):
|
|
38
|
+
setattr(self, field.name, field.type(value))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def takes_stream(name, mode):
|
|
42
|
+
def decorator(func):
|
|
43
|
+
@wraps(func)
|
|
44
|
+
def wrapper(*args, **kwargs):
|
|
45
|
+
kwargs = inspect.getcallargs(func, *args, **kwargs)
|
|
46
|
+
if name in kwargs and isinstance(kwargs[name], (str, Path)):
|
|
47
|
+
with open(kwargs[name], mode) as f:
|
|
48
|
+
kwargs[name] = f
|
|
49
|
+
return func(**kwargs)
|
|
50
|
+
else:
|
|
51
|
+
return func(**kwargs)
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_header(zmap_data):
|
|
59
|
+
keys = {}
|
|
60
|
+
line_nr = 0
|
|
61
|
+
for line in zmap_data:
|
|
62
|
+
if is_comment(line):
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
line = [entry.strip() for entry in line.split(",")]
|
|
66
|
+
if not line[-1]:
|
|
67
|
+
line.pop() # deal with input lines ending with comma ','
|
|
68
|
+
if line_nr == 0:
|
|
69
|
+
_, identifier, keys["nr_nodes_per_line"] = line
|
|
70
|
+
if identifier != "GRID":
|
|
71
|
+
raise ZMAPFormatException(
|
|
72
|
+
f"Expected GRID as second entry in line, "
|
|
73
|
+
f"got: {identifier} in line: {line}"
|
|
74
|
+
)
|
|
75
|
+
elif line_nr == 1:
|
|
76
|
+
(
|
|
77
|
+
keys["node_width"],
|
|
78
|
+
dft_nan_value,
|
|
79
|
+
user_nan_value,
|
|
80
|
+
keys["precision"],
|
|
81
|
+
keys["start_column"],
|
|
82
|
+
) = line
|
|
83
|
+
keys["nan_value"] = dft_nan_value if dft_nan_value else user_nan_value
|
|
84
|
+
elif line_nr == 2:
|
|
85
|
+
(
|
|
86
|
+
keys["nrow"],
|
|
87
|
+
keys["ncol"],
|
|
88
|
+
keys["xmin"],
|
|
89
|
+
keys["xmax"],
|
|
90
|
+
keys["ymin"],
|
|
91
|
+
keys["ymax"],
|
|
92
|
+
) = line
|
|
93
|
+
elif line_nr == 3:
|
|
94
|
+
_, _, _ = line
|
|
95
|
+
elif line_nr >= 4 and line[0] != "@":
|
|
96
|
+
raise ZMAPFormatException(
|
|
97
|
+
f"Did not reach the values section, expected @, found: {line}"
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
return keys
|
|
101
|
+
except ValueError as err:
|
|
102
|
+
raise ZMAPFormatException(f"Failed to unpack line: {line}") from err
|
|
103
|
+
line_nr += 1
|
|
104
|
+
raise ZMAPFormatException("End reached without complete header")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_comment(line):
|
|
108
|
+
return line.startswith(("!", "+"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_values(zmap_data, nan_value):
|
|
112
|
+
"""Parse actual values in zmap plus ascii files.
|
|
113
|
+
|
|
114
|
+
Note that header's node_width and nr_nodes_per_line in ZMAP header are not applied,
|
|
115
|
+
meaning that values import here is more tolerant than original zmap spec.
|
|
116
|
+
"""
|
|
117
|
+
values = []
|
|
118
|
+
for line in zmap_data:
|
|
119
|
+
if not is_comment(line):
|
|
120
|
+
values += line.split()
|
|
121
|
+
|
|
122
|
+
return np.ma.masked_equal(
|
|
123
|
+
np.array(values, dtype=np.float32),
|
|
124
|
+
nan_value,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@takes_stream("zmap_file", "r")
|
|
129
|
+
def parse_zmap(zmap_file, load_values=True):
|
|
130
|
+
header = parse_header(zmap_file)
|
|
131
|
+
zmap_data = ZMAPSurface(**header)
|
|
132
|
+
if load_values:
|
|
133
|
+
zmap_data.values = parse_values(zmap_file, zmap_data.nan_value)
|
|
134
|
+
return zmap_data
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ZMAPFormatException(Exception):
|
|
138
|
+
pass
|