Skip to content

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 map M_dep;
  • element_maps_bare[j] — the same slices with the SC factors set to identity → the consistent bare map M_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:

\[ M_{\text{dep}} \;=\; \prod_j M_j^{\text{slice}}\, K_j^{\text{SC}}, \qquad M_{\text{bare}} \;=\; \prod_j M_j^{\text{slice}}\cdot \mathbb{1}, \]

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,

\[ \cos\mu \;=\; \frac{\operatorname{tr} M_2}{2\sqrt{\det M_2}}, \qquad \det M_2 = \frac{(\beta\gamma)_{\text{in}}}{(\beta\gamma)_{\text{out}}}, \]

(coupled cells: eigenphases of the \(\det^{1/4}\)-normalized 4×4 block, modes I/II paired by symplectic eigenvector overlap). Then

\[ \sigma_0 = \mu\!\left(M_{\text{bare}}\right),\qquad \sigma_{\text{model}} = \mu\!\left(M_{\text{dep}}\right),\qquad \eta_{\text{model}} = \frac{\sigma_{\text{model}}}{\sigma_0}. \]

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

\[ \Delta\mu_{\text{rms}} \;=\; \int_{s_a}^{s_b} \frac{ds}{\beta_x^{\text{beam}}(s)}, \qquad \beta_x^{\text{beam}}(s) = \frac{\Sigma_{11}(s)}{\varepsilon_x(s)}, \quad \varepsilon_x = \sqrt{\Sigma_{11}\Sigma_{22} - \Sigma_{12}^{2}}, \]

— 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:

\[ \int_{\text{cell}} \frac{ds}{\beta_{\text{matched}}(s)} \;=\; \mu\!\left(M_{\text{dep}}\right) \;=\; \sigma_{\text{model}}, \]

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_advance sets projected_only=True and 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) with mismatch_x/y/z and covariance-based mismatch_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} when sigma0= is supplied (uses the branch-consistent mu_*_branch_deg denominator).

μ_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"):

  • mapsbeam_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's probe_M slices — Strang halves, SC kicks, edges, freq-jump D, 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 reports max_step_{x,y,z}_deg and resolution_ok_{x,y,z} (False when a single row step exceeds 18°, i.e. fewer than 20 samples per 2π; resolution_ok and max_step_deg summarise 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

H⁻ stripping · Hofmann chart & tune footprint →