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:
- power-supply circuits for the error draws;
- pulse-to-pulse jitter on top of a frozen, corrected seed;
- a fast model-ORM SVD correction;
- 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:
circuits = {"ARC2-QF": ["QF201", "QF202", ...], "BINJ-QFB1": ["QFB1"]}
study.add_error("QUAD_*", "gradient_rel", "uniform", half_width=2e-3,
circuits=circuits, circuit_mode="about_mean",
assignment="random", into_slot=True)
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:
- The magnets furthest from the batch median go to the individually powered positions.
- 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:
def jitter(js):
js.add_error("BEND_*", "field_rel", sigma=1e-4, cutoff=3,
circuits=dipole_circuits, circuit_mode="common")
js.add_error("QUAD_*", "gradient_rel", sigma=1e-3, cutoff=3,
circuits=quad_circuits, circuit_mode="common", into_slot=True)
js.add_beam_error("centroid_x", sigma=0.05)
pulses = run_jitter(corrected_lattice, cfg, jitter, 1000, static_seed=s)
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 readstrue − 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…). Passbpm_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/orrcond(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.
oc = OrmCorrector(design_lattice, cfg, steerer_idx, bpm_idx,
rcond=1e-3, vmax_Tm=0.01)
info = oc.correct(errored_lattice, n_iter=3,
offsets=bpm_offsets_mm, noise_mm=0.01, rng=rng)
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:
- draw the errors (the study's own
_apply_errors/_apply_beam_errors); - correct, with per-seed BPM offsets and noise from stream
(seed, 5); - run the envelope;
- 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_son 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.