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