LUME-Impact Basics¶
from impact import Impact
# Nicer plotting
import matplotlib.pyplot as plt
import matplotlib
import os
from bokeh.plotting import output_notebook
from bokeh.plotting import show
from impact.plotting import layout_plot
matplotlib.rcParams["figure.figsize"] = (8, 4)
output_notebook(hide_banner=True)
Point to a valid input file
ifile = "templates/lcls_injector/ImpactT.in"
Make Impact object
I = Impact(ifile)
Change some things
I.header["Np"] = 10000
I.header["Nx"] = 16
I.header["Ny"] = 16
I.header["Nz"] = 16
I.header["Dt"] = 5e-13
# Turn Space Charge off. Both these syntaxes work
I.header["Bcurr"] = 0
I["header:Bcurr"] = 0
# Other switches
I.timeout = None
# Switches for MPI
I.numprocs = 0 # Auto-select
# This is equivalent to:
# I.use_mpi=True
# I.header['Nprow'] = 2
# I.header['Npcol'] = 2
Plot the layout.
I.plot()
Change stop location. Here this is does the same as I.ele['stop_1']['s'] = 1.5
.
I.stop = 1.5
Run Impact-T. This automatically finds the appropriate executable.
I.run()
Plot now shows the output statistics.
I.plot()
# plt.savefig('../assets/plot.png', dpi=150)
These are used to create the input.
I.input.keys()
dict_keys(['original_input', 'input_particle_file', 'header', 'lattice', 'fieldmaps'])
This is the output parsed
I.output.keys()
dict_keys(['run_info', 'stats', 'slice_info', 'particles'])
stats from the various fort. files
I.output["stats"].keys()
dict_keys(['t', 'mean_z', 'mean_gamma', 'mean_beta', 'max_r', 'sigma_gamma', 'mean_x', 'sigma_x', 'norm_emit_x', 'mean_y', 'sigma_y', 'norm_emit_y', 'sigma_z', 'norm_emit_z', 'max_amplitude_x', 'max_amplitude_y', 'max_amplitude_z', 'loadbalance_min_n_particle', 'loadbalance_max_n_particle', 'n_particle', 'moment3_x', 'moment3_y', 'moment3_z', 'moment4_x', 'moment4_y', 'moment4_z', 'cov_x__x', 'cov_x__y', 'cov_x__z', 'cov_y__y', 'cov_y__z', 'cov_z__z', 'mean_kinetic_energy', 'cov_x__px', 'cov_y__py', 'cov_z__pz', 'cov_x__py', 'cov_x__pz', 'cov_px__px', 'cov_y__px', 'cov_px__py', 'cov_z__px', 'cov_px__pz', 'cov_y__pz', 'cov_py__py', 'cov_z__py', 'cov_py__pz', 'cov_pz__pz'])
Slice info
I.output["slice_info"].keys()
dict_keys(['initial_particle_slices', 'final_particle_slices'])
Particles¶
Particles are automatically parsed in to openpmd-beamphysics ParticleGroup objects
I.output["particles"]
{'initial_particles': <ParticleGroup with 10000 particles at 0x7f38e977d670>, 'final_particles': <ParticleGroup with 10000 particles at 0x7f38e96da630>, 'YAG02': <ParticleGroup with 10000 particles at 0x7f38e96d9fa0>}
I.particles
points to this. Get the final particles and calculate some statistics:
P = I.particles["final_particles"]
P["mean_energy"]
np.float64(5996955.348819711)
Show the units:
P.units("mean_energy")
pmd_unit('eV', 1.602176634e-19, (2, 1, -2, 0, 0, 0, 0))
ParticleGroup
has built-in plotting
P.plot("delta_z", "pz")
# plt.savefig('../assets/zpz.png', dpi=150)
Stats¶
Impact's own calculated statistics can be retieved
len(I.stat("norm_emit_x")), I.stat("norm_emit_x")[-1]
(631, np.float64(3.5584349e-08))
Stats can also be computed from the particles. For example:
I.particles["final_particles"]["norm_emit_x"]
np.float64(3.5580305994127266e-08)
Compare these:
key1 = "mean_z"
key2 = "sigma_x"
units1 = str(I.units(key1))
units2 = str(I.units(key2))
plt.xlabel(key1 + f" ({units1})")
plt.ylabel(key2 + f" ({units2})")
plt.plot(I.stat(key1), I.stat(key2))
plt.scatter(
[I.particles[name][key1] for name in I.particles],
[I.particles[name][key2] for name in I.particles],
color="red",
)
<matplotlib.collections.PathCollection at 0x7f38e93f55b0>
This kind of plot is built-in for convenience, with a layout:
I.plot("sigma_x")
Even fancier options, and sending some options to matplotlib:
I.plot(
["sigma_x", "sigma_y"],
y2=["mean_kinetic_energy"],
ylim2=(0, 8e6),
figsize=(10, 5),
include_field=True,
)
Partial tracking¶
Particles can be started anywhere in the lattice. Here we will take some intermediate particles, and re-track.
Get particles at the YAG02
marker:
Pmid = I.particles["YAG02"]
Make a copy, so that the previous object is preserved.
I2 = I.copy()
I.verbose = False
The copy needs to be configured before tracking
I2.configure()
Track to 2 m
Pfinal = I2.track(Pmid, 2.0)
Compare these:
key1 = "mean_z"
key2 = "sigma_x"
units1 = str(I.units(key1))
units2 = str(I.units(key2))
plt.xlabel(key1 + f" ({units1})")
plt.ylabel(key2 + f" ({units2})")
plt.plot(I.stat(key1), I.stat(key2))
plt.plot(I2.stat(key1), I2.stat(key2))
plt.scatter(
[I.particles[name][key1] for name in I.particles],
[I.particles[name][key2] for name in I.particles],
color="red",
)
# Blue X are retracked particles
plt.scatter(
[P[key1] for P in [Pmid, Pfinal]],
[P[key2] for P in [Pmid, Pfinal]],
color="blue",
marker="x",
)
<matplotlib.collections.PathCollection at 0x7f38f18160c0>
Single particle tracking¶
Similar to above, but with initial conditions specified in the function for a single particle.
This is useful for auto-phasing and scaling elements, and tracing reference orbits.
Space charge is turned off for single particle tracking.
%%time
I3 = I.copy()
I3.verbose = False
I3.configure()
P3 = I3.track1(s=2.2, z0=1.0, pz0=10e6)
P3.z, P3.gamma
CPU times: user 159 ms, sys: 31.8 ms, total: 191 ms Wall time: 342 ms
(array([2.20002469]), array([1.84584053]))
Change something and plot:
I.ele["QE01"]["b1_gradient"] = 0
layout = layout_plot(I.input["lattice"], height=300)
show(layout)
ControlGroup objects¶
Some elements need to be changed together, either relatively or absolutely. A single traveling wave cavity, for example, is made from four fieldmaps, with defined relative phases
for name in ["L0A_entrance", "L0A_body_1", "L0A_body_2", "L0A_exit"]:
print(name, I[name]["theta0_deg"])
L0A_entrance 264.5 L0A_body_1 294.5 L0A_body_2 354.5 L0A_exit 264.5
Make a copy and add a group to control these.
I4 = I.copy()
I4.add_group(
"L0A",
ele_names=["L0A_entrance", "L0A_body_1", "L0A_body_2", "L0A_exit"],
var_name="theta0_deg",
attributes="theta0_deg",
)
ControlGroup(**{"ele_names": ["L0A_entrance", "L0A_body_1", "L0A_body_2", "L0A_exit"], "var_name": "theta0_deg", "attributes": ["theta0_deg", "theta0_deg", "theta0_deg", "theta0_deg"], "factors": [1.0, 1.0, 1.0, 1.0], "reference_values": [264.5, 294.5, 354.5, 264.5], "absolute": false, "value": 0.0, "name": "L0A"})
Make a change
I4["L0A"]["theta0_deg"] = 0.123456
These get propagated to the underlying elements
for name in I4["L0A"].ele_names:
print(name, I4[name]["theta0_deg"])
L0A_entrance 264.623456 L0A_body_1 294.623456 L0A_body_2 354.623456 L0A_exit 264.623456
Set overall scaling, respecting the special factors.
I4.add_group(
"L0A_scale",
ele_names=["L0A_entrance", "L0A_body_1", "L0A_body_2", "L0A_exit"],
var_name="rf_field_scale",
factors=[0.86571945106805, 1, 1, 0.86571945106805], # sin(k*d) with d = 3.5e-2 m
absolute=True,
)
I4["L0A_scale"]["rf_field_scale"] = 10
These get propagated to the underlying elements
for name in I4["L0A_scale"].ele_names:
print(name, I4[name]["rf_field_scale"])
L0A_entrance 8.657194510680501 L0A_body_1 10.0 L0A_body_2 10.0 L0A_exit 8.657194510680501
Instantiate from YAML¶
All of the Impact object init arguments can be passed in a YAML file. Any of:
?Impact
YAML = """
# Any argument above. One exception is initial_particles: this should be a filename that is parsed into a ParticleGroup
input_file: templates/lcls_injector/ImpactT.in
verbose: False
group:
L0A:
ele_names: [ L0A_entrance, L0A_body_1, L0A_body_2, L0A_exit ]
var_name: dtheta0_deg
attributes: theta0_deg
value: 0
L0B:
ele_names: [ L0B_entrance, L0B_body_1, L0B_body_2, L0B_exit ]
var_name: dtheta0_deg
attributes: theta0_deg
value: 0
L0A_scale:
ele_names: [ L0A_entrance, L0A_body_1, L0A_body_2, L0A_exit ]
var_name: rf_field_scale
factors: [0.86571945106805, 1, 1, 0.86571945106805] # sin(k*d) with d = 3.5e-2 m
absolute: True
value: 60e6
L0B_scale:
ele_names: [ L0B_entrance, L0B_body_1, L0B_body_2, L0B_exit ]
var_name: rf_field_scale
factors: [0.86571945106805, 1, 1, 0.86571945106805] # sin(k*d) with d = 3.5e-2 m
absolute: True
value: 60.0e6
"""
I5 = Impact.from_yaml(YAML)
I5["L0A:dtheta0_deg"], I5["L0A_entrance:theta0_deg"]
(0.0, 264.5)
I5["L0A"].reference_values
[264.5, 294.5, 354.5, 264.5]
I5["L0A"]
ControlGroup(**{"ele_names": ["L0A_entrance", "L0A_body_1", "L0A_body_2", "L0A_exit"], "var_name": "dtheta0_deg", "attributes": ["theta0_deg", "theta0_deg", "theta0_deg", "theta0_deg"], "factors": [1.0, 1.0, 1.0, 1.0], "reference_values": [264.5, 294.5, 354.5, 264.5], "absolute": false, "value": 0.0, "name": "L0A"})
Autophase¶
Autophase will calculate the relative phases of each rf element by tracking a single particle through the fieldmaps. This is done externally to Impact, and is relatively fast.
A call to Impact.autophase()
returns the relative phases found as a dict:
I5.autophase()
{'GUN': np.float64(0.05588311763767706), 'L0A': np.float64(-4.245356621603435)}
You can also give it a dict of ele_name:rel_phase_deg
with relative phases in degrees, and it will set these as it phases:
I5.autophase({"GUN": -9, "L0A": 2})
{'GUN': np.float64(-9.0), 'L0A': np.float64(2.0)}
Archive all output¶
All of .input and .output can be archived and loaded from standard h5 files.
Particles are stored in the openPMD-beamphysics format.
Call the archive
method. If no name is given, a name will be invented based on the fingerprint.
afile = I.archive()
This can be loaded into an empty model
I2 = Impact()
I2.load_archive(afile)
This also works:
I2 = Impact.from_archive(afile)
Check that the fingerprints are the same
assert I.fingerprint() == I2.fingerprint()
Look at a stat, and compare with the original object
I.stat("norm_emit_x")[-1], I2.stat("norm_emit_x")[-1]
(np.float64(3.5584349e-08), np.float64(3.5584349e-08))
The particles look the same:
I2.particles["final_particles"].plot("delta_z", "pz")
Cleanup¶
os.remove(afile)