Fault Resilience — Hardware Fault Injection Suite
Systematic fault injection and degradation analysis for SNN deployments. Generates degradation curves (accuracy vs fault rate) and identifies the most vulnerable layers.
Fault Models
| Fault Type |
Description |
SC Relevance |
STUCK_AT_ZERO |
Weight forced to 0 |
Dead synapse in FPGA LUT |
STUCK_AT_ONE |
Weight forced to 1 |
Stuck bit in BRAM |
WEIGHT_BIT_FLIP |
Sign flip of weight |
SEU (single-event upset) from radiation |
DEAD_SYNAPSE |
Weight zeroed |
Manufacturing defect |
NOISY_MEMBRANE |
Additive noise proportional to weight std |
Thermal noise in analog circuits |
BITSTREAM_BIAS |
Probability shifted toward 0.5 |
SC-specific: correlator degradation |
Components
FaultResilienceSuite — Main test harness.
| Parameter |
Type |
Meaning |
eval_fn |
callable |
f(weights) → accuracy — evaluation function |
weights |
list of ndarray |
Baseline (unfaulted) weight matrices |
Key methods:
inject_fault(fault) — Apply fault model, return faulted weight copies
run_single(fault) → FaultResult — One injection experiment
sweep(fault_type, rates, per_layer) → ResilienceReport — Sweep fault rates
-
full_audit() → ResilienceReport — All fault types × all rates × all layers
-
FaultModel — Configuration: fault_type, rate (0-1), optional layer_index, seed.
FaultResult — Result: accuracy_before, accuracy_after, degradation.
ResilienceReport — Collection of results with degradation_curve(), most_vulnerable_layer(), summary().
Usage
from sc_neurocore.resilience import FaultResilienceSuite, FaultModel
from sc_neurocore.resilience.fault_suite import FaultType
import numpy as np
def eval_fn(weights):
# Your model evaluation here
return accuracy
weights = [np.random.randn(64, 32), np.random.randn(10, 64)]
suite = FaultResilienceSuite(eval_fn=eval_fn, weights=weights)
# Single fault experiment
result = suite.run_single(FaultModel(FaultType.STUCK_AT_ZERO, rate=0.1))
print(f"Degradation: {result.degradation:.3f}")
# Sweep fault rates
report = suite.sweep(FaultType.STUCK_AT_ZERO, rates=[0.01, 0.05, 0.1, 0.2, 0.5])
curve = report.degradation_curve(FaultType.STUCK_AT_ZERO)
# Full audit: all fault types × all layers
full = suite.full_audit()
print(f"Most vulnerable layer: {full.most_vulnerable_layer()}")
print(full.summary())
See Tutorial 63: Fault Resilience.
sc_neurocore.resilience
Systematic fault injection and resilience analysis for SNN deployments.
FaultResilienceSuite
Systematic fault injection and resilience analysis.
Parameters
eval_fn : callable
Function(weights) -> accuracy. Takes list of weight matrices,
returns accuracy in [0, 1].
weights : list of ndarray
Baseline (unfaulted) weight matrices.
Source code in src/sc_neurocore/resilience/fault_suite.py
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203 | class FaultResilienceSuite:
"""Systematic fault injection and resilience analysis.
Parameters
----------
eval_fn : callable
Function(weights) -> accuracy. Takes list of weight matrices,
returns accuracy in [0, 1].
weights : list of ndarray
Baseline (unfaulted) weight matrices.
"""
def __init__(self, eval_fn, weights: list[np.ndarray]):
self.eval_fn = eval_fn
self.weights = [w.copy() for w in weights]
self._baseline_accuracy: float | None = None
@property
def baseline_accuracy(self) -> float:
if self._baseline_accuracy is None:
self._baseline_accuracy = self.eval_fn(self.weights)
return self._baseline_accuracy
def inject_fault(self, fault: FaultModel) -> list[np.ndarray]:
"""Apply a fault model to weights, return faulted copies."""
rng = np.random.RandomState(fault.seed)
faulted = [w.copy() for w in self.weights]
layers = [fault.layer_index] if fault.layer_index is not None else list(range(len(faulted)))
for i in layers:
w = faulted[i]
mask = rng.random(w.shape) < fault.rate
if fault.fault_type == FaultType.STUCK_AT_ZERO:
w[mask] = 0.0
elif fault.fault_type == FaultType.STUCK_AT_ONE:
w[mask] = 1.0
elif fault.fault_type == FaultType.WEIGHT_BIT_FLIP:
# Flip sign of affected weights
w[mask] = -w[mask]
elif fault.fault_type == FaultType.DEAD_SYNAPSE:
w[mask] = 0.0
elif fault.fault_type == FaultType.NOISY_MEMBRANE:
noise = rng.randn(*w.shape) * fault.rate * np.std(w)
w += noise * mask
elif fault.fault_type == FaultType.BITSTREAM_BIAS:
# SC-specific: shift probabilities toward 0.5
w[mask] = w[mask] * (1 - fault.rate) + 0.5 * fault.rate
faulted[i] = w
return faulted
def run_single(self, fault: FaultModel) -> FaultResult:
"""Run one fault injection experiment."""
faulted = self.inject_fault(fault)
acc_after = self.eval_fn(faulted)
return FaultResult(
fault_type=fault.fault_type,
fault_rate=fault.rate,
layer_index=fault.layer_index,
accuracy_before=self.baseline_accuracy,
accuracy_after=acc_after,
degradation=self.baseline_accuracy - acc_after,
)
def sweep(
self,
fault_type: FaultType,
rates: list[float] | None = None,
per_layer: bool = False,
) -> ResilienceReport:
"""Sweep fault rate and optionally per-layer.
Parameters
----------
fault_type : FaultType
rates : list of float
Fault rates to test.
per_layer : bool
If True, test each layer independently.
"""
if rates is None: # pragma: no cover
rates = [0.01, 0.05, 0.1, 0.2, 0.5]
report = ResilienceReport()
if per_layer:
for layer_idx in range(len(self.weights)):
for rate in rates:
fault = FaultModel(fault_type=fault_type, rate=rate, layer_index=layer_idx)
report.results.append(self.run_single(fault))
else:
for rate in rates:
fault = FaultModel(fault_type=fault_type, rate=rate)
report.results.append(self.run_single(fault))
return report
def full_audit(self) -> ResilienceReport:
"""Run all fault types at standard rates, per-layer."""
report = ResilienceReport()
rates = [0.01, 0.05, 0.1, 0.2]
for ft in FaultType:
for layer_idx in range(len(self.weights)):
for rate in rates:
fault = FaultModel(fault_type=ft, rate=rate, layer_index=layer_idx)
report.results.append(self.run_single(fault))
return report
|
inject_fault(fault)
Apply a fault model to weights, return faulted copies.
Source code in src/sc_neurocore/resilience/fault_suite.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146 | def inject_fault(self, fault: FaultModel) -> list[np.ndarray]:
"""Apply a fault model to weights, return faulted copies."""
rng = np.random.RandomState(fault.seed)
faulted = [w.copy() for w in self.weights]
layers = [fault.layer_index] if fault.layer_index is not None else list(range(len(faulted)))
for i in layers:
w = faulted[i]
mask = rng.random(w.shape) < fault.rate
if fault.fault_type == FaultType.STUCK_AT_ZERO:
w[mask] = 0.0
elif fault.fault_type == FaultType.STUCK_AT_ONE:
w[mask] = 1.0
elif fault.fault_type == FaultType.WEIGHT_BIT_FLIP:
# Flip sign of affected weights
w[mask] = -w[mask]
elif fault.fault_type == FaultType.DEAD_SYNAPSE:
w[mask] = 0.0
elif fault.fault_type == FaultType.NOISY_MEMBRANE:
noise = rng.randn(*w.shape) * fault.rate * np.std(w)
w += noise * mask
elif fault.fault_type == FaultType.BITSTREAM_BIAS:
# SC-specific: shift probabilities toward 0.5
w[mask] = w[mask] * (1 - fault.rate) + 0.5 * fault.rate
faulted[i] = w
return faulted
|
run_single(fault)
Run one fault injection experiment.
Source code in src/sc_neurocore/resilience/fault_suite.py
148
149
150
151
152
153
154
155
156
157
158
159 | def run_single(self, fault: FaultModel) -> FaultResult:
"""Run one fault injection experiment."""
faulted = self.inject_fault(fault)
acc_after = self.eval_fn(faulted)
return FaultResult(
fault_type=fault.fault_type,
fault_rate=fault.rate,
layer_index=fault.layer_index,
accuracy_before=self.baseline_accuracy,
accuracy_after=acc_after,
degradation=self.baseline_accuracy - acc_after,
)
|
sweep(fault_type, rates=None, per_layer=False)
Sweep fault rate and optionally per-layer.
Parameters
fault_type : FaultType
rates : list of float
Fault rates to test.
per_layer : bool
If True, test each layer independently.
Source code in src/sc_neurocore/resilience/fault_suite.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | def sweep(
self,
fault_type: FaultType,
rates: list[float] | None = None,
per_layer: bool = False,
) -> ResilienceReport:
"""Sweep fault rate and optionally per-layer.
Parameters
----------
fault_type : FaultType
rates : list of float
Fault rates to test.
per_layer : bool
If True, test each layer independently.
"""
if rates is None: # pragma: no cover
rates = [0.01, 0.05, 0.1, 0.2, 0.5]
report = ResilienceReport()
if per_layer:
for layer_idx in range(len(self.weights)):
for rate in rates:
fault = FaultModel(fault_type=fault_type, rate=rate, layer_index=layer_idx)
report.results.append(self.run_single(fault))
else:
for rate in rates:
fault = FaultModel(fault_type=fault_type, rate=rate)
report.results.append(self.run_single(fault))
return report
|
full_audit()
Run all fault types at standard rates, per-layer.
Source code in src/sc_neurocore/resilience/fault_suite.py
194
195
196
197
198
199
200
201
202
203 | def full_audit(self) -> ResilienceReport:
"""Run all fault types at standard rates, per-layer."""
report = ResilienceReport()
rates = [0.01, 0.05, 0.1, 0.2]
for ft in FaultType:
for layer_idx in range(len(self.weights)):
for rate in rates:
fault = FaultModel(fault_type=ft, rate=rate, layer_index=layer_idx)
report.results.append(self.run_single(fault))
return report
|
FaultModel
dataclass
One fault injection configuration.
Source code in src/sc_neurocore/resilience/fault_suite.py
| @dataclass
class FaultModel:
"""One fault injection configuration."""
fault_type: FaultType
rate: float # fraction of affected elements (0.0-1.0)
layer_index: int | None = None # None = all layers
seed: int = 42
|
ResilienceReport
dataclass
Full fault resilience report.
Source code in src/sc_neurocore/resilience/fault_suite.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 | @dataclass
class ResilienceReport:
"""Full fault resilience report."""
results: list[FaultResult] = field(default_factory=list)
def degradation_curve(self, fault_type: FaultType) -> list[tuple[float, float]]:
"""Get (fault_rate, degradation) pairs for one fault type."""
points = [(r.fault_rate, r.degradation) for r in self.results if r.fault_type == fault_type]
points.sort(key=lambda x: x[0])
return points
def most_vulnerable_layer(self) -> int | None:
"""Return the layer index with highest average degradation."""
layer_deg: dict[int, list[float]] = {}
for r in self.results:
if r.layer_index is not None:
layer_deg.setdefault(r.layer_index, []).append(r.degradation)
if not layer_deg: # pragma: no cover
return None
return max(layer_deg, key=lambda k: np.mean(layer_deg[k]))
def summary(self) -> str:
lines = [f"Fault Resilience Report: {len(self.results)} experiments"]
by_type: dict[str, list[FaultResult]] = {}
for r in self.results:
by_type.setdefault(r.fault_type.value, []).append(r)
for ft, results in by_type.items():
mean_deg = np.mean([r.degradation for r in results])
max_deg = max(r.degradation for r in results)
lines.append(f" {ft}: mean_deg={mean_deg:.3f}, max_deg={max_deg:.3f}")
mvl = self.most_vulnerable_layer()
if mvl is not None:
lines.append(f" Most vulnerable layer: {mvl}")
return "\n".join(lines)
|
degradation_curve(fault_type)
Get (fault_rate, degradation) pairs for one fault type.
Source code in src/sc_neurocore/resilience/fault_suite.py
| def degradation_curve(self, fault_type: FaultType) -> list[tuple[float, float]]:
"""Get (fault_rate, degradation) pairs for one fault type."""
points = [(r.fault_rate, r.degradation) for r in self.results if r.fault_type == fault_type]
points.sort(key=lambda x: x[0])
return points
|
most_vulnerable_layer()
Return the layer index with highest average degradation.
Source code in src/sc_neurocore/resilience/fault_suite.py
70
71
72
73
74
75
76
77
78 | def most_vulnerable_layer(self) -> int | None:
"""Return the layer index with highest average degradation."""
layer_deg: dict[int, list[float]] = {}
for r in self.results:
if r.layer_index is not None:
layer_deg.setdefault(r.layer_index, []).append(r.degradation)
if not layer_deg: # pragma: no cover
return None
return max(layer_deg, key=lambda k: np.mean(layer_deg[k]))
|