sdf-xarray 0.2.3__tar.gz → 0.2.5__tar.gz

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 (114) hide show
  1. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/CITATION.cff +1 -0
  2. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/PKG-INFO +2 -2
  3. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/README.md +1 -0
  4. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/pyproject.toml +2 -2
  5. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/src/sdf_xarray/__init__.py +12 -3
  6. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/src/sdf_xarray/_version.py +2 -2
  7. sdf_xarray-0.2.5/src/sdf_xarray/plotting.py +205 -0
  8. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/src/sdf_xarray/sdf_interface.pyx +1 -1
  9. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/0000.sdf +0 -0
  10. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/0001.sdf +0 -0
  11. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/0002.sdf +0 -0
  12. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/0003.sdf +0 -0
  13. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/0004.sdf +0 -0
  14. sdf_xarray-0.2.5/tests/example_files_2D_moving_window/input.deck +63 -0
  15. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/test_basic.py +42 -1
  16. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/test_cython.py +1 -1
  17. sdf_xarray-0.2.5/tests/test_epoch_accessor.py +183 -0
  18. sdf_xarray-0.2.3/src/sdf_xarray/plotting.py +0 -184
  19. sdf_xarray-0.2.3/tests/test_epoch_accessor.py +0 -39
  20. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.github/workflows/black.yml +0 -0
  21. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.github/workflows/build_publish.yml +0 -0
  22. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.github/workflows/lint.yml +0 -0
  23. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.github/workflows/tests.yml +0 -0
  24. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.gitignore +0 -0
  25. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.gitmodules +0 -0
  26. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/.readthedocs.yaml +0 -0
  27. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/BEAM.png +0 -0
  28. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/CMakeLists.txt +0 -0
  29. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/CONTRIBUTING.md +0 -0
  30. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/LICENCE +0 -0
  31. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/PlasmaFAIR.svg +0 -0
  32. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/.gitignore +0 -0
  33. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/_templates/custom-class-template.rst +0 -0
  34. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/_templates/custom-module-template.rst +0 -0
  35. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/api.rst +0 -0
  36. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/conf.py +0 -0
  37. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/contributing.rst +0 -0
  38. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/getting_started.rst +0 -0
  39. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/index.rst +0 -0
  40. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/key_functionality.rst +0 -0
  41. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/make.bat +0 -0
  42. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0000.sdf +0 -0
  43. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0001.sdf +0 -0
  44. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0002.sdf +0 -0
  45. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0003.sdf +0 -0
  46. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0004.sdf +0 -0
  47. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0005.sdf +0 -0
  48. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0006.sdf +0 -0
  49. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0007.sdf +0 -0
  50. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0008.sdf +0 -0
  51. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0009.sdf +0 -0
  52. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0010.sdf +0 -0
  53. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0011.sdf +0 -0
  54. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0012.sdf +0 -0
  55. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0013.sdf +0 -0
  56. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0014.sdf +0 -0
  57. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0015.sdf +0 -0
  58. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0016.sdf +0 -0
  59. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0017.sdf +0 -0
  60. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0018.sdf +0 -0
  61. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0019.sdf +0 -0
  62. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0020.sdf +0 -0
  63. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0021.sdf +0 -0
  64. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0022.sdf +0 -0
  65. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0023.sdf +0 -0
  66. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0024.sdf +0 -0
  67. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0025.sdf +0 -0
  68. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0026.sdf +0 -0
  69. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0027.sdf +0 -0
  70. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0028.sdf +0 -0
  71. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0029.sdf +0 -0
  72. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0030.sdf +0 -0
  73. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0031.sdf +0 -0
  74. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0032.sdf +0 -0
  75. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0033.sdf +0 -0
  76. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0034.sdf +0 -0
  77. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0035.sdf +0 -0
  78. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0036.sdf +0 -0
  79. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0037.sdf +0 -0
  80. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0038.sdf +0 -0
  81. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0039.sdf +0 -0
  82. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/0040.sdf +0 -0
  83. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/deck.status +0 -0
  84. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/epoch1d.dat +0 -0
  85. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/input.deck +0 -0
  86. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/normal.visit +0 -0
  87. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/tutorial_dataset_1d/restart.visit +0 -0
  88. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/docs/unit_conversion.rst +0 -0
  89. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/src/sdf_xarray/csdf.pxd +0 -0
  90. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_array_no_grids/0000.sdf +0 -0
  91. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_array_no_grids/0001.sdf +0 -0
  92. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_array_no_grids/README.md +0 -0
  93. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_array_no_grids/input.deck +0 -0
  94. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_dist_fn/0000.sdf +0 -0
  95. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_dist_fn/0001.sdf +0 -0
  96. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_dist_fn/0002.sdf +0 -0
  97. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_dist_fn/input.deck +0 -0
  98. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0000.sdf +0 -0
  99. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0001.sdf +0 -0
  100. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0002.sdf +0 -0
  101. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0003.sdf +0 -0
  102. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0004.sdf +0 -0
  103. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0005.sdf +0 -0
  104. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0006.sdf +0 -0
  105. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0007.sdf +0 -0
  106. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0008.sdf +0 -0
  107. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0009.sdf +0 -0
  108. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/0010.sdf +0 -0
  109. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/README.md +0 -0
  110. {sdf_xarray-0.2.3/tests/example_files → sdf_xarray-0.2.5/tests/example_files_1D}/input.deck +0 -0
  111. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_mismatched_files/0000.sdf +0 -0
  112. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_mismatched_files/0001.sdf +0 -0
  113. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/tests/example_mismatched_files/0002.sdf +0 -0
  114. {sdf_xarray-0.2.3 → sdf_xarray-0.2.5}/uv.lock +0 -0
