LEBT compensation (SCC)¶
linac_gen.analysis.scc computes residual-gas space-charge
compensation for DC/continuous (LEBT) transport — the physics behind
the neutralisation factor that the
SpaceChargeComp card consumes.
Until now that factor had to be guessed; this analysis derives it from
the gas physics and your actual run.
Ported from the author's standalone SCC simulator (analysis layer only — HELIX's own DC tracking supersedes its trackers), and pinned against that tool's cross-language test-vector contract.
Physics¶
- Gas library — H₂, He, N₂, Ar, Kr, Xe with NIST/ICRU-sourced parameters; ionisation cross sections use an analytic Rudd reciprocal-sum model, stripping/capture are tabulated (5–200 keV H-equivalent, per-channel extrapolation).
- τ_scc = 1 / (Σᵢ nᵢ σᵢ v_b) over the mixture; f_c build-up is the
exact relaxation
df_c/dt = (η_ss − f_c)/τ_scc. - η_ss, "Computed" mode — a self-consistent 1-D radial nonlinear Poisson–Boltzmann balance: trapped secondaries (ions for negative beams, electrons for positive) are Boltzmann-distributed in the self-consistent potential, and their production (Σ n σ_ion I/|q|) balances escape over the well; over-neutralisation is flagged, not hidden. "Assumed" mode uses your η directly.
- Beam potential φ(z) = ±I(z)/(4πε₀βc)·[1 + 2 ln(r_pipe/a)]·(1−f_c), with a = 2σ from the run's actual envelope and r_pipe from the lattice apertures.
- Residual-gas beam loss — stripping (H⁻/D⁻) and capture (H⁺/D⁺/He⁺) optical depth along z; a loss channel independent of the magnetic/intra-beam stripping diagnostics.
- End taper — a phenomenological reduction of η over the first/last 15 % of the line (floor 0.25), standing in for axial escape at the extractor and RFQ repeller. Optional; the three magic numbers are disclosed, not derived.
Calibration disclosure
Cross-section shapes and peak positions are sourced; the twelve
absolute magnitudes are an inherited calibration to
Valerio-Lizarraga CERN-THESIS-2015-121 (roughly 10× published bare
σ_ion — defensible as effective values folding in secondary-electron
cascade ionisation). Do not quote them as literature values. The
45 keV anchors are pinned by tests/analysis/test_scc.py.
External anchor: published PIP2IT/PXIE measurements
On a three-solenoid 30 keV H⁻ LEBT at the published operating conditions, the analysis lands inside every measured band from Prost, Carneiro & Shemyakin (PRAB 21, 020101 (2018)) and Carneiro et al. (NAPAC2016, TUPOB64): build-up completes well inside the measured ≲250 µs bound at 5×10⁻⁶ Torr, τ_scc is "tens of microseconds" at source-chamber pressures, the upstream steady-state neutralisation degree (≈0.97) sits in the 80–100 % fitted band, and a cleared section downstream of the second solenoid reproduces the published un-neutralised-section pattern self-consistently. One honest tension: at the chopper-region pressure (~10⁻⁷ Torr) the inherited calibration gives τ_scc ≈ 0.2 ms where the papers' bare-σ estimate is ~ms — the measurements only bound τ, so both pass, but low-pressure build-up times should be treated as order-of-magnitude.
Python API¶
from linac_gen.analysis.scc.driver import scc_analysis
from linac_gen.core.lattice import Lattice
from linac_gen.core.particle import H_MINUS
from linac_gen.core.reference import ReferenceParticle
from linac_gen.elements.drift import Drift
from linac_gen.elements.solenoid import Solenoid
from linac_gen.tracking.envelope import EnvelopeSolver
lattice = Lattice() # two-solenoid LEBT, 30 keV H-
lattice.add(Drift(name="D0", length=300.0, aperture=60.0))
lattice.add(Solenoid(name="S1", length=250.0, field=0.16, aperture=60.0))
lattice.add(Drift(name="D1", length=600.0, aperture=60.0))
lattice.add(Solenoid(name="S2", length=250.0, field=0.16, aperture=60.0))
lattice.add(Drift(name="D2", length=300.0, aperture=60.0))
ref = ReferenceParticle(species=H_MINUS, w_kin=0.030, frequency=162.5)
initial = dict(alpha_x=0.0, beta_x=0.5, emit_x=0.20 / ref.bg,
alpha_y=0.0, beta_y=0.5, emit_y=0.20 / ref.bg,
alpha_z=0.0, beta_z=1.0, emit_z=0.0, continuous=True)
results = EnvelopeSolver(lattice, ref.copy(), initial, current=15.0).run()
a = scc_analysis(results, lattice, species="H-", gas="N2",
pressure_mbar=2e-5) # DC results required
a["fc"], a["phi_V"], a["tau_scc_us"] # per-z profiles
a["scc_cards"] # suggested card factors
mode="assumed" with eta_assumed=… bypasses the balance;
build_up_us=… evaluates the transient instead of the steady state;
gas={"Ar": 1e-5, "H2": 4e-6} describes mixtures (an H₂ baseline of
10⁻⁶ mbar is always added, as in the source tool). Bunched results are
refused with a reason — the exact mirror of the
Hofmann diagnostic's
DC refusal.
Cleared regions¶
Real LEBTs are not uniformly neutralised: the PIP2IT/PXIE line is
deliberately operated with the last ~1 m before the RFQ un-neutralised
(the chopper kicker at −300 V DC sweeps the compensating ions out, while
+50 V electrically-isolated diaphragms confine them upstream — Prost,
Carneiro & Shemyakin, PRAB 21, 020101 (2018)). Pass
cleared_regions=[[start, end], …] (element index ranges, inclusive,
against the lattice the results were tracked with) and an optional
cleared_residual (default 0): inside each range f_c is forced toward
the residual through a smooth flat-top window (same tanh edge shape as
the end taper — a clearing field's fringe is soft, not a step), applied
after the end-taper and before the build-up transient. Suggested cards
over a cleared stretch inherit the low factors automatically. The
window needs a resolved record grid: on a coarse one-record-per-element
run the analysis flags the region as UNRESOLVED (enable
record_substeps in the Numerics tab, or use Iterate, which
resolves substeps itself).
Self-consistency¶
The plain analysis is one-shot: f_c comes from the σ(z) profile of a
run that was tracked at the run's own space-charge state, so the
suggested cards are a first iteration, not the fixed point (the result
carries a note saying so). scc_self_consistent closes the loop — it
repeatedly re-runs the envelope on a working copy of the lattice
with the suggested SPACE_CHARGE_COMP cards until the factors stop
moving (the physical fixed point f_card = f_c(σ(f_card))):
from linac_gen.analysis.scc.iterate import scc_self_consistent
from linac_gen.core.config import BeamConfig
beam_config = BeamConfig(species="H-", energy=0.030, frequency=162.5, current=15.0,
n_particles=1000, emit_nx=0.20, emit_ny=0.20,
alpha_x=0.0, beta_x=0.5, alpha_y=0.0, beta_y=0.5,
emit_z=0.0, alpha_z=0.0, beta_z=1.0, continuous=True)
a = scc_self_consistent(lattice, beam_config, gas="N2",
pressure_mbar=2e-5) # DC beam config required
a["iterate"] # {converged, n_iter, history, omega, tol}
a["scc_cards"] # the set the final run was tracked with
The σ-coupling enters the balance only through the well-depth logarithm,
so the loop typically converges in 2–3 envelope passes; omega < 1
under-relaxation is available as insurance if a configuration ever
oscillates, and non-convergence is reported honestly
(iterate["converged"] = False plus a note) rather than raised. The
caller's lattice is never modified — pre-existing cards are set aside
(with a note) so the iteration starts from the uncompensated line.
Limitation: the loop iterates the envelope model (no scraping losses);
verify the converged state with one MP run.
GUI¶
Results tab → LEBT compensation (SCC) tile (TWISS · DIVERGENCE ·
HALO section; needs DC results). Controls mirror the source tool: gas
species, log-pressure slider (10⁻⁷–10⁻⁴ mbar), temperature, solver mode
(Computed / Assumed with the η spin greyed accordingly), trapped-ion
temperature, end-taper toggle, and build-up time. Compute runs off
the GUI thread and plots f_c(z) with η_ss(z), φ(z), τ_scc(z), and the
gas-survival profile; Pressure scan sweeps 10⁻⁷–10⁻⁴ mbar and plots
the scalars against log₁₀ pressure. The cleared region row forces
f_c toward a residual value over an element range (tick the box, pick
the inclusive element span and the residual — see Cleared regions
above). Iterate runs the
self-consistency loop (needs the Beam tab set to a continuous beam; it
works even when no results are loaded, since it runs its own envelopes)
and reports the convergence status in the info line. Export cards
copies the suggested SPACE_CHARGE_COMP deck lines; Apply to
lattice… inserts them into the loaded lattice after confirmation
(existing SCC cards are replaced, current results clear, and the deck
must be saved to persist — the dialog says all of this).
Assistant¶
The lebt_scc tool (tier compute, background job) runs the same
analysis headlessly with the same parameters and returns the profiles,
scalars, suggested cards, and deck lines as JSON.
self_consistent=true switches it to the iteration loop and adds the
iterate convergence block to the reply; cleared_regions /
cleared_residual mirror the driver parameters (validated in-band).