Skip to content

Circuits, jitter and envelope ensembles

These tools cover transfer-line tolerance questions such as: "Is a 0.4 % magnet-to-magnet GdL spread acceptable after orbit correction, and does sorting the magnets help?" They add four pieces to the error engine:

  1. power-supply circuits for the error draws;
  2. pulse-to-pulse jitter on top of a frozen, corrected seed;
  3. a fast model-ORM SVD correction;
  4. an envelope ensemble runner that works on many seeds in parallel.

Power-supply circuits

A specification such as "integrated field within x % of each other" only matters where several magnets share one supply:

  • A series string is set to the mean transfer function of its magnets, so only the spread about that mean survives.
  • A magnet alone on its supply absorbs its own error through its current.

ErrorStudy.add_error takes a circuit map and a mode:

Circuit members may be element labels (deck names such as QD101) or element names (QUAD_001).

circuit_mode chooses how the draws are related:

Mode Draws Use for
independent one per element (the historical stream, bit for bit) uncorrelated errors
about_mean one per magnet, minus its circuit's mean; zero on single-magnet circuits and on magnets in no listed circuit magnet-to-magnet spread
common one per circuit, shared by its magnets; magnets in no listed circuit get their own draw power-supply excursions and jitter

assignment="sorted" (only with about_mean) installs the drawn values as a production batch instead of in drawn order:

  1. The magnets furthest from the batch median go to the individually powered positions.
  2. The rest are sorted and cut into contiguous runs, one run per series string, largest string first.

This is the sorting rule of the BTL exercise. For the rule alone, see linac_gen.errors.error_model.sorted_assignment.

Sorting is done per magnet family (class and length, see magnet_type). An arc-dipole outlier can never land in a short chicane-magnet position, and large-aperture quads form their own batch. The largest string takes the central (densest) run of the sorted batch, and the others alternate outward.

into_slot=True writes *_rel draws into the element's relative slot (gradient_rel) and leaves the design gradient untouched. A later circuit re-match, which sets one shared absolute setpoint, then keeps the spread.

listed_only=True draws only for matched magnets that sit on a listed circuit. Use it to give one supply family out of a broader pattern its own definition, for example the EOL dipole pair out of BEND_*.

At registration the circuit map is checked against the lattice, and a warning is raised for each of these cases:

  • a member matches no element;
  • a magnet is listed on two circuits;
  • a circuit is only partly inside the pattern (its mean would use the matched members only).

audit_errors() reports the grouping of each definition: its circuits and its members that are on no listed circuit.

The BTL 2026v0916 circuit map comes from ED0047700 and is generated by examples/pipii/btl/make_circuits_2026v0916.py into examples/pipii/btl/circuits_2026v0916.json. The JSON includes a list of the document's discrepancies.

Independent random streams (jitter)

ErrorStudy(..., rng_stream=k) draws element errors from (seed, TAG, *k, 0) and beam errors from (seed, TAG, *k, 1). The default, k = 0, is the historical stream: (seed, 0) for elements and (seed, 2) for the beam.

The constant tag word is needed because numpy's SeedSequence zero-pads short entropy. An untagged (seed, *k) would make k = (0, 0) replay the default element stream and k = 2 replay the default beam stream.

stream_entropy(seed, *key) (at least one key) gives auxiliary per-seed streams that cannot coincide with any of these for seeds and keys below 2³². Note that ErrorStudy.run's own particle sampling and multi-particle BPM noise do not see rng_stream. The ensemble draws its BPM offsets and noise from one, keyed by the study's stream.

linac_gen.errors.ensemble.run_jitter uses this to draw pulse p on top of one corrected static seed from the stream (p, 3, static_seed). Jitter therefore never replays the static seed's draws, and two static seeds never share pulses:

Pulses are not re-corrected. Every metric is relative to the frozen lattice: the orbit keys, including the maxima along the line (x_max_mm, y_max_mm), are the pulse orbit minus the frozen seed's orbit (the jitter alone), and β-beat, BMAG and dispersion use the frozen lattice as the design.

Model-ORM SVD correction

linac_gen.errors.orm_correction.OrmCorrector builds the response matrix once, from the envelope phase probe (OrmModel). It needs one zero-current run and includes all four x/y blocks. The finite-difference route in apply_correction needs two tracking runs per steerer per pass.

  • Model lattice. The response is built on the lattice you pass, normally the design lattice, which is the realistic case.
  • Readings. The corrector reads the envelope centroid (c → M c + o, including dipole field and roll errors). A BPM reads true − offset + noise.
  • Planes. By default each BPM reads only the plane in its MAD-style label (HP…/VP…) and each steerer kicks in its own plane (HT…/VT…). Pass bpm_planes={index: "xy"} for dual-plane hardware. The PIP-II warm BPMs are four-button units (BI PRD), so the BTL study reads both planes.
  • Singular-value cut. Use n_sv (keep the largest n) and/or rcond (relative cut, default 1e-9). Scan the cut: a layout where BPMs sit just behind their steerers is ill-conditioned, and with BPM noise it needs a real cut.
  • Limits. Kicks are clipped to vmax_Tm.

The BTL deck with correctors, examples/pipii/btl/btl_2026v0916_corr.dat, is generated by convert_BTL2026v0916.py. It contains 52 zero-kick THIN_STEERING correctors (25 H, 27 V) and 53 labelled BPMs, and it tracks bit-identically to btl_2026v0916.dat.

Envelope ensembles

linac_gen.errors.ensemble.run_ensemble(study, seeds, corrector=..., workers=N) runs each seed through the same steps:

  1. draw the errors (the study's own _apply_errors / _apply_beam_errors);
  2. correct, with per-seed BPM offsets and noise from stream (seed, 5);
  3. run the envelope;
  4. compute the metrics.

It uses a spawn process pool. The BLAS thread variables are set in the parent before the pool starts (a spawned child imports numpy before any initializer can run), threadpoolctl keeps each worker's BLAS on one thread, and each worker gets its share of the CPU threads setting. Serial and pooled results are identical.

The metrics for each seed, compared with design_reference, are:

  • the centroid at the observation point (the last record);
  • the maximum orbit;
  • the true orbit at the BPMs, each in its measured plane;
  • β-beat (max and rms) and BMAG at the end, both from the betatron Twiss (the Σ correlations with ΔW removed, so the arcs' dispersive beam size and space-charge x–ΔW correlation do not read as beat), plus the β and α changes at the end;
  • the change of the lattice dispersion function at the end (dispersion_along_s on the seed's lattice, current-independent);
  • the beam size;
  • the correction rms before and after, and the corrector use.

Losses need particles

The envelope has no loss model. To verify the worst and median seeds with the multi-particle tracker, start from ErrorStudy.draw_overrides(seed).lattice. That copy is uncorrected, so re-apply the seed's steerer settings, which run_ensemble(..., keep_kicks=True) keeps per seed.