@@ -16,4 +16,5 @@ authors:
16
16
  given-names: Shaun
17
17
  orcid: 'https://orcid.org/0009-0005-0693-030X'
18
18
  affiliation: University of York
19
+ doi: 10.5281/zenodo.15351323
19
20
  date-released: '2024-07-25'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sdf-xarray
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Provides a backend for xarray to read SDF files as created by the EPOCH plasma PIC code.
5
5
  Author-Email: Peter Hill <peter.hill@york.ac.uk>, Joel Adams <joel.adams@york.ac.uk>, Shaun Doherty <shaun.doherty@york.ac.uk>
6
6
  License: Copyright 2024, Peter Hill, Joel Adams, epochpic team
@@ -45,7 +45,6 @@ Requires-Python: >=3.10
45
45
  Requires-Dist: numpy>=2.0.0
46
46
  Requires-Dist: xarray>=2024.1.0
47
47
  Requires-Dist: dask>=2024.7.1
48
- Requires-Dist: cython>=3.0
49
48
  Provides-Extra: docs
50
49
  Requires-Dist: sphinx>=5.3; extra == "docs"
51
50
  Requires-Dist: sphinx_autodoc_typehints>=1.19; extra == "docs"
@@ -78,6 +77,7 @@ Description-Content-Type: text/markdown
78
77
 
79
78
  ![Dynamic TOML Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fepochpic%2Fsdf-xarray%2Frefs%2Fheads%2Fmain%2Fpyproject.toml&query=%24.project.requires-python&label=python&logo=python)
