sdf-xarray 0.3.0__tar.gz → 0.3.2__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 (130) hide show
  1. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/CITATION.cff +4 -0
  2. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/PKG-INFO +2 -2
  3. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0000.sdf +0 -0
  4. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0001.sdf +0 -0
  5. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0002.sdf +0 -0
  6. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0003.sdf +0 -0
  7. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0004.sdf +0 -0
  8. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/0005.sdf +0 -0
  9. sdf_xarray-0.3.2/docs/tutorial_dataset_2d/input.deck +65 -0
  10. sdf_xarray-0.3.2/docs/unit_conversion.rst +228 -0
  11. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/pyproject.toml +1 -0
  12. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/src/sdf_xarray/__init__.py +113 -15
  13. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/src/sdf_xarray/_version.py +3 -3
  14. sdf_xarray-0.3.2/src/sdf_xarray/dataset_accessor.py +73 -0
  15. sdf_xarray-0.3.2/tests/example_files_3D/0000.sdf +0 -0
  16. sdf_xarray-0.3.2/tests/example_files_3D/0001.sdf +0 -0
  17. sdf_xarray-0.3.2/tests/example_files_3D/input.deck +54 -0
  18. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/test_basic.py +232 -1
  19. sdf_xarray-0.3.2/tests/test_epoch_dataset_accessor.py +146 -0
  20. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/uv.lock +481 -476
  21. sdf_xarray-0.3.0/docs/unit_conversion.rst +0 -175
  22. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.github/workflows/black.yml +0 -0
  23. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.github/workflows/build_publish.yml +0 -0
  24. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.github/workflows/lint.yml +0 -0
  25. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.github/workflows/tests.yml +0 -0
  26. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.gitignore +0 -0
  27. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.gitmodules +0 -0
  28. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/.readthedocs.yaml +0 -0
  29. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/BEAM.png +0 -0
  30. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/CMakeLists.txt +0 -0
  31. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/CONTRIBUTING.md +0 -0
  32. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/LICENCE +0 -0
  33. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/PlasmaFAIR.svg +0 -0
  34. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/README.md +0 -0
  35. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/.gitignore +0 -0
  36. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/_templates/custom-class-template.rst +0 -0
  37. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/_templates/custom-module-template.rst +0 -0
  38. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/api.rst +0 -0
  39. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/conf.py +0 -0
  40. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/contributing.rst +0 -0
  41. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/getting_started.rst +0 -0
  42. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/index.rst +0 -0
  43. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/key_functionality.rst +0 -0
  44. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/known_issues.rst +0 -0
  45. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/make.bat +0 -0
  46. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0000.sdf +0 -0
  47. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0001.sdf +0 -0
  48. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0002.sdf +0 -0
  49. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0003.sdf +0 -0
  50. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0004.sdf +0 -0
  51. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0005.sdf +0 -0
  52. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0006.sdf +0 -0
  53. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0007.sdf +0 -0
  54. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0008.sdf +0 -0
  55. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0009.sdf +0 -0
  56. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0010.sdf +0 -0
  57. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0011.sdf +0 -0
  58. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0012.sdf +0 -0
  59. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0013.sdf +0 -0
  60. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0014.sdf +0 -0
  61. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0015.sdf +0 -0
  62. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0016.sdf +0 -0
  63. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0017.sdf +0 -0
  64. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0018.sdf +0 -0
  65. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0019.sdf +0 -0
  66. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0020.sdf +0 -0
  67. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0021.sdf +0 -0
  68. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0022.sdf +0 -0
  69. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0023.sdf +0 -0
  70. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0024.sdf +0 -0
  71. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0025.sdf +0 -0
  72. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0026.sdf +0 -0
  73. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0027.sdf +0 -0
  74. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0028.sdf +0 -0
  75. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0029.sdf +0 -0
  76. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0030.sdf +0 -0
  77. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0031.sdf +0 -0
  78. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0032.sdf +0 -0
  79. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0033.sdf +0 -0
  80. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0034.sdf +0 -0
  81. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0035.sdf +0 -0
  82. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0036.sdf +0 -0
  83. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0037.sdf +0 -0
  84. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0038.sdf +0 -0
  85. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0039.sdf +0 -0
  86. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/0040.sdf +0 -0
  87. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/deck.status +0 -0
  88. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/epoch1d.dat +0 -0
  89. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/input.deck +0 -0
  90. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/normal.visit +0 -0
  91. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/docs/tutorial_dataset_1d/restart.visit +0 -0
  92. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/src/sdf_xarray/csdf.pxd +0 -0
  93. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/src/sdf_xarray/plotting.py +0 -0
  94. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/src/sdf_xarray/sdf_interface.pyx +0 -0
  95. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_array_no_grids/0000.sdf +0 -0
  96. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_array_no_grids/0001.sdf +0 -0
  97. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_array_no_grids/README.md +0 -0
  98. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_array_no_grids/input.deck +0 -0
  99. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_dist_fn/0000.sdf +0 -0
  100. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_dist_fn/0001.sdf +0 -0
  101. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_dist_fn/0002.sdf +0 -0
  102. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_dist_fn/input.deck +0 -0
  103. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0000.sdf +0 -0
  104. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0001.sdf +0 -0
  105. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0002.sdf +0 -0
  106. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0003.sdf +0 -0
  107. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0004.sdf +0 -0
  108. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0005.sdf +0 -0
  109. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0006.sdf +0 -0
  110. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0007.sdf +0 -0
  111. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0008.sdf +0 -0
  112. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0009.sdf +0 -0
  113. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/0010.sdf +0 -0
  114. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/README.md +0 -0
  115. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_1D/input.deck +0 -0
  116. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/0000.sdf +0 -0
  117. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/0001.sdf +0 -0
  118. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/0002.sdf +0 -0
  119. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/0003.sdf +0 -0
  120. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/0004.sdf +0 -0
  121. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_files_2D_moving_window/input.deck +0 -0
  122. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_mismatched_files/0000.sdf +0 -0
  123. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_mismatched_files/0001.sdf +0 -0
  124. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_mismatched_files/0002.sdf +0 -0
  125. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_two_probes_2D/0000.sdf +0 -0
  126. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_two_probes_2D/0001.sdf +0 -0
  127. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_two_probes_2D/0002.sdf +0 -0
  128. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/example_two_probes_2D/input.deck +0 -0
  129. {sdf_xarray-0.3.0 → sdf_xarray-0.3.2}/tests/test_cython.py +0 -0
  130. /sdf_xarray-0.3.0/tests/test_epoch_accessor.py → /sdf_xarray-0.3.2/tests/test_epoch_dataarray_accessor.py +0 -0
