In Guide to modelling electron beams, we discussed (primarily) analytical methods for modelling electron beams, a key component in our free-electron maser. However, in the vast majority of cases, it is impossible to find analytical solutions and we must resort to numerical methods. Here, we give a description of the numerical methods used for modelling electron beams, electron guns, undulators, and other related systems.

Note: Also see Guide to modelling lasers and masers for more information about free-electron lasers/masers (as well as other types of lasers/masers).

Numerical solution of electron beam trajectories

To solve the highly-nonlinear ODE system, we need to use numerical methods for solving differential equations. While there are commercial software that can be used like EGUN, we will describe a general treatment that is (mostly) software-agnostic. For our code samples, we will use Python, but any other programming language can be used to implement the numerical methods described here.

To begin, we will start with the exact, relativistic Lorentz force equations:

The presence of the Lorentz factor makes them incredibly difficult to solve. However, if we take which holds in the non-relativistic and mildly-relativistic limit (meaning it is valid to around , more than enough for our purposes), we have:

Or in vector form:

Since the electron charge and mass are so small while the speed of light is so big (in SI units), substituting in the raw values will lead to numerical overflows and loss of significance. To avoid this we will perform nondimensionalization. First, note that the (non-relativistic) Lorentz force law with zero electric field can be written in the form:

Where is the -th component of the velocity vector and is the -th component of the acceleration vector (in tensor notation), and the two are always perpendicular to each other. Now, we will make a change of variables by defining two new variables , where and . Then:

Likewise:

Substituting back into the Lorentz force law gives us:

Simplifying and rearranging, we have:

If we make the identification and define , we have thus nondimensionalized the differential equation:

Note: The choice for is somewhat arbitrary since it is divided from both sides. This freedom in specifying will be important soon.

In its raw form, this differential equation can be numerically-unstable for very fast-moving electrons, since the electrons move so fast that blows up, leading to numerical overflows and errors. Thus, we need to perform some manipulations to make the differential equation stable enough for numerical integration. First, note that by multiplying both sides by and defining , one may obtain an equivalent differential equation, though now expressed in terms of and :

This has the advantage of keeping our velocities expressed as reasonably small values, instead of being massive numbers on the order of ! For electron beams with electrons travelling at a substantial percentage of the speed of light, which is standard for free-electron lasers and masers, this is indeed essential.

Note that this expression is very general since any magnetic field can be written in the form , and we need only perform a simple transformation to obtain from .

We will now substitute our particular case, where the only nonzero component of the field is and we have , and thus and . Thus in our case, the previous equation takes the form:

Meanwhile, since is arbitrary, we may define freely. We thus choose ,. This choice is convenient because if we then also define , we thus have , allowing us to get rid of all the remaining factors of in the differential equation, and thus avoid all the numerical issues that having a factor of causes. Thus we have the fully nondimensionalized variant of our differential equation:

In our specific case, we have where and , , and . Thus, we may write the above in explicit vector form as:

Or equivalently:

Note: assuming that the electrons move reasonably close to the beam axis and that the magnitude of their velocity stays reasonably constant (as Jackson assumes), one needs only to replace with to obtain a fully-relativistic result that applies in the strongly-relativistic regime (where is, as usual, the Lorentz factor).

Note that to convert the above back to standard SI units (that is, to recover and from the numerically-computed and ), we make use of the conversions:

Where the former expression is derived from , and the latter expression is derived from combining , , and . Equivalently, in vector notation, we have:

One must be careful to recognize that is actually negative to due to the fact that the electron has negative charge, so we must specify the magnitude of when discussing its value. For an undulator spatial period of and a magnetic field strength of we have and . Thus one finds that:

Note that since is dimensionless, must also be for dimensional consistency; in addition, it is not difficult to see via dimensional analysis that has units of (inverse length). We will now describe several methods for solving the system using different software.

Approximate solution with leapfrog integration

We’ll start by writing a custom solver to numerically integrate the differential equation. It turns out that this is actually easier than using a ODE solving library, for reasons that will become apparent later. To do so, we use a method that is commonly-used in plasma physics, known as leapfrog integration. Leapfrog integration starts by approximating the continuous solution as a series of points separated by finite intervals. As the number of intervals grows larger, the approximation grows more and more accurate.

The leapfrog integrator is a type of symplectic integrator, which roughly means that it ensures that physical systems that must follow conservation laws have generally-stable solutions that (approximately) preserve conserved quantities. It is an iterative algorithm that computes the position and velocity of a particle (or in our case, and ) for the next step from the current step as follows:

Where and is defined (in our case) by:

This can be implemented by the following (simplified) Python code:

