Skip to main content

cqlib_core/error_mitigation/
virtual_distillation.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 std::collections::HashMap;
14
15use crate::circuit::{Circuit, CircuitError, CircuitParam, ParameterValue, Qubit};
16use crate::error_mitigation::ErrorMitigationError;
17use crate::error_mitigation::Estimator;
18use crate::qis::{Hamiltonian, Pauli, PauliString};
19
20/// Virtual distillation mitigation based on the moment ratio
21/// `Tr(O rho^M) / Tr(rho^M)`.
22/// Based on: [1] W. J. Huggins et al., “Virtual Distillation for Quantum Error Mitigation,”
23///     Phys. Rev. X, vol. 11, no. 4, p. 041036, Nov. 2021, doi: 10.1103/PhysRevX.11.041036.
24///
25/// # Example
26///
27/// ```rust
28/// use cqlib_core::circuit::{Circuit, Qubit};
29/// use cqlib_core::error_mitigation::VirtualDistillation;
30///
31/// let q0 = Qubit::new(0);
32/// let mut circuit = Circuit::new(1);
33/// circuit.x(q0).unwrap();
34///
35/// let _vd = VirtualDistillation::new(circuit, 2).unwrap();
36/// ```
37#[derive(Debug, Clone)]
38pub struct VirtualDistillation {
39    circuit: Circuit,
40    copies: usize,
41}
42
43impl VirtualDistillation {
44    /// Creates a new virtual distillation helper.
45    pub fn new(circuit: Circuit, copies: usize) -> Result<Self, ErrorMitigationError> {
46        if copies < 2 {
47            return Err(ErrorMitigationError::InvalidCopies(copies));
48        }
49
50        Ok(Self { circuit, copies })
51    }
52
53    /// Returns the configured number of copies.
54    pub fn copies(&self) -> usize {
55        self.copies
56    }
57
58    /// Updates the configured number of copies.
59    pub fn set_copies(&mut self, copies: usize) -> Result<(), ErrorMitigationError> {
60        if copies < 2 {
61            return Err(ErrorMitigationError::InvalidCopies(copies));
62        }
63
64        self.copies = copies;
65        Ok(())
66    }
67
68    /// Builds a copy-swap circuit from the configured base circuit.
69    ///
70    /// The returned circuit contains:
71    /// - `copies` disjoint copies of the base circuit preparation,
72    /// - pairwise SWAP operations between the first copy and every additional copy.
73    pub fn build_copy_swap_circuit(&self) -> Result<Circuit, CircuitError> {
74        let base_circuit = self.circuit.decompose()?;
75        let base_width = base_circuit.width();
76        let mut copy_swap_circuit = Circuit::new(self.copies * base_width);
77
78        for copy_index in 0..self.copies {
79            let copy_offset = copy_index * base_width;
80            Self::append_circuit_with_offset(&mut copy_swap_circuit, &base_circuit, copy_offset)?;
81        }
82
83        for other_copy in 1..self.copies {
84            let first_copy_offset = 0;
85            let other_copy_offset = other_copy * base_width;
86            for qubit_index in 0..base_width {
87                let left = Qubit::new((first_copy_offset + qubit_index) as u32);
88                let right = Qubit::new((other_copy_offset + qubit_index) as u32);
89                copy_swap_circuit.swap(left, right)?;
90            }
91        }
92
93        Ok(copy_swap_circuit)
94    }
95
96    /// Expands a Hamiltonian to the full copy-swap circuit width.
97    ///
98    /// Virtual distillation evaluates the numerator on a wider copy-swap circuit,
99    /// so the estimator needs a Hamiltonian of matching width. This helper keeps
100    /// the original Pauli operators on their current qubit indices and appends
101    /// `n` `Z`s on higher-index qubits.
102    pub(crate) fn expand_hamiltonian(
103        hamiltonian: &Hamiltonian,
104        n: usize,
105    ) -> Result<Hamiltonian, ErrorMitigationError> {
106        if hamiltonian.terms.is_empty() {
107            return Ok(Hamiltonian::new(hamiltonian.num_qubits + n));
108        }
109
110        let expanded_terms = hamiltonian
111            .terms
112            .iter()
113            .map(|(term, coeff)| {
114                let mut expanded_term = PauliString::new(hamiltonian.num_qubits + n);
115                expanded_term.phase = term.phase;
116
117                for qubit in 0..hamiltonian.num_qubits {
118                    let pauli = match (term.x[qubit], term.z[qubit]) {
119                        (false, false) => Pauli::I,
120                        (true, false) => Pauli::X,
121                        (false, true) => Pauli::Z,
122                        (true, true) => Pauli::Y,
123                    };
124                    expanded_term.set_pauli(qubit, pauli);
125                }
126
127                for qubit in hamiltonian.num_qubits..(hamiltonian.num_qubits + n) {
128                    expanded_term.set_pauli(qubit, Pauli::Z);
129                }
130
131                (expanded_term, *coeff)
132            })
133            .collect();
134
135        Ok(Hamiltonian::from_list(expanded_terms)?)
136    }
137
138    /// Runs the denominator circuit and returns the estimated mean and variance.
139    ///
140    /// - `shots`: the number of shots to run the circuit.
141    /// - `estimator`: an estimator that evaluates the copy-swap circuit, with no
142    ///   Hamiltonian (`None`) and the provided shot count.
143    ///
144    /// # Returns
145    ///
146    /// - `(mu, var)`: the estimated mean and variance of the denominator circuit.
147    pub fn run_denominator_circuit(
148        &self,
149        shots: usize,
150        estimator: &Estimator<'_>,
151    ) -> Result<(f64, f64), CircuitError> {
152        let denominator_circuit = self.build_copy_swap_circuit()?;
153
154        Ok(estimator(&denominator_circuit, None, Some(shots)))
155    }
156
157    /// Runs the numerator circuit and returns the estimated mean and variance.
158    ///
159    /// - `hamiltonian`: the Hamiltonian to estimate on the copy-swap circuit.
160    /// - `shots`: the number of shots to run the circuit.
161    /// - `estimator`: an estimator that evaluates the copy-swap circuit, the given
162    ///   expanded Hamiltonian, and the provided shot count.
163    ///
164    /// # Returns
165    ///
166    /// - `(mu, var)`: the estimated mean and variance of the numerator circuit.
167    pub fn run_numerator_circuit(
168        &self,
169        hamiltonian: &Hamiltonian,
170        shots: usize,
171        estimator: &Estimator<'_>,
172    ) -> Result<(f64, f64), ErrorMitigationError> {
173        let numerator_circuit = self.build_copy_swap_circuit()?;
174        let extra_qubits = (self.copies - 1) * hamiltonian.num_qubits;
175        let expanded_hamiltonian = Self::expand_hamiltonian(hamiltonian, extra_qubits)?;
176        if expanded_hamiltonian.num_qubits != numerator_circuit.width() {
177            return Err(CircuitError::QubitCountMismatch {
178                expected: numerator_circuit.width(),
179                actual: expanded_hamiltonian.num_qubits,
180            }
181            .into());
182        }
183
184        Ok(estimator(
185            &numerator_circuit,
186            Some(&expanded_hamiltonian),
187            Some(shots),
188        ))
189    }
190
191    /// Runs the virtual distillation circuit and returns the mean and variance.
192    ///
193    /// - `hamiltonian`: a `qis::Hamiltonian` describing the observable to estimate for the original circuit.
194    /// - `shots_numerator`: the number of shots to run the numerator circuit.
195    /// - `shots_denominator`: the number of shots to run the denominator circuit.
196    /// - `estimator`: an estimator that evaluates circuits and returns `(mu, var)`.
197    ///
198    /// # Returns
199    ///
200    /// - `mu_vd`: the mitigated result of the observable on the original circuit.
201    /// - `var_vd`: the variance of the mitigated result.
202    pub fn run_vd(
203        &self,
204        hamiltonian: &Hamiltonian,
205        shots_numerator: usize,
206        shots_denominator: usize,
207        estimator: &Estimator<'_>,
208    ) -> Result<(f64, f64), ErrorMitigationError> {
209        if hamiltonian.num_qubits != self.circuit.width() {
210            return Err(ErrorMitigationError::HamiltonianQubitCountMismatch {
211                expected: self.circuit.width(),
212                actual: hamiltonian.num_qubits,
213            });
214        }
215
216        let (mu_numerator, var_numerator) =
217            self.run_numerator_circuit(hamiltonian, shots_numerator, estimator)?;
218        let (mu_denominator, var_denominator) =
219            self.run_denominator_circuit(shots_denominator, estimator)?;
220        let (mu_vd, var_vd) = Self::mitigate_from_statistics(
221            mu_numerator,
222            var_numerator,
223            mu_denominator,
224            var_denominator,
225        )?;
226
227        Ok((mu_vd, var_vd))
228    }
229
230    pub(crate) fn mitigate_from_statistics(
231        mu_numerator: f64,
232        var_numerator: f64,
233        mu_denominator: f64,
234        var_denominator: f64,
235    ) -> Result<(f64, f64), ErrorMitigationError> {
236        if mu_denominator == 0.0 {
237            return Err(ErrorMitigationError::ZeroDenominatorMean);
238        }
239
240        let mu_vd = mu_numerator / mu_denominator;
241
242        // D11 from the paper, using Taylor approximation and assuming independence
243        // of numerator and denominator.
244        let var_vd = var_numerator / mu_denominator.powi(2)
245            + mu_numerator.powi(2) * var_denominator / mu_denominator.powi(4);
246        Ok((mu_vd, var_vd))
247    }
248
249    fn append_circuit_with_offset(
250        target_circuit: &mut Circuit,
251        source_circuit: &Circuit,
252        qubit_offset: usize,
253    ) -> Result<(), CircuitError> {
254        let source_qubits = source_circuit.qubits();
255        let qubit_positions: HashMap<_, _> = source_qubits
256            .iter()
257            .enumerate()
258            .map(|(position, qubit)| (*qubit, position))
259            .collect();
260
261        for op in source_circuit.operations() {
262            let mapped_qubits: Vec<Qubit> = op
263                .qubits
264                .iter()
265                .map(|qubit| {
266                    let position = qubit_positions[qubit];
267                    Qubit::new((qubit_offset + position) as u32)
268                })
269                .collect();
270            let mapped_params: Vec<ParameterValue> = op
271                .params
272                .iter()
273                .map(|param| match param {
274                    CircuitParam::Fixed(value) => ParameterValue::Fixed(*value),
275                    CircuitParam::Index(index) => {
276                        source_circuit.parameters()[*index as usize].clone().into()
277                    }
278                })
279                .collect();
280
281            target_circuit.append(
282                op.instruction.clone(),
283                mapped_qubits,
284                mapped_params,
285                op.label.as_deref(),
286            )?;
287        }
288
289        Ok(())
290    }
291}
292
293#[cfg(test)]
294#[path = "./virtual_distillation_test.rs"]
295mod virtual_distillation_test;