xtgeo 4.8.0__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.

Potentially problematic release.


This version of xtgeo might be problematic. Click here for more details.

Files changed (117) hide show
  1. cxtgeo.py +582 -0
  2. cxtgeoPYTHON_wrap.c +20938 -0
  3. xtgeo/__init__.py +246 -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 +21 -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 +340 -0
  23. xtgeo/cube/cube1.py +1023 -0
  24. xtgeo/grid3d/__init__.py +15 -0
  25. xtgeo/grid3d/_ecl_grid.py +774 -0
  26. xtgeo/grid3d/_ecl_inte_head.py +148 -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 +266 -0
  32. xtgeo/grid3d/_grdecl_grid.py +388 -0
  33. xtgeo/grid3d/_grid3d.py +29 -0
  34. xtgeo/grid3d/_grid3d_fence.py +181 -0
  35. xtgeo/grid3d/_grid3d_utils.py +228 -0
  36. xtgeo/grid3d/_grid_boundary.py +76 -0
  37. xtgeo/grid3d/_grid_etc1.py +1566 -0
  38. xtgeo/grid3d/_grid_export.py +221 -0
  39. xtgeo/grid3d/_grid_hybrid.py +66 -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 +125 -0
  45. xtgeo/grid3d/_grid_roxapi.py +292 -0
  46. xtgeo/grid3d/_grid_wellzone.py +165 -0
  47. xtgeo/grid3d/_gridprop_export.py +178 -0
  48. xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
  49. xtgeo/grid3d/_gridprop_import_grdecl.py +130 -0
  50. xtgeo/grid3d/_gridprop_import_roff.py +52 -0
  51. xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
  52. xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
  53. xtgeo/grid3d/_gridprop_op1.py +174 -0
  54. xtgeo/grid3d/_gridprop_roxapi.py +239 -0
  55. xtgeo/grid3d/_gridprop_value_init.py +140 -0
  56. xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
  57. xtgeo/grid3d/_gridprops_import_roff.py +83 -0
  58. xtgeo/grid3d/_roff_grid.py +469 -0
  59. xtgeo/grid3d/_roff_parameter.py +303 -0
  60. xtgeo/grid3d/grid.py +2537 -0
  61. xtgeo/grid3d/grid_properties.py +699 -0
  62. xtgeo/grid3d/grid_property.py +1341 -0
  63. xtgeo/grid3d/types.py +15 -0
  64. xtgeo/io/__init__.py +1 -0
  65. xtgeo/io/_file.py +592 -0
  66. xtgeo/metadata/__init__.py +17 -0
  67. xtgeo/metadata/metadata.py +431 -0
  68. xtgeo/roxutils/__init__.py +7 -0
  69. xtgeo/roxutils/_roxar_loader.py +54 -0
  70. xtgeo/roxutils/_roxutils_etc.py +122 -0
  71. xtgeo/roxutils/roxutils.py +207 -0
  72. xtgeo/surface/__init__.py +18 -0
  73. xtgeo/surface/_regsurf_boundary.py +26 -0
  74. xtgeo/surface/_regsurf_cube.py +210 -0
  75. xtgeo/surface/_regsurf_cube_window.py +391 -0
  76. xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
  77. xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
  78. xtgeo/surface/_regsurf_export.py +388 -0
  79. xtgeo/surface/_regsurf_grid3d.py +271 -0
  80. xtgeo/surface/_regsurf_gridding.py +347 -0
  81. xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
  82. xtgeo/surface/_regsurf_import.py +347 -0
  83. xtgeo/surface/_regsurf_lowlevel.py +122 -0
  84. xtgeo/surface/_regsurf_oper.py +631 -0
  85. xtgeo/surface/_regsurf_roxapi.py +241 -0
  86. xtgeo/surface/_regsurf_utils.py +81 -0
  87. xtgeo/surface/_surfs_import.py +43 -0
  88. xtgeo/surface/_zmap_parser.py +138 -0
  89. xtgeo/surface/regular_surface.py +2967 -0
  90. xtgeo/surface/surfaces.py +276 -0
  91. xtgeo/well/__init__.py +24 -0
  92. xtgeo/well/_blockedwell_roxapi.py +221 -0
  93. xtgeo/well/_blockedwells_roxapi.py +68 -0
  94. xtgeo/well/_well_aux.py +30 -0
  95. xtgeo/well/_well_io.py +327 -0
  96. xtgeo/well/_well_oper.py +574 -0
  97. xtgeo/well/_well_roxapi.py +304 -0
  98. xtgeo/well/_wellmarkers.py +486 -0
  99. xtgeo/well/_wells_utils.py +158 -0
  100. xtgeo/well/blocked_well.py +216 -0
  101. xtgeo/well/blocked_wells.py +122 -0
  102. xtgeo/well/well1.py +1514 -0
  103. xtgeo/well/wells.py +211 -0
  104. xtgeo/xyz/__init__.py +6 -0
  105. xtgeo/xyz/_polygons_oper.py +272 -0
  106. xtgeo/xyz/_xyz.py +741 -0
  107. xtgeo/xyz/_xyz_data.py +646 -0
  108. xtgeo/xyz/_xyz_io.py +490 -0
  109. xtgeo/xyz/_xyz_lowlevel.py +42 -0
  110. xtgeo/xyz/_xyz_oper.py +613 -0
  111. xtgeo/xyz/_xyz_roxapi.py +766 -0
  112. xtgeo/xyz/points.py +681 -0
  113. xtgeo/xyz/polygons.py +811 -0
  114. xtgeo-4.8.0.dist-info/METADATA +145 -0
  115. xtgeo-4.8.0.dist-info/RECORD +117 -0
  116. xtgeo-4.8.0.dist-info/WHEEL +5 -0
  117. xtgeo-4.8.0.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._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 = _internal.grid3d.Grid(self)
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,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._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
+ }
@@ -0,0 +1,178 @@
1
+ """GridProperty export functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import os
8
+ import struct
9
+ from contextlib import ExitStack
10
+ from typing import IO, TYPE_CHECKING, Any, Literal
11
+
12
+ import numpy as np
13
+ import resfo
14
+ import roffio
15
+
16
+ from xtgeo.common.exceptions import InvalidFileFormatError
17
+ from xtgeo.common.log import null_logger
18
+ from xtgeo.io._file import FileFormat, FileWrapper
19
+
20
+ from ._roff_parameter import RoffParameter
21
+
22
+ if TYPE_CHECKING:
23
+ from xtgeo.common.types import FileLike
24
+
25
+ from .grid_property import GridProperty
26
+
27
+ logger = null_logger(__name__)
28
+
29
+
30
+ def to_file(
31
+ gridprop: GridProperty,
32
+ pfile: FileLike,
33
+ fformat: Literal["roff", "roffasc", "grdecl", "bgrdecl", "xtgcpprop"] = "roff",
34
+ name: str | None = None,
35
+ append: bool = False,
36
+ dtype: type[np.float32] | type[np.float64] | type[np.int32] | None = None,
37
+ fmt: str | None = None,
38
+ ) -> None:
39
+ """Export the grid property to file."""
40
+ logger.debug("Export property to file %s as %s", pfile, fformat)
41
+
42
+ binary = fformat in (
43
+ FileFormat.ROFF_BINARY.value + FileFormat.BGRDECL.value + ["xtgcpprop"]
44
+ )
45
+
46
+ if not name:
47
+ name = gridprop.name or ""
48
+
49
+ xtg_file = FileWrapper(pfile, mode="rb")
50
+ xtg_file.check_folder(raiseerror=OSError)
51
+
52
+ if fformat in FileFormat.ROFF_BINARY.value + FileFormat.ROFF_ASCII.value:
53
+ _export_roff(gridprop, xtg_file.name, name, binary, append=append)
54
+ elif fformat in FileFormat.GRDECL.value + FileFormat.BGRDECL.value:
55
+ _export_grdecl(
56
+ gridprop,
57
+ xtg_file.name,
58
+ name,
59
+ dtype=dtype or np.int32 if gridprop.isdiscrete else np.float32,
60
+ append=append,
61
+ binary=binary,
62
+ fmt=fmt,
63
+ )
64
+ elif fformat == "xtgcpprop":
65
+ _export_xtgcpprop(gridprop, xtg_file.name)
66
+ else:
67
+ extensions = FileFormat.extensions_string(
68
+ [
69
+ FileFormat.ROFF_BINARY,
70
+ FileFormat.ROFF_ASCII,
71
+ FileFormat.GRDECL,
72
+ FileFormat.BGRDECL,
73
+ ]
74
+ )
75
+ raise InvalidFileFormatError(
76
+ f"File format {fformat} is invalid for type GridProperty. "
77
+ f"Supported formats are {extensions}."
78
+ )
79
+
80
+
81
+ def _export_roff(
82
+ gridprop: GridProperty,
83
+ pfile: str | io.BytesIO | io.StringIO,
84
+ name: str,
85
+ binary: bool,
86
+ append: bool = False,
87
+ ) -> None:
88
+ if append:
89
+ logger.warning(
90
+ "Append is not implemented for roff format, defaulting to write."
91
+ )
92
+ roff = RoffParameter.from_xtgeo_grid_property(gridprop)
93
+ roff.name = name
94
+ roff.to_file(pfile, roffio.Format.BINARY if binary else roffio.Format.ASCII)
95
+
96
+
97
+ def _export_grdecl(
98
+ gridprop: GridProperty,
99
+ pfile: str | io.BytesIO | io.StringIO,
100
+ name: str,
101
+ dtype: type[np.float32] | type[np.float64] | type[np.int32],
102
+ append: bool = False,
103
+ binary: bool = False,
104
+ fmt: str | None = None,
105
+ ) -> None:
106
+ """Export ascii or binary GRDECL"""
107
+ vals = gridprop.values.ravel(order="F")
108
+ if np.ma.isMaskedArray(vals):
109
+ undef_export = 1 if gridprop.isdiscrete or "int" in str(dtype) else 0.0
110
+ vals = np.ma.filled(vals, fill_value=undef_export)
111
+
112
+ mode = "a" if append else "w"
113
+ if binary:
114
+ mode += "b"
115
+
116
+ with ExitStack() as stack:
117
+ fout = (
118
+ stack.enter_context(open(pfile, mode)) if isinstance(pfile, str) else pfile
119
+ )
120
+
121
+ if append:
122
+ fout.seek(os.SEEK_END)
123
+
124
+ if binary:
125
+ resfo.write(
126
+ fout,
127
+ [(name.ljust(8), vals.astype(dtype))],
128
+ fileformat=resfo.Format.UNFORMATTED,
129
+ )
130
+ else:
131
+ # Always the case when not binary
132
+ assert isinstance(fout, io.TextIOWrapper)
133
+ fout.write(name)
134
+ fout.write("\n")
135
+ for i, v in enumerate(vals):
136
+ fout.write(" ")
137
+ if fmt:
138
+ fout.write(fmt % v)
139
+ elif gridprop.isdiscrete:
140
+ fout.write(str(v))
141
+ else:
142
+ fout.write(f"{v:3e}")
143
+ if i % 6 == 5:
144
+ fout.write("\n")
145
+ fout.write(" /\n")
146
+
147
+
148
+ def _export_xtgcpprop(
149
+ gridprop: GridProperty, pfile: str | io.BytesIO | io.StringIO
150
+ ) -> None:
151
+ """Export to experimental xtgcpproperty format, python version."""
152
+ logger.debug("Export as xtgcpprop...")
153
+ gridprop._metadata.required = gridprop
154
+
155
+ magic = 1352 if gridprop.isdiscrete else 1351
156
+ prevalues = (1, magic, 4, gridprop.ncol, gridprop.nrow, gridprop.nlay)
157
+ mystruct = struct.Struct("= i i i q q q")
158
+ pre = mystruct.pack(*prevalues)
159
+
160
+ meta = gridprop.metadata.get_metadata()
161
+
162
+ # Convert StringIO to BytesIO as this is a binary format
163
+ with ExitStack() as stack:
164
+ if isinstance(pfile, io.StringIO):
165
+ data = pfile.getvalue().encode("utf-8")
166
+ fout: IO[Any] = io.BytesIO(data)
167
+ elif isinstance(pfile, io.BytesIO):
168
+ fout = pfile
169
+ else:
170
+ fout = stack.enter_context(open(pfile, "wb"))
171
+ fout.write(pre)
172
+ gridprop.get_npvalues1d(fill_value=gridprop.undef).astype(np.float32).tofile(
173
+ fout
174
+ )
175
+ fout.write("\nXTGMETA.v01\n".encode())
176
+ fout.write(json.dumps(meta).encode())
177
+
178
+ logger.debug("Export as xtgcpprop... done")