def leapfrog(fun, x0, v0, t_span, step_size=1E-4):
  t0, t_end = t_span
  N = int((t_end - t0)/step_size) # num steps
  t = np.linspace(t0, t_end, N)
  x = np.zeros((N, len(x0)))
  v = np.zeros((N, len(v0)))
  x[0] = x0
  v[0] = v0
  h = step_size
  for i in range(N-1):
    # calculate a[i]
    a_current_step = fun(t[i], x[i], v[i])
    x[i+1] = x[i] + v[i]*h + 1/2 * a_current_step*h**2
    # calculate a[i+1]
    if fun_args is not None:
      a_next_step = fun(t[i], x[i+1], v[i], **fun_args)
    else:
      a_next_step = fun(t[i], x[i+1], v[i])
    v[i+1] = v[i] + (a_current_step + a_next_step)/2 * h
  return {
      "t": t,
      "x": x,
      "v": v
  }

Note: The code for the numerical solver can be found in the Jupyter notebook Simulations/UndulatorDesignSimulations.ipynb in the laser research repository. We have found that it has yielded the best results of any numerical method for computing beam trajectories of any of the methods discussed.

Validation of numerical solution with SciPy

To check that the solution of the leapfrog integrator is correct, an alternative (though very-similar) formulation of the ODEs can be solved via solve_ivp’s ODE solver. To start, we have:

Or alternatively, after dividing both sides by :

Where . Now after defining , as well as we have:

Where is a precomputed constant (of the order in units of THz), and is of the same order of magnitude (in units of THz). We thus solve for the scaled positions and the scaled velocities , which has the advantage that the latter are direct derivatives of the former, without needing to multiply by a factor of . Note also that , the electron charge-to-mass ratio, has a well-established CODATA value that can be used when computing . However, since the oscillation frequencies are still so high (in the THz range), we perform a transformation of variables and . Thus:

Therefore, after substitution, we have transformed the system into two second-order quasilinear differential equations for and :

Or equivalently, in matrix-vector form:

This completely removes dependence on the frequencies, which are now entirely contained within the definitions of the new variables . Note that in our initial conditions, we must use the following transforms:

Note: The code for the SciPy solver can be found in the Jupyter notebook Simulations/UndulatorDesignSimulations.ipynb in the laser research repository. However, the results are still work-in-progress and yield inconclusive results at the present time.

Approximate solution with OSCARS

We can also use the dedicated OSCARS code to numerically compute the results. A table of the results for different beam energies are shown below; all tests are done with and .

Magnetic fieldBeam energyCurrent*OrientationSuccess?
0.1 T1 MeV1.0 AYes
0.05520 keV1.0 AYes
0.03515 keV1.0 AYes
0.03510 keV1.0 ANo
0.01510 keV1.0 ANo
0.01512 keV1.0 ANo
0.01512 keV1.0 ANo
0.01512 keV1.0 AYes
0.005512 keV1.0 AYes
0.005510 keV1.0 ANo
0.005510 keV1.0 ANo

*: Here (in coordinates) is the initial direction of the particles, with being the angle to the -axis (optical axis).

Note: The code for the numerical solver can be found in Oscars/OscarsBasicSimulation.ipynb in the laser research repository.

The results indicate a cutoff beam energy of 512 keV would be required, which is indeed monstrously high. This, however, may be due to the fact that OSCARS was designed for simulating free-electron lasers with electron beam energies in the GeV range, not free-electron masers with beam energies in the keV range, which is a difference of 6 orders of magnitude.

Approximate solution with SciPy (archived)

Note: This section is an older section, and is preserved here only for archival purposes.

We’ll now cover an alternative method for numerically calculating the beam trajectories using the solve_ivp solver from the SciPy library.

Typically, ODE solvers expect coupled ODEs to be written in vector ODE form . This means we must write the system in its phase-space form. Let us define such that we have:

This can then be numerically implemented with the following code:

# right-hand side of differential equation
def f_vector(t, y_vec, omega0=1E9, ku=2*np.pi/5):
	# decompose the phase-space vector y into its components
	x, z, vx, vz = y_vec
	# prefactor
	fac = omega0 * sin(ku*z)
	return [vx, vz, -fac*vz, fac*vx]

Then, supplying f_vector as the first argument to solve_ivp yields the solution. See the SciPy solve_ivp docs for further information.

Fully relativistic solution with SciPy

Our previous analysis assumed that we could set , which holds in the non-relativistic and mildly-relativistic regime. However, if we want our solution to hold in the relativistic regime, we cannot make this assumption, and must solve with explicitly in the differential equations.

Unfortunately, since the Lorentz factor itself also depends on and , it cannot simply be factored out of the derivative; instead, we must use the expanded form, given by:

