Extrapolation¶
propaq's noise and truncation models are naturally compatible with error mitigation techniques such as zero noise extrapolation. One can also sweep different cutoffs and extrapolate to zero cutoff, i.e. zero-cutoff extrapolation.
Zero-noise extrapolation¶
ZeroNoiseExtrapolator sweeps the
damping rate of a NoiseModel and
extrapolates to \(\gamma \to 0\).
import numpy as np
from propaq.extrapolators import ZeroNoiseExtrapolator
from propaq.propagators import PauliPropagator
def linear(gamma, a, b):
return a + b * gamma
zne = ZeroNoiseExtrapolator(
fitting_fn=linear,
noise_values=[0.01, 0.02, 0.03, 0.04, 0.05],
)
result = zne.run(PauliPropagator(truncation=..., noise=...), observable, circuit, initial_state=0)
print("zero-noise estimate:", result.zero_noise_value)
print("sweep:", list(zip(result.noise_values, result.expectation_values)))
print("fit params:", result.fit_params)
fitting_fn is passed straight to scipy.optimize.curve_fit, so its first
argument is the noise value and the rest are fit parameters. Extra keyword
arguments to run (such as p0=) are forwarded to curve_fit.
The propagator's existing noise model is replaced for the duration of the sweep and restored afterwards, including if the sweep raises.
A user can also sweep a custom noise model by subclassing
ZeroNoiseExtrapolator and overriding
build_noise, which
builds the model instance for a given sweep value (default:
UniformNoiseModel(value)):
import math
from propaq.noise import GateNoiseModel
class DephasingNoise(GateNoiseModel):
"""Per-qubit dephasing noise"""
_X_MASK = 0x5555555555555555 # the low bit of every interleaved (x, z) pair
def __init__(self, gamma: float) -> None:
self.gamma = gamma
def damping_factor_term(self, basis_kind, words, n_units, weight):
x_count = sum(bin(w & self._X_MASK).count("1") for w in words)
return math.exp(-self.gamma * x_count)
class DephasingExtrapolator(ZeroNoiseExtrapolator):
def build_noise(self, value):
return DephasingNoise(gamma=value)
zne_dephasing = DephasingExtrapolator(
fitting_fn=linear,
noise_values=[0.01, 0.02, 0.03, 0.04, 0.05],
)
result = zne_dephasing.run(PauliPropagator(), observable, circuit, initial_state=0)
Note that only one parameter can be swept at a time, so if the model depends on multiple parameters, the others must be fixed.
The result is a ZNEResult, which carries the
extrapolated value along with the raw sweep, the fitted parameters and their
covariance.
Zero-cutoff extrapolation¶
The truncation analogue sweeps a cutoff already present in the propagator's truncation pipeline, and extrapolates to zero cutoff. There are two concrete extrapolators, one per truncator kind:
| Extrapolator | Sweeps |
|---|---|
WeightCutoffExtrapolator |
the weight of a WeightTruncator |
CoefficientCutoffExtrapolator |
the coefficient of a CoefficientTruncator |
from propaq.extrapolators import CoefficientCutoffExtrapolator
from propaq.propagators import PauliPropagator
from propaq.truncation import CoefficientTruncator, WeightTruncator
prop = PauliPropagator(
truncation=[WeightTruncator(weight=12), CoefficientTruncator(coefficient=1e-4)]
)
zce = CoefficientCutoffExtrapolator(
fitting_fn=linear,
cutoff_values=[1e-3, 5e-4, 2e-4, 1e-4],
)
result = zce.run(prop, observable, circuit, initial_state=0)
print("zero-cutoff estimate:", result.zero_cutoff_value)
ZeroCutoffExtrapolator and implementing its two
abstract methods: truncator_cls (the
truncator class to match against, used to locate the target truncator in the propagator's
pipeline) and build_truncator
(construct a fresh truncator carrying a given cutoff), mirroring
build_noise on the noise side:
from propaq.extrapolators import ZeroCutoffExtrapolator
from propaq.truncation import WeightTruncator
class LightConeWeightCutoffExtrapolator(ZeroCutoffExtrapolator):
"""Light-cone-like truncation, where we only keep terms with weight <= 2 * depth + 1"""
def truncator_cls(self):
return WeightTruncator
def build_truncator(self, depth):
return WeightTruncator(2 * int(depth) + 1 if depth is not None else None)
extrapolator = LightConeWeightCutoffExtrapolator(
fitting_fn=linear,
cutoff_values=[1, 2, 3, 4],
)
result = extrapolator.run(
PauliPropagator(truncation=WeightTruncator(weight=16)), observable, circuit, initial_state=0
)
truncator_cls. Since we have a composable truncation pipeline,
we need to identify which truncator to sweep. This allows one to hold other truncators fixed while sweeping a single one.
The result is a ZCEResult, with the same
fields as ZNEResult but keyed on cutoff_values / zero_cutoff_value.