Hofmann stability & tune footprint¶
Two space-charge diagnostics that build on the channel tunes. Both are honest about what is exact and what is indicative.
Hofmann stability analysis (corrected anisotropic solver)¶
HELIX solves the coherent-mode KV dispersion relations of I. Hofmann,
Phys. Rev. E 57, 4713 (1998) for the non-oscillatory l = 2,
3 (even/odd), and 4 (even) branches in their full anisotropic
forms, and evaluates them per lattice-period cell directly from the
phase-probe channel model — no external code or chart digitization
involved.
The implementation is a verbatim physics port of the corrected solver from the PRAB manuscript "Higher-order anisotropic Hofmann stability charts with probabilistic margins and a machine-learning surrogate, applied to PIP-II" (A. Pathak), which fixes three defects relative to the printed 1998 equations:
- Coordinate map (Eq. (24) + p. 4716): once the plane with the larger
envelope is Hofmann's "x", both
h = a/bandα = ν_y/ν_xfollow from that same assignment, restoring the emittance identityε_x/ε_y = h²/α. - Odd-branch interchange (p. 4719): swapping
ν_x ↔ ν_yrescales theν_x-normalised quantities too —D_odd(s, α, h, S²) = D_even(s/α², 1/α, 1/h, S²/α²). - Eq. (41)/(42) sign: the
S⁴block enters with−S⁴, exactly as Eq. (41) prints it. The journal's Eq. (42) carries the opposite sign (a misprint): Eq. (43)'s five closed-form isotropic roots are the independent arbiter and are satisfied only by−S⁴(pinned bytest_eq43_roots_fix_the_l4_even_sp4_sign).
Chart coordinates from a HELIX run¶
Every input comes from one probe-bearing envelope run
(EnvelopeSolver(..., phase_probe=True)), per cell:
R = ν_z/ν_x— ratio of the depressed channel tunes (mu_z_dep/mu_x_dep, identically(k_z0·η_z)/(k_x0·η_x));Y = η_x— transverse tune depression;ε_z/ε_x— geometric emittance ratio,emit_z_mmmrad/emit_xsampled at each repeat-span entry (clock-safe acrossFREQjumps).
from linac_gen.analysis.hofmann_stability import (
hofmann_stability, anisotropy_margin)
from linac_gen.analysis.hofmann_probabilistic import instability_probability
from linac_gen.analysis.period_detect import detect_periods
from linac_gen.core.particle import PROTON
from linac_gen.core.reference import ReferenceParticle
from linac_gen.io.tracewin_parser import parse_tracewin
from linac_gen.tracking.envelope import EnvelopeSolver
from linac_gen.tracking.matrix_tracking import compute_transfer_matrix, compute_twiss
lattice, _ = parse_tracewin("examples/hofmann_stability/hofmann_demo_linac.dat")
ref = ReferenceParticle(species=PROTON, w_kin=2.5, frequency=162.5)
period = detect_periods(lattice)[0] # section A: 16 cells
a, b = period.spans()[0]
M = compute_transfer_matrix(lattice, ref, start=a, end=b - 1)
tx, ty, tz = (compute_twiss(M, pl) for pl in ("x", "y", "z"))
initial = dict(alpha_x=tx["alpha"], beta_x=tx["beta"], emit_x=2.0,
alpha_y=ty["alpha"], beta_y=ty["beta"], emit_y=2.0,
alpha_z=tz["alpha"], beta_z=tz["beta"], emit_z=0.035)
results = EnvelopeSolver(lattice, ref.copy(), initial, current=5.0,
phase_probe=True).run() # probe-bearing results
tab = hofmann_stability(results, period)
print(tab["g_combined"], tab["flagged"]) # per-cell γ/ν_0x + flags
mg = anisotropy_margin(results, period) # distance-to-onset in ε_z/ε_x
p = instability_probability(tab, N_mc=200) # Monte-Carlo jitter layer
hofmann_stability returns, per cell: the per-branch growth rates
g_l2 / g_l3_even / g_l3_odd / g_l4_even (all γ/ν_0x; the l=4 odd
branch — anisotropic S² block, isotropic-limit S⁴ block — is off by
default), the combined maximum, the space-charge parameter S², and three
verdicts:
valid—S² ≤ 10, the manuscript's perturbative-validity gate; growth rates outside it are extrapolations (flagged_extrap), never design evidence.flagged—γ/ν_0x > 0.01inside the gate.fold_risk— the bare per-cell advance approaches the 180° principal-value fold ofmu_folded; a true advance above 180° would be silently reflected, corruptingRandY, so treat amber cells with suspicion.
Guards. The analysis refuses — with a reason string, never an
exception — on x–y coupled lattices (solenoid channels: the channel tunes
are normal modes I/II, not the x/z planes of Hofmann's relations) and on
DC/continuous beams (no longitudinal tune). Cells with η_x ≥ 1 (no net
depression) are kept but warned about; the solver treats them as trivially
stable.
anisotropy_margin holds each cell's (R, Y) fixed and raises
ε_z/ε_x (1 → 9, inside the S² domain) until a higher-order (l=3,
l=4 even) channel first exceeds the threshold — the design question
"how much more anisotropy could this period absorb". Onsets caused by the
η̂ = 1 coordinate-flip seam (S² jumping > 2× in one step) are flagged
is_seam and excluded from the smooth-margin summary.
instability_probability draws N_mc perturbations per cell from an
engineering jitter budget (current 3 %, mismatch 10 %, ε-ratio 5 %, tunes
2 % — the manuscript's defaults, overridable via PerturbationBudget) and
returns P(γ/ν_0x > threshold). The full probability grid and the
bootstrap contour bands (ProbabilisticChart in
linac_gen.analysis.hofmann_probabilistic) are headless-only tools —
the bootstrap is hours-class serial; use n_workers to farm it.
Cost (measured, Apple-silicon laptop): ~31 ms per cell for the three
branch families; a full chart() at the GUI default steps=100 ≈ 4 s and
at chart()'s own default steps=200 ≈ 11 s; probability layer
≈ 0.2 s/cell at N_mc=200.
Trust anchors. tests/analysis/test_hofmann_dispersion.py pins the
solver against Hofmann's printed isotropic forms Eqs. (37)/(42)/(46)
(transcribed independently), the off-isotropic Eq. (24)/Eq. (5)/p. 4719
identities the iso-limit cannot see, and 20 golden chart points that are
bitwise-identical to the source manuscript package.
Trajectory overlay & legacy bands¶
linac_gen.analysis.hofmann still provides the exact per-cell trajectory
(k_z/k_x, k_x/k_0x) (hofmann_trajectory; conventions as in Hofmann,
Franchetti, Boine-Frankenheim, Qiang & Ryne, PRST-AB 6, 024202,
2003) and the old resonance_bands() overlay, whose w ∝ (1 − depression)
band widths are a qualitative heuristic only — superseded by the
solved growth-rate chart above, and kept as an optional overlay for
orientation at the rational ratios k_z/k_x = m/n.
Frozen-SC tune footprint¶
linac_gen.analysis.footprint.tune_footprint measures the incoherent
per-particle tune spread of one period cell.
A linear monodromy gives every particle the same eigenfrequency — no footprint. So HELIX tracks a sparse amplitude ladder repeatedly through the cell with the real nonlinear element transport, while the space-charge field is frozen from a reference pass of the matched Gaussian field beam:
- the frozen field is the 2-D Gaussian-equivalent transverse SC field
(the same
_gauss_field_2dformula the PIC solver uses), with σ_x(s), σ_y(s) taped from a beam drawn from the matched Σ — not from the sparse probe ladder, whose std would set an arbitrary field strength; - because the Gaussian field is nonlinear, core particles see the full central gradient and tail particles a weaker one → a real amplitude-dependent spread;
- per-particle tunes come from a windowed-FFT NAFF-lite of the Courant–Snyder-normalized coordinate;
- a ray launched in one plane stays in it (the field across an axis is exactly zero); its tune in the other plane is that of a shadow copy started with an angle of 1e-6 of the matched rms divergence there, and is left out where that small motion grows without bound, as it does where the frozen field's centre is unstable in that plane (the model line counts such rays).
from linac_gen.analysis.footprint import tune_footprint
fp = tune_footprint(lattice, ref, period, base_initial=initial,
current=5.0, n_turns=256, n_particles=120)
print(fp["mu_x_core_deg"], fp["mu_x_spread_pp_deg"]) # core tune, spread
Returned keys include per-particle qx / qy (fraction of a cell) and
launch amplitudes ax_sigma / ay_sigma; the small-amplitude
mu_{x,y}_core_deg; and the spread mu_{x,y}_spread_pp_deg (peak-to-peak)
/ mu_{x,y}_spread_rms_deg.
Physics & honesty. For a defocusing SC field the core is the most
depressed (tune rises with amplitude toward the bare tune), and the
Gaussian core sees ~2× the rms-equivalent gradient — so the ordering is
core < rms channel tune < bare, not a core-equals-centroid identity.
At zero current the footprint collapses to a point (bare tune, zero width).
This is a frozen-field footprint, not a self-consistent PIC footprint
(future work); the longitudinal plane is not modelled, and accelerating
cells use a frozen-energy-per-turn approximation. Absolute tunes near
strong depression approach the FFT resolution floor (~360°/n_turns per
cell) and are approximate.
GUI¶
Both live on the Results tab. The Hofmann stability chart popup
opens with the exact per-cell trajectory; Compute chart then solves
the corrected dispersion relations off-thread (progress on the
button, abortable on close) and renders the growth-rate heatmap
γ/ν_0x(R, Y) at the trajectory's median per-cell ε_z/ε_x with each cell
classified — green = valid + stable, red = flagged, grey hollow =
S² > 10 (extrapolation), amber ring = fold risk. A P(unstable)
checkbox adds the per-cell Monte-Carlo probabilities and a
legacy bands checkbox restores the old indicative m/n overlay.
Computed charts are cached keyed on the solver fingerprint, so reopening
at the same ε and resolution is instant. The Tune footprint (frozen
SC) popup's Compute button runs the footprint off-thread — it
re-tracks the cell — then scatters the per-particle tunes coloured by
launch amplitude.
Assistant¶
The hofmann_stability assistant tool runs the same analysis headlessly
(tier compute; runs its own envelope phase probe when the session
results lack probe maps). Parameters: period_index, margin,
probability, n_mc, threshold. It returns the per-cell JSON rows,
the flag summary, and the refusal reason when the lattice is coupled or
the beam is DC.
Cross-references¶
- Phase advance & tune depression
linac_gen/analysis/hofmann_dispersion.py:1linac_gen/analysis/hofmann_stability.py:1linac_gen/analysis/hofmann_probabilistic.py:1linac_gen/analysis/hofmann.py:1linac_gen/analysis/footprint.py:1