subsurface-terra 2025.1.0rc15__py3-none-any.whl → 2025.1.0rc17__py3-none-any.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.
Files changed (82) hide show
  1. subsurface/__init__.py +31 -31
  2. subsurface/_version.py +34 -21
  3. subsurface/api/__init__.py +13 -13
  4. subsurface/api/interfaces/__init__.py +3 -3
  5. subsurface/api/interfaces/stream.py +136 -136
  6. subsurface/api/reader/read_wells.py +78 -78
  7. subsurface/core/geological_formats/boreholes/_combine_trajectories.py +117 -117
  8. subsurface/core/geological_formats/boreholes/_map_attrs_to_survey.py +236 -234
  9. subsurface/core/geological_formats/boreholes/_survey_to_unstruct.py +163 -163
  10. subsurface/core/geological_formats/boreholes/boreholes.py +140 -140
  11. subsurface/core/geological_formats/boreholes/collars.py +26 -26
  12. subsurface/core/geological_formats/boreholes/survey.py +86 -86
  13. subsurface/core/geological_formats/fault.py +47 -47
  14. subsurface/core/reader_helpers/reader_unstruct.py +11 -11
  15. subsurface/core/reader_helpers/readers_data.py +130 -130
  16. subsurface/core/reader_helpers/readers_wells.py +13 -13
  17. subsurface/core/structs/__init__.py +3 -3
  18. subsurface/core/structs/base_structures/__init__.py +2 -2
  19. subsurface/core/structs/base_structures/_aux.py +69 -0
  20. subsurface/core/structs/base_structures/_liquid_earth_mesh.py +121 -121
  21. subsurface/core/structs/base_structures/_unstructured_data_constructor.py +70 -70
  22. subsurface/core/structs/base_structures/base_structures_enum.py +6 -6
  23. subsurface/core/structs/base_structures/structured_data.py +282 -282
  24. subsurface/core/structs/base_structures/unstructured_data.py +338 -319
  25. subsurface/core/structs/structured_elements/octree_mesh.py +10 -10
  26. subsurface/core/structs/structured_elements/structured_grid.py +59 -59
  27. subsurface/core/structs/structured_elements/structured_mesh.py +9 -9
  28. subsurface/core/structs/unstructured_elements/__init__.py +3 -3
  29. subsurface/core/structs/unstructured_elements/line_set.py +72 -72
  30. subsurface/core/structs/unstructured_elements/point_set.py +43 -43
  31. subsurface/core/structs/unstructured_elements/tetrahedron_mesh.py +35 -35
  32. subsurface/core/structs/unstructured_elements/triangular_surface.py +62 -62
  33. subsurface/core/utils/utils_core.py +38 -38
  34. subsurface/modules/reader/__init__.py +13 -13
  35. subsurface/modules/reader/faults/faults.py +80 -80
  36. subsurface/modules/reader/from_binary.py +46 -46
  37. subsurface/modules/reader/mesh/_GOCAD_mesh.py +82 -82
  38. subsurface/modules/reader/mesh/_trimesh_reader.py +447 -447
  39. subsurface/modules/reader/mesh/csv_mesh_reader.py +53 -53
  40. subsurface/modules/reader/mesh/dxf_reader.py +177 -177
  41. subsurface/modules/reader/mesh/glb_reader.py +30 -30
  42. subsurface/modules/reader/mesh/mx_reader.py +232 -232
  43. subsurface/modules/reader/mesh/obj_reader.py +53 -53
  44. subsurface/modules/reader/mesh/omf_mesh_reader.py +43 -43
  45. subsurface/modules/reader/mesh/surface_reader.py +56 -56
  46. subsurface/modules/reader/mesh/surfaces_api.py +41 -41
  47. subsurface/modules/reader/profiles/__init__.py +3 -3
  48. subsurface/modules/reader/profiles/profiles_core.py +197 -197
  49. subsurface/modules/reader/read_netcdf.py +38 -38
  50. subsurface/modules/reader/topography/__init__.py +7 -7
  51. subsurface/modules/reader/topography/topo_core.py +100 -100
  52. subsurface/modules/reader/volume/read_grav3d.py +447 -428
  53. subsurface/modules/reader/volume/read_volume.py +327 -230
  54. subsurface/modules/reader/volume/segy_reader.py +105 -105
  55. subsurface/modules/reader/volume/seismic.py +173 -173
  56. subsurface/modules/reader/volume/volume_utils.py +43 -43
  57. subsurface/modules/reader/wells/DEP/__init__.py +43 -43
  58. subsurface/modules/reader/wells/DEP/_well_files_reader.py +167 -167
  59. subsurface/modules/reader/wells/DEP/_wells_api.py +61 -61
  60. subsurface/modules/reader/wells/DEP/_welly_reader.py +180 -180
  61. subsurface/modules/reader/wells/DEP/pandas_to_welly.py +212 -212
  62. subsurface/modules/reader/wells/_read_to_df.py +57 -57
  63. subsurface/modules/reader/wells/read_borehole_interface.py +148 -148
  64. subsurface/modules/reader/wells/wells_utils.py +68 -68
  65. subsurface/modules/tools/mocking_aux.py +104 -104
  66. subsurface/modules/visualization/__init__.py +2 -2
  67. subsurface/modules/visualization/to_pyvista.py +320 -320
  68. subsurface/modules/writer/to_binary.py +12 -12
  69. subsurface/modules/writer/to_rex/common.py +78 -78
  70. subsurface/modules/writer/to_rex/data_struct.py +74 -74
  71. subsurface/modules/writer/to_rex/gempy_to_rexfile.py +791 -791
  72. subsurface/modules/writer/to_rex/material_encoder.py +44 -44
  73. subsurface/modules/writer/to_rex/mesh_encoder.py +152 -152
  74. subsurface/modules/writer/to_rex/to_rex.py +115 -115
  75. subsurface/modules/writer/to_rex/utils.py +15 -15
  76. subsurface/optional_requirements.py +116 -116
  77. {subsurface_terra-2025.1.0rc15.dist-info → subsurface_terra-2025.1.0rc17.dist-info}/METADATA +194 -194
  78. subsurface_terra-2025.1.0rc17.dist-info/RECORD +99 -0
  79. {subsurface_terra-2025.1.0rc15.dist-info → subsurface_terra-2025.1.0rc17.dist-info}/WHEEL +1 -1
  80. {subsurface_terra-2025.1.0rc15.dist-info → subsurface_terra-2025.1.0rc17.dist-info}/licenses/LICENSE +203 -203
  81. subsurface_terra-2025.1.0rc15.dist-info/RECORD +0 -98
  82. {subsurface_terra-2025.1.0rc15.dist-info → subsurface_terra-2025.1.0rc17.dist-info}/top_level.txt +0 -0
@@ -1,230 +1,327 @@
1
- import os
2
- from io import BytesIO
3
- from typing import Union
4
-
5
- from subsurface.core.structs import StructuredData
6
-
7
- from .... import optional_requirements
8
- from ....core.structs import UnstructuredData
9
- from subsurface.core.reader_helpers.readers_data import GenericReaderFilesHelper
10
- import numpy as np
11
- import pandas as pd
12
-
13
-
14
- def read_VTK_structured_grid(file_or_buffer: Union[str, BytesIO], active_scalars: str) -> StructuredData:
15
- pv = optional_requirements.require_pyvista()
16
-
17
- if isinstance(file_or_buffer, BytesIO):
18
- # If file_or_buffer is a BytesIO, write it to a temporary file
19
- from tempfile import NamedTemporaryFile
20
- with NamedTemporaryFile('wb', suffix='.vtk', delete=False) as temp_file:
21
- # Write the BytesIO content to the temporary file
22
- getvalue: bytes = file_or_buffer.getvalue()
23
- temp_file.write(getvalue)
24
- temp_file.flush() # Make sure all data is written
25
- temp_file_name = temp_file.name # Store the temporary file name
26
- try:
27
- # Use pyvista.read() to read from the temporary file
28
- pyvista_obj = pv.read(temp_file_name)
29
- finally:
30
- # Ensure the temporary file is deleted after reading
31
- os.remove(temp_file_name)
32
- else:
33
- # If it's a file path, read directly
34
- pyvista_obj = pv.read(file_or_buffer)
35
- try:
36
- pyvista_struct: pv.ExplicitStructuredGrid = pv_cast_to_explicit_structured_grid(pyvista_obj)
37
- except Exception as e:
38
- raise f"The file is not a structured grid: {e}"
39
-
40
- if PLOT := False:
41
- pyvista_struct.set_active_scalars(active_scalars)
42
- pyvista_struct.plot()
43
-
44
- struct: StructuredData = StructuredData.from_pyvista_structured_grid(
45
- grid=pyvista_struct,
46
- data_array_name=active_scalars
47
- )
48
-
49
- return struct
50
-
51
-
52
- def read_volumetric_mesh_to_subsurface(reader_helper_coord: GenericReaderFilesHelper,
53
- reader_helper_attr: GenericReaderFilesHelper) -> UnstructuredData:
54
- df_coord = read_volumetric_mesh_coord_file(reader_helper_coord)
55
- if len(df_coord.columns) == 1:
56
- raise ValueError(
57
- "The attributes file has only one column, probably the columns are not being separated correctly. Use 'sep' in Additional Reader Arguments"
58
- )
59
-
60
- df_attr = read_volumetric_mesh_attr_file(reader_helper_attr)
61
- # Check if there are more than one column and if it is only one raise an error that probably the columns have not been properly separated. Use "sep" in Additional Reader Arguments
62
- if len(df_attr.columns) == 1:
63
- raise ValueError(
64
- "The attributes file has only one column, probably the columns are not being separated correctly. Use 'sep' in Additional Reader Arguments"
65
- )
66
-
67
- combined_df = df_coord.merge(df_attr, left_index=True, right_index=True)
68
- ud = UnstructuredData.from_array(
69
- vertex=combined_df[['x', 'y', 'z']], cells="points",
70
- attributes=combined_df[['pres', 'temp', 'sg', 'xco2']]
71
- )
72
- return ud
73
-
74
-
75
- def read_volumetric_mesh_coord_file(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
76
- df = pd.read_csv(
77
- filepath_or_buffer=reader_helper.file_or_buffer,
78
- **reader_helper.pandas_reader_kwargs
79
- )
80
- if reader_helper.columns_map is not None:
81
- df.rename(
82
- mapper=reader_helper.columns_map,
83
- axis="columns",
84
- inplace=True
85
- )
86
-
87
- df.dropna(axis=0, inplace=True)
88
-
89
- df.x = df.x.astype(float)
90
- df.y = df.y.astype(float)
91
- df.z = df.z.astype(float)
92
- # Throw error if empty
93
- if df.empty:
94
- raise ValueError("The file is empty")
95
-
96
- return df
97
-
98
-
99
- def read_volumetric_mesh_attr_file(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
100
- df = pd.read_table(reader_helper.file_or_buffer, **reader_helper.pandas_reader_kwargs)
101
- df.columns = df.columns.astype(str).str.strip()
102
- return df
103
-
104
-
105
- def pv_cast_to_explicit_structured_grid(pyvista_object: 'pv.DataSet') -> 'pv.ExplicitStructuredGrid':
106
- pv = optional_requirements.require_pyvista()
107
-
108
- match pyvista_object:
109
- case pv.RectilinearGrid() as rectl_grid:
110
- return __pv_convert_rectilinear_to_explicit(rectl_grid)
111
- case pv.UnstructuredGrid() as unstr_grid:
112
- return __pv_convert_unstructured_to_explicit(unstr_grid)
113
- case _:
114
- return pyvista_object.cast_to_explicit_structured_grid()
115
-
116
-
117
- def __pv_convert_unstructured_to_explicit(unstr_grid):
118
- """
119
- Convert a PyVista UnstructuredGrid to an ExplicitStructuredGrid if possible.
120
- """
121
- pv = optional_requirements.require_pyvista()
122
-
123
- # First check if the grid has the necessary attributes to be treated as structured
124
- if not hasattr(unstr_grid, 'n_cells') or unstr_grid.n_cells == 0:
125
- raise ValueError("The unstructured grid has no cells.")
126
-
127
- # Try to detect if the grid has a structured topology
128
- # Check if the grid has cell type 11 (VTK_VOXEL) or 12 (VTK_HEXAHEDRON)
129
- cell_types = unstr_grid.celltypes
130
-
131
- # Voxels (11) and hexahedra (12) are the cell types used in structured grids
132
- if not all(ct in [11, 12] for ct in cell_types):
133
- raise ValueError("The unstructured grid contains non-hexahedral cells and cannot be converted to explicit structured.")
134
-
135
- # Try to infer dimensions from the grid
136
- try:
137
- # Method 1: Try PyVista's built-in conversion if available
138
- return unstr_grid.cast_to_explicit_structured_grid()
139
- except (AttributeError, TypeError):
140
- pass
141
-
142
- try:
143
- # Method 2: If the grid has dimensions stored as field data
144
- if "dimensions" in unstr_grid.field_data:
145
- dims = unstr_grid.field_data["dimensions"]
146
- if len(dims) == 3:
147
- nx, ny, nz = dims
148
- # Verify that dimensions match the number of cells
149
- if (nx-1)*(ny-1)*(nz-1) != unstr_grid.n_cells:
150
- raise ValueError("Stored dimensions do not match the number of cells.")
151
-
152
- # Extract points and reorder if needed
153
- points = unstr_grid.points.reshape((nx, ny, nz, 3))
154
-
155
- # Create explicit structured grid
156
- explicit_grid = pv.ExplicitStructuredGrid((nx, ny, nz), points.reshape((-1, 3)))
157
- explicit_grid.compute_connectivity()
158
-
159
- # Transfer data arrays
160
- for name, array in unstr_grid.cell_data.items():
161
- explicit_grid.cell_data[name] = array.copy()
162
- for name, array in unstr_grid.point_data.items():
163
- explicit_grid.point_data[name] = array.copy()
164
- for name, array in unstr_grid.field_data.items():
165
- if name != "dimensions": # Skip dimensions field
166
- explicit_grid.field_data[name] = array.copy()
167
-
168
- return explicit_grid
169
- except (ValueError, KeyError):
170
- pass
171
-
172
- # If none of the above methods work, use PyVista's extract_cells function
173
- # to reconstruct the structured grid if possible
174
- try:
175
- # This is a best-effort approach that tries multiple strategies
176
- return pv.core.filters.convert_unstructured_to_structured_grid(unstr_grid)
177
- except Exception as e:
178
- raise ValueError(f"Failed to convert unstructured grid to explicit structured grid: {e}")
179
-
180
- def __pv_convert_rectilinear_to_explicit(rectl_grid):
181
-
182
- pv = optional_requirements.require_pyvista()
183
-
184
- # Extract the coordinate arrays from the input RectilinearGrid.
185
- x = np.asarray(rectl_grid.x)
186
- y = np.asarray(rectl_grid.y)
187
- z = np.asarray(rectl_grid.z)
188
-
189
- # Helper function: "double" the coordinates to produce an expanded set
190
- # that, when processed internally via np.unique, returns the original nodal values.
191
- def doubled_coords(arr):
192
- return np.repeat(arr, 2)[1:-1]
193
-
194
- # Double the coordinate arrays.
195
- xcorn = doubled_coords(x)
196
- ycorn = doubled_coords(y)
197
- zcorn = doubled_coords(z)
198
-
199
- # Build a complete grid of corner points via meshgrid. Fortran ('F') order ensures
200
- # the connectivity ordering aligns with VTK's expectations.
201
- xx, yy, zz = np.meshgrid(xcorn, ycorn, zcorn, indexing='ij')
202
- corners = np.column_stack((xx.ravel(order='F'),
203
- yy.ravel(order='F'),
204
- zz.ravel(order='F')))
205
-
206
- # The dimensions to pass to the ExplicitStructuredGrid constructor should be
207
- # the counts of unique coordinates in each direction.
208
- dims = (len(np.unique(xcorn)),
209
- len(np.unique(ycorn)),
210
- len(np.unique(zcorn)))
211
-
212
- # Create the ExplicitStructuredGrid.
213
- explicit_grid = pv.ExplicitStructuredGrid(dims, corners)
214
- explicit_grid.compute_connectivity()
215
-
216
- # --- Copy associated data arrays ---
217
-
218
- # Transfer all cell data arrays.
219
- for name, array in rectl_grid.cell_data.items():
220
- explicit_grid.cell_data[name] = array.copy()
221
-
222
- # Transfer all point data arrays.
223
- for name, array in rectl_grid.point_data.items():
224
- explicit_grid.point_data[name] = array.copy()
225
-
226
- # (Optional) Transfer field data as well.
227
- for name, array in rectl_grid.field_data.items():
228
- explicit_grid.field_data[name] = array.copy()
229
-
230
- return explicit_grid
1
+ import os
2
+ from io import BytesIO
3
+ from typing import Union
4
+
5
+ from subsurface.core.structs import StructuredData
6
+
7
+ from .... import optional_requirements
8
+ from ....core.structs import UnstructuredData
9
+ from subsurface.core.reader_helpers.readers_data import GenericReaderFilesHelper
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+
14
+ def read_VTK_structured_grid(file_or_buffer: Union[str, BytesIO], active_scalars: str) -> StructuredData:
15
+ pv = optional_requirements.require_pyvista()
16
+
17
+ if isinstance(file_or_buffer, BytesIO):
18
+ # If file_or_buffer is a BytesIO, write it to a temporary file
19
+ from tempfile import NamedTemporaryFile
20
+ with NamedTemporaryFile('wb', suffix='.vtk', delete=False) as temp_file:
21
+ # Write the BytesIO content to the temporary file
22
+ getvalue: bytes = file_or_buffer.getvalue()
23
+ temp_file.write(getvalue)
24
+ temp_file.flush() # Make sure all data is written
25
+ temp_file_name = temp_file.name # Store the temporary file name
26
+ try:
27
+ # Use pyvista.read() to read from the temporary file
28
+ pyvista_obj = pv.read(temp_file_name)
29
+ finally:
30
+ # Ensure the temporary file is deleted after reading
31
+ os.remove(temp_file_name)
32
+ else:
33
+ # If it's a file path, read directly
34
+ pyvista_obj = pv.read(file_or_buffer)
35
+ try:
36
+ pyvista_struct: pv.ExplicitStructuredGrid = pv_cast_to_explicit_structured_grid(pyvista_obj)
37
+ except Exception as e:
38
+ raise f"The file is not a structured grid: {e}"
39
+
40
+ if PLOT := False:
41
+ pyvista_struct.set_active_scalars(active_scalars)
42
+ pyvista_struct.plot()
43
+
44
+ struct: StructuredData = StructuredData.from_pyvista_structured_grid(
45
+ grid=pyvista_struct,
46
+ data_array_name=active_scalars
47
+ )
48
+
49
+ return struct
50
+
51
+
52
+ def read_volumetric_mesh_to_subsurface(reader_helper_coord: GenericReaderFilesHelper,
53
+ reader_helper_attr: GenericReaderFilesHelper) -> UnstructuredData:
54
+ df_coord = read_volumetric_mesh_coord_file(reader_helper_coord)
55
+ if len(df_coord.columns) == 1:
56
+ raise ValueError(
57
+ "The attributes file has only one column, probably the columns are not being separated correctly. Use 'sep' in Additional Reader Arguments"
58
+ )
59
+
60
+ df_attr = read_volumetric_mesh_attr_file(reader_helper_attr)
61
+ # Check if there are more than one column and if it is only one raise an error that probably the columns have not been properly separated. Use "sep" in Additional Reader Arguments
62
+ if len(df_attr.columns) == 1:
63
+ raise ValueError(
64
+ "The attributes file has only one column, probably the columns are not being separated correctly. Use 'sep' in Additional Reader Arguments"
65
+ )
66
+
67
+ combined_df = df_coord.merge(df_attr, left_index=True, right_index=True)
68
+ ud = UnstructuredData.from_array(
69
+ vertex=combined_df[['x', 'y', 'z']], cells="points",
70
+ attributes=combined_df[['pres', 'temp', 'sg', 'xco2']]
71
+ )
72
+ return ud
73
+
74
+
75
+ def read_volumetric_mesh_coord_file(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
76
+ df = pd.read_csv(
77
+ filepath_or_buffer=reader_helper.file_or_buffer,
78
+ **reader_helper.pandas_reader_kwargs
79
+ )
80
+ if reader_helper.columns_map is not None:
81
+ df.rename(
82
+ mapper=reader_helper.columns_map,
83
+ axis="columns",
84
+ inplace=True
85
+ )
86
+
87
+ df.dropna(axis=0, inplace=True)
88
+
89
+ df.x = df.x.astype(float)
90
+ df.y = df.y.astype(float)
91
+ df.z = df.z.astype(float)
92
+ # Throw error if empty
93
+ if df.empty:
94
+ raise ValueError("The file is empty")
95
+
96
+ return df
97
+
98
+
99
+ def read_volumetric_mesh_attr_file(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
100
+ df = pd.read_table(reader_helper.file_or_buffer, **reader_helper.pandas_reader_kwargs)
101
+ df.columns = df.columns.astype(str).str.strip()
102
+ return df
103
+
104
+
105
+ def pv_cast_to_explicit_structured_grid(pyvista_object: 'pv.DataSet') -> 'pv.ExplicitStructuredGrid':
106
+ pv = optional_requirements.require_pyvista()
107
+
108
+ match pyvista_object:
109
+ case pv.RectilinearGrid() as rectl_grid:
110
+ return __pv_convert_rectilinear_to_explicit(rectl_grid)
111
+ case pv.UnstructuredGrid() as unstr_grid:
112
+ return __pv_convert_unstructured_to_explicit(unstr_grid)
113
+ case _:
114
+ return pyvista_object.cast_to_explicit_structured_grid()
115
+
116
+
117
+ def __pv_convert_unstructured_to_explicit(unstr_grid):
118
+ """
119
+ Convert a PyVista UnstructuredGrid to an ExplicitStructuredGrid if possible.
120
+ """
121
+ pv = optional_requirements.require_pyvista()
122
+
123
+ # First check if the grid has the necessary attributes to be treated as structured
124
+ if not hasattr(unstr_grid, 'n_cells') or unstr_grid.n_cells == 0:
125
+ raise ValueError("The unstructured grid has no cells.")
126
+
127
+ # Try to detect if the grid has a structured topology
128
+ # Check if the grid has cell type 11 (VTK_VOXEL) or 12 (VTK_HEXAHEDRON)
129
+ cell_types = unstr_grid.celltypes
130
+
131
+ # Voxels (11) and hexahedra (12) are the cell types used in structured grids
132
+ if not all(ct in [11, 12] for ct in cell_types):
133
+ raise ValueError("The unstructured grid contains non-hexahedral cells and cannot be converted to explicit structured.")
134
+
135
+ # Try to infer dimensions from the grid
136
+ try:
137
+ # Method 1: Try PyVista's built-in conversion if available
138
+ return unstr_grid.cast_to_explicit_structured_grid()
139
+ except (AttributeError, TypeError):
140
+ pass
141
+
142
+ try:
143
+ # Method 2: If the grid has dimensions stored as field data
144
+ if "dimensions" in unstr_grid.field_data:
145
+ dims = unstr_grid.field_data["dimensions"]
146
+ if len(dims) == 3:
147
+ nx, ny, nz = dims
148
+ # Verify that dimensions match the number of cells
149
+ if (nx-1)*(ny-1)*(nz-1) != unstr_grid.n_cells:
150
+ raise ValueError("Stored dimensions do not match the number of cells.")
151
+
152
+ # Extract points and reorder if needed
153
+ points = unstr_grid.points.reshape((nx, ny, nz, 3))
154
+
155
+ # Create explicit structured grid
156
+ explicit_grid = pv.ExplicitStructuredGrid((nx, ny, nz), points.reshape((-1, 3)))
157
+ explicit_grid.compute_connectivity()
158
+
159
+ # Transfer data arrays
160
+ for name, array in unstr_grid.cell_data.items():
161
+ explicit_grid.cell_data[name] = array.copy()
162
+ for name, array in unstr_grid.point_data.items():
163
+ explicit_grid.point_data[name] = array.copy()
164
+ for name, array in unstr_grid.field_data.items():
165
+ if name != "dimensions": # Skip dimensions field
166
+ explicit_grid.field_data[name] = array.copy()
167
+
168
+ return explicit_grid
169
+ except (ValueError, KeyError):
170
+ pass
171
+
172
+ # If none of the above methods work, use PyVista's extract_cells function
173
+ # to reconstruct the structured grid if possible
174
+ try:
175
+ # This is a best-effort approach that tries multiple strategies
176
+ return pv.core.filters.convert_unstructured_to_structured_grid(unstr_grid)
177
+ except Exception as e:
178
+ raise ValueError(f"Failed to convert unstructured grid to explicit structured grid: {e}")
179
+
180
+
181
+ def __pv_convert_rectilinear_to_explicit(rectl_grid, *, temp_dtype=None):
182
+ """
183
+ Convert a PyVista RectilinearGrid to an ExplicitStructuredGrid with low peak memory.
184
+
185
+ Behavior:
186
+ - Output points are in world coordinates, dtype matches rectl_grid.points.dtype.
187
+ - Data arrays are shallow-transferred (no deep copies).
188
+ - temp_dtype controls the large temporary `corners` buffer dtype.
189
+ * If temp_dtype is None (default), use float32 when the output dtype is wider (e.g., float64),
190
+ else use the output dtype. This reduces peak memory automatically.
191
+ * If temp_dtype < output dtype, coordinates are recentred for precision and origin is added back after.
192
+
193
+ Parameters
194
+ ----------
195
+ rectl_grid : pv.RectilinearGrid
196
+ temp_dtype : numpy dtype or None
197
+ Dtype for building the temporary `corners` array. Examples:
198
+ - None (default): auto -> float32 if output dtype is wider, else output dtype.
199
+ - np.float32: memory-friendly; auto recenters & restores origin.
200
+ - np.float64: highest precision (more memory).
201
+
202
+ Returns
203
+ -------
204
+ pv.ExplicitStructuredGrid
205
+ """
206
+ import numpy as np
207
+ pv = optional_requirements.require_pyvista()
208
+
209
+ # Output dtype follows source grid points (usually float64)
210
+ out_dtype = getattr(rectl_grid.points, "dtype", np.float64)
211
+
212
+ # Auto-pick temp dtype: prefer float32 when output is wider (e.g., float64)
213
+ if temp_dtype is None:
214
+ temp_dtype = np.float32 if (
215
+ np.dtype(out_dtype).kind == 'f' and np.dtype(out_dtype).itemsize > 4) else out_dtype
216
+
217
+ # Coordinate arrays
218
+ x = np.asarray(rectl_grid.x)
219
+ y = np.asarray(rectl_grid.y)
220
+ z = np.asarray(rectl_grid.z)
221
+
222
+ # Decide if we must recenter (when temp dtype is lower precision than output dtype)
223
+ def _is_lower_precision(src, dst):
224
+ s, d = np.dtype(src), np.dtype(dst)
225
+ if s.kind != 'f' or d.kind != 'f':
226
+ return s != d
227
+ return s.itemsize < d.itemsize
228
+
229
+ if _is_lower_precision(temp_dtype, out_dtype):
230
+ origin = np.array([x[0], y[0], z[0]], dtype=np.float64)
231
+ x_base, y_base, z_base = x - origin[0], y - origin[1], z - origin[2]
232
+ else:
233
+ origin = None
234
+ x_base, y_base, z_base = x, y, z
235
+
236
+ # Double coordinates (interior duplication expected by ExplicitStructuredGrid ctor)
237
+ def _doubled(arr):
238
+ # [a,b,c,d] -> [a, b,b, c,c, d]
239
+ return np.repeat(arr, 2)[1:-1]
240
+
241
+ xcorn = _doubled(x_base)
242
+ ycorn = _doubled(y_base)
243
+ zcorn = _doubled(z_base)
244
+
245
+ nx2, ny2, nz2 = len(xcorn), len(ycorn), len(zcorn)
246
+ slab = ny2 * nz2
247
+ N = nx2 * slab
248
+
249
+ # Build corners via slab/chunked fill (avoids N-sized intermediates)
250
+ yz = np.empty((slab, 2), dtype=temp_dtype)
251
+ yz[:, 0] = np.repeat(ycorn, nz2).astype(temp_dtype, copy=False) # Y pattern
252
+ yz[:, 1] = np.tile(zcorn, ny2).astype(temp_dtype, copy=False) # Z pattern
253
+
254
+ corners = np.empty((N, 3), dtype=temp_dtype)
255
+ for i, xv in enumerate(xcorn):
256
+ start = i * slab
257
+ end = start + slab
258
+ corners[start:end, 0] = xv
259
+ corners[start:end, 1:3] = yz
260
+
261
+ # Construct explicit grid
262
+ dims = (len(x), len(y), len(z))
263
+ explicit = pv.ExplicitStructuredGrid(dims, corners)
264
+ explicit.compute_connectivity()
265
+
266
+ # Always return world coordinates; add origin back and cast to out_dtype in one fused pass
267
+ if origin is not None:
268
+ new_pts = np.empty_like(explicit.points, dtype=out_dtype)
269
+ np.add(explicit.points, origin, out=new_pts, dtype=out_dtype)
270
+ explicit.points = new_pts
271
+ else:
272
+ if explicit.points.dtype != out_dtype:
273
+ explicit.points = explicit.points.astype(out_dtype, copy=False)
274
+
275
+ # Shallow-transfer all data arrays (no deep copies)
276
+ for name, arr in rectl_grid.cell_data.items():
277
+ explicit.cell_data[name] = arr
278
+ for name, arr in rectl_grid.point_data.items():
279
+ explicit.point_data[name] = arr
280
+ for name, arr in rectl_grid.field_data.items():
281
+ explicit.field_data[name] = arr
282
+
283
+ __validate_rectilinear_to_explicit_conversion(rectl_grid, explicit)
284
+
285
+ return explicit
286
+
287
+
288
+ def __validate_rectilinear_to_explicit_conversion(rectl_grid, explicit_grid, *, atol=1e-6, rtol=1e-8) -> None:
289
+ """
290
+ Validate core equivalence between a RectilinearGrid and its ExplicitStructuredGrid.
291
+ Raises ValueError on mismatch. Avoids large 3D uniques / big temporaries.
292
+ """
293
+ import numpy as np
294
+
295
+ # dims & counts
296
+ nx, ny, nz = map(int, rectl_grid.dimensions)
297
+ if tuple(map(int, explicit_grid.dimensions)) != (nx, ny, nz):
298
+ raise ValueError(f"Dimensions differ: explicit {tuple(explicit_grid.dimensions)} vs rect {tuple(rectl_grid.dimensions)}")
299
+
300
+ expected_cells = (nx - 1) * (ny - 1) * (nz - 1)
301
+ if explicit_grid.n_cells != expected_cells:
302
+ raise ValueError(f"Cell count mismatch: explicit {explicit_grid.n_cells} vs expected {expected_cells}")
303
+
304
+ expected_points = nx * ny * nz
305
+ if explicit_grid.n_points != expected_points:
306
+ raise ValueError(f"Point count mismatch: explicit {explicit_grid.n_points} vs expected {expected_points}")
307
+
308
+ # bounds
309
+ if not np.allclose(explicit_grid.bounds, rectl_grid.bounds, rtol=rtol, atol=atol):
310
+ raise ValueError(f"Bounds differ: explicit {explicit_grid.bounds} vs rect {rectl_grid.bounds}")
311
+
312
+ # axis coordinates (order-independent)
313
+ pts = explicit_grid.points # shape (M,3), M = nx*ny*nz (already deduped/sorted by unique)
314
+ x_exp = np.unique(pts[:, 0])
315
+ y_exp = np.unique(pts[:, 1])
316
+ z_exp = np.unique(pts[:, 2])
317
+
318
+ x_rect = np.asarray(rectl_grid.x)
319
+ y_rect = np.asarray(rectl_grid.y)
320
+ z_rect = np.asarray(rectl_grid.z)
321
+
322
+ if len(x_exp) != len(x_rect) or not np.allclose(x_exp, x_rect, rtol=rtol, atol=atol):
323
+ raise ValueError("X axis coordinates differ.")
324
+ if len(y_exp) != len(y_rect) or not np.allclose(y_exp, y_rect, rtol=rtol, atol=atol):
325
+ raise ValueError("Y axis coordinates differ.")
326
+ if len(z_exp) != len(z_rect) or not np.allclose(z_exp, z_rect, rtol=rtol, atol=atol):
327
+ raise ValueError("Z axis coordinates differ.")