Skip to content

Digital Twin — FPGA Mismatch Simulation

Simulate FPGA hardware imperfections during SNN training. Training through these imperfections produces networks that tolerate hardware mismatch at deployment time.

Hardware Imperfection Model

Real FPGA implementations suffer from:

Imperfection Source Typical Magnitude
Q8.8 quantization Fixed-point arithmetic Step = 1/256
Weight perturbation Process variation in LUT/BRAM CV ≈ 2%
Threshold mismatch Per-neuron comparator variation CV ≈ 5%
Clock jitter PLL/oscillator noise ±1% period
Routing delay Path-dependent timing skew Clipped to ±10%

Calibrated against published data: ~20% coefficient of variation for analog mixed-signal neuromorphic processors. Digital FPGAs have lower variation (~1-5%) but Q8.8 quantization is the dominant error source.

Components

  • FPGAMismatchModel — Wraps weight matrices and neuron parameters with hardware imperfections.
Parameter Default Meaning
quantization_bits 16 Fixed-point width (16 = Q8.8)
weight_cv 0.02 Weight perturbation coefficient of variation
threshold_cv 0.05 Per-neuron threshold variation
clock_jitter_pct 0.01 Clock period variation
seed 42 RNG seed for reproducibility

Methods:

  • quantize(values) — Apply Q-format quantization
  • perturb_weights(weights) — Add process variation + quantize
  • perturb_thresholds(thresholds) — Add per-neuron mismatch + quantize
  • jitter_timing(n_steps) — Generate per-step timing variation (clipped to [0.9, 1.1])
  • apply_to_network_weights(weights) — Apply all imperfections to a list of weight matrices
  • mismatch_report(weights) — Report expected error statistics

Usage

from sc_neurocore.digital_twin import FPGAMismatchModel
import numpy as np

model = FPGAMismatchModel(quantization_bits=16, weight_cv=0.02)

# Apply to trained weights
weights = [np.random.randn(64, 32) * 0.1, np.random.randn(10, 64) * 0.1]
faulted = model.apply_to_network_weights(weights)

# Get error report
report = model.mismatch_report(weights)
print(f"MAE: {report['mean_absolute_error']:.6f}")
print(f"Max error: {report['max_absolute_error']:.6f}")

See Tutorial 48: Digital Twin.

sc_neurocore.digital_twin

Simulate FPGA imperfections during training for deployment confidence.

FPGAMismatchModel dataclass

Wraps weight matrices and neuron parameters with FPGA imperfections.

Parameters

quantization_bits : int Fixed-point bit width (default 16 for Q8.8). weight_cv : float Coefficient of variation for weight perturbation (default 0.02 = 2%). threshold_cv : float Per-neuron threshold variation (default 0.05 = 5%). clock_jitter_pct : float Clock period variation (default 0.01 = 1%). seed : int Random seed for reproducibility.

Source code in src/sc_neurocore/digital_twin/mismatch.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
93
94
95
96
97
98
@dataclass
class FPGAMismatchModel:
    """Wraps weight matrices and neuron parameters with FPGA imperfections.

    Parameters
    ----------
    quantization_bits : int
        Fixed-point bit width (default 16 for Q8.8).
    weight_cv : float
        Coefficient of variation for weight perturbation (default 0.02 = 2%).
    threshold_cv : float
        Per-neuron threshold variation (default 0.05 = 5%).
    clock_jitter_pct : float
        Clock period variation (default 0.01 = 1%).
    seed : int
        Random seed for reproducibility.
    """

    quantization_bits: int = 16
    weight_cv: float = 0.02
    threshold_cv: float = 0.05
    clock_jitter_pct: float = 0.01
    seed: int = 42

    def __post_init__(self):
        self._rng = np.random.RandomState(self.seed)

    def quantize(self, values: np.ndarray) -> np.ndarray:
        """Apply Q-format quantization noise."""
        fraction = self.quantization_bits // 2
        scale = 1 << fraction
        quantized = np.round(values * scale) / scale
        return quantized

    def perturb_weights(self, weights: np.ndarray) -> np.ndarray:
        """Add process variation noise to weights."""
        noise = self._rng.normal(0, self.weight_cv, weights.shape)
        return self.quantize(weights * (1.0 + noise))

    def perturb_thresholds(self, thresholds: np.ndarray) -> np.ndarray:
        """Add per-neuron threshold mismatch."""
        noise = self._rng.normal(0, self.threshold_cv, thresholds.shape)
        return self.quantize(thresholds * (1.0 + noise))

    def jitter_timing(self, n_steps: int) -> np.ndarray:
        """Generate clock jitter: per-step timing variation."""
        jitter = self._rng.normal(1.0, self.clock_jitter_pct, n_steps)
        return np.clip(jitter, 0.9, 1.1)

    def apply_to_network_weights(self, weights: list[np.ndarray]) -> list[np.ndarray]:
        """Apply all hardware imperfections to a list of weight matrices."""
        return [self.perturb_weights(w) for w in weights]

    def mismatch_report(self, weights: list[np.ndarray]) -> dict:
        """Report expected mismatch statistics for given weights."""
        perturbed = self.apply_to_network_weights(weights)
        total_params = sum(w.size for w in weights)
        total_error = sum(np.abs(w - p).sum() for w, p in zip(weights, perturbed))
        max_error = max(np.abs(w - p).max() for w, p in zip(weights, perturbed))
        return {
            "total_parameters": total_params,
            "mean_absolute_error": float(total_error / max(total_params, 1)),
            "max_absolute_error": float(max_error),
            "weight_cv": self.weight_cv,
            "threshold_cv": self.threshold_cv,
            "quantization_bits": self.quantization_bits,
        }

