sdf-xarray 0.9.0__tar.gz → 0.10.0__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.
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/PKG-INFO +1 -1
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/animation.md +8 -17
- sdf_xarray-0.10.0/docs/known_issues.md +8 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/loading_data.md +32 -17
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/__init__.py +138 -43
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/_version.py +3 -3
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/csdf.pxd +2 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/download.py +18 -11
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/sdf_interface.pyx +110 -14
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/tests/test_cython.py +46 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/tests/test_dataset.py +87 -0
- sdf_xarray-0.9.0/docs/known_issues.md +0 -15
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.github/workflows/build_publish.yml +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.github/workflows/lint.yml +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.github/workflows/tests.yml +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.gitignore +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.gitmodules +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/.readthedocs.yaml +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/CITATION.cff +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/CMakeLists.txt +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/CONTRIBUTING.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/LICENCE +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/README.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/.gitignore +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_static/BEAM.png +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_static/PlasmaFAIR.svg +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_static/force_render_dark_xarray_objects.css +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_static/sdf-xarray-logo-dark.svg +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_static/sdf-xarray-logo-light.svg +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_templates/custom-class-template.rst +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/_templates/custom-module-template.rst +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/api.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/conf.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/contributing.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_1D_density.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_1D_poynting_flux.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_2D_dist_fn_multispecies.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_2D_dist_fn_multispecies_alternative.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_2D_poynting_flux.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animations.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/histograms/plot_hist_ke.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/histograms/plot_hist_ke_probe.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/histograms/plot_hist_px.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/histograms.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/live_demo.ipynb +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plots.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_1D_density.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_1D_poynting_flux.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_2D_density.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_2D_dist_fn_multispecies.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_2D_poynting_flux.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_time_temperature.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_x_px_scatter.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/workshop_details.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/figures/voxel_plot_high_res.png +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/index.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/installation.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/make.bat +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/plotting.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/understanding_datasets.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/unit_conversion.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/why_sdf_xarray.md +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/pyproject.toml +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/dataarray_accessor.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/dataset_accessor.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/src/sdf_xarray/plotting.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/tests/test_datatree.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/tests/test_epoch_dataarray_accessor.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/tests/test_epoch_dataset_accessor.py +0 -0
- {sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdf-xarray
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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>, Chris Herdman <chris.herdman@york.ac.uk>, Liam Pattinson <liam.pattinson@york.ac.uk>, Sviatoslav Shekhanov <sviatoslav.shekhanov@york.ac.uk>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -12,7 +12,6 @@ mimic <inv:#xarray.DataArray.plot>.
|
|
|
12
12
|
|
|
13
13
|
```{code-cell} ipython3
|
|
14
14
|
import sdf_xarray as sdfxr
|
|
15
|
-
import xarray as xr
|
|
16
15
|
import matplotlib.pyplot as plt
|
|
17
16
|
from matplotlib.animation import FuncAnimation
|
|
18
17
|
from IPython.display import HTML
|
|
@@ -139,31 +138,23 @@ EPOCH allows for simulations that have a moving simulation window
|
|
|
139
138
|
`move_window` and change the x-axis limits accordingly.
|
|
140
139
|
|
|
141
140
|
```{warning}
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
Importing some datasets with moving windows can cause vertical banding
|
|
142
|
+
in the <inv:#xarray.Dataset>, which will affect the animation. Each output stores
|
|
143
|
+
its own x grid, and the same position can differ by floating point rounding between
|
|
144
|
+
outputs. Both values are then kept on the combined x axis, so each time step has
|
|
145
|
+
nan values in some columns. Setting `join = "override"` in
|
|
146
|
+
<inv:#xarray.open_mfdataset> does not fix this for moving window data, as it
|
|
147
|
+
relabels every time step with the first output's x grid.
|
|
144
148
|
```
|
|
145
149
|
|
|
146
150
|
```{code-cell} ipython3
|
|
147
|
-
ds =
|
|
148
|
-
"tutorial_dataset_2d_moving_window/*.sdf",
|
|
149
|
-
preprocess = sdfxr.SDFPreprocess(),
|
|
150
|
-
combine = "nested",
|
|
151
|
-
join = "outer",
|
|
152
|
-
compat="no_conflicts",
|
|
153
|
-
concat_dim="time",
|
|
154
|
-
)
|
|
151
|
+
ds = sdfxr.open_mfdataset("tutorial_dataset_2d_moving_window/*.sdf")
|
|
155
152
|
|
|
156
153
|
da = ds["Derived_Number_Density_Beam_Electrons"]
|
|
157
154
|
anim = da.epoch.animate(move_window = True, fps = 5)
|
|
158
155
|
anim.show()
|
|
159
156
|
```
|
|
160
157
|
|
|
161
|
-
```{warning}
|
|
162
|
-
Importing some datasets with moving windows can cause vertical banding
|
|
163
|
-
in the <inv:#xarray.Dataset>, which will affect the animation. The cause for
|
|
164
|
-
this is unknown but can be circumvented by setting `join = "override"`.
|
|
165
|
-
```
|
|
166
|
-
|
|
167
158
|
## Customisation
|
|
168
159
|
|
|
169
160
|
The animation can be customised in much the same way as <inv:#xarray.DataArray.plot>,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Known Issues
|
|
2
|
+
|
|
3
|
+
These are the known 'quirks' in `sdf_xarray`:
|
|
4
|
+
|
|
5
|
+
- [Issue #112](https://github.com/epochpic/sdf-xarray/issues/112) Due to the
|
|
6
|
+
way [cylindrical EPOCH](https://github.com/epochpic/cylindrical_epoch) sets up
|
|
7
|
+
grids we can't currently load it's SDF files. If there is enough interest in this
|
|
8
|
+
we will attempt to find a solution otherwise it shall remain low priority.
|
|
@@ -85,7 +85,11 @@ is by using the <project:#sdf_xarray.open_mfdataset>.
|
|
|
85
85
|
```{tip}
|
|
86
86
|
If your simulation includes multiple `output` blocks that specify different variables
|
|
87
87
|
for output at various time steps, variables not present at a specific step will default
|
|
88
|
-
to a nan value.
|
|
88
|
+
to a nan value. This includes distribution functions and particle data that are only
|
|
89
|
+
output at some time steps. Grids that change between outputs (such as a moving window,
|
|
90
|
+
or a distribution function with an automatic range) are combined onto the union of
|
|
91
|
+
every output's grid points, with nan values outside the part of the grid each output
|
|
92
|
+
covers. To remove these nan values we suggest using the <inv:#xarray.DataArray.dropna>
|
|
89
93
|
function or following our implmentation in [](#loading-sparse-data).
|
|
90
94
|
```
|
|
91
95
|
|
|
@@ -98,13 +102,20 @@ sdfxr.open_mfdataset("tutorial_dataset_1d/*.sdf")
|
|
|
98
102
|
Alternatively, files can be loaded using <inv:#xarray.open_mfdataset>. However, when loading in
|
|
99
103
|
all the files we have do some processing of the data so that we can correctly align it along
|
|
100
104
|
the time dimension; This is done via the `preprocess` parameter utilising the
|
|
101
|
-
<project:#sdf_xarray.SDFPreprocess> function.
|
|
105
|
+
<project:#sdf_xarray.SDFPreprocess> function. Passing `combine="nested"` and
|
|
106
|
+
`concat_dim="time"` makes sure the files are only joined along time. Otherwise
|
|
107
|
+
<inv:#xarray> treats any coordinate that differs between files (such as a moving
|
|
108
|
+
window's x grid) as another dimension to concatenate along, which can fail or request
|
|
109
|
+
far more memory than needed. Unlike <project:#sdf_xarray.open_mfdataset>, this does
|
|
110
|
+
not handle particle positions that are missing from some files.
|
|
102
111
|
|
|
103
112
|
```{code-cell} ipython3
|
|
104
113
|
import xarray as xr
|
|
105
114
|
|
|
106
115
|
xr.open_mfdataset(
|
|
107
116
|
"tutorial_dataset_1d/*.sdf",
|
|
117
|
+
combine="nested",
|
|
118
|
+
concat_dim="time",
|
|
108
119
|
join="outer",
|
|
109
120
|
compat="no_conflicts",
|
|
110
121
|
preprocess=sdfxr.SDFPreprocess()
|
|
@@ -123,6 +134,9 @@ sdfxr.open_mfdatatree("tutorial_dataset_1d/*.sdf")
|
|
|
123
134
|
|
|
124
135
|
## Loading sparse data
|
|
125
136
|
|
|
137
|
+
By default, <project:#sdf_xarray.open_mfdataset> puts every variable on a single
|
|
138
|
+
time dimension, and a variable missing from an output (including distribution
|
|
139
|
+
functions and particle data) is filled with nan values at that time step.
|
|
126
140
|
When dealing with sparse data (where different variables are saved at different,
|
|
127
141
|
non-overlapping time steps) you can optimize memory usage by loading the data with
|
|
128
142
|
<project:#sdf_xarray.open_mfdataset> using the parameter `separate_times=True`. This
|
|
@@ -140,12 +154,17 @@ sdfxr.open_mfdataset("tutorial_dataset_1d/*.sdf", separate_times=True)
|
|
|
140
154
|
## Loading particle data
|
|
141
155
|
|
|
142
156
|
```{warning}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
<project:#sdf_xarray.open_mfdataset> can load particle data from multiple
|
|
158
|
+
SDF outputs, and outputs that don't include particles are filled with nan
|
|
159
|
+
values. However, every output that does include a species must contain the
|
|
160
|
+
same number of particles of that species. The number of particles often
|
|
161
|
+
varies between outputs (for example with particle injection, losses or probes),
|
|
162
|
+
in which case loading fails with a conflicting dimension sizes error, and you
|
|
163
|
+
should load each file individually instead.
|
|
164
|
+
|
|
165
|
+
The `ID_<species>` dimension is the position in each output's particle array,
|
|
166
|
+
not a particle identity, so the same index at two time steps is not
|
|
167
|
+
necessarily the same particle.
|
|
149
168
|
```
|
|
150
169
|
|
|
151
170
|
```{note}
|
|
@@ -157,7 +176,7 @@ Failing to do so will result in dimension name conflicts.
|
|
|
157
176
|
|
|
158
177
|
By default, particle data isn't kept as it takes up a lot of space.
|
|
159
178
|
Pass `keep_particles=True` as a keyword argument to
|
|
160
|
-
<
|
|
179
|
+
<project:#sdf_xarray.open_dataset> (for single files) or <project:#sdf_xarray.open_mfdataset> (for
|
|
161
180
|
multiple files).
|
|
162
181
|
|
|
163
182
|
```{code-cell} ipython3
|
|
@@ -168,14 +187,10 @@ sdfxr.open_dataset("tutorial_dataset_1d/0010.sdf", keep_particles=True)
|
|
|
168
187
|
|
|
169
188
|
## Loading specific variables
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
In this instance, you can optimize memory usage by loading only the data
|
|
176
|
-
you need using the keyword argument `data_vars` and passing one or more
|
|
177
|
-
variables. This creates a dataset consisting only of the given variable(s)
|
|
178
|
-
and the relevant coordinates/dimensions, significantly reducing memory
|
|
190
|
+
If you only need some of the variables, you can load just those using the
|
|
191
|
+
keyword argument `data_vars` and passing one or more variables to
|
|
192
|
+
<project:#sdf_xarray.open_mfdataset>. This creates a dataset consisting only of
|
|
193
|
+
the given variable(s) and the relevant coordinates/dimensions, reducing memory
|
|
179
194
|
consumption.
|
|
180
195
|
|
|
181
196
|
```{code-cell} ipython3
|
|
@@ -239,30 +239,42 @@ def combine_datasets(
|
|
|
239
239
|
) -> xr.Dataset:
|
|
240
240
|
"""
|
|
241
241
|
Combine all datasets using a single time dimension, optionally extract
|
|
242
|
-
data from only the listed data_vars
|
|
242
|
+
data from only the listed data_vars.
|
|
243
|
+
|
|
244
|
+
Coordinates with the "point_data" attribute come from SDF point meshes,
|
|
245
|
+
i.e. particle positions such as ``X_Particles_proton``. They have one
|
|
246
|
+
value per particle along an ``ID_<species>`` dimension rather than a
|
|
247
|
+
grid, and SDFPreprocess gives them a time dimension because particles
|
|
248
|
+
move. They are usually only in some files (with ``keep_particles=True``,
|
|
249
|
+
on dumps that output particles). When concatenating along time, xarray
|
|
250
|
+
fills a data variable missing from a file with ``NaN``'s, but raises if a
|
|
251
|
+
coordinate is missing. So each file's point coordinates are temporarily
|
|
252
|
+
turned into data variables and turned back into coordinates after
|
|
253
|
+
combining. Their names are recorded here because particle data variables
|
|
254
|
+
(e.g. ``Particles_Px_proton``) carry "point_data" too, so they can't be
|
|
255
|
+
told apart by the attribute later.
|
|
243
256
|
"""
|
|
257
|
+
point_coords: set[str] = set()
|
|
258
|
+
preprocess = SDFPreprocess(data_vars=data_vars)
|
|
244
259
|
|
|
245
|
-
|
|
246
|
-
ds =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
preprocess=SDFPreprocess(),
|
|
264
|
-
**kwargs,
|
|
265
|
-
)
|
|
260
|
+
def _preprocess(ds: xr.Dataset) -> xr.Dataset:
|
|
261
|
+
ds = preprocess(ds)
|
|
262
|
+
names = [k for k, v in ds.coords.items() if v.attrs.get("point_data", False)]
|
|
263
|
+
point_coords.update(names)
|
|
264
|
+
return ds.reset_coords(names)
|
|
265
|
+
|
|
266
|
+
ds = xr.open_mfdataset(
|
|
267
|
+
path_glob,
|
|
268
|
+
join="outer",
|
|
269
|
+
coords="minimal",
|
|
270
|
+
compat="no_conflicts",
|
|
271
|
+
combine="nested",
|
|
272
|
+
concat_dim="time",
|
|
273
|
+
data_vars="all",
|
|
274
|
+
preprocess=_preprocess,
|
|
275
|
+
**kwargs,
|
|
276
|
+
)
|
|
277
|
+
ds = ds.set_coords(sorted(point_coords))
|
|
266
278
|
|
|
267
279
|
ds.attrs["deck"] = _load_deck(ds.attrs["filename"], deck_path)
|
|
268
280
|
|
|
@@ -626,33 +638,91 @@ def make_time_dims(path_glob):
|
|
|
626
638
|
|
|
627
639
|
|
|
628
640
|
class SDFBackendArray(BackendArray):
|
|
629
|
-
"""
|
|
641
|
+
"""Adapter class required for lazy loading.
|
|
642
|
+
|
|
643
|
+
Reads go through :py:meth:`SDFFile.read`, which only reads the range of the
|
|
644
|
+
last axis that is actually requested. SDF arrays are Fortran ordered, so
|
|
645
|
+
that range is one contiguous block on disk (see
|
|
646
|
+
`sdf_xarray.SDFBackendArray.raw_indexing_method`).
|
|
647
|
+
|
|
648
|
+
``component`` is only used for particle positions. SDF stores the positions
|
|
649
|
+
of a particle species as one point mesh block, holding a 1D array of every
|
|
650
|
+
particle's position for each spatial direction. Each of these directions is
|
|
651
|
+
a component, and becomes its own coordinate, e.g. ``X_Particles_Electron``
|
|
652
|
+
and ``Y_Particles_Electron``. ``component`` is the index of the direction
|
|
653
|
+
this array holds, in the order of the mesh's labels (e.g. 0 for ``X``, 1
|
|
654
|
+
for ``Y``), and ``variable_name`` is then the name of the point mesh. For
|
|
655
|
+
every other variable ``component`` is ``None``.
|
|
656
|
+
"""
|
|
630
657
|
|
|
631
|
-
__slots__ = ("datastore", "dtype", "shape", "variable_name")
|
|
658
|
+
__slots__ = ("component", "datastore", "dtype", "shape", "variable_name")
|
|
632
659
|
|
|
633
|
-
def __init__(self, variable_name, datastore, shape, dtype):
|
|
660
|
+
def __init__(self, variable_name, datastore, shape, dtype, component=None):
|
|
634
661
|
self.datastore = datastore
|
|
635
662
|
self.variable_name = variable_name
|
|
636
663
|
self.shape = shape
|
|
637
664
|
self.dtype = dtype
|
|
665
|
+
self.component = component
|
|
638
666
|
|
|
639
|
-
def
|
|
640
|
-
with self.datastore.acquire_context(needs_lock) as ds:
|
|
641
|
-
return ds.variables[self.variable_name]
|
|
642
|
-
|
|
643
|
-
def __getitem__(self, key: indexing.ExplicitIndexer) -> np.typing.ArrayLike:
|
|
667
|
+
def __getitem__(self, indexer: indexing.ExplicitIndexer) -> np.typing.ArrayLike:
|
|
644
668
|
return indexing.explicit_indexing_adapter(
|
|
645
|
-
|
|
669
|
+
indexer,
|
|
646
670
|
self.shape,
|
|
647
|
-
indexing.IndexingSupport.
|
|
648
|
-
self.
|
|
671
|
+
indexing.IndexingSupport.BASIC,
|
|
672
|
+
self.raw_indexing_method,
|
|
649
673
|
)
|
|
650
674
|
|
|
651
|
-
def
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
675
|
+
def raw_indexing_method(self, indexer: tuple) -> np.typing.ArrayLike:
|
|
676
|
+
"""Read the data selected from disk.
|
|
677
|
+
|
|
678
|
+
xarray only calls this once the data is needed (e.g. by ``.values``,
|
|
679
|
+
``load()``, ``compute()`` or dask), with one int or slice per
|
|
680
|
+
dimension, and expects back the same array numpy would return for
|
|
681
|
+
``full_array[indexer]``. Any negative ints or steps have already
|
|
682
|
+
been made positive by xarray.
|
|
683
|
+
|
|
684
|
+
SDF stores arrays in Fortran order, where the first index changes
|
|
685
|
+
fastest. A 2D array of shape (nx, ny) is stored as all nx values at
|
|
686
|
+
y=0, then all nx values at y=1, and so on, so for nx=3 and ny=4 each
|
|
687
|
+
element's position in the file is::
|
|
688
|
+
|
|
689
|
+
y=0 y=1 y=2 y=3
|
|
690
|
+
x=0 0 3 6 9
|
|
691
|
+
x=1 1 4 7 10
|
|
692
|
+
x=2 2 5 8 11
|
|
693
|
+
|
|
694
|
+
Each column (one y, every x) is contiguous, and the columns are stored
|
|
695
|
+
one after another. So a range of the *last* index, with every other
|
|
696
|
+
dimension in full, is one contiguous block where ``[:, 1:3]`` is positions
|
|
697
|
+
3 to 8. A range of any other index is scattered through the file where
|
|
698
|
+
``[1:3, :]`` is positions 1, 2, 4, 5, 7, 8, 10 and 11. So we only read
|
|
699
|
+
the range of the last dimension that is needed from disk, then index the
|
|
700
|
+
other dimensions in memory.
|
|
701
|
+
"""
|
|
702
|
+
*other_indices, last_index = indexer
|
|
703
|
+
|
|
704
|
+
if isinstance(last_index, slice):
|
|
705
|
+
start, stop, step = last_index.indices(self.shape[-1])
|
|
706
|
+
# An empty slice such as 5:1 has stop < start, so read nothing
|
|
707
|
+
stop = max(start, stop)
|
|
708
|
+
data = self.read_last_dim(start, stop)
|
|
709
|
+
# SDF can't skip elements, so a step such as ::2 is applied here
|
|
710
|
+
return data[*other_indices, ::step]
|
|
711
|
+
|
|
712
|
+
data = self.read_last_dim(last_index, last_index + 1)
|
|
713
|
+
# Index with 0, not 0:1, so the dimension is dropped as numpy would
|
|
714
|
+
return data[*other_indices, 0]
|
|
715
|
+
|
|
716
|
+
def read_last_dim(self, start: int, stop: int) -> np.ndarray:
|
|
717
|
+
"""Read the range [start, stop) of the last dimension from disk."""
|
|
718
|
+
with self.datastore.acquire_context() as sdf_file:
|
|
719
|
+
if self.component is None:
|
|
720
|
+
variable = sdf_file.variables[self.variable_name]
|
|
721
|
+
return sdf_file.read(variable, start, stop)
|
|
722
|
+
|
|
723
|
+
# Particle positions: one component (e.g. x) of a point mesh
|
|
724
|
+
mesh = sdf_file.grids[self.variable_name]
|
|
725
|
+
return sdf_file.read_mesh_component(mesh, self.component, start, stop)
|
|
656
726
|
|
|
657
727
|
|
|
658
728
|
class SDFDataStore(AbstractDataStore):
|
|
@@ -765,7 +835,25 @@ class SDFDataStore(AbstractDataStore):
|
|
|
765
835
|
|
|
766
836
|
base_name = _process_grid_name(value.name, _norm_grid_name)
|
|
767
837
|
|
|
768
|
-
|
|
838
|
+
if value.is_point_data:
|
|
839
|
+
# Particle positions are not dimension coordinates, so they
|
|
840
|
+
# don't need to be in memory to build an index; keep them lazy
|
|
841
|
+
coord_data = [
|
|
842
|
+
indexing.LazilyIndexedArray(
|
|
843
|
+
SDFBackendArray(
|
|
844
|
+
key,
|
|
845
|
+
self,
|
|
846
|
+
shape=value.shape[:1],
|
|
847
|
+
dtype=value.dtype,
|
|
848
|
+
component=component,
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
for component in range(len(value.labels))
|
|
852
|
+
]
|
|
853
|
+
else:
|
|
854
|
+
coord_data = value.data
|
|
855
|
+
|
|
856
|
+
for label, coord, unit in zip(value.labels, coord_data, value.units):
|
|
769
857
|
full_name = f"{label}_{base_name}"
|
|
770
858
|
dim_name = (
|
|
771
859
|
f"ID_{_process_grid_name(key, _grid_species_name)}"
|
|
@@ -801,7 +889,14 @@ class SDFDataStore(AbstractDataStore):
|
|
|
801
889
|
# scalar, or because it's an array over something
|
|
802
890
|
# else. We have no more information, so just make up
|
|
803
891
|
# some (hopefully) unique dimension names
|
|
804
|
-
|
|
892
|
+
if isinstance(value, Constant):
|
|
893
|
+
shape = ()
|
|
894
|
+
data = value.data
|
|
895
|
+
else:
|
|
896
|
+
shape = value.shape
|
|
897
|
+
data = indexing.LazilyIndexedArray(
|
|
898
|
+
SDFBackendArray(key, self, shape=shape, dtype=value.dtype)
|
|
899
|
+
)
|
|
805
900
|
dims = [f"dim_{key}_{n}" for n, _ in enumerate(shape)]
|
|
806
901
|
base_name = _rename_with_underscore(key)
|
|
807
902
|
|
|
@@ -811,7 +906,7 @@ class SDFDataStore(AbstractDataStore):
|
|
|
811
906
|
if value.units is not None:
|
|
812
907
|
data_attrs["units"] = value.units
|
|
813
908
|
|
|
814
|
-
var = Variable(dims,
|
|
909
|
+
var = Variable(dims, data, attrs=data_attrs)
|
|
815
910
|
|
|
816
911
|
# Provide preferred_chunks for constants so dask aligns to natural shapes
|
|
817
912
|
var.encoding["preferred_chunks"] = dict(zip(dims, shape))
|
|
@@ -884,14 +979,14 @@ class SDFDataStore(AbstractDataStore):
|
|
|
884
979
|
"long_name": long_name,
|
|
885
980
|
}
|
|
886
981
|
lazy_data = indexing.LazilyIndexedArray(
|
|
887
|
-
SDFBackendArray(key, self, shape=value.shape, dtype=value.
|
|
982
|
+
SDFBackendArray(key, self, shape=value.shape, dtype=value.dtype)
|
|
888
983
|
)
|
|
889
984
|
var = Variable(var_coords, lazy_data, data_attrs)
|
|
890
985
|
# Set preferred chunks to match on-disk layout
|
|
891
986
|
# For point data (1D): full dimension
|
|
892
987
|
# For grid data (N-D): individual grid chunk sizes
|
|
893
988
|
if value.is_point_data:
|
|
894
|
-
var.encoding["preferred_chunks"] = {var_coords[0]:
|
|
989
|
+
var.encoding["preferred_chunks"] = {var_coords[0]: value.shape[0]}
|
|
895
990
|
else:
|
|
896
991
|
# Align with on-disk grid structure
|
|
897
992
|
chunk_dict = {}
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.10.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 10, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g878b38c62'
|
|
@@ -61,6 +61,7 @@ cdef extern from "sdf.h":
|
|
|
61
61
|
int64_t dims[SDF_MAXDIMS]
|
|
62
62
|
int64_t local_dims[SDF_MAXDIMS]
|
|
63
63
|
int64_t data_length
|
|
64
|
+
int64_t data_location
|
|
64
65
|
int32_t ndims, geometry, datatype, blocktype
|
|
65
66
|
int32_t stagger, datatype_out
|
|
66
67
|
char const_value[16]
|
|
@@ -125,3 +126,4 @@ cdef extern from "sdf_helper.h":
|
|
|
125
126
|
cdef extern from "stack_allocator.h":
|
|
126
127
|
void sdf_stack_destroy(sdf_file_t *h)
|
|
127
128
|
void sdf_stack_init(sdf_file_t *h)
|
|
129
|
+
void sdf_stack_free_block(sdf_file_t *h, sdf_block_t *b)
|
|
@@ -58,18 +58,25 @@ def fetch_dataset(
|
|
|
58
58
|
import pooch # noqa: PLC0415
|
|
59
59
|
|
|
60
60
|
logger = pooch.get_logger()
|
|
61
|
-
datasets = pooch.create(
|
|
62
|
-
path=pooch.os_cache("sdf_datasets"),
|
|
63
|
-
base_url="doi:10.5281/zenodo.17618509",
|
|
64
|
-
registry=None,
|
|
65
|
-
retry_if_failed=10,
|
|
66
|
-
)
|
|
67
|
-
datasets.load_registry_from_doi()
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
cache_dir = Path(pooch.os_cache("sdf_datasets"))
|
|
63
|
+
cache_path = cache_dir / dataset_name
|
|
64
|
+
|
|
65
|
+
# Only download dataset if a local copy is not found
|
|
66
|
+
if not cache_path.is_dir():
|
|
67
|
+
datasets = pooch.create(
|
|
68
|
+
path=cache_dir,
|
|
69
|
+
base_url="doi:10.5281/zenodo.17618509",
|
|
70
|
+
registry=None,
|
|
71
|
+
retry_if_failed=10,
|
|
72
|
+
)
|
|
73
|
+
datasets.load_registry_from_doi()
|
|
74
|
+
|
|
75
|
+
datasets.fetch(
|
|
76
|
+
f"{dataset_name}.zip",
|
|
77
|
+
processor=pooch.Unzip(extract_dir="."),
|
|
78
|
+
progressbar=True,
|
|
79
|
+
)
|
|
73
80
|
|
|
74
81
|
if save_path is not None:
|
|
75
82
|
save_path = Path(save_path)
|
|
@@ -167,6 +167,7 @@ cdef class SDFFile:
|
|
|
167
167
|
cdef public dict[str, Mesh] grids
|
|
168
168
|
|
|
169
169
|
def __cinit__(self, filename: str):
|
|
170
|
+
self.filename = filename
|
|
170
171
|
self._c_sdf_file = csdf.sdf_open(
|
|
171
172
|
filename.encode("UTF-8"), 0, csdf.SDF_READ, False
|
|
172
173
|
)
|
|
@@ -254,8 +255,14 @@ cdef class SDFFile:
|
|
|
254
255
|
|
|
255
256
|
block = block.next
|
|
256
257
|
|
|
257
|
-
cpdef read(self, var: Block):
|
|
258
|
+
cpdef read(self, var: Block, start=None, stop=None):
|
|
258
259
|
"""Read a variable from the file, returning numpy array
|
|
260
|
+
|
|
261
|
+
For variables, ``start`` and ``stop`` select a range of the last index,
|
|
262
|
+
i.e. ``var[..., start:stop]``, so that only that part of the variable is
|
|
263
|
+
read from disk. As SDF arrays are Fortran ordered (the first index
|
|
264
|
+
changes fastest), this range is one contiguous block on disk, so it is
|
|
265
|
+
a single read.
|
|
259
266
|
"""
|
|
260
267
|
|
|
261
268
|
if self._c_sdf_file is NULL:
|
|
@@ -276,23 +283,112 @@ cdef class SDFFile:
|
|
|
276
283
|
if block is NULL:
|
|
277
284
|
raise RuntimeError(f"Could not read variable '{var.name}'")
|
|
278
285
|
|
|
286
|
+
shape = var.shape
|
|
287
|
+
cdef bint is_section = False
|
|
288
|
+
cdef csdf.int64_t starts[csdf.SDF_MAXDIMS]
|
|
289
|
+
cdef csdf.int64_t ends[csdf.SDF_MAXDIMS]
|
|
290
|
+
if not is_mesh and shape and (start is not None or stop is not None):
|
|
291
|
+
start = 0 if start is None else start
|
|
292
|
+
stop = shape[-1] if stop is None else stop
|
|
293
|
+
if not 0 <= start <= stop <= shape[-1]:
|
|
294
|
+
raise IndexError(
|
|
295
|
+
f"Invalid range [{start}, {stop}) for '{var.name}' "
|
|
296
|
+
f"with shape {shape}"
|
|
297
|
+
)
|
|
298
|
+
if start == stop:
|
|
299
|
+
# Nothing to read, and the library can't read an empty section
|
|
300
|
+
return np.empty((*shape[:-1], 0), dtype=var.dtype)
|
|
301
|
+
is_section = (start, stop) != (0, shape[-1])
|
|
302
|
+
|
|
303
|
+
if is_section and block.blocktype not in (
|
|
304
|
+
csdf.SDF_BLOCKTYPE_PLAIN_VARIABLE, csdf.SDF_BLOCKTYPE_POINT_VARIABLE
|
|
305
|
+
):
|
|
306
|
+
# Only these readers support array sections (`sdf_read_array`, for
|
|
307
|
+
# example, ignores them), so read everything and slice it here
|
|
308
|
+
return self.read(var)[..., start:stop].copy()
|
|
309
|
+
|
|
310
|
+
if is_section:
|
|
311
|
+
for i in range(block.ndims):
|
|
312
|
+
# The array section is clamped to the local (per-rank) dims,
|
|
313
|
+
# which for point data are only set during the read itself.
|
|
314
|
+
# For serial reads these are always the global dims
|
|
315
|
+
block.local_dims[i] = block.dims[i]
|
|
316
|
+
starts[i] = 0
|
|
317
|
+
ends[i] = block.dims[i]
|
|
318
|
+
starts[block.ndims - 1] = start
|
|
319
|
+
ends[block.ndims - 1] = stop
|
|
320
|
+
csdf.sdf_block_set_array_section(block, block.ndims, starts, ends, NULL)
|
|
321
|
+
shape = (*shape[:-1], stop - start)
|
|
322
|
+
|
|
279
323
|
self._c_sdf_file.current_block = block
|
|
280
|
-
|
|
324
|
+
try:
|
|
325
|
+
csdf.sdf_helper_read_data(self._c_sdf_file, block)
|
|
326
|
+
|
|
327
|
+
if is_mesh:
|
|
328
|
+
# Meshes store the data for separate dimensions in block.grids
|
|
329
|
+
if not (block.grids is not NULL and block.grids[0] is not NULL):
|
|
330
|
+
raise RuntimeError(f"Could not read variable '{var.name}'")
|
|
331
|
+
|
|
332
|
+
data = []
|
|
333
|
+
for i, dim in enumerate(shape):
|
|
334
|
+
data.append(
|
|
335
|
+
self._make_array((dim,), var.dtype, block.grids[i])
|
|
336
|
+
)
|
|
337
|
+
return tuple(data)
|
|
338
|
+
|
|
339
|
+
# Normal variables
|
|
340
|
+
return self._make_array(shape, var.dtype, block.data)
|
|
341
|
+
finally:
|
|
342
|
+
# The library keeps every block it reads cached until the file is
|
|
343
|
+
# closed, but we've just copied it into numpy, so release it now.
|
|
344
|
+
# Otherwise we hold two copies of everything that's been read
|
|
345
|
+
csdf.sdf_stack_free_block(self._c_sdf_file, block)
|
|
346
|
+
if is_section:
|
|
347
|
+
csdf.sdf_block_set_array_section(block, block.ndims, NULL, NULL, NULL)
|
|
348
|
+
|
|
349
|
+
cpdef read_mesh_component(self, mesh: Mesh, int component, start=None, stop=None):
|
|
350
|
+
"""Read one component (e.g. just ``X``) of a mesh, returning numpy array
|
|
351
|
+
|
|
352
|
+
For point meshes, ``start`` and ``stop`` select a range of points so
|
|
353
|
+
that only those are read from disk.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if self._c_sdf_file is NULL:
|
|
357
|
+
raise RuntimeError(
|
|
358
|
+
f"Can't read '{mesh.name}', file '{self.filename}' is closed"
|
|
359
|
+
)
|
|
281
360
|
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
if not (block.grids is not NULL and block.grids[0] is not NULL):
|
|
285
|
-
raise RuntimeError(f"Could not read variable '{var.name}'")
|
|
361
|
+
if not mesh.is_point_data or mesh.parent:
|
|
362
|
+
return self.read(mesh)[component][start:stop]
|
|
286
363
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
self._make_array((dim,), var.dtype, block.grids[i])
|
|
291
|
-
)
|
|
292
|
-
return tuple(data)
|
|
364
|
+
cdef csdf.sdf_block_t* block = csdf.sdf_find_block_by_name(
|
|
365
|
+
self._c_sdf_file, mesh.name.encode("utf-8")
|
|
366
|
+
)
|
|
293
367
|
|
|
294
|
-
|
|
295
|
-
|
|
368
|
+
if block is NULL:
|
|
369
|
+
raise RuntimeError(f"Could not read variable '{mesh.name}'")
|
|
370
|
+
|
|
371
|
+
if block.datatype != block.datatype_out:
|
|
372
|
+
# The library is converting the type on read, so let it
|
|
373
|
+
return self.read(mesh)[component][start:stop]
|
|
374
|
+
|
|
375
|
+
# The library always reads every component of a point mesh, and its
|
|
376
|
+
# array sections don't work for them. The components are just stored
|
|
377
|
+
# one after the other though (see `sdf_read_point_mesh`), so read the
|
|
378
|
+
# part we want directly
|
|
379
|
+
npoints = block.dims[0]
|
|
380
|
+
start = 0 if start is None else start
|
|
381
|
+
stop = npoints if stop is None else stop
|
|
382
|
+
if not 0 <= component < block.ndims or not 0 <= start <= stop <= npoints:
|
|
383
|
+
raise IndexError(
|
|
384
|
+
f"Invalid component {component} or range [{start}, {stop}) "
|
|
385
|
+
f"for '{mesh.name}' with {block.ndims} components of {npoints} points"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
offset = block.data_location + (component * npoints + start) * mesh.dtype.itemsize
|
|
389
|
+
return np.fromfile(
|
|
390
|
+
self.filename, dtype=mesh.dtype, count=stop - start, offset=offset
|
|
391
|
+
)
|
|
296
392
|
|
|
297
393
|
cdef _read_mid_grid(self, mesh: Mesh):
|
|
298
394
|
"""Read a midpoint grid"""
|
|
@@ -138,3 +138,49 @@ if __name__ == "__main__":
|
|
|
138
138
|
pprint(f.variables["Electric Field/Ex"].data)
|
|
139
139
|
print("grid")
|
|
140
140
|
pprint(f.grids["grid"].data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_read_section_matches_full_read():
|
|
144
|
+
with SDFFile(str(TEST_FILES_DIR / "0010.sdf")) as f:
|
|
145
|
+
dropped_key_affixes = ["cpu", "output file", "boundary"]
|
|
146
|
+
variables = [
|
|
147
|
+
v
|
|
148
|
+
for v in f.variables.values()
|
|
149
|
+
if getattr(v, "shape", ())
|
|
150
|
+
and all(key not in v.name.lower() for key in dropped_key_affixes)
|
|
151
|
+
]
|
|
152
|
+
assert any(v.is_point_data for v in variables)
|
|
153
|
+
for var in variables:
|
|
154
|
+
full = f.read(var)
|
|
155
|
+
size = var.shape[-1]
|
|
156
|
+
ranges = [(0, 0), (0, 1), (1, size), (size // 3, size // 2 + 1)]
|
|
157
|
+
for start, stop in ranges:
|
|
158
|
+
npt.assert_array_equal(
|
|
159
|
+
f.read(var, start, stop), full[..., start:stop], err_msg=var.name
|
|
160
|
+
)
|
|
161
|
+
# Reading a section must not affect later full reads
|
|
162
|
+
npt.assert_array_equal(f.read(var), full, err_msg=var.name)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_read_section_out_of_range():
|
|
166
|
+
with SDFFile(str(TEST_FILES_DIR / "0010.sdf")) as f:
|
|
167
|
+
var = f.variables["Electric Field/Ex"]
|
|
168
|
+
with pytest.raises(IndexError):
|
|
169
|
+
f.read(var, 0, var.shape[-1] + 1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_read_mesh_component_matches_full_read():
|
|
173
|
+
with SDFFile(str(TEST_FILES_DIR / "0010.sdf")) as f:
|
|
174
|
+
meshes = [g for g in f.grids.values() if g.is_point_data]
|
|
175
|
+
assert meshes
|
|
176
|
+
for mesh in meshes:
|
|
177
|
+
full = f.read(mesh)
|
|
178
|
+
size = mesh.shape[0]
|
|
179
|
+
for component in range(len(mesh.labels)):
|
|
180
|
+
npt.assert_array_equal(
|
|
181
|
+
f.read_mesh_component(mesh, component), full[component]
|
|
182
|
+
)
|
|
183
|
+
npt.assert_array_equal(
|
|
184
|
+
f.read_mesh_component(mesh, component, 1, size // 2),
|
|
185
|
+
full[component][1 : size // 2],
|
|
186
|
+
)
|
|
@@ -7,6 +7,7 @@ import xarray as xr
|
|
|
7
7
|
|
|
8
8
|
import sdf_xarray as sdfxr
|
|
9
9
|
from sdf_xarray import (
|
|
10
|
+
SDFFile,
|
|
10
11
|
SDFPreprocess,
|
|
11
12
|
_load_deck,
|
|
12
13
|
_process_latex_name,
|
|
@@ -19,6 +20,7 @@ TEST_MISMATCHED_FILES_DIR = download.fetch_dataset("test_mismatched_files")
|
|
|
19
20
|
TEST_ARRAYS_DIR = download.fetch_dataset("test_array_no_grids")
|
|
20
21
|
TEST_3D_DIST_FN = download.fetch_dataset("test_dist_fn")
|
|
21
22
|
TEST_2D_PARTICLE_DATA = download.fetch_dataset("test_two_probes_2D")
|
|
23
|
+
TEST_2D_MOVING_WINDOW = download.fetch_dataset("test_files_2D_moving_window")
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
# Type hinting support
|
|
@@ -206,6 +208,33 @@ def test_multiple_files_multiple_time_dims():
|
|
|
206
208
|
assert df["Absorption_Total_Laser_Energy_Injected"].shape == (11,)
|
|
207
209
|
|
|
208
210
|
|
|
211
|
+
def test_open_mfdataset_varying_grid():
|
|
212
|
+
# A grid that differs between files must not become a concat dimension
|
|
213
|
+
paths = sorted(TEST_2D_MOVING_WINDOW.glob("*.sdf"))
|
|
214
|
+
with sdfxr.open_mfdataset(paths) as df:
|
|
215
|
+
assert df.sizes["time"] == len(paths)
|
|
216
|
+
assert df["Wall_time"].dims == ("time",)
|
|
217
|
+
density = df["Derived_Number_Density_electron"]
|
|
218
|
+
assert density.dims == ("time", "X_Grid_mid", "Y_Grid_mid")
|
|
219
|
+
assert df.indexes["X_Grid_mid"].is_monotonic_increasing
|
|
220
|
+
|
|
221
|
+
for i, path in enumerate(paths):
|
|
222
|
+
with xr.open_dataset(path) as single:
|
|
223
|
+
expected = single["Derived_Number_Density_electron"]
|
|
224
|
+
actual = density.isel(time=i).sel(X_Grid_mid=expected["X_Grid_mid"])
|
|
225
|
+
npt.assert_array_equal(actual, expected)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_open_mfdataset_sparse_dist_fn():
|
|
229
|
+
# The first file's dist_fn and its grid are missing from the last file
|
|
230
|
+
with sdfxr.open_mfdataset(TEST_3D_DIST_FN.glob("*.sdf")) as df:
|
|
231
|
+
dist_fn = df["dist_fn_x_px_py_Electron"]
|
|
232
|
+
assert df.sizes["time"] == 3
|
|
233
|
+
assert dist_fn.shape == (3, 16, 20, 20)
|
|
234
|
+
assert dist_fn.isel(time=slice(0, 2)).notnull().all()
|
|
235
|
+
assert dist_fn.isel(time=2).isnull().all()
|
|
236
|
+
|
|
237
|
+
|
|
209
238
|
def test_resolve_paths_from_string_pattern():
|
|
210
239
|
pattern = str(TEST_FILES_DIR / "*.sdf")
|
|
211
240
|
result = resolve_paths(pattern)
|
|
@@ -594,6 +623,30 @@ def test_open_mfdataset_data_vars_sparse_time():
|
|
|
594
623
|
npt.assert_allclose(time_values, time.values, rtol=1e-6)
|
|
595
624
|
|
|
596
625
|
|
|
626
|
+
def test_open_mfdataset_data_vars_sparse_particles():
|
|
627
|
+
with sdfxr.open_mfdataset(
|
|
628
|
+
TEST_FILES_DIR.glob("*.sdf"),
|
|
629
|
+
keep_particles=True,
|
|
630
|
+
data_vars=["Particles_Px_proton"],
|
|
631
|
+
) as df:
|
|
632
|
+
px_protons = df["Particles_Px_proton"]
|
|
633
|
+
assert sorted(px_protons.coords) == sorted(("X_Particles_proton", "time"))
|
|
634
|
+
assert px_protons.shape == (11, 1920)
|
|
635
|
+
has_data = px_protons.notnull().any("ID_proton").values
|
|
636
|
+
assert has_data.tolist() == [False] * 10 + [True]
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def test_open_mfdataset_data_vars_sparse_dist_fn():
|
|
640
|
+
with sdfxr.open_mfdataset(
|
|
641
|
+
TEST_3D_DIST_FN.glob("*.sdf"),
|
|
642
|
+
data_vars=["dist_fn_x_px_py_Electron"],
|
|
643
|
+
) as df:
|
|
644
|
+
dist_fn = df["dist_fn_x_px_py_Electron"]
|
|
645
|
+
assert dist_fn.shape == (3, 16, 20, 20)
|
|
646
|
+
assert dist_fn.isel(time=slice(0, 2)).notnull().all()
|
|
647
|
+
assert dist_fn.isel(time=2).isnull().all()
|
|
648
|
+
|
|
649
|
+
|
|
597
650
|
def test_open_mfdataset_data_vars_separate_times_single():
|
|
598
651
|
with sdfxr.open_mfdataset(
|
|
599
652
|
TEST_FILES_DIR.glob("*.sdf"),
|
|
@@ -765,3 +818,37 @@ def test_epydeck_parsing_failing_deck(tmp_path, capsys):
|
|
|
765
818
|
_load_deck("", deck)
|
|
766
819
|
out, _ = capsys.readouterr()
|
|
767
820
|
assert "error occurred while trying to load the input deck" in out
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def test_lazy_particle_coords():
|
|
824
|
+
with sdfxr.open_dataset(TEST_FILES_DIR / "0010.sdf", keep_particles=True) as df:
|
|
825
|
+
x_coord = df["X_Particles_proton"]
|
|
826
|
+
assert not x_coord.variable._in_memory
|
|
827
|
+
with SDFFile(str(TEST_FILES_DIR / "0010.sdf")) as f:
|
|
828
|
+
mesh = next(
|
|
829
|
+
g for g in f.grids.values() if g.name == x_coord.attrs["full_name"]
|
|
830
|
+
)
|
|
831
|
+
expected = f.read(mesh)[mesh.labels.index("X")]
|
|
832
|
+
npt.assert_array_equal(x_coord[2:-3].values, expected[2:-3])
|
|
833
|
+
npt.assert_array_equal(x_coord.values, expected)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@pytest.mark.parametrize(
|
|
837
|
+
"indexers",
|
|
838
|
+
[
|
|
839
|
+
{"dim0": [0, 2, 5], "dim1": [1, 3]},
|
|
840
|
+
{"dim0": [5, 0], "dim2": slice(10, 2, -2)},
|
|
841
|
+
{"dim1": -1, "dim2": [0, -1]},
|
|
842
|
+
],
|
|
843
|
+
)
|
|
844
|
+
def test_lazy_indexing_matches_eager(indexers):
|
|
845
|
+
path = TEST_3D_DIST_FN / "0000.sdf"
|
|
846
|
+
name = "dist_fn_x_px_py_Electron"
|
|
847
|
+
with xr.open_dataset(path) as eager:
|
|
848
|
+
expected = eager[name].load()
|
|
849
|
+
with xr.open_dataset(path) as lazy:
|
|
850
|
+
dims = lazy[name].dims
|
|
851
|
+
indexers = {dims[int(k[-1])]: v for k, v in indexers.items()}
|
|
852
|
+
npt.assert_array_equal(
|
|
853
|
+
lazy[name].isel(indexers).values, expected.isel(indexers).values
|
|
854
|
+
)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Known Issues
|
|
2
|
-
|
|
3
|
-
There are a couple of known 'quirks' in `sdf_xarray`:
|
|
4
|
-
|
|
5
|
-
- [Issue #57](https://github.com/epochpic/sdf-xarray/issues/57) Loading multiple
|
|
6
|
-
SDF files with <project:#sdf_xarray.open_mfdataset> can lead to out-of-memory
|
|
7
|
-
errors. The issue is believed to stem from how the underlying <inv:#xarray> library
|
|
8
|
-
handles coordinates, causing it to infer an excessively large array shape that
|
|
9
|
-
requests far more memory than is needed. Due to the significant architectural
|
|
10
|
-
changes required for a fix, the maintainers do not plan to resolve this. The
|
|
11
|
-
recommended solution is to load the files individually or in smaller batches.
|
|
12
|
-
- [Issue #112](https://github.com/epochpic/sdf-xarray/issues/112) Due to the
|
|
13
|
-
way [cylindrical EPOCH](https://github.com/epochpic/cylindrical_epoch) sets up
|
|
14
|
-
grids we can't currently load it's SDF files. If there is enough interest in this
|
|
15
|
-
we will attempt to find a solution otherwise it shall remain low priority.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/animating/animate_1D_density.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/histograms/plot_hist_ke_probe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_1D_poynting_flux.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_2D_poynting_flux.py
RENAMED
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_time_temperature.py
RENAMED
|
File without changes
|
{sdf_xarray-0.9.0 → sdf_xarray-0.10.0}/docs/epoch_workshop_2026/plotting/plot_x_px_scatter.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|