Quickstart Guide
Getting Started
Let's start by constructing a simple FODO cell Beamline, which consists of a quadrupole magnet that focuses the beam in the horizontal plane (defocuses in the vertical), followed by a drift (empty space), then a quadrupole that defocuses in the horizontal plane (focuses in the vertical), and finally another drift.
To do this, we will define four LineElements corresponding to each of these objects. The lengths of each object are specified by L (in meters), and the quadrupole strengths can be set using the property Kn1, where n means the "normal" multipole (s would be "skew") and 1 means 1st order multipole (quadrupole).
using Beamlines
qf = Quadrupole(Kn1=0.36, L=0.5)
d = Drift(L=1.2)
qd = Quadrupole(Kn1=-0.36, L=0.5)
fodo = Beamline([qf, d, qd, d])Beamline:
species_ref = Inferred
p_over_q_ref = Inferred
Index Name Kind s [m] L [m]
1 Quadrupole 0 0.5
2 Drift 0.5 1.2
3 Quadrupole 1.7 0.5
4 Drift 2.2 1.2
As you can see, because we did not specify a reference particle species species_ref, nor a signed reference magnetic rigidity p_over_q_ref, both of those show Inferred. This means that it will infer those values from either a preceeding Beamline, or simply leave it up to a tracking code to decide. One can also specify E_ref or pc_ref instead of p_over_q_ref for convenience. Let's do that now, and specify electrons with a total reference energy of 18 GeV:
fodo = Beamline([qd, d, qd, d], species_ref=Species("electron"), E_ref=18e9)Beamline:
species_ref = electron
E_ref = 1.8e10
Index Name Kind s [m] L [m]
1 Quadrupole 0 0.5
2 Drift 0.5 1.2
3 Quadrupole 1.7 0.5
4 Drift 2.2 1.2
Beamlines.jl uses the AtomicAndPhysicalConstants.jl package for specifying particle species, and so any species defined by that package may be provided.
The rest of the output looks ok, except for the fact that the name column is empty! This is because we didn't specify a name property for each element. It would often be convenient if we can make the variable symbols (e.g. qf, d, etc.) automatically fill in the name field for each element. We can do exactly this by wrapping the element definitions in a @elements block:
@elements begin
qf = Quadrupole(Kn1=0.36, L=0.5)
d = Drift(L=1.2)
qd = Quadrupole(Kn1=-0.36, L=0.5)
end
fodo = Beamline([qf, d, qd, d], species_ref=Species("electron"), E_ref=18e9)Beamline:
species_ref = electron
E_ref = 1.8e10
Index Name Kind s [m] L [m]
1 qf Quadrupole 0 0.5
2 d Drift 0.5 1.2
3 qd Quadrupole 1.7 0.5
4 d Drift 2.2 1.2
Much better!
Python users may use the dict-based naming function elements instead.
Beamlines.@elements — Macro
@elements ...;
@elements begin
...
endCan be applied before the definition of any LineElement(s) in order to make set the element name equal to the variable symbol automatically.
Examples
julia> @elements qf = Quadrupole();
julia> qf.name
"qf"
julia> @elements begin
my_drift = Drift(L=0.5);
my_sextupole = Sextupole(Kn2L=3.2);
end;
julia> my_drift.name
"my_drift"
julia> my_sextupole.name
"my_sextupole"Beamlines.elements — Function
elements(eles_dict)For more Pythonic-workflows, elements receives a dictionary with key as the element name and value as the element, and sets the name attribute for each element equal to its key.
Julia users should generally use the @elements macro instead.
Multiple LineElements in (multiple) Beamlines
Continuing with the FODO cell example above, note that fodo contains two instances of the line element d. Therefore, if the length of d is changed, then both instances of d will see this new, changed length:
d.L = 2.0
println(fodo.line[2].L)
println(fodo.line[4].L)2.0
2.0However, both drifts in fodo are unique elements. We can check this using the === operator:
println(fodo.line[2] === fodo.line[4])
println(d === fodo.line[2])
println(d === fodo.line[4])false
false
falseUnder the hood, when an element is placed in a Beamline, a shallow copy of that element is created that points to the "parent" element, from which it inherits its parameters. So, in this above example when the "get" fodo.line[2].L is executed, the code goes to the parent element d and returns d.L. "Sets", such as fodo.line[2].L = 10, will also pass through from the child to the parent:
fodo.line[2].L = 3.0
println(d)
println(fodo.line[4].L)
println(d === fodo.line[4].L)LineElement:
UniversalParams
kind = "Drift"
name = "d"
L = 3.0
tracking_method = SciBmadStandard(
radiation_damping_on = false,
radiation_fluctuations_on = false,
ibs_damping_on = false,
ibs_fluctuations_on = false,
)
3.0
falseThe only case where a child element can have parameters different from its parent is when a given parameter group is contained within the child. For example, fodo.line[2] and fodo.line[4] both have their own instance of BeamlineParams, from which we can extract things like beamline_index, s, and s_downstream. On the other hand, the parent element d does not have a BeamlineParams.
println("beamline_index:")
println(fodo.line[2].beamline_index)
println(fodo.line[4].beamline_index)
println("s_downstream:")
println(fodo.line[2].s_downstream)
println(fodo.line[4].s_downstream)
# This will error:
try
d.beamline_index
catch err
println(err)
endbeamline_index:
2
4
s_downstream:
3.5
7.0
ErrorException(" Unable to get key beamline_index from LineElement: element is not in a Beamline. \n If you placed this element in a Beamline, use `findchildren` to find \n the child instances of this element in a given Beamline.\n")The parent element can be retrieved using parent:
fodo.line[2].parentLineElement:
UniversalParams
kind = "Drift"
name = "d"
L = 3.0
tracking_method = SciBmadStandard(
radiation_damping_on = false,
radiation_fluctuations_on = false,
ibs_damping_on = false,
ibs_fluctuations_on = false,
)
Sometimes it can be a pain to find exactly where in a beamline are the corresponding child elements. As such, one can index the Beamline directly with a LineElement to obtain a vector of all child elements:
children = fodo[d];2-element Vector{LineElement}:
LineElement:
BeamlineParams InheritParams
beamline_index = 2 parent = LineElement:
s = 0.5 UniversalParams
s_downstream = 3.5 kind = "Drift"
name = "d"
L = 3.0
tracking_method = SciBmadStandard(
radiation_damping_on = false,
radiation_fluctuations_on = false,
ibs_damping_on = false,
ibs_fluctuations_on = false,
)
LineElement:
BeamlineParams InheritParams
beamline_index = 4 parent = LineElement:
s = 4.0 UniversalParams
s_downstream = 7.0 kind = "Drift"
name = "d"
L = 3.0
tracking_method = SciBmadStandard(
radiation_damping_on = false,
radiation_fluctuations_on = false,
ibs_damping_on = false,
ibs_fluctuations_on = false,
)
Finally, elements in a beamline allow one to "get" parameters that may only be defined when said element is in a beamline. We showed the s and s_downstream, but another example would be the unnormalized magnetic field, if the normalied magnetic field is stored as an independent variable:
ele = Quadrupole(Kn1=2, L=2)
bl = Beamline([ele], p_over_q_ref=3)
println(bl.line[1].Bn1) # Returns Kn1 * p_over_q_ref = 2 * 36.0The last parameter "set" will always define what the independent variable is. So if we then set the unnormalized quadrupole strength Bn1, that will be the independent variable:
ele.Bn1 = 10
println(bl.line[1].Kn1) # Returns Bn1 / p_over_q_ref = 10 / 33.3333333Now, if we then change the reference energy of the beamline, Bn1 will remain constant but Kn1 will change:
bl.p_over_q_ref = 4
println(bl.line[1].Bn1) # == 10
println(bl.line[1].Kn1) # Now equals 10 / 410.0
2.5Deferred Expressions
Earlier we set qf.Kn1 = 0.36, and qd.Kn1 = -0.36. But what if we want to ensure that qd.Kn1 == -qf.Kn1 always? We can bake-in such an interdependence, common in particle accelerator parameters, using a "deferred expression" - an expression where evaluation is postponed until its result is actually needed, rather than immediately when it is defined.
To do this, let's first define a function that returns the current value of -qf.Kn1. We can do this without giving the function any explicit name using lambda/anonymous functions:
lambdafun = () -> -qf.Kn1
println("Before: ", lambdafun())
qf.Kn1 = 0.1
println("After: ", lambdafun())Before: -0.36
After: -0.1Here lambdafun takes no arguments (specified by the empty tuple ()) and returns -qf.Kn1. In the context of programming, lambdafun is specifically called a closure, because it "encloses" qf, and at the time of evaluation gets the Kn1 of that enclosed qf and negates its sign.
Now we just wrap this function in Beamlines's DefExpr type, and we can set any LineElement parameter to be such a deferred expression:
qd.Kn1 = DefExpr(lambdafun)
qd.Kn1-0.1Now if we change qf.Kn1, evaluation of qd.Kn1 will always be -qf.Kn1:
qf.Kn1 = 0.7
qd.Kn1-0.7Deferred expressions can also be manipulated like any other number:
a = 1
da = DefExpr(()->a)
b = 2
db = DefExpr(()->b)
dc = da + db
println(dc())
a = 4
println(dc())
dd = sin(dc)
println(dd())3
6
-0.27941549819892586One can really "go crazy" with deferred expressions if they want to. They can be infinitely nested, and you can write any function that the programming language allows, for example file I/O, or even control system gets/puts with a real accelerator for a digital twin.
Beamlines.DefExpr — Type
DefExpr{T}A lazily-evaluated deferred expression returning type T. Deferred expressions are lambda functions that "close" over a variable in the current scope. They can be used to specify inter-dependent parameters in an accelerator and guarantee that no parameter ever becomes "stale".
Also see Context.
Examples
julia> a = 0.36;
julia> qf = Quadrupole(Kn1L=DefExpr(()->a)); # captures variable a
julia> qf.Kn1L
0.36
julia> a = 0.7
0.7
julia> qf.Kn1L
0.7Slightly better performance may be achieved by explicitly typing the captured variable:
julia> b::Float64 = 0.2;
julia> qf = Quadrupole(Kn1L=DefExpr(()->b)); # captures variable b with known type
julia> qf.Kn1L
0.2
julia> b = 0.4
0.4
julia> qf.Kn1L
0.4Deferred expressions can be treated and operated with as regular numbers, even outside the context of Beamlines, and can be evaluated by calling it like a function with no arguments (with ()):
julia> c = 64;
julia> cd = DefExpr(()->sin(c));
julia> c = pi;
julia> cd = DefExpr(()->sin(c));
julia> cd()
0.0
julia> c = pi/2;
julia> cd()
1.0
julia> dd = cd + 5;
julia> dd()
6.0An optional Context argument can be provided:
julia> d = DefExpr(c -> c.a + c.b);
julia> c1 = Context(a = 1, b = 2);
julia> d(c1)
3Contexts
While DefExprs can wrap variables in the given scope as shown in the previous section, it can be useful and convenient to have a contained place where all control variables exist; this is the purpose of the Context. Contexts contain variables that can be optionally used when evaluating DefExprs that are defined with a single input argument of type Context. This is best shown with an example:
julia> c1 = Context(a = 1);julia> c2 = Context(a = 2);julia> d = DefExpr(c -> c.a); # one-argument lambda functionjulia> d(c1)1julia> d(c2)2julia> c1.a = 3; # Can mutate the state of the variablesjulia> d(c1)3
Contexts can be pushed on/popped from a global stack of contexts GLOBAL_CONTEXTS. In this case, when referencing a variable from a context, if it does not exist in that given context, then the first instance of that variable from the top of the GLOBAL_CONTEXTS stack will be used:
julia> c1 = Context(a = 1);julia> push!(GLOBAL_CONTEXTS, c1);julia> c2 = Context(b = 2);julia> c2.a # `a` does not in exist in `c2`, get from `GLOBAL_CONTEXTS`1julia> push!(GLOBAL_CONTEXTS, c2);julia> c3 = Context();julia> c3.a1julia> c3.b2
If the variable does not exist anywhere, then an error will be thrown.
For DefExprs with a context input argument, a context must be provided in order to evaluate the deferred expression, or else an error will be thrown. For general, interactive use, contexts can be pushed on/popped from a global stack of contexts GLOBAL_CONTEXTS, which will be used if no context is provided:
julia> c1 = Context(a = 1);julia> d = DefExpr(c -> c.a); # one-argument lambda functionjulia> push!(GLOBAL_CONTEXTS, c1)DataStructures.Stack{Context}(Deque [Context[Context{Any}(Dict{Symbol, Any}(:a => 1, :b => 2, :c => 3)), Context{Any}(Dict{Symbol, Any}(:a => 1)), Context{Any}(Dict{Symbol, Any}(:b => 2)), Context{Any}(Dict{Symbol, Any}(:a => 1))]])julia> d() # Finds `a` from the `GLOBAL_CONTEXTS` stack1
All Beamlines have a context property to store a context, which all containing LineElement parameters defined use when getting properties at the element-level:
julia> c1 = Context(Kn1=0.36);julia> qf = Quadrupole(Kn1=DefExpr(c -> c.Kn1), L=0.5);julia> bl = Beamline([qf], context=c1);julia> bl[qf][1].Kn1 # Index the beamline with the `qf` to get all child `qf`s0.36
Beamlines.Context — Type
Context{T}A "scope" of variables that can be used when evaluating DefExprs. Contexts serve as a single structure that stores all (control) variables associated with beamline parameters. Contexts essentially act as structures with arbitrary field names. The type parameter T can be specified as the variables' return type, or Union of return types, to improve performance.
If a "get" is made of a variable not included in a given Context, then the "get" will fall-back to the global stack of contexts GLOBAL_CONTEXTS. The first instance from the top of the GLOBAL_CONTEXTS stack of the variable will be used as the variable value. This makes it easy to "push" and "pop" parameter settings one may be testing in an interactive environment.
Examples
julia> c1 = Context(a = 1, b = 2, c = 3);
julia> push!(GLOBAL_CONTEXTS, c1);
julia> c2 = Context(a = 4);
julia> d = DefExpr(c -> c.a + c.b);
julia> d() # Uses c1.a and c1.b from the GLOBAL_CONTEXTS
3
julia> d(c2) # Uses c2.a, but falls-back to c1.b from GLOBAL_CONTEXTS
6
julia> push!(GLOBAL_CONTEXTS, c2);
julia> d() # Now uses c2.a and c1.b both from GLOBAL_CONTEXTS
6
julia> pop!(GLOBAL_CONTEXTS);
julia> d()
3Beamlines also store a context, which is passed to all DefExprs when getting parameters from LineElements that are in a beamline:
julia> c1 = Context(Kn1=0.36);
julia> qf = Quadrupole(Kn1=DefExpr(c -> c.Kn1), L=0.5);
julia> bl = Beamline([qf], context=c1);
julia> bl[qf][1].Kn1
0.36Parameters
Beamlines.jl supports a continually-growing list of parameters to define accelerator elements. To see a full list of the parameters you can set, look at the docstring for the LineElement type. This can be retrieved in a Julia session using Doc.docs(LineElement).
Beamlines.LineElement — Type
LineElementThe basic building block which makes up a Beamline. May be something physical like a quadrupole magnet, or something non-physical like a point in the accelerator you want to mark. A LineElement may define a region in space distinguished by the presence of (possibly time-varying) electromagnetic fields, materials, apertures and other possible properties.
LineElement properties are split into "parameter groups" for convenient organization:
AlignmentParams ApertureParams BMultipoleParams
x_offset x1_limit KnX
y_offset x2_limit KsX
z_offset y1_limit BnX
x_rot y2_limit BsX
y_rot aperture_shape KnXL
tilt aperture_at KsXL
aperture_shifts_with_body BnXL
aperture_active BsXL
tiltX
BeamlineParams BendParams EMultipoleParams
beamline g_ref EnX
beamline_index tilt_ref EsX
s e1 EnXL
s_downstream e2 EsXL
line edge1_int etiltX
context edge2_int
branch
branch_index
FourPotentialParams InitialBeamlineParams MapParams
four_potential species_ref transport_map
four_potential_params E_ref transport_map_params
four_potential_normalized pc_ref
p_over_q_ref
dE_ref
dpc_ref
dp_over_q_ref
MetaParams PatchParams RFParams
alias dt rf_frequency
label dx harmon
description dy phi0
dz zero_phase
dx_rot traveling_wave
dy_rot is_crabcavity
dz_rot
UniversalParams
kind
name
L
tracking_method
To set a property, use the natural syntax:
ele = LineElement()
ele.L = 1 # Length
ele.Kn1 = 2 # Normal quadrupole strength
ele.rf_frequency = 1e6 For detailed descriptions of properties in a given parameter group, see the documentation for that parameter group.
For a more detailed description of each parameter, see the docstrings for individual parameter groups. These are shown in the Parameter Groups section of the documentation.
Polymorphism/Differentiability
To enable full auto-differentiability of all accelerator parameters, Beamlines.jl is fully polymorphic. Full polymorphism this means that you can set any parameter to be any type that you want. For auto-differentiability, a special number type that propagates the partial derivative(s) with actual value must be used in-place of the regular 64-bit floating point numbers – polymorphism allows that. Differentiable codes in accelerator physics are not new: an early example of such a differentiable code is the Polymorphic Tracking Code (PTC), written in Fortran back in the 90s.
As an example, let's see how to compute the derivative of the total length of the beamline w.r.t. a particular element length. We will use the GTPSA.jl package to do so.
using GTPSA
d1 = Descriptor(1, 1) # 1 variable, 1st order
@elements begin
qf = Quadrupole(Kn1=0.36, L=0.5)
d = Drift(L=1.2)
qd = Quadrupole(Kn1=-0.36, L=0.5)
end
fodo = Beamline([qf, d, qd, d], species_ref=Species("electron"), E_ref=18e9);
println(fodo.line[end].s_downstream)3.4000000000000004Now we just need to update L to be a differential-algebra variable,
ΔL = vars(d1)[1] # get the first differential
d.L += ΔL
println(fodo.line[end].s_downstream)GTPSA.TPS64{GTPSA.Dynamic}:
Descriptor(NV=1, MO=1)
COEFFICIENT ORDER EXPONENTS
3.4000000000000004E+00 0 0
2.0000000000000000E+00 1 1This output shows that the total length of fodo is equal to $3.4 + 2L$, which is exactly what we'd expect given that there are two drifts.
Here we just showed the length, but **any*** accelerator parameters defined in Beamlines.jl can be set to any number type (fully polymorphic), so that derivatives can be computed using any automatic-differentiation package in Julia.
After computing derivatives, e.g. during an optimization, one might want to restore all number types back to their primitive values ( Float64, Float32, etc). This can be done using the scalarize! function:
scalarize!(fodo)
println(fodo.line[end].s_downstream)3.4000000000000004Beamlines.scalarize! — Function
scalarize!(ele::LineElement)Modifies the LineElement so all element-level parameters are regular number types. This may be needed after optimizing the element's parameters using e.g. ForwardDiff, ReverseDiff, or GTPSA, which will set the parameter equal to a special number type that propagates the gradients.
scalarize!(bl::Beamline)Modifies the Beamline and its LineElements so all parameters are regular number types. This may be needed after optimizing the element's parameters using e.g. ForwardDiff, ReverseDiff, or GTPSA, which will set the parameter equal to a special number type that propagates the gradients.
scalarize!(branch::Branch)Modifies all Beamlines and their LineElements in the Branch so all parameters are regular number types. This may be needed after optimizing the element's parameters using e.g. ForwardDiff, ReverseDiff, or GTPSA, which will set the parameter equal to a special number type that propagates the gradients.