sdf-xarray 0.4.0__cp314-cp314t-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.
sdf_xarray/csdf.pxd ADDED
@@ -0,0 +1,127 @@
1
+ cdef extern from "<stdint.h>" nogil:
2
+ ctypedef signed int int32_t
3
+ ctypedef signed long int64_t
4
+
5
+ cdef extern from "sdf.h":
6
+ cdef enum:
7
+ SDF_VERSION
8
+ SDF_REVISION
9
+ SDF_LIB_VERSION
10
+ SDF_LIB_REVISION
11
+ SDF_MAGIC
12
+ SDF_MAXDIMS
13
+ SDF_READ
14
+ SDF_BLOCKTYPE_SCRUBBED
15
+ SDF_BLOCKTYPE_NULL
16
+ SDF_BLOCKTYPE_PLAIN_MESH
17
+ SDF_BLOCKTYPE_POINT_MESH
18
+ SDF_BLOCKTYPE_PLAIN_VARIABLE
19
+ SDF_BLOCKTYPE_POINT_VARIABLE
20
+ SDF_BLOCKTYPE_CONSTANT
21
+ SDF_BLOCKTYPE_ARRAY
22
+ SDF_BLOCKTYPE_RUN_INFO
23
+ SDF_BLOCKTYPE_SOURCE
24
+ SDF_BLOCKTYPE_STITCHED_TENSOR
25
+ SDF_BLOCKTYPE_STITCHED_MATERIAL
26
+ SDF_BLOCKTYPE_STITCHED_MATVAR
27
+ SDF_BLOCKTYPE_STITCHED_SPECIES
28
+ SDF_BLOCKTYPE_SPECIES
29
+ SDF_BLOCKTYPE_PLAIN_DERIVED
30
+ SDF_BLOCKTYPE_POINT_DERIVED
31
+ SDF_BLOCKTYPE_CONTIGUOUS_TENSOR
32
+ SDF_BLOCKTYPE_CONTIGUOUS_MATERIAL
33
+ SDF_BLOCKTYPE_CONTIGUOUS_MATVAR
34
+ SDF_BLOCKTYPE_CONTIGUOUS_SPECIES
35
+ SDF_BLOCKTYPE_CPU_SPLIT
36
+ SDF_BLOCKTYPE_STITCHED_OBSTACLE_GROUP
37
+ SDF_BLOCKTYPE_UNSTRUCTURED_MESH
38
+ SDF_BLOCKTYPE_STITCHED
39
+ SDF_BLOCKTYPE_CONTIGUOUS
40
+ SDF_BLOCKTYPE_LAGRANGIAN_MESH
41
+ SDF_BLOCKTYPE_STATION
42
+ SDF_BLOCKTYPE_STATION_DERIVED
43
+ SDF_BLOCKTYPE_DATABLOCK
44
+ SDF_BLOCKTYPE_NAMEVALUE
45
+ SDF_DATATYPE_NULL
46
+ SDF_DATATYPE_INTEGER4
47
+ SDF_DATATYPE_INTEGER8
48
+ SDF_DATATYPE_REAL4
49
+ SDF_DATATYPE_REAL8
50
+ SDF_DATATYPE_REAL16
51
+ SDF_DATATYPE_CHARACTER
52
+ SDF_DATATYPE_LOGICAL
53
+ SDF_DATATYPE_OTHER
54
+
55
+ ctypedef int comm_t
56
+
57
+ ctypedef struct sdf_block_t:
58
+ double* extents
59
+ double* dim_mults
60
+ double mult, time, time_increment
61
+ int64_t dims[SDF_MAXDIMS]
62
+ int64_t local_dims[SDF_MAXDIMS]
63
+ int64_t data_length
64
+ int32_t ndims, geometry, datatype, blocktype
65
+ int32_t stagger, datatype_out
66
+ char const_value[16]
67
+ char* id
68
+ char* units
69
+ char* mesh_id
70
+ char* material_id
71
+ char* name
72
+ char* material_name
73
+ char** dim_labels
74
+ char** dim_units
75
+ char** variable_ids
76
+ void** grids
77
+ void* data
78
+ sdf_block_t* next
79
+
80
+ ctypedef struct sdf_file_t:
81
+ int32_t sdf_lib_version, sdf_lib_revision
82
+ int32_t sdf_extension_version, sdf_extension_revision
83
+ int32_t file_version, file_revision
84
+ double time
85
+ int64_t current_location
86
+ int32_t jobid1, jobid2, endianness, summary_size
87
+ int32_t block_header_length, string_length, id_length
88
+ int32_t code_io_version, step
89
+ int32_t nblocks
90
+ char* buffer
91
+ char* filename
92
+ bint restart_flag, other_domains
93
+ bint station_file
94
+ bint restart_flag
95
+ char* code_name
96
+ sdf_block_t* blocklist
97
+ sdf_block_t* current_block
98
+
99
+ cdef struct run_info:
100
+ int64_t defines
101
+ int32_t version, revision, compile_date, run_date, io_date, minor_rev
102
+ char* commit_id
103
+ char* sha1sum
104
+ char* compile_machine
105
+ char* compile_flags
106
+
107
+ sdf_file_t *sdf_open(const char *filename, comm_t comm, int mode, int use_mmap)
108
+ bint sdf_close(sdf_file_t *h)
109
+ sdf_block_t *sdf_find_block_by_name(sdf_file_t *h, const char *name)
110
+ bint sdf_read_header(sdf_file_t *h)
111
+ bint sdf_read_blocklist_all(sdf_file_t *h)
112
+ bint sdf_read_block_info(sdf_file_t *h)
113
+ bint sdf_read_data(sdf_file_t *h)
114
+ bint sdf_get_domain_bounds(sdf_file_t *h, int rank,
115
+ int *starts, int *local_dims)
116
+ int sdf_block_set_array_section(sdf_block_t *b, const int ndims,
117
+ const int64_t *starts, const int64_t *ends,
118
+ const int64_t *strides)
119
+
120
+
121
+ cdef extern from "sdf_helper.h":
122
+ bint sdf_helper_read_data(sdf_file_t *h, sdf_block_t *b)
123
+
124
+
125
+ cdef extern from "stack_allocator.h":
126
+ void sdf_stack_destroy(sdf_file_t *h)
127
+ void sdf_stack_init(sdf_file_t *h)
@@ -0,0 +1,71 @@
1
+ import xarray as xr
2
+
3
+
4
+ @xr.register_dataset_accessor("epoch")
5
+ class EpochAccessor:
6
+ def __init__(self, xarray_obj: xr.Dataset):
7
+ # The xarray object is the Dataset, which we store as self._ds
8
+ self._ds = xarray_obj
9
+
10
+ def rescale_coords(
11
+ self,
12
+ multiplier: float,
13
+ unit_label: str,
14
+ coord_names: str | list[str],
15
+ ) -> xr.Dataset:
16
+ """
17
+ Rescales specified X and Y coordinates in the Dataset by a given multiplier
18
+ and updates the unit label attribute.
19
+
20
+ Parameters
21
+ ----------
22
+ multiplier : float
23
+ The factor by which to multiply the coordinate values (e.g., 1e6 for meters to microns).
24
+ unit_label : str
25
+ The new unit label for the coordinates (e.g., "µm").
26
+ coord_names : str or list of str
27
+ The name(s) of the coordinate variable(s) to rescale.
28
+ If a string, only that coordinate is rescaled.
29
+ If a list, all listed coordinates are rescaled.
30
+
31
+ Returns
32
+ -------
33
+ xr.Dataset
34
+ A new Dataset with the updated and rescaled coordinates.
35
+
36
+ Examples
37
+ --------
38
+ # Convert X, Y, and Z from meters to microns
39
+ >>> ds_in_microns = ds.epoch.rescale_coords(1e6, "µm", coord_names=["X_Grid", "Y_Grid", "Z_Grid"])
40
+
41
+ # Convert only X to millimeters
42
+ >>> ds_in_mm = ds.epoch.rescale_coords(1000, "mm", coord_names="X_Grid")
43
+ """
44
+
45
+ ds = self._ds
46
+ new_coords = {}
47
+
48
+ if isinstance(coord_names, str):
49
+ # Convert single string to a list
50
+ coords_to_process = [coord_names]
51
+ elif isinstance(coord_names, list):
52
+ # Use the provided list
53
+ coords_to_process = coord_names
54
+ else:
55
+ coords_to_process = list(coord_names)
56
+
57
+ for coord_name in coords_to_process:
58
+ if coord_name not in ds.coords:
59
+ raise ValueError(
60
+ f"Coordinate '{coord_name}' not found in the Dataset. Cannot rescale."
61
+ )
62
+
63
+ coord_original = ds[coord_name]
64
+
65
+ coord_rescaled = coord_original * multiplier
66
+ coord_rescaled.attrs = coord_original.attrs.copy()
67
+ coord_rescaled.attrs["units"] = unit_label
68
+
69
+ new_coords[coord_name] = coord_rescaled
70
+
71
+ return ds.assign_coords(new_coords)
sdf_xarray/download.py ADDED
@@ -0,0 +1,87 @@
1
+ from pathlib import Path
2
+ from shutil import move
3
+ from typing import TYPE_CHECKING, Literal, TypeAlias
4
+
5
+ if TYPE_CHECKING:
6
+ import pooch # noqa: F401
7
+
8
+ DatasetName: TypeAlias = Literal[
9
+ "test_array_no_grids",
10
+ "test_dist_fn",
11
+ "test_files_1D",
12
+ "test_files_2D_moving_window",
13
+ "test_files_3D",
14
+ "test_mismatched_files",
15
+ "test_two_probes_2D",
16
+ "tutorial_dataset_1d",
17
+ "tutorial_dataset_2d",
18
+ "tutorial_dataset_2d_moving_window",
19
+ "tutorial_dataset_3d",
20
+ ]
21
+
22
+
23
+ def fetch_dataset(
24
+ dataset_name: DatasetName, save_path: Path | str | None = None
25
+ ) -> Path:
26
+ """
27
+ Downloads the specified dataset from its Zenodo URL. If it is already
28
+ downloaded, then the path to the cached, unzipped directory is returned.
29
+
30
+ Parameters
31
+ ---------
32
+ dataset_name
33
+ The name of the dataset to download
34
+ save_path
35
+ The directory to save the dataset to (defaults to the cache folder ``"sdf_datasets"``.
36
+ See `pooch.os_cache` for details on how the cache works)
37
+
38
+ Returns
39
+ -------
40
+ Path
41
+ The path to the directory containing the unzipped dataset files
42
+
43
+ Examples
44
+ --------
45
+ >>> # Assuming the dataset has not been downloaded yet
46
+ >>> path = fetch_dataset("tutorial_dataset_1d")
47
+ Downloading file 'tutorial_dataset_1d.zip' ...
48
+ Unzipping contents of '.../sdf_datasets/tutorial_dataset_1d.zip' to '.../sdf_datasets/tutorial_dataset_1d'
49
+ >>> path
50
+ '.../sdf_datasets/tutorial_dataset_1d'
51
+ """
52
+ import pooch # noqa: PLC0415
53
+
54
+ logger = pooch.get_logger()
55
+ datasets = pooch.create(
56
+ path=pooch.os_cache("sdf_datasets"),
57
+ base_url="doi:10.5281/zenodo.17618510",
58
+ registry={
59
+ "test_array_no_grids.zip": "md5:583c85ed8c31d0e34e7766b6d9f2d6da",
60
+ "test_dist_fn.zip": "md5:a582ff5e8c59bad62fe4897f65fc7a11",
61
+ "test_files_1D.zip": "md5:42e53b229556c174c538c5481c4d596a",
62
+ "test_files_2D_moving_window.zip": "md5:3744483bbf416936ad6df8847c54dad1",
63
+ "test_files_3D.zip": "md5:a679e71281bab1d373dc4980e6da1a7c",
64
+ "test_mismatched_files.zip": "md5:710fdc94666edf7777523e8fc9dd1bd4",
65
+ "test_two_probes_2D.zip": "md5:0f2a4fefe84a15292d066b3320d4d533",
66
+ "tutorial_dataset_1d.zip": "md5:7fad744d8b8b2b84bba5c0e705fdef7b",
67
+ "tutorial_dataset_2d.zip": "md5:1945ecdbc1ac1798164f83ea2b3d1b31",
68
+ "tutorial_dataset_2d_moving_window.zip": "md5:a795f40d18df69263842055de4559501",
69
+ "tutorial_dataset_3d.zip": "md5:d9254648867016292440fdb028f717f7",
70
+ },
71
+ )
72
+
73
+ datasets.fetch(
74
+ f"{dataset_name}.zip", processor=pooch.Unzip(extract_dir="."), progressbar=True
75
+ )
76
+ cache_path = Path(datasets.path) / dataset_name
77
+
78
+ if save_path is not None:
79
+ save_path = Path(save_path)
80
+ logger.info(
81
+ "Moving contents of '%s' to '%s'",
82
+ cache_path,
83
+ save_path / dataset_name,
84
+ )
85
+ return move(cache_path, save_path / dataset_name)
86
+
87
+ return cache_path
sdf_xarray/plotting.py ADDED
@@ -0,0 +1,293 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ import xarray as xr
7
+
8
+ if TYPE_CHECKING:
9
+ import matplotlib.pyplot as plt
10
+ from matplotlib.animation import FuncAnimation
11
+
12
+ from types import MethodType
13
+
14
+
15
+ def get_frame_title(
16
+ data: xr.DataArray,
17
+ frame: int,
18
+ display_sdf_name: bool = False,
19
+ title_custom: str | None = None,
20
+ t: str = "time",
21
+ ) -> str:
22
+ """Generate the title for a frame
23
+
24
+ Parameters
25
+ ----------
26
+ data
27
+ DataArray containing the target data
28
+ frame
29
+ Frame number
30
+ display_sdf_name
31
+ Display the sdf file name in the animation title
32
+ title_custom
33
+ Custom title to add to the plot
34
+ t
35
+ Time coordinate
36
+ """
37
+
38
+ # Adds custom text to the start of the title, if specified
39
+ title_custom = "" if title_custom is None else f"{title_custom}, "
40
+ # Adds the time axis and associated units to the title
41
+ t_axis_value = data[t][frame].values
42
+
43
+ t_axis_units = data[t].attrs.get("units", False)
44
+ t_axis_units_formatted = f" [{t_axis_units}]" if t_axis_units else ""
45
+ title_t_axis = f"{data[t].long_name} = {t_axis_value:.2e}{t_axis_units_formatted}"
46
+
47
+ # Adds sdf name to the title, if specifed
48
+ title_sdf = f", {frame:04d}.sdf" if display_sdf_name else ""
49
+ return f"{title_custom}{title_t_axis}{title_sdf}"
50
+
51
+
52
+ def calculate_window_boundaries(
53
+ data: xr.DataArray,
54
+ xlim: tuple[float, float] | None = None,
55
+ x_axis_name: str = "X_Grid_mid",
56
+ t: str = "time",
57
+ ) -> np.ndarray:
58
+ """Calculate the bounderies a moving window frame. If the user specifies xlim, this will
59
+ be used as the initial bounderies and the window will move along acordingly.
60
+
61
+ Parameters
62
+ ----------
63
+ data
64
+ DataArray containing the target data
65
+ xlim
66
+ x limits
67
+ x_axis_name
68
+ Name of coordinate to assign to the x-axis
69
+ t
70
+ Time coordinate
71
+ """
72
+ x_grid = data[x_axis_name].values
73
+ x_half_cell = (x_grid[1] - x_grid[0]) / 2
74
+ N_frames = data[t].size
75
+
76
+ # Find the window bounderies by finding the first and last non-NaN values in the 0th lineout
77
+ # along the x-axis.
78
+ window_boundaries = np.zeros((N_frames, 2))
79
+ for i in range(N_frames):
80
+ # Check if data is 1D
81
+ if data.ndim == 2:
82
+ target_lineout = data[i].values
83
+ # Check if data is 2D
84
+ if data.ndim == 3:
85
+ target_lineout = data[i, :, 0].values
86
+ x_grid_non_nan = x_grid[~np.isnan(target_lineout)]
87
+ window_boundaries[i, 0] = x_grid_non_nan[0] - x_half_cell
88
+ window_boundaries[i, 1] = x_grid_non_nan[-1] + x_half_cell
89
+
90
+ # User's choice for initial window edge supercides the one calculated
91
+ if xlim is not None:
92
+ window_boundaries = window_boundaries + xlim - window_boundaries[0]
93
+ return window_boundaries
94
+
95
+
96
+ def compute_global_limits(
97
+ data: xr.DataArray,
98
+ min_percentile: float = 0,
99
+ max_percentile: float = 100,
100
+ ) -> tuple[float, float]:
101
+ """Remove all NaN values from the target data to calculate the global minimum and maximum of the data.
102
+ User defined percentiles can remove extreme outliers.
103
+
104
+ Parameters
105
+ ----------
106
+ data
107
+ DataArray containing the target data
108
+ min_percentile
109
+ Minimum percentile of the data
110
+ max_percentile
111
+ Maximum percentile of the data
112
+ """
113
+
114
+ # Removes NaN values, needed for moving windows
115
+ values_no_nan = data.values[~np.isnan(data.values)]
116
+
117
+ # Finds the global minimum and maximum of the plot, based on the percentile of the data
118
+ global_min = np.percentile(values_no_nan, min_percentile)
119
+ global_max = np.percentile(values_no_nan, max_percentile)
120
+ return global_min, global_max
121
+
122
+
123
+ def animate(
124
+ data: xr.DataArray,
125
+ fps: float = 10,
126
+ min_percentile: float = 0,
127
+ max_percentile: float = 100,
128
+ title: str | None = None,
129
+ display_sdf_name: bool = False,
130
+ t: str | None = None,
131
+ ax: plt.Axes | None = None,
132
+ **kwargs,
133
+ ) -> FuncAnimation:
134
+ """Generate an animation using an xarray.DataArray
135
+
136
+ Parameters
137
+ ---------
138
+ data
139
+ DataArray containing the target data
140
+ fps
141
+ Frames per second for the animation
142
+ min_percentile
143
+ Minimum percentile of the data
144
+ max_percentile
145
+ Maximum percentile of the data
146
+ title
147
+ Custom title to add to the plot
148
+ display_sdf_name
149
+ Display the sdf file name in the animation title
150
+ t
151
+ Coordinate for t axis (the coordinate which will be animated over). If `None`, use data.dims[0]
152
+ ax
153
+ Matplotlib axes on which to plot
154
+ kwargs
155
+ Keyword arguments to be passed to matplotlib
156
+
157
+ Examples
158
+ --------
159
+ >>> ds["Derived_Number_Density_Electron"].epoch.animate()
160
+ """
161
+ import matplotlib.pyplot as plt # noqa: PLC0415
162
+ from matplotlib.animation import FuncAnimation # noqa: PLC0415
163
+
164
+ kwargs_original = kwargs.copy()
165
+
166
+ # Create plot if no ax is provided
167
+ if ax is None:
168
+ fig, ax = plt.subplots()
169
+ # Prevents figure from prematurely displaying in Jupyter notebook
170
+ plt.close(fig)
171
+
172
+ # Sets the animation coordinate (t) for iteration. If time is in the coords
173
+ # then it will set time to be t. If it is not it will fallback to the last
174
+ # coordinate passed in. By default coordinates are passed in from xarray in
175
+ # the form x, y, z so in order to preserve the x and y being on their
176
+ # respective axes we animate over the final coordinate that is passed in
177
+ # which in this example is z
178
+ coord_names = list(data.dims)
179
+ if t is None:
180
+ t = "time" if "time" in coord_names else coord_names[-1]
181
+ coord_names.remove(t)
182
+
183
+ N_frames = data[t].size
184
+
185
+ if data.ndim == 2:
186
+ kwargs.setdefault("x", coord_names[0])
187
+ plot = data.isel({t: 0}).plot(ax=ax, **kwargs)
188
+ ax.set_title(get_frame_title(data, 0, display_sdf_name, title, t))
189
+ global_min, global_max = compute_global_limits(
190
+ data, min_percentile, max_percentile
191
+ )
192
+ ax.set_ylim(global_min, global_max)
193
+
194
+ if data.ndim == 3:
195
+ if "norm" not in kwargs:
196
+ global_min, global_max = compute_global_limits(
197
+ data, min_percentile, max_percentile
198
+ )
199
+ kwargs["norm"] = plt.Normalize(vmin=global_min, vmax=global_max)
200
+ kwargs["add_colorbar"] = False
201
+ # Set default x and y coordinates for 3D data if not provided
202
+ kwargs.setdefault("x", coord_names[0])
203
+ kwargs.setdefault("y", coord_names[1])
204
+
205
+ # Finds the time step with the minimum data value
206
+ # This is needed so that the animation can use the correct colour bar
207
+ argmin_time = np.unravel_index(data.argmin(), data.shape)[0]
208
+
209
+ # Initialize the plot, the final output will still start at the first time step
210
+ plot = data.isel({t: argmin_time}).plot(ax=ax, **kwargs)
211
+ ax.set_title(get_frame_title(data, 0, display_sdf_name, title, t))
212
+ kwargs["cmap"] = plot.cmap
213
+
214
+ # Add colorbar
215
+ if kwargs_original.get("add_colorbar", True):
216
+ long_name = data.attrs.get("long_name")
217
+ units = data.attrs.get("units")
218
+ fig = plot.get_figure()
219
+ fig.colorbar(plot, ax=ax, label=f"{long_name} [{units}]")
220
+
221
+ # check if there is a moving window by finding NaNs in the data
222
+ move_window = np.isnan(np.sum(data.values))
223
+ if move_window:
224
+ window_boundaries = calculate_window_boundaries(
225
+ data, kwargs.get("xlim"), kwargs["x"]
226
+ )
227
+
228
+ def update(frame):
229
+ # Set the xlim for each frame in the case of a moving window
230
+ if move_window:
231
+ kwargs["xlim"] = window_boundaries[frame]
232
+
233
+ # Update plot for the new frame
234
+ ax.clear()
235
+
236
+ plot = data.isel({t: frame}).plot(ax=ax, **kwargs)
237
+ ax.set_title(get_frame_title(data, frame, display_sdf_name, title, t))
238
+
239
+ if data.ndim == 2:
240
+ ax.set_ylim(global_min, global_max)
241
+ return plot
242
+
243
+ return FuncAnimation(
244
+ ax.get_figure(),
245
+ update,
246
+ frames=range(N_frames),
247
+ interval=1000 / fps,
248
+ repeat=True,
249
+ )
250
+
251
+
252
+ def show(anim):
253
+ """Shows the FuncAnimation in a Jupyter notebook.
254
+
255
+ Parameters
256
+ ----------
257
+ anim
258
+ `matplotlib.animation.FuncAnimation`
259
+ """
260
+ from IPython.display import HTML # noqa: PLC0415
261
+
262
+ return HTML(anim.to_jshtml())
263
+
264
+
265
+ @xr.register_dataarray_accessor("epoch")
266
+ class EpochAccessor:
267
+ def __init__(self, xarray_obj):
268
+ self._obj = xarray_obj
269
+
270
+ def animate(self, *args, **kwargs) -> FuncAnimation:
271
+ """Generate animations of Epoch data.
272
+
273
+ Parameters
274
+ ----------
275
+ args
276
+ Positional arguments passed to :func:`animation`.
277
+ kwargs
278
+ Keyword arguments passed to :func:`animation`.
279
+
280
+ Examples
281
+ --------
282
+ >>> anim = ds["Electric_Field_Ey"].epoch.animate()
283
+ >>> anim.save("myfile.mp4")
284
+ >>> # Or in a jupyter notebook:
285
+ >>> anim.show()
286
+ """
287
+
288
+ # Add anim.show() functionality
289
+ # anim.show() will display the animation in a jupyter notebook
290
+ anim = animate(self._obj, *args, **kwargs)
291
+ anim.show = MethodType(show, anim)
292
+
293
+ return anim