Noise models¶
A noise model damps term coefficients as the observable propagates. It can be used to model noisy devices, or drive coefficients to zero to make a simulation tractable.
from propaq.noise import UniformNoiseModel
from propaq.propagators import PauliPropagator
prop = PauliPropagator(noise=UniformNoiseModel(damping=0.001))
The model can also be swapped after construction with set_noise, and read back
off the propagator's noise property.
Uniform depolarizing noise¶
UniformNoiseModel is the built-in
depolarizing-style model: a term of weight \(w\) is scaled by
with \(\gamma\) the damping rate.
Python-defined models¶
Subclass GateNoiseModel and define the appropriate method for your model.
Weight-only noise¶
Implement damping_factor(term_weight, active_modes) -> float for a model
whose damping depends only on a term's Pauli/Majorana weight (like
UniformNoiseModel):
import math
from propaq.noise import GateNoiseModel
class StretchedExponentialNoise(GateNoiseModel):
def __init__(self, gamma: float, beta: float) -> None:
self.gamma = gamma
self.beta = beta
def damping_factor(self, term_weight: float, active_modes: int) -> float:
return math.exp(-((self.gamma * term_weight) ** self.beta))
prop = PauliPropagator(noise=StretchedExponentialNoise(0.01, 0.8))
We precompute the values for each weight and cache them, so these models are effectively zero overhead.
Key-aware noise¶
In general, a noise model will need the actual operator's string representation,
not just its weight. For these cases, implement damping_factor_term(basis_kind, words, n_units, weight) -> float.
class BoundaryQubitNoise(GateNoiseModel):
"""Damp terms acting nontrivially on qubit 0 more than the rest."""
def __init__(self) -> None:
pass
def damping_factor_term(self, basis_kind, words, n_units, weight):
touches_qubit_0 = bool(words[0] & 0b11)
gamma = 0.3 if touches_qubit_0 else 0.05
return math.exp(-gamma * weight)
prop = PauliPropagator(noise=BoundaryQubitNoise())
Key-aware noise is on the hot path
damping_factor_term is called once per live term at every noise
application boundary, with the GIL held, so every call pays GIL
acquisition and Python dispatch. We have observed that this can
cost up to 50% added runtime for a circuit.
We recommend prototyping your model in Python first, then porting it to a native plugin for production runs.
Native plugins¶
NativeNoiseModel loads a noise model from a
C, Rust or AOT-compiled Julia shared library:
from propaq.noise import NativeNoiseModel
noise = NativeNoiseModel(
path="./thermal_decay_noise.so",
config='{"gamma": 0.02, "beta": 0.8}',
)
config is a JSON string handed once to the plugin's propaq_noise_create.
See the plugin guide for the full ABI.
Interaction with truncation¶
Noise and truncation compound. As depth increases, damping pushes more and more
coefficient mass under CoefficientTruncator's cutoff, so a run with noise
enabled typically has a smaller live term count than the same run without it.
A noise model with a moderate damping rate can be used to make a simulation tractable.
If you want the noiseless answer, run a sweep of damping rates and
extrapolate to \(\gamma \to 0\). See extrapolation.