Convergence guide¶
Practical guidance on grid sizing, particle count, and step density for HELIX simulations to actually converge. Wrong choices produce plausible-looking but wrong answers — this is where most simulation hours go.
TL;DR — production defaults¶
For a typical PIP-II-stage simulation (post-RFQ, ≤ 5 mA, ≤ 1 km lattice, 1 % σ accuracy target):
| Parameter | Value | Notes |
|---|---|---|
Particles n_particles |
5000-20000 | 1 % σ floor at √N statistical limit |
Grid nx=ny=nz |
96 (the default; 64 for quick tests) | 96³ converged for 5 mA |
grid_extent |
5.0σ | ~99.99% Gaussian retention |
kernel |
cic (with RF) or tsc (no RF) |
see Kernels |
green_kind |
igf |
always |
| Step density | PARTRAN_STEP 100 50 |
step1 = 100/m integration substeps, step2 = 50/m SC kicks (both apply to DRIFT + FIELD_MAP only) |
PARTRAN_STEP semantics — the card carries two steps-per-metre numbers:
- step1 drives the integration substep count for DRIFT and FIELD_MAP elements;
- step2 sets the space-charge kick cadence inside those same elements;
- quads, bends, solenoids (and other transfer-map elements) are always tracked in exactly 2 substeps with one mid-plane SC kick, regardless of this config;
drift_single_push(StepConfig, default on): a drift's map is exact, so itsstep1sub-steps only matter for what happens between them — space-charge kicks, sub-step diagnostics, the bunch-train phase fold. When none of those is active the tracker applies the map once and locates any loss inside the pipe analytically (see Drift). Where space charge kicks, the sub-steps stay and results are unchanged; a drift inside a fully compensated region (SpaceChargeCompfactor ≥ 1) applies no kick and is pushed once too. A zero-current run keeps the same transfer maps and no longer spends most of its time in drift sub-steps; the one place its result differs from the sub-stepped walk is a particle already outside a drift's pipe at the entrance (a drift narrower than the element before it), which is now lost at the entrance where the sub-stepped walk kept it if it had wandered back inside by the first sub-step end — transmission can be slightly lower there, and is closer to TraceWin's every-step check. Switch it off (Numerics tab,--drift-single-push off, orStepConfig(drift_single_push=False)) to reproduce the sub-stepped walk of HELIX ≤ 1.9.1 exactly. Note that with it on, astep1convergence scan at zero current only probes field maps.
Convergence checks¶
For any production run, verify convergence in three dimensions:
1. Particle count¶
Run the same lattice with n_particles ∈ {2000, 5000, 10000, 20000}
and plot σ_x at end of lattice. The 1/√N noise floor visible at
2000 should disappear at 10000. If σ_x changes significantly
beyond 10000, the simulation hasn't converged — likely indicates
strong non-Gaussian effects.
2. Grid size¶
Run with nx ∈ {32, 48, 64, 96, 128} (square cubic grids), plot
σ_x at end of lattice. Should plateau by 96; if not, double the
extent or check for unusual aspect ratios.
The scans live in the GUI's Numerics tab (Scan parameters group) — see Numerics tab; it automates the grid, extent, step1, and step2 sweeps.
3. Step density¶
Run with step1_per_m ∈ {25, 50, 100, 200}. Below 50/m,
short-element field maps under-sample. Above 100/m, gains
diminish but cost rises linearly.
Memory note: BTL CIC ε_z growth¶
Per memory note project_cic_grid_noise_in_no_rf_transport: in
long no-RF transport sections like the BTL, the CIC kernel + IGF
Green's function suffer from grid-noise accumulation that blows up
ε_z by ×14. Switching to TSC drops it to ×1.6 (matches TraceWin's
×1.7). See Kernels.
Memory note: PXIE LEBT silent failure¶
Per feedback_lebt_scc_and_envelope_continuous: PXIE LEBT needs
Ki=1 (SC compensation) AND the envelope solver needs
continuous=True. Forgetting either produces 6.93% mean error
that looks like a physics bug. Always verify these flags for
LEBT-class runs.
Memory note: PIC kernel calibration offset¶
Per project_lg_pic_kernel_calibration_offset: HELIX's PIC kernel
has a documented ~3% σ overshoot vs TraceWin partran on PIP-II.
This is separate from grid-noise / convergence issues — it's
a known absolute-calibration bias. When validating against TraceWin,
expect ~3% residual on σ. Higher residuals usually indicate a
parameter mismatch or convergence issue.
Runtime expectations — the transfer-matrix cache and who uses it¶
HELIX has an opt-in per-element transfer-matrix cache keyed on
(id(elem), elem_fingerprint, ref_fingerprint) with 12-decimal FP
rounding (linac_gen/tracking/matrix_tracking.py). Opt-in means a
caller must pass a cache= dict explicitly; with the default
cache=None every matrix is recomputed on every call.
Who actually passes a cache: only the GUI's structure-analysis
panels — Phase Advance and Tune Depression — via
structure_phase_advance(..., cache=...). There, the first analysis
of a freshly-loaded full PIP-II lattice costs ~340 s and repeat
analyses ~3 s (127× faster).
Who does NOT: envelope and multi-particle runs (the solvers call
get_element_matrix(element, ref) uncached), and the matching
engine — matching mutates element parameters in place on every
optimiser step, which is exactly the situation the cache is documented
as unsafe for. A simulation run therefore neither benefits from nor
populates the cache; expect the same wall-clock on every run.
Don't try to pre-warm the cache in a background thread
A MatrixCacheWarmer lives at
gui/linac_gen_gui/interphase/services/matrix_cache_warmer.py but
is deliberately not instantiated by the GUI (see comment at
app.py:174-177). Past experiments showed that even with a
running_changed guard the warmer's fitted-matrix Jacobian walks competed
for CPU on project / lattice load and made the GUI feel slow. If
you re-enable it without a new design (e.g. a truly idle warmer
that fires only after N seconds of zero UI events), expect the
same regression.