Phase advance & tune depression¶
HELIX reports three distinct phase-advance quantities. Keeping them separate is the whole point of this page — mixing them is the classic way to get tune depression wrong.
| symbol | name | what it is | source |
|---|---|---|---|
| σ₀ | bare channel tune | Floquet phase of the bare one-period map | channel_phase_advance (or structure_phase_advance) |
| σ_model | depressed channel tune | Floquet phase of the one-period map with the frozen linearized space-charge field of a matched envelope | channel_phase_advance / channel_phase_advance_matched |
| Δμ_rms | beam RMS phase advance | ∫ds/β of the tracked beam moments | beam_phase_advance |
The primary tune-depression KPI is η_model = σ_model / σ₀_model —
same cell, same mode, same branch, extracted from the same probe walk.
Δμ_rms is a beam-state-dependent secondary number (it is what TraceWin's
kx/ky columns report) and carries validity flags.
The phase probe¶
σ₀_model and σ_model both come from the envelope phase probe. Run the
envelope with phase_probe=True and it records, for every element, the
exact tangent factors it actually applies to Σ — the identical Strang
slices, midpoint-σ space-charge kicks, frequency-jump rescale D, edge
maps — in two parallel products:
element_maps_dep[j]— with the space-charge factors present → the depressed one-period mapM_dep;element_maps_bare[j]— the same slices with the SC factors set to identity → the consistent bare mapM_bare.
Building σ₀ from a separate zero-current run would be wrong: a plain
I=0 run uses whole-element FieldMap maps, not the SC walk's slicing, and
the two disagree at the fraction-of-a-degree level. σ₀ must be the
M_bare of the same walk.
from linac_gen.analysis.period_detect import detect_periods
from linac_gen.analysis.phase_advance import channel_phase_advance
from linac_gen.core.lattice import Lattice
from linac_gen.core.particle import PROTON
from linac_gen.core.reference import ReferenceParticle
from linac_gen.elements.drift import Drift
from linac_gen.elements.quadrupole import Quadrupole
from linac_gen.tracking.envelope import EnvelopeSolver
lattice = Lattice() # a 3-cell FODO channel
for _ in range(3):
lattice.add(Quadrupole(name="QF", length=40.0, gradient=+50.0, aperture=20.0))
lattice.add(Drift(name="D", length=100.0, aperture=20.0))
lattice.add(Quadrupole(name="QD", length=40.0, gradient=-50.0, aperture=20.0))
lattice.add(Drift(name="D", length=100.0, aperture=20.0))
ref = ReferenceParticle(species=PROTON, w_kin=2.5, frequency=162.5)
period = detect_periods(lattice)[0]
initial = dict(alpha_x=0.0, beta_x=1.0, emit_x=1.0,
alpha_y=0.0, beta_y=1.0, emit_y=1.0,
alpha_z=0.0, beta_z=1.0, emit_z=0.0, continuous=True)
results = EnvelopeSolver(lattice, ref, initial, current=5.0,
phase_probe=True).run()
ch = channel_phase_advance(results, period)
print(ch["mu_x_bare_deg"], ch["mu_x_dep_deg"], ch["eta_x"]) # per cell
print(ch["eta_machine_x"]) # machine η
channel_phase_advance requires probe-bearing results — a plain
tracked run (or an MP/PIC run) raises a clear error, because there is no
recorded tangent map to extract from. The helper
run_phase_probe(lattice, ref, initial, current) runs a probe-enabled
envelope for you (extra keyword arguments pass through to
EnvelopeSolver).
Matched channel — the primary σ_model¶
On a plain probe run, σ_model still depends on how well your input beam
was matched. For the authoritative depressed tune, use
channel_phase_advance_matched, which first solves the full-Σ matched
fixed point of the period (normalized-coordinate iteration with
eigenemittance renormalization — required because det(M₄_dep) < 1 under
acceleration damps a naïve covariance iteration toward a singular state)
and then probes with the matched Σ:
from linac_gen.analysis.phase_advance import channel_phase_advance_matched
ch = channel_phase_advance_matched(lattice, ref, period,
current=5.0, base_initial=initial)
print(ch["eta_machine_x"], ch["matched_state"])
When the longitudinal fixed point is not meaningful (strong acceleration
per cell), pass longitudinal="frozen"; the result is tagged
sigma_model_approx="frozen-longitudinal" and the GUI/CLI say so.
The two curves, mathematically¶
The tune-depression popup (and the Matching-tab panel) plot two different quantities. Keeping their definitions straight is essential for reading the plot — in particular for understanding why the beam markers may legitimately sit above 1 while the model curve cannot.
Model curve — eigenphase ratio of the one-cell maps¶
For each cell the phase probe accumulates two one-cell matrices from the same slice sequence and reference trajectory:
where \(K_j^{\text{SC}}\) are the linearized space-charge kick matrices (midpoint-σ Strang factors of the envelope's own field). The phase advance is the eigenphase of the one-cell map; for an uncoupled 2×2 block, with the det-normalization acceleration requires,
(coupled cells: eigenphases of the \(\det^{1/4}\)-normalized 4×4 block, modes I/II paired by symplectic eigenvector overlap). Then
This is a single-particle Floquet quantity of the linear channel (lattice + frozen linearized SC field); the beam enters only through the field linearization. Because the SC kick is defocusing, \(\sigma_{\text{model}} < \sigma_0\) always — η_model ≤ 1 structurally. A model value above 1 would be a bug, not physics.
Beam curve — phase integral of the beam's own ellipse¶
— the same Courant–Snyder phase integral, but evaluated on the beam's own statistical β(s) (the projected rms Twiss of the propagated moment matrix Σ(s), whether from the envelope solver or a tracked distribution) rather than on the matched channel β. The plotted markers are \(\Delta\mu_{\text{rms}}/\sigma_0\) per cell. Geometrically, \(\Delta\mu_{\text{rms}}\) is the rotation angle of the beam's rms ellipse in normalized phase space across the cell.
The identity connecting them — and why the beam markers can exceed 1¶
Floquet's theorem: if the beam ellipse is matched to the depressed channel — Σ(s) periodic, so \(\beta^{\text{beam}}(s) = \beta^{\text{channel}}_{\text{dep}}(s)\) — the one-period phase integral equals the eigenphase exactly:
and the two curves coincide. For a mismatched beam, \(\beta^{\text{beam}}(s)\) is not periodic: it oscillates about the matched β at twice the betatron frequency. Wherever the beam runs below the matched β the integrand \(1/\beta\) is larger, so the per-cell integral overshoots; a half mismatch-period later it undershoots. Beam markers straddling 1 and alternating cell to cell are a mismatch signature, not a focusing statement — the gap between the beam markers and the model curve is a mismatch meter.
Two further contaminations in coupled (solenoid) sections: (i)
\(\beta_x^{\text{beam}} = \Sigma_{11}/\varepsilon_x^{\text{proj}}\)
uses the projected emittance, inflated by the x–y cross terms, so
the integral is not a mode phase at all (the projected_only guard);
(ii) the denominator σ₀ is a mode eigenphase, so the ratio is only
apples-to-apples after mode pairing. This is why the popup draws the
model series (modes I/II) and the beam series as visibly separate
curves instead of merging them into one "η".
Reading the popup: quote the model curve (η_model) as the tune
depression; read the beam markers as a match-quality diagnostic. If
the beam markers matter to you (e.g. for a TraceWin kx/ky
comparison, which is the same beam quantity), first rematch the beam
into the section — the markers then collapse onto the model curve, up
to the projected-β caveat.
Coupled sections (solenoid channels)¶
Inside a solenoid the transverse planes are coupled: the plane-wise 2×2
trace is meaningless. channel_phase_advance detects coupling
(coupled_xy=True) and switches to normal modes I/II, extracted from
the det-normalized 4×4 map (det(M₄)^{1/4} normalization so adiabatic
damping is not mistaken for instability) and paired bare↔depressed and
cell↔cell by symplectic eigenvector overlap. Machine totals are the sums
of the paired per-cell principal increments — never the aliased eigen-angle
of a whole multi-cell product. Keys become mu_{I,II}_{bare,dep}_deg,
eta_{I,II}.
Projected β is not a normal-mode tune. Integrating ∫ds/β on the projected x/y β inside a coupled solenoid interior overshoots (measured +16–20 % through the PIP-II HWR).
beam_phase_advancesetsprojected_only=Trueand downgrades its authority when it sees coupling; trust the channel modes there.
Beam RMS phase advance — beam_phase_advance¶
beam_phase_advance(results, period, sigma0=...) integrates ∫ds/β from the
tracked beam moments over the period. It is TraceWin-comparable but
beam-state dependent, and returns validity flags rather than a bare number:
mu_x_deg/mu_y_deg/mu_z_deg;matched(bool) withmismatch_x/y/zand covariance-basedmismatch_bmag_x/y/z(BMAG) — acceleration/frequency scaling is not counted as mismatch;projected_only— the coupled-interior guard above;n_samples/samples_per_period/resolution_ok— sampling adequacy (≥ 20 samples per 2π);n_skipped_x/y/n_total— invalid-β samples split the integral rather than being bridged (bridging fabricates phase across a gap);sigma_over_sigma0_{x,y}whensigma0=is supplied (uses the branch-consistentmu_*_branch_degdenominator).
μ_z uses the effective (z, z′) β, converted from the native β_φW via
the recorder Jacobian and the 1/γ² longitudinal drift factor — not the
deg/MeV β, which is off by ~685× at PIP-II injection if used directly.
Two ways to integrate the beam phase — method¶
Both beam_phase_advance and beam_phase_advance_along_s take
method="auto" | "maps" | "trapezoid" (default "auto"):
maps—beam_phase_advance_from_maps: for every element the beam ellipse is re-seeded from the recorded Σ at the element entrance and walked through the maps the solver actually applied to Σ inside that element (the phase probe'sprobe_Mslices — Strang halves, SC kicks, edges, freq-jumpD, tilt rotations), summing|atan2(m₁₂, m₁₁β − m₁₂α)|per slice exactly like the structure walk. A beam matched to a cell reproduces the cell's depressed eigenphase to round-off on any record grid (Floquet). Interior substep rows of the along-s curve are linearly interpolated inside their element (interpolated_rows); every element exit row — hence every per-cell and per-period number — is exact.trapezoid— ∫ds/β on the record grid. This is TraceWin's own kx/ky/kz convention (endpoint-density average per element), so the TraceWin-outputs writer keeps it. On an element-exit grid (a run without Record per-sub-step, or any multi-particle run) it is 12–38 % too high through 1.4–2.1 m cavities even for a perfectly matched beam (fnalscl, 116–405 MeV: cell 1 +12 % x / +14 % y, cell 13 +30 % / +38 %); with substep recording through those cavities it is exact to 1e-5, but drifts at I = 0 get no substeps and x–y-coupled interiors integrate a projected β, so the substep trapezoid is not exact in general. The routine now reportsmax_step_{x,y,z}_degandresolution_ok_{x,y,z}(False when a single row step exceeds 18°, i.e. fewer than 20 samples per 2π;resolution_okandmax_step_degsummarise x and y — the z "phase" of a DC or RF-free beam is never resolvable), and the popups draw an unresolved plane dotted and grey with a note instead of a solid marker.
auto takes the maps when the results carry a probe (every GUI envelope run
with the matrix solver does; the Sacherer solver records none) and falls back
to the trapezoid otherwise. Multi-particle results have
Σ but no maps: pass the companion envelope probe as maps_from= (the popups
do this from their cache) — its space-charge slices are linearised on the
envelope beam, so the result is labelled approximate for MP. Both
functions return method and maps_source ("own" / "companion").
The sigma_over_sigma0_{x,y,z} denominator is the oriented branch of the
structure tune in x and y and 360° − that branch in z: the (Δφ, ΔW) pair has
the opposite handedness (a drift's phase slip makes m₁₂ < 0), so the old rule,
which divided z by the branch itself, made a matched beam read 0.36 instead
of ≈ 1. A "nearest branch" rule would be wrong in x/y for periods above
180° with a depressed beam below it, so it is not used.
structure_phase_advance_along_s now replays the reference particle exactly
like compute_transfer_matrix (RF phase advanced through drifts and
magnets, stateful cavities reset): on decks whose cavity matrices depend on
the arrival phase (absolute-phase NCELLS, SET_SYNC_PHASE field maps) the old
walk evaluated them at a wrong phase and the per-cell σ₀ diverged (fnalscl
cell 1: 135° for an eigenphase of 50.7°).
Branch conventions¶
Phase can be quoted three ways; HELIX exposes all three explicitly so η is never the ratio of two different conventions:
mu_*_principal_deg— folded to [0, 180°] per cell;mu_*_oriented_deg— signed, mod 360°;- unwrapped machine totals — the sum of per-cell increments (what η uses).
The CLI twiss command prints the principal value, adding the oriented
value in parentheses when they differ; -q output is unchanged.
New space-charge diagnostics¶
Two diagnostics build directly on the channel tunes:
- Hofmann stability analysis — per-cell growth rates from the corrected anisotropic l=2/3/4 KV dispersion relations, with the trajectory overlaid on the solved growth-rate chart.
- Tune footprint — the incoherent per-particle tune spread under a frozen space-charge field.
Period detection¶
from linac_gen.analysis.period_detect import detect_periods
for p in detect_periods(lattice):
print(p.label, p.start, p.end, p.n_repeats)
print(p.spans()) # explicit (start, end) raw-index pair per repeat
detect_periods() returns PeriodicStructure dataclasses carrying
start / end (element indices, end-exclusive), inner_period_length,
inner_slice_end (index just past the first repeat), n_repeats, label,
source, and repeat_spans — accessed via p.spans(), the explicit
contiguous span of each repeat. Always iterate cells with p.spans()
(not a constant raw stride): when skipped commands/markers are unevenly
distributed, a fixed stride lands on the wrong boundaries.
GUI¶
Beam curves and markers in both popups come from beam_phase_advance_along_s(method="auto");
the info line says which method produced them ("exact (probe maps)", "companion probe maps",
or an "unresolved" warning with the largest row step when only a coarse trapezoid was possible).
The Results tab carries the Phase advance σ₀ · σ popup (channel tunes + along-s curves, coupled → eigenmodes), the tune-depression popup (η_model per cell as the primary series, Δμ_rms as markers), plus the new Hofmann stability chart and Tune footprint popups. The Matching tab shows a σ₀ / σ_model + η_model / Δμ_rms panel with the validity tags.
Multi-particle results carry no probe maps (the PIC field has no recorded tangent map), so the model curves cannot be extracted from them directly — the beam Δμ_rms markers still work. The tune-depression popup then offers a Compute channel model button: it runs a companion envelope probe in the background with the current Beam-tab configuration and plots σ₀/σ_model/η_model alongside the MP beam markers, labelled with their provenance. The probe result is cached and shared, so the Hofmann chart fills from the same run; the cache invalidates automatically when the lattice or any beam-config field changes.
Both the tune-depression and phase-advance popups also compare the beam current the results were produced at (recorded by the envelope solver, the multi-particle tracker and the backtracker, and carried by saved results files) against the live Beam-tab current, and show a STALE banner when they differ — the σ curves then reflect an earlier configuration and a re-run (Ctrl+R) refreshes them. Results whose run current is unknown (hand-built recorders, openPMD imports) show no banner.
Cross-references¶
- Hofmann chart & tune footprint
- Tracking modes
- Matching → recipes — periodic Twiss matching.
linac_gen/analysis/phase_advance.py:1linac_gen/analysis/period_detect.py:37—PeriodicStructure.