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,292 @@
|
|
|
1
|
+
"""Roxar API functions for XTGeo Grid Geometry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
import xtgeo._internal as _internal
|
|
12
|
+
from xtgeo.common import XTGeoDialog, null_logger
|
|
13
|
+
from xtgeo.roxutils._roxar_loader import roxar, roxar_grids
|
|
14
|
+
from xtgeo.roxutils.roxutils import RoxUtils
|
|
15
|
+
|
|
16
|
+
xtg = XTGeoDialog()
|
|
17
|
+
|
|
18
|
+
logger = null_logger(__name__)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from xtgeo.grid3d.grid import Grid
|
|
22
|
+
# from xtgeo.roxutils._roxar_loader import RoxarGrid3DType
|
|
23
|
+
|
|
24
|
+
if roxar is not None:
|
|
25
|
+
from roxar import grids as RoxarGridType
|
|
26
|
+
from roxar.grids import Grid3D as RoxarGrid3DType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# self is Grid() instance
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ======================================================================================
|
|
33
|
+
# Load/import
|
|
34
|
+
# ======================================================================================
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_grid_from_rms(
|
|
38
|
+
projectname: str, gname: str, realisation: int, info: bool
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Load a Grid via ROXAR API spec and convert to XTGeo internal storage.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
dictionary of parameters to be used in the Grid constructor function.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
rox = RoxUtils(projectname, readonly=True)
|
|
48
|
+
return _load_grid_from_rms_viaroxapi(rox, gname, realisation, info)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _display_roxapi_grid_info(roxgrid: RoxarGrid3DType) -> None:
|
|
52
|
+
"""Push info to screen (mostly for debugging), experimental."""
|
|
53
|
+
|
|
54
|
+
indexer = roxgrid.grid_indexer
|
|
55
|
+
ncol, nrow, _ = indexer.dimensions
|
|
56
|
+
|
|
57
|
+
xtg.say("ROXAPI with support for CornerPointGridGeometry")
|
|
58
|
+
geom = roxgrid.get_geometry()
|
|
59
|
+
defined_cells = geom.get_defined_cells()
|
|
60
|
+
xtg.say(f"Defined cells \n{defined_cells}")
|
|
61
|
+
|
|
62
|
+
xtg.say(f"IJK handedness: {geom.ijk_handedness}")
|
|
63
|
+
for ipi in range(ncol + 1):
|
|
64
|
+
for jpi in range(nrow + 1):
|
|
65
|
+
tpi, bpi, zco = geom.get_pillar_data(ipi, jpi)
|
|
66
|
+
xtg.say(f"For pillar {ipi}, {jpi}\n")
|
|
67
|
+
xtg.say(f"Tops\n{tpi}")
|
|
68
|
+
xtg.say(f"Bots\n{bpi}")
|
|
69
|
+
xtg.say(f"Depths\n{zco}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_grid_from_rms_viaroxapi(
|
|
73
|
+
rox: RoxUtils, gname: str, realisation: int, info: bool
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Import a Grid via ROXAR API spec."""
|
|
76
|
+
proj = rox.project
|
|
77
|
+
|
|
78
|
+
logger.info("Loading grid with realisation %s...", realisation)
|
|
79
|
+
try:
|
|
80
|
+
if gname not in proj.grid_models:
|
|
81
|
+
raise KeyError(f"No such gridmodel: {gname}")
|
|
82
|
+
|
|
83
|
+
logger.info("Get roxgrid...")
|
|
84
|
+
roxgrid = proj.grid_models[gname].get_grid(realisation=realisation)
|
|
85
|
+
|
|
86
|
+
if roxgrid.has_dual_index_system:
|
|
87
|
+
xtg.warnuser(
|
|
88
|
+
f"The roxar grid {gname} has dual index system.\n"
|
|
89
|
+
"XTGeo does not implement extraction of simbox grid\n"
|
|
90
|
+
"and only considers physical index."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if info:
|
|
94
|
+
_display_roxapi_grid_info(roxgrid)
|
|
95
|
+
|
|
96
|
+
result = _convert_to_xtgeo_grid(roxgrid, gname)
|
|
97
|
+
|
|
98
|
+
except KeyError as keyerror:
|
|
99
|
+
raise RuntimeError(keyerror)
|
|
100
|
+
|
|
101
|
+
if rox._roxexternal:
|
|
102
|
+
rox.safe_close()
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _convert_to_xtgeo_grid(roxgrid: RoxarGrid3DType, gname: str) -> dict[str, Any]:
|
|
108
|
+
"""Convert from roxar CornerPointGeometry to xtgeo, version 2 using _xtgformat=2."""
|
|
109
|
+
indexer = roxgrid.grid_indexer
|
|
110
|
+
|
|
111
|
+
ncol, nrow, nlay = indexer.dimensions
|
|
112
|
+
|
|
113
|
+
# update other attributes
|
|
114
|
+
result: dict[str, Any] = {}
|
|
115
|
+
|
|
116
|
+
nncol = ncol + 1
|
|
117
|
+
nnrow = nrow + 1
|
|
118
|
+
nnlay = nlay + 1
|
|
119
|
+
|
|
120
|
+
result["name"] = gname
|
|
121
|
+
|
|
122
|
+
geom = roxgrid.get_geometry()
|
|
123
|
+
|
|
124
|
+
coordsv = np.zeros((nncol, nnrow, 6), dtype=np.float64)
|
|
125
|
+
zcornsv = np.zeros((nncol, nnrow, nnlay, 4), dtype=np.float32)
|
|
126
|
+
actnumsv = np.zeros((ncol, nrow, nlay), dtype=np.int32)
|
|
127
|
+
for icol in range(nncol):
|
|
128
|
+
for jrow in range(nnrow):
|
|
129
|
+
topc, basc, zcorn = geom.get_pillar_data(icol, jrow)
|
|
130
|
+
coordsv[icol, jrow, 0:3] = topc
|
|
131
|
+
coordsv[icol, jrow, 3:6] = basc
|
|
132
|
+
zcorn = np.ma.filled(zcorn, fill_value=np.nan)
|
|
133
|
+
zcornsv[icol, jrow, :, :] = zcorn.T
|
|
134
|
+
|
|
135
|
+
median_zcornsv = np.nanmedian(zcornsv)
|
|
136
|
+
# replace nan with median of the zorner values
|
|
137
|
+
zcornsv[np.isnan(zcornsv)] = median_zcornsv
|
|
138
|
+
|
|
139
|
+
_internal.grid3d.process_edges_rmsapi(zcornsv)
|
|
140
|
+
|
|
141
|
+
result["coordsv"] = coordsv
|
|
142
|
+
result["zcornsv"] = zcornsv
|
|
143
|
+
|
|
144
|
+
actnumsv[geom.get_defined_cells()] = 1
|
|
145
|
+
result["actnumsv"] = actnumsv
|
|
146
|
+
|
|
147
|
+
# subgrids
|
|
148
|
+
if len(indexer.zonation) > 1:
|
|
149
|
+
logger.debug("Zonation length (N subzones) is %s", len(indexer.zonation))
|
|
150
|
+
subz = {}
|
|
151
|
+
for inum, zrange in indexer.zonation.items():
|
|
152
|
+
logger.debug("inum: %s, zrange: %s", inum, zrange)
|
|
153
|
+
zname = roxgrid.zone_names[inum]
|
|
154
|
+
logger.debug("zname is: %s", zname)
|
|
155
|
+
zra = [nn + 1 for ira in zrange for nn in ira] # nested lists
|
|
156
|
+
subz[zname] = zra
|
|
157
|
+
|
|
158
|
+
result["subgrids"] = subz
|
|
159
|
+
|
|
160
|
+
result["roxgrid"] = roxgrid
|
|
161
|
+
result["roxindexer"] = indexer
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ======================================================================================
|
|
167
|
+
# Save/export
|
|
168
|
+
# ======================================================================================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def save_grid_to_rms(
|
|
172
|
+
self: Grid,
|
|
173
|
+
projectname: str,
|
|
174
|
+
gname: str,
|
|
175
|
+
realisation: int,
|
|
176
|
+
info: bool = False,
|
|
177
|
+
method: str | Literal["cpg", "roff"] = "cpg",
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Save (i.e. store in RMS) via RMSAPI (former ROXAPI) spec.
|
|
180
|
+
|
|
181
|
+
Using method 'cpg' means that the CPG method is applied (from ROXAPI 1.3).
|
|
182
|
+
This is possible from version ROXAPI ver 1.3, where the CornerPointGeometry
|
|
183
|
+
class is defined.
|
|
184
|
+
|
|
185
|
+
An alternative is to use simple roff import (via some /tmp area),
|
|
186
|
+
can be used from version 1.2. The "roff" method is also better if the user
|
|
187
|
+
want to activate undefined cells as a part of the work flow.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
rox = RoxUtils(projectname, readonly=False)
|
|
191
|
+
|
|
192
|
+
if method == "cpg":
|
|
193
|
+
self._set_xtgformat2()
|
|
194
|
+
_save_grid_to_rms_cornerpoint(self, rox, gname, realisation, info)
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
_save_grid_to_rms_viaroff(self, rox, gname, realisation)
|
|
198
|
+
|
|
199
|
+
if rox._roxexternal:
|
|
200
|
+
rox.project.save()
|
|
201
|
+
|
|
202
|
+
rox.safe_close()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _save_grid_to_rms_cornerpoint(
|
|
206
|
+
self: Grid, rox: RoxUtils, gname: str, realisation: int, info: bool
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Convert xtgeo geometry to pillar spec in ROXAPI and store _xtgformat=2."""
|
|
209
|
+
|
|
210
|
+
grid_model = rox.project.grid_models.create(gname)
|
|
211
|
+
grid_model.set_empty(realisation)
|
|
212
|
+
grid = grid_model.get_grid(realisation)
|
|
213
|
+
|
|
214
|
+
roxar_grids_: RoxarGridType = roxar_grids # for mypy
|
|
215
|
+
geom = roxar_grids_.CornerPointGridGeometry.create(self.dimensions)
|
|
216
|
+
|
|
217
|
+
grid_cpp = self._get_grid_cpp()
|
|
218
|
+
tpi, bpi, zco, zma = grid_cpp.convert_xtgeo_to_rmsapi()
|
|
219
|
+
zco = np.ma.array(zco, mask=zma)
|
|
220
|
+
|
|
221
|
+
# NOTE (KEEP), it is a bit unclear if mask is needed. A possible simpilification is
|
|
222
|
+
# zco = self._zcornsv.astype(np.float64)
|
|
223
|
+
# zco = np.moveaxis(zco, 2, 3)
|
|
224
|
+
# tpi = self._coordsv[:, :, 0:3]
|
|
225
|
+
# bpi = self._coordsv[:, :, 3:6]
|
|
226
|
+
# This would be 10-20% faster, but the mask is kept for now.
|
|
227
|
+
|
|
228
|
+
for ipi in range(self.ncol + 1):
|
|
229
|
+
for jpi in range(self.nrow + 1):
|
|
230
|
+
geom.set_pillar_data(
|
|
231
|
+
ipi,
|
|
232
|
+
jpi,
|
|
233
|
+
top_point=tpi[ipi, jpi],
|
|
234
|
+
bottom_point=bpi[ipi, jpi],
|
|
235
|
+
depth_values=zco[ipi, jpi],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
geom.set_defined_cells(self._actnumsv.astype(bool))
|
|
239
|
+
grid.set_geometry(geom)
|
|
240
|
+
_set_subgrids(self, rox, grid)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _set_subgrids(self: Grid, rox: RoxUtils, grid: RoxarGrid3DType) -> None:
|
|
244
|
+
"""Export the subgrid index (zones) to Roxar API.
|
|
245
|
+
|
|
246
|
+
From roxar API:
|
|
247
|
+
set_zonation(zone_dict)
|
|
248
|
+
|
|
249
|
+
zone_dict A dictionary with start-layer (zero based) and name for each zone.
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
if not self.subgrids:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if rox.version_required("1.6"):
|
|
257
|
+
subs = self.subgrids
|
|
258
|
+
roxar_subs = {}
|
|
259
|
+
for name, zrange in subs.items():
|
|
260
|
+
roxar_subs[int(zrange[0] - 1)] = name
|
|
261
|
+
|
|
262
|
+
grid.set_zonation(roxar_subs)
|
|
263
|
+
|
|
264
|
+
else:
|
|
265
|
+
xtg.warnuser(
|
|
266
|
+
"Implementation of subgrids is lacking in Roxar API for this "
|
|
267
|
+
"RMS version. Will continue to store in RMS but without subgrid index."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _save_grid_to_rms_viaroff(
|
|
272
|
+
self: Grid, rox: RoxUtils, gname: str, realisation: int
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Save xtgeo geometry to internal RMS via i/o ROFF tricks."""
|
|
275
|
+
logger.info("Realisation is %s", realisation)
|
|
276
|
+
|
|
277
|
+
# make a temporary folder and work within the with.. block
|
|
278
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
279
|
+
logger.info("Made a tmp folder: %s", tmpdir)
|
|
280
|
+
|
|
281
|
+
fname = os.path.join(tmpdir, gname)
|
|
282
|
+
self.to_file(fname)
|
|
283
|
+
|
|
284
|
+
grd = rox.project.grid_models
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
del grd[gname]
|
|
288
|
+
logger.info("Overwriting existing grid in RMS")
|
|
289
|
+
except KeyError:
|
|
290
|
+
logger.info("Grid in RMS is new")
|
|
291
|
+
|
|
292
|
+
grd.load(fname)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Private module for translating coordiantes plus flipping and rotation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from xtgeo.grid3d import Grid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _rotate_grid3d(
|
|
15
|
+
new_grd: Grid, add_rotation: float, rotation_xy: tuple[float, float] | None = None
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Rotate the grid around the cell 1,1,1 corner, or some other coordinate."""
|
|
18
|
+
|
|
19
|
+
# Convert angle to radians
|
|
20
|
+
angle_rad = math.radians(add_rotation)
|
|
21
|
+
|
|
22
|
+
# Create rotation matrix coefficients
|
|
23
|
+
cos_theta = math.cos(angle_rad)
|
|
24
|
+
sin_theta = math.sin(angle_rad)
|
|
25
|
+
|
|
26
|
+
# extract
|
|
27
|
+
coord_array = new_grd._coordsv.copy()
|
|
28
|
+
rotated_coords = coord_array.copy()
|
|
29
|
+
|
|
30
|
+
if rotation_xy is None:
|
|
31
|
+
x0, y0, _ = new_grd._coordsv[0, 0, :3]
|
|
32
|
+
else:
|
|
33
|
+
x0, y0 = rotation_xy
|
|
34
|
+
|
|
35
|
+
x_coords = coord_array[:, :, [0, 3]].copy() # x1 and x2
|
|
36
|
+
y_coords = coord_array[:, :, [1, 4]].copy() # y1 and y2
|
|
37
|
+
|
|
38
|
+
# Translate to origin
|
|
39
|
+
x_translated = x_coords - x0
|
|
40
|
+
y_translated = y_coords - y0
|
|
41
|
+
|
|
42
|
+
# Rotate using rotation matrix
|
|
43
|
+
x_rotated = x_translated * cos_theta - y_translated * sin_theta
|
|
44
|
+
y_rotated = x_translated * sin_theta + y_translated * cos_theta
|
|
45
|
+
|
|
46
|
+
# Translate back and assign
|
|
47
|
+
rotated_coords[:, :, [0, 3]] = x_rotated + x0
|
|
48
|
+
rotated_coords[:, :, [1, 4]] = y_rotated + y0
|
|
49
|
+
|
|
50
|
+
# Z coordinates remain unchanged (indices 2 and 5)
|
|
51
|
+
new_grd._coordsv = rotated_coords.copy()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _flip_vertically(grid: Grid) -> None:
|
|
55
|
+
"""Flip the grid vertically."""
|
|
56
|
+
|
|
57
|
+
# find average depth of corners
|
|
58
|
+
avg_z = grid._zcornsv.mean()
|
|
59
|
+
|
|
60
|
+
grid._zcornsv = grid._zcornsv[:, :, ::-1, :]
|
|
61
|
+
grid._zcornsv *= -1
|
|
62
|
+
|
|
63
|
+
# find the new average and compute the difference for shifting
|
|
64
|
+
new_avg_z = grid._zcornsv.mean()
|
|
65
|
+
diff = avg_z - new_avg_z
|
|
66
|
+
grid._zcornsv += diff
|
|
67
|
+
|
|
68
|
+
grid._actnumsv = np.flip(grid._actnumsv, axis=2).copy()
|
|
69
|
+
|
|
70
|
+
# Handle properties if they exist
|
|
71
|
+
if grid._props and grid._props.props:
|
|
72
|
+
for prop in grid._props.props:
|
|
73
|
+
prop.values = np.flip(prop.values, axis=2).copy()
|
|
74
|
+
|
|
75
|
+
# When we flip the grid, the subgrid info must also be flipped
|
|
76
|
+
subgrids = grid.get_subgrids()
|
|
77
|
+
if subgrids:
|
|
78
|
+
reverted = dict(reversed(subgrids.items()))
|
|
79
|
+
grid.set_subgrids(reverted)
|
|
80
|
+
|
|
81
|
+
if grid._ijk_handedness == "left":
|
|
82
|
+
grid._ijk_handedness = "right"
|
|
83
|
+
else:
|
|
84
|
+
grid._ijk_handedness = "left"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _translate_geometry(grid: Grid, translate: tuple[float, float, float]) -> None:
|
|
88
|
+
grid._coordsv[:, :, 0] += translate[0]
|
|
89
|
+
grid._coordsv[:, :, 1] += translate[1]
|
|
90
|
+
grid._coordsv[:, :, 2] += translate[2]
|
|
91
|
+
grid._coordsv[:, :, 3] += translate[0]
|
|
92
|
+
grid._coordsv[:, :, 4] += translate[1]
|
|
93
|
+
grid._coordsv[:, :, 5] += translate[2]
|
|
94
|
+
|
|
95
|
+
grid._zcornsv += translate[2]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _transfer_to_target(grid: Grid, target: tuple[float, float, float]) -> None:
|
|
99
|
+
# get the coordinates for the active cells
|
|
100
|
+
x, y, z = grid.get_xyz(asmasked=True)
|
|
101
|
+
|
|
102
|
+
# set the grid center to the desired target coordinates
|
|
103
|
+
shift_x = target[0] - x.values.mean()
|
|
104
|
+
shift_y = target[1] - y.values.mean()
|
|
105
|
+
shift_z = target[2] - z.values.mean()
|
|
106
|
+
|
|
107
|
+
_translate_geometry(grid, (shift_x, shift_y, shift_z))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def translate_coordinates(
|
|
111
|
+
self: Grid,
|
|
112
|
+
translate: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
113
|
+
flip: tuple[int, int, int] = (1, 1, 1),
|
|
114
|
+
add_rotation: float = 0.0,
|
|
115
|
+
rotation_point: tuple[float, float] | None = None,
|
|
116
|
+
target_coordinates: tuple[float, float, float] | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Rotate, flip grid and translate grid coordinates.
|
|
119
|
+
|
|
120
|
+
This should be done in a sequence like this
|
|
121
|
+
1) Add rotation (with value ``add_rotation``) to rotate the grid counter-clockwise
|
|
122
|
+
2) Flip the grid
|
|
123
|
+
3a) translate the grid by adding (x, y, z), OR
|
|
124
|
+
3b) set a target location the grid centre.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
self._set_xtgformat2()
|
|
128
|
+
|
|
129
|
+
if abs(add_rotation) > 1e-10: # Skip rotation if angle is essentially zero
|
|
130
|
+
_rotate_grid3d(self, add_rotation, rotation_xy=rotation_point)
|
|
131
|
+
|
|
132
|
+
if flip[2] == -1:
|
|
133
|
+
_flip_vertically(self)
|
|
134
|
+
|
|
135
|
+
if flip[0] == -1:
|
|
136
|
+
self.reverse_column_axis()
|
|
137
|
+
|
|
138
|
+
if flip[1] == -1:
|
|
139
|
+
self.reverse_row_axis()
|
|
140
|
+
|
|
141
|
+
use_translate = False
|
|
142
|
+
if not all(abs(x) < 1e-10 for x in translate):
|
|
143
|
+
_translate_geometry(self, translate)
|
|
144
|
+
use_translate = True
|
|
145
|
+
|
|
146
|
+
if target_coordinates and use_translate:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"Using both key 'translate' and key 'target_coordinates' is not allowed. "
|
|
149
|
+
"Use either."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if target_coordinates:
|
|
153
|
+
# transfer the grid's geometrical centre to a given location
|
|
154
|
+
_transfer_to_target(self, target_coordinates)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Private module for grid vs well zonelog checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from xtgeo.common import XTGeoDialog, null_logger
|
|
10
|
+
from xtgeo.grid3d.grid_property import GridProperty
|
|
11
|
+
from xtgeo.well import Well
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from xtgeo.grid3d import Grid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
xtg = XTGeoDialog()
|
|
18
|
+
logger = null_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def report_zone_mismatch(
|
|
22
|
+
self: Grid,
|
|
23
|
+
well: Well | None = None,
|
|
24
|
+
zonelogname: str = "ZONELOG",
|
|
25
|
+
zoneprop: GridProperty | None = None,
|
|
26
|
+
zonelogrange: tuple[int, int] = (0, 9999),
|
|
27
|
+
zonelogshift: int = 0,
|
|
28
|
+
depthrange: tuple[int | float, int | float] | None = None,
|
|
29
|
+
perflogname: str | None = None,
|
|
30
|
+
perflogrange: tuple[int | float, int | float] = (1, 9999),
|
|
31
|
+
filterlogname: str | None = None,
|
|
32
|
+
filterlogrange: tuple[int | float, int | float] = (1e-32, 9999.0),
|
|
33
|
+
resultformat: Literal[1, 2] = 1,
|
|
34
|
+
) -> dict[str, float | int | Well] | tuple[float, int, int] | None: # pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
|
35
|
+
"""Reports well to zone mismatch; this works together with a Well object.
|
|
36
|
+
|
|
37
|
+
The idea is to sample the current zone property for the well in the grid as fast as
|
|
38
|
+
possible.
|
|
39
|
+
|
|
40
|
+
Then the sampled zonelog is compared with the actual zonelog, and the difference
|
|
41
|
+
is reported.
|
|
42
|
+
|
|
43
|
+
One can apply a perforation log as a mask, meaning that we filter zonelog
|
|
44
|
+
match in intervals with a perforation log only if requested.
|
|
45
|
+
|
|
46
|
+
This method was completely redesigned in version 2.8
|
|
47
|
+
"""
|
|
48
|
+
self._set_xtgformat1()
|
|
49
|
+
|
|
50
|
+
if not isinstance(well, Well):
|
|
51
|
+
raise ValueError("Input well is not a Well() instance")
|
|
52
|
+
|
|
53
|
+
if zoneprop is None or not isinstance(zoneprop, GridProperty):
|
|
54
|
+
raise ValueError("Input zoneprop is missing or not a GridProperty() instance")
|
|
55
|
+
|
|
56
|
+
if zonelogname not in well.get_dataframe(copy=False).columns:
|
|
57
|
+
logger.warning("Zonelog %s is missing for well %s", zonelogname, well.name)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if perflogname == "None" or perflogname is None: # "None" for backwards compat
|
|
61
|
+
logger.info("No perforation log as filter")
|
|
62
|
+
perflogname = None
|
|
63
|
+
|
|
64
|
+
# get the IJK along the well as logs; use a copy of the well instance
|
|
65
|
+
wll = well.copy()
|
|
66
|
+
wll_df = wll.get_dataframe()
|
|
67
|
+
wll_df[zonelogname] += zonelogshift
|
|
68
|
+
|
|
69
|
+
if depthrange:
|
|
70
|
+
d1, d2 = depthrange
|
|
71
|
+
wll_df = wll_df[(d1 < wll_df.Z_TVDSS) & (d2 > wll_df.Z_TVDSS)]
|
|
72
|
+
wll.set_dataframe(wll_df)
|
|
73
|
+
|
|
74
|
+
wll.get_gridproperties(zoneprop, self)
|
|
75
|
+
zonename = zoneprop.name if zoneprop.name is not None else "Zone"
|
|
76
|
+
zmodel = zonename + "_model"
|
|
77
|
+
|
|
78
|
+
# from here, work with the dataframe only
|
|
79
|
+
df = wll.get_dataframe()
|
|
80
|
+
|
|
81
|
+
# zonelogrange
|
|
82
|
+
z1, z2 = zonelogrange
|
|
83
|
+
zmin = zmax = 0
|
|
84
|
+
try:
|
|
85
|
+
zmin = int(df[zonelogname].min())
|
|
86
|
+
except ValueError as verr:
|
|
87
|
+
if "cannot convert" in str(verr):
|
|
88
|
+
msg = f"TVD range {depthrange} is possibly to narrow? ({str(verr)})"
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
try:
|
|
91
|
+
zmax = int(df[zonelogname].max())
|
|
92
|
+
except ValueError as verr:
|
|
93
|
+
if "cannot convert" in str(verr):
|
|
94
|
+
msg = f"TVD range {depthrange} is possibly to narrow? ({str(verr)})"
|
|
95
|
+
raise ValueError(msg)
|
|
96
|
+
|
|
97
|
+
skiprange = list(range(zmin, z1)) + list(range(z2 + 1, zmax + 1))
|
|
98
|
+
|
|
99
|
+
for zname in (zonelogname, zmodel):
|
|
100
|
+
if skiprange: # needed check; du to a bug in pandas version 0.21 .. 0.23
|
|
101
|
+
df[zname] = df[zname].replace(skiprange, -888)
|
|
102
|
+
df[zname] = df[zname].fillna(-999)
|
|
103
|
+
if perflogname:
|
|
104
|
+
if perflogname in df.columns:
|
|
105
|
+
df[perflogname] = df[perflogname].replace(np.nan, -1)
|
|
106
|
+
pfr1, pfr2 = perflogrange
|
|
107
|
+
df[zname] = np.where(df[perflogname] < pfr1, -899, df[zname])
|
|
108
|
+
df[zname] = np.where(df[perflogname] > pfr2, -899, df[zname])
|
|
109
|
+
else:
|
|
110
|
+
return None
|
|
111
|
+
if filterlogname:
|
|
112
|
+
if filterlogname in df.columns:
|
|
113
|
+
df[filterlogname] = df[filterlogname].replace(np.nan, -1)
|
|
114
|
+
ffr1, ffr2 = filterlogrange
|
|
115
|
+
df[zname] = np.where(df[filterlogname] < ffr1, -919, df[zname])
|
|
116
|
+
df[zname] = np.where(df[filterlogname] > ffr2, -919, df[zname])
|
|
117
|
+
else:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# now there are various variotions on how to count mismatch:
|
|
121
|
+
# dfuse 1: count matches when zonelogname is valid (exclude -888)
|
|
122
|
+
# dfuse 2: count matches when zonelogname OR zmodel are valid (exclude < -888
|
|
123
|
+
# or -999)
|
|
124
|
+
# The first one is the original approach
|
|
125
|
+
|
|
126
|
+
dfuse1 = df.copy(deep=True)
|
|
127
|
+
dfuse1 = dfuse1.loc[dfuse1[zonelogname] > -888]
|
|
128
|
+
|
|
129
|
+
dfuse1["zmatch1"] = np.where(dfuse1[zmodel] == dfuse1[zonelogname], 1, 0)
|
|
130
|
+
mcount1 = dfuse1["zmatch1"].sum()
|
|
131
|
+
tcount1 = dfuse1["zmatch1"].count()
|
|
132
|
+
if not np.isnan(mcount1):
|
|
133
|
+
mcount1 = int(mcount1)
|
|
134
|
+
if not np.isnan(tcount1):
|
|
135
|
+
tcount1 = int(tcount1)
|
|
136
|
+
|
|
137
|
+
res1 = dfuse1["zmatch1"].mean() * 100
|
|
138
|
+
|
|
139
|
+
if resultformat == 1:
|
|
140
|
+
return (res1, mcount1, tcount1)
|
|
141
|
+
|
|
142
|
+
dfuse2 = df.copy(deep=True)
|
|
143
|
+
dfuse2 = dfuse2.loc[(df[zmodel] > -888) | (df[zonelogname] > -888)]
|
|
144
|
+
dfuse2["zmatch2"] = np.where(dfuse2[zmodel] == dfuse2[zonelogname], 1, 0)
|
|
145
|
+
mcount2 = dfuse2["zmatch2"].sum()
|
|
146
|
+
tcount2 = dfuse2["zmatch2"].count()
|
|
147
|
+
if not np.isnan(mcount2):
|
|
148
|
+
mcount2 = int(mcount2)
|
|
149
|
+
if not np.isnan(tcount2):
|
|
150
|
+
tcount2 = int(tcount2)
|
|
151
|
+
|
|
152
|
+
res2 = dfuse2["zmatch2"].mean() * 100
|
|
153
|
+
|
|
154
|
+
# update Well() copy (segment only)
|
|
155
|
+
wll.set_dataframe(dfuse2)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"MATCH1": res1,
|
|
159
|
+
"MCOUNT1": mcount1,
|
|
160
|
+
"TCOUNT1": tcount1,
|
|
161
|
+
"MATCH2": res2,
|
|
162
|
+
"MCOUNT2": mcount2,
|
|
163
|
+
"TCOUNT2": tcount2,
|
|
164
|
+
"WELLINTV": wll,
|
|
165
|
+
}
|