FieldMap3D¶
A FieldMap3D integrates particles through a fully 3-D Cartesian field map: F_x(x,y,z), F_y(x,y,z), F_z(x,y,z) on a regular grid. Used when the cavity / magnet has non-axisymmetric structure (asymmetric coupler, tilted field, real measured 3-D mapping) and the 1-D / 2-D cylindrical reduction is not adequate.
TL;DR¶
Same TraceWin FIELD_MAP card as the 1-D / 2-D variant, with
geom digit 7 selecting 3-D Cartesian for one or both of E and B
channels:
geom = 7700— 3-D RF E + 3-D RF B (full 3-D cavity).geom = 7000— 3-D RF B only.geom = 70— 3-D static B (e.g. measured solenoid 3-D map).
The expected files for geom = 7700 and prefix MyCavity:
MyCavity.edx, MyCavity.edy, MyCavity.edz # 3-D RF E components
MyCavity.bdx, MyCavity.bdy, MyCavity.bdz # 3-D RF B components
Tutorial¶
The integrator is selectable via the class attribute
FieldMap3D.integrator_kind: "kd" (default — the legacy
first-order kick-then-drift scheme) or "dkd" (second-order
Drift–Kick–Drift, a.k.a. velocity Verlet). Only DKD is
symplectic — it samples position-dependent fields at the
half-drifted location, removing the O(ds²) phase-advance asymmetry
that drives long-trajectory emittance growth in strongly focusing
solenoid maps. The field at each substep is sampled from the 3-D
table by trilinear interpolation (interp_kind, also a class
attribute: "linear" or "cubic"). ~3-5× slower per step than
the 2-D cylindrical case, but exact for non-axisymmetric fields.
When to prefer FieldMap3D¶
- Cavity has a fundamental power coupler that breaks axisymmetry (off-axis kick).
- You have a measured 3-D field map and don't want to reduce it to cylindrical components.
- You need to model asymmetric solenoid bucking coils (e.g. PXIE LEBT solenoid + bucker).
For axisymmetric cavities the 2-D cylindrical (geom = 1XX0 or
5XX0) is usually preferable — same physics, less storage, faster.
Performance¶
A 256-m PIP-II run with mostly 3-D field maps (HWR + SSR1 + SSR2 + LB650 + HB650) takes about 6 minutes per envelope pass. Replacing the LB650/HB650 sections with 1-D models drops it to ~3 minutes with negligible σ change for the design beam.
Transfer matrix¶
Same story as 1-D / 2-D FieldMap: no closed-form 6×6. HELIX integrates the Lorentz force per substep and chains finite-differenced 6×6 Jacobians for the envelope/matching paths.
The 3-D field source means the cross blocks of the transfer
matrix can be non-zero — a tilted solenoid couples (x, y'), an
asymmetric coupler couples (x, Δφ), etc. This is the main reason
to use FieldMap3D over its axisymmetric cousin.
API reference¶
linac_gen.elements.field_map_3d.FieldMap3D(
name: str,
length: float, # mm
field_data: FieldMapData, # pre-loaded 3-D Cartesian table(s)
scale: float = 1.0,
phase: float = 0.0, # deg
frequency: float = 0.0, # MHz
aperture: float = 0.0, # mm
n_steps: int = 100,
ke: float = 1.0, kb: float = 1.0,
ki: float = 0.0,
p_flag: int = 0,
dx: float = 0.0, dy: float = 0.0, dz: float = 0.0,
tilt_deg: float = 0.0,
pitch_deg: float = 0.0, yaw_deg: float = 0.0,
voltage_rel: float = 0.0,
phase_offset: float = 0.0,
frequency_offset: float = 0.0,
ka: int = 1,
field_file: str | None = None, # provenance (writer round-trip)
geom: int | None = None, # provenance (writer round-trip)
)
| Parameter | Default | Units | Notes |
|---|---|---|---|
name |
(required) | — | identifier |
length |
(required) | mm | physical length |
field_data |
(required) | — | FieldMapData with 3-D Cartesian channels (geom digit 7) |
scale |
1.0 | — | global field amplitude multiplier |
phase |
0.0 | deg | RF phase φ_s |
frequency |
0.0 | MHz | RF frequency |
aperture |
0.0 | mm | round-aperture radius |
n_steps |
100 | — | substeps |
ke, kb |
1.0 | — | E / B amplitude scales |
ki |
0.0 | — | SC compensation intensity (needs .scc + active SC solver) |
p_flag |
0 | int | 1 = SET_SYNC_PHASE iterative phase calibration |
dx..yaw_deg |
0.0 | mm/deg | misalignment (Misalignment mixin) |
voltage_rel, phase_offset, frequency_offset |
0.0 | —/deg/MHz | LLRF error knobs (FieldError mixin) |
ka |
1 | int | TraceWin aperture flag — retained for round-trip, ignored by the 3-D tracker |
field_file, geom |
None | — | provenance set by the parser |
Two class attributes (not constructor parameters) control the numerics; flip them globally, or set them on an instance:
| Attribute | Default | Values | Notes |
|---|---|---|---|
integrator_kind |
"kd" |
"kd" | "dkd" |
KD = legacy first-order kick-then-drift; DKD = second-order Drift–Kick–Drift — the only symplectic option |
interp_kind |
"linear" |
"linear" | "cubic" |
grid-interpolation order |
Sampling implementation (fused C++ kernel)¶
With interp_kind = "linear" the per-substep field sampling runs
through a fused C++ kernel (linac_gen._fieldmap_kernels) when the
compiled module is present: one pass computes the cell indices and
weights per particle and applies them to every component of a
channel, instead of one scipy RegularGridInterpolator call per
component. The two paths are bitwise identical — pinned by
tests/elements/test_fieldmap_kernels.py — so this is a speed
switch, never a physics choice. Measured on the complete PIP-II
linac (256.5 m, 164 field maps): MP+SC 1.84× and envelope+SC 2.93×
end-to-end.
Selection (default is the kernel whenever it is built):
- GUI — Numerics tab → Field map → Sampling (
kernel/scipy); the choice is saved in the project file. - CLI —
python -m linac_gen run … --fieldmap-sampling scipy(orfieldmap_samplingin the project'sconvergenceblock). - API —
linac_gen.elements.field_map_3d.use_fused_kernel(False)(query withfused_kernel_enabled()/kernel_available()). - Env var —
LINAC_GEN_FIELDMAP_KERNEL=0disables it at process start.
If the compiled module is absent the scipy path is used silently
(identical results, slower). Build instructions:
linac_gen/csrc/README.md. Cubic interpolation always uses scipy.
Fused kick-then-drift step¶
With the kernel sampler and the default KD integrator, the whole
particle part of each substep — sampling every channel, the energy kick,
the adiabatic damping, the phase slip, the Lorentz kick and the drift —
runs in one compiled pass over the particles (kd_step in
linac_gen._fieldmap_kernels) instead of about fifteen numpy array
operations. It reproduces the numpy step bit for bit for every
value that is not NaN: the same expressions in the same order, and the
cos/sin phasors still computed by numpy
(tests/elements/test_fieldmap_step.py; end to end, every recorder
series, the final particles and the losses are identical on the PIP-II
MEBT and MEBT-to-foil lines, the latter with the stripping foil's random
scattering seeded — seed=None draws a new one every run). A particle
that already holds a NaN keeps NaN, but its sign bit may differ, and
numpy's floating-point warnings for such values are not raised. The on-axis field that the reference
particle and the SET_SYNC_PHASE calibration use is sampled once per
position and element and remembered (an HWR cavity used to sample the
same 400 positions 2200 times).
Measured on one thread (as the GUI then ran field maps; it now uses the CPU threads setting, every CPU by default):
| Line | Field maps | Whole run |
|---|---|---|
| PIP-II MEBT, 100 000 particles | 25.2 → 9.5 s (2.6×) | 41.7 → 25.8 s (1.6×) |
| PIP-II MEBT to foil, 5 000 particles | 154.6 → 45.6 s (3.4×) | 301.7 → 190.5 s (1.6×) |
The step follows the Sampling choice (scipy sampling runs the numpy
step), covers the KD integrator (DKD and backward tracking keep the numpy
code) and FieldMap3D only (1-D / 2-D FieldMap elements
keep theirs). LINAC_GEN_FIELDMAP_STEP=0, or
field_map_3d.use_fused_step(False), turns it off; results do not
change either way.
Source¶
linac_gen/elements/field_map_3d.py:88 (constructor), :366-389
(integrator selection).
See also¶
- FieldMap — 1-D / 2-D variants.
- Convergence guide — choosing step count, integrator order.