-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add periodic masking option to Aperture. #739
Changes from 8 commits
1948629
fea841c
8b1ed60
3323f51
110e07b
1412a5a
2b7dfbf
a5714ec
099e19e
5c67be0
520b5c9
90497ed
c05e2c4
914b0f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# Copyright 2022-2023 ImpactX contributors | ||
# Authors: Axel Huebl, Chad Mitchell | ||
# License: BSD-3-Clause-LBNL | ||
# | ||
|
||
import numpy as np | ||
import openpmd_api as io | ||
from scipy.stats import moment | ||
|
||
|
||
def get_moments(beam): | ||
"""Calculate standard deviations of beam position & momenta | ||
and emittance values | ||
|
||
Returns | ||
------- | ||
sigx, sigy, sigt, emittance_x, emittance_y, emittance_t | ||
""" | ||
sigx = moment(beam["position_x"], moment=2) ** 0.5 # variance -> std dev. | ||
sigpx = moment(beam["momentum_x"], moment=2) ** 0.5 | ||
sigy = moment(beam["position_y"], moment=2) ** 0.5 | ||
sigpy = moment(beam["momentum_y"], moment=2) ** 0.5 | ||
sigt = moment(beam["position_t"], moment=2) ** 0.5 | ||
sigpt = moment(beam["momentum_t"], moment=2) ** 0.5 | ||
|
||
epstrms = beam.cov(ddof=0) | ||
emittance_x = (sigx**2 * sigpx**2 - epstrms["position_x"]["momentum_x"] ** 2) ** 0.5 | ||
emittance_y = (sigy**2 * sigpy**2 - epstrms["position_y"]["momentum_y"] ** 2) ** 0.5 | ||
emittance_t = (sigt**2 * sigpt**2 - epstrms["position_t"]["momentum_t"] ** 2) ** 0.5 | ||
|
||
return (sigx, sigy, sigt, emittance_x, emittance_y, emittance_t) | ||
|
||
|
||
# initial/final beam | ||
series = io.Series("diags/openPMD/monitor.h5", io.Access.read_only) | ||
last_step = list(series.iterations)[-1] | ||
initial = series.iterations[1].particles["beam"].to_df() | ||
final = series.iterations[last_step].particles["beam"].to_df() | ||
|
||
series_lost = io.Series("diags/openPMD/particles_lost.h5", io.Access.read_only) | ||
particles_lost = series_lost.iterations[0].particles["beam"].to_df() | ||
|
||
# compare number of particles | ||
num_particles = 100000 | ||
assert num_particles == len(initial) | ||
# we lost particles in apertures | ||
assert num_particles > len(final) | ||
assert num_particles == len(particles_lost) + len(final) | ||
|
||
print("Initial Beam:") | ||
sigx, sigy, sigt, emittance_x, emittance_y, emittance_t = get_moments(initial) | ||
print(f" sigx={sigx:e} sigy={sigy:e} sigt={sigt:e}") | ||
print( | ||
f" emittance_x={emittance_x:e} emittance_y={emittance_y:e} emittance_t={emittance_t:e}" | ||
) | ||
|
||
atol = 0.0 # ignored | ||
rtol = 2.0 * num_particles**-0.5 # from random sampling of a smooth distribution | ||
print(f" rtol={rtol} (ignored: atol~={atol})") | ||
|
||
assert np.allclose( | ||
[sigx, sigy, sigt, emittance_x, emittance_y, emittance_t], | ||
[ | ||
1.559531175539e-3, | ||
2.205510139392e-3, | ||
1.0e-3, | ||
1.0e-6, | ||
2.0e-6, | ||
1.0e-6, | ||
], | ||
rtol=rtol, | ||
atol=atol, | ||
) | ||
|
||
# particle-wise comparison against the periodic rectangular aperture boundary | ||
xmax = 1.5e-4 | ||
ymax = 1.0e-4 | ||
repeat_x = 1.0e-3 | ||
repeat_y = 1.0e-3 | ||
|
||
|
||
# kept particles, shifted to the fundamental domain | ||
xshifted = abs(final["position_x"]) + xmax | ||
yshifted = abs(final["position_y"]) + ymax | ||
u = np.fmod(xshifted, repeat_x) - xmax | ||
v = np.fmod(yshifted, repeat_y) - ymax | ||
|
||
# difference from maximum aperture | ||
dx = abs(u) - xmax | ||
dy = abs(v) - ymax | ||
|
||
print() | ||
print(f" fundamental x_max={u.max()}") | ||
print(f" fundamental x_min={u.min()}") | ||
assert np.less_equal(dx.max(), 0.0) | ||
|
||
print(f" fundamental y_max={v.max()}") | ||
print(f" fundamental y_min={v.min()}") | ||
assert np.less_equal(dy.max(), 0.0) | ||
|
||
# lost particles, shifted to the fundamental domain | ||
xshifted = abs(particles_lost["position_x"]) - xmax | ||
yshifted = abs(particles_lost["position_y"]) - ymax | ||
u = np.fmod(xshifted, repeat_x) - xmax | ||
v = np.fmod(yshifted, repeat_y) - ymax | ||
|
||
# difference from maximum aperture | ||
dx = abs(u) - xmax | ||
dy = abs(v) - ymax | ||
|
||
print() | ||
print(f" fundamental x_max={u.max()}") | ||
print(f" fundamental x_min={u.min()}") | ||
assert np.greater_equal(dx.max(), 0.0) | ||
|
||
print(f" fundamental y_max={v.max()}") | ||
print(f" fundamental y_min={v.min()}") | ||
assert np.greater_equal(dy.max(), 0.0) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the example that users are looking at, it would be nice to add another drift and monitor after the pepperpot to complement the setup of an actual emittance measurement and plot the outcome on the last monitor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. For automated testing, the existing test is good because it is simple and checks the functionality implemented. I propose that we keep it as-is for this PR, and we can add a follow-up example that includes a full pepperpot emittance reconstruction. We could rename the existing test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
############################################################################### | ||
# Particle Beam(s) | ||
############################################################################### | ||
beam.npart = 100000 | ||
beam.units = static | ||
beam.kin_energy = 250.0 | ||
beam.charge = 1.0e-9 | ||
beam.particle = proton | ||
beam.distribution = waterbag | ||
beam.lambdaX = 1.559531175539e-3 | ||
beam.lambdaY = 2.205510139392e-3 | ||
beam.lambdaT = 1.0e-3 | ||
beam.lambdaPx = 6.41218345413e-4 | ||
beam.lambdaPy = 9.06819680526e-4 | ||
beam.lambdaPt = 1.0e-3 | ||
beam.muxpx = 0.0 | ||
beam.muypy = 0.0 | ||
beam.mutpt = 0.0 | ||
|
||
|
||
############################################################################### | ||
# Beamline: lattice elements and segments | ||
############################################################################### | ||
lattice.elements = monitor drift pepperpot monitor | ||
lattice.nslice = 1 | ||
|
||
monitor.type = beam_monitor | ||
monitor.backend = h5 | ||
|
||
drift.type = drift | ||
drift.ds = 0.123 | ||
|
||
pepperpot.type = aperture | ||
pepperpot.shape = rectangular | ||
pepperpot.xmax = 1.5e-4 | ||
pepperpot.ymax = 1.0e-4 | ||
pepperpot.repeat_x = 1.0e-3 | ||
pepperpot.repeat_y = 1.0e-3 | ||
|
||
|
||
############################################################################### | ||
# Algorithms | ||
############################################################################### | ||
algo.particle_shape = 2 | ||
algo.space_charge = false | ||
|
||
|
||
############################################################################### | ||
# Diagnostics | ||
############################################################################### | ||
diag.slice_step_diagnostics = true | ||
diag.backend = h5 |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. Maybe it is worth adding another drift and monitor to represent an actual pepperpot emittance measurement setup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely! See the above suggestion. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# Copyright 2022-2023 ImpactX contributors | ||
# Authors: Axel Huebl, Chad Mitchell | ||
# License: BSD-3-Clause-LBNL | ||
# | ||
# -*- coding: utf-8 -*- | ||
|
||
import amrex.space3d as amr | ||
from impactx import ImpactX, distribution, elements | ||
|
||
# work-around for https://github.com/ECP-WarpX/impactx/issues/499 | ||
pp_amrex = amr.ParmParse("amrex") | ||
pp_amrex.add("the_arena_is_managed", 1) | ||
|
||
sim = ImpactX() | ||
|
||
# set numerical parameters and IO control | ||
sim.particle_shape = 2 # B-spline order | ||
sim.space_charge = False | ||
# sim.diagnostics = False # benchmarking | ||
sim.slice_step_diagnostics = True | ||
sim.particle_lost_diagnostics_backend = "h5" | ||
|
||
# domain decomposition & space charge mesh | ||
sim.init_grids() | ||
|
||
# load a 250 MeV proton beam with an initial | ||
# horizontal rms emittance of 1 um and an | ||
# initial vertical rms emittance of 2 um | ||
kin_energy_MeV = 250.0 # reference energy | ||
bunch_charge_C = 1.0e-9 # used with space charge | ||
npart = 100000 # number of macro particles | ||
|
||
# reference particle | ||
ref = sim.particle_container().ref_particle() | ||
ref.set_charge_qe(1.0).set_mass_MeV(938.27208816).set_kin_energy_MeV(kin_energy_MeV) | ||
|
||
# particle bunch | ||
distr = distribution.Waterbag( | ||
lambdaX=1.559531175539e-3, | ||
lambdaY=2.205510139392e-3, | ||
lambdaT=1.0e-3, | ||
lambdaPx=6.41218345413e-4, | ||
lambdaPy=9.06819680526e-4, | ||
lambdaPt=1.0e-3, | ||
) | ||
sim.add_particles(bunch_charge_C, distr, npart) | ||
|
||
# add beam diagnostics | ||
monitor = elements.BeamMonitor("monitor", backend="h5") | ||
|
||
# design the accelerator lattice | ||
sim.lattice.extend( | ||
[ | ||
monitor, | ||
elements.Drift(name="drift", ds=0.123), | ||
elements.Aperture( | ||
name="pepperpot", | ||
xmax=1.5e-4, | ||
ymax=1.0e-4, | ||
repeat_x=1.0e-3, | ||
repeat_y=1.0e-3, | ||
shape="rectangular", | ||
), | ||
monitor, | ||
] | ||
) | ||
|
||
# run simulation | ||
sim.evolve() | ||
|
||
# clean shutdown | ||
sim.finalize() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need that line?
I see, this variable is being used in line 60.
I wonder why this number is hard-coded instead of directly defined as len(initial), as it doesn't matter at which initial number of particles this test is being done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only to check that the number of particles generated, and the number of particles remaining after the lattice (including those lost) both coincide with the number specified in the input file. We have this check in all of our CI tests.