@@ -16,5 +16,9 @@ authors:
16
16
  given-names: Shaun
17
17
  orcid: 'https://orcid.org/0009-0005-0693-030X'
18
18
  affiliation: University of York
19
+ - family-names: Herdman
20
+ given-names: Chris
21
+ orcid: 'https://orcid.org/0000-0002-5159-0130'
22
+ affiliation: University of York
19
23
  doi: 10.5281/zenodo.15351323
20
24
  date-released: '2024-07-25'
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdf-xarray
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Provides a backend for xarray to read SDF files as created by the EPOCH plasma PIC code.
5
- Author-Email: Peter Hill <peter.hill@york.ac.uk>, Joel Adams <joel.adams@york.ac.uk>, Shaun Doherty <shaun.doherty@york.ac.uk>
5
+ Author-Email: Peter Hill <peter.hill@york.ac.uk>, Joel Adams <joel.adams@york.ac.uk>, Shaun Doherty <shaun.doherty@york.ac.uk>, Chris Herdman <chris.herdman@york.ac.uk>
6
6
  License-Expression: BSD-3-Clause
7
7
  Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Intended Audience :: Science/Research
@@ -0,0 +1,65 @@
1
+ begin:control
2
+ nx = 64
3
+ ny = 64
4
+
5
+ # Final time of simulation
6
+ t_end = 5 * femto
7
+
8
+ # Size of domain
9
+ x_min = 0
10
+ x_max = 6 * micron
11
+
12
+ y_min = 0
13
+ y_max = 6 * micron
14
+
15
+ stdout_frequency = 1
16
+ nparticles = nx * ny * 50
17
+ end:control
18
+
19
+ begin:constant
20
+ n_elec = 1000
21
+ L_target_x = 2 * micron
22
+ L_target_y = 4 * micron
23
+
24
+ x_center = (x_min + x_max) / 2
25
+ y_center = (y_min + y_max) / 2
26
+
27
+ density_profile = if( (abs(x - x_center) lt L_target_x/2), if( (abs(y - y_center) lt L_target_y/2), 1, 0), 0)
28
+ end:constant
29
+
30
+ begin:boundaries
31
+ bc_x_min = periodic
32
+ bc_x_max = periodic
33
+ bc_y_min = periodic
34
+ bc_y_max = periodic
35
+ end:boundaries
36
+
37
+
38
+ begin:species
39
+ name = Electron
40
+ frac = 0.5
41
+ number_density = n_elec * density_profile
42
+ identify:electron
43
+ end:species
44
+
45
+ begin:species
46
+ name = Ion
47
+ frac = 0.5
48
+ number_density = n_elec * density_profile
49
+ identify:proton
50
+ end:species
51
+
52
+ begin:output_global
53
+ force_last_to_be_restartable = F
54
+ end:output_global
55
+
56
+ begin:output
57
+ name = normal
58
+
59
+ dt_snapshot = 1 * femto
60
+
61
+ grid = always
62
+ number_density = always + species
63
+
64
+ end:output
65
+
@@ -0,0 +1,228 @@
1
+ .. _sec-unit-conversion:
2
+
3
+ ===============
4
+ Unit Conversion
5
+ ===============
6
+
7
+ The ``sdf-xarray`` package automatically extracts the units for each
8
+ coordinate/variable/constant from an SDF file and stores them as an :class:`xarray.Dataset`
9
+ attribute called ``"units"``. Sometimes we want to convert our data from one format to
10
+ another, e.g. converting the grid coordinates from meters to microns, time from seconds
11
+ to femto-seconds or particle energy from Joules to electron-volts.
12
+
13
+ .. ipython:: python
14
+
15
+ from sdf_xarray import open_mfdataset
16
+ import matplotlib.pyplot as plt
17
+ plt.rcParams.update({
18
+ "axes.labelsize": 16,
19
+ "xtick.labelsize": 14,
20
+ "ytick.labelsize": 14,
21
+ "axes.titlesize": 16
22
+ })
23
+
24
+
25
+ =====================
26
+ Rescaling Coordinates
27
+ =====================
28
+
29
+ For simple scaling and unit relabeling of coordinates (e.g., converting meters to microns),
30
+ the most straightforward approach is to use the ``rescale_coords()`` method
31
+ via the custom ``xarray.Dataset.epoch`` dataset accessor.
32
+
33
+ This method scales the coordinate values by a given multiplier and updates the ``"units"``
34
+ attribute in one step.
35
+
36
+ Rescaling Grid Coordinates
37
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
38
+
39
+ We can use the ``xarray.Dataset.epoch.rescale_coords()`` method to convert X, Y, and Z
40
+ coordinates from meters (m) to microns (µm) by applying a multiplier of ``1e6``.
41
+
42
+ .. ipython:: python
43
+
44
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
45
+
46
+ with open_mfdataset("tutorial_dataset_2d/*.sdf") as ds:
47
+ ds_in_microns = ds.epoch.rescale_coords(
48
+ multiplier=1e6,
49
+ unit_label="µm",
50
+ coord_names=["X_Grid_mid", "Y_Grid_mid"]
51
+ )
52
+ derived_number_density = ds["Derived_Number_Density_Electron"].isel(time=0).compute()
53
+ derived_number_density_microns = ds_in_microns["Derived_Number_Density_Electron"].isel(time=0).compute()
54
+
55
+ derived_number_density.plot(ax=ax1, x="X_Grid_mid", y="Y_Grid_mid")
56
+ ax1.set_title("Original X Coordinate (m)")
57
+
58
+ derived_number_density_microns.plot(ax=ax2, x="X_Grid_mid", y="Y_Grid_mid")
59
+ ax2.set_title("Rescaled X Coordinate (µm)")
60
+
61
+ @savefig coordinate_conversion.png width=9in
62
+ fig.tight_layout()
63
+
64
+
65
+ Rescaling Time Coordinate
66
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
67
+
68
+ We can also use the ``xarray.Dataset.epoch.rescale_coords()`` method to convert the time
69
+ coordinate from seconds (s) to femto-seconds (fs) by applying a multiplier of ``1e15``.
70
+
71
+ .. ipython:: python
72
+
73
+ with open_mfdataset("tutorial_dataset_2d/*.sdf") as ds:
74
+ ds_time_in_femto = ds.epoch.rescale_coords(
75
+ multiplier=1e15,
76
+ unit_label="fs",
77
+ coord_names="time"
78
+ )
79
+
80
+ print(f"[Original] units: {ds['time'].attrs['units']}, values: {ds['time'].values}")
81
+ print(f"[Rescaled] units: {ds_time_in_femto['time'].attrs['units']}, values: {ds_time_in_femto['time'].values}")
82
+
83
+
84
+ ================================
85
+ Unit Conversion with pint-xarray
86
+ ================================
87
+
88
+ While this is sufficient for most use cases, we can enhance this functionality
89
+ using the `pint <https://pint.readthedocs.io/en/stable/getting/index.html>`_ library.
90
+ Pint allows us to specify the units of a given array and convert them
91
+ to another, which is incredibly handy. We can take this a step further,
92
+ however, and utilize the `pint-xarray
93
+ <https://pint-xarray.readthedocs.io/en/latest/>`_ library. This library
94
+ allows us to infer units directly from an `xarray.Dataset.attrs` while
95
+ retaining all the information about the `xarray.Dataset`. This works
96
+ very similarly to taking a NumPy array and multiplying it by a constant or
97
+ another array, which returns a new array; however, this library will also
98
+ retain the unit logic (specifically the ``"units"`` information).
99
+
100
+ .. note::
101
+ Unit conversion is not supported on coordinates in ``pint-xarray`` which is due to an
102
+ underlying issue with how ``xarray`` implements indexes.
103
+
104
+ Installation
105
+ ~~~~~~~~~~~~
106
+
107
+ To install the pint libraries you can simply run the following optional
108
+ dependency pip command which will install both the ``pint`` and ``pint-xarray``
109
+ libraries. You can install these optional dependencies via pip:
110
+
111
+ .. code:: console
112
+
113
+ $ pip install "sdf_xarray[pint]"
114
+
115
+ .. note::
116
+ Once you install ``pint-xarray`` it is automatically picked up and loaded
117
+ by the code so you should have access to the ``xarray.Dataset.pint`` accessor.
118
+
119
+ Quantifying Arrays
120
+ ~~~~~~~~~~~~~~~~~~
121
+
122
+ When using ``pint-xarray``, the library attempts to infer units from the
123
+ ``"units"`` attribute on each `xarray.DataArray`. Alternatively, you can
124
+ also specify the units yourself by passing a string into the
125
+ ``xarray.Dataset.DataArray.pint.quantify()`` function call. Once the type is inferred
126
+ the original `xarray.DataArray` will be converted to a `pint.Quantity`
127
+ and the ``"units"`` attribute will
128
+ be removed.
129
+
130
+ In the following example we will extract the time-resolved total particle
131
+ energy of electrons which is measured in Joules and convert it to electron
132
+ volts.
133
+
134
+ .. ipython:: python
135
+
136
+ with open_mfdataset("tutorial_dataset_1d/*.sdf") as ds:
137
+ total_particle_energy = ds["Total_Particle_Energy_Electron"]
138
+
139
+ total_particle_energy
140
+
141
+ total_particle_energy = ds["Total_Particle_Energy_Electron"].pint.quantify()
142
+
143
+ total_particle_energy
144
+
145
+
146
+ Now that this dataset has been converted a `pint.Quantity`, we can check
147
+ it's units and dimensionality
148
+
149
+ .. ipython:: python
150
+
151
+ total_particle_energy.pint.units
152
+ total_particle_energy.pint.dimensionality
153
+
154
+
155
+ Converting Units
156
+ ~~~~~~~~~~~~~~~~
157
+
158
+ We can now convert it to electron volts utilising the `pint.Quantity.to`
159
+ function
160
+
161
+ .. ipython:: python
162
+
163
+ total_particle_energy_ev = total_particle_energy.pint.to("eV")
164
+
165
+ Unit Propagation
166
+ ~~~~~~~~~~~~~~~~
167
+
168
+ Suppose instead of converting to ``"eV"``, we want to convert to ``"W"``
169
+ (watts). To do this, we divide the total particle energy by time. However,
170
+ since coordinates in `xarray.Dataset` cannot be directly converted to
171
+ `pint.Quantity`, we must first extract the coordinate values manually
172
+ and create a new Pint quantity for time.
173
+
174
+ Once both arrays are quantified, Pint will automatically handle the unit
175
+ propagation when we perform arithmetic operations like division.
176
+
177
+ .. note::
178
+ Pint does not automatically simplify ``"J/s"`` to ``"W"``, so we use
179
+ `pint.Quantity.to` to convert the unit string. Since these units are
180
+ the same it will not change the underlying data, only the units. This is
181
+ only a small formatting choice and is not required.
182
+
183
+ .. ipython:: python
184
+
185
+ import pint
186
+ time_values = total_particle_energy.coords["time"].data
187
+ time = pint.Quantity(time_values, "s")
188
+ total_particle_energy_w = total_particle_energy / time
189
+ total_particle_energy_w.pint.units
190
+ total_particle_energy_w = total_particle_energy_w.pint.to("W")
191
+ total_particle_energy_w.pint.units
192
+
193
+ Dequantifying and Restoring Units
194
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
195
+
196
+ .. note::
197
+ If this function is not called prior to plotting then the ``units`` will be
198
+ inferred from the `pint.Quantity` array which will return the long
199
+ name of the units. i.e. instead of returning ``"eV"`` it will return
200
+ ``"electron_volt"``.
201
+
202
+ The ``xarray.Dataset.DataArray.pint.dequantify`` function converts the data from
203
+ `pint.Quantity` back to the original `xarray.DataArray` and adds
204
+ the ``"units"`` attribute back in. It also has an optional ``format`` parameter
205
+ that allows you to specify the formatting type of ``"units"`` attribute. We
206
+ have used the ``format="~P"`` option as it shortens the unit to its
207
+ "short pretty" format (``"eV"``). For more options, see the `Pint formatting
208
+ documentation <https://pint.readthedocs.io/en/stable/user/formatting.html>`_.
209
+
210
+ .. ipython:: python
211
+
212
+ total_particle_energy_ev = total_particle_energy_ev.pint.dequantify(format="~P")
213
+ total_particle_energy_w = total_particle_energy_w.pint.dequantify(format="~P")
214
+ total_particle_energy_ev
215
+
216
+ To confirm the conversion has worked correctly, we can plot the original and
217
+ converted `xarray.Dataset` side by side:
218
+
219
+ .. ipython:: python
220
+
221
+ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16,8))
222
+ ds["Total_Particle_Energy_Electron"].plot(ax=ax1)
223
+ total_particle_energy_ev.plot(ax=ax2)
224
+ total_particle_energy_w.plot(ax=ax3)
225
+ ax4.set_visible(False)
226
+ fig.suptitle("Comparison of conversion from Joules to electron volts and watts", fontsize="18")
227
+ @savefig unit_conversion.png width=9in
228
+ fig.tight_layout()
@@ -16,6 +16,7 @@ authors = [
16
16
  { name = "Peter Hill", email = "peter.hill@york.ac.uk" },
17
17
  { name = "Joel Adams", email = "joel.adams@york.ac.uk" },
18
18
  { name = "Shaun Doherty", email = "shaun.doherty@york.ac.uk" },
19
+ { name = "Chris Herdman", email = "chris.herdman@york.ac.uk" },
19
20
  ]
