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

Python
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
Python
 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
 99
100
101
102
@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) -> None:
        self._rng = np.random.RandomState(self.seed)

    def quantize(self, values: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
        """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[Any, Any]) -> np.ndarray[Any, Any]:
        """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[Any, Any]) -> np.ndarray[Any, Any]:
        """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[Any, Any]:
        """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[Any, Any]]
    ) -> list[np.ndarray[Any, Any]]:
        """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[Any, Any]]) -> dict[str, object]:
        """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
Python
61
62
63
64
65
66
def quantize(self, values: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
    """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
Python
68
69
70
71
def perturb_weights(self, weights: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
    """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
Python
73
74
75
76
def perturb_thresholds(self, thresholds: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
    """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
Python
78
79
80
81
def jitter_timing(self, n_steps: int) -> np.ndarray[Any, Any]:
    """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
Python
83
84
85
86
87
def apply_to_network_weights(
    self, weights: list[np.ndarray[Any, Any]]
) -> list[np.ndarray[Any, Any]]:
    """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
Python
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def mismatch_report(self, weights: list[np.ndarray[Any, Any]]) -> dict[str, object]:
    """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,
    }

Compiler-Service Sync Contract

The digital-twin replay boundary used by compiler-service planning lives in sc_neurocore.compiler_service. It records checkpoint cadence, drift budget, event channels, and replay-verification requirements before any live FPGA update package is accepted. See Compiler Service Contract.