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.

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.cpython-313-darwin.so +0 -0
  5. xtgeo/_internal.cpython-313-darwin.so +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 +6 -0
  117. 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