This notebook was created by Sergey Tomin (sergey.tomin@desy.de). Source and license info is on GitHub. June 2018.
This version uses the new Matcher API. The legacy match() version is available here.
12. Undulator Matching
Appendix: FODO in thin lens approximation
This notebook was created as an answer for a question what min/max average beta function the EuXFEL SASE1 undulator can have. The rigorous way is to ask the optics expert, but that does not give much intuition for the beam optics of this SASE line. Another problem is that FEL experts like to work with average betas. To meet all these requirements, I wrote this notebook, which also might help someone to understand how to deal with linear optics in OCELOT.
Let's consider the FODO cell of the SASE undulator
Introducing as the distance between defocusing and focusing quadrupoles.
We neglect focusing effect in the vertical plane of the horizontal planar undulator.
The transfer matrix of the quadrupole in thin lens approximation:
where is focal length, is quad strength and is the quad length.
Drift:
%matplotlib inline
import sympy as sp
sp.init_printing()
Half cell transfer matrix
Calculate the matrix for a half cell, starting in the middle of a defocusing quadrupole and ending in the middle of a focusing lens.
Denote as the focal length of the half quadrupole.
fh, d = sp.symbols("fh, d")
Mqf = sp.Matrix([[1, 0],[-1/fh, 1]])
Md = sp.Matrix([[1, d],[0, 1]])
Mqd = sp.Matrix([[1, 0],[1/fh, 1]])
M1 = Mqf*Md*Mqd
sp.simplify(M1)
For second half cell
M2 = sp.Matrix([
[(d - fh)/-fh, d],
[ -d/fh**2, (-d -fh)/-fh]])
M_cell = sp.simplify(M1*M2)
M_cell
Phase advance and stability of the FODO cell
Phase advance is related to the transfer matrix by:
Stability requires:
trace_M = sp.simplify(M_cell[0,0] + M_cell[1,1])
trace_M
Phase advance:
using trigonometric double-angle formulae:
and finally
Stability
rewriting
Finally
- functions
General solution of the Hill's equation :
Applying initial conditions at the point with and the particle coordinates , we will get:
inserting the result into the equation above:
Now consider half of the FODO cell (see picture above) () and and (we consider horizontal plane) and remembering that in the middle of the quads . From another side, we have already calculated the transfer matrix for a half of FODO cell. Collecting all together:
after some gymnastics with trigonometric identities (and taking into account )
If we neglect the fact the beta function in the drift space takes the form of a quadratic function () and assume instead a linear behavior, the average -function will be
What means that minimum average beta-function will be with phase advance . For SASE1 undulator and taking into account that our calculations were done with some level approximation:
More accurate solution
Taking into account quadratic shape of the in the drift space and go through some simple steps, finally we will get more exact solution:
- Twiss parameters transformation
Where are elements of the transfer matrix
- twiss parameters after the thin quadrupole:
In the middle of the quadrupole then
- In the drift space behave:
- Because FODO cell is symetric and we are considering the thin lens approximation the average is:
- Inserting expression for and express focal length against the phase advance we will get:
The result looks simple; this expression can probably be found in optics handbooks.
The minimum possible average will be with what gives us .
And for our case it will be
Might be useful to find condition for minimum possible
phi = sp.Symbol("phi")
sp.diff((1 + sp.sin(phi/2))/sp.sin(phi), phi)
import numpy as np
from scipy.optimize import root
from matplotlib import pyplot as plt
fun = lambda phi: -(np.sin(phi/2) + 1)*np.cos(phi)/np.sin(phi)**2 + np.cos(phi/2)/(2*np.sin(phi))
res = root(fun, 0.1)
print("Cell phase advance for minimum possible bmax: phi = ", res.x*180/np.pi)
Cell phase advance for minimum possible bmax: phi = [76.34541525]
SASE1 undulator. Numerical simulation
The matching examples below use MatchProblem from the new Matcher module.
%matplotlib inline
from ocelot import *
from ocelot.gui.accelerator import *
try:
from scipy.integrate import simpson
except ImportError:
from scipy.integrate import simps as simpson
from ocelot.cpbd import optics
from ocelot.cpbd.matcher import MatchProblem
d1 = Drift(l=0.43065, eid='d1')
d2 = Drift(l=0.55565, eid='d2')
l = 0.1137
k = 2*0.1137/l
qd = Quadrupole(l=l/2, k1=-k, tilt=0.0)
qf = Quadrupole(l=l, k1=k, tilt=0.0)
K = 3
u40 = Undulator(lperiod=0.04, nperiods=125, Kx=K, Ky=0.0)
m1 = Marker()
m2 = Marker()
fodo_cell = [m1, qd, d1, u40, d2, qf, d1, u40, d2, qd, m2]
fodo_lat = MagneticLattice(fodo_cell)
tws = twiss(fodo_lat, nPoints=1000)
plot_opt_func(fodo_lat, tws, top_plot=["mux", "muy"], legend=False)
plt.show()
initializing ocelot...

