# beamtracer.optimization.einzel

*module*

An einzel lens: the first design problem of `beamtracer.optimization`.

Three coaxial tubes in cylindrical ``(x, r)`` geometry, the beam axis along ``x``: the
outer two at 0 V, the middle one at the design voltage ``V``, inside a grounded wall.
Each tube is ``electrode_length`` long and ``thickness`` thick, its bore at the radius
``aperture``, with half-round ends (made of a simgeometry rectangle and two circles: a
sharp corner has a singular surface field, which grows as the mesh is refined); ``gap``
separates neighbouring tubes. A parallel beam of ions (protons by default) is launched upstream with ``energy`` and
radius ``beam_radius``, and the objective is its rms size at the ``target`` plane
downstream. Trajectories that hit an electrode are lost, and the lost fraction of the
current is a constraint, as is the largest field on the electrode surfaces.

`EinzelLens` holds the fixed settings and makes the
`beamtracer.optimization.Design` and
`beamtracer.optimization.Problem` of any of the parameters ``voltage``,
``gap`` and ``aperture``; `EinzelLens.thin_lens_voltage` and
`EinzelLens.paraxial_voltage` are the voltages that focus at the target in the
thin-lens and paraxial approximations.

The thin lens
-------------
For a paraxial ray in an axially symmetric electrostatic field, with $\phi(z)$ the
potential relative to the ions' zero of kinetic energy (the kinetic energy per charge,
$\phi = T / q = T_0 / q - V(z)$ on the axis for ions starting at $V = 0$),
the ray equation (non-relativistic)

$$
r'' + \frac{\phi'}{2\phi} r' + \frac{\phi''}{4\phi} r = 0
$$

becomes, with the Picht transformation $R = r \phi^{1/4}$,

$$
R'' + \frac{3}{16} \left(\frac{\phi'}{\phi}\right)^2 R = 0 .
$$

An einzel lens has the same potential on both sides, so $R$ and $r$ agree up
to a constant outside it; for a weak (thin) lens $R$ hardly changes inside it, and
a parallel ray of radius $r$ leaves with the slope $-r/f$,

$$
\frac{1}{f} = \frac{3}{16} \int \left(\frac{\phi'}{\phi}\right)^2 dz .
$$

See the paraxial ray equation and electrostatic lenses in M. Reiser, *Theory and Design
of Charged Particle Beams* (Wiley, 2nd ed. 2008), S. Humphries, *Principles of Charged
Particle Acceleration* (Wiley, 1986) and H. Wollnik, *Optics of Charged Particles*
(Academic Press, 1987). It holds for paraxial rays and
a lens short compared with ``f``; $\phi$ must stay positive (no ion is
reflected). Here the thin lens sits at the middle electrode, and its focal length is the
distance to the target. `EinzelLens.paraxial_voltage` integrates the Picht equation
through the whole lens instead (thick, still paraxial and without aberrations).

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L1-L1)

## beamtracer.optimization.einzel.ELECTRODES

*attribute* · *module attribute*

```python
ELECTRODES = ('upstream', 'middle', 'downstream')
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L82-L82)

## beamtracer.optimization.einzel.NEGLIGIBLE_CURRENT

*attribute* · *module attribute*

```python
NEGLIGIBLE_CURRENT = 1e-09
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L86-L86)

## beamtracer.optimization.einzel.PARAMETERS

*attribute* · *module attribute*

```python
PARAMETERS = {'voltage': 'V', 'gap': 'm', 'aperture': 'm'}
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L79-L79)

## beamtracer.optimization.einzel.EinzelLens

*class* · *dataclass*

```python
class EinzelLens
```

The fixed settings of an einzel lens, and its problems.

The beam: ions of ``energy`` [eV], ``charge`` [e] and ``mass`` [u] (protons by
default), ``current`` [A] (0: no space charge), launched parallel at
``x = start`` [m] from the axis to ``beam_radius`` [m] as ``num_particles`` rings.
The lens: three tubes of ``electrode_length`` [m] and ``thickness`` [m], the middle
one centred on ``x = centre`` [m], with half-round ends if ``rounded``;
``voltage`` [V], ``gap`` [m] and ``aperture`` [m] are the values of the parameters
not being optimized. The domain is ``length`` by ``radius`` [m] with nodes every
``cell`` [m], grounded at both ends and on the outer wall; the objective is the rms size (``sqrt(<r^2> / 2)``, the projected rms) at
``x = target`` [m]. The constraints are the largest surface field over
``field_limit`` [V/m], minus 1, and the lost fraction of the current minus
``max_loss``. ``iterations``, ``tolerance`` [V] and ``relaxation`` set the Vlasov
iteration with space charge; without it, one field solve and trace is the solution.
Every physical setting takes an astropy Quantity or a string too (``"30 keV"``,
``4 * u.mm``), converted to these units.

A frozen dataclass, so it pickles: its `config` and `evaluate` are the
build and evaluation functions of its problems, also in a process pool.

**Numerics**

*ray*

$$
r'' + \frac{\phi'}{2\phi} r' + \frac{\phi''}{4\phi} r = 0, \qquad \phi(z) = \frac{E}{q} - V(z)
$$

*objective*

$$
\text{spot} = \sqrt{\tfrac12 \langle r^2 \rangle} \ \text{at the target plane}
$$

*constraints*

$$
\text{loss} = \max(0, 1 - T) - \text{max\_loss}, \qquad \text{surface\_field} = \frac{\max |E_\mathrm{surface}|}{\text{field\_limit}} - 1
$$

*transmission*

$$
T = \frac{I_\mathrm{target}}{\pi J R_b^2}
$$

Assumptions:

- three coaxial tubes, the outer two grounded, the middle one at $V$, inside a grounded wall; a parallel beam launched upstream as rings
- without space charge (`current` 0) one field solve and trace is the solution, with $10^{-9}$ A as the weight of the moments
- a design that does not fit is a failed evaluation

References:

- [Reiser, Theory and Design of Charged Particle Beams, 2nd ed. (2008)](https://doi.org/10.1002/9783527622047)
- [Humphries, Principles of Charged Particle Acceleration (1986)](https://fieldp.com/cpa.html)
- [Wollnik, Optics of Charged Particles (1987)](https://shop.elsevier.com/books/optics-of-charged-particles/wollnik/978-0-12-762130-2)

Built from:

- `beamtracer.model.BeamOptics`
- `beamtracer.optimization.einzel.EinzelLens.thin_lens_focal_length`
- `beamtracer.optimization.einzel.EinzelLens.paraxial_focus`

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L110-L547)

### beamtracer.optimization.einzel.EinzelLens.aperture

*attribute* · *class attribute* · *instance attribute*

```python
aperture: float = unit_field('m', 0.005)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L165-L165)

### beamtracer.optimization.einzel.EinzelLens.beam_radius

*attribute* · *class attribute* · *instance attribute*

```python
beam_radius: float = unit_field('m', 0.002)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L160-L160)

### beamtracer.optimization.einzel.EinzelLens.cell

*attribute* · *class attribute* · *instance attribute*

```python
cell: float = unit_field('m', 0.0005)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L173-L173)

### beamtracer.optimization.einzel.EinzelLens.centre

*attribute* · *class attribute* · *instance attribute*

```python
centre: float = unit_field('m', 0.04)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L169-L169)

### beamtracer.optimization.einzel.EinzelLens.charge

*attribute* · *class attribute* · *instance attribute*

```python
charge: float = 1.0
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L157-L157)

