use crate::quantum_ops::{content_hash, GrantRef, MitigationReceipt};
use ed25519_dalek::SigningKey;
pub const MIT_FRAC: u32 = 20;
pub const MIT_ONE: i64 = 1 << MIT_FRAC;
#[inline]
fn fmul(a: i64, b: i64) -> i64 {
((a as i128 * b as i128) >> MIT_FRAC) as i64
}
#[inline]
fn fdiv(p: i64, q: i64) -> i64 {
if q == 0 {
return 0;
}
(((p as i128) << MIT_FRAC) / q as i128) as i64
}
fn isqrt_i128(v: i128) -> i128 {
if v <= 0 {
return 0;
}
let mut x = v;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + v / x) / 2;
}
x
}
fn sqrt_fx_of_square(sq: i128) -> i64 {
isqrt_i128(sq) as i64
}
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn draw_pm1(st: &mut u64, e_fx: i64) -> i64 {
let p_plus = (MIT_ONE + e_fx) / 2; let r = (splitmix64(st) >> (64 - MIT_FRAC)) as i64; if r < p_plus {
1
} else {
-1
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ZneMethod {
Linear,
Richardson,
}
impl ZneMethod {
fn tag(self) -> &'static str {
match self {
ZneMethod::Linear => "zne.linear",
ZneMethod::Richardson => "zne.richardson",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZneResult {
pub method: &'static str,
pub points: Vec<(i64, i64)>,
pub raw_fx: i64,
pub mitigated_fx: i64,
pub error_fx: i64,
pub shots: u64,
}
fn fit_linear(points: &[(i64, i64)]) -> (i64, i64) {
let n = points.len() as i128;
if n == 0 {
return (0, 0);
}
let (mut sx, mut sy, mut sxy, mut sxx) = (0i128, 0i128, 0i128, 0i128);
for &(x, y) in points {
sx += x as i128;
sy += y as i128;
sxy += x as i128 * y as i128; sxx += x as i128 * x as i128; }
let denom = n * sxx - sx * sx; if denom == 0 {
return ((sy / n) as i64, 0); }
let numer = n * sxy - sx * sy; let slope = ((numer << MIT_FRAC) / denom) as i64;
let mean_x = (sx / n) as i64;
let mean_y = (sy / n) as i64;
let intercept = mean_y - fmul(slope, mean_x);
(intercept, slope)
}
fn richardson_zero(points: &[(i64, i64)]) -> i64 {
let m = points.len();
let mut acc = 0i64;
for i in 0..m {
let xi = points[i].0;
let mut w = MIT_ONE;
for (j, &(xj, _)) in points.iter().enumerate() {
if j == i {
continue;
}
let d = xj - xi;
if d == 0 {
continue; }
w = fdiv(fmul(w, xj), d);
}
acc += fmul(w, points[i].1);
}
acc
}
pub fn zne_extrapolate(points: &[(i64, i64)], method: ZneMethod, shots: u64) -> ZneResult {
let mut pts = points.to_vec();
pts.sort_by_key(|p| p.0);
let raw_fx = pts.first().map(|p| p.1).unwrap_or(0);
let (intercept, slope) = fit_linear(&pts);
let lin0 = intercept;
let (mitigated_fx, error_fx) = match method {
ZneMethod::Linear => {
let n = pts.len().max(1) as i128;
let mut ss = 0i128;
for &(x, y) in &pts {
let pred = intercept + fmul(slope, x);
let r = (y - pred) as i128;
ss += r * r; }
let rms = sqrt_fx_of_square(ss / n);
(lin0, rms)
}
ZneMethod::Richardson => {
let capped: Vec<(i64, i64)> = pts.iter().take(8).copied().collect();
let rich = richardson_zero(&capped);
(rich, (rich - lin0).abs())
}
};
ZneResult {
method: method.tag(),
points: pts,
raw_fx,
mitigated_fx,
error_fx,
shots,
}
}
impl ZneResult {
pub fn input_bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(self.points.len() * 16 + 8);
b.extend_from_slice(b"wai:zne-input\x01");
for &(x, y) in &self.points {
b.extend_from_slice(&x.to_le_bytes());
b.extend_from_slice(&y.to_le_bytes());
}
b
}
#[allow(clippy::too_many_arguments)]
pub fn seal(
&self,
signer: &SigningKey,
signer_id: impl Into<String>,
backend_id: impl Into<String>,
observable: impl Into<String>,
joules_micro: u64,
grant: GrantRef,
) -> MitigationReceipt {
MitigationReceipt::seal(
signer,
signer_id,
backend_id,
self.method,
observable,
content_hash(&self.input_bytes()),
None,
self.raw_fx,
self.mitigated_fx,
self.error_fx,
self.shots,
joules_micro,
grant,
None,
)
}
}
pub fn simulate_zne(
ideal_fx: i64,
gamma_fx: i64,
scales_fx: &[i64],
shots: u32,
seed: u64,
) -> Vec<(i64, i64)> {
scales_fx
.iter()
.map(|&lam| {
let e_true = fmul(ideal_fx, exp_neg_fx(fmul(gamma_fx, lam)));
let mut st = seed
.wrapping_mul(0x1000_0001)
.wrapping_add(lam as u64)
.wrapping_add(0xA5A5);
let mut sum = 0i64;
for _ in 0..shots {
sum += draw_pm1(&mut st, e_true);
}
let meas = ((sum as i128 * MIT_ONE as i128) / shots.max(1) as i128) as i64;
(lam, meas)
})
.collect()
}
fn exp_neg_fx(x_fx: i64) -> i64 {
if x_fx <= 0 {
return MIT_ONE;
}
let mut halvings = 0;
let mut x = x_fx;
while x > (MIT_ONE >> 2) {
x >>= 1;
halvings += 1;
}
let mut term = MIT_ONE;
let mut acc = 0i64;
let mut sign = 1i64;
for k in 0..6 {
acc += sign * term;
term = fmul(term, x) / (k + 1) as i64;
sign = -sign;
}
let mut r = acc;
for _ in 0..halvings {
r = fmul(r, r);
}
r.clamp(0, MIT_ONE)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReadoutModel {
pub qubits: Vec<[i64; 4]>,
}
impl ReadoutModel {
pub fn symmetric(n: usize, p_meas0_given1: i64, p_meas1_given0: i64) -> ReadoutModel {
let mat = [
MIT_ONE - p_meas1_given0, p_meas0_given1, p_meas1_given0, MIT_ONE - p_meas0_given1, ];
ReadoutModel {
qubits: vec![mat; n],
}
}
pub fn bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(self.qubits.len() * 32 + 8);
b.extend_from_slice(b"wai:readout-model\x01");
for q in &self.qubits {
for v in q {
b.extend_from_slice(&v.to_le_bytes());
}
}
b
}
fn inv(&self, q: usize) -> [i64; 4] {
let [a, b, c, d] = self.qubits[q];
let det = fmul(a, d) - fmul(b, c);
let id = fdiv(MIT_ONE, det);
[fmul(id, d), -fmul(id, b), -fmul(id, c), fmul(id, a)]
}
}
fn apply_qubit(v: &mut [i64], n: usize, q: usize, op: [i64; 4]) {
let bit = 1usize << q;
for x in 0..(1usize << n) {
if x & bit == 0 {
let v0 = v[x];
let v1 = v[x | bit];
v[x] = fmul(op[0], v0) + fmul(op[1], v1);
v[x | bit] = fmul(op[2], v0) + fmul(op[3], v1);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReadoutResult {
pub n: usize,
pub raw_fx: i64,
pub mitigated_fx: i64,
pub negativity_fx: i64,
pub shots: u64,
pub counts_bytes: Vec<u8>,
pub model_hash: [u8; 32],
}
pub fn readout_mitigate(
counts: &[u64],
obs: &[i64],
model: &ReadoutModel,
) -> ReadoutResult {
let n = model.qubits.len();
let dim = 1usize << n;
assert_eq!(counts.len(), dim, "counts must have 2^n entries");
assert_eq!(obs.len(), dim, "obs must have 2^n entries");
let total: u64 = counts.iter().sum::<u64>().max(1);
let mut p: Vec<i64> = counts
.iter()
.map(|&c| ((c as i128 * MIT_ONE as i128) / total as i128) as i64)
.collect();
let raw_fx = p
.iter()
.zip(obs)
.map(|(&pi, &oi)| fmul(pi, oi))
.sum::<i64>();
for q in 0..n {
apply_qubit(&mut p, n, q, model.inv(q));
}
let mitigated_fx = p.iter().zip(obs).map(|(&pi, &oi)| fmul(pi, oi)).sum::<i64>();
let negativity_fx = p.iter().filter(|&&x| x < 0).map(|&x| -x).sum::<i64>();
let mut cb = Vec::with_capacity(dim * 8 + 8);
cb.extend_from_slice(b"wai:readout-counts\x01");
for &c in counts {
cb.extend_from_slice(&c.to_le_bytes());
}
ReadoutResult {
n,
raw_fx,
mitigated_fx,
negativity_fx,
shots: total,
counts_bytes: cb,
model_hash: content_hash(&model.bytes()),
}
}
impl ReadoutResult {
pub fn seal(
&self,
signer: &SigningKey,
signer_id: impl Into<String>,
backend_id: impl Into<String>,
observable: impl Into<String>,
joules_micro: u64,
grant: GrantRef,
) -> MitigationReceipt {
MitigationReceipt::seal(
signer,
signer_id,
backend_id,
"readout.tensored",
observable,
content_hash(&self.counts_bytes),
Some(self.model_hash),
self.raw_fx,
self.mitigated_fx,
self.negativity_fx,
self.shots,
joules_micro,
grant,
None,
)
}
}
pub fn simulate_readout(
p_true_fx: &[i64],
model: &ReadoutModel,
shots: u32,
seed: u64,
) -> Vec<u64> {
let n = model.qubits.len();
let dim = 1usize << n;
let mut p = p_true_fx.to_vec();
for q in 0..n {
apply_qubit(&mut p, n, q, model.qubits[q]);
}
let mut cdf = vec![0i64; dim];
let mut run = 0i64;
for i in 0..dim {
run += p[i].max(0);
cdf[i] = run;
}
let norm = run.max(1);
let mut counts = vec![0u64; dim];
let mut st = seed.wrapping_mul(0x2545_F491).wrapping_add(1);
for _ in 0..shots {
let r = (((splitmix64(&mut st) >> (64 - MIT_FRAC)) as i128 * norm as i128) >> MIT_FRAC) as i64;
let mut idx = dim - 1;
for (i, &c) in cdf.iter().enumerate() {
if r < c {
idx = i;
break;
}
}
counts[idx] += 1;
}
counts
}
pub const PX: u8 = 0;
pub const PY: u8 = 1;
pub const PZ: u8 = 2;
pub const PI: u8 = 3;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Snapshot {
pub bases: Vec<u8>,
pub outcomes: Vec<i8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShadowResult {
pub estimate_fx: i64,
pub error_fx: i64,
pub n_snapshots: usize,
pub input: Vec<u8>,
}
fn canonical_snapshots(snaps: &[Snapshot]) -> Vec<u8> {
let mut b = Vec::with_capacity(snaps.len() * 8 + 8);
b.extend_from_slice(b"wai:shadow-snapshots\x01");
for s in snaps {
for &x in &s.bases {
b.push(x);
}
for &o in &s.outcomes {
b.push(o as u8);
}
}
b
}
fn snapshot_contribution(snap: &Snapshot, pauli: &[u8]) -> Option<i64> {
let mut prod = 1i64;
for (q, &pq) in pauli.iter().enumerate() {
if pq == PI {
continue;
}
if snap.bases[q] != pq {
return None; }
prod *= 3 * snap.outcomes[q] as i64;
}
Some(prod)
}
pub fn estimate_pauli(snaps: &[Snapshot], pauli: &[u8], n_batches: usize) -> ShadowResult {
let m = snaps.len();
let batches = n_batches.clamp(1, m.max(1));
let mut means: Vec<i64> = Vec::with_capacity(batches);
for b in 0..batches {
let lo = b * m / batches;
let hi = (b + 1) * m / batches;
if hi <= lo {
continue;
}
let mut acc = 0i128;
for snap in &snaps[lo..hi] {
let c = snapshot_contribution(snap, pauli).unwrap_or(0);
acc += c as i128 * MIT_ONE as i128;
}
means.push((acc / (hi - lo) as i128) as i64);
}
means.sort_unstable();
let estimate_fx = if means.is_empty() {
0
} else {
means[means.len() / 2]
};
let error_fx = if means.len() < 2 {
0
} else {
(means[means.len() - 1] - means[0]) / 2
};
ShadowResult {
estimate_fx,
error_fx,
n_snapshots: m,
input: canonical_snapshots(snaps),
}
}
impl ShadowResult {
#[allow(clippy::too_many_arguments)]
pub fn seal(
&self,
signer: &SigningKey,
signer_id: impl Into<String>,
backend_id: impl Into<String>,
observable: impl Into<String>,
raw_fx: i64,
joules_micro: u64,
grant: GrantRef,
) -> MitigationReceipt {
MitigationReceipt::seal(
signer,
signer_id,
backend_id,
"shadow.pauli",
observable,
content_hash(&self.input),
None,
raw_fx,
self.estimate_fx,
self.error_fx,
self.n_snapshots as u64,
joules_micro,
grant,
None,
)
}
}
pub fn simulate_shadows(bloch: &[[i64; 3]], n_snapshots: usize, seed: u64) -> Vec<Snapshot> {
let n = bloch.len();
let mut out = Vec::with_capacity(n_snapshots);
let mut st = seed.wrapping_mul(0x9E37_79B9).wrapping_add(7);
for _ in 0..n_snapshots {
let mut bases = Vec::with_capacity(n);
let mut outcomes = Vec::with_capacity(n);
for qb in bloch.iter() {
let basis = (splitmix64(&mut st) % 3) as u8; let r = qb[basis as usize];
let o = draw_pm1(&mut st, r) as i8;
bases.push(basis);
outcomes.push(o);
}
out.push(Snapshot { bases, outcomes });
}
out
}
pub fn bloch_expectation(bloch: &[[i64; 3]], pauli: &[u8]) -> i64 {
let mut prod = MIT_ONE;
for (q, &pq) in pauli.iter().enumerate() {
if pq == PI {
continue;
}
prod = fmul(prod, bloch[q][pq as usize]);
}
prod
}
#[cfg(test)]
mod tests {
use super::*;
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
#[test]
fn zne_recovers_ideal_from_noisy_decay() {
let ideal = (0.8 * MIT_ONE as f64) as i64;
let gamma = (0.25 * MIT_ONE as f64) as i64;
let scales: Vec<i64> = [1.0, 2.0, 3.0]
.iter()
.map(|s| (s * MIT_ONE as f64) as i64)
.collect();
let table = simulate_zne(ideal, gamma, &scales, 20_000, 0xBEEF);
let raw = table[0].1;
let rich = zne_extrapolate(&table, ZneMethod::Richardson, 100_000);
let err_raw = (raw - ideal).abs();
let err_mit = (rich.mitigated_fx - ideal).abs();
assert!(
err_mit < err_raw,
"mitigated must beat raw: raw_err={err_raw}, mit_err={err_mit}"
);
assert!(err_mit < (0.08 * MIT_ONE as f64) as i64, "mit within 8%: {err_mit}");
}
#[test]
fn zne_is_deterministic() {
let ideal = MIT_ONE / 2;
let g = MIT_ONE / 5;
let s: Vec<i64> = [1.0, 2.0, 3.0].iter().map(|x| (x * MIT_ONE as f64) as i64).collect();
let a = simulate_zne(ideal, g, &s, 5000, 42);
let b = simulate_zne(ideal, g, &s, 5000, 42);
assert_eq!(a, b, "same seed → byte-identical table");
assert_eq!(
zne_extrapolate(&a, ZneMethod::Linear, 15_000),
zne_extrapolate(&b, ZneMethod::Linear, 15_000)
);
}
#[test]
fn zne_linear_exact_on_a_line() {
let pts = vec![
(MIT_ONE, MIT_ONE / 2), (2 * MIT_ONE, MIT_ONE / 4), (3 * MIT_ONE, 0), ];
let r = zne_extrapolate(&pts, ZneMethod::Linear, 0);
assert!((r.mitigated_fx - (3 * MIT_ONE / 4)).abs() < 16, "intercept ~0.75");
assert!(r.error_fx < 16, "residual ~0 on an exact line");
}
#[test]
fn zne_seals_and_verifies_no_model() {
let pts = vec![(MIT_ONE, MIT_ONE / 2), (2 * MIT_ONE, MIT_ONE / 4)];
let r = zne_extrapolate(&pts, ZneMethod::Richardson, 10_000);
let rec = r.seal(&key(1), "did:key:lab", "wai.quantum.circuit", "ZZ", 500_000, GrantRef::unbounded("quantum.mitigate"));
assert!(rec.verify());
assert!(rec.noise_model_hash.is_none());
assert!(rec.input_matches(&r.input_bytes()));
}
#[test]
fn readout_inversion_recovers_parity() {
let n = 2;
let dim = 1usize << n;
let mut p_true = vec![0i64; dim];
p_true[0b00] = MIT_ONE / 2;
p_true[0b11] = MIT_ONE / 2;
let obs: Vec<i64> = (0..dim)
.map(|x| if (x.count_ones() & 1) == 0 { MIT_ONE } else { -MIT_ONE })
.collect();
let model = ReadoutModel::symmetric(n, MIT_ONE / 12, MIT_ONE / 12); let counts = simulate_readout(&p_true, &model, 200_000, 0xC0FFEE);
let res = readout_mitigate(&counts, &obs, &model);
assert!(res.raw_fx < (0.9 * MIT_ONE as f64) as i64, "raw pulled down: {}", res.raw_fx);
let err = (res.mitigated_fx - MIT_ONE).abs();
assert!(err < (0.03 * MIT_ONE as f64) as i64, "mitigated ~1.0: {}", res.mitigated_fx);
}
#[test]
fn readout_seals_with_pinned_model() {
let n = 2;
let dim = 1usize << n;
let mut p_true = vec![0i64; dim];
p_true[0] = MIT_ONE;
let obs = vec![MIT_ONE, -MIT_ONE, -MIT_ONE, MIT_ONE];
let model = ReadoutModel::symmetric(n, MIT_ONE / 20, MIT_ONE / 20);
let counts = simulate_readout(&p_true, &model, 50_000, 1);
let res = readout_mitigate(&counts, &obs, &model);
let rec = res.seal(&key(2), "lab", "sim", "ZZ", 400_000, GrantRef::unbounded("quantum.mitigate"));
assert!(rec.verify());
assert!(rec.noise_model_matches(&model.bytes()));
assert!(rec.input_matches(&res.counts_bytes));
}
#[test]
fn shadows_recover_pauli_expectation() {
let bloch = [
[(0.6 * MIT_ONE as f64) as i64, 0, (0.5 * MIT_ONE as f64) as i64],
[0, (0.7 * MIT_ONE as f64) as i64, 0],
[0, 0, (0.9 * MIT_ONE as f64) as i64],
];
let pauli = [PX, PY, PZ];
let truth = bloch_expectation(&bloch, &pauli);
let snaps = simulate_shadows(&bloch, 60_000, 0x5EED);
let est = estimate_pauli(&snaps, &pauli, 12);
let err = (est.estimate_fx - truth).abs();
assert!(
err < (0.05 * MIT_ONE as f64) as i64,
"shadow estimate {} vs truth {} (err {})",
est.estimate_fx, truth, err
);
}
#[test]
fn shadows_deterministic_and_sealed() {
let bloch = [[0, 0, MIT_ONE], [0, 0, MIT_ONE / 2]];
let a = simulate_shadows(&bloch, 4000, 9);
let b = simulate_shadows(&bloch, 4000, 9);
assert_eq!(a, b);
let pauli = [PZ, PI];
let ea = estimate_pauli(&a, &pauli, 8);
assert_eq!(ea, estimate_pauli(&b, &pauli, 8));
let rec = ea.seal(&key(3), "lab", "sim", "Z0", 0, 300_000, GrantRef::unbounded("quantum.mitigate"));
assert!(rec.verify());
assert!(rec.input_matches(&ea.input));
}
#[test]
fn shadow_wrong_basis_contributes_zero() {
let snap = Snapshot { bases: vec![PZ], outcomes: vec![1] };
assert_eq!(snapshot_contribution(&snap, &[PX]), None);
let snap2 = Snapshot { bases: vec![PX], outcomes: vec![-1] };
assert_eq!(snapshot_contribution(&snap2, &[PX]), Some(-3));
}
}