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