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///     // Both the numerator and the denominator carry a Hamiltonian: the
225///     // numerator additionally acts on the first copy (qubit 0 here).
226///     let term = &hamiltonian.unwrap().terms[0].0;
227///     if term.x[0] || term.z[0] {
228///         (1.5, 0.25)
229///     } else {
230///         (2.0, 1.0)
231///     }
232/// };
233///
234/// mitigation
235///     .run(
236///         &hamiltonian,
237///         RunArgs::VirtualDistillation {
238///             shots_numerator: 512,
239///             shots_denominator: 512,
240///         },
241///         &estimator,
242///     )
243///     .unwrap();
244///
245/// let mitigated = mitigation
246///     .get_mitigated(ProcessArgs::VirtualDistillation)
247///     .unwrap();
248///
249/// assert!(mitigated.variance.is_some());
250/// ```
251#[derive(Debug, Clone)]
252pub struct ErrorMitigation {
253    circuit: Circuit,
254    method: MitigationMethod,
255    state: ErrorMitigationState,
256}
257
258impl ErrorMitigation {
259    /// Creates a new unified mitigation pipeline for the given circuit and method.
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use cqlib_core::circuit::Circuit;
265    /// use cqlib_core::error_mitigation::{
266    ///     ErrorMitigation, MitigationMethod, VirtualDistillationConfig,
267    /// };
268    ///
269    /// let mitigation = ErrorMitigation::new(
270    ///     Circuit::new(1),
271    ///     MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }),
272    /// );
273    ///
274    /// assert!(mitigation.is_ok());
275    /// ```
276    pub fn new(circuit: Circuit, method: MitigationMethod) -> Result<Self, ErrorMitigationError> {
277        Self::validate_method(&method)?;
278        Ok(Self {
279            circuit,
280            method,
281            state: ErrorMitigationState::Initialized,
282        })
283    }
284
285    /// Executes the configured mitigation pipeline and stores raw results.
286    ///
287    /// This method does not return the final mitigated value directly. Call
288    /// [`ErrorMitigation::get_mitigated`] afterwards to perform the method-specific
289    /// post-processing step.
290    ///
291    /// # Example
292    ///
293    /// ```rust
294    /// use cqlib_core::circuit::Circuit;
295    /// use cqlib_core::error_mitigation::{
296    ///     ErrorMitigation, MitigationMethod, RunArgs, VirtualDistillationConfig,
297    /// };
298    /// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
299    /// use num_complex::Complex64;
300    ///
301    /// let mut pauli = PauliString::new(1);
302    /// pauli.set_pauli(0, Pauli::Z);
303    /// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
304    ///
305    /// let mut mitigation = ErrorMitigation::new(
306    ///     Circuit::new(1),
307    ///     MitigationMethod::VirtualDistillation(VirtualDistillationConfig { copies: 2 }),
308    /// )
309    /// .unwrap();
310    ///
311    /// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
312    /// let estimator = |_circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, _shots: Option<usize>| {
313    ///     // Both the numerator and the denominator carry a Hamiltonian: the
314    ///     // numerator additionally acts on the first copy (qubit 0 here).
315    ///     let term = &hamiltonian.unwrap().terms[0].0;
316    ///     if term.x[0] || term.z[0] {
317    ///         (1.0, 0.1)
318    ///     } else {
319    ///         (2.0, 0.2)
320    ///     }
321    /// };
322    ///
323    /// mitigation
324    ///     .run(
325    ///         &hamiltonian,
326    ///         RunArgs::VirtualDistillation {
327    ///             shots_numerator: 256,
328    ///             shots_denominator: 256,
329    ///         },
330    ///         &estimator,
331    ///     )
332    ///     .unwrap();
333    /// ```
334    pub fn run(
335        &mut self,
336        hamiltonian: &Hamiltonian,
337        run_args: RunArgs,
338        estimator: &Estimator<'_>,
339    ) -> Result<(), ErrorMitigationError> {
340        match self.state {
341            ErrorMitigationState::Initialized => {}
342            ErrorMitigationState::RunCompleted(_) => return Err(ErrorMitigationError::AlreadyRun),
343        }
344
345        let run_record = match (&self.method, run_args) {
346            (MitigationMethod::Zne(config), RunArgs::Zne { gate_set, shots }) => {
347                RunRecord::Zne(self.run_zne(config, hamiltonian, gate_set, shots, estimator)?)
348            }
349            (
350                MitigationMethod::VirtualDistillation(config),
351                RunArgs::VirtualDistillation {
352                    shots_numerator,
353                    shots_denominator,
354                },
355            ) => RunRecord::VirtualDistillation(Box::new(self.run_virtual_distillation(
356                config,
357                hamiltonian,
358                shots_numerator,
359                shots_denominator,
360                estimator,
361            )?)),
362            (MitigationMethod::Zne(_), _) | (MitigationMethod::VirtualDistillation(_), _) => {
363                return Err(ErrorMitigationError::RunArgsMethodMismatch);
364            }
365        };
366
367        self.state = ErrorMitigationState::RunCompleted(Box::new(run_record));
368        Ok(())
369    }
370
371    /// Produces the final mitigated result from the stored raw execution outputs.
372    ///
373    /// `run()` must be called first. The provided [`ProcessArgs`] must match the
374    /// configured mitigation method. This method can be called repeatedly with
375    /// different processing arguments; it does not consume data or rerun the estimator.
376    ///
377    /// # Example
378    ///
379    /// ```rust
380    /// use cqlib_core::circuit::Circuit;
381    /// use cqlib_core::error_mitigation::{
382    ///     ErrorMitigation, ExtrapolateMethod, MitigationMethod, ProcessArgs, RunArgs, ZneConfig,
383    /// };
384    /// use cqlib_core::qis::{Hamiltonian, Pauli, PauliString};
385    /// use num_complex::Complex64;
386    ///
387    /// let mut pauli = PauliString::new(1);
388    /// pauli.set_pauli(0, Pauli::Z);
389    /// let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap();
390    ///
391    /// let mut mitigation = ErrorMitigation::new(
392    ///     Circuit::new(1),
393    ///     MitigationMethod::Zne(ZneConfig {
394    ///         fold_levels: vec![0, 1, 2],
395    ///     }),
396    /// )
397    /// .unwrap();
398    ///
399    /// // A portal to real quantum device or classical simulation backend that calculates <h> and its variance
400    /// let estimator = |circuit: &Circuit, _hamiltonian: Option<&Hamiltonian>, _shots: Option<usize>| {
401    ///     (circuit.operations().len() as f64, 0.0)
402    /// };
403    ///
404    /// mitigation
405    ///     .run(
406    ///         &hamiltonian,
407    ///         RunArgs::Zne {
408    ///             gate_set: None,
409    ///             shots: None,
410    ///         },
411    ///         &estimator,
412    ///     )
413    ///     .unwrap();
414    ///
415    /// let mitigated = mitigation
416    ///     .get_mitigated(ProcessArgs::Zne {
417    ///         method: ExtrapolateMethod::Polynomial,
418    ///         degree: Some(1),
419    ///     })
420    ///     .unwrap();
421    ///
422    /// assert!(mitigated.expectation.is_finite());
423    /// ```
424    pub fn get_mitigated(
425        &self,
426        process_args: ProcessArgs,
427    ) -> Result<MitigatedResult, ErrorMitigationError> {
428        let mitigated = match (&self.method, &self.state, process_args) {
429            (MitigationMethod::Zne(_), ErrorMitigationState::Initialized, _) => {
430                return Err(ErrorMitigationError::RunRequiredBeforeMitigation);
431            }
432            (MitigationMethod::VirtualDistillation(_), ErrorMitigationState::Initialized, _) => {
433                return Err(ErrorMitigationError::RunRequiredBeforeMitigation);
434            }
435            (
436                MitigationMethod::Zne(config),
437                ErrorMitigationState::RunCompleted(record),
438                ProcessArgs::Zne { method, degree },
439            ) => match record.as_ref() {
440                RunRecord::Zne(record) => self.get_zne_mitigated(config, record, method, degree)?,
441                RunRecord::VirtualDistillation(_) => {
442                    return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
443                }
444            },
445            (
446                MitigationMethod::VirtualDistillation(_),
447                ErrorMitigationState::RunCompleted(record),
448                ProcessArgs::VirtualDistillation,
449            ) => match record.as_ref() {
450                RunRecord::VirtualDistillation(record) => {
451                    self.get_virtual_distillation_mitigated(record.as_ref())?
452                }
453                RunRecord::Zne(_) => {
454                    return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
455                }
456            },
457            (MitigationMethod::Zne(_), ErrorMitigationState::RunCompleted(_), _) => {
458                return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
459            }
460            (
461                MitigationMethod::VirtualDistillation(_),
462                ErrorMitigationState::RunCompleted(_),
463                _,
464            ) => {
465                return Err(ErrorMitigationError::ProcessArgsMethodMismatch);
466            }
467        };
468
469        Ok(mitigated)
470    }
471
472    fn validate_method(method: &MitigationMethod) -> Result<(), ErrorMitigationError> {
473        match method {
474            MitigationMethod::Zne(config) => {
475                for &level in &config.fold_levels {
476                    if level < 0 {
477                        return Err(ErrorMitigationError::InvalidFoldLevel(level));
478                    }
479                }
480                Ok(())
481            }
482            MitigationMethod::VirtualDistillation(config) => {
483                if config.copies < 2 {
484                    return Err(ErrorMitigationError::InvalidCopies(config.copies));
485                }
486                Ok(())
487            }
488        }
489    }
490
491    fn run_zne(
492        &self,
493        config: &ZneConfig,
494        hamiltonian: &Hamiltonian,
495        gate_set: Option<Vec<Instruction>>,
496        shots: Option<usize>,
497        estimator: &Estimator<'_>,
498    ) -> Result<ZneRunRecord, ErrorMitigationError> {
499        let zne = ZNEMitigation::new(self.circuit.clone(), config.fold_levels.clone());
500        if hamiltonian.num_qubits != self.circuit.width() {
501            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
502                expected: self.circuit.width(),
503                actual: hamiltonian.num_qubits,
504            });
505        }
506
507        let folded_circuits = zne.fold_circuits(gate_set.as_deref())?;
508        let noisy_expectations = folded_circuits
509            .iter()
510            .map(|circuit| estimator(circuit, Some(hamiltonian), shots).0)
511            .collect();
512
513        Ok(ZneRunRecord {
514            noisy_expectations,
515            noise_factors: zne.noise_factors().to_vec(),
516        })
517    }
518
519    fn run_virtual_distillation(
520        &self,
521        config: &VirtualDistillationConfig,
522        hamiltonian: &Hamiltonian,
523        shots_numerator: usize,
524        shots_denominator: usize,
525        estimator: &Estimator<'_>,
526    ) -> Result<VirtualDistillationRunRecord, ErrorMitigationError> {
527        let vd = VirtualDistillation::new(self.circuit.clone(), config.copies)?;
528        if hamiltonian.num_qubits != self.circuit.width() {
529            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
530                expected: self.circuit.width(),
531                actual: hamiltonian.num_qubits,
532            });
533        }
534
535        let numerator = vd.run_numerator_circuit(hamiltonian, shots_numerator, estimator)?;
536        let denominator = vd.run_denominator_circuit(shots_denominator, estimator)?;
537
538        Ok(VirtualDistillationRunRecord {
539            numerator,
540            denominator,
541        })
542    }
543
544    fn get_zne_mitigated(
545        &self,
546        config: &ZneConfig,
547        record: &ZneRunRecord,
548        method: ExtrapolateMethod,
549        degree: Option<usize>,
550    ) -> Result<MitigatedResult, ErrorMitigationError> {
551        let zne = ZNEMitigation::new(self.circuit.clone(), config.fold_levels.clone());
552
553        debug_assert_eq!(record.noise_factors, zne.noise_factors().to_vec());
554        debug_assert_eq!(record.noise_factors.len(), record.noisy_expectations.len());
555        let degree = match method {
556            ExtrapolateMethod::Polynomial => {
557                degree.unwrap_or_else(|| record.noisy_expectations.len().saturating_sub(1).min(1))
558            }
559            ExtrapolateMethod::Exponential => degree.unwrap_or(0),
560        };
561        let expectation = zne.extrapolate(&record.noisy_expectations, method, degree)?;
562
563        Ok(MitigatedResult {
564            expectation,
565            variance: None,
566        })
567    }
568
569    fn get_virtual_distillation_mitigated(
570        &self,
571        record: &VirtualDistillationRunRecord,
572    ) -> Result<MitigatedResult, ErrorMitigationError> {
573        let (expectation, variance) = VirtualDistillation::mitigate_from_statistics(
574            record.numerator.0,
575            record.numerator.1,
576            record.denominator.0,
577            record.denominator.1,
578        )?;
579
580        Ok(MitigatedResult {
581            expectation,
582            variance: Some(variance),
583        })
584    }
585}