20
21
  requires-python = ">=3.10,<3.14"
21
22
  dependencies = ["numpy>=2.0.0", "xarray>=2024.1.0", "dask>=2024.7.1"]
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import os
2
3
  import re
3
4
  from collections import Counter, defaultdict
@@ -18,10 +19,16 @@ from xarray.core import indexing
18
19
  from xarray.core.utils import close_on_error, try_read_magic_number_from_path
19
20
  from xarray.core.variable import Variable
20
21
 
21
- # NOTE: Do not delete this line, otherwise the "epoch" accessor will not be
22
- # imported when the user imports sdf_xarray
22
+ # NOTE: Do not delete these lines, otherwise the "epoch" dataset and dataarray
23
+ # accessors will not be imported when the user imports sdf_xarray
24
+ import sdf_xarray.dataset_accessor
23
25
  import sdf_xarray.plotting # noqa: F401
24
26
 
27
+ # NOTE: This attempts to initialise with the "pint" accessor if the user
28
+ # has installed the package
29
+ with contextlib.suppress(ImportError):
30
+ import pint_xarray # noqa: F401
31
+
25
32
  from .sdf_interface import Constant, SDFFile # type: ignore # noqa: PGH003
26
33
 
27
34
  # TODO Remove this once the new kwarg options are fully implemented
