subsurface-terra 2025.1.0rc14__py3-none-any.whl → 2025.1.0rc16__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 (81) 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 -0
  9. subsurface/core/geological_formats/boreholes/_survey_to_unstruct.py +163 -0
  10. subsurface/core/geological_formats/boreholes/boreholes.py +140 -116
  11. subsurface/core/geological_formats/boreholes/collars.py +26 -26
  12. subsurface/core/geological_formats/boreholes/survey.py +86 -380
  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/_liquid_earth_mesh.py +121 -121
  20. subsurface/core/structs/base_structures/_unstructured_data_constructor.py +70 -70
  21. subsurface/core/structs/base_structures/base_structures_enum.py +6 -6
  22. subsurface/core/structs/base_structures/structured_data.py +282 -282
  23. subsurface/core/structs/base_structures/unstructured_data.py +319 -319
  24. subsurface/core/structs/structured_elements/octree_mesh.py +10 -10
  25. subsurface/core/structs/structured_elements/structured_grid.py +59 -59
  26. subsurface/core/structs/structured_elements/structured_mesh.py +9 -9
  27. subsurface/core/structs/unstructured_elements/__init__.py +3 -3
  28. subsurface/core/structs/unstructured_elements/line_set.py +72 -72
  29. subsurface/core/structs/unstructured_elements/point_set.py +43 -43
  30. subsurface/core/structs/unstructured_elements/tetrahedron_mesh.py +35 -35
  31. subsurface/core/structs/unstructured_elements/triangular_surface.py +62 -62
  32. subsurface/core/utils/utils_core.py +38 -38
  33. subsurface/modules/reader/__init__.py +13 -13
  34. subsurface/modules/reader/faults/faults.py +80 -80
  35. subsurface/modules/reader/from_binary.py +46 -46
  36. subsurface/modules/reader/mesh/_GOCAD_mesh.py +82 -82
  37. subsurface/modules/reader/mesh/_trimesh_reader.py +447 -447
  38. subsurface/modules/reader/mesh/csv_mesh_reader.py +53 -53
  39. subsurface/modules/reader/mesh/dxf_reader.py +177 -177
  40. subsurface/modules/reader/mesh/glb_reader.py +30 -30
  41. subsurface/modules/reader/mesh/mx_reader.py +232 -232
  42. subsurface/modules/reader/mesh/obj_reader.py +53 -53
  43. subsurface/modules/reader/mesh/omf_mesh_reader.py +43 -43
  44. subsurface/modules/reader/mesh/surface_reader.py +56 -56
  45. subsurface/modules/reader/mesh/surfaces_api.py +41 -41
  46. subsurface/modules/reader/profiles/__init__.py +3 -3
  47. subsurface/modules/reader/profiles/profiles_core.py +197 -197
  48. subsurface/modules/reader/read_netcdf.py +38 -38
  49. subsurface/modules/reader/topography/__init__.py +7 -7
  50. subsurface/modules/reader/topography/topo_core.py +100 -100
  51. subsurface/modules/reader/volume/read_grav3d.py +478 -428
  52. subsurface/modules/reader/volume/read_volume.py +327 -230
  53. subsurface/modules/reader/volume/segy_reader.py +105 -105
  54. subsurface/modules/reader/volume/seismic.py +173 -173
  55. subsurface/modules/reader/volume/volume_utils.py +43 -43
  56. subsurface/modules/reader/wells/DEP/__init__.py +43 -43
  57. subsurface/modules/reader/wells/DEP/_well_files_reader.py +167 -167
  58. subsurface/modules/reader/wells/DEP/_wells_api.py +61 -61
  59. subsurface/modules/reader/wells/DEP/_welly_reader.py +180 -180
  60. subsurface/modules/reader/wells/DEP/pandas_to_welly.py +212 -212
  61. subsurface/modules/reader/wells/_read_to_df.py +57 -57
  62. subsurface/modules/reader/wells/read_borehole_interface.py +148 -148
  63. subsurface/modules/reader/wells/wells_utils.py +68 -68
  64. subsurface/modules/tools/mocking_aux.py +104 -104
  65. subsurface/modules/visualization/__init__.py +2 -2
  66. subsurface/modules/visualization/to_pyvista.py +320 -320
  67. subsurface/modules/writer/to_binary.py +12 -12
  68. subsurface/modules/writer/to_rex/common.py +78 -78
  69. subsurface/modules/writer/to_rex/data_struct.py +74 -74
  70. subsurface/modules/writer/to_rex/gempy_to_rexfile.py +791 -791
  71. subsurface/modules/writer/to_rex/material_encoder.py +44 -44
  72. subsurface/modules/writer/to_rex/mesh_encoder.py +152 -152
  73. subsurface/modules/writer/to_rex/to_rex.py +115 -115
  74. subsurface/modules/writer/to_rex/utils.py +15 -15
  75. subsurface/optional_requirements.py +116 -116
  76. {subsurface_terra-2025.1.0rc14.dist-info → subsurface_terra-2025.1.0rc16.dist-info}/METADATA +194 -194
  77. subsurface_terra-2025.1.0rc16.dist-info/RECORD +98 -0
  78. {subsurface_terra-2025.1.0rc14.dist-info → subsurface_terra-2025.1.0rc16.dist-info}/WHEEL +1 -1
  79. {subsurface_terra-2025.1.0rc14.dist-info → subsurface_terra-2025.1.0rc16.dist-info}/licenses/LICENSE +203 -203
  80. subsurface_terra-2025.1.0rc14.dist-info/RECORD +0 -96
  81. {subsurface_terra-2025.1.0rc14.dist-info → subsurface_terra-2025.1.0rc16.dist-info}/top_level.txt +0 -0
@@ -1,282 +1,282 @@
1
- import enum
2
- from dataclasses import dataclass
3
- from typing import Dict, List, Tuple, Union, Literal
4
-
5
- import numpy as np
6
- import xarray as xr
7
-
8
- from ....optional_requirements import require_pyvista
9
-
10
-
11
- class StructuredDataType(enum.Enum):
12
- REGULAR_AXIS_ALIGNED = 0 #: Regular axis aligned grid. Distance between consecutive points is constant
13
- REGULAR_AXIS_UNALIGNED = 1 #: Regular axis unaligned grid. Distance between consecutive points is constant
14
- IRREGULAR_AXIS_ALIGNED = 2 #: Irregular axis aligned grid. Distance between consecutive points is not constant
15
- IRREGULAR_AXIS_UNALIGNED = 3 #: Irregular axis unaligned grid. Distance between consecutive points is not constant
16
-
17
-
18
- @dataclass(frozen=False)
19
- class StructuredData:
20
- data: xr.Dataset
21
- _active_data_array_name: str = "data_array"
22
- type: StructuredDataType = StructuredDataType.REGULAR_AXIS_ALIGNED
23
- dtype: Literal["float32", "float64"] = "float32"
24
-
25
- """Primary structure definition for structured data
26
-
27
- Check out other constructors: `StructuredData.from_numpy`,
28
- `StructuredData.from_data_array` and `StructuredData.from_dict`
29
-
30
- Args:
31
- data (xr.Dataset): object containing
32
- structured data, i.e. data that can be stored in multidimensional
33
- numpy array. The preferred type to pass as data is directly a
34
- xr.Dataset to be sure all the attributes are set and named as the user
35
- wants.
36
- data_array_name (str): If data is a numpy array or xarray DataArray, data_name
37
- provides the name for the xarray data variable
38
-
39
- Attributes:
40
- data (xarray.Dataset)
41
- """
42
-
43
- @property
44
- def active_data_array_name(self):
45
- data_var_list = list(self.data.data_vars.keys())
46
- if self._active_data_array_name not in data_var_list:
47
- raise ValueError("data_array_name not found in data_vars: {}".format(data_var_list))
48
- return self._active_data_array_name
49
-
50
- @active_data_array_name.setter
51
- def active_data_array_name(self, data_array_name: str):
52
- self._active_data_array_name = data_array_name
53
-
54
- @classmethod
55
- def from_numpy(cls, array: np.ndarray, coords: dict = None, data_array_name: str = "data_array",
56
- dim_names: List[str] = None):
57
- if dim_names is None:
58
- dim_names = cls._default_dim_names(array.ndim)
59
- # if they are more than 3 we do not know the dimension name but it should valid:
60
-
61
- dataset: xr.Dataset = xr.Dataset(
62
- data_vars=
63
- {
64
- data_array_name: (dim_names, array)
65
- },
66
- coords=coords
67
- )
68
-
69
- return cls(dataset, data_array_name)
70
-
71
- @classmethod
72
- def from_data_array(cls, data_array: xr.DataArray, data_array_name: str = "data_array"):
73
- dataset: xr.Dataset = xr.Dataset(
74
- data_vars={
75
- data_array_name: data_array
76
- },
77
- coords=data_array.coords
78
- )
79
-
80
- return cls(dataset, data_array_name)
81
-
82
- @classmethod
83
- def from_dict(cls, data_dict: Dict[str, xr.DataArray], coords: Dict[str, str] = None, data_array_name: str = "data_array"):
84
- dataset: xr.Dataset = xr.Dataset(data_vars=data_dict, coords=coords)
85
- return cls(dataset, data_array_name)
86
-
87
- @classmethod
88
- def from_pyvista_structured_grid(
89
- cls,
90
- grid: Union["pyvista.ExplicitStructuredGrid", "pyvista.StructuredGrid"],
91
- data_array_name: str = "data_array"
92
- ):
93
- pyvista = require_pyvista()
94
- # Extract p
95
-
96
- # Extract cell data and point data (if any)
97
- data_vars = {}
98
-
99
- # TODO: I need to do something with the bounds
100
-
101
- dimensions = np.array(grid.dimensions) - 1
102
- default_dim_names = cls._default_dim_names(dimensions.shape[0])
103
-
104
- bounds: tuple = grid.bounds
105
- coords = {}
106
- for i, dim in enumerate(default_dim_names):
107
- coords[dim] = np.linspace(
108
- start=bounds[i * 2],
109
- stop=bounds[i * 2 + 1],
110
- num=dimensions[i],
111
- endpoint=False
112
- )
113
-
114
- for name in grid.cell_data:
115
- cell_attr_data: pyvista.pyvista_ndarray = grid[name]
116
- cell_attr_data_reshaped = cell_attr_data.reshape(dimensions, order='F')
117
-
118
- data_vars[name] = xr.DataArray(
119
- data=cell_attr_data_reshaped,
120
- dims=default_dim_names,
121
- name=name
122
- )
123
-
124
- dataset: xr.Dataset = xr.Dataset(
125
- data_vars=data_vars,
126
- coords=coords
127
- )
128
- return cls(dataset, data_array_name)
129
-
130
- @property
131
- def values(self) -> np.ndarray:
132
- return self.data[self.active_data_array_name].values
133
-
134
- _bounds: Tuple[float, float, float, float, float, float] = None
135
-
136
- @property
137
- def bounds(self):
138
- if self._bounds is not None:
139
- return self._bounds
140
-
141
- array_: xr.DataArray = self.data[self.active_data_array_name]
142
- bounds = self._get_bounds_from_coord(array_)
143
- return bounds
144
-
145
- @bounds.setter
146
- def bounds(self, bounds: Tuple[float, float, float, float, float, float]):
147
- """
148
- Set the bounds of the structured data. This is useful for defining the
149
- spatial extent of the data in a structured grid.
150
-
151
- Args:
152
- bounds (Tuple[float, float, float, float, float, float]): A tuple containing
153
- the minimum and maximum values for each dimension (xmin, xmax, ymin, ymax, zmin, zmax).
154
- """
155
- self._bounds = bounds
156
-
157
- @property
158
- def shape(self):
159
- return self.active_data_array.shape
160
-
161
- @property
162
- def active_data_array(self):
163
- return self.data[self.active_data_array_name]
164
-
165
- @staticmethod
166
- def _get_bounds_from_coord(xr_obj: xr.DataArray):
167
- bounds = {}
168
- for coord in xr_obj.coords:
169
- bounds[coord] = (xr_obj[coord].min().item(), xr_obj[coord].max().item())
170
- return bounds
171
-
172
- def default_data_array_to_binary_legacy(self, order: Literal["K", "A", "C", "F"] = 'F'):
173
- bytearray_le = self._to_bytearray(order=order)
174
- header = self._set_binary_header()
175
-
176
- return bytearray_le, header
177
-
178
- def to_binary(self, order: Literal["K", "A", "C", "F"] = 'F') -> bytes:
179
- """Converts the structured data to a binary file
180
-
181
- Notes:
182
- Only the active data array is converted to binary for now
183
- """
184
-
185
- body_ = self._to_bytearray(order)
186
- header = self._set_binary_header()
187
-
188
- import json
189
- header_json = json.dumps(header)
190
- header_json_bytes = header_json.encode('utf-8')
191
- header_json_length = len(header_json_bytes)
192
- header_json_length_bytes = header_json_length.to_bytes(4, byteorder='little')
193
- file = header_json_length_bytes + header_json_bytes + body_
194
- return file
195
-
196
- def _set_binary_header(self) -> Dict:
197
- data_array = self.active_data_array
198
-
199
- match self.type:
200
- case StructuredDataType.REGULAR_AXIS_ALIGNED:
201
- header = {
202
- "data_shape": self.shape,
203
- "bounds" : self.bounds,
204
- "transform" : None,
205
- "dtype" : self.dtype,
206
- "data_name" : self.active_data_array_name
207
- }
208
- case _:
209
- raise NotImplementedError(f"StructuredDataType {self.type} not implemented yet")
210
-
211
- return header
212
-
213
- def _to_bytearray(self, order: Literal["K", "A", "C", "F"]) -> bytes:
214
- data_array = self.active_data_array
215
-
216
- data = data_array.values.astype(self.dtype).tobytes(order)
217
- bytearray_le = data
218
- return bytearray_le
219
-
220
- @classmethod
221
- def _default_dim_names(cls, n_dims: int):
222
- if n_dims == 2:
223
- dim_names = ['x', 'y']
224
- elif n_dims == 3:
225
- dim_names = ['x', 'y', 'z']
226
- else:
227
- dim_names = ['dim' + str(i) for i in range(n_dims)]
228
- return dim_names
229
-
230
- def to_netcdf(self, path: str, **to_netcdf_kwargs):
231
- """
232
- Serializes the current StructuredData instance to a NetCDF file.
233
-
234
- Args:
235
- path (str): The path (including file name) where the NetCDF file will be saved.
236
- **to_netcdf_kwargs: Additional keyword arguments forwarded to xarray's `to_netcdf`.
237
- """
238
- # Copy the dataset (shallow copy of the data structure, no copying of the underlying arrays)
239
- ds = self.data.copy(deep=False)
240
-
241
- # Store relevant metadata as global attributes:
242
- ds.attrs["active_data_array_name"] = self._active_data_array_name
243
- ds.attrs["structured_data_type"] = self.type.name # e.g., "REGULAR_AXIS_ALIGNED"
244
- ds.attrs["dtype"] = self.dtype # e.g., "float32"
245
-
246
- # Use xarray's to_netcdf
247
- ds.to_netcdf(path, **to_netcdf_kwargs)
248
-
249
- @classmethod
250
- def from_netcdf(cls, path: str, **from_netcdf_kwargs):
251
- """
252
- Deserializes a NetCDF file into a StructuredData instance.
253
-
254
- Args:
255
- path (str): The path to the NetCDF file to read.
256
- **from_netcdf_kwargs: Additional keyword arguments forwarded to xarray's `open_dataset`.
257
-
258
- Returns:
259
- StructuredData: A new instance of StructuredData loaded from the file.
260
- """
261
- ds = xr.open_dataset(path, **from_netcdf_kwargs)
262
-
263
- # Retrieve what was stored in attrs (with defaults if missing)
264
- data_array_name = ds.attrs.get("active_data_array_name", "data_array")
265
- dtype_str: str = ds.attrs.get("dtype", "float32")
266
- if dtype_str not in ["float32", "float64"]:
267
- raise ValueError(f"Unsupported dtype: {dtype_str}")
268
-
269
- sdt_str = ds.attrs.get("structured_data_type", "REGULAR_AXIS_ALIGNED")
270
-
271
- # Convert strings back to your enum or any other type
272
- # (assuming StructuredDataType is an Enum where name matches sdt_str)
273
- if sdt_str not in StructuredDataType.__members__:
274
- raise ValueError(f"Unsupported structured_data_type: {sdt_str}")
275
- structured_data_type: StructuredDataType = StructuredDataType[sdt_str]
276
-
277
- return cls(
278
- data=ds,
279
- _active_data_array_name=data_array_name,
280
- type=structured_data_type,
281
- dtype=dtype_str
282
- )
1
+ import enum
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List, Tuple, Union, Literal
4
+
5
+ import numpy as np
6
+ import xarray as xr
7
+
8
+ from ....optional_requirements import require_pyvista
9
+
10
+
11
+ class StructuredDataType(enum.Enum):
12
+ REGULAR_AXIS_ALIGNED = 0 #: Regular axis aligned grid. Distance between consecutive points is constant
13
+ REGULAR_AXIS_UNALIGNED = 1 #: Regular axis unaligned grid. Distance between consecutive points is constant
14
+ IRREGULAR_AXIS_ALIGNED = 2 #: Irregular axis aligned grid. Distance between consecutive points is not constant
15
+ IRREGULAR_AXIS_UNALIGNED = 3 #: Irregular axis unaligned grid. Distance between consecutive points is not constant
16
+
17
+
18
+ @dataclass(frozen=False)
19
+ class StructuredData:
20
+ data: xr.Dataset
21
+ _active_data_array_name: str = "data_array"
22
+ type: StructuredDataType = StructuredDataType.REGULAR_AXIS_ALIGNED
23
+ dtype: Literal["float32", "float64"] = "float32"
24
+
25
+ """Primary structure definition for structured data
26
+
27
+ Check out other constructors: `StructuredData.from_numpy`,
28
+ `StructuredData.from_data_array` and `StructuredData.from_dict`
29
+
30
+ Args:
31
+ data (xr.Dataset): object containing
32
+ structured data, i.e. data that can be stored in multidimensional
33
+ numpy array. The preferred type to pass as data is directly a
34
+ xr.Dataset to be sure all the attributes are set and named as the user
35
+ wants.
36
+ data_array_name (str): If data is a numpy array or xarray DataArray, data_name
37
+ provides the name for the xarray data variable
38
+
39
+ Attributes:
40
+ data (xarray.Dataset)
41
+ """
42
+
43
+ @property
44
+ def active_data_array_name(self):
45
+ data_var_list = list(self.data.data_vars.keys())
46
+ if self._active_data_array_name not in data_var_list:
47
+ raise ValueError("data_array_name not found in data_vars: {}".format(data_var_list))
48
+ return self._active_data_array_name
49
+
50
+ @active_data_array_name.setter
51
+ def active_data_array_name(self, data_array_name: str):
52
+ self._active_data_array_name = data_array_name
53
+
54
+ @classmethod
55
+ def from_numpy(cls, array: np.ndarray, coords: dict = None, data_array_name: str = "data_array",
56
+ dim_names: List[str] = None):
57
+ if dim_names is None:
58
+ dim_names = cls._default_dim_names(array.ndim)
59
+ # if they are more than 3 we do not know the dimension name but it should valid:
60
+
61
+ dataset: xr.Dataset = xr.Dataset(
62
+ data_vars=
63
+ {
64
+ data_array_name: (dim_names, array)
65
+ },
66
+ coords=coords
67
+ )
68
+
69
+ return cls(dataset, data_array_name)
70
+
71
+ @classmethod
72
+ def from_data_array(cls, data_array: xr.DataArray, data_array_name: str = "data_array"):
73
+ dataset: xr.Dataset = xr.Dataset(
74
+ data_vars={
75
+ data_array_name: data_array
76
+ },
77
+ coords=data_array.coords
78
+ )
79
+
80
+ return cls(dataset, data_array_name)
81
+
82
+ @classmethod
83
+ def from_dict(cls, data_dict: Dict[str, xr.DataArray], coords: Dict[str, str] = None, data_array_name: str = "data_array"):
84
+ dataset: xr.Dataset = xr.Dataset(data_vars=data_dict, coords=coords)
85
+ return cls(dataset, data_array_name)
86
+
87
+ @classmethod
88
+ def from_pyvista_structured_grid(
89
+ cls,
90
+ grid: Union["pyvista.ExplicitStructuredGrid", "pyvista.StructuredGrid"],
91
+ data_array_name: str = "data_array"
92
+ ):
93
+ pyvista = require_pyvista()
94
+ # Extract p
95
+
96
+ # Extract cell data and point data (if any)
97
+ data_vars = {}
98
+
99
+ # TODO: I need to do something with the bounds
100
+
101
+ dimensions = np.array(grid.dimensions) - 1
102
+ default_dim_names = cls._default_dim_names(dimensions.shape[0])
103
+
104
+ bounds: tuple = grid.bounds
105
+ coords = {}
106
+ for i, dim in enumerate(default_dim_names):
107
+ coords[dim] = np.linspace(
108
+ start=bounds[i * 2],
109
+ stop=bounds[i * 2 + 1],
110
+ num=dimensions[i],
111
+ endpoint=False
112
+ )
113
+
114
+ for name in grid.cell_data:
115
+ cell_attr_data: pyvista.pyvista_ndarray = grid[name]
116
+ cell_attr_data_reshaped = cell_attr_data.reshape(dimensions, order='F')
117
+
118
+ data_vars[name] = xr.DataArray(
119
+ data=cell_attr_data_reshaped,
120
+ dims=default_dim_names,
121
+ name=name
122
+ )
123
+
124
+ dataset: xr.Dataset = xr.Dataset(
125
+ data_vars=data_vars,
126
+ coords=coords
127
+ )
128
+ return cls(dataset, data_array_name)
129
+
130
+ @property
131
+ def values(self) -> np.ndarray:
132
+ return self.data[self.active_data_array_name].values
133
+
134
+ _bounds: Tuple[float, float, float, float, float, float] = None
135
+
136
+ @property
137
+ def bounds(self):
138
+ if self._bounds is not None:
139
+ return self._bounds
140
+
141
+ array_: xr.DataArray = self.data[self.active_data_array_name]
142
+ bounds = self._get_bounds_from_coord(array_)
143
+ return bounds
144
+
145
+ @bounds.setter
146
+ def bounds(self, bounds: Tuple[float, float, float, float, float, float]):
147
+ """
148
+ Set the bounds of the structured data. This is useful for defining the
149
+ spatial extent of the data in a structured grid.
150
+
151
+ Args:
152
+ bounds (Tuple[float, float, float, float, float, float]): A tuple containing
153
+ the minimum and maximum values for each dimension (xmin, xmax, ymin, ymax, zmin, zmax).
154
+ """
155
+ self._bounds = bounds
156
+
157
+ @property
158
+ def shape(self):
159
+ return self.active_data_array.shape
160
+
161
+ @property
162
+ def active_data_array(self):
163
+ return self.data[self.active_data_array_name]
164
+
165
+ @staticmethod
166
+ def _get_bounds_from_coord(xr_obj: xr.DataArray):
167
+ bounds = {}
168
+ for coord in xr_obj.coords:
169
+ bounds[coord] = (xr_obj[coord].min().item(), xr_obj[coord].max().item())
170
+ return bounds
171
+
172
+ def default_data_array_to_binary_legacy(self, order: Literal["K", "A", "C", "F"] = 'F'):
173
+ bytearray_le = self._to_bytearray(order=order)
174
+ header = self._set_binary_header()
175
+
176
+ return bytearray_le, header
177
+
178
+ def to_binary(self, order: Literal["K", "A", "C", "F"] = 'F') -> bytes:
179
+ """Converts the structured data to a binary file
180
+
181
+ Notes:
182
+ Only the active data array is converted to binary for now
183
+ """
184
+
185
+ body_ = self._to_bytearray(order)
186
+ header = self._set_binary_header()
187
+
188
+ import json
189
+ header_json = json.dumps(header)
190
+ header_json_bytes = header_json.encode('utf-8')
191
+ header_json_length = len(header_json_bytes)
192
+ header_json_length_bytes = header_json_length.to_bytes(4, byteorder='little')
193
+ file = header_json_length_bytes + header_json_bytes + body_
194
+ return file
195
+
196
+ def _set_binary_header(self) -> Dict:
197
+ data_array = self.active_data_array
198
+
199
+ match self.type:
200
+ case StructuredDataType.REGULAR_AXIS_ALIGNED:
201
+ header = {
202
+ "data_shape": self.shape,
203
+ "bounds" : self.bounds,
204
+ "transform" : None,
205
+ "dtype" : self.dtype,
206
+ "data_name" : self.active_data_array_name
207
+ }
208
+ case _:
209
+ raise NotImplementedError(f"StructuredDataType {self.type} not implemented yet")
210
+
211
+ return header
212
+
213
+ def _to_bytearray(self, order: Literal["K", "A", "C", "F"]) -> bytes:
214
+ data_array = self.active_data_array
215
+
216
+ data = data_array.values.astype(self.dtype).tobytes(order)
217
+ bytearray_le = data
218
+ return bytearray_le
219
+
220
+ @classmethod
221
+ def _default_dim_names(cls, n_dims: int):
222
+ if n_dims == 2:
223
+ dim_names = ['x', 'y']
224
+ elif n_dims == 3:
225
+ dim_names = ['x', 'y', 'z']
226
+ else:
227
+ dim_names = ['dim' + str(i) for i in range(n_dims)]
228
+ return dim_names
229
+
230
+ def to_netcdf(self, path: str, **to_netcdf_kwargs):
231
+ """
232
+ Serializes the current StructuredData instance to a NetCDF file.
233
+
234
+ Args:
235
+ path (str): The path (including file name) where the NetCDF file will be saved.
236
+ **to_netcdf_kwargs: Additional keyword arguments forwarded to xarray's `to_netcdf`.
237
+ """
238
+ # Copy the dataset (shallow copy of the data structure, no copying of the underlying arrays)
239
+ ds = self.data.copy(deep=False)
240
+
241
+ # Store relevant metadata as global attributes:
242
+ ds.attrs["active_data_array_name"] = self._active_data_array_name
243
+ ds.attrs["structured_data_type"] = self.type.name # e.g., "REGULAR_AXIS_ALIGNED"
244
+ ds.attrs["dtype"] = self.dtype # e.g., "float32"
245
+
246
+ # Use xarray's to_netcdf
247
+ ds.to_netcdf(path, **to_netcdf_kwargs)
248
+
249
+ @classmethod
250
+ def from_netcdf(cls, path: str, **from_netcdf_kwargs):
251
+ """
252
+ Deserializes a NetCDF file into a StructuredData instance.
253
+
254
+ Args:
255
+ path (str): The path to the NetCDF file to read.
256
+ **from_netcdf_kwargs: Additional keyword arguments forwarded to xarray's `open_dataset`.
257
+
258
+ Returns:
259
+ StructuredData: A new instance of StructuredData loaded from the file.
260
+ """
261
+ ds = xr.open_dataset(path, **from_netcdf_kwargs)
262
+
263
+ # Retrieve what was stored in attrs (with defaults if missing)
264
+ data_array_name = ds.attrs.get("active_data_array_name", "data_array")
265
+ dtype_str: str = ds.attrs.get("dtype", "float32")
266
+ if dtype_str not in ["float32", "float64"]:
267
+ raise ValueError(f"Unsupported dtype: {dtype_str}")
268
+
269
+ sdt_str = ds.attrs.get("structured_data_type", "REGULAR_AXIS_ALIGNED")
270
+
271
+ # Convert strings back to your enum or any other type
272
+ # (assuming StructuredDataType is an Enum where name matches sdt_str)
273
+ if sdt_str not in StructuredDataType.__members__:
274
+ raise ValueError(f"Unsupported structured_data_type: {sdt_str}")
275
+ structured_data_type: StructuredDataType = StructuredDataType[sdt_str]
276
+
277
+ return cls(
278
+ data=ds,
279
+ _active_data_array_name=data_array_name,
280
+ type=structured_data_type,
281
+ dtype=dtype_str
282
+ )