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;