@@ -77,8 +84,45 @@ def _resolve_glob(path_glob: PathLike | Iterable[PathLike]):
77
84
  return paths
78
85
 
79
86
 
80
- def combine_datasets(path_glob: Iterable | str, **kwargs) -> xr.Dataset:
81
- """Combine all datasets using a single time dimension"""
87
+ def purge_unselected_data_vars(ds: xr.Dataset, data_vars: list[str]) -> xr.Dataset:
88
+ """
89
+ If the user has exclusively requested only certain variables be
90
+ loaded in then we purge all other variables and dimensions
91
+ """
92
+ existing_data_vars = set(ds.data_vars.keys())
93
+ vars_to_keep = set(data_vars) & existing_data_vars
94
+ vars_to_drop = existing_data_vars - vars_to_keep
95
+ ds = ds.drop_vars(vars_to_drop)
96
+
97
+ existing_dims = set(ds.sizes)
98
+ dims_to_keep = set()
99
+ for var in vars_to_keep:
100
+ dims_to_keep.update(ds[var].coords._names)
101
+ dims_to_keep.update(ds[var].dims)
102
+
103
+ coords_to_drop = existing_dims - dims_to_keep
104
+ return ds.drop_dims(coords_to_drop)
105
+
106
+
107
+ def combine_datasets(
108
+ path_glob: Iterable | str, data_vars: list[str], **kwargs
109
+ ) -> xr.Dataset:
110
+ """
111
+ Combine all datasets using a single time dimension, optionally extract
112
+ data from only the listed data_vars
113
+ """
114
+
115
+ if data_vars is not None:
116
+ return xr.open_mfdataset(
117
+ path_glob,
118
+ join="outer",
119
+ coords="different",
120
+ compat="no_conflicts",
121
+ combine="nested",
122
+ concat_dim="time",
123
+ preprocess=SDFPreprocess(data_vars=data_vars),
124
+ **kwargs,
125
+ )
82
126
 