### beamtracer.optimization.einzel.EinzelLens.current

*attribute* · *class attribute* · *instance attribute*

```python
current: float = unit_field('A', 0.0)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L159-L159)

### beamtracer.optimization.einzel.EinzelLens.electrode_length

*attribute* · *class attribute* · *instance attribute*

```python
electrode_length: float = unit_field('m', 0.008)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L166-L166)

### beamtracer.optimization.einzel.EinzelLens.energy

*attribute* · *class attribute* · *instance attribute*

```python
energy: float = unit_field('eV', 30000.0)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L156-L156)

### beamtracer.optimization.einzel.EinzelLens.field_limit

*attribute* · *class attribute* · *instance attribute*

```python
field_limit: float = unit_field('V/m', 5000000.0)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L174-L174)

### beamtracer.optimization.einzel.EinzelLens.gap

*attribute* · *class attribute* · *instance attribute*

```python
gap: float = unit_field('m', 0.004)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L164-L164)

### beamtracer.optimization.einzel.EinzelLens.iterations

*attribute* · *class attribute* · *instance attribute*

```python
iterations: int = 40
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L176-L176)

### beamtracer.optimization.einzel.EinzelLens.length

*attribute* · *class attribute* · *instance attribute*

```python
length: float = unit_field('m', 0.13)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L171-L171)

### beamtracer.optimization.einzel.EinzelLens.mass

*attribute* · *class attribute* · *instance attribute*

```python
mass: float = unit_field('u', 1.007276)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L158-L158)

### beamtracer.optimization.einzel.EinzelLens.max_loss

*attribute* · *class attribute* · *instance attribute*

