sdf-xarray 0.3.2__cp312-cp312-win_amd64.whl → 0.5.0__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lib/SDFC_14.4.7/sdfc.lib +0 -0
- sdf_xarray/__init__.py +274 -13
- sdf_xarray/_version.py +3 -3
- sdf_xarray/dataset_accessor.py +53 -2
- sdf_xarray/download.py +88 -0
- sdf_xarray/plotting.py +442 -80
- sdf_xarray/sdf_interface.cp312-win_amd64.pyd +0 -0
- sdf_xarray/sdf_interface.pyx +4 -2
- {sdf_xarray-0.3.2.dist-info → sdf_xarray-0.5.0.dist-info}/METADATA +26 -52
- {sdf_xarray-0.3.2.dist-info → sdf_xarray-0.5.0.dist-info}/RECORD +13 -12
- {sdf_xarray-0.3.2.dist-info → sdf_xarray-0.5.0.dist-info}/entry_points.txt +3 -0
- {sdf_xarray-0.3.2.dist-info → sdf_xarray-0.5.0.dist-info}/WHEEL +0 -0
- {sdf_xarray-0.3.2.dist-info → sdf_xarray-0.5.0.dist-info}/licenses/LICENCE +0 -0
sdf_xarray/plotting.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from types import MethodType
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
4
8
|
|
|
5
9
|
import numpy as np
|
|
6
10
|
import xarray as xr
|
|
@@ -10,41 +14,77 @@ if TYPE_CHECKING:
|
|
|
10
14
|
from matplotlib.animation import FuncAnimation
|
|
11
15
|
|
|
12
16
|
|
|
17
|
+
@dataclass
|
|
18
|
+
class AnimationUnit:
|
|
19
|
+
update: Callable[[int], object]
|
|
20
|
+
n_frames: int
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
def get_frame_title(
|
|
14
24
|
data: xr.DataArray,
|
|
15
25
|
frame: int,
|
|
16
26
|
display_sdf_name: bool = False,
|
|
17
27
|
title_custom: str | None = None,
|
|
28
|
+
t: str = "time",
|
|
18
29
|
) -> str:
|
|
19
|
-
"""Generate the title for a frame
|
|
30
|
+
"""Generate the title for a frame
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
data
|
|
35
|
+
DataArray containing the target data
|
|
36
|
+
frame
|
|
37
|
+
Frame number
|
|
38
|
+
display_sdf_name
|
|
39
|
+
Display the sdf file name in the animation title
|
|
40
|
+
title_custom
|
|
41
|
+
Custom title to add to the plot
|
|
42
|
+
t
|
|
43
|
+
Time coordinate
|
|
44
|
+
"""
|
|
45
|
+
|
|
20
46
|
# Adds custom text to the start of the title, if specified
|
|
21
47
|
title_custom = "" if title_custom is None else f"{title_custom}, "
|
|
22
|
-
# Adds the time and associated units to the title
|
|
23
|
-
|
|
48
|
+
# Adds the time axis and associated units to the title
|
|
49
|
+
t_axis_value = data[t][frame].values
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
t_axis_units = data[t].attrs.get("units", False)
|
|
52
|
+
t_axis_units_formatted = f" [{t_axis_units}]" if t_axis_units else ""
|
|
53
|
+
title_t_axis = f"{data[t].long_name} = {t_axis_value:.2e}{t_axis_units_formatted}"
|
|
28
54
|
|
|
29
55
|
# Adds sdf name to the title, if specifed
|
|
30
56
|
title_sdf = f", {frame:04d}.sdf" if display_sdf_name else ""
|
|
31
|
-
return f"{title_custom}{
|
|
57
|
+
return f"{title_custom}{title_t_axis}{title_sdf}"
|
|
32
58
|
|
|
33
59
|
|
|
34
60
|
def calculate_window_boundaries(
|
|
35
|
-
data: xr.DataArray,
|
|
61
|
+
data: xr.DataArray,
|
|
62
|
+
xlim: tuple[float, float] | None = None,
|
|
63
|
+
x_axis_name: str = "X_Grid_mid",
|
|
64
|
+
t: str = "time",
|
|
36
65
|
) -> np.ndarray:
|
|
37
|
-
"""Calculate the
|
|
38
|
-
be used as the initial
|
|
66
|
+
"""Calculate the boundaries a moving window frame. If the user specifies xlim, this will
|
|
67
|
+
be used as the initial boundaries and the window will move along acordingly.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
data
|
|
72
|
+
DataArray containing the target data
|
|
73
|
+
xlim
|
|
74
|
+
x limits
|
|
75
|
+
x_axis_name
|
|
76
|
+
Name of coordinate to assign to the x-axis
|
|
77
|
+
t
|
|
78
|
+
Time coordinate
|
|
39
79
|
"""
|
|
40
|
-
x_grid = data[
|
|
80
|
+
x_grid = data[x_axis_name].values
|
|
41
81
|
x_half_cell = (x_grid[1] - x_grid[0]) / 2
|
|
42
|
-
|
|
82
|
+
n_frames = data[t].size
|
|
43
83
|
|
|
44
|
-
# Find the window
|
|
84
|
+
# Find the window boundaries by finding the first and last non-NaN values in the 0th lineout
|
|
45
85
|
# along the x-axis.
|
|
46
|
-
window_boundaries = np.zeros((
|
|
47
|
-
for i in range(
|
|
86
|
+
window_boundaries = np.zeros((n_frames, 2))
|
|
87
|
+
for i in range(n_frames):
|
|
48
88
|
# Check if data is 1D
|
|
49
89
|
if data.ndim == 2:
|
|
50
90
|
target_lineout = data[i].values
|
|
@@ -55,8 +95,8 @@ def calculate_window_boundaries(
|
|
|
55
95
|
window_boundaries[i, 0] = x_grid_non_nan[0] - x_half_cell
|
|
56
96
|
window_boundaries[i, 1] = x_grid_non_nan[-1] + x_half_cell
|
|
57
97
|
|
|
58
|
-
# User's choice for initial window edge
|
|
59
|
-
if xlim:
|
|
98
|
+
# User's choice for initial window edge supercedes the one calculated
|
|
99
|
+
if xlim is not None:
|
|
60
100
|
window_boundaries = window_boundaries + xlim - window_boundaries[0]
|
|
61
101
|
return window_boundaries
|
|
62
102
|
|
|
@@ -68,6 +108,15 @@ def compute_global_limits(
|
|
|
68
108
|
) -> tuple[float, float]:
|
|
69
109
|
"""Remove all NaN values from the target data to calculate the global minimum and maximum of the data.
|
|
70
110
|
User defined percentiles can remove extreme outliers.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
data
|
|
115
|
+
DataArray containing the target data
|
|
116
|
+
min_percentile
|
|
117
|
+
Minimum percentile of the data
|
|
118
|
+
max_percentile
|
|
119
|
+
Maximum percentile of the data
|
|
71
120
|
"""
|
|
72
121
|
|
|
73
122
|
# Removes NaN values, needed for moving windows
|
|
@@ -79,6 +128,199 @@ def compute_global_limits(
|
|
|
79
128
|
return global_min, global_max
|
|
80
129
|
|
|
81
130
|
|
|
131
|
+
def _set_axes_labels(ax: plt.Axes, axis_kwargs: dict) -> None:
|
|
132
|
+
"""Set the labels for the x and y axes"""
|
|
133
|
+
if "xlabel" in axis_kwargs:
|
|
134
|
+
ax.set_xlabel(axis_kwargs["xlabel"])
|
|
135
|
+
if "ylabel" in axis_kwargs:
|
|
136
|
+
ax.set_ylabel(axis_kwargs["ylabel"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _setup_2d_plot(
|
|
140
|
+
data: xr.DataArray,
|
|
141
|
+
ax: plt.Axes,
|
|
142
|
+
coord_names: list[str],
|
|
143
|
+
kwargs: dict,
|
|
144
|
+
axis_kwargs: dict,
|
|
145
|
+
min_percentile: float,
|
|
146
|
+
max_percentile: float,
|
|
147
|
+
t: str,
|
|
148
|
+
) -> tuple[float, float]:
|
|
149
|
+
"""Setup 2D plot initialization."""
|
|
150
|
+
|
|
151
|
+
kwargs.setdefault("x", coord_names[0])
|
|
152
|
+
|
|
153
|
+
data.isel({t: 0}).plot(ax=ax, **kwargs)
|
|
154
|
+
|
|
155
|
+
global_min, global_max = compute_global_limits(data, min_percentile, max_percentile)
|
|
156
|
+
|
|
157
|
+
_set_axes_labels(ax, axis_kwargs)
|
|
158
|
+
|
|
159
|
+
if "ylim" not in kwargs:
|
|
160
|
+
ax.set_ylim(global_min, global_max)
|
|
161
|
+
|
|
162
|
+
return global_min, global_max
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _setup_3d_plot(
|
|
166
|
+
data: xr.DataArray,
|
|
167
|
+
ax: plt.Axes,
|
|
168
|
+
coord_names: list[str],
|
|
169
|
+
kwargs: dict,
|
|
170
|
+
kwargs_original: dict,
|
|
171
|
+
axis_kwargs: dict,
|
|
172
|
+
min_percentile: float,
|
|
173
|
+
max_percentile: float,
|
|
174
|
+
t: str,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Setup 3D plot initialization."""
|
|
177
|
+
import matplotlib.pyplot as plt # noqa: PLC0415
|
|
178
|
+
|
|
179
|
+
if "norm" not in kwargs:
|
|
180
|
+
global_min, global_max = compute_global_limits(
|
|
181
|
+
data, min_percentile, max_percentile
|
|
182
|
+
)
|
|
183
|
+
kwargs["norm"] = plt.Normalize(vmin=global_min, vmax=global_max)
|
|
184
|
+
|
|
185
|
+
kwargs["add_colorbar"] = False
|
|
186
|
+
kwargs.setdefault("x", coord_names[0])
|
|
187
|
+
kwargs.setdefault("y", coord_names[1])
|
|
188
|
+
|
|
189
|
+
argmin_time = np.unravel_index(np.argmin(data.values), data.shape)[0]
|
|
190
|
+
plot = data.isel({t: argmin_time}).plot(ax=ax, **kwargs)
|
|
191
|
+
kwargs["cmap"] = plot.cmap
|
|
192
|
+
|
|
193
|
+
_set_axes_labels(ax, axis_kwargs)
|
|
194
|
+
|
|
195
|
+
if kwargs_original.get("add_colorbar", True):
|
|
196
|
+
long_name = data.attrs.get("long_name")
|
|
197
|
+
units = data.attrs.get("units")
|
|
198
|
+
fig = plot.get_figure()
|
|
199
|
+
fig.colorbar(plot, ax=ax, label=f"{long_name} [{units}]")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _generate_animation(
|
|
203
|
+
data: xr.DataArray,
|
|
204
|
+
clear_axes: bool = False,
|
|
205
|
+
min_percentile: float = 0,
|
|
206
|
+
max_percentile: float = 100,
|
|
207
|
+
title: str | None = None,
|
|
208
|
+
display_sdf_name: bool = False,
|
|
209
|
+
move_window: bool = False,
|
|
210
|
+
t: str | None = None,
|
|
211
|
+
ax: plt.Axes | None = None,
|
|
212
|
+
kwargs: dict | None = None,
|
|
213
|
+
) -> AnimationUnit:
|
|
214
|
+
"""
|
|
215
|
+
Internal function for generating the plotting logic required for animations.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
---------
|
|
219
|
+
data
|
|
220
|
+
DataArray containing the target data
|
|
221
|
+
clear_axes
|
|
222
|
+
Decide whether to run ``ax.clear()`` in every update
|
|
223
|
+
min_percentile
|
|
224
|
+
Minimum percentile of the data
|
|
225
|
+
max_percentile
|
|
226
|
+
Maximum percentile of the data
|
|
227
|
+
title
|
|
228
|
+
Custom title to add to the plot
|
|
229
|
+
display_sdf_name
|
|
230
|
+
Display the sdf file name in the animation title
|
|
231
|
+
move_window
|
|
232
|
+
Update the ``xlim`` to be only values that are not NaNs at each time interval
|
|
233
|
+
t
|
|
234
|
+
Coordinate for t axis (the coordinate which will be animated over).
|
|
235
|
+
If ``None``, use ``data.dims[0]``
|
|
236
|
+
ax
|
|
237
|
+
Matplotlib axes on which to plot
|
|
238
|
+
kwargs
|
|
239
|
+
Keyword arguments to be passed to matplotlib
|
|
240
|
+
|
|
241
|
+
Examples
|
|
242
|
+
--------
|
|
243
|
+
>>> anim = animate(ds["Derived_Number_Density_Electron"])
|
|
244
|
+
>>> anim.save("animation.gif")
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
if kwargs is None:
|
|
248
|
+
kwargs = {}
|
|
249
|
+
kwargs_original = kwargs.copy()
|
|
250
|
+
|
|
251
|
+
axis_kwargs = {}
|
|
252
|
+
for key in ("xlabel", "ylabel"):
|
|
253
|
+
if key in kwargs:
|
|
254
|
+
axis_kwargs[key] = kwargs.pop(key)
|
|
255
|
+
|
|
256
|
+
# Sets the animation coordinate (t) for iteration. If time is in the coords
|
|
257
|
+
# then it will set time to be t. If it is not it will fallback to the last
|
|
258
|
+
# coordinate passed in. By default coordinates are passed in from xarray in
|
|
259
|
+
# the form x, y, z so in order to preserve the x and y being on their
|
|
260
|
+
# respective axes we animate over the final coordinate that is passed in
|
|
261
|
+
# which in this example is z
|
|
262
|
+
coord_names = list(data.dims)
|
|
263
|
+
if t is None:
|
|
264
|
+
t = "time" if "time" in coord_names else coord_names[-1]
|
|
265
|
+
coord_names.remove(t)
|
|
266
|
+
|
|
267
|
+
N_frames = data[t].size
|
|
268
|
+
|
|
269
|
+
global_min = global_max = None
|
|
270
|
+
if data.ndim == 2:
|
|
271
|
+
global_min, global_max = _setup_2d_plot(
|
|
272
|
+
data=data,
|
|
273
|
+
ax=ax,
|
|
274
|
+
coord_names=coord_names,
|
|
275
|
+
kwargs=kwargs,
|
|
276
|
+
axis_kwargs=axis_kwargs,
|
|
277
|
+
min_percentile=min_percentile,
|
|
278
|
+
max_percentile=max_percentile,
|
|
279
|
+
t=t,
|
|
280
|
+
)
|
|
281
|
+
elif data.ndim == 3:
|
|
282
|
+
_setup_3d_plot(
|
|
283
|
+
data=data,
|
|
284
|
+
ax=ax,
|
|
285
|
+
coord_names=coord_names,
|
|
286
|
+
kwargs=kwargs,
|
|
287
|
+
kwargs_original=kwargs_original,
|
|
288
|
+
axis_kwargs=axis_kwargs,
|
|
289
|
+
min_percentile=min_percentile,
|
|
290
|
+
max_percentile=max_percentile,
|
|
291
|
+
t=t,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
ax.set_title(get_frame_title(data, 0, display_sdf_name, title, t))
|
|
295
|
+
|
|
296
|
+
window_boundaries = None
|
|
297
|
+
if move_window:
|
|
298
|
+
window_boundaries = calculate_window_boundaries(
|
|
299
|
+
data, kwargs.get("xlim"), kwargs["x"]
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def update(frame):
|
|
303
|
+
if clear_axes:
|
|
304
|
+
ax.clear()
|
|
305
|
+
# Set the xlim for each frame in the case of a moving window
|
|
306
|
+
if move_window:
|
|
307
|
+
kwargs["xlim"] = window_boundaries[frame]
|
|
308
|
+
|
|
309
|
+
plot = data.isel({t: frame}).plot(ax=ax, **kwargs)
|
|
310
|
+
ax.set_title(get_frame_title(data, frame, display_sdf_name, title, t))
|
|
311
|
+
_set_axes_labels(ax, axis_kwargs)
|
|
312
|
+
|
|
313
|
+
if data.ndim == 2 and "ylim" not in kwargs and global_min is not None:
|
|
314
|
+
ax.set_ylim(global_min, global_max)
|
|
315
|
+
|
|
316
|
+
return plot
|
|
317
|
+
|
|
318
|
+
return AnimationUnit(
|
|
319
|
+
update=update,
|
|
320
|
+
n_frames=N_frames,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
82
324
|
def animate(
|
|
83
325
|
data: xr.DataArray,
|
|
84
326
|
fps: float = 10,
|
|
@@ -86,99 +328,214 @@ def animate(
|
|
|
86
328
|
max_percentile: float = 100,
|
|
87
329
|
title: str | None = None,
|
|
88
330
|
display_sdf_name: bool = False,
|
|
331
|
+
move_window: bool = False,
|
|
332
|
+
t: str | None = None,
|
|
89
333
|
ax: plt.Axes | None = None,
|
|
90
334
|
**kwargs,
|
|
91
335
|
) -> FuncAnimation:
|
|
92
|
-
"""
|
|
336
|
+
"""
|
|
337
|
+
Generate an animation using an `xarray.DataArray`. The intended use
|
|
338
|
+
of this function is via `sdf_xarray.plotting.EpochAccessor.animate`.
|
|
93
339
|
|
|
94
340
|
Parameters
|
|
95
341
|
---------
|
|
96
342
|
data
|
|
97
|
-
|
|
343
|
+
DataArray containing the target data
|
|
98
344
|
fps
|
|
99
|
-
Frames per second for the animation
|
|
345
|
+
Frames per second for the animation
|
|
100
346
|
min_percentile
|
|
101
|
-
Minimum percentile of the data
|
|
347
|
+
Minimum percentile of the data
|
|
102
348
|
max_percentile
|
|
103
|
-
Maximum percentile of the data
|
|
349
|
+
Maximum percentile of the data
|
|
104
350
|
title
|
|
105
|
-
Custom title to add to the plot
|
|
351
|
+
Custom title to add to the plot
|
|
106
352
|
display_sdf_name
|
|
107
353
|
Display the sdf file name in the animation title
|
|
354
|
+
move_window
|
|
355
|
+
Update the ``xlim`` to be only values that are not NaNs at each time interval
|
|
356
|
+
t
|
|
357
|
+
Coordinate for t axis (the coordinate which will be animated over).
|
|
358
|
+
If ``None``, use ``data.dims[0]``
|
|
108
359
|
ax
|
|
109
|
-
Matplotlib axes on which to plot
|
|
360
|
+
Matplotlib axes on which to plot
|
|
110
361
|
kwargs
|
|
111
|
-
Keyword arguments to be passed to matplotlib
|
|
362
|
+
Keyword arguments to be passed to matplotlib
|
|
112
363
|
|
|
113
364
|
Examples
|
|
114
365
|
--------
|
|
115
|
-
>>>
|
|
366
|
+
>>> anim = animate(ds["Derived_Number_Density_Electron"])
|
|
367
|
+
>>> anim.save("animation.gif")
|
|
116
368
|
"""
|
|
117
369
|
import matplotlib.pyplot as plt # noqa: PLC0415
|
|
118
370
|
from matplotlib.animation import FuncAnimation # noqa: PLC0415
|
|
119
371
|
|
|
120
|
-
|
|
121
|
-
|
|
372
|
+
# Create plot if no ax is provided
|
|
122
373
|
if ax is None:
|
|
123
|
-
|
|
374
|
+
fig, ax = plt.subplots()
|
|
375
|
+
# Prevents figure from prematurely displaying in Jupyter notebook
|
|
376
|
+
plt.close(fig)
|
|
377
|
+
|
|
378
|
+
animation = _generate_animation(
|
|
379
|
+
data,
|
|
380
|
+
clear_axes=True,
|
|
381
|
+
min_percentile=min_percentile,
|
|
382
|
+
max_percentile=max_percentile,
|
|
383
|
+
title=title,
|
|
384
|
+
display_sdf_name=display_sdf_name,
|
|
385
|
+
move_window=move_window,
|
|
386
|
+
t=t,
|
|
387
|
+
ax=ax,
|
|
388
|
+
kwargs=kwargs,
|
|
389
|
+
)
|
|
124
390
|
|
|
125
|
-
|
|
126
|
-
|
|
391
|
+
return FuncAnimation(
|
|
392
|
+
ax.get_figure(),
|
|
393
|
+
animation.update,
|
|
394
|
+
frames=range(animation.n_frames),
|
|
395
|
+
interval=1000 / fps,
|
|
396
|
+
repeat=True,
|
|
397
|
+
)
|
|
127
398
|
|
|
128
|
-
# Initialise plot and set y-limits for 1D data
|
|
129
|
-
if data.ndim == 2:
|
|
130
|
-
kwargs.setdefault("x", "X_Grid_mid")
|
|
131
|
-
plot = data.isel(time=0).plot(ax=ax, **kwargs)
|
|
132
|
-
ax.set_title(get_frame_title(data, 0, display_sdf_name, title))
|
|
133
|
-
ax.set_ylim(global_min, global_max)
|
|
134
399
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
plt.colorbar(plot, ax=ax, label=f"{long_name} [${units}$]")
|
|
152
|
-
|
|
153
|
-
# check if there is a moving window by finding NaNs in the data
|
|
154
|
-
move_window = np.isnan(np.sum(data.values))
|
|
155
|
-
if move_window:
|
|
156
|
-
window_boundaries = calculate_window_boundaries(data, kwargs.get("xlim", False))
|
|
400
|
+
def animate_multiple(
|
|
401
|
+
*datasets: xr.DataArray,
|
|
402
|
+
datasets_kwargs: list[dict[str, Any]] | None = None,
|
|
403
|
+
fps: float = 10,
|
|
404
|
+
min_percentile: float = 0,
|
|
405
|
+
max_percentile: float = 100,
|
|
406
|
+
title: str | None = None,
|
|
407
|
+
display_sdf_name: bool = False,
|
|
408
|
+
move_window: bool = False,
|
|
409
|
+
t: str | None = None,
|
|
410
|
+
ax: plt.Axes | None = None,
|
|
411
|
+
**common_kwargs,
|
|
412
|
+
) -> FuncAnimation:
|
|
413
|
+
"""
|
|
414
|
+
Generate an animation using multiple `xarray.DataArray`. The intended use
|
|
415
|
+
of this function is via `sdf_xarray.dataset_accessor.EpochAccessor.animate_multiple`.
|
|
157
416
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
417
|
+
Parameters
|
|
418
|
+
---------
|
|
419
|
+
datasets
|
|
420
|
+
`xarray.DataArray` objects containing the data to be animated
|
|
421
|
+
datasets_kwargs
|
|
422
|
+
A list of dictionaries, following the same order as ``datasets``, containing
|
|
423
|
+
per-dataset matplotlib keyword arguments. The list does not need to be the same
|
|
424
|
+
length as ``datasets``; missing entries are initialised as empty dictionaries
|
|
425
|
+
fps
|
|
426
|
+
Frames per second for the animation
|
|
427
|
+
min_percentile
|
|
428
|
+
Minimum percentile of the data
|
|
429
|
+
max_percentile
|
|
430
|
+
Maximum percentile of the data
|
|
431
|
+
title
|
|
432
|
+
Custom title to add to the plot
|
|
433
|
+
display_sdf_name
|
|
434
|
+
Display the sdf file name in the animation title
|
|
435
|
+
move_window
|
|
436
|
+
Update the ``xlim`` to be only values that are not NaNs at each time interval
|
|
437
|
+
t
|
|
438
|
+
Coordinate for t axis (the coordinate which will be animated over). If ``None``,
|
|
439
|
+
use ``data.dims[0]``
|
|
440
|
+
ax
|
|
441
|
+
Matplotlib axes on which to plot
|
|
442
|
+
common_kwargs
|
|
443
|
+
Matplotlib keyword arguments applied to all datasets. These are overridden by
|
|
444
|
+
per-dataset entries in ``datasets_kwargs``
|
|
162
445
|
|
|
163
|
-
|
|
164
|
-
|
|
446
|
+
Examples
|
|
447
|
+
--------
|
|
448
|
+
>>> anim = animate_multiple(
|
|
449
|
+
ds["Derived_Number_Density_Electron"],
|
|
450
|
+
ds["Derived_Number_Density_Ion"],
|
|
451
|
+
datasets_kwargs=[{"label": "Electron"}, {"label": "Ion"}],
|
|
452
|
+
ylim=(0e27,4e27),
|
|
453
|
+
display_sdf_name=True,
|
|
454
|
+
ylabel="Derived Number Density [1/m$^3$]"
|
|
455
|
+
)
|
|
456
|
+
>>> anim.save("animation.gif")
|
|
457
|
+
"""
|
|
458
|
+
import matplotlib.pyplot as plt # noqa: PLC0415
|
|
459
|
+
from matplotlib.animation import FuncAnimation # noqa: PLC0415
|
|
165
460
|
|
|
166
|
-
|
|
167
|
-
|
|
461
|
+
if not datasets:
|
|
462
|
+
raise ValueError("At least one dataset must be provided")
|
|
168
463
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
464
|
+
# Create plot if no ax is provided
|
|
465
|
+
if ax is None:
|
|
466
|
+
fig, ax = plt.subplots()
|
|
467
|
+
# Prevents figure from prematurely displaying in Jupyter notebook
|
|
468
|
+
plt.close(fig)
|
|
469
|
+
|
|
470
|
+
n_datasets = len(datasets)
|
|
471
|
+
if datasets_kwargs is None:
|
|
472
|
+
# Initialise an empty series of dicts the same size as the number of datasets
|
|
473
|
+
datasets_kwargs = [{} for _ in range(n_datasets)]
|
|
474
|
+
else:
|
|
475
|
+
# The user might only want to use kwargs on some of the datasets so we make sure
|
|
476
|
+
# to initialise additional empty dicts and append them to the list
|
|
477
|
+
datasets_kwargs.extend({} for _ in range(n_datasets - len(datasets_kwargs)))
|
|
478
|
+
|
|
479
|
+
animations: list[AnimationUnit] = []
|
|
480
|
+
for da, kw in zip(datasets, datasets_kwargs):
|
|
481
|
+
animations.append(
|
|
482
|
+
_generate_animation(
|
|
483
|
+
da,
|
|
484
|
+
ax=ax,
|
|
485
|
+
min_percentile=min_percentile,
|
|
486
|
+
max_percentile=max_percentile,
|
|
487
|
+
title=title,
|
|
488
|
+
display_sdf_name=display_sdf_name,
|
|
489
|
+
move_window=move_window,
|
|
490
|
+
t=t,
|
|
491
|
+
# Per-dataset kwargs override common matplotlib kwargs
|
|
492
|
+
kwargs={**common_kwargs, **kw},
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
lengths = [anim.n_frames for anim in animations]
|
|
497
|
+
n_frames = min(lengths)
|
|
498
|
+
|
|
499
|
+
if len(set(lengths)) > 1:
|
|
500
|
+
warnings.warn(
|
|
501
|
+
"Datasets have different frame counts; truncating to the shortest",
|
|
502
|
+
stacklevel=2,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Render the legend if a label exists for any 2D dataset
|
|
506
|
+
show_legend = any(
|
|
507
|
+
"label" in kw and da.ndim == 2 for da, kw in zip(datasets, datasets_kwargs)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def update(frame):
|
|
511
|
+
ax.clear()
|
|
512
|
+
for anim in animations:
|
|
513
|
+
anim.update(frame)
|
|
514
|
+
if show_legend:
|
|
515
|
+
ax.legend(loc="upper right")
|
|
172
516
|
|
|
173
517
|
return FuncAnimation(
|
|
174
518
|
ax.get_figure(),
|
|
175
519
|
update,
|
|
176
|
-
frames=range(
|
|
520
|
+
frames=range(n_frames),
|
|
177
521
|
interval=1000 / fps,
|
|
178
522
|
repeat=True,
|
|
179
523
|
)
|
|
180
524
|
|
|
181
525
|
|
|
526
|
+
def show(anim):
|
|
527
|
+
"""Shows the FuncAnimation in a Jupyter notebook.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
anim
|
|
532
|
+
`matplotlib.animation.FuncAnimation`
|
|
533
|
+
"""
|
|
534
|
+
from IPython.display import HTML # noqa: PLC0415
|
|
535
|
+
|
|
536
|
+
return HTML(anim.to_jshtml())
|
|
537
|
+
|
|
538
|
+
|
|
182
539
|
@xr.register_dataarray_accessor("epoch")
|
|
183
540
|
class EpochAccessor:
|
|
184
541
|
def __init__(self, xarray_obj):
|
|
@@ -190,16 +547,21 @@ class EpochAccessor:
|
|
|
190
547
|
Parameters
|
|
191
548
|
----------
|
|
192
549
|
args
|
|
193
|
-
Positional arguments passed to :func:`
|
|
550
|
+
Positional arguments passed to :func:`animation`.
|
|
194
551
|
kwargs
|
|
195
|
-
Keyword arguments passed to :func:`
|
|
552
|
+
Keyword arguments passed to :func:`animation`.
|
|
196
553
|
|
|
197
554
|
Examples
|
|
198
555
|
--------
|
|
199
|
-
>>>
|
|
200
|
-
>>>
|
|
201
|
-
>>>
|
|
202
|
-
>>>
|
|
203
|
-
>>> ani.save("myfile.mp4")
|
|
556
|
+
>>> anim = ds["Electric_Field_Ey"].epoch.animate()
|
|
557
|
+
>>> anim.save("animation.gif")
|
|
558
|
+
>>> # Or in a jupyter notebook:
|
|
559
|
+
>>> anim.show()
|
|
204
560
|
"""
|
|
205
|
-
|
|
561
|
+
|
|
562
|
+
# Add anim.show() functionality
|
|
563
|
+
# anim.show() will display the animation in a jupyter notebook
|
|
564
|
+
anim = animate(self._obj, *args, **kwargs)
|
|
565
|
+
anim.show = MethodType(show, anim)
|
|
566
|
+
|
|
567
|
+
return anim
|
|
Binary file
|
sdf_xarray/sdf_interface.pyx
CHANGED
|
@@ -110,12 +110,12 @@ _CONSTANT_UNITS_RE = re.compile(r"(?P<name>.*) \((?P<units>.*)\)$")
|
|
|
110
110
|
cdef class Constant:
|
|
111
111
|
_id: str
|
|
112
112
|
name: str
|
|
113
|
-
data: int | str | float
|
|
113
|
+
data: int | str | float | bool
|
|
114
114
|
units: str | None
|
|
115
115
|
|
|
116
116
|
@staticmethod
|
|
117
117
|
cdef Constant from_block(str name, csdf.sdf_block_t* block):
|
|
118
|
-
data: int | str | float | double
|
|
118
|
+
data: int | str | float | double | bool
|
|
119
119
|
|
|
120
120
|
if block.datatype == csdf.SDF_DATATYPE_REAL4:
|
|
121
121
|
data = (<float*>block.const_value)[0]
|
|
@@ -125,6 +125,8 @@ cdef class Constant:
|
|
|
125
125
|
data = (<csdf.int32_t*>block.const_value)[0]
|
|
126
126
|
if block.datatype == csdf.SDF_DATATYPE_INTEGER8:
|
|
127
127
|
data = (<csdf.int64_t*>block.const_value)[0]
|
|
128
|
+
if block.datatype == csdf.SDF_DATATYPE_LOGICAL:
|
|
129
|
+
data = (<bint*>block.const_value)[0]
|
|
128
130
|
|
|
129
131
|
# There's no metadata with e.g. units, but there's a
|
|
130
132
|
# convention to put one in brackets at the end of the name,
|