83
127
  return xr.open_mfdataset(
84
128
  path_glob,
@@ -97,6 +141,7 @@ def open_mfdataset(
97
141
  separate_times: bool = False,
98
142
  keep_particles: bool = False,
99
143
  probe_names: list[str] | None = None,
144
+ data_vars: list[str] | None = None,
100
145
  ) -> xr.Dataset:
101
146
  """Open a set of EPOCH SDF files as one `xarray.Dataset`
102
147
 
@@ -128,19 +173,34 @@ def open_mfdataset(
128
173
  If ``True``, also load particle data (this may use a lot of memory!)
129
174
  probe_names :
130
175
  List of EPOCH probe names
176
+ data_vars :
177
+ List of data vars to load in (If not specified loads in all variables)
131
178
  """
132
179
 
133
180
  path_glob = _resolve_glob(path_glob)
181
+
134
182
  if not separate_times:
135
183
  return combine_datasets(
136
- path_glob, keep_particles=keep_particles, probe_names=probe_names
184
+ path_glob,
185
+ data_vars=data_vars,
186
+ keep_particles=keep_particles,
187
+ probe_names=probe_names,
137
188
  )
138
189
 
139
190
  _, var_times_map = make_time_dims(path_glob)
140
- all_dfs = [
141
- xr.open_dataset(f, keep_particles=keep_particles, probe_names=probe_names)
142
- for f in path_glob
143
- ]
191
+
192
+ all_dfs = []
193
+ for f in path_glob:
194
+ ds = xr.open_dataset(f, keep_particles=keep_particles, probe_names=probe_names)
195
+
196
+ # If the data_vars are specified then only load them in and disregard the rest.
197
+ # If there are no remaining data variables then skip adding the dataset to list
198
+ if data_vars is not None:
199
+ ds = purge_unselected_data_vars(ds, data_vars)
200
+ if not ds.data_vars:
201
+ continue
202
+
203
+ all_dfs.append(ds)
144
204
 
145
205
  for df in all_dfs:
146
206
  for da in df:
@@ -158,7 +218,6 @@ def open_mfdataset(
158
218
 
159
219
  return xr.combine_by_coords(
160
220
  all_dfs,
161
- data_vars="all",
162
221
  coords="different",
163
222
  combine_attrs="drop_conflicts",
164
223
  join="outer",
@@ -516,10 +575,43 @@ class SDFEntrypoint(BackendEntrypoint):
516
575
 
517
576
 
518
577
  class SDFPreprocess:
519
- """Preprocess SDF files for xarray ensuring matching job ids and sets time dimension"""
578
+ """Preprocess SDF files for xarray ensuring matching job ids and sets
579
+ time dimension.
580
+
581
+ This class is used as a 'preprocess' function within ``xr.open_mfdataset``. It
582
+ performs three main duties on each individual file's Dataset:
583
+
584
+ 1. Checks for a **matching job ID** across all files to ensure dataset consistency.
585
+ 2. **Filters** the Dataset to keep only the variables specified in `data_vars`
586
+ and their required coordinates.
587
+ 3. **Expands dimensions** to include a single 'time' coordinate, preparing the
588
+ Dataset for concatenation.
589
+
590
+ EPOCH can output variables at different intervals, so some SDF files
591
+ may not contain the requested variable. We combine this data into one
592
+ dataset by concatenating across the time dimension.
593
+
594
+ The combination is performed using ``join="outer"`` (in the calling ``open_mfdataset`` function),
595
+ meaning that the final combined dataset will contain the variable across the
596
+ entire time span, with NaNs filling the time steps where the variable was absent in
597
+ the individual file.
598
+
599
+ With large SDF files, this filtering method will save on memory consumption when
600
+ compared to loading all variables from all files before concatenation.
520
601
 
521
- def __init__(self):
602
+ Parameters
603
+ ----------
604
+ data_vars :
605
+ A list of data variables to load in (If not specified loads
606
+ in all variables)
607
+ """
608
+
609
+ def __init__(
610
+ self,
611
+ data_vars: list[str] | None = None,
612
+ ):
522
613
  self.job_id: int | None = None
614
+ self.data_vars = data_vars
523
615
 
524
616
  def __call__(self, ds: xr.Dataset) -> xr.Dataset:
525
617
  if self.job_id is None:
@@ -530,17 +622,23 @@ class SDFPreprocess:
530
622
  f"Mismatching job ids (got {ds.attrs['jobid1']}, expected {self.job_id})"
531
623
  )
532
624
 
533
- ds = ds.expand_dims(time=[ds.attrs["time"]])
625
+ # If the user has exclusively requested only certain variables be
626
+ # loaded in then we purge all other variables and coordinates
627
+ if self.data_vars:
628
+ ds = purge_unselected_data_vars(ds, self.data_vars)
629
+
630
+ time_val = ds.attrs.get("time", np.nan)
631
+ ds = ds.expand_dims(time=[time_val])
534
632
  ds = ds.assign_coords(
535
633
  time=(
536
634
  "time",
537
- [ds.attrs["time"]],
635
+ [time_val],
538
636
  {"units": "s", "long_name": "Time", "full_name": "time"},
539
637
  )
540
638
  )
541
639
  # Particles' spartial coordinates also evolve in time
542
640
  for coord, value in ds.coords.items():
543
641
  if value.attrs.get("point_data", False):
544
- ds.coords[coord] = value.expand_dims(time=[ds.attrs["time"]])
642
+ ds.coords[coord] = value.expand_dims(time=[time_val])
545
643
 
546
644
  return ds
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.3.0'
32
- __version_tuple__ = version_tuple = (0, 3, 0)
31
+ __version__ = version = '0.3.2'
32
+ __version_tuple__ = version_tuple = (0, 3, 2)
33
33
 
34
- __commit_id__ = commit_id = 'gcca942b3f'
34
+ __commit_id__ = commit_id = 'g331520e50'
@@ -0,0 +1,73 @@
1
+ from typing import Union
2
+
3
+ import xarray as xr
4
+
5
+
6
+ @xr.register_dataset_accessor("epoch")
7
+ class EpochAccessor:
8
+ def __init__(self, xarray_obj: xr.Dataset):
9
+ # The xarray object is the Dataset, which we store as self._ds
10
+ self._ds = xarray_obj
11
+
12
+ def rescale_coords(
13
+ self,
14
+ multiplier: float,
15
+ unit_label: str,
16
+ coord_names: Union[str, list[str]],
17
+ ) -> xr.Dataset:
18
+ """
19
+ Rescales specified X and Y coordinates in the Dataset by a given multiplier
20
+ and updates the unit label attribute.
21
+
22
+ Parameters
23
+ ----------
24
+ multiplier : float
25
+ The factor by which to multiply the coordinate values (e.g., 1e6 for meters to microns).
26
+ unit_label : str
27
+ The new unit label for the coordinates (e.g., "µm").
28
+ coord_names : str or list of str
29
+ The name(s) of the coordinate variable(s) to rescale.
30
+ If a string, only that coordinate is rescaled.
31
+ If a list, all listed coordinates are rescaled.
32
+
33
+ Returns
34
+ -------
35
+ xr.Dataset
36
+ A new Dataset with the updated and rescaled coordinates.
37
+
38
+ Examples
39
+ --------
40
+ # Convert X, Y, and Z from meters to microns
41
+ >>> ds_in_microns = ds.epoch.rescale_coords(1e6, "µm", coord_names=["X_Grid", "Y_Grid", "Z_Grid"])
42
+
43
+ # Convert only X to millimeters
44
+ >>> ds_in_mm = ds.epoch.rescale_coords(1000, "mm", coord_names="X_Grid")
45
+ """
46
+
47
+ ds = self._ds
48
+ new_coords = {}
49
+
50
+ if isinstance(coord_names, str):
51
+ # Convert single string to a list
52
+ coords_to_process = [coord_names]
53
+ elif isinstance(coord_names, list):
54
+ # Use the provided list
55
+ coords_to_process = coord_names
56
+ else:
57
+ coords_to_process = list(coord_names)
58
+
59
+ for coord_name in coords_to_process:
60
+ if coord_name not in ds.coords:
61
+ raise ValueError(
62
+ f"Coordinate '{coord_name}' not found in the Dataset. Cannot rescale."
63
+ )
64
+
65
+ coord_original = ds[coord_name]
66
+
67
+ coord_rescaled = coord_original * multiplier
68
+ coord_rescaled.attrs = coord_original.attrs.copy()
69
+ coord_rescaled.attrs["units"] = unit_label
70
+
71
+ new_coords[coord_name] = coord_rescaled
72
+
73
+ return ds.assign_coords(new_coords)
@@ -0,0 +1,54 @@
1
+ begin:control
2
+ nx = 64
3
+ ny = 64
4
+ nz = 64
5
+
6
+ # Final time of simulation
7
+ t_end = 5 * femto
8
+
9
+ # Size of domain
10
+ x_min = 0
11
+ x_max = 5.0e5
12
+
13
+ y_min = 0
14
+ y_max = 5.0e5
15
+
16
+ z_min = 0
17
+ z_max = 5.0e5
18
+
19
+ stdout_frequency = 1
20
+ end:control
21
+
22
+
23
+ begin:boundaries
24
+ bc_x_min = periodic
25
+ bc_x_max = periodic
26
+ bc_y_min = periodic
27
+ bc_y_max = periodic
28
+ bc_z_min = periodic
29
+ bc_z_max = periodic
30
+ end:boundaries
31
+
32
+
33
+ begin:species
34
+ name = electron
35
+ charge = -1
36
+ mass = 1.0
37
+ temperature_x = 273
38
+ number_density = 10
39
+ nparticles = nx * ny * nz * 2
40
+ end:species
41
+
42
+ begin:output_global
43
+ force_last_to_be_restartable = F
44
+ end:output_global
45
+
46
+ begin:output
47
+ name = normal
48
+
49
+ dt_snapshot = 1 * femto
50
+
51
+ grid = always
52
+
53
+ end:output
54
+