```python
max_loss: float = 0.0
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L175-L175)

### beamtracer.optimization.einzel.EinzelLens.num_particles

*attribute* · *class attribute* · *instance attribute*

```python
num_particles: int = 60
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L161-L161)

### beamtracer.optimization.einzel.EinzelLens.radius

*attribute* · *class attribute* · *instance attribute*

```python
radius: float = unit_field('m', 0.02)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L172-L172)

### beamtracer.optimization.einzel.EinzelLens.relaxation

*attribute* · *class attribute* · *instance attribute*

```python
relaxation: float = 0.8
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L178-L178)

### beamtracer.optimization.einzel.EinzelLens.rounded

*attribute* · *class attribute* · *instance attribute*

```python
rounded: bool = True
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L168-L168)

### beamtracer.optimization.einzel.EinzelLens.save_trajectories

*attribute* · *class attribute* · *instance attribute*

```python
save_trajectories: int = 0
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L179-L179)

### beamtracer.optimization.einzel.EinzelLens.start

*attribute* · *class attribute* · *instance attribute*

```python
start: float = unit_field('m', 0.001)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L162-L162)

### beamtracer.optimization.einzel.EinzelLens.target

*attribute* · *class attribute* · *instance attribute*

```python
target: float = unit_field('m', 0.12)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L170-L170)

### beamtracer.optimization.einzel.EinzelLens.thickness

*attribute* · *class attribute* · *instance attribute*

```python
thickness: float = unit_field('m', 0.002)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L167-L167)

### beamtracer.optimization.einzel.EinzelLens.tolerance

*attribute* · *class attribute* · *instance attribute*

```python
tolerance: float = unit_field('V', 0.01)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L177-L177)

### beamtracer.optimization.einzel.EinzelLens.voltage

*attribute* · *class attribute* · *instance attribute*

```python
voltage: float = unit_field('V', 15000.0)
```

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L163-L163)

### beamtracer.optimization.einzel.EinzelLens.axis_potential

*method*

```python
def axis_potential(values: Mapping[str, float] | None = None) -> tuple[np.ndarray, np.ndarray]
```

Return ``x`` [m] and the potential on the axis per volt on the middle tube.

From a field solve at the given geometry (without space charge, the potential is
proportional to the voltage).

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L407-L419)

### beamtracer.optimization.einzel.EinzelLens.config

*method*

```python
def config(values: Mapping[str, float] | None = None) -> BeamOpticsConfig
```

Return the `beamtracer.BeamOpticsConfig` of the lens.

``values`` are some of ``voltage``, ``gap`` and ``aperture``; the others are the
lens defaults. Raises `ValueError` for a lens that does not fit between
the start of the beam and the target.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L248-L305)

### beamtracer.optimization.einzel.EinzelLens.design

*method*

```python
def design(bounds: Mapping[str, tuple[float, float]]) -> Design
```

Return the design of the parameters in ``bounds`` (``{name: (lower, upper)}``).

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L379-L387)

### beamtracer.optimization.einzel.EinzelLens.electrode

*method*

```python
def electrode(name: str, x: float, aperture: float, voltage: float) -> dict[str, ObjectParams]
```

Return the objects of a tube centred on ``x`` with its bore at ``aperture``.

A tube of ``electrode_length`` along the axis and ``thickness`` across it. With
``rounded`` its ends are half circles (a rectangle and two circles, named
``name``, ``name.upstream`` and ``name.downstream``); without, it is a rectangle
with sharp corners, whose surface field is singular.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L200-L241)

### beamtracer.optimization.einzel.EinzelLens.electrode_positions

*method*

```python
def electrode_positions(gap: float) -> tuple[float, float, float]
```

Return the centres [m] of the three tubes.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L243-L246)

### beamtracer.optimization.einzel.EinzelLens.evaluate

*method*

```python
def evaluate(config: BeamOpticsConfig) -> Evaluation
```

Solve the lens of a configuration and return its `measure`.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L375-L377)

### beamtracer.optimization.einzel.EinzelLens.kinetic_potential

*method*

```python
def kinetic_potential(potential: np.ndarray) -> np.ndarray
```

Return the ions' kinetic energy per charge, ``phi`` [V], at a potential [V].

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L421-L423)

### beamtracer.optimization.einzel.EinzelLens.measure

*method*

```python
def measure(model: BeamOptics) -> Evaluation
```

Return the `beamtracer.optimization.Evaluation` of a solved lens.

The objective ``spot`` is the rms size [m] at the target; the constraints are
``surface_field`` (the largest surface field over ``field_limit``, minus 1) and
``loss`` (the lost fraction of the current minus ``max_loss``). The diagnostics
hold the field [V/m], the transmission, the rms, 90 % and largest radii [m] at
the target, and the Vlasov iterations.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L313-L364)

