Skip to main content

ruqu_core/
mitigation.rs

1//! Error mitigation pipeline for quantum circuits.
2//!
3//! Implements three established mitigation strategies:
4//!
5//! * **Zero-Noise Extrapolation (ZNE)** -- amplify noise by circuit folding, then
6//!   extrapolate back to the zero-noise limit.
7//! * **Measurement Error Mitigation** -- correct readout errors via calibration
8//!   matrices built from per-qubit `(p01, p10)` error rates.
9//! * **Clifford Data Regression (CDR)** -- learn a linear correction model by
10//!   comparing noisy and ideal results on near-Clifford training circuits.
11
12use crate::circuit::QuantumCircuit;
13use crate::gate::Gate;
14use std::collections::HashMap;
15
16// ============================================================================
17// 1. Zero-Noise Extrapolation (ZNE)
18// ============================================================================
19
20/// Configuration for Zero-Noise Extrapolation.
21#[derive(Debug, Clone)]
22pub struct ZneConfig {
23    /// Noise scaling factors to sample (must include 1.0 as the baseline).
24    pub noise_factors: Vec<f64>,
25    /// Method used to extrapolate to the zero-noise limit.
26    pub extrapolation: ExtrapolationMethod,
27}
28
29/// Extrapolation method for ZNE.
30#[derive(Debug, Clone)]
31pub enum ExtrapolationMethod {
32    /// Simple linear fit through all data points.
33    Linear,
34    /// Polynomial fit of the given degree via least-squares.
35    Polynomial(usize),
36    /// Richardson extrapolation (exact for polynomials of degree n-1 where n
37    /// is the number of data points).
38    Richardson,
39}
40
41/// Fold a quantum circuit to amplify noise by the given `factor`.
42///
43/// Gate folding replaces each unitary gate G with the sequence G (G^dag G)^k
44/// where k is determined by the noise factor.
45///
46/// * For integer factors (e.g. 3), every non-measurement gate G becomes
47///   G G^dag G (i.e. one extra G^dag G pair).
48/// * For fractional factors (e.g. 1.5 on a 4-gate circuit), a prefix of
49///   gates are folded so the total gate count matches the target.
50///
51/// Non-unitary operations (Measure, Reset, Barrier) are never folded.
52pub 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    // Collect indices of unitary (foldable) gates.
59    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    // Total number of unitary gate slots after folding. Each fold adds 2 gates
69    // (G^dag G), so total = n_unitary * factor, rounded to the nearest integer.
70    let target_unitary_slots = (n_unitary as f64 * factor).round() as usize;
71
72    // Each folded gate occupies 3 slots (G G^dag G), unfolded occupies 1.
73    // If we fold k gates: total = k * 3 + (n_unitary - k) = 2k + n_unitary
74    // => k = (target_unitary_slots - n_unitary) / 2
75    let num_folds = if target_unitary_slots > n_unitary {
76        (target_unitary_slots - n_unitary) / 2
77    } else {
78        0
79    };
80
81    // Determine how many full folding rounds per gate, and how many extra gates
82    // get one additional round.
83    let full_rounds = num_folds / n_unitary.max(1);
84    let extra_folds = num_folds % n_unitary.max(1);
85
86    // Build a set of unitary-gate indices that get the extra fold.
87    // We fold the first `extra_folds` unitary gates one additional time.
88    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        // This is a unitary gate. Determine how many fold rounds it gets.
97        let rounds = full_rounds + if unitary_counter < extra_folds { 1 } else { 0 };
98        unitary_counter += 1;
99
100        // Original gate.
101        folded.add_gate(gate.clone());
102
103        // Append (G^dag G) `rounds` times.
104        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
114/// Compute the conjugate transpose (dagger) of a gate.
115///
116/// For single-qubit gates with known matrix U, we compute U^dag by conjugating
117/// and transposing the 2x2 matrix. For two-qubit gates, the dagger is computed
118/// from the known structure.
119fn gate_dagger(gate: &Gate) -> Gate {
120    match gate {
121        // Self-inverse gates: H, X, Y, Z, CNOT, CZ, SWAP, Barrier.
122        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        // S^dag = Sdg, Sdg^dag = S.
131        Gate::S(q) => Gate::Sdg(*q),
132        Gate::Sdg(q) => Gate::S(*q),
133
134        // T^dag = Tdg, Tdg^dag = T.
135        Gate::T(q) => Gate::Tdg(*q),
136        Gate::Tdg(q) => Gate::T(*q),
137
138        // Rotation gates: dagger negates the angle.
139        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        // Custom unitary: conjugate transpose of the 2x2 matrix.
146        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        // Non-unitary ops should not reach here, but handle gracefully.
155        Gate::Measure(q) => Gate::Measure(*q),
156        Gate::Reset(q) => Gate::Reset(*q),
157        Gate::Barrier => Gate::Barrier,
158    }
159}
160
161/// Richardson extrapolation to the zero-noise limit.
162///
163/// Given n data points `(noise_factors[i], values[i])`, the Richardson
164/// extrapolation computes the unique polynomial of degree n-1 that passes
165/// through all points, then evaluates it at x = 0. This is equivalent to
166/// the Lagrange interpolation formula evaluated at zero.
167pub 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    // Lagrange interpolation at x = 0:
177    //   P(0) = sum_i  values[i] * product_{j != i} (0 - x_j) / (x_i - x_j)
178    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                // (0 - x_j) / (x_i - x_j)
184                weight *= -noise_factors[j] / (noise_factors[i] - noise_factors[j]);
185            }
186        }
187        result += values[i] * weight;
188    }
189    result
190}
191
192/// Polynomial extrapolation via least-squares fit.
193///
194/// Fits a polynomial of the specified `degree` to the data, then evaluates
195/// at x = 0 (returning the constant term of the fit).
196pub 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; // number of coefficients
204    assert!(n >= p, "need at least degree+1 data points for a degree-{degree} polynomial");
205
206    // Build the Vandermonde matrix A (n x p) where A[i][j] = x_i^j.
207    // Then solve A^T A c = A^T y via normal equations.
208    // Since we only need c[0] (the value at x=0), we solve the full system.
209
210    // A^T A  (p x p)
211    let mut ata = vec![vec![0.0_f64; p]; p];
212    // A^T y  (p x 1)
213    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        // Precompute powers of x up to 2 * degree.
220        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    // Solve p x p linear system via Gaussian elimination with partial pivoting.
236    let coeffs = solve_linear_system(&mut ata, &mut aty);
237
238    // The value at x = 0 is simply c[0].
239    coeffs[0]
240}
241
242/// Linear extrapolation to x = 0.
243///
244/// Fits y = a*x + b via least-squares and returns b (the y-intercept).
245pub fn linear_extrapolate(noise_factors: &[f64], values: &[f64]) -> f64 {
246    polynomial_extrapolate(noise_factors, values, 1)
247}
248
249/// Solve a dense linear system Ax = b using Gaussian elimination with partial
250/// pivoting. Modifies `a` and `b` in place. Returns the solution vector.
251fn 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    // Forward elimination with partial pivoting.
256    for col in 0..n {
257        // Find pivot.
258        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        // Swap rows.
269        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        // Eliminate below.
281        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    // Back substitution.
291    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// ============================================================================
304// 2. Measurement Error Mitigation
305// ============================================================================
306
307/// Corrects readout errors using a full calibration matrix built from
308/// per-qubit error probabilities.
309#[derive(Debug, Clone)]
310pub struct MeasurementCorrector {
311    num_qubits: usize,
312    /// Row-major 2^n x 2^n calibration matrix. Entry `[i][j]` is the
313    /// probability of observing bitstring `i` when the true state is `j`.
314    calibration_matrix: Vec<Vec<f64>>,
315}
316
317impl MeasurementCorrector {
318    /// Build the calibration matrix from per-qubit readout errors.
319    ///
320    /// `readout_errors[q] = (p01, p10)` where:
321    /// * `p01` = probability of reading 1 when the true state is 0
322    /// * `p10` = probability of reading 0 when the true state is 1
323    ///
324    /// The full calibration matrix is the tensor product of the individual
325    /// 2x2 matrices:
326    ///   M_q = [[1 - p01, p10],
327    ///          [p01,     1 - p10]]
328    pub fn new(readout_errors: &[(f64, f64)]) -> Self {
329        let num_qubits = readout_errors.len();
330        let dim = 1usize << num_qubits;
331
332        // Build per-qubit 2x2 matrices.
333        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        // Tensor product to build the full dim x dim matrix.
344        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    /// Correct measurement counts by applying the inverse of the calibration
364    /// matrix.
365    ///
366    /// For small qubit counts (<= 12), the full matrix is inverted directly.
367    /// For larger systems, the tensor product structure is exploited for
368    /// efficient correction.
369    ///
370    /// Returns corrected counts as floating-point values since the inverse
371    /// may produce non-integer results.
372    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        // Build the probability vector from counts.
379        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        // Invert and apply.
389        let corrected_probs = if self.num_qubits <= 12 {
390            // Direct matrix inversion for small systems.
391            let inv = invert_matrix(&self.calibration_matrix);
392            mat_vec_mul(&inv, &prob_vec)
393        } else {
394            // Exploit tensor product structure for large systems.
395            // The inverse of A tensor B = A^-1 tensor B^-1.
396            // Apply the per-qubit inverse matrices sequentially.
397            self.tensor_product_correct(&prob_vec)
398        };
399
400        // Convert back to counts (scaled by total shots).
401        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    /// Accessor for the calibration matrix.
414    pub fn calibration_matrix(&self) -> &Vec<Vec<f64>> {
415        &self.calibration_matrix
416    }
417
418    /// Apply per-qubit inverse correction using tensor product structure.
419    ///
420    /// This avoids building and inverting the full 2^n x 2^n matrix by
421    /// applying each qubit's 2x2 inverse separately in sequence.
422    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        // Extract per-qubit 2x2 matrices from the calibration matrix and invert.
427        for q in 0..self.num_qubits {
428            // Re-derive per-qubit matrix from the calibration matrix structure.
429            // For qubit q, the 2x2 submatrix is extracted by looking at how
430            // bit q affects the matrix entry.
431            let qubit_mat = self.extract_qubit_matrix(q);
432            let inv = invert_2x2(&qubit_mat);
433
434            // Apply the 2x2 inverse along the q-th qubit axis.
435            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    /// Extract the 2x2 calibration matrix for a single qubit from the full
452    /// calibration matrix.
453    fn extract_qubit_matrix(&self, qubit: usize) -> [[f64; 2]; 2] {
454        // The per-qubit matrix is encoded in the tensor product structure.
455        // To extract qubit q's matrix, look at a pair of indices that differ
456        // only in bit q. The simplest choice: indices 0 and (1 << q).
457        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
467/// Convert a bit vector to an integer index.
468fn 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
478/// Convert an integer index back to a bit vector.
479fn index_to_bits(idx: usize, num_qubits: usize) -> Vec<bool> {
480    (0..num_qubits).map(|q| (idx >> q) & 1 == 1).collect()
481}
482
483/// Invert a 2x2 matrix.
484fn 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
494/// Invert a square matrix via Gauss-Jordan elimination with partial pivoting.
495fn invert_matrix(mat: &[Vec<f64>]) -> Vec<Vec<f64>> {
496    let n = mat.len();
497    // Augmented matrix [A | I].
498    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    // Forward elimination.
510    for col in 0..n {
511        // Partial pivoting.
512        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        // Scale pivot row.
530        let inv_pivot = 1.0 / pivot;
531        for k in 0..(2 * n) {
532            aug[col][k] *= inv_pivot;
533        }
534
535        // Eliminate all other rows.
536        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    // Extract the right half as the inverse.
548    aug.iter()
549        .map(|row| row[n..].to_vec())
550        .collect()
551}
552
553/// Multiply a matrix by a vector.
554fn 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// ============================================================================
561// 3. Clifford Data Regression (CDR)
562// ============================================================================
563
564/// Configuration for Clifford Data Regression.
565#[derive(Debug, Clone)]
566pub struct CdrConfig {
567    /// Number of near-Clifford training circuits to generate.
568    pub num_training_circuits: usize,
569    /// Seed for the random replacement of non-Clifford gates.
570    pub seed: u64,
571}
572
573/// Generate near-Clifford training circuits from the original circuit.
574///
575/// Each training circuit is a copy of the original where non-Clifford gates
576/// (T, Tdg, Rx, Ry, Rz, Phase, Rzz) are replaced with random Clifford
577/// gates acting on the same qubits. The resulting circuits are efficiently
578/// simulable by a stabilizer backend.
579pub 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    // Simple LCG-based deterministic RNG (no external dependency needed for
586    // training circuit generation; keeps this module self-contained).
587    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    // Clifford single-qubit replacements.
596    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    // Clifford two-qubit replacements.
608    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                // Non-Clifford single-qubit gates: replace with random Clifford.
622                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                // Non-Clifford two-qubit gates: replace with random Clifford.
636                Gate::Rzz(q1, q2, _) => {
637                    let r = lcg_next(&mut rng_state);
638                    clifford_2q(*q1, *q2, r)
639                }
640
641                // Clifford and non-unitary gates: keep as-is.
642                other => other.clone(),
643            };
644            training.add_gate(replacement);
645        }
646
647        circuits.push(training);
648    }
649
650    circuits
651}
652
653/// Apply Clifford Data Regression correction to a target noisy expectation value.
654///
655/// Given pairs `(noisy_values[i], ideal_values[i])` from the training circuits,
656/// fits the linear model `ideal = a * noisy + b` via least-squares and applies
657/// the same transformation to `target_noisy`.
658pub 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    // Least-squares linear regression: ideal = a * noisy + b
668    //
669    // a = (n * sum(x*y) - sum(x) * sum(y)) / (n * sum(x^2) - (sum(x))^2)
670    // b = (sum(y) - a * sum(x)) / n
671
672    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        // All noisy values are the same; return the mean ideal value.
682        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
691// ============================================================================
692// 4. Helpers
693// ============================================================================
694
695/// Compute the Z-basis expectation value `<Z>` for a single qubit from
696/// shot counts.
697///
698/// For each bitstring, if the qubit is in state 0, it contributes +1;
699/// if in state 1, it contributes -1. The expectation is the weighted
700/// average over all shots.
701pub 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        // |0> -> +1, |1> -> -1
709        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// ============================================================================
721// Tests
722// ============================================================================
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727    use crate::types::Complex;
728
729    // ---- Richardson extrapolation ----------------------------------------
730
731    #[test]
732    fn test_richardson_recovers_polynomial() {
733        // For a quadratic f(x) = 3x^2 - 2x + 5, three data points should
734        // recover f(0) = 5 exactly via Richardson (degree-2 interpolation).
735        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        // f(x) = 2x + 7 => f(0) = 7
751        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        // f(x) = x^3 - x + 1 => f(0) = 1
763        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    // ---- Linear extrapolation -------------------------------------------
776
777    #[test]
778    fn test_linear_extrapolation_exact() {
779        // y = 3x + 2 => y(0) = 2
780        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]; // slope = 3, intercept = 2
793        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    // ---- Polynomial extrapolation ---------------------------------------
801
802    #[test]
803    fn test_polynomial_extrapolation_quadratic() {
804        // f(x) = x^2 + 1 => f(0) = 1
805        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    // ---- Fold circuit ---------------------------------------------------
815
816    #[test]
817    fn test_fold_circuit_factor_1() {
818        // factor = 1.0 should return a circuit with the same gates.
819        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        // factor = 3 should triple each unitary gate: G G^dag G.
837        // Original: H, CNOT (2 unitary gates).
838        // Folded:   H H^dag H, CNOT CNOT^dag CNOT (6 unitary gates).
839        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        // 2 unitary gates * factor 3 = 6 gate slots.
846        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        // Measurements should pass through unchanged.
856        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        // factor = 1.5 on 4 unitary gates.
882        // target slots = round(4 * 1.5) = 6, so num_folds = (6 - 4) / 2 = 1.
883        // One gate gets folded (3 slots), three remain (1 slot each) = 6 total.
884        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    // ---- MeasurementCorrector -------------------------------------------
899
900    #[test]
901    fn test_measurement_corrector_zero_error_is_identity() {
902        // With no readout errors, the calibration matrix should be the identity.
903        let corrector = MeasurementCorrector::new(&[(0.0, 0.0), (0.0, 0.0)]);
904        let cal = corrector.calibration_matrix();
905
906        let dim = 4; // 2 qubits -> 2^2 = 4
907        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        // Single qubit with p01 = 0.1, p10 = 0.05.
922        // M = [[0.9, 0.05], [0.1, 0.95]]
923        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        // With zero errors, correction should return the same probabilities.
935        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        // With errors, the corrected counts should differ from raw counts.
959        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        // The correction should shift counts toward the true distribution.
970        // M^{-1} applied to [0.55, 0.45]^T should yield something different.
971        assert!(
972            (c0 + c1 - 1000.0).abs() < 1.0,
973            "total corrected counts should sum to ~1000"
974        );
975        // Just verify it actually changed.
976        assert!(
977            (c0 - 550.0).abs() > 1.0 || (c1 - 450.0).abs() > 1.0,
978            "correction should change the counts"
979        );
980    }
981
982    // ---- CDR linear regression ------------------------------------------
983
984    #[test]
985    fn test_cdr_correct_known_linear() {
986        // If ideal = 2 * noisy - 1, then for target_noisy = 3.0:
987        //   corrected = 2 * 3.0 - 1 = 5.0
988        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        // If ideal == noisy, correction should return target_noisy unchanged.
1001        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        // ideal = noisy + 0.5
1014        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    // ---- Generate training circuits -------------------------------------
1025
1026    #[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        // All gates in the original are Clifford, so training circuits should
1058        // have the same number of gates.
1059        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); // non-Clifford
1072
1073        let config = CdrConfig {
1074            num_training_circuits: 20,
1075            seed: 123,
1076        };
1077
1078        let training = generate_training_circuits(&circuit, &config);
1079
1080        // None of the training circuits should contain a T gate.
1081        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        // Same seed should produce the same number of circuits with the same
1106        // gate counts.
1107        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    // ---- expectation_from_counts ----------------------------------------
1114
1115    #[test]
1116    fn test_expectation_all_zero() {
1117        // All shots yield |0> => <Z> = +1.0
1118        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        // All shots yield |1> => <Z> = -1.0
1131        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        // 50/50 split => <Z> = 0
1144        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        // 2 qubits: |00> x 300, |01> x 200, |10> x 100, |11> x 400
1158        // For qubit 0: |0> appears in |00> + |10> = 400, |1> in |01> + |11> = 600
1159        //   <Z_0> = (400 - 600) / 1000 = -0.2
1160        // For qubit 1: |0> appears in |00> + |01> = 500, |1> in |10> + |11> = 500
1161        //   <Z_1> = (500 - 500) / 1000 = 0.0
1162        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    // ---- Gate dagger correctness ----------------------------------------
1192
1193    #[test]
1194    fn test_gate_dagger_self_inverse() {
1195        // H, X, Y, Z are their own inverses.
1196        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            // For self-inverse gates, the matrix of the dagger should equal
1200            // the matrix of the original.
1201            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        // S^dag = Sdg, so matrix of S^dag should equal matrix of Sdg.
1218        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        // Rx(theta)^dag = Rx(-theta). Product should be identity.
1235        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        // Product m * m_dag should be identity.
1243        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    /// Helper: multiply two 2x2 complex matrices.
1261    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}