sdf-xarray 0.4.0__cp314-cp314-macosx_10_13_x86_64.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.
- include/SDFC_14.4.7/sdf.h +804 -0
- include/SDFC_14.4.7/sdf_derived.h +34 -0
- include/SDFC_14.4.7/sdf_extension.h +36 -0
- include/SDFC_14.4.7/sdf_helper.h +46 -0
- include/SDFC_14.4.7/sdf_list_type.h +68 -0
- include/SDFC_14.4.7/sdf_vector_type.h +68 -0
- include/SDFC_14.4.7/stack_allocator.h +49 -0
- include/SDFC_14.4.7/uthash.h +963 -0
- lib/SDFC_14.4.7/SDFCConfig.cmake +18 -0
- lib/SDFC_14.4.7/SDFCConfigVersion.cmake +65 -0
- lib/SDFC_14.4.7/SDFCTargets-release.cmake +19 -0
- lib/SDFC_14.4.7/SDFCTargets.cmake +105 -0
- lib/SDFC_14.4.7/libsdfc.a +0 -0
- sdf_xarray/__init__.py +645 -0
- sdf_xarray/_version.py +34 -0
- sdf_xarray/csdf.pxd +127 -0
- sdf_xarray/dataset_accessor.py +71 -0
- sdf_xarray/download.py +87 -0
- sdf_xarray/plotting.py +293 -0
- sdf_xarray/sdf_interface.cpython-314-darwin.so +0 -0
- sdf_xarray/sdf_interface.pyx +342 -0
- sdf_xarray-0.4.0.dist-info/METADATA +151 -0
- sdf_xarray-0.4.0.dist-info/RECORD +26 -0
- sdf_xarray-0.4.0.dist-info/WHEEL +6 -0
- sdf_xarray-0.4.0.dist-info/entry_points.txt +3 -0
- sdf_xarray-0.4.0.dist-info/licenses/LICENCE +28 -0
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
|
|
Binary file
|