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}