quantize(values)

Apply Q-format quantization noise.

Source code in src/sc_neurocore/digital_twin/mismatch.py
59
60
61
62
63
64
def quantize(self, values: np.ndarray) -> np.ndarray:
    """Apply Q-format quantization noise."""
    fraction = self.quantization_bits // 2
    scale = 1 << fraction
    quantized = np.round(values * scale) / scale
    return quantized

perturb_weights(weights)

Add process variation noise to weights.

Source code in src/sc_neurocore/digital_twin/mismatch.py
66
67
68
69
def perturb_weights(self, weights: np.ndarray) -> np.ndarray:
    """Add process variation noise to weights."""
    noise = self._rng.normal(0, self.weight_cv, weights.shape)
    return self.quantize(weights * (1.0 + noise))

perturb_thresholds(thresholds)

Add per-neuron threshold mismatch.

Source code in src/sc_neurocore/digital_twin/mismatch.py
71
72
73
74
def perturb_thresholds(self, thresholds: np.ndarray) -> np.ndarray:
    """Add per-neuron threshold mismatch."""
    noise = self._rng.normal(0, self.threshold_cv, thresholds.shape)
    return self.quantize(thresholds * (1.0 + noise))

jitter_timing(n_steps)

Generate clock jitter: per-step timing variation.

Source code in src/sc_neurocore/digital_twin/mismatch.py
76
77
78
79
def jitter_timing(self, n_steps: int) -> np.ndarray:
    """Generate clock jitter: per-step timing variation."""
    jitter = self._rng.normal(1.0, self.clock_jitter_pct, n_steps)
    return np.clip(jitter, 0.9, 1.1)

apply_to_network_weights(weights)

Apply all hardware imperfections to a list of weight matrices.

Source code in src/sc_neurocore/digital_twin/mismatch.py
81
82
83
def apply_to_network_weights(self, weights: list[np.ndarray]) -> list[np.ndarray]:
    """Apply all hardware imperfections to a list of weight matrices."""
    return [self.perturb_weights(w) for w in weights]

mismatch_report(weights)

Report expected mismatch statistics for given weights.

Source code in src/sc_neurocore/digital_twin/mismatch.py
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def mismatch_report(self, weights: list[np.ndarray]) -> dict:
    """Report expected mismatch statistics for given weights."""
    perturbed = self.apply_to_network_weights(weights)
    total_params = sum(w.size for w in weights)
    total_error = sum(np.abs(w - p).sum() for w, p in zip(weights, perturbed))
    max_error = max(np.abs(w - p).max() for w, p in zip(weights, perturbed))
    return {
        "total_parameters": total_params,
        "mean_absolute_error": float(total_error / max(total_params, 1)),
        "max_absolute_error": float(max_error),
        "weight_cv": self.weight_cv,
        "threshold_cv": self.threshold_cv,
        "quantization_bits": self.quantization_bits,
    }