Skip to content

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
36
37
38
39
40
41
42
43
@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
64
65
66
67
68
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]))