Stratified Self-Normalised Sampling Tutorial 07 of 10
The running example: a 3x3 damping patch
Tutorial 06 built a sampler for non-Pauli noise. It runs a channel a stabilizer simulator cannot represent, like amplitude damping, by rewriting it as a signed sum of branches the simulator can run. For AMPLITUDE_DAMP(0.05) those branches are
To take one shot, the location draws a branch with probability
This
Now put nine of them in a circuit: a distance-3 rotated surface-code patch, one round, one AMPLITUDE_DAMP(0.05) on each of the nine data qubits.
from qliff.qec.codes import rotated_surface_code
circuit = rotated_surface_code(3, 3, 1, 0.05, channel="AMPLITUDE_DAMP")Each of the nine locations multiplies the weight by its
Every trajectory carries the same magnitude
Draw three thousand weighted trajectories and look at the weights themselves, not their average:
import numpy as np
from qliff.qec.sampler import WeightedDetectorSampler
_dets, _obs, weights = WeightedDetectorSampler(circuit).sample(3000, seed=7)
print("distinct |w|:", np.unique(np.round(np.abs(weights), 9)))
print("signs:", int((weights > 0).sum()), "plus /", int((weights < 0).sum()), "minus")
# -> distinct |w|: [1.24535216]
# -> signs: 2699 plus / 301 minusThree thousand shots, one distinct magnitude. Every trajectory came back carrying
- weight = +Γ
- weight = -Γ
That is not a coincidence, it is the sampling rule. At every noise location the shot's weight multiplies by
So the logical error rate comes out of the flat logical_error_rate as the mean of
And
- noise locations A = d2 × d
- flat shot-budget factor Γ2
| distance d | locations A | Γ | Γ2 |
|---|---|---|---|
| 3 | 27 | 1.93 | 3.7 |
| 5 | 125 | 21.06 | 4.4 x 102 |
| 7 | 343 | 4282 | 1.8 x 107 |
A full circuit-level model puts a noise location after every gate rather than once per data qubit per round, which pushes the same numbers much harder: the paper this estimator comes from (arXiv:2512.07304, Myers, Teo, Mishra, Chai and Hui Khoon Ng at CQT/NUS) reports
The Gamma wall
Legend:
- purple curve =
vs noise locations (log y) - red dot = selected
Pauli shots= binomial budget at a 10% barflat shots=Pauli shotstimes
Sort the shots by how many locations faulted
The move is to split the trajectories into strata by their fault count
where
The point is that
Choosing a branch at each location is then choosing one term of the product
That is stratum_masses in qliff/noise/sampler.py, one convolution per location. For the running example,
| stratum k | mass m_k | sampled P(k) | mean sign |
|---|---|---|---|
| 0 | +0.707874 | 0.568412 | 1.000000 |
| 1 | +0.249318 | 0.331392 | 0.604114 |
| 2 | +0.039027 | 0.085869 | 0.364953 |
| 3 | +0.003564 | 0.012979 | 0.220473 |
Two distributions, not one, and they differ.
The masses are signed in general
For amplitude damping both
Where the answer lives
Legend:
- purple bar =
, the signed mass of stratum (red if negative) - grey bar =
, what the sampler draws from mean sign=
Self-normalising one stratum
Now sample inside a stratum. Fix fault_set), and at each of them draw a fault branch with probability pick_branch). Write
using
with
The common factor DetectorSampler.sample_strata hands back signs and stratum labels instead of weights. It takes the shots per stratum explicitly, and neyman_counts spreads a budget over the strata (proportional to
from qliff.noise.sampler import neyman_counts
from qliff.qec.sampler import DetectorSampler
sampler = DetectorSampler(circuit)
_locs, _phis, pk, _mass = sampler.strata
counts = neyman_counts({k: p for k, p in enumerate(pk) if p > 0.0}, None, 3000)
_dets, _obs, signs, strata = sampler.sample_strata(counts, seed=7)
print(sorted(set(signs.tolist())))
# -> [-1.0, 1.0]That denominator is a sampled sum of
Because _loo_beta) so it never scores the shot it was fit on. At logical_error_rate(..., stratify=True) in qliff/qec/threshold.py.
Two consequences:
never appears in the answer. The flat estimator averages numbers of size . This one averages signs and multiplies by masses that sum to 1. At that is the difference between an estimator that returns a number and one that does not. - The
stratum is free. Every trajectory is the same identity-everywhere run, so is evaluated rather than estimated. On the running example that one configuration settles 70.8% of the answer. The pilot pass still seats by , but its zero spread sends the Neyman remainder to the strata that vary.
- flat importance sampling
- stratified, control-variate
| flat | stratified | |
|---|---|---|
| per-shot payload | signed weight ±Γ | sign ±1 + stratum k |
| estimator | mean(w · wrong) | Σk ΓP(k) · mean(yk) |
| fault-count weights | sampled | computed |
| k = 0 stratum | ~57% of shots | pilot share only, exact |
| residual noise source | Γ across all A locations | sign cancellation across k faults |
What survives: the signs
Cancelling
the ratio of the location's signed fault weight to its absolute one. With identical locations the mean sign of a stratum-
and it barely moves with
The measured signs match:
import numpy as np
for k in (0, 1, 2, 3):
sel = strata == k
print(f"k={k} n {int(sel.sum()):4d} sum(sign) {signs[sel].sum():+6.0f} mean {signs[sel].mean():.4f}")
# -> k=0 n 1695 sum(sign) +1695 mean 1.0000
# -> k=1 n 989 sum(sign) +619 mean 0.6259
# -> k=2 n 257 sum(sign) +81 mean 0.3152
# -> k=3 n 40 sum(sign) +2 mean 0.0500At
Notice the exponent.
Sign cancellation inside one stratum
Legend:
- green
+= trajectory sign - red
-= trajectory sign - filled cell = decoder wrong
sum(sign)= net sign of the stratum (washes out as r^k)
The Pauli sanity check
Everything above collapses to something ordinary when the noise is Pauli. Every branch has
from qliff import Circuit
from qliff.noise.sampler import build_strata
pauli = Circuit(5)
for q in range(5):
pauli.append("X_ERROR", [q], 0.08)
_locs, _phis, pk, mass = build_strata(pauli)
print([round(m, 8) for m in mass[:4]])
print([round(x, 8) for x in pk[:4]])
print("identical:", all(abs(m - x) < 1e-12 for m, x in zip(mass, pk)))
# -> [0.65908152, 0.28655718, 0.04983603, 0.00433357]
# -> [0.65908152, 0.28655718, 0.04983603, 0.00433357]
# -> identical: TrueWith every sign
Worked example: stratum k = 1 of the 3x3 damping patch
Nine locations, all identical,
- Mass. One location faults, the other eight do not, and there are nine ways to choose which:
. - Sampling odds. The sampler's per-location fault probability is
, so . It draws stratum 1 more often than its mass deserves, because it is drawing from , not . - Weight magnitude. Every stratum-1 trajectory carries
, times its sign. The magnitude is identical for all 989 of them. - Mean sign. The one faulty location picks
with probability and with probability , so the mean sign is . Times that is : the identity closes. - The estimate. The sampler drew 989 trajectories here, 804 with sign
and 185 with sign , so against an expectation of . is over that 459, and stratum 1 contributes to the logical error rate.
Result: the 0.249318 of the answer that lives at
What it costs
The control variate does not need a crossover argument to be safe. At
Measured on the running patch, with a prep round so the damping has something to act on, decoded by the coherent decoder, spread of the estimate over eight seeds:
- flat importance sampling
- stratified, control-variate
| rounds | A | Γ | flat spread | stratified spread |
|---|---|---|---|---|
| 2 | 18 | 1.55 | 0.0066 | 0.0030 |
| 4 | 36 | 2.41 | 0.0208 | 0.0117 |
| 6 | 54 | 3.73 | 0.0188 | 0.0224 |
Read the table as the method's shape, not a verdict. Three things set how far ahead it gets:
- How badly the signs cancel. The per-fault discount is
. Amplitude damping's is steep, so within a handful of faults a stratum's signs have washed out. A channel whose fault branches are mostly positive has near 1 and barely pays. - Where the shots go.
logical_error_rate(..., stratify=True)spends a quarter of the shots on a pilot proportional to, measures each stratum's spread on the first decoder, then spends the rest by Neyman, (both through neyman_counts). Proportional allocation alone only ties flat sampling. Thefactor is the win, moving shots off the strata that never vary and onto the ones that carry the failures. - How large
is. At flat is barely inconvenienced, so the margin is modest. At flat cannot produce a number at all, whatever its asymptotics say, and a noisy estimate beats none. Surviving the high- end is the whole reason the method exists.
A stratum the budget could not seat is missing, not conservative
neyman_counts seats two shots per stratum, heaviest first, only while the budget lasts. When the strata no shot reached hold more than 0.1% of the absolute quasiprobability mass, logical_error_rate(..., stratify=True) raises a RuntimeWarning naming the fraction: that is the part of the answer the run did not look at, a bias rather than noise. On the running example every stratum is seated. Raise shots when it fires.
Implementation
The tabs rebuild the page in code, all on the same damping patch.
Pseudocode. The two estimators side by side, so the structural difference is visible at a glance: what a shot carries, and how the strata recombine.
Std Python. Four locations in NumPy, small enough to brute-force. It builds the masses by convolution, enumerates all
Qliff. The same objects from the library: build_strata for the masses, WeightedDetectorSampler for the flat weights, DetectorSampler.sample_strata with neyman_counts for the signs and strata, and logical_error_rate(..., stratify=True) for the end-to-end estimate.
# --- flat importance sampling (tutorial 06) --------------------------------
for shot in 1..N:
weight = 1
for location in circuit:
draw branch k with probability |q_k| / gamma
weight = weight * sign(q_k) * gamma # |weight| = Gamma, always
emit (detection events, weight)
ler = mean over shots of weight * decoder_is_wrong # a mean of +/- Gamma
# --- stratified, control-variate -------------------------------------------
a[i] = identity-branch weight of location i # a[i] + b[i] = 1
b[i] = summed fault weight of location i
mass = coefficients of prod_i (a[i] + b[i] * x) # computed, no sampling
phi[i] = gfault[i] / gamma[i] # sampling fault odds
P = poisson_binomial(phi)
Gamma = prod_i gamma[i]
ler = 0
for k with P(k) > 0:
n_k = shots for stratum k (pilot ~ P(k), then Neyman: n_k ~ P(k)*sqrt(V_k))
coef = Gamma * P(k) # stratum weight, computed
r_k = mass[k] / coef # known mean sign E[sign]
sv = []
wv = []
for shot in 1..n_k:
S = draw k faulty locations from P
sign = product over i in S of sign(chosen branch weight)
sv += [sign]
wv += [sign * decoder_is_wrong]
beta = leave_one_out Cov(wv, sv) / Var(sv) # variance-optimal, no bias
y = [w - beta * (s - r_k) for s, w in zip(sv, wv)]
ler = ler + coef * mean(y) # unbiased, no denominatorimport itertools
import numpy as np
p = 0.05
root = np.sqrt(1.0 - p)
q = np.array([(1.0 - p + root) / 2.0, (1.0 - p - root) / 2.0, p]) # I, Z, R
A = 4
# 1. Every location splits into an identity weight a and a summed fault weight b.
# Trace preservation forces a + b = 1; the sampler instead sees the ABSOLUTE
# sums, gfault and gamma, and those do not add up to 1.
a = q[0]
b = q[1:].sum()
gfault = np.abs(q[1:]).sum()
gamma = np.abs(q).sum()
print("a", round(a, 6), " b", round(b, 6), " a+b", round(a + b, 6))
print("gfault", round(gfault, 6), " gamma", round(gamma, 6))
print("phi", round(gfault / gamma, 6), " r = b/gfault", round(b / gfault, 6))
# -> a 0.96234 b 0.03766 a+b 1.0
# -> gfault 0.06234 gamma 1.024679
# -> phi 0.060838 r = b/gfault 0.604114
# 2. Stratum masses = the coefficients of x^k in prod_i (a + b x): analytic, no sampling.
mass = np.array([1.0])
for _ in range(A):
mass = np.convolve(mass, [a, b])
print("mass", np.round(mass, 6), " sum", round(mass.sum(), 12))
# -> mass [8.57657e-01 1.34254e-01 7.88100e-03 2.06000e-04 2.00000e-06] sum 1.0
# 3. Brute force over every branch assignment of the 4 locations. Each trajectory
# has a true signed weight (the product of the q's) and a fault count k. Say the
# shot is a logical error when two or more locations fired the reset branch.
true_ler = 0.0
per_k = np.zeros(A + 1)
for pick in itertools.product(range(3), repeat=A):
weight = float(np.prod(q[list(pick)]))
k = sum(1 for i in pick if i != 0)
wrong = 1.0 if sum(1 for i in pick if i == 2) >= 2 else 0.0
true_ler += weight * wrong
per_k[k] += weight * wrong
print("true LER", round(true_ler, 8))
# -> true LER 0.01401875
# 4. The identity the estimator rests on: LER = sum_k mass[k] * F_k. Note that the
# conditional rates are ratios of SIGNED masses, so they need not sit in [0, 1];
# only their mass-weighted sum has to be a probability.
f_k = per_k / mass
print("F_k ", np.round(f_k, 6))
print("sum_k mass[k]*F_k", round(float((mass * f_k).sum()), 8))
# -> F_k [0. 0. 1.762677 0.607564 1.175289]
# -> sum_k mass[k]*F_k 0.01401875
# 5. Flat sampling gives every trajectory the SAME magnitude, |w| = Gamma, so only
# the sign moves. Within stratum k the magnitude is Gamma*P(k) and the mean sign
# is r^k = mass/(Gamma*P(k)), the quantity the estimator turns into F_k.
phi = gfault / gamma
pk = np.array([1.0])
for _ in range(A):
pk = np.convolve(pk, [1 - phi, phi])
print("Gamma", round(gamma**A, 6))
print("mean sign per stratum", np.round(mass / (gamma**A * pk), 6))
print("r^k ", np.round((b / gfault) ** np.arange(A + 1), 6))
# -> Gamma 1.102433
# -> mean sign per stratum [1. 0.604114 0.364953 0.220473 0.133191]
# -> r^k [1. 0.604114 0.364953 0.220473 0.133191]import numpy as np
from qliff.noise.sampler import build_strata, neyman_counts
from qliff.qec.codes import rotated_surface_code
from qliff.qec.sampler import DetectorSampler, WeightedDetectorSampler
from qliff.qec.threshold import logical_error_rate
circuit = rotated_surface_code(3, 3, 1, 0.05, channel="AMPLITUDE_DAMP")
# 1. Flat importance sampling: one magnitude, two signs.
_dets, _obs, weights = WeightedDetectorSampler(circuit).sample(3000, seed=7)
print("distinct |w|:", np.unique(np.round(np.abs(weights), 9)))
print("signs:", int((weights > 0).sum()), "plus /", int((weights < 0).sum()), "minus")
# -> distinct |w|: [1.24535216]
# -> signs: 2699 plus / 301 minus
# 2. The strata, computed rather than sampled.
locs, _phis, pk, mass = build_strata(circuit)
big = float(np.prod([g for _branches, g in locs]))
print("locations", len(locs), " Gamma", round(big, 6), " sum(mass)", round(sum(mass), 12))
for k in range(4):
print(f"k={k} mass {mass[k]:+.6f} P(k) {pk[k]:.6f} mean sign {mass[k] / (big * pk[k]):.6f}")
# -> locations 9 Gamma 1.245352 sum(mass) 1.0
# -> k=0 mass +0.707874 P(k) 0.568412 mean sign 1.000000
# -> k=1 mass +0.249318 P(k) 0.331392 mean sign 0.604114
# -> k=2 mass +0.039027 P(k) 0.085869 mean sign 0.364953
# -> k=3 mass +0.003564 P(k) 0.012979 mean sign 0.220473
# 3. The stratified sampler returns SIGNS and stratum labels, never a weight.
counts = neyman_counts({k: p for k, p in enumerate(pk) if p > 0.0}, None, 3000)
_d, _o, signs, strata = DetectorSampler(circuit).sample_strata(counts, seed=7)
print("trajectories", len(signs))
for k in np.unique(strata)[:4]:
sel = strata == k
print(f"k={k} n {int(sel.sum()):4d} sum(sign) {signs[sel].sum():+6.0f} mean {signs[sel].mean():.4f}")
# -> trajectories 2996
# -> k=0 n 1695 sum(sign) +1695 mean 1.0000
# -> k=1 n 989 sum(sign) +619 mean 0.6259
# -> k=2 n 257 sum(sign) +81 mean 0.3152
# -> k=3 n 40 sum(sign) +2 mean 0.0500
# 4. End to end. `prep` projects into the code space first, so the damping has an
# excited population to act on; stratify picks the estimator.
memory = rotated_surface_code(
3, 3, 2, 0.05, channel="AMPLITUDE_DAMP", prep=True
)
for stratify in (False, True):
ler, stderr = logical_error_rate(memory, "coherent", shots=1200, seed=0, stratify=stratify)
print(f"stratify={str(stratify):5s} LER = {ler:.5f} +/- {stderr:.5f}")
# -> stratify=False LER = 0.00646 +/- 0.00599
# -> stratify=True LER = 0.02497 +/- 0.00560One circuit, two ways to spend a shot. Flat importance sampling books