Skip to content

Commit 6741180

Browse files
Merge pull request #66 from epochpic/multiple_probes
Fixed mutiple probes not working in the same file
2 parents a4211f1 + 4f30ece commit 6741180

File tree

7 files changed

+271
-4
lines changed

7 files changed

+271
-4
lines changed

docs/key_functionality.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ done via the ``preprocess`` parameter.
5151
Reading particle data
5252
~~~~~~~~~~~~~~~~~~~~~
5353

54+
.. warning::
55+
It is **not recommended** to use :func:`xarray.open_mfdataset` or :func:`open_mfdataset` to load particle data from multiple SDF outputs. The number of particles often varies between outputs, which can lead to inconsistent array shapes that these functions cannot handle. Instead, consider loading each file individually and then concatenating them manually.
56+
57+
.. note::
58+
When loading multiple probes from a single SDF file, you **must** use the `probe_names` parameter to assign a unique name to each. For example, use `probe_names=["Front_Electron_Probe", "Back_Electron_Probe"]`. Failing to do so will result in dimension name conflicts.
59+
5460
By default, particle data isn't kept as it takes up a lot of space.
5561
Pass ``keep_particles=True`` as a keyword argument to
5662
:func:`xarray.open_dataset` (for single files) or :func:`xarray.open_mfdataset` (for

src/sdf_xarray/__init__.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def open_mfdataset(
6969
*,
7070
separate_times: bool = False,
7171
keep_particles: bool = False,
72+
probe_names: list[str] | None = None,
7273
) -> xr.Dataset:
7374
"""Open a set of EPOCH SDF files as one `xarray.Dataset`
7475
@@ -98,6 +99,8 @@ def open_mfdataset(
9899
different output frequencies
99100
keep_particles :
100101
If ``True``, also load particle data (this may use a lot of memory!)
102+
probe_names :
103+
List of EPOCH probe names
101104
"""
102105

103106
# TODO: This is not very robust, look at how xarray.open_mfdataset does it
@@ -108,10 +111,15 @@ def open_mfdataset(
108111
path_glob = sorted(list(path_glob)) # noqa: C414
109112

110113
if not separate_times:
111-
return combine_datasets(path_glob, keep_particles=keep_particles)
114+
return combine_datasets(
115+
path_glob, keep_particles=keep_particles, probe_names=probe_names
116+
)
112117

113118
time_dims, var_times_map = make_time_dims(path_glob)
114-
all_dfs = [xr.open_dataset(f, keep_particles=keep_particles) for f in path_glob]
119+
all_dfs = [
120+
xr.open_dataset(f, keep_particles=keep_particles, probe_names=probe_names)
121+
for f in path_glob
122+
]
115123

116124
for df in all_dfs:
117125
for da in df:
@@ -211,14 +219,23 @@ class SDFDataStore(AbstractDataStore):
211219
"drop_variables",
212220
"keep_particles",
213221
"lock",
222+
"probe_names",
214223
)
215224

216-
def __init__(self, manager, drop_variables=None, keep_particles=False, lock=None):
225+
def __init__(
226+
self,
227+
manager,
228+
drop_variables=None,
229+
keep_particles=False,
230+
lock=None,
231+
probe_names=None,
232+
):
217233
self._manager = manager
218234
self._filename = self.ds.filename
219235
self.drop_variables = drop_variables
220236
self.keep_particles = keep_particles
221237
self.lock = ensure_lock(lock)
238+
self.probe_names = probe_names
222239

223240
@classmethod
224241
def open(
@@ -227,6 +244,7 @@ def open(
227244
lock=None,
228245
drop_variables=None,
229246
keep_particles=False,
247+
probe_names=None,
230248
):
231249
if isinstance(filename, os.PathLike):
232250
filename = os.fspath(filename)
@@ -237,6 +255,7 @@ def open(
237255
lock=lock,
238256
drop_variables=drop_variables,
239257
keep_particles=keep_particles,
258+
probe_names=probe_names,
240259
)
241260

242261
def _acquire(self, needs_lock=True):
@@ -347,7 +366,28 @@ def _process_grid_name(grid_name: str, transform_func) -> str:
347366

348367
if value.is_point_data:
349368
# Point (particle) variables are 1D
350-
var_coords = (f"ID_{_process_grid_name(key, _grid_species_name)}",)
369+
370+
# Particle data does not maintain a fixed dimension size
371+
# throughout the simulation. An example of a particle name comes
372+
# in the form of `Particles/Px/Ion_H` which is then modified
373+
# using `_process_grid_name()` into `Ion_H`. This is fine as the
374+
# other components of the momentum (`Py`, `Pz`) will have the same
375+
# size as they represent the same bunch of particles.
376+
377+
# Probes however have names in the form of `Electron_Front_Probe/Px`
378+
# which are changed to just `Px`; this is fine when there is only one
379+
# probe in the system but when there are multiple they will have
380+
# conflicting sizes so we can't keep the names as simply `Px` so we
381+
# instead set their dimension as the full name `Electron_Front_Probe_Px`.
382+
is_probe_name_match = self.probe_names is not None and any(
383+
name in key for name in self.probe_names
384+
)
385+
name_processor = (
386+
_rename_with_underscore
387+
if is_probe_name_match
388+
else _grid_species_name
389+
)
390+
var_coords = (f"ID_{_process_grid_name(key, name_processor)}",)
351391
else:
352392
# These are DataArrays
353393

@@ -414,6 +454,7 @@ def open_dataset(
414454
*,
415455
drop_variables=None,
416456
keep_particles=False,
457+
probe_names=None,
417458
):
418459
if isinstance(filename_or_obj, Path):
419460
# sdf library takes a filename only
@@ -424,6 +465,7 @@ def open_dataset(
424465
filename_or_obj,
425466
drop_variables=drop_variables,
426467
keep_particles=keep_particles,
468+
probe_names=probe_names,
427469
)
428470
with close_on_error(store):
429471
return store.load()
@@ -432,6 +474,7 @@ def open_dataset(
432474
"filename_or_obj",
433475
"drop_variables",
434476
"keep_particles",
477+
"probe_names",
435478
]
436479

437480
def guess_can_open(self, filename_or_obj):
7 MB
Binary file not shown.
6.85 MB
Binary file not shown.
6.18 MB
Binary file not shown.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
2+
### USER DEFINED CONSTANTS ###
3+
begin:constant
4+
# User input constants
5+
n_elec = 6e+29 #average electron number density
6+
I_peak = 1e+28 #peak intensity of the laser
7+
8+
scale_length = 0.5e-6 #scale length of the target
9+
L_target_x = 1.5e-6 #length of the target
10+
L_target_y = 10e-6 #width of the target
11+
12+
lambda_L = 800e-9 #wavelength of the laser
13+
y_fwhm_L = 2e-6 #intensity fwhm in y
14+
tau_fwhm_L = 16e-15 #intensity fwhm in t
15+
16+
mppc = 100 #macro particles per cell
17+
end:constant
18+
19+
begin:control
20+
nx = 150
21+
ny = 200
22+
nsteps = -1
23+
t_end = 200e-15
24+
x_min = -10e-06
25+
x_max = 5e-06
26+
y_min = -10e-06
27+
y_max = -y_min
28+
nparticles = nint(((L_target_x * L_target_y) / ((x_max - x_min) * (y_max - y_min))) * nx * ny * mppc)
29+
dt_multiplier = 0.8
30+
smooth_currents = T
31+
dlb_threshold = 0.8
32+
stdout_frequency = 100 # Print ETA
33+
particle_tstart = 30e-15 # Time before the laser hits the target
34+
end:control
35+
36+
### CALCULATD CONSTANTS ###
37+
begin:constant
38+
w_t = tau_fwhm_L / (2*sqrt(loge(2))) # Beam waist in t
39+
w_y = y_fwhm_L / (2*sqrt(loge(2))) # Beam waist in y
40+
t_hw01m = 0.5 * tau_fwhm_L * sqrt(loge(10)/loge(2))
41+
42+
x_foc = -x_min # Boundary to focal point distance
43+
k_L = 2.0 * pi / lambda_L # Laser wave number
44+
x_Rr = pi * w_y^2 / lambda_L # Rayleigh range
45+
omega_L = 2 * pi * c / lambda_L # Angular frequency of the laser
46+
47+
w_boundary = w_y * sqrt(1 + (x_foc/x_Rr)^2) # Waist on boundary
48+
I_boundary = I_peak * (w_y / w_boundary)^2 # Intens. on boundary
49+
r_curve = x_foc * (1 + (x_Rr/x_foc)^2) # Boundary curv. rad.
50+
phase_Gouy = atan(-x_foc/r_curve) # Boundary Gouy shift
51+
end:constant
52+
53+
begin:laser
54+
boundary = x_min
55+
intensity = I_boundary
56+
omega = omega_L
57+
phase = k_L * y^2 / (2 * r_curve) - phase_Gouy
58+
profile = gauss(y, 0, w_boundary)
59+
t_profile = gauss(time, t_hw01m, w_t)
60+
t_start = 0.0
61+
t_end = end
62+
end:laser
63+
64+
begin:qed
65+
use_qed = F
66+
qed_start_time = 0
67+
produce_photons = T
68+
photon_energy_min = 1 * kev
69+
produce_pairs = T
70+
photon_dynamics = T
71+
end:qed
72+
73+
begin:collisions
74+
use_collisions = T
75+
coulomb_log = auto
76+
collide = all
77+
end:collisions
78+
79+
begin:boundaries
80+
bc_x_min_particle = simple_outflow
81+
bc_x_max_particle = simple_outflow
82+
bc_y_min_particle = reflect
83+
bc_y_max_particle = reflect
84+
bc_x_min_field = simple_laser
85+
bc_x_max_field = conduct
86+
bc_y_min_field = conduct
87+
bc_y_max_field = conduct
88+
end:boundaries
89+
90+
begin:species
91+
name = Electron
92+
frac = 0.5
93+
dump = T
94+
temperature = 300
95+
number_density = if((abs(y) lt L_target_y/2), if((x lt 0) or (x gt L_target_x), 0, if((x gt 0) and (x lt scale_length), n_elec * (1-exp(x/scale_length))/(1-exp(1)), n_elec)), 0)
96+
number_density_min = 1
97+
identify:electron
98+
end:species
99+
100+
begin:species
101+
name = Ion_C
102+
charge = 6
103+
atomic_number = 12
104+
mass = 1836.2 * 12
105+
frac = 0.25
106+
dump = T
107+
number_density = number_density(Electron) * (6/7)
108+
temperature = temperature_x(Electron)
109+
number_density_min = 1
110+
end:species
111+
112+
begin:species
113+
name = Ion_H
114+
charge = 1
115+
atomic_number = 1
116+
mass = 1836.2
117+
fraction = 0.25
118+
dump = T
119+
number_density = number_density(Electron) / 7
120+
temperature = temperature_x(Electron)
121+
number_density_min = 1
122+
identify:proton
123+
end:species
124+
125+
begin:species
126+
name = Photon
127+
nparticles = 0
128+
dump = T
129+
identify:photon
130+
end:species
131+
132+
begin:species
133+
name = Positron
134+
nparticles = 0
135+
dump = T
136+
identify:positron
137+
end:species
138+
139+
### THE MANY PARTICLE PROBES ###
140+
141+
#Electron#
142+
begin:probe
143+
name = Electron_Front_Probe
144+
point = (x_min, 0)
145+
normal = (-1.0, 0.0)
146+
ek_min = 0.0
147+
ek_max = -1.0
148+
include_species : Electron
149+
dumpmask = always
150+
end:probe
151+
begin:probe
152+
name = Electron_Back_Probe
153+
point = (x_max, 0)
154+
normal = (1.0, 0.0)
155+
ek_min = 0.0
156+
ek_max = -1.0
157+
include_species : Electron
158+
dumpmask = always
159+
end:probe
160+
161+
################################
162+
163+
begin:output_global
164+
force_final_to_be_restartable = F
165+
end:output_global
166+
167+
begin:output
168+
name = normal
169+
grid = always
170+
particle_probes = always
171+
dt_snapshot = 15e-14
172+
ex = always + single
173+
ey = always + single
174+
ez = always + single
175+
bx = always + single
176+
by = always + single
177+
bz = always + single
178+
particles = always
179+
particle_weight = always
180+
px = always + single
181+
py = always + single
182+
pz = always + single
183+
average_particle_energy = always + species + no_sum + single
184+
number_density = always + species + no_sum + single
185+
absorption = always
186+
total_energy_sum = always + species
187+
end:output
188+

tests/test_basic.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
EXAMPLE_ARRAYS_DIR = pathlib.Path(__file__).parent / "example_array_no_grids"
1313
EXAMPLE_3D_DIST_FN = pathlib.Path(__file__).parent / "example_dist_fn"
14+
EXAMPLE_2D_PARTICLE_DATA = pathlib.Path(__file__).parent / "example_two_probes_2D"
1415

1516

1617
def test_basic():
@@ -226,3 +227,32 @@ def test_erroring_drop_variables():
226227
xr.open_dataset(
227228
EXAMPLE_FILES_DIR / "0000.sdf", drop_variables=["Electric_Field/E"]
228229
)
230+
231+
232+
def test_loading_multiple_probes():
233+
with xr.open_dataset(
234+
EXAMPLE_2D_PARTICLE_DATA / "0002.sdf",
235+
keep_particles=True,
236+
probe_names=["Electron_Front_Probe", "Electron_Back_Probe"],
237+
) as df:
238+
assert "X_Probe_Electron_Front_Probe" in df.coords
239+
assert "X_Probe_Electron_Back_Probe" in df.coords
240+
assert "ID_Electron_Front_Probe_Px" in df.dims
241+
assert "ID_Electron_Back_Probe_Px" in df.dims
242+
243+
244+
def test_loading_one_probe_drop_second_probe():
245+
with xr.open_dataset(
246+
EXAMPLE_2D_PARTICLE_DATA / "0002.sdf",
247+
keep_particles=True,
248+
drop_variables=[
249+
"Electron_Back_Probe_Px",
250+
"Electron_Back_Probe_Py",
251+
"Electron_Back_Probe_Pz",
252+
"Electron_Back_Probe_weight",
253+
],
254+
probe_names=["Electron_Front_Probe"],
255+
) as df:
256+
assert "X_Probe_Electron_Front_Probe" in df.coords
257+
assert "ID_Electron_Front_Probe_Px" in df.dims
258+
assert "ID_Electron_Back_Probe_Px" not in df.dims

0 commit comments

Comments
 (0)