def beta(k):
qd.k1 = -k
qf.k1 = k
fodo_lat.update_transfer_maps()
tws = twiss(fodo_lat, nPoints=1000)
bx = np.array([tw.beta_x for tw in tws])
s = np.array([tw.s for tw in tws])
bx_av = simpson(bx, x=s)/fodo_lat.totalLen
phi = tws[-1].mux
L = fodo_lat.totalLen
bx_av_th = L/6 * (5 + np.cos(phi))/np.sin(phi)
return bx_av, bx.max(), bx.min(), tws[-1].mux*180/np.pi, bx_av_th
k = np.arange(0.3, 2.9, 0.05)
Bx_min = []
Bx_max = []
Bx_av = []
Bx_av_theory = []
Phi = []
for k1 in k:
bx_av, bx_max, bx_min, phi, bx_av_th = beta(k1)
Bx_min.append(bx_min)
Bx_max.append(bx_max)
Bx_av.append(bx_av)
Phi.append(phi)
Bx_av_theory.append(bx_av_th)
fig, ax1 = plt.subplots()
plt.title(r"$\beta$ - average and max against quad strength")
ax1.plot(k, Bx_av, label=r"$\beta_{av}$")
ax1.plot(k, Bx_av_theory,"g--", label=r"$\beta_{av}^{theory}$")
ax1.plot(k, Bx_max, label=r"$\beta_{max}$")
ax1.set_ylabel(r"$\beta$, m")
ax1.set_xlabel("k, 1/m2")
plt.legend()
print("min(beta_av) = ", np.min(Bx_av), "m" )
ax2 = ax1.twinx()
ax2.plot(k, Phi, "r", label=r"$\phi_{cell}$")
ax2.set_ylabel(r"$\phi_{cell}$", color='r')
ax2.tick_params('y', colors='r')
plt.legend()
plt.grid(False)
plt.show()
min(beta_av) = 9.991835898036324 m

