1use crate::circuit::QuantumCircuit;
13use crate::gate::Gate;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
22pub struct ZneConfig {
23 pub noise_factors: Vec<f64>,
25 pub extrapolation: ExtrapolationMethod,
27}
28
29#[derive(Debug, Clone)]
31pub enum ExtrapolationMethod {
32 Linear,
34 Polynomial(usize),
36 Richardson,
39}
40
41pub fn fold_circuit(circuit: &QuantumCircuit, factor: f64) -> QuantumCircuit {
53 assert!(factor >= 1.0, "noise factor must be >= 1.0");
54
55 let gates = circuit.gates();
56 let mut folded = QuantumCircuit::new(circuit.num_qubits());
57
58 let unitary_indices: Vec<usize> = gates
60 .iter()
61 .enumerate()
62 .filter(|(_, g)| !g.is_non_unitary())
63 .map(|(i, _)| i)
64 .collect();
65
66 let n_unitary = unitary_indices.len();
67
68 let target_unitary_slots = (n_unitary as f64 * factor).round() as usize;
71
72 let num_folds = if target_unitary_slots > n_unitary {
76 (target_unitary_slots - n_unitary) / 2
77 } else {
78 0
79 };
80
81 let full_rounds = num_folds / n_unitary.max(1);
84 let extra_folds = num_folds % n_unitary.max(1);
85
86 let mut unitary_counter: usize = 0;
89
90 for gate in gates.iter() {
91 if gate.is_non_unitary() {
92 folded.add_gate(gate.clone());
93 continue;
94 }
95
96 let rounds = full_rounds + if unitary_counter < extra_folds { 1 } else { 0 };
98 unitary_counter += 1;
99
100 folded.add_gate(gate.clone());
102
103 for _ in 0..rounds {
105 let dag = gate_dagger(gate);
106 folded.add_gate(dag);
107 folded.add_gate(gate.clone());
108 }
109 }
110
111 folded
112}
113
114fn gate_dagger(gate: &Gate) -> Gate {
120 match gate {
121 Gate::H(q) => Gate::H(*q),
123 Gate::X(q) => Gate::X(*q),
124 Gate::Y(q) => Gate::Y(*q),
125 Gate::Z(q) => Gate::Z(*q),
126 Gate::CNOT(c, t) => Gate::CNOT(*c, *t),
127 Gate::CZ(q1, q2) => Gate::CZ(*q1, *q2),
128 Gate::SWAP(q1, q2) => Gate::SWAP(*q1, *q2),
129
130 Gate::S(q) => Gate::Sdg(*q),
132 Gate::Sdg(q) => Gate::S(*q),
133
134 Gate::T(q) => Gate::Tdg(*q),
136 Gate::Tdg(q) => Gate::T(*q),
137
138 Gate::Rx(q, theta) => Gate::Rx(*q, -theta),
140 Gate::Ry(q, theta) => Gate::Ry(*q, -theta),
141 Gate::Rz(q, theta) => Gate::Rz(*q, -theta),
142 Gate::Phase(q, theta) => Gate::Phase(*q, -theta),
143 Gate::Rzz(q1, q2, theta) => Gate::Rzz(*q1, *q2, -theta),
144
145 Gate::Unitary1Q(q, m) => {
147 let dag = [
148 [m[0][0].conj(), m[1][0].conj()],
149 [m[0][1].conj(), m[1][1].conj()],
150 ];
151 Gate::Unitary1Q(*q, dag)
152 }
153
154 Gate::Measure(q) => Gate::Measure(*q),
156 Gate::Reset(q) => Gate::Reset(*q),
157 Gate::Barrier => Gate::Barrier,
158 }
159}
160
161pub fn richardson_extrapolate(noise_factors: &[f64], values: &[f64]) -> f64 {
168 assert_eq!(
169 noise_factors.len(),
170 values.len(),
171 "noise_factors and values must have the same length"
172 );
173 let n = noise_factors.len();
174 assert!(n > 0, "need at least one data point");
175
176 let mut result = 0.0;
179 for i in 0..n {
180 let mut weight = 1.0;
181 for j in 0..n {
182 if j != i {
183 weight *= -noise_factors[j] / (noise_factors[i] - noise_factors[j]);
185 }
186 }
187 result += values[i] * weight;
188 }
189 result
190}
191
192pub fn polynomial_extrapolate(noise_factors: &[f64], values: &[f64], degree: usize) -> f64 {
197 assert_eq!(
198 noise_factors.len(),
199 values.len(),
200 "noise_factors and values must have the same length"
201 );
202 let n = noise_factors.len();
203 let p = degree + 1; assert!(n >= p, "need at least degree+1 data points for a degree-{degree} polynomial");
205
206 let mut ata = vec![vec![0.0_f64; p]; p];
212 let mut aty = vec![0.0_f64; p];
214
215 for i in 0..n {
216 let x = noise_factors[i];
217 let y = values[i];
218
219 let max_power = 2 * degree;
221 let mut x_powers = Vec::with_capacity(max_power + 1);
222 x_powers.push(1.0);
223 for k in 1..=max_power {
224 x_powers.push(x_powers[k - 1] * x);
225 }
226
227 for j in 0..p {
228 aty[j] += y * x_powers[j];
229 for k in 0..p {
230 ata[j][k] += x_powers[j + k];
231 }
232 }
233 }
234
235 let coeffs = solve_linear_system(&mut ata, &mut aty);
237
238 coeffs[0]
240}
241
242pub fn linear_extrapolate(noise_factors: &[f64], values: &[f64]) -> f64 {
246 polynomial_extrapolate(noise_factors, values, 1)
247}
248
249fn solve_linear_system(a: &mut Vec<Vec<f64>>, b: &mut Vec<f64>) -> Vec<f64> {
252 let n = b.len();
253 assert!(n > 0);
254
255 for col in 0..n {
257 let mut max_row = col;
259 let mut max_val = a[col][col].abs();
260 for row in (col + 1)..n {
261 let v = a[row][col].abs();
262 if v > max_val {
263 max_val = v;
264 max_row = row;
265 }
266 }
267
268 if max_row != col {
270 a.swap(col, max_row);
271 b.swap(col, max_row);
272 }
273
274 let pivot = a[col][col];
275 assert!(
276 pivot.abs() > 1e-15,
277 "singular or near-singular matrix in least-squares solve"
278 );
279
280 for row in (col + 1)..n {
282 let factor = a[row][col] / pivot;
283 for k in col..n {
284 a[row][k] -= factor * a[col][k];
285 }
286 b[row] -= factor * b[col];
287 }
288 }
289
290 let mut x = vec![0.0; n];
292 for col in (0..n).rev() {
293 let mut sum = b[col];
294 for k in (col + 1)..n {
295 sum -= a[col][k] * x[k];
296 }
297 x[col] = sum / a[col][col];
298 }
299
300 x
301}
302
303#[derive(Debug, Clone)]
310pub struct MeasurementCorrector {
311 num_qubits: usize,
312 calibration_matrix: Vec<Vec<f64>>,
315}
316
317impl MeasurementCorrector {
318 pub fn new(readout_errors: &[(f64, f64)]) -> Self {
329 let num_qubits = readout_errors.len();
330 let dim = 1usize << num_qubits;
331
332 let qubit_matrices: Vec<[[f64; 2]; 2]> = readout_errors
334 .iter()
335 .map(|&(p01, p10)| {
336 [
337 [1.0 - p01, p10],
338 [p01, 1.0 - p10],
339 ]
340 })
341 .collect();
342
343 let mut cal = vec![vec![0.0; dim]; dim];
345 for row in 0..dim {
346 for col in 0..dim {
347 let mut val = 1.0;
348 for q in 0..num_qubits {
349 let row_bit = (row >> q) & 1;
350 let col_bit = (col >> q) & 1;
351 val *= qubit_matrices[q][row_bit][col_bit];
352 }
353 cal[row][col] = val;
354 }
355 }
356
357 Self {
358 num_qubits,
359 calibration_matrix: cal,
360 }
361 }
362
363 pub fn correct_counts(
373 &self,
374 counts: &HashMap<Vec<bool>, usize>,
375 ) -> HashMap<Vec<bool>, f64> {
376 let dim = 1usize << self.num_qubits;
377
378 let total_shots: usize = counts.values().sum();
380 let total_f64 = total_shots as f64;
381
382 let mut prob_vec = vec![0.0; dim];
383 for (bits, &count) in counts {
384 let idx = bits_to_index(bits, self.num_qubits);
385 prob_vec[idx] = count as f64 / total_f64;
386 }
387
388 let corrected_probs = if self.num_qubits <= 12 {
390 let inv = invert_matrix(&self.calibration_matrix);
392 mat_vec_mul(&inv, &prob_vec)
393 } else {
394 self.tensor_product_correct(&prob_vec)
398 };
399
400 let mut result = HashMap::new();
402 for idx in 0..dim {
403 let corrected_count = corrected_probs[idx] * total_f64;
404 if corrected_count.abs() > 1e-10 {
405 let bits = index_to_bits(idx, self.num_qubits);
406 result.insert(bits, corrected_count);
407 }
408 }
409
410 result
411 }
412
413 pub fn calibration_matrix(&self) -> &Vec<Vec<f64>> {
415 &self.calibration_matrix
416 }
417
418 fn tensor_product_correct(&self, prob_vec: &[f64]) -> Vec<f64> {
423 let dim = 1usize << self.num_qubits;
424 let mut result = prob_vec.to_vec();
425
426 for q in 0..self.num_qubits {
428 let qubit_mat = self.extract_qubit_matrix(q);
432 let inv = invert_2x2(&qubit_mat);
433
434 let mut new_result = vec![0.0; dim];
436 let stride = 1usize << q;
437 for block_start in (0..dim).step_by(stride * 2) {
438 for offset in 0..stride {
439 let i0 = block_start + offset;
440 let i1 = i0 + stride;
441 new_result[i0] = inv[0][0] * result[i0] + inv[0][1] * result[i1];
442 new_result[i1] = inv[1][0] * result[i0] + inv[1][1] * result[i1];
443 }
444 }
445 result = new_result;
446 }
447
448 result
449 }
450
451 fn extract_qubit_matrix(&self, qubit: usize) -> [[f64; 2]; 2] {
454 let i0 = 0;
458 let i1 = 1usize << qubit;
459
460 [
461 [self.calibration_matrix[i0][i0], self.calibration_matrix[i0][i1]],
462 [self.calibration_matrix[i1][i0], self.calibration_matrix[i1][i1]],
463 ]
464 }
465}
466
467fn bits_to_index(bits: &[bool], num_qubits: usize) -> usize {
469 let mut idx = 0usize;
470 for q in 0..num_qubits {
471 if q < bits.len() && bits[q] {
472 idx |= 1 << q;
473 }
474 }
475 idx
476}
477
478fn index_to_bits(idx: usize, num_qubits: usize) -> Vec<bool> {
480 (0..num_qubits).map(|q| (idx >> q) & 1 == 1).collect()
481}
482
483fn invert_2x2(m: &[[f64; 2]; 2]) -> [[f64; 2]; 2] {
485 let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
486 assert!(det.abs() > 1e-15, "singular 2x2 matrix");
487 let inv_det = 1.0 / det;
488 [
489 [m[1][1] * inv_det, -m[0][1] * inv_det],
490 [-m[1][0] * inv_det, m[0][0] * inv_det],
491 ]
492}
493
494fn invert_matrix(mat: &[Vec<f64>]) -> Vec<Vec<f64>> {
496 let n = mat.len();
497 let mut aug: Vec<Vec<f64>> = mat
499 .iter()
500 .enumerate()
501 .map(|(i, row)| {
502 let mut aug_row = row.clone();
503 aug_row.resize(2 * n, 0.0);
504 aug_row[n + i] = 1.0;
505 aug_row
506 })
507 .collect();
508
509 for col in 0..n {
511 let mut max_row = col;
513 let mut max_val = aug[col][col].abs();
514 for row in (col + 1)..n {
515 let v = aug[row][col].abs();
516 if v > max_val {
517 max_val = v;
518 max_row = row;
519 }
520 }
521 aug.swap(col, max_row);
522
523 let pivot = aug[col][col];
524 assert!(
525 pivot.abs() > 1e-15,
526 "singular matrix in calibration inversion"
527 );
528
529 let inv_pivot = 1.0 / pivot;
531 for k in 0..(2 * n) {
532 aug[col][k] *= inv_pivot;
533 }
534
535 for row in 0..n {
537 if row == col {
538 continue;
539 }
540 let factor = aug[row][col];
541 for k in 0..(2 * n) {
542 aug[row][k] -= factor * aug[col][k];
543 }
544 }
545 }
546
547 aug.iter()
549 .map(|row| row[n..].to_vec())
550 .collect()
551}
552
553fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
555 mat.iter()
556 .map(|row| row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum())
557 .collect()
558}
559
560#[derive(Debug, Clone)]
566pub struct CdrConfig {
567 pub num_training_circuits: usize,
569 pub seed: u64,
571}
572
573pub fn generate_training_circuits(
580 circuit: &QuantumCircuit,
581 config: &CdrConfig,
582) -> Vec<QuantumCircuit> {
583 let mut circuits = Vec::with_capacity(config.num_training_circuits);
584
585 let mut rng_state = config.seed;
588 let lcg_next = |state: &mut u64| -> u64 {
589 *state = state
590 .wrapping_mul(6364136223846793005)
591 .wrapping_add(1442695040888963407);
592 *state
593 };
594
595 let clifford_1q = |q: u32, choice: u64| -> Gate {
597 match choice % 6 {
598 0 => Gate::H(q),
599 1 => Gate::X(q),
600 2 => Gate::Y(q),
601 3 => Gate::Z(q),
602 4 => Gate::S(q),
603 _ => Gate::Sdg(q),
604 }
605 };
606
607 let clifford_2q = |q1: u32, q2: u32, choice: u64| -> Gate {
609 match choice % 3 {
610 0 => Gate::CNOT(q1, q2),
611 1 => Gate::CZ(q1, q2),
612 _ => Gate::SWAP(q1, q2),
613 }
614 };
615
616 for _ in 0..config.num_training_circuits {
617 let mut training = QuantumCircuit::new(circuit.num_qubits());
618
619 for gate in circuit.gates() {
620 let replacement = match gate {
621 Gate::T(q) | Gate::Tdg(q) => {
623 let r = lcg_next(&mut rng_state);
624 clifford_1q(*q, r)
625 }
626 Gate::Rx(q, _) | Gate::Ry(q, _) | Gate::Rz(q, _) | Gate::Phase(q, _) => {
627 let r = lcg_next(&mut rng_state);
628 clifford_1q(*q, r)
629 }
630 Gate::Unitary1Q(q, _) => {
631 let r = lcg_next(&mut rng_state);
632 clifford_1q(*q, r)
633 }
634
635 Gate::Rzz(q1, q2, _) => {
637 let r = lcg_next(&mut rng_state);
638 clifford_2q(*q1, *q2, r)
639 }
640
641 other => other.clone(),
643 };
644 training.add_gate(replacement);
645 }
646
647 circuits.push(training);
648 }
649
650 circuits
651}
652
653pub fn cdr_correct(noisy_values: &[f64], ideal_values: &[f64], target_noisy: f64) -> f64 {
659 assert_eq!(
660 noisy_values.len(),
661 ideal_values.len(),
662 "noisy_values and ideal_values must have the same length"
663 );
664 let n = noisy_values.len();
665 assert!(n >= 2, "need at least 2 training points for CDR");
666
667 let sum_x: f64 = noisy_values.iter().sum();
673 let sum_y: f64 = ideal_values.iter().sum();
674 let sum_xy: f64 = noisy_values.iter().zip(ideal_values.iter()).map(|(x, y)| x * y).sum();
675 let sum_x2: f64 = noisy_values.iter().map(|x| x * x).sum();
676
677 let n_f64 = n as f64;
678 let denom = n_f64 * sum_x2 - sum_x * sum_x;
679
680 if denom.abs() < 1e-15 {
681 return sum_y / n_f64;
683 }
684
685 let a = (n_f64 * sum_xy - sum_x * sum_y) / denom;
686 let b = (sum_y - a * sum_x) / n_f64;
687
688 a * target_noisy + b
689}
690
691pub fn expectation_from_counts(counts: &HashMap<Vec<bool>, usize>, qubit: u32) -> f64 {
702 let mut total_shots: usize = 0;
703 let mut z_sum: f64 = 0.0;
704
705 for (bits, &count) in counts {
706 total_shots += count;
707 let bit_val = bits.get(qubit as usize).copied().unwrap_or(false);
708 let z_eigenvalue = if bit_val { -1.0 } else { 1.0 };
710 z_sum += z_eigenvalue * count as f64;
711 }
712
713 if total_shots == 0 {
714 return 0.0;
715 }
716
717 z_sum / total_shots as f64
718}
719
720#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::types::Complex;
728
729 #[test]
732 fn test_richardson_recovers_polynomial() {
733 let noise_factors = vec![1.0, 2.0, 3.0];
736 let values: Vec<f64> = noise_factors
737 .iter()
738 .map(|&x| 3.0 * x * x - 2.0 * x + 5.0)
739 .collect();
740
741 let result = richardson_extrapolate(&noise_factors, &values);
742 assert!(
743 (result - 5.0).abs() < 1e-10,
744 "Richardson should recover f(0) = 5.0, got {result}"
745 );
746 }
747
748 #[test]
749 fn test_richardson_linear_data() {
750 let noise_factors = vec![1.0, 2.0];
752 let values = vec![9.0, 11.0];
753 let result = richardson_extrapolate(&noise_factors, &values);
754 assert!(
755 (result - 7.0).abs() < 1e-10,
756 "Richardson on linear data: expected 7.0, got {result}"
757 );
758 }
759
760 #[test]
761 fn test_richardson_cubic() {
762 let noise_factors = vec![1.0, 1.5, 2.0, 3.0];
764 let values: Vec<f64> = noise_factors
765 .iter()
766 .map(|&x| x * x * x - x + 1.0)
767 .collect();
768 let result = richardson_extrapolate(&noise_factors, &values);
769 assert!(
770 (result - 1.0).abs() < 1e-9,
771 "Richardson on cubic data: expected 1.0, got {result}"
772 );
773 }
774
775 #[test]
778 fn test_linear_extrapolation_exact() {
779 let noise_factors = vec![1.0, 2.0, 3.0];
781 let values: Vec<f64> = noise_factors.iter().map(|&x| 3.0 * x + 2.0).collect();
782 let result = linear_extrapolate(&noise_factors, &values);
783 assert!(
784 (result - 2.0).abs() < 1e-10,
785 "Linear extrapolation: expected 2.0, got {result}"
786 );
787 }
788
789 #[test]
790 fn test_linear_extrapolation_two_points() {
791 let noise_factors = vec![1.0, 3.0];
792 let values = vec![5.0, 11.0]; let result = linear_extrapolate(&noise_factors, &values);
794 assert!(
795 (result - 2.0).abs() < 1e-10,
796 "Linear extrapolation with 2 points: expected 2.0, got {result}"
797 );
798 }
799
800 #[test]
803 fn test_polynomial_extrapolation_quadratic() {
804 let noise_factors = vec![1.0, 2.0, 3.0];
806 let values: Vec<f64> = noise_factors.iter().map(|&x| x * x + 1.0).collect();
807 let result = polynomial_extrapolate(&noise_factors, &values, 2);
808 assert!(
809 (result - 1.0).abs() < 1e-10,
810 "Polynomial (degree 2): expected 1.0, got {result}"
811 );
812 }
813
814 #[test]
817 fn test_fold_circuit_factor_1() {
818 let mut circuit = QuantumCircuit::new(2);
820 circuit.h(0);
821 circuit.cnot(0, 1);
822 circuit.measure(0);
823 circuit.measure(1);
824
825 let folded = fold_circuit(&circuit, 1.0);
826
827 assert_eq!(
828 folded.gates().len(),
829 circuit.gates().len(),
830 "fold factor=1 should produce the same number of gates"
831 );
832 }
833
834 #[test]
835 fn test_fold_circuit_factor_3() {
836 let mut circuit = QuantumCircuit::new(2);
840 circuit.h(0);
841 circuit.cnot(0, 1);
842
843 let folded = fold_circuit(&circuit, 3.0);
844
845 let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
847 assert_eq!(
848 unitary_count, 6,
849 "fold factor=3 on 2-gate circuit: expected 6 unitary gates, got {unitary_count}"
850 );
851 }
852
853 #[test]
854 fn test_fold_circuit_factor_3_preserves_measurements() {
855 let mut circuit = QuantumCircuit::new(1);
857 circuit.h(0);
858 circuit.measure(0);
859
860 let folded = fold_circuit(&circuit, 3.0);
861
862 let measure_count = folded
863 .gates()
864 .iter()
865 .filter(|g| matches!(g, Gate::Measure(_)))
866 .count();
867 assert_eq!(
868 measure_count, 1,
869 "measurements should not be folded"
870 );
871
872 let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
873 assert_eq!(
874 unitary_count, 3,
875 "1 H gate folded at factor 3 => 3 unitary gates"
876 );
877 }
878
879 #[test]
880 fn test_fold_circuit_fractional_factor() {
881 let mut circuit = QuantumCircuit::new(2);
885 circuit.h(0);
886 circuit.x(1);
887 circuit.cnot(0, 1);
888 circuit.z(0);
889
890 let folded = fold_circuit(&circuit, 1.5);
891 let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
892 assert_eq!(
893 unitary_count, 6,
894 "fold factor=1.5 on 4-gate circuit: expected 6 unitary gates, got {unitary_count}"
895 );
896 }
897
898 #[test]
901 fn test_measurement_corrector_zero_error_is_identity() {
902 let corrector = MeasurementCorrector::new(&[(0.0, 0.0), (0.0, 0.0)]);
904 let cal = corrector.calibration_matrix();
905
906 let dim = 4; for i in 0..dim {
908 for j in 0..dim {
909 let expected = if i == j { 1.0 } else { 0.0 };
910 assert!(
911 (cal[i][j] - expected).abs() < 1e-12,
912 "cal[{i}][{j}] = {}, expected {expected}",
913 cal[i][j]
914 );
915 }
916 }
917 }
918
919 #[test]
920 fn test_measurement_corrector_single_qubit() {
921 let corrector = MeasurementCorrector::new(&[(0.1, 0.05)]);
924 let cal = corrector.calibration_matrix();
925
926 assert!((cal[0][0] - 0.9).abs() < 1e-12);
927 assert!((cal[0][1] - 0.05).abs() < 1e-12);
928 assert!((cal[1][0] - 0.1).abs() < 1e-12);
929 assert!((cal[1][1] - 0.95).abs() < 1e-12);
930 }
931
932 #[test]
933 fn test_measurement_corrector_correction_identity() {
934 let corrector = MeasurementCorrector::new(&[(0.0, 0.0)]);
936
937 let mut counts = HashMap::new();
938 counts.insert(vec![false], 600);
939 counts.insert(vec![true], 400);
940
941 let corrected = corrector.correct_counts(&counts);
942
943 let c0 = corrected.get(&vec![false]).copied().unwrap_or(0.0);
944 let c1 = corrected.get(&vec![true]).copied().unwrap_or(0.0);
945
946 assert!(
947 (c0 - 600.0).abs() < 1e-6,
948 "expected 600.0 for |0>, got {c0}"
949 );
950 assert!(
951 (c1 - 400.0).abs() < 1e-6,
952 "expected 400.0 for |1>, got {c1}"
953 );
954 }
955
956 #[test]
957 fn test_measurement_corrector_nontrivial_correction() {
958 let corrector = MeasurementCorrector::new(&[(0.1, 0.05)]);
960
961 let mut counts = HashMap::new();
962 counts.insert(vec![false], 550);
963 counts.insert(vec![true], 450);
964
965 let corrected = corrector.correct_counts(&counts);
966 let c0 = corrected.get(&vec![false]).copied().unwrap_or(0.0);
967 let c1 = corrected.get(&vec![true]).copied().unwrap_or(0.0);
968
969 assert!(
972 (c0 + c1 - 1000.0).abs() < 1.0,
973 "total corrected counts should sum to ~1000"
974 );
975 assert!(
977 (c0 - 550.0).abs() > 1.0 || (c1 - 450.0).abs() > 1.0,
978 "correction should change the counts"
979 );
980 }
981
982 #[test]
985 fn test_cdr_correct_known_linear() {
986 let noisy_values = vec![1.0, 2.0, 3.0, 4.0];
989 let ideal_values: Vec<f64> = noisy_values.iter().map(|&x| 2.0 * x - 1.0).collect();
990
991 let result = cdr_correct(&noisy_values, &ideal_values, 3.0);
992 assert!(
993 (result - 5.0).abs() < 1e-10,
994 "CDR correction: expected 5.0, got {result}"
995 );
996 }
997
998 #[test]
999 fn test_cdr_correct_identity_model() {
1000 let noisy_values = vec![1.0, 2.0, 3.0];
1002 let ideal_values = vec![1.0, 2.0, 3.0];
1003
1004 let result = cdr_correct(&noisy_values, &ideal_values, 5.0);
1005 assert!(
1006 (result - 5.0).abs() < 1e-10,
1007 "CDR identity model: expected 5.0, got {result}"
1008 );
1009 }
1010
1011 #[test]
1012 fn test_cdr_correct_offset() {
1013 let noisy_values = vec![0.0, 1.0, 2.0];
1015 let ideal_values = vec![0.5, 1.5, 2.5];
1016
1017 let result = cdr_correct(&noisy_values, &ideal_values, 3.0);
1018 assert!(
1019 (result - 3.5).abs() < 1e-10,
1020 "CDR offset model: expected 3.5, got {result}"
1021 );
1022 }
1023
1024 #[test]
1027 fn test_generate_training_circuits_count() {
1028 let mut circuit = QuantumCircuit::new(2);
1029 circuit.h(0);
1030 circuit.t(0);
1031 circuit.cnot(0, 1);
1032 circuit.rx(1, 0.5);
1033
1034 let config = CdrConfig {
1035 num_training_circuits: 10,
1036 seed: 42,
1037 };
1038
1039 let training = generate_training_circuits(&circuit, &config);
1040 assert_eq!(training.len(), 10);
1041 }
1042
1043 #[test]
1044 fn test_generate_training_circuits_preserves_clifford_gates() {
1045 let mut circuit = QuantumCircuit::new(2);
1046 circuit.h(0);
1047 circuit.cnot(0, 1);
1048 circuit.x(1);
1049
1050 let config = CdrConfig {
1051 num_training_circuits: 5,
1052 seed: 0,
1053 };
1054
1055 let training = generate_training_circuits(&circuit, &config);
1056
1057 for tc in &training {
1060 assert_eq!(
1061 tc.gates().len(),
1062 circuit.gates().len(),
1063 "training circuit should have same gate count"
1064 );
1065 }
1066 }
1067
1068 #[test]
1069 fn test_generate_training_circuits_replaces_non_clifford() {
1070 let mut circuit = QuantumCircuit::new(1);
1071 circuit.t(0); let config = CdrConfig {
1074 num_training_circuits: 20,
1075 seed: 123,
1076 };
1077
1078 let training = generate_training_circuits(&circuit, &config);
1079
1080 for tc in &training {
1082 for gate in tc.gates() {
1083 assert!(
1084 !matches!(gate, Gate::T(_)),
1085 "training circuit should not contain T gate"
1086 );
1087 }
1088 }
1089 }
1090
1091 #[test]
1092 fn test_generate_training_circuits_deterministic() {
1093 let mut circuit = QuantumCircuit::new(1);
1094 circuit.rx(0, 1.0);
1095 circuit.t(0);
1096
1097 let config = CdrConfig {
1098 num_training_circuits: 5,
1099 seed: 42,
1100 };
1101
1102 let training1 = generate_training_circuits(&circuit, &config);
1103 let training2 = generate_training_circuits(&circuit, &config);
1104
1105 assert_eq!(training1.len(), training2.len());
1108 for (t1, t2) in training1.iter().zip(training2.iter()) {
1109 assert_eq!(t1.gates().len(), t2.gates().len());
1110 }
1111 }
1112
1113 #[test]
1116 fn test_expectation_all_zero() {
1117 let mut counts = HashMap::new();
1119 counts.insert(vec![false], 1000);
1120
1121 let exp = expectation_from_counts(&counts, 0);
1122 assert!(
1123 (exp - 1.0).abs() < 1e-12,
1124 "all |0>: expected <Z> = 1.0, got {exp}"
1125 );
1126 }
1127
1128 #[test]
1129 fn test_expectation_all_one() {
1130 let mut counts = HashMap::new();
1132 counts.insert(vec![true], 500);
1133
1134 let exp = expectation_from_counts(&counts, 0);
1135 assert!(
1136 (exp - (-1.0)).abs() < 1e-12,
1137 "all |1>: expected <Z> = -1.0, got {exp}"
1138 );
1139 }
1140
1141 #[test]
1142 fn test_expectation_equal_split() {
1143 let mut counts = HashMap::new();
1145 counts.insert(vec![false], 500);
1146 counts.insert(vec![true], 500);
1147
1148 let exp = expectation_from_counts(&counts, 0);
1149 assert!(
1150 exp.abs() < 1e-12,
1151 "equal split: expected <Z> = 0.0, got {exp}"
1152 );
1153 }
1154
1155 #[test]
1156 fn test_expectation_multi_qubit() {
1157 let mut counts = HashMap::new();
1163 counts.insert(vec![false, false], 300);
1164 counts.insert(vec![true, false], 200);
1165 counts.insert(vec![false, true], 100);
1166 counts.insert(vec![true, true], 400);
1167
1168 let exp0 = expectation_from_counts(&counts, 0);
1169 let exp1 = expectation_from_counts(&counts, 1);
1170
1171 assert!(
1172 (exp0 - (-0.2)).abs() < 1e-12,
1173 "qubit 0: expected -0.2, got {exp0}"
1174 );
1175 assert!(
1176 exp1.abs() < 1e-12,
1177 "qubit 1: expected 0.0, got {exp1}"
1178 );
1179 }
1180
1181 #[test]
1182 fn test_expectation_empty_counts() {
1183 let counts: HashMap<Vec<bool>, usize> = HashMap::new();
1184 let exp = expectation_from_counts(&counts, 0);
1185 assert!(
1186 exp.abs() < 1e-12,
1187 "empty counts should give 0.0, got {exp}"
1188 );
1189 }
1190
1191 #[test]
1194 fn test_gate_dagger_self_inverse() {
1195 let gates = vec![Gate::H(0), Gate::X(0), Gate::Y(0), Gate::Z(0)];
1197 for gate in &gates {
1198 let dag = gate_dagger(gate);
1199 if let (Some(m_orig), Some(m_dag)) = (gate.matrix_1q(), dag.matrix_1q()) {
1202 for i in 0..2 {
1203 for j in 0..2 {
1204 let diff = (m_orig[i][j] - m_dag[i][j]).norm();
1205 assert!(
1206 diff < 1e-12,
1207 "gate_dagger of self-inverse gate should match: diff = {diff}"
1208 );
1209 }
1210 }
1211 }
1212 }
1213 }
1214
1215 #[test]
1216 fn test_gate_dagger_s_sdg() {
1217 let s_dag = gate_dagger(&Gate::S(0));
1219 let sdg = Gate::Sdg(0);
1220
1221 let m1 = s_dag.matrix_1q().unwrap();
1222 let m2 = sdg.matrix_1q().unwrap();
1223
1224 for i in 0..2 {
1225 for j in 0..2 {
1226 let diff = (m1[i][j] - m2[i][j]).norm();
1227 assert!(diff < 1e-12, "S dagger should equal Sdg");
1228 }
1229 }
1230 }
1231
1232 #[test]
1233 fn test_gate_dagger_rotation_inverse() {
1234 let theta = 1.23;
1236 let rx = Gate::Rx(0, theta);
1237 let rx_dag = gate_dagger(&rx);
1238
1239 let m = rx.matrix_1q().unwrap();
1240 let m_dag = rx_dag.matrix_1q().unwrap();
1241
1242 let product = mat_mul_2x2(&m, &m_dag);
1244 for i in 0..2 {
1245 for j in 0..2 {
1246 let expected = if i == j {
1247 Complex::ONE
1248 } else {
1249 Complex::ZERO
1250 };
1251 let diff = (product[i][j] - expected).norm();
1252 assert!(
1253 diff < 1e-12,
1254 "Rx * Rx^dag should be identity at [{i}][{j}]: diff = {diff}"
1255 );
1256 }
1257 }
1258 }
1259
1260 fn mat_mul_2x2(
1262 a: &[[Complex; 2]; 2],
1263 b: &[[Complex; 2]; 2],
1264 ) -> [[Complex; 2]; 2] {
1265 let mut result = [[Complex::ZERO; 2]; 2];
1266 for i in 0..2 {
1267 for j in 0..2 {
1268 for k in 0..2 {
1269 result[i][j] = result[i][j] + a[i][k] * b[k][j];
1270 }
1271 }
1272 }
1273 result
1274 }
1275}