Which can be rearranged to yield:

However, since we now have second-order time derivatives, we must perform a reduction of order. We can do this by converting the two differential equations into four first-order differential equations:

Note that , the time derivative of the Lorentz factor, is given by:

Where and (since we have from our system of differential equations). Thus, substituting, we have:

We note that since we can also write our result as follows:

However, our system is still not in the correct form, since we have a term involving time derivatives of on both the left- and right-hand sides for the 4th ODE in the system (the one for ). To see why, we can expand the 4th ODE using our definition of , giving us:

By slight rearrangement, we can resolve our issue by moving the term to the left-hand side, giving us (after multiplying again by ):

Thus, our system of ODEs becomes:

Which can be written in the matrix form as follows:

We can translate this system to Python code as follows:

import numpy as np
 
c = 3E8 # m/s
electron_charge = -1.602E-19 # coulomb
B0 = 1.2 # tesla
electron_mass = 9.109E-31 # kilograms
 
# calculate the speed (scalar) from
# a velocity vector
def speed(vx, vz):
	return np.linalg.norm([vx, vz])
 
# calculate the magnitude of the acceleration
# from an acceleration vector
def accel(ax, az):
	return np.linalg.norm([ax, az])
 
# calculate the lorentz factor from the velocity
def lorentz(v, c=c):
	return 1/np.sqrt(1 - v**2 / c**2)
 
# right-hand side of differential equation
def f_vector(t, y_vec, 
		c=c, v0=0.6*c, q=electron_charge, B0=B0, m=electron_mass):
	# decompose the phase-space vector y into its components
	x, z, vx, vz = y_vec
	v = speed(vx, vz)
	# calculate the Lorentz factor
	gamma = lorentz(v, c)
	# now supply the derivatives
	dx_dt = v0
	dz_dt = vz
	dvx_dt = 0
	dvz_dt = vx/(v * vz * gamma**3/c**2 - gamma) * q*B0/m * sin(k0 * x)
	return [dx_dt, dz_dt, dvx_dt, dvz_dt]

Note: A real electron beam is not a perfect curve and has a nonzero beam cross-section, which is caused by electrons having different energies (and thus different velocities) and some electrons travelling off-centerline. We can simulate this by numerically integrating with different starting positions and with different initial velocities . This gives us multiple trajectories that we can overlay together to get a better picture of what a realistic beam would look like. An alternative approach is detailed when we get to electron gun modelling (which will come later).

The Hamiltonian approach

Lastly, we’ll discuss one approach that is more advanced, but especially powerful when used alongside symplectic integrators. This is the Hamiltonian approach. We start with the classical Hamiltonian of an electron in a magnetic field, which is given by:

Where is the magnetic vector potential, and . The equations of motion are then naturally expressed in terms of the Hamiltonian, as follows:

Now, we know that the (idealized) magnetic field of the system is given by:

Therefore by working backwards, we have (up to the addition of an irrotational field):

Taking the curl it is easy to see that , as we started with. This gives us the following Hamiltonian:

Note: since does not depend on , Hamilton’s equations immediately tell us that .

We can explicitly evaluate this Hamiltonian by substituting in and , giving us a regular system of first-order ODEs that can be solved by any ODE solver. However, there are also specialized libraries like pyhamsys that can directly numerically-integrate a given Hamiltonian with symplectic integrators, ensuring energy stability throughout the simulation.

Modelling of permanent magnets for undulators

The magnetic field can be found using the magnetostatic equations of motion:

Note: Here is the permeability of the magnets, which can be found on tables of material properties and is also usually provided by the manufacturer that made the magnets. For our purposes we can assume

Numerical solution

For the undulator, we have so far used a simple analytical expression for the magnetic field , which comes from Jackson. It is given by:

The reason this expression for the field works reasonably well is that we expect the dominant mode in the Fourier expansion of the field to be the fundamental mode. To describe a realistic beam, however, will involve a Fourier series and will depend on and , that is:

Where is the wavevector associated with the -th mode, and where we assume a Gaussian cross-section of the field. It is possible to find the coefficients by either fitting to an experimentally-measured beam or approximating it with some theoretical assumptions, but here we will not do so to avoid needless complexity and verbosity. To be able to find more accurate solutions for fields within the undulator, please see Numerical simulations for magnetostatics.

Software implementation of numerical simulations

For our simulations of the undulator, we use OSCARS (an open-source specialized electron beam/undulator modelling software from Brookhaven). OSCARS has an advanced beam-tracing algorithm that calculates the magnetic fields and beam trajectories inside an undulator. However, we also use our own custom-written codes. The simulations are all open-source and can be found here here.

References