### beamtracer.optimization.einzel.EinzelLens.paraxial_focus

*method*

```python
def paraxial_focus(x: np.ndarray, potential: np.ndarray) -> float
```

Return where a paraxial ray, parallel at the start, crosses the axis [m].

The Picht equation integrated through the on-axis ``potential`` [V] (a thick
lens, without aberrations); a ray still away from the axis at the end of the
domain is continued straight (the field is zero there). ``inf`` if it diverges.

**Numerics**

*picht*

$$
R'' + \frac{3}{16}\left(\frac{\phi'}{\phi}\right)^2 R = 0, \qquad R = r\, \phi^{1/4}, \qquad R(z_0) = 1, \ R'(z_0) = 0
$$

*focus*

$$
z_f: R(z_f) = 0, \qquad \text{or } z_\mathrm{end} - R / R' \text{ for a ray still off the axis at the end}
$$

Assumptions:

- a thick lens, paraxial and without aberrations; integrated by `solve_ivp` (rtol $10^{-9}$, at most one cell per step), the coefficient interpolated linearly
- $\infty$ for a diverging ray; the voltage that focuses on the target by Brent's method

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L486-L528)

### beamtracer.optimization.einzel.EinzelLens.paraxial_voltage

*method*

```python
def paraxial_voltage(values: Mapping[str, float] | None = None) -> float
```

Return the voltage whose paraxial (thick-lens) focus is on the target.

Like `thin_lens_voltage`, but integrating the paraxial ray through the
lens (`paraxial_focus`).

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L530-L547)

### beamtracer.optimization.einzel.EinzelLens.problem

*method*

```python
def problem(bounds: Mapping[str, tuple[float, float]], constraints: tuple[str, ...] = ('loss')) -> Problem
```

Return the problem of minimizing the spot over the parameters in ``bounds``.

``constraints`` are some of ``"loss"`` and ``"surface_field"``.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L389-L403)

### beamtracer.optimization.einzel.EinzelLens.solve

*method*

```python
def solve(values: Mapping[str, float] | None = None) -> BeamOptics
```

Return the solved `beamtracer.BeamOptics` of the lens.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L307-L309)

### beamtracer.optimization.einzel.EinzelLens.surface_fields

*method* · *staticmethod*

```python
def surface_fields(model: BeamOptics) -> dict[str, float]
```

Return the largest surface field [V/m] of each tube (of all its objects).

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L366-L373)

### beamtracer.optimization.einzel.EinzelLens.thin_lens_focal_length

*method*

```python
def thin_lens_focal_length(x: np.ndarray, potential: np.ndarray) -> float
```

Return the thin-lens focal length [m] of the on-axis ``potential`` [V].

``1 / f = (3 / 16) int (phi' / phi)^2 dx`` (see the module docstring), with
``phi`` the kinetic energy per charge; infinite without a lens, ``NaN`` if an ion
is stopped.

**Numerics**

*thin lens*

$$
\frac{1}{f} = \frac{3}{16} \int \left(\frac{\phi'}{\phi}\right)^2 dz
$$

*discrete*

$$
\phi' \text{ by central differences}, \qquad \int \text{ by the trapezoidal rule on the mesh nodes}
$$

*voltage*

$$
\frac{1}{f(V)} = \frac{1}{x_\mathrm{target} - x_\mathrm{centre}} \quad \text{(Brent's method)}
$$

Order of accuracy: 2.

Assumptions:

- paraxial rays, a lens short compared with $f$, the same potential on both sides; $\phi > 0$ everywhere (NaN if an ion is stopped), $\infty$ without a lens
- the on-axis potential per volt from one vacuum solve at 1 kV on the middle tube
- the order is in the node spacing

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L425-L450)

### beamtracer.optimization.einzel.EinzelLens.thin_lens_voltage

*method*

```python
def thin_lens_voltage(values: Mapping[str, float] | None = None) -> float
```

Return the voltage whose thin lens at the middle tube focuses on the target.

The voltage has the sign of the lens default ``voltage`` (for positive ions,
positive decelerates them and negative accelerates them), at the geometry of
``values``. Raises `ValueError` if no voltage of that sign focuses there.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L465-L484)

### beamtracer.optimization.einzel.EinzelLens.values

*method*

```python
def values(values: Mapping[str, float] | None = None) -> dict[str, float]
```

Return all three parameters: ``values``, the defaults for the others.

[View source](https://github.com/max-models/beamtracer/blob/devel/src/beamtracer/optimization/einzel.py#L187-L198)
