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 ©_swap_circuit,
535 Some(&expanded_hamiltonian),
536 Some(shots_numerator),
537 );
538 let denominator = estimator(©_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}