Skip to main content

cqlib_core/error_mitigation/
unified.rs

1// This code is part of Cqlib.
2// Modified to allow repeated post-processing of collected estimates.
3//
4// (C) Copyright China Telecom Quantum Group 2026
5//
6// This code is licensed under the Apache License, Version 2.0. You may
7// obtain a copy of this license in the LICENSE.txt file in the root directory
8// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9//
10// Any modifications or derivative works of this code must retain this
11// copyright notice, and modified files need to carry a notice indicating
12// that they have been altered from the originals.
13
14use crate::circuit::{Circuit, Instruction};
15use crate::qis::Hamiltonian;
16
17use super::{
18    ErrorMitigationError, Estimator, ExtrapolateMethod, VirtualDistillation, ZNEMitigation,
19};
20
21/// Method-specific configuration for zero-noise extrapolation.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ZneConfig {
24    pub fold_levels: Vec<i32>,
25}
26
27/// Method-specific configuration for virtual distillation.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct VirtualDistillationConfig {
30    pub copies: usize,
31}
32
33/// Supported error-mitigation methods.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum MitigationMethod {
36    Zne(ZneConfig),
37    VirtualDistillation(VirtualDistillationConfig),
38}
39
40/// Runtime arguments required to execute one mitigation pipeline.
41#[derive(Debug, Clone)]
42pub enum RunArgs {
43    Zne {
44        gate_set: Option<Vec<Instruction>>,
45        shots: Option<usize>,
46    },
47    VirtualDistillation {
48        shots_numerator: usize,
49        shots_denominator: usize,
50    },
51}
52
53impl PartialEq for RunArgs {
54    fn eq(&self, other: &Self) -> bool {
55        match (self, other) {
56            (
57                Self::Zne {
58                    gate_set: lhs_gate_set,
59                    shots: lhs_shots,
60                },
61                Self::Zne {
62                    gate_set: rhs_gate_set,
63                    shots: rhs_shots,
64                },
65            ) => {
66                lhs_shots == rhs_shots
67                    && match (lhs_gate_set, rhs_gate_set) {
68                        (None, None) => true,
69                        (Some(lhs), Some(rhs)) => {
70                            lhs.len() == rhs.len()
71                                && lhs
72                                    .iter()
73                                    .zip(rhs.iter())
74                                    .all(|(lhs, rhs)| lhs.name() == rhs.name())
75                        }
76                        _ => false,
77                    }
78            }
79            (
80                Self::VirtualDistillation {
81                    shots_numerator: lhs_numerator,
82                    shots_denominator: lhs_denominator,
83                },
84                Self::VirtualDistillation {
85                    shots_numerator: rhs_numerator,
86                    shots_denominator: rhs_denominator,
87                },
88            ) => lhs_numerator == rhs_numerator && lhs_denominator == rhs_denominator,
89            _ => false,
90        }
91    }
92}
93
94impl Eq for RunArgs {}
95
96/// Processing arguments required to produce the final mitigated result.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum ProcessArgs {
99    Zne {
100        method: ExtrapolateMethod,
101        degree: Option<usize>,
102    },
103    VirtualDistillation,
104}
105
106/// Final mitigated observable estimate.
107#[derive(Debug, Clone, PartialEq)]
108pub struct MitigatedResult {
109    pub expectation: f64,
110    pub variance: Option<f64>,
111}
112
113#[derive(Debug, Clone)]
114enum ErrorMitigationState {
115    Initialized,
116    RunCompleted(Box<RunRecord>),
117}
118
119#[derive(Debug, Clone)]
120enum RunRecord {
121    Zne(ZneRunRecord),
122    VirtualDistillation(Box<VirtualDistillationRunRecord>),
123}
124
125#[derive(Debug, Clone)]
126struct ZneRunRecord {
127    noisy_expectations: Vec<f64>,
128    noise_factors: Vec<i32>,
129}
130
131#[derive(Debug, Clone)]
132struct VirtualDistillationRunRecord {
133    numerator: (f64, f64),
134    denominator: (f64, f64),
135}
136
137/// Unified facade over supported error-mitigation methods.
138///
139/// The workflow is always sequential:
140/// 1. create an [`ErrorMitigation`] instance with one mitigation method,
141/// 2. call [`ErrorMitigation::run`] to construct and execute the required circuits,
142/// 3. call [`ErrorMitigation::get_mitigated`] to post-process the stored raw data.
143///
144/// # Example
145///
146/// Zero-noise extrapolation:
147///
148/// ```rust
149/// use cqlib_core::circuit::{Circuit, Qubit};
150/// use cqlib_core::error_mitigation::{
151///     ErrorMitigation, ExtrapolateMethod, MitigationMethod, ProcessArgs, RunArgs, ZneConfig,
152/// };
153/// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
154/// use num_complex::Complex64;
155///
156/// let q0 = Qubit::new(0);
157/// let mut circuit = Circuit::new(1);
158/// circuit.h(q0).unwrap();
159///
160/// let mut pauli = PauliString::new(1);
161/// pauli.set_pauli(0, Pauli::Z);
162/// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
163///
164/// let mut mitigation = ErrorMitigation::new(
165///     circuit,
166///     MitigationMethod::Zne(ZneConfig {
167///         fold_levels: vec![0, 1, 2],
168///     }),
169/// )
170/// .unwrap();
171///
172/// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
173/// let estimator = |circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, shots: Option<usize>| {
174///     assert!(hamiltonian.is_some());
175///     assert_eq!(shots, Some(1024));
176///     (circuit.operations().len() as f64, 0.0)
177/// };
178///
179/// mitigation
180///     .run(
181///         &hamiltonian,
182///         RunArgs::Zne {
183///             gate_set: None,
184///             shots: Some(1024),
185///         },
186///         &estimator,
187///     )
188///     .unwrap();
189///
190/// let mitigated = mitigation
191///     .get_mitigated(ProcessArgs::Zne {
192///         method: ExtrapolateMethod::Polynomial,
193///         degree: Some(1),
194///     })
195///     .unwrap();
196///
197/// assert!(mitigated.variance.is_none());
198/// ```
199///
200/// Virtual distillation:
201///
202/// ```rust
203/// use cqlib_core::circuit::Circuit;
204/// use cqlib_core::error_mitigation::{
205///     ErrorMitigation, MitigationMethod, ProcessArgs, RunArgs, VirtualDistillationConfig,
206/// };
207/// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
208/// use num_complex::Complex64;
209///
210/// let circuit = Circuit::new(1);
211///
212/// let mut pauli = PauliString::new(1);
213/// pauli.set_pauli(0, Pauli::Z);
214/// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
215///
216/// let mut mitigation = ErrorMitigation::new(
217///     circuit,
218///     MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }),
219/// )
220/// .unwrap();
221///
222/// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
223/// let estimator = |_circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, _shots: Option<usize>| {
224///     if hamiltonian.is_some() {
225///         (1.5, 0.25)
226///     } else {
227///         (2.0, 1.0)
228///     }
229/// };
230///
231/// mitigation
232///     .run(
233///         &hamiltonian,
234///         RunArgs::VirtualDistillation {
235///             shots_numerator: 512,
236///             shots_denominator: 512,
237///         },
238///         &estimator,
239///     )
240///     .unwrap();
241///
242/// let mitigated = mitigation
243///     .get_mitigated(ProcessArgs::VirtualDistillation)
244///     .unwrap();
245///
246/// assert!(mitigated.variance.is_some());
247/// ```
248#[derive(Debug, Clone)]
249pub struct ErrorMitigation {
250    circuit: Circuit,
251    method: MitigationMethod,
252    state: ErrorMitigationState,
253}
254
255impl ErrorMitigation {
256    /// Creates a new unified mitigation pipeline for the given circuit and method.
257    ///
258    /// # Example
259    ///
260    /// ```rust
261    /// use cqlib_core::circuit::Circuit;
262    /// use cqlib_core::error_mitigation::{
263    ///     ErrorMitigation, MitigationMethod, VirtualDistillationConfig,
264    /// };
265    ///
266    /// let mitigation = ErrorMitigation::new(
267    ///     Circuit::new(1),
268    ///     MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }),
269    /// );
270    ///
271    /// assert!(mitigation.is_ok());
272    /// ```
273    pub fn new(circuit: Circuit, method: MitigationMethod) -> Result<Self, ErrorMitigationError> {
274        Self::validate_method(&method)?;
275        Ok(Self {
276            circuit,
277            method,
278            state: ErrorMitigationState::Initialized,
279        })
280    }
281
282    /// Executes the configured mitigation pipeline and stores raw results.
283    ///
284    /// This method does not return the final mitigated value directly. Call
285    /// [`ErrorMitigation::get_mitigated`] afterwards to perform the method-specific
286    /// post-processing step.
287    ///
288    /// # Example
289    ///
290    /// ```rust
291    /// use cqlib_core::circuit::Circuit;
292    /// use cqlib_core::error_mitigation::{
293    ///     ErrorMitigation, MitigationMethod, RunArgs, VirtualDistillationConfig,
294    /// };
295    /// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
296    /// use num_complex::Complex64;
297    ///
298    /// let mut pauli = PauliString::new(1);
299    /// pauli.set_pauli(0, Pauli::Z);
300    /// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
301    ///
302    /// let mut mitigation = ErrorMitigation::new(
303    ///     Circuit::new(1),
304    ///     MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }),
305    /// )
306    /// .unwrap();
307    ///
308    /// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
309    /// let estimator = |_circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, _shots: Option<usize>| {
310    ///     if hamiltonian.is_some() {
311    ///         (1.0, 0.1)
312    ///     } else {
313    ///         (2.0, 0.2)
314    ///     }
315    /// };
316    ///
317    /// mitigation
318    ///     .run(
319    ///         &hamiltonian,
320    ///         RunArgs::VirtualDistillation {
321    ///             shots_numerator: 256,
322    ///             shots_denominator: 256,
323    ///         },
324    ///         &estimator,
325    ///     )
326    ///     .unwrap();
327    /// ```
328    pub fn run(
329        &mut self,
330        hamiltonian: &Hamiltonian,
331        run_args: RunArgs,
332        estimator: &Estimator<'_>,
333    ) -> Result<(), ErrorMitigationError> {
334        match self.state {
335            ErrorMitigationState::Initialized => {}
336            ErrorMitigationState::RunCompleted(_) => return Err(ErrorMitigationError::AlreadyRun),
337        }
338
339        let run_record = match (&self.method, run_args) {
340            (MitigationMethod::Zne(config), RunArgs::Zne { gate_set, shots }) => {
341                RunRecord::Zne(self.run_zne(config, hamiltonian, gate_set, shots, estimator)?)
342            }
343            (
344                MitigationMethod::VirtualDistillation(config),
345                RunArgs::VirtualDistillation {
346                    shots_numerator,
347                    shots_denominator,
348                },
349            ) => RunRecord::VirtualDistillation(Box::new(self.run_virtual_distillation(
350                config,
351                hamiltonian,
352                shots_numerator,
353                shots_denominator,
354                estimator,
355            )?)),
356            (MitigationMethod::Zne(_), _) | (MitigationMethod::VirtualDistillation(_), _) => {
357                return Err(ErrorMitigationError::RunArgsMethodMismatch);
358            }
359        };
360
361        self.state = ErrorMitigationState::RunCompleted(Box::new(run_record));
362        Ok(())
363    }
364
365    /// Produces the final mitigated result from the stored raw execution outputs.
366    ///
367    /// `run()` must be called first. The provided [`ProcessArgs`] must match the
368    /// configured mitigation method. This method can be called repeatedly with
369    /// different processing arguments; it does not consume data or rerun the estimator.
370    ///
371    /// # Example
372    ///
373    /// ```rust
374    /// use cqlib_core::circuit::Circuit;
375    /// use cqlib_core::error_mitigation::{
376    ///     ErrorMitigation, ExtrapolateMethod, MitigationMethod, ProcessArgs, RunArgs, ZneConfig,
377    /// };
378    /// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
379    /// use num_complex::Complex64;
380    ///
381    /// let mut pauli = PauliString::new(1);
382    /// pauli.set_pauli(0, Pauli::Z);
383    /// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
384    ///
385    /// let mut mitigation = ErrorMitigation::new(
386    ///     Circuit::new(1),
387    ///     MitigationMethod::Zne(ZneConfig {
388    ///         fold_levels: vec![0, 1, 2],
389    ///     }),
390    /// )
391    /// .unwrap();
392    ///
393    /// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
394    /// let estimator = |circuit: &Circuit, _hamiltonian: Option<&Hamiltonian>, _shots: Option<usize>| {
395    ///     (circuit.operations().len() as f64, 0.0)
396    /// };
397    ///
398    /// mitigation
399    ///     .run(
400    ///         &hamiltonian,
401    ///         RunArgs::Zne {
402    ///             gate_set: None,
403    ///             shots: None,
404    ///         },
405    ///         &estimator,
406    ///     )
407    ///     .unwrap();
408    ///
409    /// let mitigated = mitigation
410    ///     .get_mitigated(ProcessArgs::Zne {
411    ///         method: ExtrapolateMethod::Polynomial,
412    ///         degree: Some(1),
413    ///     })
414    ///     .unwrap();
415    ///
416    /// assert!(mitigated.expectation.is_finite());
417    /// ```
418    pub fn get_mitigated(
419        &self,
420        process_args: ProcessArgs,
421    ) -> Result<MitigatedResult, ErrorMitigationError> {
422        let mitigated = match (&self.method, &self.state, process_args) {
423            (MitigationMethod::Zne(_), ErrorMitigationState::Initialized, _) => {
424                return Err(ErrorMitigationError::RunRequiredBeforeMitigation);
425            }
426            (MitigationMethod::VirtualDistillation(_), ErrorMitigationState::Initialized, _) => {
427                return Err(ErrorMitigationError::RunRequiredBeforeMitigation);
428            }
429            (
430                MitigationMethod::Zne(config),
431                ErrorMitigationState::RunCompleted(record),
432                ProcessArgs::Zne { method, degree },
433            ) => match record.as_ref() {
434                RunRecord::Zne(record) => self.get_zne_mitigated(config, record, method, degree)?,
435                RunRecord::VirtualDistillation(_) => {
436                    return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
437                }
438            },
439            (
440                MitigationMethod::VirtualDistillation(_),
441                ErrorMitigationState::RunCompleted(record),
442                ProcessArgs::VirtualDistillation,
443            ) => match record.as_ref() {
444                RunRecord::VirtualDistillation(record) => {
445                    self.get_virtual_distillation_mitigated(record.as_ref())?
446                }
447                RunRecord::Zne(_) => {
448                    return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
449                }
450            },
451            (MitigationMethod::Zne(_), ErrorMitigationState::RunCompleted(_), _) => {
452                return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
453            }
454            (
455                MitigationMethod::VirtualDistillation(_),
456                ErrorMitigationState::RunCompleted(_),
457                _,
458            ) => {
459                return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
460            }
461        };
462
463        Ok(mitigated)
464    }
465
466    fn validate_method(method: &MitigationMethod) -> Result<(), ErrorMitigationError> {
467        match method {
468            MitigationMethod::Zne(config) => {
469                for &level in &config.fold_levels {
470                    if level < 0 {
471                        return Err(ErrorMitigationError::InvalidFoldLevel(level));
472                    }
473                }
474                Ok(())
475            }
476            MitigationMethod::VirtualDistillation(config) => {
477                if config.copies < 2 {
478                    return Err(ErrorMitigationError::InvalidCopies(config.copies));
479                }
480                Ok(())
481            }
482        }
483    }
484
485    fn run_zne(
486        &self,
487        config: &ZneConfig,
488        hamiltonian: &Hamiltonian,
489        gate_set: Option<Vec<Instruction>>,
490        shots: Option<usize>,
491        estimator: &Estimator<'_>,
492    ) -> Result<ZneRunRecord, ErrorMitigationError> {
493        let zne = ZNEMitigation::new(self.circuit.clone(), config.fold_levels.clone());
494        if hamiltonian.num_qubits != self.circuit.width() {
495            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
496                expected: self.circuit.width(),
497                actual: hamiltonian.num_qubits,
498            });
499        }
500
501        let folded_circuits = zne.fold_circuits(gate_set.as_deref())?;
502        let noisy_expectations = folded_circuits
503            .iter()
504            .map(|circuit| estimator(circuit, Some(hamiltonian), shots).0)
505            .collect();
506
507        Ok(ZneRunRecord {
508            noisy_expectations,
509            noise_factors: zne.noise_factors().to_vec(),
510        })
511    }
512
513    fn run_virtual_distillation(
514        &self,
515        config: &VirtualDistillationConfig,
516        hamiltonian: &Hamiltonian,
517        shots_numerator: usize,
518        shots_denominator: usize,
519        estimator: &Estimator<'_>,
520    ) -> Result<VirtualDistillationRunRecord, ErrorMitigationError> {
521        let vd = VirtualDistillation::new(self.circuit.clone(), config.copies)?;
522        if hamiltonian.num_qubits != self.circuit.width() {
523            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
524                expected: self.circuit.width(),
525                actual: hamiltonian.num_qubits,
526            });
527        }
528
529        let copy_swap_circuit = vd.build_copy_swap_circuit()?;
530        let extra_qubits = copy_swap_circuit.width() - hamiltonian.num_qubits;
531        let expanded_hamiltonian =
532            VirtualDistillation::expand_hamiltonian(hamiltonian, extra_qubits)?;
533        let numerator = estimator(
534            &copy_swap_circuit,
535            Some(&expanded_hamiltonian),
536            Some(shots_numerator),
537        );
538        let denominator = estimator(&copy_swap_circuit, None, Some(shots_denominator));
539
540        Ok(VirtualDistillationRunRecord {
541            numerator,
542            denominator,
543        })
544    }
545
546    fn get_zne_mitigated(
547        &self,
548        config: &ZneConfig,
549        record: &ZneRunRecord,
550        method: ExtrapolateMethod,
551        degree: Option<usize>,
552    ) -> Result<MitigatedResult, ErrorMitigationError> {
553        let zne = ZNEMitigation::new(self.circuit.clone(), config.fold_levels.clone());
554
555        debug_assert_eq!(record.noise_factors, zne.noise_factors().to_vec());
556        debug_assert_eq!(record.noise_factors.len(), record.noisy_expectations.len());
557        let degree = match method {
558            ExtrapolateMethod::Polynomial => {
559                degree.unwrap_or_else(|| record.noisy_expectations.len().saturating_sub(1).min(1))
560            }
561            ExtrapolateMethod::Exponential => degree.unwrap_or(0),
562        };
563        let expectation = zne.extrapolate(&record.noisy_expectations, method, degree)?;
564
565        Ok(MitigatedResult {
566            expectation,
567            variance: None,
568        })
569    }
570
571    fn get_virtual_distillation_mitigated(
572        &self,
573        record: &VirtualDistillationRunRecord,
574    ) -> Result<MitigatedResult, ErrorMitigationError> {
575        let (expectation, variance) = VirtualDistillation::mitigate_from_statistics(
576            record.numerator.0,
577            record.numerator.1,
578            record.denominator.0,
579            record.denominator.1,
580        )?;
581
582        Ok(MitigatedResult {
583            expectation,
584            variance: Some(variance),
585        })
586    }
587}