This skill enables Claude to design, implement, and apply modular robustness testing for compound AI systems — pipelines composed of multiple interconnected neural networks or ML models. Using the SETA framework (Statistical Fault Attribution), Claude instruments a multi-model pipeline to record execution traces under clean and perturbed inputs, defines per-component metamorphic relations (correctness specifications), and computes Failure Contribution scores that statistically pinpoint which component is most responsible for system-level failures. This replaces opaque end-to-end metrics with fine-grained, per-module fault attribution.
When to Use
- When a user has a multi-model AI pipeline (e.g., detection -> classification -> OCR) and wants to know which component degrades most under perturbations
- When building robustness tests for a compound AI system and the user needs more than end-to-end accuracy
- When a user asks to trace how errors propagate from one model to the next in a pipeline
- When a user wants to compare component-level robustness across different perturbation types (noise, blur, weather)
- When debugging a production ML pipeline where system accuracy drops but it is unclear which model is at fault
- When a user needs to prioritize which model in a pipeline to retrain, harden, or replace
- When designing test harnesses for safety-critical multi-network systems (autonomous vehicles, medical imaging pipelines, inspection systems)
Key Technique
Modular testing via execution traces and metamorphic relations. SETA models a compound AI system as a state-transition system S = (Q, I, Phi, F, R, S) where Q is the set of computational modules, R contains routing functions that determine which module fires next, and F holds the model functions. For each test input x, the framework records an execution trace T(x) — a sequence of nodes v = (q, x_v, y_v, S_q) capturing which module q was activated, what it received, what it produced, and how to score it. A perturbation g(x) produces a perturbed trace T(g(x)). By aligning nodes across traces using structural identifiers, SETA compares each component's behavior under clean vs. perturbed conditions.
Per-component correctness via composite metamorphic relations. Instead of a single end-to-end metric, each module i gets a composite score S_i(x, x_tilde) = product of M_ij(x, x_tilde) over all its metamorphic relations. These relations encode domain-specific invariants: "detections should be a subset of originals" (for detectors), "class label should not change" (for classifiers), "IoU must exceed 0.9 for persisting detections," or "Levenshtein distance below threshold" (for OCR). The system-level score is S(x, x_tilde) = product of all S_i. A system failure occurs when S = 0.
Statistical fault attribution via Failure Contribution scores. The key metric is FC_i = E[Z_i * I(S(x, x_tilde) = 0)] where Z_i = 1 - S_i measures component i's deviation and I(S=0) indicates system failure. Empirically: FC_i = sum over all (input, perturbation) pairs of Z_i * I(S=0), divided by the total number of system failures. The normalized attribution weight alpha_i = FC_i / sum(FC_j) tells you each component's share of blame. A high alpha_i means component i is the primary bottleneck.
Step-by-Step Workflow
-
Map the pipeline topology. Enumerate every model/module in the compound system. Identify the routing logic: which modules are always invoked, which are conditionally invoked (e.g., a detector triggers specialized classifiers). Document input/output formats for each module.
-
Instrument execution trace recording. Wrap each module to capture (module_id, input, output) tuples during inference. Store traces as ordered lists of node records. Ensure the wrapper is architecture-agnostic — treat each module as a black box with known I/O types.
@dataclass class TraceNode: module_id: str input_data: Any output_data: Any score_fn: Callable # metamorphic relation scorer def record_trace(pipeline, x) -> list[TraceNode]: trace = [] # Run pipeline, intercepting each module call for module_id, module_fn in pipeline.modules_in_order(x): inp = pipeline.get_input(module_id, x, trace) out = module_fn(inp) trace.append(TraceNode(module_id, inp, out, pipeline.scorer(module_id))) return trace -
Define metamorphic relations for each component. Write scoring functions S_q(y, y_tilde) -> {0, 1} that encode what "correct under perturbation" means for each module. Common patterns:
- Detectors: (a) detections are subset of originals, (b) IoU > threshold for persisting boxes, (c) class labels unchanged, (d) downstream invocations unchanged
- Classifiers: output label matches reference label
- OCR/text models: Levenshtein distance below threshold tau
- Regressors: absolute difference below epsilon
- Embeddings: cosine similarity above threshold
-
Select and configure perturbations. Choose perturbation functions g(x) appropriate to the input modality. For images: use the
imagecorruptionslibrary (gaussian_noise, fog, frost, motion_blur, etc.) at multiple severity levels (1-5). For text: typo injection, synonym replacement. For audio: noise overlay, speed changes. Apply each perturbation to the full test set. -
Run paired inference (clean + perturbed). For each test input x and perturbation g, run the pipeline twice: once on x to get reference trace T(x), once on g(x) to get perturbed trace T(g(x)). Store both traces for alignment.
-
Align traces and compute per-component scores. Match nodes between T(x) and T(g(x)) by module_id. For each matched pair, evaluate S_i(x, x_tilde) using the metamorphic relation. For nodes present in T(x) but missing in T(g(x)) (routing divergence), record S_i = 0.
def compute_component_scores(trace_clean, trace_perturbed): scores = {} clean_map = {n.module_id: n for n in trace_clean} pert_map = {n.module_id: n for n in trace_perturbed} for mid, clean_node in clean_map.items(): if mid in pert_map: scores[mid] = clean_node.score_fn(clean_node.output_data, pert_map[mid].output_data) else: scores[mid] = 0 # routing divergence — component effectively failed return scores -
Compute system-level pass/fail. S(x, x_tilde) = product of all S_i. If any component fails its metamorphic relation, the system fails.
-
Accumulate Failure Contribution scores across the test set. For every (x, g) pair where S = 0, accumulate Z_i = 1 - S_i for each component. Divide by total system failures to get FC_i. Normalize to get alpha_i.
def compute_failure_contributions(all_results): fc = defaultdict(float) total_failures = 0 for scores, system_passed in all_results: if not system_passed: total_failures += 1 for mid, s in scores.items(): fc[mid] += (1 - s) alpha = {mid: fc[mid] / total_failures for mid in fc} if total_failures > 0 else {} total = sum(alpha.values()) alpha = {mid: v / total for mid, v in alpha.items()} if total > 0 else {} return fc, alpha -
Generate the fault attribution report. Present results as a table: component name, FC_i, alpha_i (attribution weight), broken down by perturbation type and severity. Highlight the component with highest alpha — this is where hardening effort should focus.
-
Recommend targeted interventions. Based on attribution weights, recommend specific actions: retrain the highest-alpha component with augmented data matching the most damaging perturbation, add input preprocessing to mitigate specific corruption types, or replace the weakest module with a more robust architecture.
Concrete Examples
Example 1: Autonomous Rail Inspection Pipeline
User: "I have a rail inspection system with a YOLOv4-tiny detector that feeds into 5 specialized classifiers (speed signs, signals, crossings, etc.). Accuracy drops under fog. Which component should I fix?"
Approach:
- Map the pipeline: f0 (YOLO detector) -> conditional routing -> f1-f5 (classifiers)
- Instrument each module to record traces
- Define MRs: for f0, detections subset + IoU > 0.9 + labels unchanged + routing unchanged; for f1-f5, label equality
- Apply fog perturbation at severity 1-5 from
imagecorruptions - Run paired inference on 500 test images x 5 fog levels = 2500 pairs
- Compute FC scores
Output:
SETA Fault Attribution Report — Fog Perturbation
=================================================
Component | FC_i | alpha_i | Failures (of 2500)
------------------|--------|---------|-------------------
f0 (YOLO det.) | 0.847 | 0.621 | 1,843
f1 (speed_sign) | 0.198 | 0.145 | 312
f3 (crossing_sig) | 0.214 | 0.157 | 287
f2 (main_signal) | 0.072 | 0.053 | 104
f4 (catenary) | 0.033 | 0.024 | 48
Recommendation: f0 (detector) accounts for 62.1% of system failures
under fog. Retrain with fog-augmented data or add defogging
preprocessing before detection.
Example 2: Document Processing Pipeline
User: "My pipeline has OCR -> NER -> relation extraction. It works on clean scans but fails on noisy faxes. Help me find the bottleneck."
Approach:
- Map pipeline: f0 (OCR engine) -> f1 (NER model) -> f2 (relation extractor)
- Define MRs: f0 uses Levenshtein distance <= 2, f1 uses entity set equality, f2 uses relation tuple matching
- Apply perturbations: Gaussian noise, JPEG compression, brightness variation at multiple severity levels
- Run on 200 test documents x 3 perturbation types x 3 severities = 1800 pairs
Output:
SETA Fault Attribution Report — Noisy Document Input
=====================================================
Component | FC_i | alpha_i | By Perturbation (alpha)
-----------------|--------|---------|------------------------
f0 (OCR) | 0.912 | 0.704 | noise:0.82 jpeg:0.54 bright:0.71
f1 (NER) | 0.247 | 0.191 | noise:0.12 jpeg:0.31 bright:0.18
f2 (RelExtract) | 0.136 | 0.105 | noise:0.06 jpeg:0.15 bright:0.11
Recommendation: OCR dominates failures (70.4%), especially under
Gaussian noise (alpha=0.82). Consider a denoising preprocessor or
switch to a noise-robust OCR model.
Example 3: Implementing SETA Harness in Python
User: "I want to add robustness testing to my image pipeline. Can you write the testing harness?"
Approach: Generate a reusable Python module implementing the full SETA loop.
Output (key structure):
# seta_harness.py
from dataclasses import dataclass, field
from collections import defaultdict
from typing import Any, Callable, Protocol
class MetamorphicRelation(Protocol):
def __call__(self, y_clean: Any, y_perturbed: Any) -> int: ...
@dataclass
class ModuleSpec:
module_id: str
fn: Callable
relations: list[MetamorphicRelation]
@dataclass
class PipelineSpec:
modules: list[ModuleSpec]
router: Callable # (input, prev_outputs) -> next_module_id
class SETAHarness:
def __init__(self, pipeline: PipelineSpec, perturbations: list[Callable]):
self.pipeline = pipeline
self.perturbations = perturbations
def run_trace(self, x):
"""Execute pipeline, return trace as dict of module_id -> output."""
...
def score_component(self, module_id, y_clean, y_pert) -> int:
"""Evaluate composite MR for one component."""
spec = self._get_spec(module_id)
return int(all(mr(y_clean, y_pert) for mr in spec.relations))
def evaluate(self, test_data) -> dict:
"""Run full SETA evaluation, return FC and alpha scores."""
fc = defaultdict(float)
n_failures = 0
for x in test_data:
trace_clean = self.run_trace(x)
for g in self.perturbations:
trace_pert = self.run_trace(g(x))
scores = {}
system_ok = True
for mid in trace_clean:
s = self.score_component(mid, trace_clean[mid], trace_pert.get(mid))
scores[mid] = s
if s == 0:
system_ok = False
if not system_ok:
n_failures += 1
for mid, s in scores.items():
fc[mid] += (1 - s)
alpha = {m: fc[m]/sum(fc.values()) for m in fc} if fc else {}
return {"fc": dict(fc), "alpha": alpha, "total_failures": n_failures}
Best Practices
- Do: Define metamorphic relations that are tight but realistic. An IoU threshold of 0.9 for detector persistence is meaningful; 0.5 is too loose to catch real degradation.
- Do: Test across multiple perturbation types and severities. A component robust to noise may be fragile to blur — SETA's per-perturbation breakdown reveals this.
- Do: Record routing divergences (when perturbation causes a different subgraph to execute). These are first-class failures in SETA, not just metric differences.
- Do: Normalize FC scores to alpha weights for cross-component comparison. Raw FC values depend on how many test cases invoke each component.
- Avoid: Relying solely on end-to-end accuracy. A pipeline with 90% accuracy may hide a detector that fails 40% of the time but is compensated by downstream filtering.
- Avoid: Applying white-box perturbations (adversarial gradients) across module boundaries. SETA is designed for black-box, input-level perturbations that propagate naturally through the pipeline.
Error Handling
- Trace alignment failure: If the perturbed input causes a completely different execution path (e.g., detector finds zero objects so no classifier runs), handle gracefully by scoring all skipped downstream modules as S_i = 0 and recording the routing divergence.
- Empty test set for a component: Some conditional modules may be invoked on very few test inputs. Flag components with fewer than 30 activations as having low-confidence FC scores. Consider stratified sampling to ensure coverage.
- Non-deterministic models: If a module produces different outputs on repeated runs of the same input, run multiple reference traces and use majority-vote outputs as the reference. Alternatively, set random seeds and disable dropout at test time.
- Perturbation incompatibility: If a perturbation produces out-of-domain inputs (e.g., negative pixel values), clip or normalize before feeding to the pipeline. Document which perturbations are valid for which input domains.
Limitations
- SETA requires black-box access to each component's inputs and outputs. Fully opaque API-only services where you cannot intercept intermediate representations need proxy wrappers to capture I/O.
- The framework assumes you can define meaningful metamorphic relations for each component. For generative or open-ended models (LLMs, diffusion models), defining binary pass/fail MRs is non-trivial and may require embedding-based similarity thresholds.
- FC scores identify which component fails, not why it fails. Root-cause analysis within a component still requires component-specific debugging.
- Statistical significance requires sufficient test data. With fewer than ~100 system failures, FC estimates have high variance. Use confidence intervals when reporting.
- SETA doubles inference cost (clean + perturbed runs per input). For large test sets or expensive models, budget computation accordingly or use sampling.
Reference
Paper: SETA: Statistical Fault Attribution for Compound AI Systems — Chowdhury & D'Souza, CAIN 2026. Look for: the formal state-transition model (Section 3), the FC_i formula and Algorithm 3 (Section 4), and the rail inspection case study with per-perturbation robustness breakdowns (Section 5).