Skip to content

Commit 4cb75b0

Browse files
authored
Add nonlinear thin dipole fringe field map (#1158)
* Set up modified DipEdge inputs. * Add intermediate variable definitions. * Implement nonlinear map. * Update default values for field integral inputs. * Update documentation. * Add entry/exit input flag. * Fix enumeration type conversion. * Add g=0 special case; make K2 optional. * Remove debug print statement. * Add scaling benchmark example. * Fix c7 coefficient and invert 1 + delta. * Fix one missing instance of 1 + delta. * Update benchmark parameter values. * Update tolerance. * Update parameters.rst Add some additional details to documentation. * Update python.rst Add additional documentation details. * Vectorization Comment Probably worth vectorizing now...
1 parent 10feefc commit 4cb75b0

11 files changed

Lines changed: 650 additions & 48 deletions

File tree

docs/source/usage/examples.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Single Particle Dynamics
4242
examples/charge_sign/README.rst
4343
examples/symplectic_integration/README.rst
4444
examples/active_plasma_lens/README.rst
45-
45+
examples/edge_effects/README.rst
4646

4747
Collective Effects
4848
------------------

docs/source/usage/parameters.rst

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,37 @@ This element requires these additional parameters:
322322
``dipedge``
323323
^^^^^^^^^^^
324324

325-
``dipedge`` for dipole edge focusing. This requires these additional parameters:
325+
``dipedge`` for dipole edge focusing. The model here is based on:
326+
327+
K. Hwang and S. Y. Lee, "Dipole fringe field map for compact synchrotrons," Phys. Rev. Accel. Beams 18, 122401 (2015)
328+
329+
as represented in the explicit, symplectic form provided in:
330+
331+
C. Mitchell and K. Hwang, "Explicit symplectic representations of nonlinear dipole fringe field maps," in Proc. NAPAC2025, TUP040, Sacramento, CA, 2025
332+
333+
Here, ``g`` denotes the magnetic gap, which is a length scale that sets the rate of decay of the fringe field. The values ``K0`` - ``K6`` denote dimensionless field integrals, describing the shape of the fringe field, as defined in eqs. (28-34) of the first reference above. In particular, ``K2`` is the well-known fringe field parameter denoted ``FINT`` in MAD-X. The default values of the field integrals ``K0`` - ``K6`` are those given in eq. (52), corresponding to a ``tanh`` (i.e. logistic) field profile.
334+
335+
If ``model = "linear"``, then the linearized map is used. This model is identical to:
336+
337+
* K. L. Brown, SLAC Report No. 75 (1982)
338+
339+
when expanded to first order in ``g/rc`` (gap / radius of curvature).
340+
341+
This requires these additional parameters:
326342

327343
* ``<element_name>.psi`` (``float``, in radians) the pole face rotation angle
328344
* ``<element_name>.rc`` (``float``, in meters) the bend radius
329-
* ``<element_name>.g`` (``float``, in meters) the gap size
330-
* ``<element_name>.K2`` (``float``, dimensionless) normalized field integral for fringe field
345+
* ``<element_name>.g`` (``float``, in meters) the full magnetic gap size
346+
* ``<element_name>.R`` (``float``, in meters) scale length for the field integrals (default: ``1 m``)
347+
* ``<element_name>.K0`` (``float``, dimensionless) normalized field integral for fringe field
348+
* ``<element_name>.K1`` (``float``, dimensionless) normalized field integral for fringe field
349+
* ``<element_name>.K2`` (``float``, dimensionless) normalized field integral for fringe field (FINT)
350+
* ``<element_name>.K3`` (``float``, dimensionless) normalized field integral for fringe field
351+
* ``<element_name>.K4`` (``float``, dimensionless) normalized field integral for fringe field
352+
* ``<element_name>.K5`` (``float``, dimensionless) normalized field integral for fringe field
353+
* ``<element_name>.K6`` (``float``, dimensionless) normalized field integral for fringe field
354+
* ``<element_name>.model`` (``string``) the fringe field model: ``linear`` (default) or ``nonlinear``
355+
* ``<element_name>.location`` (``string``) the fringe field edge location: ``entry`` (default) or ``exit``
331356
* ``<element_name>.dx`` (``float``, in meters) horizontal translation error
332357
* ``<element_name>.dy`` (``float``, in meters) vertical translation error
333358
* ``<element_name>.rotation`` (``float``, in degrees) rotation error in the transverse plane

docs/source/usage/python.rst

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -801,20 +801,40 @@ This module provides elements and methods for the accelerator lattice.
801801
802802
Edge focusing associated with bend entry or exit
803803

804-
This model assumes a first-order effect of nonzero gap.
805-
Here we use the linear fringe field map, given to first order in g/rc (gap / radius of curvature).
804+
The model here is based on:
806805

807-
References:
808-
809-
* K. L. Brown, SLAC Report No. 75 (1982).
810806
* K. Hwang and S. Y. Lee, PRAB 18, 122401 (2015).
811807

812-
:param psi: Pole face angle in rad
813-
:param rc: Radius of curvature in m
814-
:param g: Gap parameter in m
815-
:param K2: Fringe field integral (unitless)
816-
:param dx: horizontal translation error in m
817-
:param dy: vertical translation error in m
808+
as represented in the explicit, symplectic form provided in:
809+
810+
* C. Mitchell and K. Hwang, in Proc. NAPAC2025, TUP040, Sacramento, CA (2025).
811+
812+
Here, ``g`` denotes the magnetic gap, which is a length scale that sets the rate of decay of the fringe field. The values ``K0`` - ``K6`` denote
813+
dimensionless field integrals, describing the shape of the fringe field, as defined in eqs. (28-34) of the first reference above. In
814+
particular, ``K2`` is the well-known fringe field parameter denoted ``FINT`` in MAD-X. The default values of the field integrals ``K0`` - ``K6`` are
815+
those given in eq. (52), corresponding to a ``tanh`` (i.e. logistic) field profile.
816+
817+
When ``model = "linear"``, the linearized map is used. This model is identical to:
818+
819+
* K. L. Brown, SLAC Report No. 75 (1982)
820+
821+
when expanded to first order in ``g/rc`` (gap / radius of curvature).
822+
823+
:param psi: Pole face angle [radians]
824+
:param rc: Radius of curvature [m]
825+
:param g: Gap parameter [m]
826+
:param R: Length scale used in fringe field integrals [m]
827+
:param K0: Fringe field integral [unitless]
828+
:param K1: Fringe field integral [unitless]
829+
:param K2: Fringe field integral [unitless]
830+
:param K3: Fringe field integral [unitless]
831+
:param K4: Fringe field integral [unitless]
832+
:param K5: Fringe field integral [unitless]
833+
:param K6: Fringe field integral [unitless]
834+
:param model: the fringe field model: ``linear`` (default) or ``nonlinear``
835+
:param location: the fringe field edge location: ``entry`` (default) or ``exit``
836+
:param dx: horizontal translation error [m]
837+
:param dy: vertical translation error [m]
818838
:param rotation: rotation error in the transverse plane [degrees]
819839
:param name: an optional name for the element
820840

examples/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,3 +1698,14 @@ add_impactx_test(solenoid-softedge-solvable.py
16981698
examples/solenoid_softedge/analysis_solenoid_softedge_solvable.py
16991699
OFF # no plot script yet
17001700
)
1701+
1702+
# Exactly-solvable (non-uniform) soft-edge solenoid ##############
1703+
#
1704+
file(COPY ${ImpactX_SOURCE_DIR}/examples/edge_effects/initial_coords.csv
1705+
DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/dipedge-nonlinear.py)
1706+
add_impactx_test(dipedge-nonlinear.py
1707+
examples/edge_effects/run_dipedge.py
1708+
ON # ImpactX MPI-parallel
1709+
examples/edge_effects/analysis_dipedge.py
1710+
OFF # no plot script yet
1711+
)

examples/edge_effects/README.rst

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.. _dipedge-nonlinear:
2+
3+
Error scaling test for nonlinear dipole fringe field map
4+
========================================================
5+
6+
This benchmark tests the use of the nonlinear ``DipEdge`` model for integrating through a dipole fringe field.
7+
8+
Six distinct initial conditions are tested for a nominal proton beam with 800 MeV kinetic energy. The values of the field integrals K0-K6 are set to the default
9+
values, as used in:
10+
11+
K. Hwang and S. Y. Lee, "Dipole fringe field map for compact synchrotrons," Phys. Rev. Accel. Beams 18, 122401 (2015)
12+
13+
The initial conditions are chosen with increasing distance from the origin in phase space. The value of the Lie generator is a dynamical
14+
invariant of the ideal fringe field map. In reality, there is an error in the final variables that scales with (x,px,y,py,t,pt,g) as degree 3. As a result, the
15+
Lie generator is not an exact invariant of the numerically-computed fringe field map.
16+
17+
In this test, the change in the Lie generator for each initial condition should coincide with its (small) nominal value.
18+
19+
Run
20+
---
21+
22+
This example can be run **either** as:
23+
24+
* **Python** script: ``python3 run_dipedge.py``
25+
26+
For `MPI-parallel <https://www.mpi-forum.org>`__ runs, prefix these lines with ``mpiexec -n 4 ...`` or ``srun -n 4 ...``, depending on the system.
27+
28+
.. tab-set::
29+
30+
.. tab-item:: Python: Script
31+
32+
.. literalinclude:: run_dipedge.py
33+
:language: python3
34+
:caption: You can copy this file from ``examples/edge_effects/run_dipedge.py``.
35+
36+
Analyze
37+
-------
38+
39+
We run the following script to analyze correctness:
40+
41+
.. dropdown:: Script ``analysis_exact_quad.py``
42+
43+
.. literalinclude:: analysis_exact_quad.py
44+
:language: python3
45+
:caption: You can copy this file from ``examples/edge_effects/analysis_dipedge.py``.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright 2022-2023 ImpactX contributors
4+
# Authors: Axel Huebl, Chad Mitchell
5+
# License: BSD-3-Clause-LBNL
6+
#
7+
8+
import math
9+
10+
import numpy as np
11+
import openpmd_api as io
12+
13+
# initial/final beam
14+
series = io.Series("diags/openPMD/monitor.h5", io.Access.read_only)
15+
last_step = list(series.iterations)[-1]
16+
initial = series.iterations[1].particles["beam"].to_df()
17+
beam_final = series.iterations[last_step].particles["beam"]
18+
final = beam_final.to_df()
19+
20+
# Basic input parameters
21+
g = 1.0e-3
22+
phi = math.pi / 8.0
23+
rc = 10.0
24+
R = 1.0
25+
K0 = math.pi**2 / 6.0
26+
K3 = 1.0 / 6.0
27+
Kar = [K0, 0, 0, K3, 0, 0, 0]
28+
delta = 0.0
29+
30+
# Derived quantities
31+
cs = math.cos(phi)
32+
sn = math.sin(phi)
33+
tn = sn / cs
34+
sc = 1.0 / cs
35+
36+
# Lie generator coefficients
37+
c1 = g * Kar[1] / (rc * cs)
38+
c2 = sn * g**2 * Kar[0] / rc**2 * 1.0 / (2.0 * cs**3 * (1 + delta))
39+
c3 = g**2 / rc * Kar[0] / (cs**2 * (1 + delta))
40+
c4 = 1 / (1 + delta) * g / rc * Kar[1] * sn / cs**2
41+
c5 = sn / cs * 1.0 / (2 * rc)
42+
c6 = g * Kar[1] / rc * sn**2 / (4 * rc * (1 + delta) * cs**3)
43+
c7 = (
44+
1
45+
/ (2 * cs**3 * (1 + delta))
46+
* (g * Kar[1] / (2 * rc**2) + (1 + sn**2) * g / rc**2 * Kar[2])
47+
)
48+
c8 = 1 / 6 * tn**3 / (2 * rc**2 * (1 + delta))
49+
c9 = 1 / 2 * (tn * sc**2 / (2 * rc**2 * (1 + delta)))
50+
c10 = 1 / (2 * (1 + delta)) * tn**2 / rc
51+
c11 = 1 / (2 * rc * (1 + delta))
52+
c12 = 1 / 24 * (4 / cs - 8 / cs**3) * Kar[3] / (rc**2 * g * (1 + delta))
53+
c13 = sn**2 / (2 * cs**3) * g**2 / (rc * R) * Kar[4]
54+
c14 = 1 / 2 * sn / cs**3 * g / (rc * R) * Kar[5]
55+
c15 = Kar[6] / (rc * R) * 1 / cs**3
56+
57+
xi = initial["position_x"]
58+
pxi = initial["momentum_x"]
59+
yi = initial["position_y"]
60+
pyi = initial["momentum_y"]
61+
ti = initial["position_t"]
62+
pti = initial["momentum_t"]
63+
64+
Omega_initial = (
65+
xi * c1
66+
- xi * c2
67+
+ pxi * c3
68+
+ (xi * pxi - yi * pyi) * c4
69+
+ (xi**2 - yi**2) * c5
70+
- xi**2 * c6
71+
+ yi**2 * c7
72+
- xi**3 * c8
73+
+ xi * yi**2 * c9
74+
+ (xi**2 * pxi - yi**2 * pxi - 2 * xi * yi * pyi) * c10
75+
- yi**2 * pxi * c11
76+
+ yi**4 * c12
77+
+ xi * c13
78+
+ (yi**2 - xi**2) * c14
79+
+ (xi * yi**2 / 2 - xi**3 / 6) * c15
80+
)
81+
82+
xf = final["position_x"]
83+
pxf = final["momentum_x"]
84+
yf = final["position_y"]
85+
pyf = final["momentum_y"]
86+
tf = final["position_t"]
87+
ptf = final["momentum_t"]
88+
89+
Omega_final = (
90+
xf * c1
91+
- xf * c2
92+
+ pxf * c3
93+
+ (xf * pxf - yf * pyf) * c4
94+
+ (xf**2 - yf**2) * c5
95+
- xf**2 * c6
96+
+ yf**2 * c7
97+
- xf**3 * c8
98+
+ xf * yf**2 * c9
99+
+ (xf**2 * pxf - yf**2 * pxf - 2 * xf * yf * pyf) * c10
100+
- yf**2 * pxf * c11
101+
+ yf**4 * c12
102+
+ xf * c13
103+
+ (yf**2 - xf**2) * c14
104+
+ (xf * yf**2 / 2 - xf**3 / 6) * c15
105+
)
106+
107+
Delta_Omega = (Omega_final - Omega_initial).abs()
108+
109+
dx = (xf - xi).abs().max()
110+
dpx = (pxf - pxi).abs().max()
111+
dy = (yf - yi).abs().max()
112+
dpy = (pyf - pyi).abs().max()
113+
dt = (tf - ti).abs().max()
114+
dpt = (ptf - pti).abs().max()
115+
116+
print("Change in the coordinates and momenta:")
117+
print("dx", dx)
118+
print("dpx", dpx)
119+
print("dy", dy)
120+
print("dpy", dpy)
121+
print("dt", dt)
122+
print("dpt", dpt)
123+
124+
print("Change in the Lie generator, for each initial condition:")
125+
print(Delta_Omega)
126+
127+
atol = 1.5e-10
128+
print(f" atol={atol}")
129+
130+
assert np.allclose(
131+
[Delta_Omega.max()],
132+
[
133+
0.0,
134+
],
135+
atol=atol,
136+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
x px y py t pt
2+
0 1.0e-3 1.0e-3 1.0e-3 1.0e-3 1.0e-4 0
3+
1 2.0e-3 2.0e-3 2.0e-3 2.0e-3 2.0e-4 0
4+
2 4.0e-3 4.0e-3 4.0e-3 4.0e-3 4.0e-4 0
5+
3 8.0e-3 8.0e-3 8.0e-3 8.0e-3 8.0e-4 0
6+
4 1.6e-2 1.6e-2 1.6e-2 1.6e-2 1.6e-3 0
7+
5 3.2e-2 3.2e-2 3.2e-2 3.2e-2 3.2e-3 0
8+
6 6.4e-2 6.4e-2 6.4e-2 6.4e-2 6.4e-3 0

0 commit comments

Comments
 (0)