Limitations
The maximum possible strength of the undulator quadrupoles is which means that the minimum average beta-function is close to 10 m (see the Figure above).
Now we need to consider the limitations of the matching sections.
Inverse the task
Find the quad strength and FODO cell params if beta average is defined
FODO estimator
from scipy.optimize import root
def fodo_estimator(beta_av, Lcell, lq=0.1137):
fun = lambda phi: Lcell/6 * (5 + np.cos(phi))/np.sin(phi) - beta_av
res = root(fun, 0.1)
phi = res.x[0]
b_av = Lcell/6 * (5 + np.cos(phi))/np.sin(phi)
f = Lcell/(4*np.sin(phi/2))
kq = 1/f/lq
bmax = (1 + np.sin(phi/2))/np.sin(phi)*Lcell
bmin = (1 - np.sin(phi/2))/np.sin(phi)*Lcell
return kq, phi, bmin, bmax, b_av
kq, phi, bmin, bmax, b_av = fodo_estimator(beta_av=11, Lcell=12.2)
print("k1 = ", kq, "1/m^2")
print("bmin / bmax = ", bmin, "/", bmax, "m")
print("calculated beta_av = ", b_av, "m")
print("phi = ", phi*180/np.pi, "deg")
k1 = 1.771751963648942 1/m^2 bmin / bmax = 4.851965262428619 / 20.314939511625177 m calculated beta_av = 11.000000000000059 m phi = 75.8191420392388 deg
FODO correction due to undulator focusing
def fodo_correction_SASE1(beta_av=60, K=3):
kq, phi, bmin, bmax, b_av = fodo_estimator(beta_av=beta_av, Lcell=12.2)
print("Estimation: beta_x = ", np.round(bmin,3), "; beta_y = ", np.round(bmax, 3),
"; phi = ", np.round(phi*180/np.pi), "deg"
"; kf/kd = ", np.round(kq, 4),"/", np.round(-kq, 4))
# SASE1 FODO cell
d1 = Drift(l=0.43065, eid='d1')
d2 = Drift(l=0.55565, eid='d2')
qd = Quadrupole(l=0.1137/2, k1=-kq, tilt=0.0)
qf = Quadrupole(l=0.1137, k1=kq, tilt=0.0)
u40 = Undulator(lperiod=0.04, nperiods=125, Kx=K, Ky=0.0)
m1 = Marker()
m2 = Marker()
fodo_cell = [m1, qd, d1, u40, d2, qf, d1, u40, d2, qd, m2]
fodo_lat = MagneticLattice(fodo_cell)
tws = Twiss()
tws.beta_x = bmin
tws.beta_y = bmax
tws.E = 14
problem = MatchProblem(fodo_lat, tws, periodic=True)
problem.vary_element(qf, quantity="k1", limits=(-3, 3), name="qf.k1")
problem.vary_element(qd, quantity="k1", limits=(-3, 3), name="qd.k1")
problem.target_twiss(m2, "mux", phi, weight=1e6, tol=1e-8)
problem.target_twiss(m2, "muy", phi, weight=1e6, tol=1e-8)
result = problem.solve(solver="ls_trf", max_iter=2000, tol=1e-10)
kf = qf.k1
kd = qd.k1
tws = twiss(fodo_lat, tws0=tws, nPoints=1000)
print("Correction: beta_x = ", np.round(tws[0].beta_x, 3),
"beta_y = ", np.round(tws[0].beta_y, 3),
" kf/kd = ", np.round(kf,4), "/", np.round(kd,4),
"; success = ", result.success)
#plot_opt_func(fodo_lat, tws, top_plot=["mux", "muy"], legend=False)
#plt.show()
return kf, kd, bmin, bmax
Current lattice m
import sase1_lattice as sase1
lat = MagneticLattice(sase1.cell)#, stop=sase1.fodo_match)
print(sase1.tws)
#sase1.tws.beta_x = 15
#sase1.tws.beta_y = 50
tws = twiss(lat, tws0=sase1.tws)
plot_opt_func(lat, tws, legend=False)
plt.show()
emit_x = 0.0 emit_y = 0.0 emit_xn = 0.0 emit_yn = 0.0 beta_x = 10.506745988156398 beta_y = 42.02704133328497 alpha_x = 0.676085898798978 alpha_y = -2.1449229692237783 Dx = 0.0 Dy = 0.0 Dxp = 0.0 Dyp = 0.0 mux = 0.0 muy = 0.0 nu_x = 0.0 nu_y = 0.0 E = 14 s = 0.0