80
79
  [![Available on PyPI](https://img.shields.io/pypi/v/sdf-xarray?color=blue&logo=pypi)](https://pypi.org/project/sdf-xarray/)
80
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15351323.svg)](https://doi.org/10.5281/zenodo.15351323)
81
81
  ![Build/Publish](https://github.com/epochpic/sdf-xarray/actions/workflows/build_publish.yml/badge.svg)
82
82
  ![Tests](https://github.com/epochpic/sdf-xarray/actions/workflows/tests.yml/badge.svg)
83
83
  [![Read the Docs](https://img.shields.io/readthedocs/sdf-xarray?logo=readthedocs&link=https%3A%2F%2Fsdf-xarray.readthedocs.io%2F)](https://sdf-xarray.readthedocs.io)
@@ -2,6 +2,7 @@
2
2
 
3
3
  ![Dynamic TOML Badge](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fepochpic%2Fsdf-xarray%2Frefs%2Fheads%2Fmain%2Fpyproject.toml&query=%24.project.requires-python&label=python&logo=python)
4
4
  [![Available on PyPI](https://img.shields.io/pypi/v/sdf-xarray?color=blue&logo=pypi)](https://pypi.org/project/sdf-xarray/)
5
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15351323.svg)](https://doi.org/10.5281/zenodo.15351323)
5
6
  ![Build/Publish](https://github.com/epochpic/sdf-xarray/actions/workflows/build_publish.yml/badge.svg)
6
7
  ![Tests](https://github.com/epochpic/sdf-xarray/actions/workflows/tests.yml/badge.svg)
7
8
  [![Read the Docs](https://img.shields.io/readthedocs/sdf-xarray?logo=readthedocs&link=https%3A%2F%2Fsdf-xarray.readthedocs.io%2F)](https://sdf-xarray.readthedocs.io)
@@ -3,7 +3,7 @@ requires = [
3
3
  "scikit-build-core>=0.3",
4
4
  "setuptools_scm",
5
5
  "numpy>=2.0.0",
6
- "cython>=3.0",
6
+ "cython~=3.0",
7
7
  ]
8
8
  build-backend = "scikit_build_core.build"
9
9
 
@@ -22,7 +22,6 @@ dependencies = [
22
22
  "numpy>=2.0.0",
23
23
  "xarray>=2024.1.0",
24
24
  "dask>=2024.7.1",
25
- "cython>=3.0",
26
25
  ]
27
26
  description = "Provides a backend for xarray to read SDF files as created by the EPOCH plasma PIC code."
28
27
  classifiers = [
@@ -105,4 +104,5 @@ extend-select = [
105
104
  ignore = [
106
105
  "PLR2004", # magic-comparison
107
106
  "B9", # flake8-bugbear opinionated warnings
107
+ "PLR0913", # remove maximum number of arguments in a function
108
108
  ]
@@ -253,9 +253,18 @@ class SDFDataStore(AbstractDataStore):
253
253
  def load(self): # noqa: PLR0912, PLR0915
254
254
  # Drop any requested variables
255
255
  if self.drop_variables:
256
+ # Build a mapping from underscored names to real variable names
257
+ name_map = {_rename_with_underscore(var): var for var in self.ds.variables}
258
+
256
259
  for variable in self.drop_variables:
257
- # TODO: nicer error handling
258
- self.ds.variables.pop(variable)
260
+ key = _rename_with_underscore(variable)
261
+ original_name = name_map.get(key)
262
+
263
+ if original_name is None:
264
+ raise KeyError(
265
+ f"Variable '{variable}' not found (interpreted as '{key}')."
266
+ )
267
+ self.ds.variables.pop(original_name)
259
268
 
260
269
  # These two dicts are global metadata about the run or file
261
270
  attrs = {**self.ds.header, **self.ds.run_info}
@@ -434,7 +443,7 @@ class SDFEntrypoint(BackendEntrypoint):
434
443
 
435
444
  description = "Use .sdf files in Xarray"
436
445
 
437
- url = "https://epochpic.github.io/documentation/visualising_output/python.html"
446
+ url = "https://epochpic.github.io/documentation/visualising_output/python_beam.html"
438
447
 
439
448
 
440
449
  class SDFPreprocess:
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.3'
21
- __version_tuple__ = version_tuple = (0, 2, 3)
20
+ __version__ = version = '0.2.5'
21
+ __version_tuple__ = version_tuple = (0, 2, 5)
@@ -0,0 +1,205 @@
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
+
13
+ def get_frame_title(
14
+ data: xr.DataArray,
15
+ frame: int,
16
+ display_sdf_name: bool = False,
17
+ title_custom: str | None = None,
18
+ ) -> str:
19
+ """Generate the title for a frame"""
20
+ # Adds custom text to the start of the title, if specified
21
+ title_custom = "" if title_custom is None else f"{title_custom}, "
22
+ # Adds the time and associated units to the title
23
+ time = data["time"][frame].to_numpy()
24
+
25
+ time_units = data["time"].attrs.get("units", False)
26
+ time_units_formatted = f" [{time_units}]" if time_units else ""
27
+ title_time = f"time = {time:.2e}{time_units_formatted}"
28
+
29
+ # Adds sdf name to the title, if specifed
30
+ title_sdf = f", {frame:04d}.sdf" if display_sdf_name else ""
31
+ return f"{title_custom}{title_time}{title_sdf}"
32
+
33
+
34
+ def calculate_window_boundaries(
35
+ data: xr.DataArray, xlim: tuple[float, float] | False = False
36
+ ) -> np.ndarray:
37
+ """Calculate the bounderies a moving window frame. If the user specifies xlim, this will
38
+ be used as the initial bounderies and the window will move along acordingly.
39
+ """
40
+ x_grid = data["X_Grid_mid"].values
41
+ x_half_cell = (x_grid[1] - x_grid[0]) / 2
42
+ N_frames = data["time"].size
43
+
44
+ # Find the window bounderies by finding the first and last non-NaN values in the 0th lineout
45
+ # along the x-axis.
46
+ window_boundaries = np.zeros((N_frames, 2))
47
+ for i in range(N_frames):
48
+ # Check if data is 1D
49
+ if data.ndim == 2:
50
+ target_lineout = data[i].values
51
+ # Check if data is 2D
52
+ if data.ndim == 3:
53
+ target_lineout = data[i, :, 0].values
54
+ x_grid_non_nan = x_grid[~np.isnan(target_lineout)]
55
+ window_boundaries[i, 0] = x_grid_non_nan[0] - x_half_cell
56
+ window_boundaries[i, 1] = x_grid_non_nan[-1] + x_half_cell
57
+
58
+ # User's choice for initial window edge supercides the one calculated
59
+ if xlim:
60
+ window_boundaries = window_boundaries + xlim - window_boundaries[0]
61
+ return window_boundaries
62
+
63
+
64
+ def compute_global_limits(
65
+ data: xr.DataArray,
66
+ min_percentile: float = 0,
67
+ max_percentile: float = 100,
68
+ ) -> tuple[float, float]:
69
+ """Remove all NaN values from the target data to calculate the global minimum and maximum of the data.
70
+ User defined percentiles can remove extreme outliers.
71
+ """
72
+
73
+ # Removes NaN values, needed for moving windows
74
+ values_no_nan = data.values[~np.isnan(data.values)]
75
+
76
+ # Finds the global minimum and maximum of the plot, based on the percentile of the data
77
+ global_min = np.percentile(values_no_nan, min_percentile)
78
+ global_max = np.percentile(values_no_nan, max_percentile)
79
+ return global_min, global_max
80
+
81
+
82
+ def animate(
83
+ data: xr.DataArray,
84
+ fps: float = 10,
85
+ min_percentile: float = 0,
86
+ max_percentile: float = 100,
87
+ title: str | None = None,
88
+ display_sdf_name: bool = False,
89
+ ax: plt.Axes | None = None,
90
+ **kwargs,
91
+ ) -> FuncAnimation:
92
+ """Generate an animation
93
+
94
+ Parameters
95
+ ---------
96
+ data
97
+ The dataarray containing the target data
98
+ fps
99
+ Frames per second for the animation (default: 10)
100
+ min_percentile
101
+ Minimum percentile of the data (default: 0)
102
+ max_percentile
103
+ Maximum percentile of the data (default: 100)
104
+ title
105
+ Custom title to add to the plot.
106
+ display_sdf_name
107
+ Display the sdf file name in the animation title
108
+ ax
109
+ Matplotlib axes on which to plot.
110
+ kwargs
111
+ Keyword arguments to be passed to matplotlib.
112
+
113
+ Examples
114
+ --------
115
+ >>> dataset["Derived_Number_Density_Electron"].epoch.animate()
116
+ """
117
+ import matplotlib.pyplot as plt
118
+ from matplotlib.animation import FuncAnimation
119
+
120
+ kwargs_original = kwargs.copy()
121
+
122
+ if ax is None:
123
+ _, ax = plt.subplots()
124
+
125
+ N_frames = data["time"].size
126
+ global_min, global_max = compute_global_limits(data, min_percentile, max_percentile)
127
+
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
+
135
+ # Initilise plot and set colour bar for 2D data
136
+ if data.ndim == 3:
137
+ kwargs["norm"] = plt.Normalize(vmin=global_min, vmax=global_max)
138
+ kwargs["add_colorbar"] = False
139
+ # Set default x and y coordinates for 2D data if not provided
140
+ kwargs.setdefault("x", "X_Grid_mid")
141
+ kwargs.setdefault("y", "Y_Grid_mid")
142
+
143
+ # Initialize the plot with the first timestep
144
+ plot = data.isel(time=0).plot(ax=ax, **kwargs)
145
+ ax.set_title(get_frame_title(data, 0, display_sdf_name, title))
146
+
147
+ # Add colorbar
148
+ if kwargs_original.get("add_colorbar", True):
149
+ long_name = data.attrs.get("long_name")
150
+ units = data.attrs.get("units")
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))
157
+
158
+ def update(frame):
159
+ # Set the xlim for each frame in the case of a moving window
160
+ if move_window:
161
+ kwargs["xlim"] = window_boundaries[frame]
162
+
163
+ # Update plot for the new frame
164
+ ax.clear()
165
+
166
+ data.isel(time=frame).plot(ax=ax, **kwargs)
167
+ ax.set_title(get_frame_title(data, frame, display_sdf_name, title))
168
+
169
+ # Update y-limits for 1D data
170
+ if data.ndim == 2:
171
+ ax.set_ylim(global_min, global_max)
172
+
173
+ return FuncAnimation(
174
+ ax.get_figure(),
175
+ update,
176
+ frames=range(N_frames),
177
+ interval=1000 / fps,
178
+ repeat=True,
179
+ )
180
+
181
+
182
+ @xr.register_dataarray_accessor("epoch")
183
+ class EpochAccessor:
184
+ def __init__(self, xarray_obj):
185
+ self._obj = xarray_obj
186
+
187
+ def animate(self, *args, **kwargs) -> FuncAnimation:
188
+ """Generate animations of Epoch data.
189
+
190
+ Parameters
191
+ ----------
192
+ args
193
+ Positional arguments passed to :func:`generate_animation`.
194
+ kwargs
195
+ Keyword arguments passed to :func:`generate_animation`.
196
+
197
+ Examples
198
+ --------
199
+ >>> import xarray as xr
200
+ >>> from sdf_xarray import SDFPreprocess
201
+ >>> ds = xr.open_mfdataset("*.sdf", preprocess=SDFPreprocess())
202
+ >>> ani = ds["Electric_Field_Ey"].epoch.animate()
203
+ >>> ani.save("myfile.mp4")
204
+ """
205
+ return animate(self._obj, *args, **kwargs)
@@ -39,7 +39,7 @@ cdef class Block:
39
39
 
40
40
  @dataclasses.dataclass
41
41
  cdef class Variable(Block):
42
- units: tuple[str] | None
42
+ units: str | None
43
43
  mult: float | None
44
44
  grid: str | None
45
45
  grid_mid: str | None
@@ -0,0 +1,63 @@
1
+ begin:control
2
+ nx = 100
3
+ ny = 100
4
+
5
+ # final time of simulation
6
+ t_end = 10e-9
7
+
8
+ # size of domain
9
+ x_min = 0
10
+ x_end = 1
11
+
12
+ y_min = 0
13
+ y_max = 1
14
+
15
+ stdout_frequency = 10
16
+ end:control
17
+
18
+
19
+ begin:boundaries
20
+ bc_x_min = simple_outflow
21
+ bc_x_max = simple_outflow
22
+ bc_y_min = periodic
23
+ bc_y_max = periodic
24
+ end:boundaries
25
+
26
+
27
+ begin:window
28
+ move_window = T
29
+ window_start_time = 0
30
+ bc_x_min_after_move = simple_outflow
31
+ bc_x_max_after_move = simple_outflow
32
+ window_v_x = 2e8
33
+ end:window
34
+
35
+ begin:constant
36
+ x0 = 1.31
37
+ y0 = 0.5
38
+ r0 = 0.4^2
39
+ r2 = (x - x0)^2 + (y - y0)^2
40
+ end:constant
41
+
42
+ begin:species
43
+ # electron
44
+ name = electron
45
+ charge = -1.0
46
+ mass = 1.0
47
+ nparticles_per_cell = 5
48
+
49
+ number_density = if(abs(x-x0) lt 0.3, 2, 1)
50
+ number_density = if(abs(y-y0) lt 0.2, number_density(electron), 1)
51
+
52
+ temperature_ev = 0
53
+ end:species
54
+
55
+
56
+ begin:output
57
+ name = normal
58
+ dt_snapshot = 10e-9/4
59
+
60
+ # Properties on grid
61
+ grid = always
62
+ number_density = always + species + no_sum
63
+ end:output
@@ -5,7 +5,7 @@ import xarray as xr
5
5
 
6
6
  from sdf_xarray import SDFPreprocess, _process_latex_name, open_mfdataset
7
7
 
8
- EXAMPLE_FILES_DIR = pathlib.Path(__file__).parent / "example_files"
8
+ EXAMPLE_FILES_DIR = pathlib.Path(__file__).parent / "example_files_1D"
9
9
  EXAMPLE_MISMATCHED_FILES_DIR = (
10
10
  pathlib.Path(__file__).parent / "example_mismatched_files"
11
11
  )
@@ -185,3 +185,44 @@ def test_3d_distribution_function():
185
185
  with xr.open_dataset(EXAMPLE_3D_DIST_FN / "0000.sdf") as df:
186
186
  distribution_function = "dist_fn_x_px_py_Electron"
187
187
  assert df[distribution_function].shape == (16, 20, 20)
188
+
189
+
190
+ def test_drop_variables():
191
+ with xr.open_dataset(
192
+ EXAMPLE_FILES_DIR / "0000.sdf", drop_variables=["Electric_Field_Ex"]
193
+ ) as df:
194
+ assert "Electric_Field_Ex" not in df
195
+
196
+
197
+ def test_drop_variables_multiple():
198
+ with xr.open_dataset(
199
+ EXAMPLE_FILES_DIR / "0000.sdf",
200
+ drop_variables=["Electric_Field_Ex", "Electric_Field_Ey"],
201
+ ) as df:
202
+ assert "Electric_Field_Ex" not in df
203
+ assert "Electric_Field_Ey" not in df
204
+
205
+
206
+ def test_drop_variables_original():
207
+ with xr.open_dataset(
208
+ EXAMPLE_FILES_DIR / "0000.sdf",
209
+ drop_variables=["Electric_Field/Ex", "Electric_Field/Ey"],
210
+ ) as df:
211
+ assert "Electric_Field_Ex" not in df
212
+ assert "Electric_Field_Ey" not in df
213
+
214
+
215
+ def test_drop_variables_mixed():
216
+ with xr.open_dataset(
217
+ EXAMPLE_FILES_DIR / "0000.sdf",
218
+ drop_variables=["Electric_Field/Ex", "Electric_Field_Ey"],
219
+ ) as df:
220
+ assert "Electric_Field_Ex" not in df
221
+ assert "Electric_Field_Ey" not in df
222
+
223
+
224
+ def test_erroring_drop_variables():
225
+ with pytest.raises(KeyError):
226
+ xr.open_dataset(
227
+ EXAMPLE_FILES_DIR / "0000.sdf", drop_variables=["Electric_Field/E"]
228
+ )
@@ -6,7 +6,7 @@ import pytest
6
6
 
7
7
  from sdf_xarray import SDFFile
8
8
 
9
- EXAMPLE_FILES_DIR = pathlib.Path(__file__).parent / "example_files"
9
+ EXAMPLE_FILES_DIR = pathlib.Path(__file__).parent / "example_files_1D"
10
10
 
11
11
 
12
12
  def test_sdffile():
@@ -0,0 +1,183 @@
1
+ import pathlib
2
+ import tempfile
3
+
4
+ import matplotlib as mpl
5
+ import numpy as np
6
+ import pytest
7
+ import xarray as xr
8
+ from matplotlib.animation import PillowWriter
9
+
10
+ import sdf_xarray.plotting as sxp
11
+ from sdf_xarray import SDFPreprocess
12
+
13
+ mpl.use("Agg")
14
+
15
+ EXAMPLE_FILES_DIR_1D = pathlib.Path(__file__).parent / "example_files_1D"
16
+ EXAMPLE_FILES_DIR_2D_MW = (
17
+ pathlib.Path(__file__).parent / "example_files_2D_moving_window"
18
+ )
19
+
20
+
21
+ def test_animation_accessor():
22
+ array = xr.DataArray(
23
+ [1, 2, 3],
24
+ dims=["x"],
25
+ coords={"x": [0, 1, 2]},
26
+ attrs={"long_name": "Test Array", "units": "m"},
27
+ )
28
+ assert hasattr(array, "epoch")
29
+ assert hasattr(array.epoch, "animate")
30
+
31
+
32
+ def test_animate_headless():
33
+ with xr.open_mfdataset(
34
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
35
+ ) as ds:
36
+ anim = ds["Derived_Number_Density_electron"].epoch.animate()
37
+
38
+ # Specify a custom writable temporary directory
39
+ with tempfile.TemporaryDirectory() as temp_dir:
40
+ temp_file_path = f"{temp_dir}/output.gif"
41
+ try:
42
+ anim.save(temp_file_path, writer=PillowWriter(fps=2))
43
+ except Exception as e:
44
+ pytest.fail(f"animate().save() failed in headless mode: {e}")
45
+
46
+
47
+ def test_get_frame_title_no_optional_params():
48
+ with xr.open_mfdataset(
49
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
50
+ ) as ds:
51
+ data = ds["Derived_Number_Density_electron"]
52
+ expected_result = "time = 5.47e-14 [s]"
53
+ result = sxp.get_frame_title(data, 0)
54
+ assert expected_result == result
55
+
56
+
57
+ def test_get_frame_title_sdf_name():
58
+ with xr.open_mfdataset(
59
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
60
+ ) as ds:
61
+ data = ds["Derived_Number_Density_electron"]
62
+ expected_result = "time = 5.47e-14 [s], 0000.sdf"
63
+ result = sxp.get_frame_title(data, 0, display_sdf_name=True)
64
+ assert expected_result == result
65
+
66
+
67
+ def test_get_frame_title_custom_title():
68
+ with xr.open_mfdataset(
69
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
70
+ ) as ds:
71
+ data = ds["Derived_Number_Density_electron"]
72
+ expected_result = "Test Title, time = 5.47e-14 [s]"
73
+ result = sxp.get_frame_title(data, 0, title_custom="Test Title")
74
+ assert expected_result == result
75
+
76
+
77
+ def test_get_frame_title_custom_title_and_sdf_name():
78
+ with xr.open_mfdataset(
79
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
80
+ ) as ds:
81
+ data = ds["Derived_Number_Density_electron"]
82
+ expected_result = "Test Title, time = 5.47e-14 [s], 0000.sdf"
83
+ result = sxp.get_frame_title(
84
+ data, 0, display_sdf_name=True, title_custom="Test Title"
85
+ )
86
+ assert expected_result == result
87
+
88
+
89
+ def test_calculate_window_boundaries_1D():
90
+ with xr.open_mfdataset(
91
+ EXAMPLE_FILES_DIR_2D_MW.glob("*.sdf"),
92
+ preprocess=SDFPreprocess(),
93
+ combine="nested",
94
+ ) as ds:
95
+ data = ds["Derived_Number_Density_electron"][:, :, 50]
96
+ expected_result = np.array(
97
+ [[0, 1], [0.49, 1.49], [0.99, 1.99], [1.49, 2.49], [1.99, 2.99]]
98
+ )
99
+ result = sxp.calculate_window_boundaries(data)
100
+ assert result == pytest.approx(expected_result, abs=0.1)
101
+
102
+
103
+ def test_calculate_window_boundaries_2D():
104
+ with xr.open_mfdataset(
105
+ EXAMPLE_FILES_DIR_2D_MW.glob("*.sdf"),
106
+ preprocess=SDFPreprocess(),
107
+ combine="nested",
108
+ ) as ds:
109
+ data = ds["Derived_Number_Density_electron"]
110
+ expected_result = np.array(
111
+ [[0, 1], [0.49, 1.49], [0.99, 1.99], [1.49, 2.49], [1.99, 2.99]]
112
+ )
113
+ result = sxp.calculate_window_boundaries(data)
114
+ assert result == pytest.approx(expected_result, abs=0.1)
115
+
116
+
117
+ def test_calculate_window_boundaries_1D_xlim():
118
+ with xr.open_mfdataset(
119
+ EXAMPLE_FILES_DIR_2D_MW.glob("*.sdf"),
120
+ preprocess=SDFPreprocess(),
121
+ combine="nested",
122
+ ) as ds:
123
+ data = ds["Derived_Number_Density_electron"][:, :, 50]
124
+ expected_result = np.array(
125
+ [[0.1, 0.9], [0.59, 1.39], [1.09, 1.89], [1.59, 2.39], [2.09, 2.89]]
126
+ )
127
+ result = sxp.calculate_window_boundaries(data, xlim=(0.1, 0.9))
128
+ assert result == pytest.approx(expected_result, abs=0.1)
129
+
130
+
131
+ def test_calculate_window_boundaries_2D_xlim():
132
+ with xr.open_mfdataset(
133
+ EXAMPLE_FILES_DIR_2D_MW.glob("*.sdf"),
134
+ preprocess=SDFPreprocess(),
135
+ combine="nested",
136
+ ) as ds:
137
+ data = ds["Derived_Number_Density_electron"]
138
+ expected_result = np.array(
139
+ [[0.1, 0.9], [0.59, 1.39], [1.09, 1.89], [1.59, 2.39], [2.09, 2.89]]
140
+ )
141
+ result = sxp.calculate_window_boundaries(data, xlim=(0.1, 0.9))
142
+ assert result == pytest.approx(expected_result, abs=0.1)
143
+
144
+
145
+ def test_compute_global_limits():
146
+ with xr.open_mfdataset(
147
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
148
+ ) as ds:
149
+ result_min, result_max = sxp.compute_global_limits(
150
+ ds["Derived_Number_Density_electron"]
151
+ )
152
+ expected_result_min = 8.07e19
153
+ expected_result_max = 1.17e20
154
+ assert result_min == pytest.approx(expected_result_min, abs=1e18)
155
+ assert result_max == pytest.approx(expected_result_max, abs=1e19)
156
+
157
+
158
+ def test_compute_global_limits_percentile():
159
+ with xr.open_mfdataset(
160
+ EXAMPLE_FILES_DIR_1D.glob("*.sdf"), preprocess=SDFPreprocess()
161
+ ) as ds:
162
+ result_min, result_max = sxp.compute_global_limits(
163
+ ds["Derived_Number_Density_electron"], 40, 45
164
+ )
165
+ expected_result_min = 9.84e19
166
+ expected_result_max = 9.94e19
167
+ assert result_min == pytest.approx(expected_result_min, abs=1e18)
168
+ assert result_max == pytest.approx(expected_result_max, abs=1e18)
169
+
170
+
171
+ def test_compute_global_limits_NaNs():
172
+ with xr.open_mfdataset(
173
+ EXAMPLE_FILES_DIR_2D_MW.glob("*.sdf"),
174
+ preprocess=SDFPreprocess(),
175
+ combine="nested",
176
+ ) as ds:
177
+ result_min, result_max = sxp.compute_global_limits(
178
+ ds["Derived_Number_Density_electron"]
179
+ )
180
+ expected_result_min = 5.51e-1
181
+ expected_result_max = 2.70
182
+ assert result_min == pytest.approx(expected_result_min, abs=1e-2)
183
+ assert result_max == pytest.approx(expected_result_max, abs=1e-1)
@@ -1,184 +0,0 @@
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
-
13
- def get_frame_title(data: xr.DataArray, frame: int, display_sdf_name: bool) -> str:
14
- """Generate the title for a frame"""
15
- sdf_name = f", {frame:04d}.sdf" if display_sdf_name else ""
16
- time = data["time"][frame].to_numpy()
17
- return f"t = {time:.2e}s{sdf_name}"
18
-
19
-
20
- def calculate_window_velocity_and_edges(
21
- data: xr.DataArray, x_axis_coord: str
22
- ) -> tuple[float, tuple[float, float], np.ndarray]:
23
- """Calculate the moving window's velocity and initial edges.
24
-
25
- 1. Finds a lineout of the target atribute in the x coordinate of the first frame
26
- 2. Removes the NaN values to isolate the simulation window
27
- 3. Produces the index size of the window, indexed at zero
28
- 4. Uses distance moved and final time of the simulation to calculate velocity and initial xlims
29
- """
30
- time_since_start = data["time"].values - data["time"].values[0]
31
- initial_window_edge = (0, 0)
32
- target_lineout = data.values[0, :, 0]
33
- target_lineout_window = target_lineout[~np.isnan(target_lineout)]
34
- x_grid = data[x_axis_coord].values
35
- window_size_index = target_lineout_window.size - 1
36
-
37
- velocity_window = (x_grid[-1] - x_grid[window_size_index]) / time_since_start[-1]
38
- initial_window_edge = (x_grid[0], x_grid[window_size_index])
39
- return velocity_window, initial_window_edge, time_since_start
40
-
41
-
42
- def compute_global_limits(data: xr.DataArray) -> tuple[float, float]:
43
- """Remove all NaN values from the target data to calculate the 1st and 99th percentiles,
44
- excluding extreme outliers.
45
- """
46
- values_no_nan = data.values[~np.isnan(data.values)]
47
- global_min = np.percentile(values_no_nan, 1)
48
- global_max = np.percentile(values_no_nan, 99)
49
- return global_min, global_max
50
-
51
-
52
- def is_1d(data: xr.DataArray) -> bool:
53
- """Check if the data is 1D."""
54
- return len(data.shape) == 2
55
-
56
-
57
- def is_2d(data: xr.DataArray) -> bool:
58
- """Check if the data is 2D or 3D."""
59
- return len(data.shape) == 3
60
-
61
-
62
- def generate_animation(
63
- data: xr.DataArray,
64
- display_sdf_name: bool = False,
65
- fps: int = 10,
66
- move_window: bool = False,
67
- ax: plt.Axes | None = None,
68
- **kwargs,
69
- ) -> FuncAnimation:
70
- """Generate an animation
71
-
72
- Parameters
73
- ---------
74
- dataset
75
- The dataset containing the simulation data
76
- target_attribute
77
- The attribute to plot for each timestep
78
- display_sdf_name
79
- Display the sdf file name in the animation title
80
- fps
81
- Frames per second for the animation (default: 10)
82
- move_window
83
- If the simulation has a moving window, the animation will move along
84
- with it (default: False)
85
- ax
86
- Matplotlib axes on which to plot.
87
- kwargs
88
- Keyword arguments to be passed to matplotlib.
89
-
90
- Examples
91
- --------
92
- >>> generate_animation(dataset["Derived_Number_Density_Electron"])
93
- """
94
- import matplotlib.pyplot as plt
95
- from matplotlib.animation import FuncAnimation
96
-
97
- if ax is None:
98
- _, ax = plt.subplots()
99
-
100
- N_frames = data["time"].size
101
- global_min, global_max = compute_global_limits(data)
102
-
103
- if is_2d(data):
104
- kwargs["norm"] = plt.Normalize(vmin=global_min, vmax=global_max)
105
- kwargs["add_colorbar"] = False
106
- # Set default x and y coordinates for 2D data if not provided
107
- kwargs.setdefault("x", "X_Grid_mid")
108
- kwargs.setdefault("y", "Y_Grid_mid")
109
-
110
- # Initialize the plot with the first timestep
111
- plot = data.isel(time=0).plot(ax=ax, **kwargs)
112
- ax.set_title(get_frame_title(data, 0, display_sdf_name))
113
-
114
- # Add colorbar
115
- long_name = data.attrs.get("long_name")
116
- units = data.attrs.get("units")
117
- plt.colorbar(plot, ax=ax, label=f"{long_name} [${units}$]")
118
-
119
- # Initialise plo and set y-limits for 1D data
120
- if is_1d(data):
121
- plot = data.isel(time=0).plot(ax=ax, **kwargs)
122
- ax.set_title(get_frame_title(data, 0, display_sdf_name))
123
- ax.set_ylim(global_min, global_max)
124
-
125
- if move_window:
126
- window_velocity, window_initial_edge, time_since_start = (
127
- calculate_window_velocity_and_edges(data, kwargs["x"])
128
- )
129
-
130
- # User's choice for initial window edge supercides the one calculated
131
- if "xlim" in kwargs:
132
- window_initial_edge = kwargs["xlim"]
133
-
134
- def update(frame):
135
- # Set the xlim for each frame in the case of a moving window
136
- if move_window:
137
- kwargs["xlim"] = (
138
- window_initial_edge[0] + window_velocity * time_since_start[frame],
139
- window_initial_edge[1] * 0.99
140
- + window_velocity * time_since_start[frame],
141
- )
142
-
143
- # Update plot for the new frame
144
- ax.clear()
145
- data.isel(time=frame).plot(ax=ax, **kwargs)
146
- ax.set_title(get_frame_title(data, frame, display_sdf_name))
147
-
148
- # # Update y-limits for 1D data
149
- if is_1d(data):
150
- ax.set_ylim(global_min, global_max)
151
-
152
- return FuncAnimation(
153
- ax.get_figure(),
154
- update,
155
- frames=range(N_frames),
156
- interval=1000 / fps,
157
- repeat=True,
158
- )
159
-
160
-
161
- @xr.register_dataarray_accessor("epoch")
162
- class EpochAccessor:
163
- def __init__(self, xarray_obj):
164
- self._obj = xarray_obj
165
-
166
- def animate(self, *args, **kwargs) -> FuncAnimation:
167
- """Generate animations of Epoch data.
168
-
169
- Parameters
170
- ----------
171
- args
172
- Positional arguments passed to :func:`generate_animation`.
173
- kwargs
174
- Keyword arguments passed to :func:`generate_animation`.
175
-
176
- Examples
177
- --------
178
- >>> import xarray as xr
179
- >>> from sdf_xarray import SDFPreprocess
180
- >>> ds = xr.open_mfdataset("*.sdf", preprocess=SDFPreprocess())
181
- >>> ani = ds["Electric_Field_Ey"].epoch.animate()
182
- >>> ani.save("myfile.mp4")
183
- """
184
- return generate_animation(self._obj, *args, **kwargs)
@@ -1,39 +0,0 @@
1
- import pathlib
2
- import tempfile
3
-
4
- import matplotlib as mpl
5
- import pytest
6
- import xarray as xr
7
- from matplotlib.animation import PillowWriter
8
-
9
- from sdf_xarray import SDFPreprocess
10
-
11
- mpl.use("Agg")
12
-
13
- EXAMPLE_FILES_DIR = pathlib.Path(__file__).parent / "example_files"
14
-
15
-
16
- def test_animation_accessor():
17
- array = xr.DataArray(
18
- [1, 2, 3],
19
- dims=["x"],
20
- coords={"x": [0, 1, 2]},
21
- attrs={"long_name": "Test Array", "units": "m"},
22
- )
23
- assert hasattr(array, "epoch")
24
- assert hasattr(array.epoch, "animate")
25
-
26
-
27
- def test_animate_headless():
28
- with xr.open_mfdataset(
29
- EXAMPLE_FILES_DIR.glob("*.sdf"), preprocess=SDFPreprocess()
30
- ) as ds:
31
- anim = ds["Derived_Number_Density_electron"].epoch.animate()
32
-
33
- # Specify a custom writable temporary directory
34
- with tempfile.TemporaryDirectory() as temp_dir:
35
- temp_file_path = f"{temp_dir}/output.gif"
36
- try:
37
- anim.save(temp_file_path, writer=PillowWriter(fps=2))
38
- except Exception as e:
39
- pytest.fail(f"animate().save() failed in headless mode: {e}")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes