1use crate::quantum_ops::{content_hash, GrantRef, MitigationReceipt};
39use ed25519_dalek::SigningKey;
40
41pub const MIT_FRAC: u32 = 20;
44pub const MIT_ONE: i64 = 1 << MIT_FRAC;
46
47#[inline]
48fn fmul(a: i64, b: i64) -> i64 {
49 ((a as i128 * b as i128) >> MIT_FRAC) as i64
50}
51
52#[inline]
53fn fdiv(p: i64, q: i64) -> i64 {
54 if q == 0 {
55 return 0;
56 }
57 (((p as i128) << MIT_FRAC) / q as i128) as i64
58}
59
60fn isqrt_i128(v: i128) -> i128 {
62 if v <= 0 {
63 return 0;
64 }
65 let mut x = v;
66 let mut y = (x + 1) / 2;
67 while y < x {
68 x = y;
69 y = (x + v / x) / 2;
70 }
71 x
72}
73
74fn sqrt_fx_of_square(sq: i128) -> i64 {
78 isqrt_i128(sq) as i64
79}
80
81fn splitmix64(state: &mut u64) -> u64 {
82 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
83 let mut z = *state;
84 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
85 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
86 z ^ (z >> 31)
87}
88
89fn draw_pm1(st: &mut u64, e_fx: i64) -> i64 {
92 let p_plus = (MIT_ONE + e_fx) / 2; let r = (splitmix64(st) >> (64 - MIT_FRAC)) as i64; if r < p_plus {
95 1
96 } else {
97 -1
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum ZneMethod {
108 Linear,
111 Richardson,
114}
115
116impl ZneMethod {
117 fn tag(self) -> &'static str {
118 match self {
119 ZneMethod::Linear => "zne.linear",
120 ZneMethod::Richardson => "zne.richardson",
121 }
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct ZneResult {
128 pub method: &'static str,
129 pub points: Vec<(i64, i64)>,
131 pub raw_fx: i64,
133 pub mitigated_fx: i64,
135 pub error_fx: i64,
138 pub shots: u64,
139}
140
141fn fit_linear(points: &[(i64, i64)]) -> (i64, i64) {
144 let n = points.len() as i128;
145 if n == 0 {
146 return (0, 0);
147 }
148 let (mut sx, mut sy, mut sxy, mut sxx) = (0i128, 0i128, 0i128, 0i128);
149 for &(x, y) in points {
150 sx += x as i128;
151 sy += y as i128;
152 sxy += x as i128 * y as i128; sxx += x as i128 * x as i128; }
155 let denom = n * sxx - sx * sx; if denom == 0 {
157 return ((sy / n) as i64, 0); }
159 let numer = n * sxy - sx * sy; let slope = ((numer << MIT_FRAC) / denom) as i64;
162 let mean_x = (sx / n) as i64;
163 let mean_y = (sy / n) as i64;
164 let intercept = mean_y - fmul(slope, mean_x);
165 (intercept, slope)
166}
167
168fn richardson_zero(points: &[(i64, i64)]) -> i64 {
171 let m = points.len();
172 let mut acc = 0i64;
173 for i in 0..m {
174 let xi = points[i].0;
175 let mut w = MIT_ONE;
177 for (j, &(xj, _)) in points.iter().enumerate() {
178 if j == i {
179 continue;
180 }
181 let d = xj - xi;
182 if d == 0 {
183 continue; }
185 w = fdiv(fmul(w, xj), d);
186 }
187 acc += fmul(w, points[i].1);
188 }
189 acc
190}
191
192pub fn zne_extrapolate(points: &[(i64, i64)], method: ZneMethod, shots: u64) -> ZneResult {
196 let mut pts = points.to_vec();
197 pts.sort_by_key(|p| p.0);
198 let raw_fx = pts.first().map(|p| p.1).unwrap_or(0);
199
200 let (intercept, slope) = fit_linear(&pts);
201 let lin0 = intercept;
202
203 let (mitigated_fx, error_fx) = match method {
204 ZneMethod::Linear => {
205 let n = pts.len().max(1) as i128;
207 let mut ss = 0i128;
208 for &(x, y) in &pts {
209 let pred = intercept + fmul(slope, x);
210 let r = (y - pred) as i128;
211 ss += r * r; }
213 let rms = sqrt_fx_of_square(ss / n);
214 (lin0, rms)
215 }
216 ZneMethod::Richardson => {
217 let capped: Vec<(i64, i64)> = pts.iter().take(8).copied().collect();
218 let rich = richardson_zero(&capped);
219 (rich, (rich - lin0).abs())
220 }
221 };
222
223 ZneResult {
224 method: method.tag(),
225 points: pts,
226 raw_fx,
227 mitigated_fx,
228 error_fx,
229 shots,
230 }
231}
232
233impl ZneResult {
234 pub fn input_bytes(&self) -> Vec<u8> {
236 let mut b = Vec::with_capacity(self.points.len() * 16 + 8);
237 b.extend_from_slice(b"wai:zne-input\x01");
238 for &(x, y) in &self.points {
239 b.extend_from_slice(&x.to_le_bytes());
240 b.extend_from_slice(&y.to_le_bytes());
241 }
242 b
243 }
244
245 #[allow(clippy::too_many_arguments)]
248 pub fn seal(
249 &self,
250 signer: &SigningKey,
251 signer_id: impl Into<String>,
252 backend_id: impl Into<String>,
253 observable: impl Into<String>,
254 joules_micro: u64,
255 grant: GrantRef,
256 ) -> MitigationReceipt {
257 MitigationReceipt::seal(
258 signer,
259 signer_id,
260 backend_id,
261 self.method,
262 observable,
263 content_hash(&self.input_bytes()),
264 None,
265 self.raw_fx,
266 self.mitigated_fx,
267 self.error_fx,
268 self.shots,
269 joules_micro,
270 grant,
271 None,
272 )
273 }
274}
275
276pub fn simulate_zne(
281 ideal_fx: i64,
282 gamma_fx: i64,
283 scales_fx: &[i64],
284 shots: u32,
285 seed: u64,
286) -> Vec<(i64, i64)> {
287 scales_fx
288 .iter()
289 .map(|&lam| {
290 let e_true = fmul(ideal_fx, exp_neg_fx(fmul(gamma_fx, lam)));
291 let mut st = seed
292 .wrapping_mul(0x1000_0001)
293 .wrapping_add(lam as u64)
294 .wrapping_add(0xA5A5);
295 let mut sum = 0i64;
296 for _ in 0..shots {
297 sum += draw_pm1(&mut st, e_true);
298 }
299 let meas = ((sum as i128 * MIT_ONE as i128) / shots.max(1) as i128) as i64;
300 (lam, meas)
301 })
302 .collect()
303}
304
305fn exp_neg_fx(x_fx: i64) -> i64 {
308 if x_fx <= 0 {
309 return MIT_ONE;
310 }
311 let mut halvings = 0;
313 let mut x = x_fx;
314 while x > (MIT_ONE >> 2) {
315 x >>= 1;
316 halvings += 1;
317 }
318 let mut term = MIT_ONE;
320 let mut acc = 0i64;
321 let mut sign = 1i64;
322 for k in 0..6 {
323 acc += sign * term;
324 term = fmul(term, x) / (k + 1) as i64;
325 sign = -sign;
326 }
327 let mut r = acc;
328 for _ in 0..halvings {
329 r = fmul(r, r);
330 }
331 r.clamp(0, MIT_ONE)
332}
333
334#[derive(Clone, Debug, PartialEq, Eq)]
342pub struct ReadoutModel {
343 pub qubits: Vec<[i64; 4]>,
344}
345
346impl ReadoutModel {
347 pub fn symmetric(n: usize, p_meas0_given1: i64, p_meas1_given0: i64) -> ReadoutModel {
350 let mat = [
351 MIT_ONE - p_meas1_given0, p_meas0_given1, p_meas1_given0, MIT_ONE - p_meas0_given1, ];
356 ReadoutModel {
357 qubits: vec![mat; n],
358 }
359 }
360
361 pub fn bytes(&self) -> Vec<u8> {
363 let mut b = Vec::with_capacity(self.qubits.len() * 32 + 8);
364 b.extend_from_slice(b"wai:readout-model\x01");
365 for q in &self.qubits {
366 for v in q {
367 b.extend_from_slice(&v.to_le_bytes());
368 }
369 }
370 b
371 }
372
373 fn inv(&self, q: usize) -> [i64; 4] {
374 let [a, b, c, d] = self.qubits[q];
375 let det = fmul(a, d) - fmul(b, c);
376 let id = fdiv(MIT_ONE, det);
377 [fmul(id, d), -fmul(id, b), -fmul(id, c), fmul(id, a)]
378 }
379}
380
381fn apply_qubit(v: &mut [i64], n: usize, q: usize, op: [i64; 4]) {
384 let bit = 1usize << q;
385 for x in 0..(1usize << n) {
386 if x & bit == 0 {
387 let v0 = v[x];
388 let v1 = v[x | bit];
389 v[x] = fmul(op[0], v0) + fmul(op[1], v1);
390 v[x | bit] = fmul(op[2], v0) + fmul(op[3], v1);
391 }
392 }
393}
394
395#[derive(Clone, Debug, PartialEq, Eq)]
397pub struct ReadoutResult {
398 pub n: usize,
399 pub raw_fx: i64,
401 pub mitigated_fx: i64,
403 pub negativity_fx: i64,
406 pub shots: u64,
407 pub counts_bytes: Vec<u8>,
409 pub model_hash: [u8; 32],
411}
412
413pub fn readout_mitigate(
417 counts: &[u64],
418 obs: &[i64],
419 model: &ReadoutModel,
420) -> ReadoutResult {
421 let n = model.qubits.len();
422 let dim = 1usize << n;
423 assert_eq!(counts.len(), dim, "counts must have 2^n entries");
424 assert_eq!(obs.len(), dim, "obs must have 2^n entries");
425
426 let total: u64 = counts.iter().sum::<u64>().max(1);
427 let mut p: Vec<i64> = counts
428 .iter()
429 .map(|&c| ((c as i128 * MIT_ONE as i128) / total as i128) as i64)
430 .collect();
431
432 let raw_fx = p
433 .iter()
434 .zip(obs)
435 .map(|(&pi, &oi)| fmul(pi, oi))
436 .sum::<i64>();
437
438 for q in 0..n {
439 apply_qubit(&mut p, n, q, model.inv(q));
440 }
441
442 let mitigated_fx = p.iter().zip(obs).map(|(&pi, &oi)| fmul(pi, oi)).sum::<i64>();
443 let negativity_fx = p.iter().filter(|&&x| x < 0).map(|&x| -x).sum::<i64>();
444
445 let mut cb = Vec::with_capacity(dim * 8 + 8);
446 cb.extend_from_slice(b"wai:readout-counts\x01");
447 for &c in counts {
448 cb.extend_from_slice(&c.to_le_bytes());
449 }
450
451 ReadoutResult {
452 n,
453 raw_fx,
454 mitigated_fx,
455 negativity_fx,
456 shots: total,
457 counts_bytes: cb,
458 model_hash: content_hash(&model.bytes()),
459 }
460}
461
462impl ReadoutResult {
463 pub fn seal(
465 &self,
466 signer: &SigningKey,
467 signer_id: impl Into<String>,
468 backend_id: impl Into<String>,
469 observable: impl Into<String>,
470 joules_micro: u64,
471 grant: GrantRef,
472 ) -> MitigationReceipt {
473 MitigationReceipt::seal(
474 signer,
475 signer_id,
476 backend_id,
477 "readout.tensored",
478 observable,
479 content_hash(&self.counts_bytes),
480 Some(self.model_hash),
481 self.raw_fx,
482 self.mitigated_fx,
483 self.negativity_fx,
484 self.shots,
485 joules_micro,
486 grant,
487 None,
488 )
489 }
490}
491
492pub fn simulate_readout(
497 p_true_fx: &[i64],
498 model: &ReadoutModel,
499 shots: u32,
500 seed: u64,
501) -> Vec<u64> {
502 let n = model.qubits.len();
503 let dim = 1usize << n;
504 let mut p = p_true_fx.to_vec();
506 for q in 0..n {
507 apply_qubit(&mut p, n, q, model.qubits[q]);
508 }
509 let mut cdf = vec![0i64; dim];
511 let mut run = 0i64;
512 for i in 0..dim {
513 run += p[i].max(0);
514 cdf[i] = run;
515 }
516 let norm = run.max(1);
517 let mut counts = vec![0u64; dim];
518 let mut st = seed.wrapping_mul(0x2545_F491).wrapping_add(1);
519 for _ in 0..shots {
520 let r = (((splitmix64(&mut st) >> (64 - MIT_FRAC)) as i128 * norm as i128) >> MIT_FRAC) as i64;
521 let mut idx = dim - 1;
522 for (i, &c) in cdf.iter().enumerate() {
523 if r < c {
524 idx = i;
525 break;
526 }
527 }
528 counts[idx] += 1;
529 }
530 counts
531}
532
533pub const PX: u8 = 0;
539pub const PY: u8 = 1;
540pub const PZ: u8 = 2;
541pub const PI: u8 = 3;
543
544#[derive(Clone, Debug, PartialEq, Eq)]
547pub struct Snapshot {
548 pub bases: Vec<u8>,
549 pub outcomes: Vec<i8>,
550}
551
552#[derive(Clone, Debug, PartialEq, Eq)]
554pub struct ShadowResult {
555 pub estimate_fx: i64,
556 pub error_fx: i64,
558 pub n_snapshots: usize,
559 pub input: Vec<u8>,
561}
562
563fn canonical_snapshots(snaps: &[Snapshot]) -> Vec<u8> {
564 let mut b = Vec::with_capacity(snaps.len() * 8 + 8);
565 b.extend_from_slice(b"wai:shadow-snapshots\x01");
566 for s in snaps {
567 for &x in &s.bases {
568 b.push(x);
569 }
570 for &o in &s.outcomes {
571 b.push(o as u8);
572 }
573 }
574 b
575}
576
577fn snapshot_contribution(snap: &Snapshot, pauli: &[u8]) -> Option<i64> {
582 let mut prod = 1i64;
583 for (q, &pq) in pauli.iter().enumerate() {
584 if pq == PI {
585 continue;
586 }
587 if snap.bases[q] != pq {
588 return None; }
590 prod *= 3 * snap.outcomes[q] as i64;
591 }
592 Some(prod)
593}
594
595pub fn estimate_pauli(snaps: &[Snapshot], pauli: &[u8], n_batches: usize) -> ShadowResult {
599 let m = snaps.len();
600 let batches = n_batches.clamp(1, m.max(1));
601 let mut means: Vec<i64> = Vec::with_capacity(batches);
602 for b in 0..batches {
603 let lo = b * m / batches;
604 let hi = (b + 1) * m / batches;
605 if hi <= lo {
606 continue;
607 }
608 let mut acc = 0i128;
609 for snap in &snaps[lo..hi] {
610 let c = snapshot_contribution(snap, pauli).unwrap_or(0);
612 acc += c as i128 * MIT_ONE as i128;
613 }
614 means.push((acc / (hi - lo) as i128) as i64);
615 }
616 means.sort_unstable();
617 let estimate_fx = if means.is_empty() {
618 0
619 } else {
620 means[means.len() / 2]
621 };
622 let error_fx = if means.len() < 2 {
623 0
624 } else {
625 (means[means.len() - 1] - means[0]) / 2
626 };
627 ShadowResult {
628 estimate_fx,
629 error_fx,
630 n_snapshots: m,
631 input: canonical_snapshots(snaps),
632 }
633}
634
635impl ShadowResult {
636 #[allow(clippy::too_many_arguments)]
639 pub fn seal(
640 &self,
641 signer: &SigningKey,
642 signer_id: impl Into<String>,
643 backend_id: impl Into<String>,
644 observable: impl Into<String>,
645 raw_fx: i64,
646 joules_micro: u64,
647 grant: GrantRef,
648 ) -> MitigationReceipt {
649 MitigationReceipt::seal(
650 signer,
651 signer_id,
652 backend_id,
653 "shadow.pauli",
654 observable,
655 content_hash(&self.input),
656 None,
657 raw_fx,
658 self.estimate_fx,
659 self.error_fx,
660 self.n_snapshots as u64,
661 joules_micro,
662 grant,
663 None,
664 )
665 }
666}
667
668pub fn simulate_shadows(bloch: &[[i64; 3]], n_snapshots: usize, seed: u64) -> Vec<Snapshot> {
674 let n = bloch.len();
675 let mut out = Vec::with_capacity(n_snapshots);
676 let mut st = seed.wrapping_mul(0x9E37_79B9).wrapping_add(7);
677 for _ in 0..n_snapshots {
678 let mut bases = Vec::with_capacity(n);
679 let mut outcomes = Vec::with_capacity(n);
680 for qb in bloch.iter() {
681 let basis = (splitmix64(&mut st) % 3) as u8; let r = qb[basis as usize];
683 let o = draw_pm1(&mut st, r) as i8;
684 bases.push(basis);
685 outcomes.push(o);
686 }
687 out.push(Snapshot { bases, outcomes });
688 }
689 out
690}
691
692pub fn bloch_expectation(bloch: &[[i64; 3]], pauli: &[u8]) -> i64 {
696 let mut prod = MIT_ONE;
697 for (q, &pq) in pauli.iter().enumerate() {
698 if pq == PI {
699 continue;
700 }
701 prod = fmul(prod, bloch[q][pq as usize]);
702 }
703 prod
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 fn key(s: u8) -> SigningKey {
711 SigningKey::from_bytes(&[s; 32])
712 }
713
714 #[test]
717 fn zne_recovers_ideal_from_noisy_decay() {
718 let ideal = (0.8 * MIT_ONE as f64) as i64;
720 let gamma = (0.25 * MIT_ONE as f64) as i64;
721 let scales: Vec<i64> = [1.0, 2.0, 3.0]
725 .iter()
726 .map(|s| (s * MIT_ONE as f64) as i64)
727 .collect();
728 let table = simulate_zne(ideal, gamma, &scales, 20_000, 0xBEEF);
729 let raw = table[0].1;
730
731 let rich = zne_extrapolate(&table, ZneMethod::Richardson, 100_000);
732 let err_raw = (raw - ideal).abs();
735 let err_mit = (rich.mitigated_fx - ideal).abs();
736 assert!(
737 err_mit < err_raw,
738 "mitigated must beat raw: raw_err={err_raw}, mit_err={err_mit}"
739 );
740 assert!(err_mit < (0.08 * MIT_ONE as f64) as i64, "mit within 8%: {err_mit}");
742 }
743
744 #[test]
745 fn zne_is_deterministic() {
746 let ideal = MIT_ONE / 2;
747 let g = MIT_ONE / 5;
748 let s: Vec<i64> = [1.0, 2.0, 3.0].iter().map(|x| (x * MIT_ONE as f64) as i64).collect();
749 let a = simulate_zne(ideal, g, &s, 5000, 42);
750 let b = simulate_zne(ideal, g, &s, 5000, 42);
751 assert_eq!(a, b, "same seed → byte-identical table");
752 assert_eq!(
753 zne_extrapolate(&a, ZneMethod::Linear, 15_000),
754 zne_extrapolate(&b, ZneMethod::Linear, 15_000)
755 );
756 }
757
758 #[test]
759 fn zne_linear_exact_on_a_line() {
760 let pts = vec![
762 (MIT_ONE, MIT_ONE / 2), (2 * MIT_ONE, MIT_ONE / 4), (3 * MIT_ONE, 0), ];
766 let r = zne_extrapolate(&pts, ZneMethod::Linear, 0);
767 assert!((r.mitigated_fx - (3 * MIT_ONE / 4)).abs() < 16, "intercept ~0.75");
769 assert!(r.error_fx < 16, "residual ~0 on an exact line");
770 }
771
772 #[test]
773 fn zne_seals_and_verifies_no_model() {
774 let pts = vec![(MIT_ONE, MIT_ONE / 2), (2 * MIT_ONE, MIT_ONE / 4)];
775 let r = zne_extrapolate(&pts, ZneMethod::Richardson, 10_000);
776 let rec = r.seal(&key(1), "did:key:lab", "wai.quantum.circuit", "ZZ", 500_000, GrantRef::unbounded("quantum.mitigate"));
777 assert!(rec.verify());
778 assert!(rec.noise_model_hash.is_none());
779 assert!(rec.input_matches(&r.input_bytes()));
780 }
781
782 #[test]
785 fn readout_inversion_recovers_parity() {
786 let n = 2;
787 let dim = 1usize << n;
789 let mut p_true = vec![0i64; dim];
790 p_true[0b00] = MIT_ONE / 2;
791 p_true[0b11] = MIT_ONE / 2;
792 let obs: Vec<i64> = (0..dim)
793 .map(|x| if (x.count_ones() & 1) == 0 { MIT_ONE } else { -MIT_ONE })
794 .collect();
795
796 let model = ReadoutModel::symmetric(n, MIT_ONE / 12, MIT_ONE / 12); let counts = simulate_readout(&p_true, &model, 200_000, 0xC0FFEE);
798 let res = readout_mitigate(&counts, &obs, &model);
799
800 assert!(res.raw_fx < (0.9 * MIT_ONE as f64) as i64, "raw pulled down: {}", res.raw_fx);
802 let err = (res.mitigated_fx - MIT_ONE).abs();
803 assert!(err < (0.03 * MIT_ONE as f64) as i64, "mitigated ~1.0: {}", res.mitigated_fx);
804 }
805
806 #[test]
807 fn readout_seals_with_pinned_model() {
808 let n = 2;
809 let dim = 1usize << n;
810 let mut p_true = vec![0i64; dim];
811 p_true[0] = MIT_ONE;
812 let obs = vec![MIT_ONE, -MIT_ONE, -MIT_ONE, MIT_ONE];
813 let model = ReadoutModel::symmetric(n, MIT_ONE / 20, MIT_ONE / 20);
814 let counts = simulate_readout(&p_true, &model, 50_000, 1);
815 let res = readout_mitigate(&counts, &obs, &model);
816 let rec = res.seal(&key(2), "lab", "sim", "ZZ", 400_000, GrantRef::unbounded("quantum.mitigate"));
817 assert!(rec.verify());
818 assert!(rec.noise_model_matches(&model.bytes()));
819 assert!(rec.input_matches(&res.counts_bytes));
820 }
821
822 #[test]
825 fn shadows_recover_pauli_expectation() {
826 let bloch = [
828 [(0.6 * MIT_ONE as f64) as i64, 0, (0.5 * MIT_ONE as f64) as i64],
829 [0, (0.7 * MIT_ONE as f64) as i64, 0],
830 [0, 0, (0.9 * MIT_ONE as f64) as i64],
831 ];
832 let pauli = [PX, PY, PZ];
834 let truth = bloch_expectation(&bloch, &pauli);
835 let snaps = simulate_shadows(&bloch, 60_000, 0x5EED);
836 let est = estimate_pauli(&snaps, &pauli, 12);
837 let err = (est.estimate_fx - truth).abs();
838 assert!(
839 err < (0.05 * MIT_ONE as f64) as i64,
840 "shadow estimate {} vs truth {} (err {})",
841 est.estimate_fx, truth, err
842 );
843 }
844
845 #[test]
846 fn shadows_deterministic_and_sealed() {
847 let bloch = [[0, 0, MIT_ONE], [0, 0, MIT_ONE / 2]];
848 let a = simulate_shadows(&bloch, 4000, 9);
849 let b = simulate_shadows(&bloch, 4000, 9);
850 assert_eq!(a, b);
851 let pauli = [PZ, PI];
852 let ea = estimate_pauli(&a, &pauli, 8);
853 assert_eq!(ea, estimate_pauli(&b, &pauli, 8));
854 let rec = ea.seal(&key(3), "lab", "sim", "Z0", 0, 300_000, GrantRef::unbounded("quantum.mitigate"));
855 assert!(rec.verify());
856 assert!(rec.input_matches(&ea.input));
857 }
858
859 #[test]
860 fn shadow_wrong_basis_contributes_zero() {
861 let snap = Snapshot { bases: vec![PZ], outcomes: vec![1] };
863 assert_eq!(snapshot_contribution(&snap, &[PX]), None);
864 let snap2 = Snapshot { bases: vec![PX], outcomes: vec![-1] };
865 assert_eq!(snapshot_contribution(&snap2, &[PX]), Some(-3));
866 }
867}