m
The matching problem uses explicit quadrupole bounds.
kf, kd, bmin, bmax = fodo_correction_SASE1(beta_av=60, K=3)
# Because of variable redundancy, we can help the optimizer find a more elegant solution.
# Comment these lines or change initial conditions if you want to play with matching.
sase1.qf_2177_t2.k1 = -0.2244
sase1.qf_2192_t2.k1 = 0.2309
sase1.qf_2207_t2.k1 = -0.1911
sase1.qf_2218_t2.k1 = 0.1653
sase1.qa_2229_t2.k1 = -0.0881
sase1.qa_2235_t2.k1 = 0.0953
# These quadrupoles are shared with the lattice part before fodo_match,
# so they must be set before solving the upstream matching section.
sase1.qa_2241_sa1.k1 = kd
sase1.qa_2247_sa1.k1 = kf
sase1.qa_2247_sa1_h.k1 = kf
problem = MatchProblem(lat, sase1.tws, periodic=False)
problem.vary_element(sase1.qf_2177_t2, quantity="k1", limits=[-0.654, 0], name=sase1.qf_2177_t2.id + ".k1")
problem.vary_element(sase1.qf_2192_t2, quantity="k1", limits=[0 ,0.654], name=sase1.qf_2192_t2.id + ".k1")
problem.vary_element(sase1.qf_2207_t2, quantity="k1", limits=[-0.654, 0], name=sase1.qf_2207_t2.id + ".k1")
problem.vary_element(sase1.qf_2218_t2, quantity="k1", limits=[0 ,0.654], name=sase1.qf_2218_t2.id + ".k1")
problem.vary_element(sase1.qa_2229_t2, quantity="k1", limits=[-1.94, 0], name=sase1.qa_2229_t2.id + ".k1")
problem.vary_element(sase1.qa_2235_t2, quantity="k1", limits=[0, 1.94], name=sase1.qa_2235_t2.id + ".k1")
problem.target_twiss(sase1.fodo_match, "beta_x", bmax, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "beta_y", bmin, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "alpha_x", 0.0, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "alpha_y", 0.0, weight=1e6, tol=1e-5)
result = problem.solve(solver="ls_trf", max_iter=2000, tol=1e-10)
print("Matching success =", result.success, "; merit =", result.merit)
tws = twiss(lat, tws0=sase1.tws)
plot_opt_func(lat, tws, legend=False)
plt.show()
Estimation: beta_x = 54.076 ; beta_y = 66.34 ; phi = 12.0 deg; kf/kd = 0.2937 / -0.2937 Correction: beta_x = 54.076 beta_y = 66.34 kf/kd = 0.2648 / -0.2583 ; success = True Matching success = True ; merit = 0.0

m
The same Matcher setup is used for a stronger focusing solution.
kf, kd, bmin, bmax = fodo_correction_SASE1(beta_av=11, K=3)
# Because of variable redundancy, we can help the optimizer find a more elegant solution.
# Comment these lines or change initial conditions if you want to play with matching.
sase1.qf_2177_t2.k1 = -0.2227
sase1.qf_2192_t2.k1 = 0.211
sase1.qf_2207_t2.k1 = -0.2176
sase1.qf_2218_t2.k1 = 0.2392
sase1.qa_2229_t2.k1 = -0.9728
sase1.qa_2235_t2.k1 = 1.3215
# These quadrupoles are shared with the lattice part before fodo_match,
# so they must be set before solving the upstream matching section.
sase1.qa_2241_sa1.k1 = kd
sase1.qa_2247_sa1.k1 = kf
sase1.qa_2247_sa1_h.k1 = kf
problem = MatchProblem(lat, sase1.tws, periodic=False)
problem.vary_element(sase1.qf_2177_t2, quantity="k1", limits=[-0.654, 0], name=sase1.qf_2177_t2.id + ".k1")
problem.vary_element(sase1.qf_2192_t2, quantity="k1", limits=[0 ,0.654], name=sase1.qf_2192_t2.id + ".k1")
problem.vary_element(sase1.qf_2207_t2, quantity="k1", limits=[-0.654, 0], name=sase1.qf_2207_t2.id + ".k1")
problem.vary_element(sase1.qf_2218_t2, quantity="k1", limits=[0 ,0.654], name=sase1.qf_2218_t2.id + ".k1")
problem.vary_element(sase1.qa_2229_t2, quantity="k1", limits=[-1.94, 0], name=sase1.qa_2229_t2.id + ".k1")
problem.vary_element(sase1.qa_2235_t2, quantity="k1", limits=[0, 1.94], name=sase1.qa_2235_t2.id + ".k1")
problem.target_twiss(sase1.fodo_match, "beta_x", bmax, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "beta_y", bmin, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "alpha_x", 0.0, weight=1e6, tol=1e-5)
problem.target_twiss(sase1.fodo_match, "alpha_y", 0.0, weight=1e6, tol=1e-5)
result = problem.solve(solver="ls_trf", max_iter=2000, tol=1e-10)
print("Matching success =", result.success, "; merit =", result.merit)
tws = twiss(lat, tws0=sase1.tws)
plot_opt_func(lat, tws, legend=False)
plt.show()
Estimation: beta_x = 4.852 ; beta_y = 20.315 ; phi = 76.0 deg; kf/kd = 1.7718 / -1.7718 Correction: beta_x = 4.852 beta_y = 20.315 kf/kd = 1.7811 / -1.7755 ; success = True Matching success = True ; merit = 1.1194006433165288e-07
