Skip to main content

cqlib_core/error_mitigation/
zne_mitigation.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13use crate::circuit::{Circuit, CircuitError, Instruction, Operation, Parameter};
14use crate::error_mitigation::ErrorMitigationError;
15use crate::error_mitigation::Estimator;
16use crate::qis::Hamiltonian;
17use std::collections::HashSet;
18
19/// Extrapolation methods supported by [`ZNEMitigation::extrapolate`].
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum ExtrapolateMethod {
22    Polynomial,
23    Exponential,
24}
25
26/// Zero-noise extrapolation (ZNE) mitigation helper.
27///
28/// This mirrors the Python `ZNEMitigation` data model and currently implements
29/// only circuit folding.
30///
31/// # Example
32///
33/// ```rust
34/// use cqlib_core::circuit::gate::{Instruction, StandardGate};
35/// use cqlib_core::circuit::{Circuit, Qubit};
36/// use cqlib_core::error_mitigation::ZNEMitigation;
37///
38/// let q0 = Qubit::new(0);
39/// let q1 = Qubit::new(1);
40///
41/// let mut circuit = Circuit::new(2);
42/// circuit.h(q0).unwrap();
43/// circuit.cx(q0, q1).unwrap();
44///
45/// // Build ZNE with fold levels [0, 1, 2], noise factors [1, 3, 5].
46/// let zne = ZNEMitigation::new(circuit, vec![0, 1, 2]);
47///
48/// // Global folding for each level.
49/// let folded_all = zne.fold_circuits(None).unwrap();
50/// assert_eq!(folded_all.len(), 3);
51///
52/// // Selective folding for H only.
53/// let folded_h_only = zne
54///     .fold_circuits(Some(&[Instruction::Standard(StandardGate::H)]))
55///     .unwrap();
56/// assert_eq!(folded_h_only.len(), 3);
57/// ```
58#[derive(Debug, Clone)]
59pub struct ZNEMitigation {
60    circuit: Circuit,
61    fold_levels: Vec<i32>,
62    noise_factors: Vec<i32>,
63}
64
65impl ZNEMitigation {
66    /// Creates a new ZNE mitigation helper.
67    ///
68    /// `noise_factors` follow the Python implementation: `2 * level + 1`.
69    pub fn new(circuit: Circuit, fold_levels: Vec<i32>) -> Self {
70        let noise_factors = fold_levels.iter().map(|level| 2 * level + 1).collect();
71        Self {
72            circuit,
73            fold_levels,
74            noise_factors,
75        }
76    }
77
78    /// Returns the original (unfolded) circuit.
79    pub fn circuit(&self) -> &Circuit {
80        &self.circuit
81    }
82
83    /// Returns configured fold levels.
84    pub fn fold_levels(&self) -> &[i32] {
85        &self.fold_levels
86    }
87
88    /// Returns noise factors corresponding to fold levels.
89    ///
90    /// Each factor is `2 * level + 1` due to unitary folding.
91    pub fn noise_factors(&self) -> &[i32] {
92        &self.noise_factors
93    }
94
95    /// Fold the circuit for each configured level using unitary folding.
96    ///
97    /// If `gate_set` is `None`, this performs global folding:
98    /// `U -> U (U^† U)^level`.
99    ///
100    /// If `gate_set` is provided, only operations whose instruction name matches
101    /// one of the instruction names in `gate_set` are folded.
102    pub fn fold_circuits(
103        &self,
104        gate_set: Option<&[Instruction]>,
105    ) -> Result<Vec<Circuit>, CircuitError> {
106        self.fold_levels
107            .iter()
108            .map(|level| self.fold_to_level(*level, gate_set))
109            .collect()
110    }
111
112    /// Run the error-mitigation sequence and return expectation-value estimates.
113    ///
114    /// This method folds the circuit at each configured level and computes one
115    /// expectation value per folded circuit.
116    ///
117    /// - `gate_set`: optional selective gate set for folding. `None` means global folding.
118    /// - `hamiltonian`: a `qis::Hamiltonian` describing the observable to estimate.
119    /// - `estimator`: a shared estimator that receives the folded circuit,
120    ///   `Some(hamiltonian)`, and an optional shot count.
121    ///   `run_em_sequence` currently passes `None` for the shot count.
122    pub fn run_em_sequence(
123        &self,
124        gate_set: Option<&[Instruction]>,
125        hamiltonian: &Hamiltonian,
126        estimator: &Estimator<'_>,
127    ) -> Result<Vec<f64>, ErrorMitigationError> {
128        self.run_em_sequence_with_shots(gate_set, hamiltonian, None, estimator)
129    }
130
131    /// Run the error-mitigation sequence with an optional shot count.
132    pub fn run_em_sequence_with_shots(
133        &self,
134        gate_set: Option<&[Instruction]>,
135        hamiltonian: &Hamiltonian,
136        shots: Option<usize>,
137        estimator: &Estimator<'_>,
138    ) -> Result<Vec<f64>, ErrorMitigationError> {
139        if hamiltonian.num_qubits != self.circuit.width() {
140            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
141                expected: self.circuit.width(),
142                actual: hamiltonian.num_qubits,
143            });
144        }
145
146        let mut hexp_seq = Vec::new();
147        for level in self.fold_levels() {
148            let circuit = self.fold_to_level(*level, gate_set)?;
149            let (expectation, _variance) = estimator(&circuit, Some(hamiltonian), shots);
150            hexp_seq.push(expectation);
151        }
152        Ok(hexp_seq)
153    }
154
155    /// Unified extrapolation API.
156    ///
157    /// - `method = ExtrapolateMethod::Polynomial`: uses `degree` and delegates
158    ///   to [`ZNEMitigation::poly_extrapolate`].
159    /// - `method = ExtrapolateMethod::Exponential`: ignores `degree` and
160    ///   delegates to [`ZNEMitigation::exp_extrapolate`].
161    pub fn extrapolate(
162        &self,
163        noisy_results: &[f64],
164        method: ExtrapolateMethod,
165        degree: usize,
166    ) -> Result<f64, ErrorMitigationError> {
167        match method {
168            ExtrapolateMethod::Polynomial => self.poly_extrapolate(noisy_results, degree),
169            ExtrapolateMethod::Exponential => self.exp_extrapolate(noisy_results),
170        }
171    }
172
173    /// Given the noisy results, extrapolate the expectation value using a polynomial fit.
174    ///
175    /// - `noisy_results`: the noisy results to extrapolate.
176    /// - `degree`: the degree of the polynomial to use for extrapolation.
177    ///
178    /// Returns the extrapolated expectation value.
179    pub fn poly_extrapolate(
180        &self,
181        noisy_results: &[f64],
182        degree: usize,
183    ) -> Result<f64, ErrorMitigationError> {
184        let n = self.noise_factors.len();
185        self.validate_noisy_results(noisy_results)?;
186        if degree >= n {
187            return Err(ErrorMitigationError::InvalidPolynomialDegree {
188                degree,
189                num_points: n,
190            });
191        }
192
193        let d = degree + 1;
194        let x: Vec<f64> = self.noise_factors.iter().map(|&v| v as f64).collect();
195        let y = noisy_results;
196
197        // Build normal equations: (V^T V) c = V^T y, where
198        // V[i, j] = x_i^j and c stores coefficients in ascending order.
199        let mut a = vec![vec![0.0; d]; d];
200        let mut b = vec![0.0; d];
201        for row in 0..n {
202            let mut x_pows = vec![1.0; d];
203            for j in 1..d {
204                x_pows[j] = x_pows[j - 1] * x[row];
205            }
206
207            for j in 0..d {
208                b[j] += y[row] * x_pows[j];
209                for k in 0..d {
210                    a[j][k] += x_pows[j] * x_pows[k];
211                }
212            }
213        }
214
215        let coeffs = Self::solve_linear_system(a, b)?;
216        Ok(coeffs[0])
217    }
218
219    /// Given the noisy results, extrapolate the expectation value using an
220    /// exponential-decay model:
221    ///
222    /// `y(x) = A * exp(-x / tau)`.
223    ///
224    /// The fit is performed in log-space by linear regression on:
225    ///
226    /// `ln(y) = ln(A) + m * x`, where `m = -1 / tau`.
227    ///
228    /// Returns `A`, which is the extrapolated value at `x = 0`.
229    pub fn exp_extrapolate(&self, noisy_results: &[f64]) -> Result<f64, ErrorMitigationError> {
230        let n = self.noise_factors.len();
231        self.validate_noisy_results(noisy_results)?;
232        if !noisy_results.iter().all(|&v| v > 0.0) {
233            return Err(ErrorMitigationError::NonPositiveNoisyResults);
234        }
235
236        let x: Vec<f64> = self.noise_factors.iter().map(|&v| v as f64).collect();
237        let y_log: Vec<f64> = noisy_results.iter().map(|&v| v.ln()).collect();
238
239        let n_f = n as f64;
240        let sum_x: f64 = x.iter().sum();
241        let sum_y: f64 = y_log.iter().sum();
242        let sum_xx: f64 = x.iter().map(|v| v * v).sum();
243        let sum_xy: f64 = x.iter().zip(y_log.iter()).map(|(xi, yi)| xi * yi).sum();
244
245        let denom = n_f * sum_xx - sum_x * sum_x;
246        if denom.abs() <= 1e-14 {
247            return Err(ErrorMitigationError::SingularExponentialFit);
248        }
249
250        let slope = (n_f * sum_xy - sum_x * sum_y) / denom;
251        let intercept = (sum_y - slope * sum_x) / n_f;
252        Ok(intercept.exp())
253    }
254
255    fn solve_linear_system(
256        mut a: Vec<Vec<f64>>,
257        mut b: Vec<f64>,
258    ) -> Result<Vec<f64>, ErrorMitigationError> {
259        let n = a.len();
260        debug_assert!(n > 0, "Coefficient matrix must not be empty.");
261        debug_assert_eq!(b.len(), n, "Right-hand side length must match matrix size.");
262        for row in &a {
263            debug_assert_eq!(row.len(), n, "Coefficient matrix must be square.");
264        }
265
266        let eps = 1e-14_f64;
267
268        for i in 0..n {
269            // Partial pivoting for numerical stability.
270            let mut pivot_row = i;
271            let mut pivot_abs = a[i][i].abs();
272
273            #[allow(clippy::needless_range_loop)]
274            for r in (i + 1)..n {
275                let cand = a[r][i].abs();
276                if cand > pivot_abs {
277                    pivot_abs = cand;
278                    pivot_row = r;
279                }
280            }
281
282            if pivot_abs <= eps {
283                return Err(ErrorMitigationError::SingularPolynomialFit);
284            }
285
286            if pivot_row != i {
287                a.swap(i, pivot_row);
288                b.swap(i, pivot_row);
289            }
290
291            for r in (i + 1)..n {
292                let factor = a[r][i] / a[i][i];
293                a[r][i] = 0.0;
294                #[allow(clippy::needless_range_loop)]
295                for c in (i + 1)..n {
296                    a[r][c] -= factor * a[i][c];
297                }
298                b[r] -= factor * b[i];
299            }
300        }
301
302        let mut x = vec![0.0; n];
303        for i in (0..n).rev() {
304            let mut sum = b[i];
305            for (j, xj) in x.iter().enumerate().skip(i + 1) {
306                sum -= a[i][j] * *xj;
307            }
308            x[i] = sum / a[i][i];
309        }
310
311        Ok(x)
312    }
313
314    fn validate_noisy_results(&self, noisy_results: &[f64]) -> Result<(), ErrorMitigationError> {
315        if noisy_results.is_empty() {
316            return Err(ErrorMitigationError::EmptyNoisyResults);
317        }
318
319        let expected = self.noise_factors.len();
320        let actual = noisy_results.len();
321        if actual != expected {
322            return Err(ErrorMitigationError::NoisyResultsLengthMismatch { expected, actual });
323        }
324
325        Ok(())
326    }
327
328    fn fold_to_level(
329        &self,
330        level: i32,
331        gate_set: Option<&[Instruction]>,
332    ) -> Result<Circuit, CircuitError> {
333        if level < 0 {
334            return Err(CircuitError::InvalidControlOperation(
335                "Fold level must be non-negative.".to_string(),
336            ));
337        }
338
339        if level == 0 {
340            return Ok(self.circuit.clone());
341        }
342
343        match gate_set {
344            None => self.fold_all(level as usize),
345            Some(gates) => self.fold_selected(level as usize, gates),
346        }
347    }
348
349    fn fold_all(&self, level: usize) -> Result<Circuit, CircuitError> {
350        let mut folded = Circuit::from_qubits(self.circuit.qubits())?;
351        folded.set_global_phase(self.circuit.global_phase());
352        let inverse = self.circuit.inverse()?;
353
354        self.append_circuit_ops(&mut folded, &self.circuit)?;
355        for _ in 0..level {
356            self.append_circuit_ops(&mut folded, &inverse)?;
357            self.append_circuit_ops(&mut folded, &self.circuit)?;
358        }
359
360        Ok(folded)
361    }
362
363    fn fold_selected(
364        &self,
365        level: usize,
366        gate_set: &[Instruction],
367    ) -> Result<Circuit, CircuitError> {
368        let gate_names: HashSet<String> = gate_set.iter().map(Instruction::name).collect();
369        let mut folded = Circuit::from_qubits(self.circuit.qubits())?;
370        folded.set_global_phase(self.circuit.global_phase());
371
372        for op in self.circuit.operations() {
373            self.append_operation(&mut folded, &self.circuit, op)?;
374            if gate_names.contains(&op.instruction.name()) {
375                for _ in 0..level {
376                    let inv = self.invert_operation(op)?;
377                    self.append_operation(&mut folded, &inv.0, &inv.1)?;
378                    self.append_operation(&mut folded, &self.circuit, op)?;
379                }
380            }
381        }
382
383        Ok(folded)
384    }
385
386    fn append_circuit_ops(
387        &self,
388        target: &mut Circuit,
389        source: &Circuit,
390    ) -> Result<(), CircuitError> {
391        for op in source.operations() {
392            self.append_operation(target, source, op)?;
393        }
394        Ok(())
395    }
396
397    fn append_operation(
398        &self,
399        target: &mut Circuit,
400        source: &Circuit,
401        op: &Operation,
402    ) -> Result<(), CircuitError> {
403        let params = op.params.iter().map(|param| match param {
404            crate::circuit::circuit_param::CircuitParam::Fixed(value) => (*value).into(),
405            crate::circuit::circuit_param::CircuitParam::Index(index) => {
406                source.parameters()[*index as usize].clone().into()
407            }
408        });
409
410        target.append(
411            op.instruction.clone(),
412            op.qubits.iter().copied(),
413            params,
414            op.label.as_deref(),
415        )
416    }
417
418    fn invert_operation(&self, op: &Operation) -> Result<(Circuit, Operation), CircuitError> {
419        let op_params: Vec<Parameter> = op
420            .params
421            .iter()
422            .map(|param| match param {
423                crate::circuit::circuit_param::CircuitParam::Fixed(value) => {
424                    Parameter::from(*value)
425                }
426                crate::circuit::circuit_param::CircuitParam::Index(index) => {
427                    self.circuit.parameters()[*index as usize].clone()
428                }
429            })
430            .collect();
431
432        let (inv_inst, inv_params) = op
433            .instruction
434            .inverse(&op_params)
435            .ok_or(CircuitError::IrreversibleOperation)?;
436
437        let mut inv_circuit = Circuit::from_qubits(self.circuit.qubits())?;
438        inv_circuit.append(
439            inv_inst,
440            op.qubits.iter().copied(),
441            inv_params.into_iter().map(Into::into),
442            op.label.as_deref(),
443        )?;
444        let inv_op = inv_circuit.operations()[0].clone();
445
446        Ok((inv_circuit, inv_op))
447    }
448}
449
450#[cfg(test)]
451#[path = "./zne_mitigation_test.rs"]
452mod zne_mitigation_test;