SPIRAL2 LLRF fault-diagnosis pipeline
A working companion to the pipeline behind the LLRF fault-diagnosis paper: how raw superconducting-cavity postmortem buffers become labeled events, features, and models. Built up together, step by step — each section below fills in with real plots and notes as we explore it in conversation, rather than all at once.
Data & code availability. The structured feature matrix, ground-truth labels, intermediate signal data, feature ontology, and full results manifest underlying this page are archived on Zenodo: doi.org/10.5281/zenodo.22729653. Pipeline code: github.com/aghribi/LLRF_SP2_pipeline.
Pipeline at a glance
One trunk — extraction through feature engineering — then five independent
consumers of the same feature matrix, converging into a shared explainability and
reporting layer. This is an orientation diagram for our own exploration, distinct from
the paper's own fig:pipeline_overview (still a hand-drawn placeholder there).
PyPostMortem, labeled by the
deterministic ground-truth classifier Classify_PostMortemFile into seven
fault categories, quality-filtered, then preprocessed (high-pass filter, per-file
trigger alignment, Z-score normalization) into a 777-feature matrix. Five modeling
tasks each consume that matrix independently — root cause's target is the same
fault-label vector multi-label classification uses, not multi-label's model output, so
the two are siblings rather than chained. Diagram drawn from
report/sections/03_system_description.tex and
04_methodology.tex as of the 2026-09-07 revision; no fabricated numbers.
Data foundation
prepare_data_cluster_v7.py Confirmed 2026-09-09
Every event starts as a raw postmortem buffer captured by the LLRF system's trigger
logic across 27 monitored signals. Labels come from Classify_PostMortemFile,
a deterministic classifier built on the first hardware fault-register bit to trip
chronologically — not the older, less reliable summary ALM header field this pipeline
used to rely on. Four of the seven categories get a further physics-threshold refinement on
top of that hardware decode: a cavity-voltage fall of at least 50% within a bounded time
window separates a genuine quench from a false alarm; the reflected-power/cavity-voltage
ratio Ucav/Ucat > 8.5 marks an RF safety threshold exceedance; oscillation, gain,
and beam-anomaly subtypes of RF regulation out of tolerance come from Z-score+FFT
analysis of the drive and reflected signals. The remaining three categories are the hardware
decode alone.
Of 14,232 raw files scanned (2019–2025), 102 fail to parse and 20 are zero-byte, leaving 14,109 ground-truth rows. Before any of the seven fault categories or Normal is even considered, each file must pass six validity checks — in order, so a file failing an earlier check is never evaluated against a later one. Chart below reflects the corrected classifier (re-run 2026-09-09, see the callout further down) — the check order and the four reliability/state checks are unchanged, only the feed-forward check's scope changed:
AMPT/KPI are per-file acquisition-quality metrics, not state
flags; Ucav/mask are direct signal/metadata validity checks.
Counts from the re-run LLRF_result.csv, 2026-09-09.
BEAM=NON in the header doesn't mean "no beam present" — it means feed-forward
disabled, and the field has only been operationally meaningful since 2021 (before that it
defaults to NON because feed-forward didn't exist yet: 2019 100.0%, 2020 96.4% of
files header-flagged NON, vs. a genuine 50.3%→9.3% decline from 2021→2025).
Classify_PostMortemFile's _check_filters() has been fixed to scope the
check to 2021+ (gated on the acquisition's own header DATE field) and re-run against
the full 2019–2025 corpus.
BEAM=NON (a raw header statistic, unaffected
by the fix itself). 2019–2020 (faded bars) sit at a constant ~100%, consistent with
feed-forward not existing yet; 2021 on is a genuine, declining operational choice.
Fixing the scope recovers real events — not an estimate this time, the classifier has actually been re-run against the full corpus and the before/after is a direct diff:
Is the header trustworthy, or could a stuck bit be discarding files that were actually
running? — the coherence check from before the correction, still valid as a read of
what the header field itself behaves like (the sample below was drawn from the old, unscoped
Beam NON grouping, which mixes pre-2021 defaulted-NON files with genuine post-2021
feed-forward-off files — useful for checking the flag's physical meaning, not a current
rejection-count figure; see the corrected counts above for that). For a random sample of files
in each rejected group (n=100, plus a 150-file passing baseline mixing Normal and
every fault category), we read the real cavity-voltage (Ucav) and drive (Uci) waveform
amplitude via PyPostMortem, independent of the header flags that got the file
rejected.
Loop OFF is unambiguous: 95% collapse near zero, fully
coherent with an open control loop. Feed-forward off splits: median
0.39 MV/m (vs. 3.16 baseline), but ~35% of the sample sits inside the baseline's own
operating range, loop still closed — feed-forward is a control-loop compensation
feature, unrelated to beam presence, so a live-looking file with it disabled needs no
special explanation. The dead-looking majority lines up with the recoverable-data check
above: most of that group fails a real check (low KPI, open loop) once you look past the
feed-forward flag. AMPT and KPI are acquisition-quality metrics,
not state flags, so their large overlap with the baseline range was always expected.
n=100 per rejected group, n=150 baseline, single random sample
(seed 20260908), one pass — a first look, not a hardened estimate.
One more thing tested along the way: is there an independent beam-presence signal in
this data at all, now that the header field turned out not to be one? The classifier's own code
carries a field literally called Presence faisceau, but it's built by renaming a
per-sample état bit labeled Temp RF int on the way out — a mismatch between
the variable name and its documented source. Tested against 250 of the same sampled files: even
in the passing baseline, this bit reads "on" for essentially none of the trace (median 0.09% of
samples). That's not what a real, sustained beam-presence signal should look like, so this field
looks mislabeled or misassigned rather than trustworthy — worth a bug report to whoever
owns Classify_PostMortemFile, separately from the feed-forward correction above.
courant pickup (pickup-probe current) was the other candidate; it also failed,
pinning at a near-constant ~33 µA for the large majority of files in every group.
Net: there is currently no independently-verified beam-presence signal in this dataset at all.
Status: V7 extracted and validated. The classifier fix is applied, re-run, and the
recovery confirmed (+756 events, entirely 2019–2020, everything else untouched).
prepare_data_cluster_v7.py (forked from V6, with the
beam_present→feed_forward_enabled feature rename applied) has
run against the corrected ground truth: 2,865 events (2,866 ground-truth-passing minus
the same single feature-extraction failure V6 also hit), 1,478 Normal / 1,387 fault,
810 features (up from 777 — expected: the same 0.95 correlation-dedup threshold
keeps a different subset once the dataset's composition changes, not a bug), 246 PCA
components. Validated directly: NaN rate in the newly-recovered 2019–2020 events
(0.20% / 0.31%) is lower than the full corpus's own average (0.45%), and their
fault-category mix (majority Normal, RF regulation out of tolerance the largest fault
category) matches the rest of the corpus — no sign the recovered events are degenerate.
Steps 01–08 below still describe V6 — re-running them against V7 (Phase 2)
hasn't started yet.
Preprocessing
preprocess_signals() Explored 2026-09-09
Three operations, in order, applied to every one of the 27 signals independently: a
4th-order Butterworth high-pass filter (cutoff 0.01, normalized to each file's own Nyquist
frequency — i.e. relative to that file's own sample rate, since decimation varies
~250× across the corpus) removes DC offset and slow drift while keeping fast
transients; each event is aligned to its own true trigger sample, read directly off the
acquisition's real time axis (zero_idx = argmin(|time|)) rather than a fixed
offset; then the filtered signal is Z-score normalized over its own event window, with the
filtered-but-unnormalized version kept alongside it (the 2026-09-07 fix from step 02, so
real-amplitude statistics have real amplitude to work from).
The old bug, with a real number attached: for a representative NDEC=200 event
(2022 10 12 CMA01_0001, an Oscillation Ucav subtype of RF regulation
out of tolerance), the true trigger sits at sample 50,047 of 100,096. The pipeline's
old hardcoded trigger_idx=3000 would have pointed 534 ms early
— 47,047 samples off, 47% of the entire file — treating what was actually
mid-acquisition steady-state as if it were the pre-trigger precursor window. Every
"pre-trigger" and "post-trigger" feature in the old pipeline was computed from the wrong
slice of the trace for a file like this one.
Two more things worth being precise about, since they're easy to gloss over. First, the
precursor window used for early-fault-onset detection isn't "the first 3,000 samples" or
"34 ms" in some fixed sense — it's scale_samples(3000, dt_us),
which converts a sample count tuned at the dominant NDEC=200 spacing
(dt ≈ 11.4 µs) into whatever sample count gives the same
physical duration for this file's own dt_us. A file with NDEC=100 (like
several of the recovered 2019 events above) needs about twice as many samples for the same
34 ms. Second, one fault category — RF regulation out of tolerance — gets a
further onset refinement on top of the informatic trigger: Alexandre's AMPT-based physical
onset detection (find_true_t0_with_ampt), applied only to that category since
it's the one his validation actually covers, per the earlier user decision documented in the
methodology.
Feature engineering
engineer_features() Explored 2026-09-09Three families, computed for every event: 17 statistics × 27 signals over the full window (459 candidates); seven physics-informed families targeting specific LLRF dynamics (39 candidates: effective decay, detuning, microphonics, RF mismatch, modulator command, phase stability, RF power flow); and temporal/derivative features — per-segment slopes, finite differences, CUSUM change points, early/late-window ratios — the largest group by far. Before correlation filtering that's 1,200+ raw candidates; after the >0.95 dedup pass, 810 features survive in V7 (up from 777 in V6). Verified directly on V7 with the same family classifier the paper's SHAP analysis uses (100% coverage, zero unclassified features, both versions):
One family worth a close look because it's the most physically interpretive: effective system decay. Post-trigger, the pipeline fits cavity voltage amplitude (|Ucav|, in MV/m) to V(t) = V0·e−t/τeff over a fixed 500-sample-equivalent window (≈5.7 ms), by linear regression on ln|Ucav|. The report is explicit that τeff is not the cavity's intrinsic QL — with the LLRF loop still active and protection logic responding, what's measured is the whole system's response. First pass at this (below, corrected) picked one clean-fitting and one poor-fitting quench event and left it at "an active loop makes the decay non-exponential" — too glib, and the log-scale plot hid the actual units and made a genuinely important detail invisible. Redone properly: measured how long each of the 31 real quench events takes to drop below 20% of its pre-trigger amplitude, across the whole category, not two cherry-picked examples.
Checked whether the pipeline's own subtype-clustering step (09, k=2 search per category) already found this: it gives 19/9 for this category on V6's 28 events, and that smaller cluster (n=9) is disproportionately the group with no valid decay window (56% vs 26% in the larger cluster) — correlated with the fast/slow split, but not identical to it. Checked why: the real top-differentiating features between those two clusters (same Cohen's-d method the report's own subtype figure uses) turn out to be modulator-command and reference-frequency statistics, not the decay features directly — the existing clustering runs over the full 810-feature space and lands on whatever axis separates best overall, which isn't necessarily collapse-timescale. The collapse-time gap shown above is a cleaner, more directly physical signal than what the current full-feature clustering surfaces — worth flagging as a concrete, real methodology improvement: either add collapse-timescale as an explicit feature, or run the subtype search on the physics-informed decay features specifically rather than the full feature space, before trusting the current quench subtype split as physically meaningful.
Last check: the whole-signal-statistic normalization-order bug (Sec. 00/01, fixed 2026-09-07)
reduced near-constant features from 65 to 15 on V6. Re-ran the same check on V7:
16 near-constant features (std<1e-6 of 810) — same story, one more. The two
previously-disclosed non-functional features (reflected_saturation_fraction,
control_saturation_fraction, still-miscalibrated legacy thresholds) are still in
the list, unchanged. The one addition, llrf_loop_closed, is easily explained: V7's
recovered 2019–2020 events skew the corpus slightly more toward closed-loop operation,
pushing that flag's variance just under the threshold — a composition effect, not a new
bug.
Precursor detection
prepare_05_phase0_precursor.py Explored 2026-09-08Early fault-onset detection, restricted to the pre-trigger-only 386-feature subset so the task can't cheat off the post-trigger transient. Five unsupervised baselines (Isolation Forest, LOF, Mahalanobis distance on a PCA-reduced Ledoit–Wolf covariance, PCA reconstruction error, DBSCAN) establish how much separability exists without labels, then a class-weighted Random Forest is trained supervised on the same subset. An ensemble score combines all of the unsupervised methods with the RF's own probability output.
step_05_v6/precursor_detection.pkl — V6, the last dataset with a full
downstream re-run, not V7 (see the "Data foundation" step above and the sidebar
footer for the V6/V7 dataset status). Step 05 is one of the steps Phase 2 (re-running
everything against V7) would need to redo, and Phase 2 hasn't happened yet. Concretely:
none of the ~756 events
recovered by the feed-forward fix (mostly 2019–2020) were in this model's
training/test data, and the mislabeled beam_present field (only correctly
meaningful post-2021 on V6) may itself have been a feature the model leaned on. The one
finding below that's unaffected by this: the buffer-length numbers (568.4ms/1136.8ms),
which come from reading raw postmortem files directly, independent of dataset version.
The large supervised/unsupervised gap (0.999 vs 0.58–0.79) is a real, learnable decision boundary, not a window-length leak: a follow-up check restricting to only the already-fixed-window columns (382 of 766, same conclusion as the 386-feature production subset above) still gets RF AUC 0.9998 while Isolation Forest/LOF on the identical columns stay at 0.65/0.59 — unsupervised methods can't see what the label-trained model sees, which window length alone can't explain.
step_05_v6/precursor_curves_exploration.json.
A model really keying on an approaching fault should look uncertain far from the trigger and grow confident as it nears — instead, confidence is already saturated at the furthest offset tested and never moves. Taken alone, that pattern would suggest the model is picking up a stable per-capture characteristic unrelated to the fault itself, rather than genuine fault-onset proximity — the open caveat the report already carries (Sec. 6/7/8).
Net reading: there is a real, fault-specific signal present far earlier than the interlock trip — genuine early separability, not a spurious correlate. What it is not, per the flat curves above, is a signal that ramps up as the fault approaches; it appears (at least within the tested window) present at roughly constant strength well in advance, on fault-destined recordings specifically. Whether that constant early signature reflects a genuine slow-developing physical precursor or an early fault-associated operating-condition change that isn't itself the cause ramping toward the trigger is still open — this test rules out the “meaningless artifact” reading, it doesn't yet distinguish between those two remaining explanations.
Binary classification
prepare_06b_phase1_binary_pretrigger.py Explored 2026-09-09 — V7Fault-vs-Normal, restricted to the pre-trigger-only 418-feature "true precursor" subset (the same restriction step 05 uses) — the operationally meaningful version of this task, since a real-time system only has pre-trigger information available. Compared across Logistic Regression, Random Forest, XGBoost, and an RBF-kernel SVM. This, not the full-window version, is the primary binary-classification result: a full-window classifier can see the interlock-trip transient directly, which makes it trivially separable and not real evidence of a useful modeling capability — see the callout below for why it's kept only as a contrast.
2,865 events (V7), 860 in the held-out test set, 418 pre-trigger-only features. RF
stability check: 99.42%±0.32% accuracy across 10 repeated splits. Near-ceiling even
without seeing the interlock trip — not proof of what the underlying early signal is,
but real, independently-confirmed evidence (via a completely different, simpler model
design than step 05's unsupervised-ensemble approach) that fault-destined events carry a
strong, already-confident signal well before the trigger, matching step 03's
probability-curve finding. A systemic scaler/PCA-fit-on-full-dataset leak was found and
fixed earlier in this project (shared leakage_safe_features.py helper,
applied across all downstream modeling steps); these numbers are post-fix.
prepare_06_phase1_binary.py) used the
full event window, post-trigger included — fair challenge raised in review that a
classifier able to see the interlock-trip transient directly is "obvious," not real
evidence of a useful capability. Its numbers, for reference only: Random Forest 100.0%
(AUC 1.0000), XGBoost 100.0% (AUC 1.0000), Logistic Regression 99.9% (AUC 1.0000), SVM
99.3% (AUC 0.9997) — essentially the same near-ceiling
result, just with an easier task. Restricting to pre-trigger-only barely moved the
numbers, which is itself the interesting finding above, not a sign the full-window version
was somehow more informative.
Classify_PostMortemFile/src/classification.py,
not guessed), excluded every feature derived from that signal, and re-ran the same
category-vs-Normal, pre-trigger-only classifier.
Excluding the defining signal barely changes anything, for almost every category
— even removing 66 of 418 features (16% of the whole pre-trigger set, four entire
channels) for "Rég signal RF hors tolérance" leaves accuracy exactly where it started. The
one exception: RF safety threshold exceeded loses 0.7 points when its own
Ucav/A Ucr ratio is removed — the only category where that
signal carries real, non-redundant weight. For "Absence autorisation RF" (329 events, no
single defining channel in the code), a full 27-channel sweep — excluding each raw
signal one at a time — stays in a tight 99.1–99.8% band throughout: no single
channel matters much on its own, the signal is diffuse across many channels. ("Coupure
externe rapide," only 11 events, stayed flat at 100.0% for every one of the 27 exclusions
too — not trusted as a real finding, almost certainly just sample-size triviality
with a ~3-event test fold, not robustness.)
Read together with the two findings above: this is broad, category-by-category evidence that the pre-trigger early-detection signal isn't a disguised readout of each fault's own "obvious" physical quantity — it's genuinely redundant across many other pre-trigger channels too, for nearly every category tested.
Multi-label classification
phaseA/B/C_*.py, prepare_07(b)_*.py Explored 2026-09-09 — V7Four competing supervised approaches to the same seven-category task, each class-weighted appropriately for its own structure: Binary Relevance, Classifier Chains, and Label Powerset (all with RF or XGBoost base estimators), plus a MultiOutput Random Forest baseline and four deep multi-head architectures (CNN, Transformer, CNN-LSTM, MLP) sharing a per-label weighted BCE loss. Every fault event in this dataset carries exactly one active label by construction of the ground-truth classifier, so the genuinely multi-label case (several simultaneous faults) hasn't been exercised on real data yet. Label Powerset is the best-performing approach on the full-window comparison run of record; all four deep architectures underperformed the classical baselines on it.
Following the same pre-trigger question raised for step 04, and to check whether the supervised numbers reflect genuinely learnable structure rather than something only a labeled classifier can reach, we completed a full 2×2 grid: supervised vs. a newly built unsupervised counterpart, each in both full-window and pre-trigger-only (418-feature) conditions.
Repeated-split mean±std, 10 splits each. Pre-trigger-only stays in a broadly similar band rather than collapsing toward chance — the same pattern step 04 already showed for binary classification. Every approach shows the same rarest-category floor either way: "Coupure externe rapide" (11 events) scores exactly 0.000 F1 across every approach and every split — a sample-size floor, not a pre-trigger-specific weakness.
argmax(y_multilabel) convention —
exact here, since no event in this dataset ever has more than one active label.
Full-window, single-split headline AUC. No single method dominates — Mahalanobis is generally strongest, but "Absence autorisation RF" stays weak across every method (0.25–0.53), consistent with that category having no single defining physical signal in the classifier code either. Restricted to pre-trigger-only, the picture holds broadly but not uniformly: Mahalanobis for "Dép seuil de sécurité RF" is essentially unchanged (0.896 → 0.929), while for "Claquage ou quench cavité" it drops (0.625 → 0.466) — a real category-specific gap, unlike the uniform "barely moves" pattern the labeled comparison showed.
The sharpest contrast in the whole grid, and it runs the opposite direction from every other pre-trigger-vs-full-window comparison in this project. Full-window clustering recovers real fault-category structure (K-Means ARI 0.699, purity 0.787); pre-trigger-only clustering is substantially weaker across all three methods (K-Means ARI drops to 0.149). Read together with the tables above: real, learnable pre-trigger structure exists and survives labeled training, but recovering that same structure with no labels at all, purely from unsupervised geometry, is a much harder task — post-trigger content (the interlock-trip transient itself) makes the categories far easier to separate geometrically without needing any labels. This qualifies, not undermines, the pre-trigger early-warning story elsewhere in this project: genuine pre-trigger separability is present and learnable, just not yet strong enough to self-organize into the right categories unsupervised.
eps=0.5 for DBSCAN and got a degenerate
result — 100% of test events flagged as outliers, in both window conditions. Not a
finding, just a hyperparameter tuned for a different feature space. Re-tuned via the same
75th-percentile-of-5-nearest-neighbor-distance heuristic step 07's own sub-type clustering
already uses, instead of inventing a new one: full window ε=24.41 (22.1% outlier
rate), pre-trigger ε=12.41 (22.7%), both now genuinely informative with real
per-category spread (0.000–1.000 outlier recall depending on category).
Root cause identification
prepare_08_phase3a_rootcause.py Explored 2026-09-10 — V7
A Random Forest predicts the primary fault category among the seven, restricted to the
1,387 V7 fault events (Normal excluded) on the leakage-safe train-fold-only-scaled feature
set. The training target is argmax over the multi-label vector — a
deterministic first-flagged-category rule, not an independently labeled physical root
cause — and because the current dataset has zero genuinely multi-fault events, that
argmax currently has one unambiguous answer per event rather than exercising any real
tie-breaking. A real limitation of the task's current formulation, disclosed as such rather
than presented as a solved comparison.
417 held-out test events (of 1,387 fault events, 30% split), class_weight='balanced'.
As with every supervised step in this project, the repeated-split mean is the number to
cite — the single seed=42 split reads 1–2 points higher on both metrics, split
noise rather than a better model.
class_weight='balanced' (added in an earlier phase 4 mitigation pass) moves
six of seven per-label F1 scores by less than 0.02 — the exception is
Claquage ou quench cavité, +0.099 (0.696→0.795), this category's own
historically-noted rarity making it the one place reweighting has real headroom to work
with. "Coupure externe rapide" is the same rarest-category floor already documented in
steps 04/05 (11 ground-truth-passing events total): weighting can't manufacture signal
from a class this small, and scores exactly 0.000 every single split either way.
dataset_version silently hardcoded to "V6"
Same bug pattern already found and fixed in prepare_09c_enhanced_subclass_discovery.py
and prepare_06_phase1_binary.py during the V7 re-run — missed in this
script at the time. Both manifests here (08_phase3a_rootcause.yaml,
_stability.yaml) carry dataset_version: V6 even though
dataset_path correctly points at cooked_data_v7 and every number
above is genuinely V7 — a display-label bug, not a data bug, but exactly the kind of
thing that would mislead a future reader of the raw manifest. Fixed the same way as the two
sibling scripts (derive from the input directory name instead of a literal), not yet
re-run on the cluster since it only touches how this one metadata field is labeled, not any
metric.
Cross-check against step 05: this task — predict which of the seven categories, given a fault happened — is close kin to step 05's full multi-label classification, just restricted to fault-only events with a plain RF instead of Label Powerset's ensemble. The repeated-split macro-F1 here (0.782±0.012) lands almost exactly on step 05's full-window Label Powerset macro-F1 (0.780±0.029) — consistent with both tasks recovering essentially the same 7-way separability rather than root-cause framing adding genuinely new discriminative information beyond "which fault category is this." The 1,387-event fault count also reconciles exactly with step 00/01's V7 total (2,865 events, 1,478 Normal + 1,387 fault).
cooked_data_v7/step_08_phase3a_v7/root_cause.pkl
on cca (one-shot read-only command, no retraining). Diagonal reproduces the 93.5%
single-split accuracy exactly (390/417). Two things the per-label F1 table doesn't show on its
own: both "Coupure externe rapide" test events (n=2 of 11 total — a 30% split of a
class this small) land on other classes, one on each of the two largest categories, rather
than clustering on one wrong answer; and "Rég signal RF hors tolérance" is the dominant
attractor for errors in both directions — it receives 9 misrouted "Absence
autorisation RF" events and 4 misrouted "Dép seuil de sécurité RF" events, while itself
leaking 9 events the other way into "Absence autorisation RF". Consistent with it being the
majority class (250/417, 60% of the test set) rather than any specific physical confusability.
class_weight='balanced'
— the same numbers as the per-label table above, plotted to make the one real change
visible at a glance: Claquage ou quench cavité is the only category weighting meaningfully
helps (+0.099). "Coupure externe rapide" (11 events total) is flat at the axis floor either
way, labeled explicitly rather than left as an easily-missed empty gap in the chart.
Fault subtype clustering
prepare_09c_enhanced_subclass_discovery.py Explored 2026-09-10 — V7Within each fault category with enough events, K-Means, Agglomerative (ward/complete/average), and Gaussian Mixture clustering are compared by silhouette score to look for subtypes. Two corrections were needed before this was trustworthy: a near-zero-denominator division bug that let a handful of events take extreme outlier values, and a subtler bias where unconstrained silhouette maximization reliably chose whichever split isolated the single tightest minority subgroup — as small as one event — over a genuine population split. Fixed by winsorizing features at the 5th/95th percentile per category and requiring every cluster hold at least 15% of that category's events; a category with no candidate satisfying that floor reports "no balanced structure found" rather than falling back to an outlier-driven pick.
On V7, six of the seven categories now clear the 20-event minimum to attempt clustering at all (only "Coupure externe rapide", 11 events, is still too small) — up from five on V6, since "Seuil pick-up" grew from 12 to 94 events in the feed-forward-scope fix. Five of those six converge to a balanced k=2 split; the sixth, the largest category by far, doesn't:
excluded_categories_note still read "Seuil pick-up (12 events)
and Coupure externe rapide (11 events) are excluded" — true on V6, stale on V7 the
moment "Seuil pick-up" crossed the 20-event floor at 94. A literal string, not derived from
the run's actual data. Fixed to compute the excluded-category list (and its real counts) from
the run's own y_multilabel instead of a hand-typed sentence — same class of
bug as the dataset_version hardcodes already fixed here and in
prepare_06_phase1_binary.py (step 04) during the 2026-09-09 V7 re-run, and
fixed in prepare_08_phase3a_rootcause.py (step 06) earlier in this session
— this script had its dataset_version field fixed already, but this
second, separate hardcoded string was missed at the time. Not yet re-run on the cluster.
cca, one-shot, against
cooked_data_v7 — silhouette and method for every row match the manifest
exactly.
ssh cca, no retraining beyond what the pipeline itself already does): the
unconstrained top pick really is degenerate — a single-event outlier, 789-vs-1, exactly
what the 15% floor exists to reject. But three independent methods
(KMeans-2, Agglom-ward-2, Agglom-complete-2) agree, unprompted, on a ~101–103-event
(~13%) minority subgroup with silhouette ≈0.22 — a score that would have beaten
3 of the 5 categories that did pass. This is genuine, cross-method-agreed structure,
not noise; it simply sits about 2 points under a floor chosen as a round number, not derived
from this category's own distribution. Neither of the two candidate explanations from
2026-09-09 (feature-space shift, or real finer substructure the search can't express) turns
out to be quite right — it's closer to "real structure, floor slightly too strict for
this one category's natural split."
Explainability & reporting
generate_macros.py SHAP + saliency done, LIME running — 2026-09-10SHAP explains the classification models per physics-informed feature family, reported as per-feature-mean importance rather than a naive per-group sum — the statistical and temporal families outnumber the seven physics families combined roughly 20:1, so a group sum would structurally favor them regardless of how informative any single physics feature is. Every number in the paper, including every figure and table above, traces to a versioned, git-tracked results manifest rather than being hand-typed: each pipeline script writes its own manifest, a generator converts it into LaTeX macros, and a checker flags any bare numeric literal in the report text that isn't backed by one.
_v6 SLURM submit scripts — not stale V7 numbers, genuinely never run.
Two of the underlying scripts (phase3_weakness_diagnosis.py,
benchmark_inference_latency.py) hardcoded cooked_data_v6 directly
rather than reading SPIRAL2_COOKED_DIR like the others, so pointing them at V7
needed a real code fix, not just a new submit script — parameterized the same way as
every other script in this pipeline. Along the way, caught one more concrete break:
prepare_07a_phaseA_shap.py hardcoded its trained-model input directory as
step_07_phaseA_br, which doesn't exist under cooked_data_v7 (the
real V7 directory is step_07_phaseA_v7_br) — would have failed outright
rather than silently used wrong data, but fixed before submitting rather than found out from
a failed job. All upstream V7 models this depends on (binary RF, Label Powerset, precursor
RF, Phase A/B trained models) were verified to already exist on cca before
launching — no retraining needed, only the analysis layer on top of them.
All seven jobs finished clean (exit 0), 19 seconds to 5 minutes each — no reruns needed. Three real analyses below: the physics-group SHAP breakdown, the Phase-3 weakness diagnosis (imbalance correlation, a second confusion-matrix cross-check, a 9-subtype physics breakdown, and learning curves), and the inference-latency benchmark.
Physics-group SHAP
Saliency (deep models) — already complete on V7
Gradient-based saliency (∂output/∂input, per fault-category head) for all four
phaseD deep architectures (CNN, CNN-LSTM, MLP, Transformer), 200 sampled events × 810
features × 7 heads each. Already run on V7 in a prior session (jobs
58270040–43 and re-run 58277150–53, all COMPLETED — found via
sacct history, not something this session launched) but never reported here
until the question above prompted checking. Classified through the exact same physics-group
function the SHAP analysis uses, for a fair comparison:
meta_KPI, an acquisition-quality metadata field (already flagged back in
step 00 as "not a state flag"), not a physics signal — the other three architectures'
top feature is a genuine physics-adjacent Temporal/Statistical measurement. Not necessarily
a problem (KPI plausibly correlates with real fault precursors indirectly), but a concrete
thing to check before trusting the MLP variant's explanations at face value; the other three
architectures don't show this pattern.
LIME — launched, running
Same gap as SHAP: only _v6 submit scripts existed for LIME (Phase A/B/C, the
classical multi-label models). Fixed the identical step_07_phaseA_br hardcode
bug in prepare_07a_phaseA_lime.py (SHAP's own twin script had the same bug,
fixed earlier this session) and launched three V7 jobs: Phase A
(58367851), Phase B (58367852),
Phase C (58367853) — all confirmed running via
squeue. LIME fits a local perturbation-sampled surrogate model per instance,
far more expensive than SHAP's TreeExplainer (2-hour budget vs. SHAP's 1-hour), so these
are still in flight; numbers land here once they finish.
Weakness diagnosis
Imbalance really does hurt performance, now quantified rather than assumed: Pearson
r (category size vs. mean F1 across 4 multi-label models) = 0.402, rising to
0.731 against log₁₀(size) — the relationship is closer to
logarithmic than linear, consistent with "Rég signal RF hors tolérance" (n=790, mean
F1=0.935) and "Coupure externe rapide" (n=11, mean F1=0.049) sitting at opposite ends. A
second, independently-built root-cause confusion matrix (plain unweighted RF, not the
class_weight='balanced' one from step 06) reproduces the same pattern found
there: 391/417 correct (93.8%, vs. step 06's 93.5% weighted), same dominant confusion
direction (10 "Rég signal RF hors tolérance" events misrouted to "Absence autorisation
RF", the reverse direction weaker at 6) — consistent, not identical, cross-check.
Inference latency
Single-event, single-threaded, no batching — the realistic "one new postmortem file arrives" deployment scenario, 200 timed calls per model on V7 test data. Every model is comfortably real-time; the RF-based models (~12–13ms) are roughly 35× slower per event than XGBoost (0.35ms) despite solving a harder task (multi-label vs. a single Label Powerset code), consistent with RF's per-tree traversal cost against XGBoost's more compact boosted ensemble. Replaces the report's previously-unverified "45ms/event"/"<100ms" claims with a real, reproducible number; feature engineering time (reading the raw file, computing features) is not included, since it already runs offline, not per-event.
Glossary & acronyms
- LLRF
- Low-Level Radio Frequency — the control system regulating a cavity's RF field amplitude and phase.
- RF
- Radio Frequency — the oscillating electromagnetic field used to accelerate the particle beam.
- Cavity
- A superconducting resonant structure that transfers RF energy to the particle beam.
- Quench
- A sudden, localized loss of superconductivity in a cavity, causing a rapid collapse of the stored RF field.
- Postmortem buffer
- A short recording of signals captured automatically around a fault trigger, used for offline diagnosis.
- ALM
- Alarme (French: alarm) — the control-system bit flagging an active interlock/fault condition.
- Feed-forward
- An LLRF control-loop mode that pre-compensates known disturbances rather than reacting to them; operationally meaningful only from 2021 onward in this dataset.
- ROC AUC
- Area under the Receiver Operating Characteristic curve — a threshold-independent measure of binary classification quality (1.0 = perfect, 0.5 = chance).
- F1 / macro-F1
- The harmonic mean of precision and recall for one class; macro-F1 averages this unweighted across all classes, so rare classes count equally with common ones.
- SHAP
- SHapley Additive exPlanations — a game-theoretic method attributing a model's prediction to its input features.
- LIME
- Local Interpretable Model-agnostic Explanations — explains one individual prediction by fitting a simple local surrogate model around it.
- Saliency
- A gradient-based explanation method for neural networks: how much a small change in each input feature would change the output.
- Silhouette score
- A clustering-quality measure from −1 to 1, comparing each point's distance to its own cluster vs. the nearest other cluster.
- SPIRAL2
- The superconducting linear accelerator facility at GANIL, Caen, France.
- GANIL
- Grand Accélérateur National d'Ions Lourds — the French national heavy-ion accelerator laboratory.
Bibliography
Methods, prior work, and SPIRAL2-specific references used throughout this pipeline. Every DOI below was checked directly against Crossref or the publisher's own record; a handful of legitimate older or preprint-venue papers have no DOI at all, and are marked as such rather than given a guessed one.
- SPIRAL2 / GANIL — A. Orduz et al., “SPIRAL2 commissioning and operations”, Journal of Instrumentation 19, T05006 (2024). 10.1088/1748-0221/19/05/t05006
- SPIRAL2 LLRF control — F. Bouly et al., “Superconducting Cavity and RF Control loop model for the SPIRAL2 LINAC”, LINAC2022 (2022). 10.18429/JACoW-LINAC2022-THPOPA13
- AI for accelerators, overview — A. Ghribi et al., “Artificial intelligence for advancing particle accelerators”, Europhysics News 56(1), 15–19 (2025). 10.1051/epn/2025106
- ML for accelerator fault diagnosis — C. Tennant et al., “Superconducting Radio-Frequency Cavity Fault Classification Using Machine Learning at Jefferson Laboratory”, Phys. Rev. Accel. Beams 23, 114601 (2020). 10.1103/PhysRevAccelBeams.23.114601
- ML for accelerator anomaly detection — C. Peters et al., “Machine Learning for Time Series Prediction of an Accelerator Beam to Recognize Equipment Malfunction”, IPAC’21 (2021). 10.18429/JACoW-IPAC2021-TUPAB328
- ML for beam-loss classification — G. Valentino et al., “Machine Learning Applied at the LHC for Beam Loss Pattern Classification”, J. Phys.: Conf. Ser. 1067, 072036 (2018). 10.1088/1742-6596/1067/7/072036
- Quench prediction (LSTM) — M. Mertik & A. Skoczeń et al., “A Conceptual Development of Quench Prediction App based on LSTM and ELQA Framework”, arXiv:1610.09201 (2016). 10.48550/arXiv.1610.09201
- Model-based fault estimation — B. Sayyar-Rodsari & C. Schweiger, “Optimal Model-Based Fault Estimation and Correction for Particle Accelerators and Industrial Plants”, SLAC tech. report (2010). 10.2172/992983
- Rule-based accelerator fault diagnosis — E. Malandain & P. Skarek, “An Expert System for Accelerator Fault Diagnosis”, PAC’87, 559–561 (1987). JACoW archive — no DOI exists (pre-DOI-era proceedings).
- XFEL anomaly detection — A. S. Nawaz, “Probabilistic Model-based Anomaly Detection for the European X-ray Free Electron Laser”, PhD thesis, Universität zu Lübeck (2021). No DOI found (institutional-repository thesis).
- Isolation Forest — F. T. Liu, K. M. Ting & Z.-H. Zhou, ICDM, 413–422 (2008). 10.1109/icdm.2008.17
- Local Outlier Factor — M. M. Breunig et al., ACM SIGMOD, 93–104 (2000). 10.1145/342009.335388
- DBSCAN — M. Ester et al., “A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise”, KDD, 226–231 (1996). No DOI exists (pre-DOI-era proceedings).
- Deep autoencoding GMM (anomaly detection) — B. Zong et al., ICLR (2018). No DOI found (ICLR/OpenReview venue).
- GANomaly — S. Akcay, A. Atapour-Abarghouei & T. P. Breckon, ACCV, 622–637 (2018/2019). 10.1007/978-3-030-20893-6_39
- Silhouette score — P. J. Rousseeuw, “Silhouettes: A Graphical Aid to the Interpretation and Validation of Cluster Analysis”, J. Comput. Appl. Math. 20, 53–65 (1987). 10.1016/0377-0427(87)90125-7
- SHAP — S. M. Lundberg & S.-I. Lee, “A Unified Approach to Interpreting Model Predictions”, NeurIPS, 4765–4774 (2017). No DOI found (NeurIPS venue).
- LIME — M. T. Ribeiro, S. Singh & C. Guestrin, “‘Why Should I Trust You?’: Explaining the Predictions of Any Classifier”, ACM SIGKDD, 1135–1144 (2016). 10.1145/2939672.2939778
- Explainable AI, survey — A. Barredo Arrieta et al., “Explainable Artificial Intelligence (XAI)”, Information Fusion 58, 82–115 (2020). 10.1016/j.inffus.2019.12.012
- Classifier Chains — J. Read et al., ECML, 254–269 (2009). 10.1007/978-3-642-04174-7_17
- ML-kNN — M.-L. Zhang & Z.-H. Zhou, Pattern Recognition 40, 2038–2048 (2007). 10.1016/j.patcog.2006.12.019
- Multi-label decision trees — C. Vens et al., “Decision Trees for Hierarchical Multi-Label Classification”, Machine Learning 73, 185–214 (2008). 10.1007/s10994-008-5077-3
- Multi-label learning, survey — M.-L. Zhang & Z.-H. Zhou, IEEE TKDE 26, 1819–1837 (2014). 10.1109/tkde.2013.39
- Causal inference — J. Pearl, Causality: Models, Reasoning, and Inference, 2nd ed., Cambridge University Press (2009). 10.1017/cbo9780511803161
- Granger causality — C. W. J. Granger, Econometrica 37, 424–438 (1969). 10.2307/1912791
Project team
- Adnan Ghribi
- Charly Lassalle
- Alexandre Dalibard Brund
- Marco Di Giacomo
- Frédéric Bouly