From 75b7a47320aa6939b7f3f9ea6bf1ce56669b1bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Mon, 28 Sep 2026 15:31:50 +0800 Subject: [PATCH 1/6] fix(core): remove unsupported crates.io category --- crates/cqlib-core/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cqlib-core/Cargo.toml b/crates/cqlib-core/Cargo.toml index 93c46666..257f8a3c 100644 --- a/crates/cqlib-core/Cargo.toml +++ b/crates/cqlib-core/Cargo.toml @@ -21,7 +21,6 @@ categories = [ "science", "simulation", "algorithms", - "science::physics", "data-structures" ] include = [ From c02851cb01adf9eb915dfbe480c3f7c07f2b6cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Tue, 29 Sep 2026 14:30:10 +0800 Subject: [PATCH 2/6] fix(em): rebuild virtual distillation as a Hadamard-test circuit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The copy-swap circuit inserted uncontrolled SWAPs between copies. Since S rho^⊗M S† = rho^⊗M, a bare permutation is a no-op on the product state, so neither Tr(O rho^M) nor Tr(rho^M) was ever measured and the mitigated estimate degenerated to ^copies (e.g. VD of on |1> returned +1 instead of -1). Replace it with the Hadamard-test circuit of Huggins et al. (PRX 11, 041036, App. C): copies preparations, one ancillary qubit prepared in |+>, and a ladder of ancilla-controlled SWAPs between adjacent copies realizing the derangement. The numerator observable is the original Hamiltonian on the first copy tensored with X on the ancilla; the denominator observable is X on the ancilla alone. BREAKING CHANGE: estimators now receive a Hamiltonian on the denominator call too (a single X on the ancilla) instead of None; distinguish numerator and denominator by observable content. The copy-swap circuit width grows from copies * base_width to copies * base_width + 1. Add exact density-matrix tests: |1> recovers = -1, pure states Ry(theta) are invariant for copies 2 and 3, and depolarizing noise matches the analytic Tr(Z rho^2)/Tr(rho^2). Update C/Python bindings and the CN/EN docs to the new contract. --- .../error_mitigation/virtual_distillation.rs | 28 +- .../binding-c/tests/error_mitigation_test.rs | 17 +- .../binding-c/tests/parity_compile_em_test.rs | 5 +- .../binding-c/tests/test_parity_compile_em.c | 3 +- .../error_mitigation/virtual_distillation.py | 15 +- .../error_mitigation/virtual_distillation.pyi | 43 ++- .../tests/error_mitigation/test_pipeline.py | 45 ++- .../error_mitigation/error_mitigation_test.rs | 14 +- .../src/error_mitigation/unified.rs | 22 +- .../error_mitigation/virtual_distillation.rs | 109 +++++-- .../virtual_distillation_test.rs | 283 +++++++++++++++--- .../api/cn/c/6_error_mitigation/0_overview.md | 2 +- .../2_virtual_distillation.md | 33 +- docs/api/cn/c/6_error_mitigation/3_unified.md | 23 +- .../python/6_error_mitigation/0_overview.md | 34 ++- .../2_virtual_distillation.md | 33 +- .../cn/python/6_error_mitigation/3_unified.md | 2 +- .../cn/rust/6_error_mitigation/0_overview.md | 27 +- .../2_virtual_distillation.md | 31 +- .../cn/rust/6_error_mitigation/3_unified.md | 2 +- .../api/en/c/6_error_mitigation/0_overview.md | 2 +- .../2_virtual_distillation.md | 35 +-- docs/api/en/c/6_error_mitigation/3_unified.md | 27 +- .../python/6_error_mitigation/0_overview.md | 34 ++- .../2_virtual_distillation.md | 33 +- .../en/python/6_error_mitigation/3_unified.md | 2 +- .../en/rust/6_error_mitigation/0_overview.md | 27 +- .../2_virtual_distillation.md | 31 +- .../en/rust/6_error_mitigation/3_unified.md | 2 +- .../1_cqlib/6_error_mitigation/0_overview.md | 2 +- .../2_virtual_distillation.md | 44 ++- .../6_error_mitigation/3_unified_api.md | 18 +- .../1_cqlib/6_error_mitigation/0_overview.md | 2 +- .../2_virtual_distillation.md | 45 ++- .../6_error_mitigation/3_unified_api.md | 19 +- 35 files changed, 765 insertions(+), 329 deletions(-) diff --git a/crates/binding-c/src/error_mitigation/virtual_distillation.rs b/crates/binding-c/src/error_mitigation/virtual_distillation.rs index 78b42084..b40ceb61 100644 --- a/crates/binding-c/src/error_mitigation/virtual_distillation.rs +++ b/crates/binding-c/src/error_mitigation/virtual_distillation.rs @@ -45,10 +45,14 @@ pub extern "C" fn virtual_distillation_free(ptr: *mut CVirtualDistillation) { } } -/// Builds the copy-swap circuit used by the virtual distillation protocol. +/// Builds the copy-swap circuit used by the virtual distillation protocol +/// (a Hadamard-test circuit: one preparation per configured copy, a Hadamard +/// on a trailing ancilla qubit, and a ladder of ancilla-controlled SWAPs +/// between adjacent copies). /// -/// Returns an owned `CCircuit*` (free with `circuit_free`), or NULL -/// on error. +/// The circuit width is `copies * base_width + 1`, where the last qubit is +/// the ancilla. Returns an owned `CCircuit*` (free with `circuit_free`), or +/// NULL on error. #[unsafe(no_mangle)] pub extern "C" fn virtual_distillation_build_circuit( ptr: *const CVirtualDistillation, @@ -66,8 +70,10 @@ pub extern "C" fn virtual_distillation_build_circuit( /// Builds the copy-swap circuit from the base circuit (the C mirror of /// `VirtualDistillation::build_copy_swap_circuit`). /// -/// The circuit contains one preparation per configured copy plus pairwise -/// SWAPs against the first copy. Returns an owned `CCircuit*` (free with +/// The circuit contains one preparation per configured copy, a Hadamard +/// gate on a trailing ancilla qubit, and a ladder of ancilla-controlled +/// SWAPs (McGates) between adjacent copies. The width is +/// `copies * base_width + 1`. Returns an owned `CCircuit*` (free with /// `circuit_free`), or NULL on NULL input or a build failure. #[unsafe(no_mangle)] pub extern "C" fn virtual_distillation_build_copy_swap_circuit( @@ -116,8 +122,9 @@ pub extern "C" fn virtual_distillation_set_copies( /// mean and variance to the out pointers. /// /// The estimator receives the copy-swap circuit, a Hamiltonian expanded to -/// the full copy-swap width, and `shots`. Returns 0 on success or a negative -/// error code. +/// the full copy-swap width (the original Pauli terms on the first copy's +/// qubits, identity elsewhere, and an X on the ancilla qubit), and `shots`. +/// Returns 0 on success or a negative error code. #[unsafe(no_mangle)] pub extern "C" fn virtual_distillation_run_numerator_circuit( ptr: *const CVirtualDistillation, @@ -148,8 +155,9 @@ pub extern "C" fn virtual_distillation_run_numerator_circuit( /// Runs the denominator circuit through `estimator` and writes the estimated /// mean and variance to the out pointers. /// -/// The estimator receives the copy-swap circuit, no Hamiltonian (NULL), and -/// `shots`. Returns 0 on success or a negative error code. +/// The estimator receives the copy-swap circuit, the denominator Hamiltonian +/// (a single X term on the ancilla qubit, identity elsewhere), and `shots`. +/// Returns 0 on success or a negative error code. #[unsafe(no_mangle)] pub extern "C" fn virtual_distillation_run_denominator_circuit( ptr: *const CVirtualDistillation, @@ -171,7 +179,7 @@ pub extern "C" fn virtual_distillation_run_denominator_circuit( } CqlibError::Ok as i32 } - Err(_) => CqlibError::CircuitError as i32, + Err(err) => em_error_code(&err), } } diff --git a/crates/binding-c/tests/error_mitigation_test.rs b/crates/binding-c/tests/error_mitigation_test.rs index e02562e1..03734fc0 100644 --- a/crates/binding-c/tests/error_mitigation_test.rs +++ b/crates/binding-c/tests/error_mitigation_test.rs @@ -493,8 +493,11 @@ fn test_zne_run_em_sequence() { // ===== Fine-grained VirtualDistillation API (checklist section 5) ===== -/// Static estimator: numerator invocations (Hamiltonian present) report -/// (4.0, 0.1); denominator invocations (Hamiltonian NULL) report (2.0, 0.05). +/// Static estimator: numerator invocations report (4.0, 0.1); denominator +/// invocations report (2.0, 0.05). Both receive a non-NULL Hamiltonian now: +/// they are told apart by content — the denominator Hamiltonian is identity +/// on qubit 0 (a single X on the ancilla), while the numerator Hamiltonian +/// carries a non-identity Pauli on qubit 0 (the original Z term). extern "C" fn static_vd_estimator( _circuit: *const binding_c::circuit::CCircuit, hamiltonian: *const binding_c::qis::CHamiltonian, @@ -502,11 +505,14 @@ extern "C" fn static_vd_estimator( expectation: *mut f64, variance: *mut f64, ) { - if expectation.is_null() || variance.is_null() { + if expectation.is_null() || variance.is_null() || hamiltonian.is_null() { return; } unsafe { - if hamiltonian.is_null() { + let hamiltonian = &(*hamiltonian).inner; + let first_term = &hamiltonian.terms[0].0; + let is_denominator = !first_term.x[0] && !first_term.z[0]; + if is_denominator { *expectation = 2.0; *variance = 0.05; } else { @@ -557,7 +563,8 @@ fn test_virtual_distillation_run_vd_sampling() { pauli_string_free(pauli); // Numerator: the estimator sees the copy-swap circuit and the expanded - // Hamiltonian; denominator: no Hamiltonian. + // Hamiltonian (original Pauli on the first copy, X on the ancilla); + // denominator: an ancilla-X Hamiltonian with identity elsewhere. let mut num_mean = f64::NAN; let mut num_var = f64::NAN; assert_eq!( diff --git a/crates/binding-c/tests/parity_compile_em_test.rs b/crates/binding-c/tests/parity_compile_em_test.rs index efcc7873..a82b5d5c 100644 --- a/crates/binding-c/tests/parity_compile_em_test.rs +++ b/crates/binding-c/tests/parity_compile_em_test.rs @@ -145,8 +145,9 @@ fn test_virtual_distillation_build_copy_swap_circuit() { let copy_swap = virtual_distillation_build_copy_swap_circuit(vd); assert!(!copy_swap.is_null()); - // The copy-swap width is copies (2) times the base width (1). - assert_eq!(circuit_num_qubits(copy_swap), 2); + // The copy-swap width is copies (2) times the base width (1) plus one + // ancilla qubit. + assert_eq!(circuit_num_qubits(copy_swap), 3); assert!( circuit_num_operations(copy_swap) > 0, "copy-swap circuit should contain operations" diff --git a/crates/binding-c/tests/test_parity_compile_em.c b/crates/binding-c/tests/test_parity_compile_em.c index 839620ee..99cba868 100644 --- a/crates/binding-c/tests/test_parity_compile_em.c +++ b/crates/binding-c/tests/test_parity_compile_em.c @@ -64,7 +64,8 @@ static void test_copy_swap_circuit(void) { CCircuit* copy_swap = virtual_distillation_build_copy_swap_circuit(vd); assert(copy_swap != NULL); - assert(circuit_num_qubits(copy_swap) == 2); + /* copies (2) * base width (1) + 1 ancilla qubit. */ + assert(circuit_num_qubits(copy_swap) == 3); assert(circuit_num_operations(copy_swap) > 0); circuit_free(copy_swap); diff --git a/crates/binding-python/cqlib/error_mitigation/virtual_distillation.py b/crates/binding-python/cqlib/error_mitigation/virtual_distillation.py index 133298d6..fb76d106 100644 --- a/crates/binding-python/cqlib/error_mitigation/virtual_distillation.py +++ b/crates/binding-python/cqlib/error_mitigation/virtual_distillation.py @@ -12,8 +12,13 @@ """Virtual distillation bindings. -Virtual distillation builds copy-swap circuits from a base circuit and combines -numerator and denominator estimates into a mitigated expectation value. +Virtual distillation builds Hadamard-test copy-swap circuits from a base +circuit: ``copies`` disjoint copies of the base circuit, a Hadamard gate on a +trailing ancillary qubit, and a ladder of ancilla-controlled SWAP gates +between adjacent copies. Numerator and denominator estimates are combined +into a mitigated expectation value; both estimator calls receive a +``Hamiltonian`` observable (the denominator observable is a single X on the +ancillary qubit). """ from collections.abc import Callable @@ -23,6 +28,12 @@ from .._native import error_mitigation as _error_mitigation_module +# Estimators receive the (widened) circuit, an optional Hamiltonian, and an +# optional shot count, and return ``(mean, variance)``. Virtual distillation +# passes a Hamiltonian on both the numerator call (the observable expanded +# with X on the ancillary qubit) and the denominator call (a single X on the +# ancillary qubit); distinguish the two by observable content, not by +# ``observable is None``. Estimator = Callable[[Circuit, Hamiltonian | None, int | None], tuple[float, float]] VirtualDistillationConfig = _error_mitigation_module.VirtualDistillationConfig VirtualDistillation = _error_mitigation_module.VirtualDistillation diff --git a/crates/binding-python/cqlib/error_mitigation/virtual_distillation.pyi b/crates/binding-python/cqlib/error_mitigation/virtual_distillation.pyi index 84921c33..7cf66c63 100644 --- a/crates/binding-python/cqlib/error_mitigation/virtual_distillation.pyi +++ b/crates/binding-python/cqlib/error_mitigation/virtual_distillation.pyi @@ -17,6 +17,12 @@ from collections.abc import Callable from cqlib.circuit import Circuit from cqlib.qis import Hamiltonian +# Estimators receive the (widened) circuit, an optional Hamiltonian, and an +# optional shot count, and return ``(mean, variance)``. Virtual distillation +# passes a Hamiltonian on both the numerator call (the observable expanded +# with X on the ancillary qubit) and the denominator call (a single X on the +# ancillary qubit); distinguish the two by observable content, not by +# ``observable is None``. Estimator = Callable[[Circuit, Hamiltonian | None, int | None], tuple[float, float]] class VirtualDistillationConfig: @@ -40,8 +46,10 @@ class VirtualDistillationConfig: class VirtualDistillation: """Low-level virtual distillation helper. - The helper builds copy-swap circuits and combines numerator and denominator - estimator outputs into a mitigated expectation value and variance. + The helper builds Hadamard-test copy-swap circuits and combines numerator + and denominator estimator outputs into a mitigated expectation value and + variance. Both estimator calls receive a ``Hamiltonian`` observable; the + denominator observable is a single X on the ancillary (last) qubit. """ def __init__(self, circuit: Circuit, copies: int) -> None: @@ -55,14 +63,27 @@ class VirtualDistillation: """Update the configured number of copies.""" ... def build_copy_swap_circuit(self) -> Circuit: - """Build the copy-swap circuit used by virtual distillation.""" + """Build the Hadamard-test copy-swap circuit used by virtual distillation. + + The returned circuit has width ``copies * base_width + 1``, where the + last qubit is the ancillary qubit. It contains, in order: the + ``copies`` copies of the base circuit, a Hadamard gate on the + ancillary qubit, and ``(copies - 1) * base_width`` ancilla-controlled + SWAP gates (exposed as ``C1-SWAP``) forming a permutation ladder + between adjacent copies. + """ ... def run_denominator_circuit( self, shots: int, estimator: Estimator, ) -> tuple[float, float]: - """Run the denominator circuit and return ``(mean, variance)``.""" + """Run the denominator circuit and return ``(mean, variance)``. + + ``estimator`` receives the copy-swap circuit together with the + denominator observable: a single X on the ancillary (last) qubit and + identity on every other qubit. + """ ... def run_numerator_circuit( self, @@ -70,7 +91,13 @@ class VirtualDistillation: shots: int, estimator: Estimator, ) -> tuple[float, float]: - """Run the numerator circuit and return ``(mean, variance)``.""" + """Run the numerator circuit and return ``(mean, variance)``. + + ``estimator`` receives the copy-swap circuit together with + ``hamiltonian`` expanded to the full circuit width: the original + Pauli terms on the first copy's qubits, identity on the remaining + copies, and X on the ancillary (last) qubit. + """ ... def run_vd( self, @@ -79,7 +106,11 @@ class VirtualDistillation: shots_denominator: int, estimator: Estimator, ) -> tuple[float, float]: - """Run virtual distillation and return ``(mitigated_mean, variance)``.""" + """Run virtual distillation and return ``(mitigated_mean, variance)``. + + The mitigated mean is the numerator estimate divided by the + denominator estimate. + """ ... def __repr__(self) -> str: ... def __copy__(self) -> VirtualDistillation: ... diff --git a/crates/binding-python/tests/error_mitigation/test_pipeline.py b/crates/binding-python/tests/error_mitigation/test_pipeline.py index d8fb499b..f3ee6498 100644 --- a/crates/binding-python/tests/error_mitigation/test_pipeline.py +++ b/crates/binding-python/tests/error_mitigation/test_pipeline.py @@ -34,6 +34,20 @@ def single_x_circuit() -> Circuit: return circuit +def is_numerator_observable(observable: Hamiltonian) -> bool: + """Distinguish VD numerator from denominator observables by content. + + The denominator observable is a single X on the ancillary (last) qubit; + the numerator also carries non-identity Paulis on the first copy's + qubits. + """ + return any( + qubit < observable.num_qubits - 1 + for term, _coefficient in observable.terms + for qubit in term.support() + ) + + def test_error_mitigation_public_exports_and_estimator_aliases() -> None: assert em.ZNEMitigation.__module__ == "cqlib.error_mitigation" assert em.ErrorMitigation.__module__ == "cqlib.error_mitigation" @@ -122,10 +136,10 @@ def test_virtual_distillation_copy_swap_and_run_paths() -> None: vd = em.VirtualDistillation(circuit, 2) copy_swap = vd.build_copy_swap_circuit() - assert copy_swap.width == 2 + assert copy_swap.width == 3 assert [ operation.instruction.instruction.name for operation in copy_swap.operations - ] == ["SWAP"] + ] == ["H", "C1-SWAP"] calls: list[tuple[int, int | None, bool]] = [] @@ -134,10 +148,16 @@ def estimator( observable: Hamiltonian | None, shots: int | None, ) -> tuple[float, float]: - calls.append((run_circuit.width, shots, observable is None)) - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + assert observable is not None + is_numerator = is_numerator_observable(observable) + # Numerator: Z on the first copy's qubit, X on the ancilla. + # Denominator: only X on the ancilla. (str prints highest index first.) + terms = [str(term) for term, _coefficient in observable.terms] + assert terms == (["+XIZ"] if is_numerator else ["+XII"]) + calls.append((run_circuit.width, shots, is_numerator)) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) assert vd.run_numerator_circuit(hamiltonian, 3, estimator) == (1.5, 0.25) assert vd.run_denominator_circuit(2, estimator) == (2.0, 1.0) @@ -146,10 +166,10 @@ def estimator( assert mitigated[0] == pytest.approx(0.75) assert mitigated[1] == pytest.approx(0.203125) assert calls == [ - (2, 3, False), - (2, 2, True), - (2, 3, False), - (2, 2, True), + (3, 3, True), + (3, 2, False), + (3, 3, True), + (3, 2, False), ] @@ -208,8 +228,9 @@ def estimator( observable: Hamiltonian | None, shots: int | None, ) -> tuple[float, float]: - assert run_circuit.width == 2 - if observable is None: + assert run_circuit.width == 3 + assert observable is not None + if not is_numerator_observable(observable): assert shots == 2 return (2.0, 1.0) assert shots == 3 diff --git a/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs b/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs index 55114d3a..34fd8823 100644 --- a/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs +++ b/crates/cqlib-core/src/error_mitigation/error_mitigation_test.rs @@ -270,14 +270,20 @@ fn test_error_mitigation_vd_run_then_get_mitigated() { }, &|circuit, hamiltonian_arg, shots| { let ops = circuit.operations(); - assert_eq!(ops.len(), 1); + assert_eq!(ops.len(), 2); assert!(matches!( ops[0].instruction, - Instruction::Standard(StandardGate::SWAP) + Instruction::Standard(StandardGate::H) )); + assert!(matches!(ops[1].instruction, Instruction::McGate(_))); - if let Some(expanded_hamiltonian) = hamiltonian_arg { - assert_eq!(expanded_hamiltonian.num_qubits, 2); + let expanded_hamiltonian = + hamiltonian_arg.expect("numerator and denominator both provide a Hamiltonian"); + assert_eq!(expanded_hamiltonian.num_qubits, 3); + let (term, _coeff) = &expanded_hamiltonian.terms[0]; + assert_eq!((term.x[2], term.z[2]), (true, false)); + + if (term.x[0], term.z[0]) != (false, false) { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { diff --git a/crates/cqlib-core/src/error_mitigation/unified.rs b/crates/cqlib-core/src/error_mitigation/unified.rs index f6ba05a3..593a4541 100644 --- a/crates/cqlib-core/src/error_mitigation/unified.rs +++ b/crates/cqlib-core/src/error_mitigation/unified.rs @@ -221,7 +221,10 @@ struct VirtualDistillationRunRecord { /// /// // A portal to real quantum device or classical simulation backend that calculates and its variance /// let estimator = |_circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, _shots: Option| { -/// if hamiltonian.is_some() { +/// // Both the numerator and the denominator carry a Hamiltonian: the +/// // numerator additionally acts on the first copy (qubit 0 here). +/// let term = &hamiltonian.unwrap().terms[0].0; +/// if term.x[0] || term.z[0] { /// (1.5, 0.25) /// } else { /// (2.0, 1.0) @@ -307,7 +310,10 @@ impl ErrorMitigation { /// /// // A portal to real quantum device or classical simulation backend that calculates and its variance /// let estimator = |_circuit: &Circuit, hamiltonian: Option<&Hamiltonian>, _shots: Option| { - /// if hamiltonian.is_some() { + /// // Both the numerator and the denominator carry a Hamiltonian: the + /// // numerator additionally acts on the first copy (qubit 0 here). + /// let term = &hamiltonian.unwrap().terms[0].0; + /// if term.x[0] || term.z[0] { /// (1.0, 0.1) /// } else { /// (2.0, 0.2) @@ -526,16 +532,8 @@ impl ErrorMitigation { }); } - let copy_swap_circuit = vd.build_copy_swap_circuit()?; - let extra_qubits = copy_swap_circuit.width() - hamiltonian.num_qubits; - let expanded_hamiltonian = - VirtualDistillation::expand_hamiltonian(hamiltonian, extra_qubits)?; - let numerator = estimator( - ©_swap_circuit, - Some(&expanded_hamiltonian), - Some(shots_numerator), - ); - let denominator = estimator(©_swap_circuit, None, Some(shots_denominator)); + let numerator = vd.run_numerator_circuit(hamiltonian, shots_numerator, estimator)?; + let denominator = vd.run_denominator_circuit(shots_denominator, estimator)?; Ok(VirtualDistillationRunRecord { numerator, diff --git a/crates/cqlib-core/src/error_mitigation/virtual_distillation.rs b/crates/cqlib-core/src/error_mitigation/virtual_distillation.rs index 387c42b4..587e8db7 100644 --- a/crates/cqlib-core/src/error_mitigation/virtual_distillation.rs +++ b/crates/cqlib-core/src/error_mitigation/virtual_distillation.rs @@ -12,7 +12,9 @@ use std::collections::HashMap; -use crate::circuit::{Circuit, CircuitError, CircuitParam, ParameterValue, Qubit}; +use num_complex::Complex64; + +use crate::circuit::{Circuit, CircuitError, CircuitParam, ParameterValue, Qubit, StandardGate}; use crate::error_mitigation::ErrorMitigationError; use crate::error_mitigation::Estimator; use crate::qis::{Hamiltonian, Pauli, PauliString}; @@ -65,53 +67,76 @@ impl VirtualDistillation { Ok(()) } - /// Builds a copy-swap circuit from the configured base circuit. + /// Builds the virtual distillation circuit from the configured base circuit. /// /// The returned circuit contains: /// - `copies` disjoint copies of the base circuit preparation, - /// - pairwise SWAP operations between the first copy and every additional copy. + /// - one ancillary qubit (the last qubit, index `copies * base_width`), + /// prepared in `|+>` by a Hadamard gate, + /// - a derangement of the copies implemented as a ladder of swap gates + /// between adjacent copies, each controlled by the ancillary qubit + /// (CSWAP). + /// + /// This is the Hadamard-test circuit of the virtual distillation protocol: + /// measuring `X` on the ancillary qubit (together with an observable on the + /// first copy) yields `Tr(O rho^M)` / `Tr(rho^M)`. Applying uncontrolled + /// swaps instead would leave the permutation-symmetric state `rho^M` + /// invariant and measure nothing related to the distilled moments. pub fn build_copy_swap_circuit(&self) -> Result { let base_circuit = self.circuit.decompose()?; let base_width = base_circuit.width(); - let mut copy_swap_circuit = Circuit::new(self.copies * base_width); + let ancilla = Qubit::new((self.copies * base_width) as u32); + let mut copy_swap_circuit = Circuit::new(self.copies * base_width + 1); for copy_index in 0..self.copies { let copy_offset = copy_index * base_width; Self::append_circuit_with_offset(&mut copy_swap_circuit, &base_circuit, copy_offset)?; } - for other_copy in 1..self.copies { - let first_copy_offset = 0; - let other_copy_offset = other_copy * base_width; + copy_swap_circuit.h(ancilla)?; + + for lower_copy in 0..(self.copies - 1) { + let lower_offset = lower_copy * base_width; + let upper_offset = (lower_copy + 1) * base_width; for qubit_index in 0..base_width { - let left = Qubit::new((first_copy_offset + qubit_index) as u32); - let right = Qubit::new((other_copy_offset + qubit_index) as u32); - copy_swap_circuit.swap(left, right)?; + let lower = Qubit::new((lower_offset + qubit_index) as u32); + let upper = Qubit::new((upper_offset + qubit_index) as u32); + copy_swap_circuit.multi_control( + StandardGate::SWAP, + [ancilla], + [lower, upper], + [], + )?; } } Ok(copy_swap_circuit) } - /// Expands a Hamiltonian to the full copy-swap circuit width. + /// Expands a Hamiltonian to the numerator observable on the full virtual + /// distillation circuit width. /// - /// Virtual distillation evaluates the numerator on a wider copy-swap circuit, - /// so the estimator needs a Hamiltonian of matching width. This helper keeps - /// the original Pauli operators on their current qubit indices and appends - /// `n` `Z`s on higher-index qubits. + /// The numerator of virtual distillation estimates `Tr(O rho^M)`, measured + /// as the expectation value of `X` on the ancillary qubit tensored with the + /// observable on the first copy and identities on the remaining copies. + /// This helper keeps the original Pauli operators on their current qubit + /// indices, pads identities on the additional copies, and appends an `X` + /// on the ancillary qubit (the last qubit). pub(crate) fn expand_hamiltonian( hamiltonian: &Hamiltonian, - n: usize, + copies: usize, ) -> Result { + let width = hamiltonian.num_qubits * copies + 1; + let ancilla = width - 1; if hamiltonian.terms.is_empty() { - return Ok(Hamiltonian::new(hamiltonian.num_qubits + n)); + return Ok(Hamiltonian::new(width)); } let expanded_terms = hamiltonian .terms .iter() .map(|(term, coeff)| { - let mut expanded_term = PauliString::new(hamiltonian.num_qubits + n); + let mut expanded_term = PauliString::new(width); expanded_term.phase = term.phase; for qubit in 0..hamiltonian.num_qubits { @@ -124,9 +149,7 @@ impl VirtualDistillation { expanded_term.set_pauli(qubit, pauli); } - for qubit in hamiltonian.num_qubits..(hamiltonian.num_qubits + n) { - expanded_term.set_pauli(qubit, Pauli::Z); - } + expanded_term.set_pauli(ancilla, Pauli::X); (expanded_term, *coeff) }) @@ -135,35 +158,58 @@ impl VirtualDistillation { Ok(Hamiltonian::from_list(expanded_terms)?) } + /// Returns the denominator observable of virtual distillation. + /// + /// The denominator estimates `Tr(rho^M)`, measured as the expectation + /// value of `X` on the ancillary qubit (the last qubit) with identities + /// everywhere else. + fn denominator_hamiltonian(&self) -> Result { + let width = self.copies * self.circuit.width() + 1; + let mut term = PauliString::new(width); + term.set_pauli(width - 1, Pauli::X); + Ok(Hamiltonian::from_list(vec![( + term, + Complex64::new(1.0, 0.0), + )])?) + } + /// Runs the denominator circuit and returns the estimated mean and variance. /// /// - `shots`: the number of shots to run the circuit. - /// - `estimator`: an estimator that evaluates the copy-swap circuit, with no - /// Hamiltonian (`None`) and the provided shot count. + /// - `estimator`: an estimator that evaluates the virtual distillation + /// circuit with the denominator observable (`X` on the ancillary qubit, + /// identities elsewhere) and the provided shot count. /// /// # Returns /// - /// - `(mu, var)`: the estimated mean and variance of the denominator circuit. + /// - `(mu, var)`: the estimated mean and variance of `Tr(rho^M)`. pub fn run_denominator_circuit( &self, shots: usize, estimator: &Estimator<'_>, - ) -> Result<(f64, f64), CircuitError> { + ) -> Result<(f64, f64), ErrorMitigationError> { let denominator_circuit = self.build_copy_swap_circuit()?; + let denominator_hamiltonian = self.denominator_hamiltonian()?; - Ok(estimator(&denominator_circuit, None, Some(shots))) + Ok(estimator( + &denominator_circuit, + Some(&denominator_hamiltonian), + Some(shots), + )) } /// Runs the numerator circuit and returns the estimated mean and variance. /// - /// - `hamiltonian`: the Hamiltonian to estimate on the copy-swap circuit. + /// - `hamiltonian`: the Hamiltonian to estimate on the virtual distillation + /// circuit. It is expanded to the full circuit width with `X` on the + /// ancillary qubit (see [`Self::expand_hamiltonian`]). /// - `shots`: the number of shots to run the circuit. - /// - `estimator`: an estimator that evaluates the copy-swap circuit, the given - /// expanded Hamiltonian, and the provided shot count. + /// - `estimator`: an estimator that evaluates the virtual distillation + /// circuit, the given expanded Hamiltonian, and the provided shot count. /// /// # Returns /// - /// - `(mu, var)`: the estimated mean and variance of the numerator circuit. + /// - `(mu, var)`: the estimated mean and variance of `Tr(H rho^M)`. pub fn run_numerator_circuit( &self, hamiltonian: &Hamiltonian, @@ -171,8 +217,7 @@ impl VirtualDistillation { estimator: &Estimator<'_>, ) -> Result<(f64, f64), ErrorMitigationError> { let numerator_circuit = self.build_copy_swap_circuit()?; - let extra_qubits = (self.copies - 1) * hamiltonian.num_qubits; - let expanded_hamiltonian = Self::expand_hamiltonian(hamiltonian, extra_qubits)?; + let expanded_hamiltonian = Self::expand_hamiltonian(hamiltonian, self.copies)?; if expanded_hamiltonian.num_qubits != numerator_circuit.width() { return Err(CircuitError::QubitCountMismatch { expected: numerator_circuit.width(), diff --git a/crates/cqlib-core/src/error_mitigation/virtual_distillation_test.rs b/crates/cqlib-core/src/error_mitigation/virtual_distillation_test.rs index e9d9cc38..530ab84c 100644 --- a/crates/cqlib-core/src/error_mitigation/virtual_distillation_test.rs +++ b/crates/cqlib-core/src/error_mitigation/virtual_distillation_test.rs @@ -11,12 +11,12 @@ // that they have been altered from the originals. use super::VirtualDistillation; -use crate::circuit::Qubit; use crate::circuit::circuit_impl::Circuit; use crate::circuit::gate::Instruction; use crate::circuit::gate::standard_gate::StandardGate; +use crate::circuit::{CircuitParam, Qubit}; use crate::error_mitigation::ErrorMitigationError; -use crate::qis::{Hamiltonian, Pauli, PauliString}; +use crate::qis::{DensityMatrix, Hamiltonian, Pauli, PauliString}; use num_complex::Complex64; fn single_qubit_z_hamiltonian() -> Hamiltonian { @@ -33,6 +33,88 @@ fn single_qubit_x_hamiltonian() -> Hamiltonian { .expect("single-qubit X Hamiltonian should be valid") } +/// Exact density-matrix estimator used by the numerical tests. +fn exact_estimator( + circuit: &Circuit, + hamiltonian: Option<&Hamiltonian>, + _shots: Option, +) -> (f64, f64) { + let mut state = DensityMatrix::new(circuit.num_qubits()); + state.apply_circuit(circuit).unwrap(); + let hamiltonian = hamiltonian.expect("numerator and denominator both provide a Hamiltonian"); + (state.expectation(hamiltonian).unwrap(), 0.0) +} + +/// Estimator that prepares `copies` depolarized states and then applies the +/// controlled derangement. It simulates the preparation ops, injects an +/// identical depolarizing channel with strength `p` on every copy register +/// (all qubits except the ancillary), and finally applies the Hadamard on the +/// ancillary and the controlled swap ladder. Depolarizing the disjoint copy +/// registers after the whole preparation phase is equivalent to depolarizing +/// each copy right after its own preparation. +fn depolarizing_estimator( + p: f64, +) -> impl Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) { + move |circuit, hamiltonian, _shots| { + let num_qubits = circuit.num_qubits(); + let mut state = DensityMatrix::new(num_qubits); + + let keep = (1.0 - p).sqrt(); + let flip = (p / 3.0).sqrt(); + let c = |re: f64, im: f64| Complex64::new(re, im); + let k0 = vec![c(keep, 0.0), c(0.0, 0.0), c(0.0, 0.0), c(keep, 0.0)]; + let k1 = vec![c(0.0, 0.0), c(flip, 0.0), c(flip, 0.0), c(0.0, 0.0)]; + let k2 = vec![c(0.0, 0.0), c(0.0, -flip), c(0.0, flip), c(0.0, 0.0)]; + let k3 = vec![c(flip, 0.0), c(0.0, 0.0), c(0.0, 0.0), c(-flip, 0.0)]; + + // The Hadamard on the ancillary qubit separates the preparation phase + // from the derangement phase. + let derangement_start = circuit + .operations() + .iter() + .position(|op| matches!(op.instruction, Instruction::Standard(StandardGate::H))) + .expect("virtual distillation circuit must contain the ancillary Hadamard"); + + let mut noise_injected = false; + for (index, op) in circuit.operations().iter().enumerate() { + if !noise_injected && index == derangement_start { + for qubit in 0..(num_qubits - 1) { + state + .apply_kraus(&[k0.clone(), k1.clone(), k2.clone(), k3.clone()], &[qubit]) + .unwrap(); + } + noise_injected = true; + } + + let qubit_indices: Vec = op.qubits.iter().map(|q| q.id() as usize).collect(); + let params: Vec = op + .params + .iter() + .map(|param| match param { + CircuitParam::Fixed(value) => *value, + CircuitParam::Index(_) => panic!("test circuits use fixed parameters only"), + }) + .collect(); + match &op.instruction { + Instruction::Standard(gate) => { + state + .apply_standard_gate(*gate, &qubit_indices, ¶ms) + .unwrap(); + } + Instruction::McGate(mc_gate) => { + let matrix = mc_gate.matrix(¶ms).unwrap(); + state.apply_unitary_gate(&qubit_indices, &matrix).unwrap(); + } + instruction => panic!("unexpected instruction in VD circuit: {instruction:?}"), + } + } + + let hamiltonian = + hamiltonian.expect("numerator and denominator both provide a Hamiltonian"); + (state.expectation(hamiltonian).unwrap(), 0.0) + } +} + #[test] fn test_vd_new_accepts_valid_input() { let circuit = Circuit::new(1); @@ -72,8 +154,10 @@ fn test_build_copy_swap_circuit_for_two_single_qubit_copies() { let copy_swap = vd.build_copy_swap_circuit().unwrap(); let ops = copy_swap.operations(); - assert_eq!(copy_swap.width(), 2); - assert_eq!(ops.len(), 3); + // Two base-circuit copies, a Hadamard on the ancillary qubit, and one + // ancillary-controlled SWAP (CSWAP) between the copies. + assert_eq!(copy_swap.width(), 3); + assert_eq!(ops.len(), 4); assert!(matches!( ops[0].instruction, @@ -89,62 +173,114 @@ fn test_build_copy_swap_circuit_for_two_single_qubit_copies() { assert!(matches!( ops[2].instruction, - Instruction::Standard(StandardGate::SWAP) + Instruction::Standard(StandardGate::H) )); - assert_eq!(ops[2].qubits.as_slice(), &[Qubit::new(0), Qubit::new(1)]); + assert_eq!(ops[2].qubits.as_slice(), &[Qubit::new(2)]); + + match &ops[3].instruction { + Instruction::McGate(mc_gate) => { + assert_eq!(mc_gate.num_ctrl_qubits(), 1); + assert_eq!(*mc_gate.base_gate(), StandardGate::SWAP); + } + instruction => panic!("expected a controlled SWAP, got {instruction:?}"), + } + assert_eq!( + ops[3].qubits.as_slice(), + &[Qubit::new(2), Qubit::new(0), Qubit::new(1)] + ); } #[test] -fn test_build_copy_swap_circuit_adds_pairwise_swaps_for_multiple_copies() { +fn test_build_copy_swap_circuit_adds_controlled_swap_ladder_for_multiple_copies() { let vd = VirtualDistillation::new(Circuit::new(1), 3).unwrap(); let copy_swap = vd.build_copy_swap_circuit().unwrap(); let ops = copy_swap.operations(); - assert_eq!(copy_swap.width(), 3); - assert_eq!(ops.len(), 2); + assert_eq!(copy_swap.width(), 4); + assert_eq!(ops.len(), 3); - let swap_count = ops + assert!(matches!( + ops[0].instruction, + Instruction::Standard(StandardGate::H) + )); + assert_eq!(ops[0].qubits.as_slice(), &[Qubit::new(3)]); + + let cswap_qubits: Vec<_> = ops[1..] .iter() - .filter(|op| matches!(op.instruction, Instruction::Standard(StandardGate::SWAP))) - .count(); - assert_eq!(swap_count, 2); + .map(|op| { + match &op.instruction { + Instruction::McGate(mc_gate) => { + assert_eq!(mc_gate.num_ctrl_qubits(), 1); + assert_eq!(*mc_gate.base_gate(), StandardGate::SWAP); + } + instruction => panic!("expected a controlled SWAP, got {instruction:?}"), + } + op.qubits.as_slice().to_vec() + }) + .collect(); + // Swap ladder between adjacent copies, all controlled by the ancillary. + assert_eq!( + cswap_qubits, + vec![ + vec![Qubit::new(3), Qubit::new(0), Qubit::new(1)], + vec![Qubit::new(3), Qubit::new(1), Qubit::new(2)], + ] + ); } #[test] -fn test_expand_hamiltonian_appends_z_on_higher_indices() { +fn test_expand_hamiltonian_places_x_on_ancilla_and_identity_on_extra_copies() { let hamiltonian = single_qubit_x_hamiltonian(); - let expanded = VirtualDistillation::expand_hamiltonian(&hamiltonian, 2).unwrap(); + let expanded = VirtualDistillation::expand_hamiltonian(&hamiltonian, 3).unwrap(); - assert_eq!(expanded.num_qubits, 3); + assert_eq!(expanded.num_qubits, 4); assert_eq!(expanded.terms.len(), 1); let (term, coeff) = &expanded.terms[0]; assert_eq!(*coeff, Complex64::new(1.0, 0.0)); - assert_eq!(term.num_qubits, 3); + assert_eq!(term.num_qubits, 4); assert_eq!(term.phase, crate::qis::Phase::Plus); + // Observable on the first copy, identities on the extra copies, X on the + // ancillary qubit. assert_eq!((term.x[0], term.z[0]), (true, false)); - assert_eq!((term.x[1], term.z[1]), (false, true)); - assert_eq!((term.x[2], term.z[2]), (false, true)); + assert_eq!((term.x[1], term.z[1]), (false, false)); + assert_eq!((term.x[2], term.z[2]), (false, false)); + assert_eq!((term.x[3], term.z[3]), (true, false)); } #[test] -fn test_run_denominator_circuit_runs_copy_swap_circuit() { +fn test_run_denominator_circuit_measures_x_on_ancilla() { let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); let observed_values = vd .run_denominator_circuit(128, &|denominator, hamiltonian, shots| { let denominator_ops = denominator.operations(); - assert_eq!(denominator.width(), 2); - assert_eq!(denominator_ops.len(), 1); + assert_eq!(denominator.width(), 3); + assert_eq!(denominator_ops.len(), 2); assert!(matches!( denominator_ops[0].instruction, - Instruction::Standard(StandardGate::SWAP) + Instruction::Standard(StandardGate::H) + )); + assert!(matches!( + denominator_ops[1].instruction, + Instruction::McGate(_) )); - assert!(hamiltonian.is_none()); assert_eq!(shots, Some(128)); + // The denominator observable Tr(rho^M) is X on the ancillary + // qubit with identities everywhere else. + let denominator_hamiltonian = + hamiltonian.expect("denominator must provide a Hamiltonian"); + assert_eq!(denominator_hamiltonian.num_qubits, 3); + assert_eq!(denominator_hamiltonian.terms.len(), 1); + let (term, coeff) = &denominator_hamiltonian.terms[0]; + assert_eq!(*coeff, Complex64::new(1.0, 0.0)); + assert_eq!((term.x[0], term.z[0]), (false, false)); + assert_eq!((term.x[1], term.z[1]), (false, false)); + assert_eq!((term.x[2], term.z[2]), (true, false)); + (1.0, 0.5) }) .unwrap(); @@ -164,19 +300,22 @@ fn test_run_numerator_circuit_passes_hamiltonian_to_estimator() { .run_numerator_circuit(&hamiltonian, 128, &|numerator, hamiltonian_arg, shots| { let numerator_ops = numerator.operations(); - assert_eq!(numerator.width(), 2); - assert_eq!(numerator_ops.len(), 3); - assert!(hamiltonian_arg.is_some()); + assert_eq!(numerator.width(), 3); + assert_eq!(numerator_ops.len(), 4); assert_eq!(shots, Some(128)); - let expanded_hamiltonian = hamiltonian_arg.unwrap(); - assert_eq!(expanded_hamiltonian.num_qubits, 2); + let expanded_hamiltonian = + hamiltonian_arg.expect("numerator must provide a Hamiltonian"); + assert_eq!(expanded_hamiltonian.num_qubits, 3); assert_eq!(expanded_hamiltonian.terms.len(), 1); + // Observable on the first copy, identity on the extra copy, X on + // the ancillary qubit. let (term, coeff) = &expanded_hamiltonian.terms[0]; assert_eq!(*coeff, Complex64::new(1.0, 0.0)); assert_eq!((term.x[0], term.z[0]), (true, false)); - assert_eq!((term.x[1], term.z[1]), (false, true)); + assert_eq!((term.x[1], term.z[1]), (false, false)); + assert_eq!((term.x[2], term.z[2]), (true, false)); (1.0, 0.25) }) @@ -194,21 +333,29 @@ fn test_run_vd_returns_mu_and_var() { .run_vd(&hamiltonian, 3, 2, &|circuit, hamiltonian_arg, shots| { let ops = circuit.operations(); - assert_eq!(ops.len(), 1); + assert_eq!(ops.len(), 2); assert!(matches!( ops[0].instruction, - Instruction::Standard(StandardGate::SWAP) + Instruction::Standard(StandardGate::H) )); + assert!(matches!(ops[1].instruction, Instruction::McGate(_))); - if let Some(expanded_hamiltonian) = hamiltonian_arg { - assert_eq!(expanded_hamiltonian.num_qubits, 2); - assert_eq!(expanded_hamiltonian.terms.len(), 1); - let (term, _coeff) = &expanded_hamiltonian.terms[0]; + let expanded_hamiltonian = + hamiltonian_arg.expect("numerator and denominator both provide a Hamiltonian"); + assert_eq!(expanded_hamiltonian.num_qubits, 3); + assert_eq!(expanded_hamiltonian.terms.len(), 1); + let (term, _coeff) = &expanded_hamiltonian.terms[0]; + // X on the ancillary qubit in both numerator and denominator. + assert_eq!((term.x[2], term.z[2]), (true, false)); + + if (term.x[0], term.z[0]) != (false, false) { + // Numerator: the observable acts on the first copy. assert_eq!((term.x[0], term.z[0]), (false, true)); - assert_eq!((term.x[1], term.z[1]), (false, true)); assert_eq!(shots, Some(3)); (1.5, 0.25) } else { + // Denominator: identities on all copy qubits. + assert_eq!((term.x[1], term.z[1]), (false, false)); assert_eq!(shots, Some(2)); (2.0, 1.0) } @@ -225,11 +372,11 @@ fn test_run_vd_forwards_zero_samples_to_estimator() { let hamiltonian = single_qubit_z_hamiltonian(); let (mu_vd, var_vd) = vd .run_vd(&hamiltonian, 0, 0, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { - assert_eq!(shots, Some(0)); + assert_eq!(shots, Some(0)); + let term = &hamiltonian_arg.unwrap().terms[0].0; + if (term.x[0], term.z[0]) != (false, false) { (1.0, 0.5) } else { - assert_eq!(shots, Some(0)); (2.0, 1.0) } }) @@ -269,7 +416,8 @@ fn test_run_vd_rejects_zero_denominator_mean() { let hamiltonian = single_qubit_z_hamiltonian(); let err = vd .run_vd(&hamiltonian, 2, 2, &|_circuit, hamiltonian_arg, _shots| { - if hamiltonian_arg.is_some() { + let term = &hamiltonian_arg.unwrap().terms[0].0; + if (term.x[0], term.z[0]) != (false, false) { (1.0, 0.0) } else { (0.0, 0.0) @@ -279,3 +427,56 @@ fn test_run_vd_rejects_zero_denominator_mean() { assert!(matches!(err, ErrorMitigationError::ZeroDenominatorMean)); } + +#[test] +fn test_vd_exact_simulation_recovers_sign_on_excited_state() { + // Preparing |1> and measuring Z: virtual distillation must return -1. + let mut circuit = Circuit::new(1); + circuit.x(Qubit::new(0)).unwrap(); + + let vd = VirtualDistillation::new(circuit, 2).unwrap(); + let hamiltonian = single_qubit_z_hamiltonian(); + let (mu_vd, _) = vd.run_vd(&hamiltonian, 0, 0, &exact_estimator).unwrap(); + + assert!((mu_vd + 1.0).abs() < 1e-12); +} + +#[test] +fn test_vd_exact_simulation_on_rotated_pure_state() { + // Pure states are invariant under distillation: VD returns the exact + // expectation value = cos(theta). + let theta = 0.7; + let mut circuit = Circuit::new(1); + circuit.ry(Qubit::new(0), theta).unwrap(); + + for copies in [2, 3] { + let vd = VirtualDistillation::new(circuit.clone(), copies).unwrap(); + let hamiltonian = single_qubit_z_hamiltonian(); + let (mu_vd, _) = vd.run_vd(&hamiltonian, 0, 0, &exact_estimator).unwrap(); + + assert!((mu_vd - theta.cos()).abs() < 1e-12); + } +} + +#[test] +fn test_vd_mitigates_depolarizing_noise() { + let p = 0.1; + let mut circuit = Circuit::new(1); + circuit.x(Qubit::new(0)).unwrap(); + + let vd = VirtualDistillation::new(circuit, 2).unwrap(); + let hamiltonian = single_qubit_z_hamiltonian(); + let (mu_vd, _) = vd + .run_vd(&hamiltonian, 0, 0, &depolarizing_estimator(p)) + .unwrap(); + + // Depolarized single-copy state rho = a|1><1| + b|0><0|; VD with M = 2 + // estimates Tr(Z rho^2) / Tr(rho^2). + let a = 1.0 - 2.0 * p / 3.0; + let b = 2.0 * p / 3.0; + let unmitigated = b - a; + let expected_vd = (b * b - a * a) / (a * a + b * b); + + assert!((mu_vd - expected_vd).abs() < 1e-12); + assert!((mu_vd + 1.0).abs() < (unmitigated + 1.0).abs()); +} diff --git a/docs/api/cn/c/6_error_mitigation/0_overview.md b/docs/api/cn/c/6_error_mitigation/0_overview.md index 4d7e0c1c..08fd7735 100644 --- a/docs/api/cn/c/6_error_mitigation/0_overview.md +++ b/docs/api/cn/c/6_error_mitigation/0_overview.md @@ -56,7 +56,7 @@ typedef void (*CEstimatorFn)(const struct CCircuit *circuit, - **指针有效期**:`circuit`、`hamiltonian` 及全部出参指针仅在回调执行期间有效;回调返回后不得继续使用,也不得保存到回调之外。 - **hamiltonian 所有权**:`hamiltonian` 句柄归库所有,回调不得释放它。回调收到的哈密顿量可能已按流程需要扩展到折叠/拷贝线路的宽度。 -- **出参写入**:回调必须通过 `expectation` 写入期望值;方差已知时写入 `variance`,未知时写入 NaN。虚拟蒸馏的分母电路估计中 `hamiltonian` 为 NULL,回调按无可观测量处理。 +- **出参写入**:回调必须通过 `expectation` 写入期望值;方差已知时写入 `variance`,未知时写入 NaN。虚拟蒸馏的分子与分母估计都会传入非 NULL 的 `hamiltonian`:分子可观测量作用在第 0 份副本的比特上并在辅助比特上携带 `X`,分母可观测量只在辅助比特上携带 `X`(其余比特为恒等)。 - **shots 语义**:`shots` 为 0 表示「未指定」,回调自行决定采样规模;非 0 时为调用方建议的采样数。 回调内部可自由调用模拟器接口(如 [态矢量](../3_qis/1_statevector.md) 的 `statevector_from_circuit` + `statevector_expectation`)完成估计。 diff --git a/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md index b3c6f069..8c1c4d00 100644 --- a/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/cn/c/6_error_mitigation/2_virtual_distillation.md @@ -44,11 +44,11 @@ ### virtual_distillation_build_circuit(ptr) -构造虚拟蒸馏协议使用的 copy-swap 线路:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。 +构造虚拟蒸馏协议使用的 copy-swap 线路(Hadamard-test 线路):先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——然后在末尾的辅助比特(最后一个比特)上施加 `H` 门制备到 `|+⟩`,最后在相邻副本之间施加由辅助比特控制的 `SWAP` 梯(CSWAP),实现各副本的轮换(derangement)。 - `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 -线路宽度为 `copies × 基底宽度`(基底宽度取分解后线路的宽度);操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 +线路宽度为 `copies × 基底宽度 + 1`(基底宽度取分解后线路的宽度,最后一个比特是辅助比特);操作数为 `copies` 份基底操作加上 1 个 `H` 与 `(copies - 1) × 基底宽度` 个受控 `SWAP`。 返回 `struct CCircuit *`:copy-swap 线路的 owned 句柄,用 `circuit_free` 释放;`ptr` 为 NULL 或构造失败时返回 NULL。 @@ -56,11 +56,11 @@ ## 分子与分母 -估计器回调通过 `hamiltonian` 参数区分两种线路:分子线路带可观测量,分母线路无可观测量。 +估计器回调在两种线路上都会收到非 NULL 的 `hamiltonian`,按内容区分:分子可观测量作用在第 0 份副本的比特上并在辅助比特上携带 `X`;分母可观测量只在辅助比特上携带 `X`(其余为恒等)。 ### virtual_distillation_run_numerator_circuit(ptr, hamiltonian, shots, estimator, out_mean, out_variance) -通过估计器回调执行分子线路,估计 `Tr(O ρ^M)`。估计器收到 copy-swap 线路、扩展到 copy-swap 全宽的哈密顿量与 `shots`。扩展规则:原 Pauli 项保持比特索引、相位与系数不变,新增的高索引比特上补 `Z`。 +通过估计器回调执行分子线路,估计 `Tr(O ρ^M)`。估计器收到 copy-swap 线路、扩展到 copy-swap 全宽的哈密顿量与 `shots`。扩展规则:原 Pauli 项保持比特索引(即作用在第 0 份副本上)、相位与系数不变,其余副本补恒等 `I`,并在辅助比特(最后一个比特)上追加 `X`。 - `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 - `hamiltonian` (`const struct CHamiltonian *`):可观测量,比特数须与基底线路宽度一致,构造方式见 [哈密顿量](../3_qis/7_hamiltonian.md)。 @@ -73,7 +73,7 @@ ### virtual_distillation_run_denominator_circuit(ptr, shots, estimator, out_mean, out_variance) -通过估计器回调执行分母线路,估计 `Tr(ρ^M)`。估计器收到 copy-swap 线路、NULL 哈密顿量与 `shots`。 +通过估计器回调执行分母线路,估计 `Tr(ρ^M)`。估计器收到 copy-swap 线路、辅助比特(最后一个比特)上为 `X` 且其余比特为恒等的哈密顿量,以及 `shots`。 - `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 - `shots` (`uintptr_t`):转发给估计器的 shot 数。 @@ -85,7 +85,7 @@ ### virtual_distillation_build_copy_swap_circuit(ptr) -从基底线路构造 copy-swap 线路:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。线路宽度为 `copies × 基底宽度`(基底宽度取分解后线路的宽度);操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 +从基底线路构造 copy-swap 线路:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——然后在末尾的辅助比特(最后一个比特)上施加 `H` 门制备到 `|+⟩`,最后在相邻副本之间施加由辅助比特控制的 `SWAP` 梯,实现各副本的轮换。线路宽度为 `copies × 基底宽度 + 1`(基底宽度取分解后线路的宽度);操作数为 `copies` 份基底操作加上 1 个 `H` 与 `(copies - 1) × 基底宽度` 个受控 `SWAP`。 - `ptr` (`const struct CVirtualDistillation *`):虚拟蒸馏句柄。 @@ -119,20 +119,19 @@ #include #include "cqlib_c.h" -/* 分子线路带可观测量,分母线路不带 */ +/* 分子与分母都携带哈密顿量——分子作用在第 0 份副本的比特上并在辅助 + 比特上携带 X,分母只在辅助比特上携带 X;精确模拟线路并求可观测量期望 */ static void estimate(const struct CCircuit *circuit, const struct CHamiltonian *hamiltonian, uintptr_t shots, double *expectation, double *variance) { - if (hamiltonian != NULL) { - *expectation = 1.5; - *variance = 0.25; - } else { - *expectation = 2.0; - *variance = 1.0; - } - (void)circuit; + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; (void)shots; } @@ -142,13 +141,13 @@ int main(void) { circuit_x(qc, 0); struct CVirtualDistillation *vd = virtual_distillation_new(qc, 2); - /* copy-swap 线路:宽度 = 2 × 1 = 2 */ + /* copy-swap 线路:宽度 = 2 × 1 + 1 = 3 */ struct CCircuit *copy_swap = virtual_distillation_build_circuit(vd); struct CPauliString *z = pauli_string_parse("Z"); struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ - /* mu_vd = 1.5 / 2.0 = 0.75 */ + /* 对 rho = |1><1|:分子 Tr(Z rho^2) = -1,分母 Tr(rho^2) = 1 */ double mean = 0.0, variance = 0.0; virtual_distillation_run_vd(vd, obs, 3, 2, estimate, &mean, &variance); printf("VD estimate: %f\n", mean); diff --git a/docs/api/cn/c/6_error_mitigation/3_unified.md b/docs/api/cn/c/6_error_mitigation/3_unified.md index 858ad020..d1654fdc 100644 --- a/docs/api/cn/c/6_error_mitigation/3_unified.md +++ b/docs/api/cn/c/6_error_mitigation/3_unified.md @@ -72,30 +72,25 @@ ## 示例 -虚拟蒸馏方法的完整流程(分子线路带可观测量、分母线路无可观测量的回调分支,见 [模块概览](0_overview.md)): +虚拟蒸馏方法的完整流程(分子与分母的回调都会收到非 NULL 的哈密顿量,见 [模块概览](0_overview.md)): ```c #include #include "cqlib_c.h" -/* 估计器回调:分子线路带可观测量(精确模拟),分母线路无可观测量 */ +/* 估计器回调:分子与分母都携带哈密顿量(分子作用在第 0 份副本的比特上 + 并在辅助比特上携带 X;分母只在辅助比特上携带 X)——精确模拟线路并求其期望 */ static void estimate(const struct CCircuit *circuit, const struct CHamiltonian *hamiltonian, uintptr_t shots, double *expectation, double *variance) { - if (hamiltonian != NULL) { - struct CStatevector *sv = statevector_from_circuit(circuit); - double value = 0.0; - statevector_expectation(sv, hamiltonian, &value); - statevector_free(sv); - *expectation = value; - *variance = 0.0; - } else { - /* 分母 Tr(rho^M):由采样型估计器从测量结果估计,此处返回示例值 */ - *expectation = 1.0; - *variance = 0.0; - } + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; (void)shots; } diff --git a/docs/api/cn/python/6_error_mitigation/0_overview.md b/docs/api/cn/python/6_error_mitigation/0_overview.md index 189df655..6b0f433c 100644 --- a/docs/api/cn/python/6_error_mitigation/0_overview.md +++ b/docs/api/cn/python/6_error_mitigation/0_overview.md @@ -30,7 +30,7 @@ 两种方法都不直接执行输入线路,而是先构造一族缓解线路: - 零噪声外推按折叠等级展开线路。全局折叠把线路改写为 `U -> U (U† U)^level`,使线路深度随等级增长;选择性折叠只对指令名匹配给定门集的操作执行同样的展开,其余操作原样保留。等级为 `0` 时缓解线路即输入线路的副本。 -- 虚拟蒸馏把线路复制 `copies` 份并按位插入 SWAP,得到一条宽度为 `copies` 倍的 copy-swap 线路。分子与分母在同一线路上执行,区别只在传给回调的观测量:分母为 `None`,分子为按拷贝数扩展后的 `Hamiltonian`。 +- 虚拟蒸馏把线路复制 `copies` 份,追加一个经 Hadamard 门制备到 `|+⟩` 的辅助比特,并在相邻副本之间按位施加由辅助比特控制的 SWAP,得到一条宽度为 `copies` 倍再加 1 的 copy-swap 线路。分子与分母在同一线路上执行,区别只在传给回调的观测量:分子为扩展到全宽的 `Hamiltonian`(第 0 份副本上的原观测量 ⊗ 辅助比特上的 `X`),分母为仅辅助比特上带 `X` 的 `Hamiltonian`。 ### 数据流 @@ -104,9 +104,9 @@ print("variance:", result.variance) | **选择性折叠** | 只对指令名匹配 `gate_set` 的操作做折叠,其余操作原样保留。 | | **外推方法** | 把不同噪声因子下的期望值外推到零噪声点的拟合模型,包括多项式与指数两种。 | | **拷贝数** | 虚拟蒸馏中密度矩阵的拷贝份数,由 `copies` 给出,最小为 2。 | -| **copy-swap 线路** | 由输入线路复制 `copies` 份并按位插入 SWAP 得到的缓解线路。 | -| **分子线路** | 用于估计 `Tr(O ρ^M)` 的 copy-swap 线路执行,观测量为按拷贝数扩展后的 `Hamiltonian`。 | -| **分母线路** | 用于估计 `Tr(ρ^M)` 的 copy-swap 线路执行,观测量为 `None`。 | +| **copy-swap 线路** | 由输入线路复制 `copies` 份、追加一个经 Hadamard 门制备的辅助比特、并在相邻副本之间按位施加辅助比特控制的 SWAP 得到的缓解线路。 | +| **分子线路** | 用于估计 `Tr(O ρ^M)` 的 copy-swap 线路执行,观测量为扩展到全宽的 `Hamiltonian`(第 0 份副本上的原观测量 ⊗ 辅助比特上的 `X`)。 | +| **分母线路** | 用于估计 `Tr(ρ^M)` 的 copy-swap 线路执行,观测量为辅助比特上的 `X`(其余比特为恒等)。 | | **estimator** | 调用方提供的回调,执行一条缓解线路并返回 `(期望值, 方差)` 二元组。 | | **统一流水线** | 由 `ErrorMitigation` 承载的顺序式流程:`run()` 收集估计值,`get_mitigated()` 做后处理。 | @@ -190,13 +190,20 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) vd = VirtualDistillation(circuit, 2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * 基础宽度 + 1 def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # 分母:Tr(ρ^M) - return (1.5, 0.25) # 分子:Tr(O ρ^M) + # 分子与分母都携带观测量:分子在第 0 份副本的比特上有非恒等 Pauli, + # 分母只有辅助比特(最后一个比特)上的 X + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # 分子:Tr(O ρ^M) + return (2.0, 1.0) # 分母:Tr(ρ^M) expectation, variance = vd.run_vd( @@ -229,9 +236,14 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation = ErrorMitigation( diff --git a/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md index 88b2c8bd..f6ae5794 100644 --- a/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/cn/python/6_error_mitigation/2_virtual_distillation.md @@ -2,7 +2,7 @@ `cqlib.error_mitigation.virtual_distillation` -`cqlib.error_mitigation.virtual_distillation` 提供虚拟蒸馏的低层接口:把基础线路复制若干份并插入 SWAP 构造 copy-swap 线路,再分别估计分子与分母,最后求比值得到缓解后的期望值。本页覆盖 `VirtualDistillation` 与 `VirtualDistillationConfig`。 +`cqlib.error_mitigation.virtual_distillation` 提供虚拟蒸馏的低层接口:把基础线路复制若干份,追加一个经 Hadamard 门制备的辅助比特,并在相邻副本之间施加由辅助比特控制的 SWAP(Hadamard-test 线路),再分别估计分子与分母,最后求比值得到缓解后的期望值。本页覆盖 `VirtualDistillation` 与 `VirtualDistillationConfig`。 ## 导入 @@ -50,7 +50,7 @@ from cqlib.error_mitigation.virtual_distillation import ( 参数: - `circuit` (`Circuit`):基础线路。构造时复制保存,不会修改传入的线路。 -- `copies` (`int`):拷贝份数,必须不小于 2,否则抛出 `ErrorMitigationError`。拷贝数决定缓解线路的宽度:copy-swap 线路的宽度为基础线路宽度的 `copies` 倍。 +- `copies` (`int`):拷贝份数,必须不小于 2,否则抛出 `ErrorMitigationError`。拷贝数决定缓解线路的宽度:copy-swap 线路的宽度为基础线路宽度的 `copies` 倍再加 1(多出的比特是 Hadamard 检验的辅助比特)。 ### 属性 @@ -59,7 +59,7 @@ from cqlib.error_mitigation.virtual_distillation import ( ### 方法 - `set_copies(copies)`:更新拷贝数。校验在赋值之前完成,校验失败时对象的拷贝数保持不变。 -- `build_copy_swap_circuit() -> Circuit`:构造 copy-swap 线路。基础线路先做分解,再逐份复制到各自比特区间,最后按位插入 SWAP。 +- `build_copy_swap_circuit() -> Circuit`:构造 copy-swap 线路。基础线路先做分解,再逐份复制到各自比特区间,最后在末尾辅助比特上施加 Hadamard 门,并在相邻副本之间按位施加由辅助比特控制的 SWAP。 - `run_denominator_circuit(shots, estimator) -> tuple[float, float]`:执行分母线路,返回回调给出的二元组。 - `run_numerator_circuit(hamiltonian, shots, estimator) -> tuple[float, float]`:执行分子线路,返回回调给出的二元组。 - `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator) -> tuple[float, float]`:依次执行分子与分母并求比值,返回 `(缓解后的期望值, 方差)`。 @@ -78,9 +78,9 @@ from cqlib.error_mitigation.virtual_distillation import ( 1. 基础线路先做分解,保证复制的是一组已展开的操作。 2. 基础线路按 `copies` 份复制,第 `i` 份落在偏移 `i` 倍基础宽度的比特区间上。 -3. 对第 1 份与其余每一份,逐位插入 SWAP。SWAP 只在第 1 份与其他各份之间插入,其余份之间不直接相连。 +3. 在末尾追加一个辅助比特,经 `H` 门制备到 `|+⟩`;随后在相邻副本之间逐位插入 SWAP,全部由辅助比特控制(CSWAP)。这一受控 SWAP 梯实现 Hadamard 检验所需的副本轮换(derangement)。 -因此拷贝数为 2 时,基础线路每条操作出现 2 次,另加按位 SWAP;拷贝数增加时 SWAP 数量按份数线性增长,而不随拷贝数两两配对增长。 +因此拷贝数为 2 时,基础线路每条操作出现 2 次,另加辅助比特上的 1 个 `H` 与按位的受控 SWAP;拷贝数增加时受控 SWAP 数量按份数线性增长,而不随拷贝数两两配对增长。 ```python from cqlib.circuit import Circuit @@ -91,7 +91,7 @@ circuit.x(0) vd = VirtualDistillation(circuit, 2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * 基础宽度 + 1 ``` --- @@ -102,11 +102,13 @@ print(copy_swap.width) # 2 | 方法 | 回调收到的线路 | 回调收到的观测量 | 回调收到的采样次数 | | --- | --- | --- | --- | -| `run_denominator_circuit(shots, estimator)` | copy-swap 线路 | `None` | `shots` | -| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap 线路 | 按拷贝数扩展后的 `Hamiltonian` | `shots` | +| `run_denominator_circuit(shots, estimator)` | copy-swap 线路 | 辅助比特上为 `X`(其余为恒等)的 `Hamiltonian` | `shots` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap 线路 | 扩展到全宽的 `Hamiltonian`(第 0 份副本上的 `O` ⊗ 辅助比特上的 `X`) | `shots` | | `run_vd(...)` | copy-swap 线路 | 先分子后分母,同上 | 分别为 `shots_numerator` 与 `shots_denominator` | -分子所用的观测量由传入的 `Hamiltonian` 扩展得到:原有 Pauli 项保留在各自比特上,更高位的比特补 `Z`,使观测量比特数与 copy-swap 线路宽度一致。扩展由类内部完成,调用方只需传入作用在基础线路上的观测量。 +分子与分母的观测量都是具体的 `Hamiltonian`,按内容区分:分子观测量在第 0 份副本的比特上有非恒等 Pauli,分母观测量只在辅助比特(最后一个比特)上非恒等。 + +分子所用的观测量由传入的 `Hamiltonian` 扩展得到:原有 Pauli 项保留在各自比特上(即第 0 份副本),其余副本补恒等 `I`,并在辅助比特上追加 `X`,使观测量比特数与 copy-swap 线路宽度一致。扩展由类内部完成,调用方只需传入作用在基础线路上的观测量。 `run_vd()` 依次完成两步:先按 `shots_numerator` 执行分子,再按 `shots_denominator` 执行分母,然后按下式合成结果: @@ -136,9 +138,16 @@ vd = VirtualDistillation(circuit, 2) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # 分母:Tr(ρ^M) - return (1.5, 0.25) # 分子:Tr(O ρ^M) + # 分子与分母都携带观测量:分子在第 0 份副本的比特上有非恒等 Pauli, + # 分母只有辅助比特(最后一个比特)上的 X + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # 分子:Tr(O ρ^M) + return (2.0, 1.0) # 分母:Tr(ρ^M) expectation, variance = vd.run_vd( diff --git a/docs/api/cn/python/6_error_mitigation/3_unified.md b/docs/api/cn/python/6_error_mitigation/3_unified.md index 74e91907..fec1ec5a 100644 --- a/docs/api/cn/python/6_error_mitigation/3_unified.md +++ b/docs/api/cn/python/6_error_mitigation/3_unified.md @@ -161,7 +161,7 @@ vd_args = RunArgs.virtual_distillation(shots_numerator=3, shots_denominator=2) 零噪声外推:`run()` 按配置的折叠等级构造缓解线路,对每条线路调用一次回调,观测量为传入的 `Hamiltonian`,采样次数取自 `RunArgs.zne(shots=...)`。回调返回的二元组只取第 1 个元素,方差被丢弃。`get_mitigated()` 对外推输入做拟合,结果的 `variance` 为 `None`。 -虚拟蒸馏:`run()` 先构造 copy-swap 线路,再在同一条线路上调用两次回调——先分子,后分母。分子传入按拷贝数扩展后的观测量与 `shots_numerator`,分母传入 `None` 与 `shots_denominator`。`get_mitigated()` 按 [虚拟蒸馏](2_virtual_distillation.md) 的合成公式求比值,结果的 `variance` 有值。 +虚拟蒸馏:`run()` 先构造 copy-swap 线路,再在同一条线路上调用两次回调——先分子,后分母。分子传入扩展到全宽的观测量(第 0 份副本上的原观测量 ⊗ 辅助比特上的 `X`)与 `shots_numerator`,分母传入仅辅助比特上带 `X` 的观测量与 `shots_denominator`。`get_mitigated()` 按 [虚拟蒸馏](2_virtual_distillation.md) 的合成公式求比值,结果的 `variance` 有值。 ### 示例 diff --git a/docs/api/cn/rust/6_error_mitigation/0_overview.md b/docs/api/cn/rust/6_error_mitigation/0_overview.md index 57238c44..50dffb35 100644 --- a/docs/api/cn/rust/6_error_mitigation/0_overview.md +++ b/docs/api/cn/rust/6_error_mitigation/0_overview.md @@ -24,7 +24,7 @@ pub type Estimator<'a> = dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) + 'a; ``` -参数依次为待执行线路、待估计的可观测量与 shot 数,返回 `(期望值, 方差)`。分母线路不携带可观测量,第二个参数为 `None`;`ZNEMitigation::run_em_sequence` 不指定 shot 数,第三个参数为 `None`。公开方法统一以 `&Estimator<'_>` 接收它,即 trait 对象引用 `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`,闭包与函数项都可以直接取引用传入。返回值中的方差只有虚拟蒸馏会使用,零噪声外推只取期望值。 +参数依次为待执行线路、待估计的可观测量与 shot 数,返回 `(期望值, 方差)`。虚拟蒸馏的分子与分母调用都会传入具体可观测量——分母可观测量是 copy-swap 线路辅助比特上的 `X`;`ZNEMitigation::run_em_sequence` 不指定 shot 数,第三个参数为 `None`。公开方法统一以 `&Estimator<'_>` 接收它,即 trait 对象引用 `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`,闭包与函数项都可以直接取引用传入。返回值中的方差只有虚拟蒸馏会使用,零噪声外推只取期望值。 ### 缓解流水线状态 @@ -100,8 +100,8 @@ assert_eq!(mitigated.expectation, 0.5); | **门折叠** | 把线路改写为 `U -> U (U† U)^level` 的构造方式;只对指定门折叠时称为选择性折叠。 | | **虚拟蒸馏** | 通过副本与 copy-swap 线路估计 `Tr(O ρ^M) / Tr(ρ^M)` 的方法,`M` 为副本数。 | | **副本(copy)** | 虚拟蒸馏中并行准备的基底线路份数,最少为 2。 | -| **copy-swap 线路** | 把多份基底线路并排放在互不重叠的比特区间、并在首份与其他每份之间插入逐比特 SWAP 的线路,宽度等于副本数乘以基底线路宽度。 | -| **分子线路 / 分母线路** | 虚拟蒸馏的两条统计线路。分子线路携带扩展后的可观测量;分母线路不携带可观测量,估计器收到 `None`。 | +| **copy-swap 线路** | 把多份基底线路并排放在互不重叠的比特区间、在末尾追加一个经 Hadamard 门制备到 `|+⟩` 的辅助比特、并在相邻副本之间施加由辅助比特控制的逐比特 SWAP 梯的线路,宽度等于副本数乘以基底线路宽度再加 1。 | +| **分子线路 / 分母线路** | 虚拟蒸馏的两条统计线路。分子线路携带扩展后的可观测量(第 0 份副本上的原可观测量与辅助比特上的 `X`);分母线路携带辅助比特上的 `X`,其余比特为恒等。 | | **缓解流水线** | `ErrorMitigation` 表示的顺序式流程:`new` 创建、`run` 收集原始数据、`get_mitigated` 做方法相关的后处理。 | --- @@ -159,7 +159,8 @@ let mut mitigation = ErrorMitigation::new( ) .unwrap(); -// 分子线路带可观测量,分母线路不带 +// 分子与分母都携带可观测量:分子在第 0 份副本的比特上有非恒等 Pauli, +// 分母只有辅助比特上的 X mitigation .run( &hamiltonian, @@ -168,7 +169,12 @@ mitigation shots_denominator: 2, }, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { @@ -232,13 +238,18 @@ let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))] let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); -// 两份基底线路加一对逐比特 SWAP +// 两份基底线路加 1 个辅助比特:辅助比特上的 H 与一次受控 SWAP let copy_swap = vd.build_copy_swap_circuit().unwrap(); -assert_eq!(copy_swap.width(), 2); +assert_eq!(copy_swap.width(), 3); let (mu_vd, var_vd) = vd .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { diff --git a/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md b/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md index 13f1ecfe..d5c6fc29 100644 --- a/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/cn/rust/6_error_mitigation/2_virtual_distillation.md @@ -32,7 +32,7 @@ pub struct VirtualDistillation { - `fn new(circuit: Circuit, copies: usize) -> Result`:创建入口,取得线路所有权。模板线路只能通过 `new` 传入,没有对应的读接口。 - `fn set_copies(&mut self, copies: usize) -> Result<(), ErrorMitigationError>`:更新副本数。更新失败时保持原值不变。 - `fn build_copy_swap_circuit(&self) -> Result`:由基底线路构造 copy-swap 线路。 -- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`:跑分母线路,返回 `(均值, 方差)`。 +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:跑分母线路,返回 `(均值, 方差)`。 - `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:跑分子线路,返回 `(均值, 方差)`。 - `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:跑完整流程,返回缓解后的 `(期望值, 方差)`。 @@ -55,10 +55,10 @@ pub struct VirtualDistillation { ### `build_copy_swap_circuit(&self) -> Result` -构造 copy-swap 线路,包含三个部分:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在首份与其他每一份之间逐比特插入 `SWAP`。 +构造 copy-swap 线路——即虚拟蒸馏协议的 Hadamard-test 线路——包含三个部分:先对基底线路做门分解,再把基底线路并排准备 `copies` 份——第 `i` 份整体平移到从 `i × 基底宽度` 开始的比特区间,各份之间互不重叠——最后在末尾的辅助比特(最后一个比特)上施加 `H` 门制备到 `|+⟩`,并在相邻副本之间施加由辅助比特控制的 `SWAP` 梯(CSWAP),实现各副本的轮换(derangement)。 -- 线路宽度为 `copies × 基底宽度`,其中基底宽度取分解后线路的宽度。 -- 操作数为 `copies` 份基底操作加上 `(copies - 1) × 基底宽度` 个 `SWAP`。 +- 线路宽度为 `copies × 基底宽度 + 1`,其中基底宽度取分解后线路的宽度,最后一个比特是辅助比特。 +- 操作数为 `copies` 份基底操作加上 1 个 `H` 与 `(copies - 1) × 基底宽度` 个受控 `SWAP`。 返回: @@ -70,8 +70,8 @@ pub struct VirtualDistillation { ### 分子与分母 -- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`:在 copy-swap 线路上估计分母 `Tr(ρ^M)`。估计器收到的可观测量参数为 `None`,shot 数为 `Some(shots)`;回调形态见 [概览](0_overview.md)。 -- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:在 copy-swap 线路上估计分子 `Tr(O ρ^M)`。可观测量会在模块内部先扩展到 copy-swap 线路的宽度:原有 Pauli 项保持原比特索引与相位、系数不变,新增的高索引比特上补 `Z`;扩展结果经估计器的可观测量参数传入,shot 数为 `Some(shots)`。 +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:在 copy-swap 线路上估计分母 `Tr(ρ^M)`。估计器收到的可观测量参数为 `Some(分母观测量)`——辅助比特(最后一个比特)上的 `X`,其余比特为恒等——shot 数为 `Some(shots)`;回调形态见 [概览](0_overview.md)。 +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:在 copy-swap 线路上估计分子 `Tr(O ρ^M)`。可观测量会在模块内部先扩展到 copy-swap 线路的宽度:原有 Pauli 项保持原比特索引(即作用在第 0 份副本上)与相位、系数不变,其余副本补恒等 `I`,并在辅助比特(最后一个比特)上追加 `X`;扩展结果经估计器的可观测量参数传入,shot 数为 `Some(shots)`。 - `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`:先校验哈密顿量的比特数等于基底线路宽度,再依次执行分子线路与分母线路,最后组合成缓解结果。分子与分母使用各自的 shot 数。 `run_vd` 的异常情况: @@ -94,11 +94,12 @@ let q0 = Qubit::new(0); let mut circuit = Circuit::new(1); circuit.x(q0).unwrap(); -// 两份单比特副本:X 作用于比特 0、X 作用于比特 1,再加一对逐比特 SWAP +// 两份单比特副本加 1 个辅助比特:X 作用于比特 0、X 作用于比特 1, +// 辅助比特(比特 2)上的 H,以及一次由辅助比特控制的 SWAP let vd = VirtualDistillation::new(circuit, 2).unwrap(); let copy_swap = vd.build_copy_swap_circuit().unwrap(); -assert_eq!(copy_swap.width(), 2); -assert_eq!(copy_swap.operations().len(), 3); +assert_eq!(copy_swap.width(), 3); +assert_eq!(copy_swap.operations().len(), 4); ``` ```rust @@ -127,11 +128,17 @@ let mut pauli = PauliString::new(1); pauli.set_pauli(0, Pauli::Z); let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); -// 分子线路带可观测量,分母线路不带;两者使用各自的 shot 数 +// 分子与分母都携带可观测量,按内容区分:分子在第 0 份副本的比特上有 +// 非恒等 Pauli,分母只有辅助比特上的 X;两者使用各自的 shot 数 let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); let (mu_vd, var_vd) = vd .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { @@ -175,6 +182,6 @@ pub struct VirtualDistillationConfig { | `ErrorMitigationError::HamiltonianQubitCountMismatch` | `run_vd` 收到的哈密顿量比特数与基底线路宽度不一致。 | | `ErrorMitigationError::ZeroDenominatorMean` | 分母线路的均值为零。 | | `ErrorMitigationError::Circuit` | 携带 `CircuitError::QubitCountMismatch`,表示扩展后的可观测量宽度与 copy-swap 线路宽度不一致。 | -| `CircuitError` | `build_copy_swap_circuit` 与 `run_denominator_circuit` 直接返回的线路错误。 | +| `CircuitError` | `build_copy_swap_circuit` 直接返回的线路错误;`run_denominator_circuit` 将其包装为 `ErrorMitigationError::Circuit`。 | 错误类型的完整定义见 [统一流水线](3_unified.md)。 diff --git a/docs/api/cn/rust/6_error_mitigation/3_unified.md b/docs/api/cn/rust/6_error_mitigation/3_unified.md index 92977a3b..e16cd87c 100644 --- a/docs/api/cn/rust/6_error_mitigation/3_unified.md +++ b/docs/api/cn/rust/6_error_mitigation/3_unified.md @@ -77,7 +77,7 @@ pub struct ErrorMitigation { 执行细节按方法区分: - 零噪声外推:按配置的折叠等级构造折叠线路,逐条调用估计器,可观测量参数为 `Some(hamiltonian)`,shot 数为 `RunArgs::Zne::shots`;只取返回值中的期望值。折叠线路、期望值、噪声因子与执行参数一并留存。 -- 虚拟蒸馏:构造 copy-swap 线路并把可观测量扩展到该线路宽度,分子线路的估计器收到 `Some(扩展后的可观测量)` 与 `Some(shots_numerator)`,分母线路收到 `None` 与 `Some(shots_denominator)`;分子与分母的均值、方差一并留存。 +- 虚拟蒸馏:构造 copy-swap 线路并把可观测量扩展到该线路宽度,分子线路的估计器收到 `Some(扩展后的可观测量)`——第 0 份副本上的原可观测量与辅助比特上的 `X`——与 `Some(shots_numerator)`,分母线路收到 `Some(分母可观测量)`——辅助比特上的 `X`,其余比特为恒等——与 `Some(shots_denominator)`;分子与分母的均值、方差一并留存。 返回: diff --git a/docs/api/en/c/6_error_mitigation/0_overview.md b/docs/api/en/c/6_error_mitigation/0_overview.md index 7b53d4ba..5c82f497 100644 --- a/docs/api/en/c/6_error_mitigation/0_overview.md +++ b/docs/api/en/c/6_error_mitigation/0_overview.md @@ -56,7 +56,7 @@ Mitigation flows obtain expectation estimates through this callback. The contrac - **Pointer validity**: `circuit`, `hamiltonian` and all out pointers are valid only for the duration of the callback; do not keep using them or store them beyond the callback. - **hamiltonian ownership**: the `hamiltonian` handle is owned by the library and must not be freed by the callback. The Hamiltonian the callback receives may already have been expanded to the width of the folded/copy circuit as required by the flow. -- **Out writes**: the callback must write the expectation value through `expectation`; write the variance through `variance` when known, NaN otherwise. In the virtual distillation denominator estimate, `hamiltonian` is NULL — treat it as "no observable". +- **Out writes**: the callback must write the expectation value through `expectation`; write the variance through `variance` when known, NaN otherwise. In virtual distillation both the numerator and the denominator estimates pass a non-NULL `hamiltonian`: the numerator observable acts on the first copy's qubits and carries `X` on the ancillary qubit, while the denominator observable carries only `X` on the ancillary qubit (identities elsewhere). - **shots semantics**: `shots == 0` means "not specified" and the callback decides the sampling scale; a non-zero value is the shot count suggested by the caller. Inside the callback you can freely call simulator interfaces (e.g. `statevector_from_circuit` + `statevector_expectation` from the [Statevector](../3_qis/1_statevector.md) page) to compute the estimate. diff --git a/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md index 266ebd87..61f7116b 100644 --- a/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/en/c/6_error_mitigation/2_virtual_distillation.md @@ -44,11 +44,11 @@ Returns `int32_t`: 0 on success; -1 (`ptr` NULL); -8 (`copies` less than 2). ### virtual_distillation_build_circuit(ptr) -Builds the copy-swap circuit used by the virtual distillation protocol: the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — and finally a `SWAP` is inserted bitwise between the first copy and each of the other copies. +Builds the copy-swap circuit used by the virtual distillation protocol (a Hadamard-test circuit): the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — then one ancillary qubit (the last qubit) is prepared in `|+⟩` by a Hadamard gate, and finally a ladder of `SWAP`s between adjacent copies, each controlled by the ancillary qubit (CSWAP), implements the derangement of the copies. - `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. -The circuit width is `copies × base width` (the base width being the width of the decomposed circuit); the operation count is `copies` copies of the base operations plus `(copies - 1) × base width` `SWAP` gates. +The circuit width is `copies × base width + 1` (the base width being the width of the decomposed circuit; the last qubit is the ancillary qubit); the operation count is `copies` copies of the base operations plus one `H` and `(copies - 1) × base width` controlled `SWAP` gates. Returns `struct CCircuit *`: an owned handle of the copy-swap circuit, freed with `circuit_free`; NULL when `ptr` is NULL or construction fails. @@ -56,11 +56,11 @@ Returns `struct CCircuit *`: an owned handle of the copy-swap circuit, freed wit ## Numerator and Denominator -The estimator callback distinguishes the two circuits through the `hamiltonian` parameter: the numerator circuit carries the observable, the denominator circuit has none. +The estimator callback receives a non-NULL `hamiltonian` for both circuits and tells them apart by content: the numerator observable acts on the first copy's qubits and carries `X` on the ancillary qubit; the denominator observable carries only `X` on the ancillary qubit (identities elsewhere). ### virtual_distillation_run_numerator_circuit(ptr, hamiltonian, shots, estimator, out_mean, out_variance) -Runs the numerator circuit through the estimator callback to estimate `Tr(O ρ^M)`. The estimator receives the copy-swap circuit, a Hamiltonian expanded to the full copy-swap width, and `shots`. Expansion rule: the original Pauli terms keep their qubit indices, phases and coefficients; the new higher-index qubits are padded with `Z`. +Runs the numerator circuit through the estimator callback to estimate `Tr(O ρ^M)`. The estimator receives the copy-swap circuit, a Hamiltonian expanded to the full copy-swap width, and `shots`. Expansion rule: the original Pauli terms keep their qubit indices (i.e. they act on the first copy), phases and coefficients; the remaining copies are padded with `I`, and an `X` is appended on the ancillary qubit (the last qubit). - `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. - `hamiltonian` (`const struct CHamiltonian *`): the observable; its qubit count must match the base circuit width, see [Hamiltonian](../3_qis/7_hamiltonian.md) for construction. @@ -73,7 +73,7 @@ Returns `int32_t`: 0 on success; -1 (`ptr`, `hamiltonian`, `out_mean` or `out_va ### virtual_distillation_run_denominator_circuit(ptr, shots, estimator, out_mean, out_variance) -Runs the denominator circuit through the estimator callback to estimate `Tr(ρ^M)`. The estimator receives the copy-swap circuit, a NULL Hamiltonian, and `shots`. +Runs the denominator circuit through the estimator callback to estimate `Tr(ρ^M)`. The estimator receives the copy-swap circuit, a Hamiltonian with `X` on the ancillary qubit (the last qubit) and identities elsewhere, and `shots`. - `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. - `shots` (`uintptr_t`): the shot count forwarded to the estimator. @@ -85,7 +85,7 @@ Returns `int32_t`: 0 on success; -1 (`ptr`, `out_mean` or `out_variance` NULL); ### virtual_distillation_build_copy_swap_circuit(ptr) -Constructs the copy-swap circuit from the base circuit: the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — and finally a `SWAP` is inserted bitwise between the first copy and each of the other copies. The circuit width is `copies × base width` (the base width being the width of the decomposed circuit); the operation count is `copies` copies of the base operations plus `(copies - 1) × base width` `SWAP` gates. +Constructs the copy-swap circuit from the base circuit: the base circuit is first decomposed into gates, then prepared `copies` times side by side — copy `i` is shifted onto the qubit range starting at `i × base width`, with no overlap between copies — then one ancillary qubit (the last qubit) is prepared in `|+⟩` by a Hadamard gate, and finally a ladder of `SWAP`s between adjacent copies, each controlled by the ancillary qubit, implements the derangement of the copies. The circuit width is `copies × base width + 1` (the base width being the width of the decomposed circuit); the operation count is `copies` copies of the base operations plus one `H` and `(copies - 1) × base width` controlled `SWAP` gates. - `ptr` (`const struct CVirtualDistillation *`): the virtual distillation handle. @@ -119,20 +119,21 @@ The variance is combined with a first-order Taylor approximation, assuming the n #include #include "cqlib_c.h" -/* The numerator circuit carries the observable, the denominator does not */ +/* Both the numerator and the denominator carry a Hamiltonian — the + numerator acts on the first copy's qubits and carries X on the + ancillary qubit, the denominator carries only X on the ancillary + qubit; simulate the circuit exactly and evaluate the observable */ static void estimate(const struct CCircuit *circuit, const struct CHamiltonian *hamiltonian, uintptr_t shots, double *expectation, double *variance) { - if (hamiltonian != NULL) { - *expectation = 1.5; - *variance = 0.25; - } else { - *expectation = 2.0; - *variance = 1.0; - } - (void)circuit; + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; (void)shots; } @@ -142,13 +143,13 @@ int main(void) { circuit_x(qc, 0); struct CVirtualDistillation *vd = virtual_distillation_new(qc, 2); - /* Copy-swap circuit: width = 2 × 1 = 2 */ + /* Copy-swap circuit: width = 2 × 1 + 1 = 3 */ struct CCircuit *copy_swap = virtual_distillation_build_circuit(vd); struct CPauliString *z = pauli_string_parse("Z"); struct CHamiltonian *obs = hamiltonian_from_pauli(z); /* takes ownership of z */ - /* mu_vd = 1.5 / 2.0 = 0.75 */ + /* For rho = |1><1|: numerator Tr(Z rho^2) = -1, denominator Tr(rho^2) = 1 */ double mean = 0.0, variance = 0.0; virtual_distillation_run_vd(vd, obs, 3, 2, estimate, &mean, &variance); printf("VD estimate: %f\n", mean); diff --git a/docs/api/en/c/6_error_mitigation/3_unified.md b/docs/api/en/c/6_error_mitigation/3_unified.md index d5efbb7e..82392598 100644 --- a/docs/api/en/c/6_error_mitigation/3_unified.md +++ b/docs/api/en/c/6_error_mitigation/3_unified.md @@ -72,32 +72,27 @@ Returns `int32_t`: 0 on success; -1 (`ptr`, `out_expectation` or `out_variance` ## Example -A complete flow with the virtual distillation method (the callback branches on the numerator circuit carrying the observable vs. the denominator circuit having none, see the [module overview](0_overview.md)): +A complete flow with the virtual distillation method (both the numerator and the denominator callbacks receive a non-NULL Hamiltonian, see the [module overview](0_overview.md)): ```c #include #include "cqlib_c.h" -/* Estimator callback: the numerator circuit carries the observable - (exact simulation); the denominator circuit has none */ +/* Estimator callback: both the numerator and the denominator carry a + Hamiltonian (the numerator acts on the first copy's qubits and carries + X on the ancillary qubit; the denominator carries only X on the + ancillary qubit) — simulate the circuit exactly and evaluate it */ static void estimate(const struct CCircuit *circuit, const struct CHamiltonian *hamiltonian, uintptr_t shots, double *expectation, double *variance) { - if (hamiltonian != NULL) { - struct CStatevector *sv = statevector_from_circuit(circuit); - double value = 0.0; - statevector_expectation(sv, hamiltonian, &value); - statevector_free(sv); - *expectation = value; - *variance = 0.0; - } else { - /* Denominator Tr(rho^M): estimated from measurement outcomes by a - sampling estimator; a sample value is returned here */ - *expectation = 1.0; - *variance = 0.0; - } + struct CStatevector *sv = statevector_from_circuit(circuit); + double value = 0.0; + statevector_expectation(sv, hamiltonian, &value); + statevector_free(sv); + *expectation = value; + *variance = 0.0; (void)shots; } diff --git a/docs/api/en/python/6_error_mitigation/0_overview.md b/docs/api/en/python/6_error_mitigation/0_overview.md index e44b6fa0..1ff01191 100644 --- a/docs/api/en/python/6_error_mitigation/0_overview.md +++ b/docs/api/en/python/6_error_mitigation/0_overview.md @@ -30,7 +30,7 @@ The unified pipeline selects the execution path by method: Neither method executes the input circuit directly; both first construct a family of mitigation circuits: - Zero-noise extrapolation expands the circuit according to the fold level. Global folding rewrites the circuit as `U -> U (U† U)^level`, so that the circuit depth grows with the level; selective folding performs the same expansion only on operations whose instruction name matches the given gate set, leaving the other operations as they are. When the level is `0` the mitigation circuit is a copy of the input circuit. -- Virtual distillation copies the circuit `copies` times and inserts SWAPs qubit-wise, yielding a copy-swap circuit of `copies` times the width. The numerator and the denominator are executed on the same circuit, differing only in the observable passed to the callback: the denominator is `None` and the numerator is the `Hamiltonian` expanded by the number of copies. +- Virtual distillation copies the circuit `copies` times, appends one ancillary qubit prepared in `|+⟩` by a Hadamard gate, and applies SWAPs between adjacent copies controlled by the ancillary qubit, yielding a copy-swap circuit of `copies` times the width plus 1. The numerator and the denominator are executed on the same circuit, differing only in the observable passed to the callback: the numerator receives the `Hamiltonian` expanded to the full width (the original observable on the first copy ⊗ `X` on the ancillary qubit), and the denominator receives the `Hamiltonian` with only `X` on the ancillary qubit. ### Data flow @@ -104,9 +104,9 @@ The call order is constrained by the object itself: each `ErrorMitigation` insta | **Selective folding** | Folding applied only to operations whose instruction name matches `gate_set`, leaving the other operations as they are. | | **Extrapolation method** | The fitting model that extrapolates expectation values at different noise factors to the zero-noise point, including the polynomial and exponential kinds. | | **Copies** | The number of copies of the density matrix in virtual distillation, given by `copies`, with a minimum of 2. | -| **copy-swap circuit** | The mitigation circuit obtained by copying the input circuit `copies` times and inserting SWAPs qubit-wise. | -| **Numerator circuit** | The copy-swap circuit execution used to estimate `Tr(O ρ^M)`, with the observable being the `Hamiltonian` expanded by the number of copies. | -| **Denominator circuit** | The copy-swap circuit execution used to estimate `Tr(ρ^M)`, with the observable being `None`. | +| **copy-swap circuit** | The mitigation circuit obtained by copying the input circuit `copies` times, appending one ancillary qubit prepared by a Hadamard gate, and applying ancilla-controlled SWAPs between adjacent copies qubit-wise. | +| **Numerator circuit** | The copy-swap circuit execution used to estimate `Tr(O ρ^M)`, with the observable being the `Hamiltonian` expanded to the full width (the original observable on the first copy ⊗ `X` on the ancillary qubit). | +| **Denominator circuit** | The copy-swap circuit execution used to estimate `Tr(ρ^M)`, with the observable being `X` on the ancillary qubit (identities elsewhere). | | **estimator** | The callback provided by the caller, which executes one mitigation circuit and returns an `(expectation value, variance)` pair. | | **Unified pipeline** | The sequential flow carried by `ErrorMitigation`: `run()` collects the estimates and `get_mitigated()` performs post-processing. | @@ -190,13 +190,20 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) vd = VirtualDistillation(circuit, 2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * base_width + 1 def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # 分母:Tr(ρ^M) - return (1.5, 0.25) # 分子:Tr(O ρ^M) + # 分子与分母都携带观测量:分子在第 0 份副本的比特上有非恒等 Pauli, + # 分母只有辅助比特(最后一个比特)上的 X + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # 分子:Tr(O ρ^M) + return (2.0, 1.0) # 分母:Tr(ρ^M) expectation, variance = vd.run_vd( @@ -229,9 +236,14 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation = ErrorMitigation( diff --git a/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md index 2d5ca07f..ae0a8a04 100644 --- a/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/en/python/6_error_mitigation/2_virtual_distillation.md @@ -2,7 +2,7 @@ `cqlib.error_mitigation.virtual_distillation` -`cqlib.error_mitigation.virtual_distillation` provides the low-level interface of virtual distillation: copy the base circuit several times and insert SWAPs to construct a copy-swap circuit, then estimate the numerator and the denominator separately, and finally compute the ratio to obtain the mitigated expectation value. This page covers `VirtualDistillation` and `VirtualDistillationConfig`. +`cqlib.error_mitigation.virtual_distillation` provides the low-level interface of virtual distillation: copy the base circuit several times, add one ancillary qubit prepared by a Hadamard gate and apply ancilla-controlled SWAPs between adjacent copies (a Hadamard-test circuit), then estimate the numerator and the denominator separately, and finally compute the ratio to obtain the mitigated expectation value. This page covers `VirtualDistillation` and `VirtualDistillationConfig`. ## Import @@ -50,7 +50,7 @@ Low-level helper class of virtual distillation; it holds a copy of the base circ Parameters: - `circuit` (`Circuit`): the base circuit. It is copied on construction and the circuit passed in is not modified. -- `copies` (`int`): the number of copies, which must not be less than 2, otherwise `ErrorMitigationError` is raised. The number of copies determines the width of the mitigation circuit: the width of the copy-swap circuit is `copies` times the width of the base circuit. +- `copies` (`int`): the number of copies, which must not be less than 2, otherwise `ErrorMitigationError` is raised. The number of copies determines the width of the mitigation circuit: the width of the copy-swap circuit is `copies` times the width of the base circuit plus 1 (the extra qubit is the ancillary qubit of the Hadamard test). ### Attributes @@ -59,7 +59,7 @@ Parameters: ### Methods - `set_copies(copies)`: update the number of copies. Validation is completed before assignment, so the number of copies of the object stays unchanged when validation fails. -- `build_copy_swap_circuit() -> Circuit`: construct the copy-swap circuit. The base circuit is first decomposed, then copied one by one into their respective qubit ranges, and finally SWAPs are inserted qubit-wise. +- `build_copy_swap_circuit() -> Circuit`: construct the copy-swap circuit. The base circuit is first decomposed, then copied one by one into their respective qubit ranges, and finally a Hadamard gate is applied to the trailing ancillary qubit, followed by SWAPs between adjacent copies controlled by the ancillary qubit. - `run_denominator_circuit(shots, estimator) -> tuple[float, float]`: execute the denominator circuit and return the pair given by the callback. - `run_numerator_circuit(hamiltonian, shots, estimator) -> tuple[float, float]`: execute the numerator circuit and return the pair given by the callback. - `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator) -> tuple[float, float]`: execute the numerator and the denominator in turn and compute the ratio, returning `(mitigated expectation value, variance)`. @@ -78,9 +78,9 @@ The output of `build_copy_swap_circuit()` consists of three parts: 1. The base circuit is first decomposed, ensuring that a group of already expanded operations is copied. 2. The base circuit is copied `copies` times, with the `i`-th copy landing on the qubit range offset by `i` times the base width. -3. For the 1st copy and each of the other copies, SWAPs are inserted qubit-wise. SWAPs are inserted only between the 1st copy and each of the other copies; the other copies are not directly connected to one another. +3. One ancillary qubit is appended as the last qubit, prepared in `|+⟩` by an `H` gate; then, between each pair of adjacent copies, SWAPs are inserted qubit-wise, all controlled by the ancillary qubit (CSWAP). This ladder of controlled SWAPs implements the derangement of the copies required by the Hadamard test. -Therefore, when the number of copies is 2, each operation of the base circuit appears 2 times, plus the qubit-wise SWAPs; as the number of copies increases, the number of SWAPs grows linearly with the number of copies rather than as pairwise combinations of copies. +Therefore, when the number of copies is 2, each operation of the base circuit appears 2 times, plus one `H` on the ancillary qubit and the qubit-wise controlled SWAPs; as the number of copies increases, the number of controlled SWAPs grows linearly with the number of copies rather than as pairwise combinations of copies. ```python from cqlib.circuit import Circuit @@ -91,7 +91,7 @@ circuit.x(0) vd = VirtualDistillation(circuit, 2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * base_width + 1 ``` --- @@ -102,11 +102,13 @@ Virtual distillation has only one mitigation circuit; both the numerator and the | Method | Circuit received by the callback | Observable received by the callback | Shots received by the callback | | --- | --- | --- | --- | -| `run_denominator_circuit(shots, estimator)` | copy-swap circuit | `None` | `shots` | -| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap circuit | The `Hamiltonian` expanded by the number of copies | `shots` | +| `run_denominator_circuit(shots, estimator)` | copy-swap circuit | `Hamiltonian` of `X` on the ancillary qubit (identities elsewhere) | `shots` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | copy-swap circuit | The `Hamiltonian` expanded to the full width (`O` on the first copy ⊗ `X` on the ancillary qubit) | `shots` | | `run_vd(...)` | copy-swap circuit | Numerator first, then denominator, as above | `shots_numerator` and `shots_denominator` respectively | -The observable used by the numerator is obtained by expanding the `Hamiltonian` passed in: the original Pauli terms are kept on their respective qubits and the higher-order qubits are padded with `Z`, so that the observable qubit count matches the width of the copy-swap circuit. The expansion is performed inside the class, and the caller only needs to pass the observable acting on the base circuit. +Both the numerator and the denominator observables are concrete `Hamiltonian` objects; they are told apart by content — the numerator observable has non-identity Paulis on the first copy's qubits, while the denominator observable is non-identity only on the ancillary (last) qubit. + +The observable used by the numerator is obtained by expanding the `Hamiltonian` passed in: the original Pauli terms are kept on their respective qubits (the first copy), the remaining copies are padded with `I`, and an `X` is appended on the ancillary qubit, so that the observable qubit count matches the width of the copy-swap circuit. The expansion is performed inside the class, and the caller only needs to pass the observable acting on the base circuit. `run_vd()` completes two steps in turn: first execute the numerator with `shots_numerator`, then execute the denominator with `shots_denominator`, and then combine the results as follows: @@ -136,9 +138,16 @@ vd = VirtualDistillation(circuit, 2) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # 分母:Tr(ρ^M) - return (1.5, 0.25) # 分子:Tr(O ρ^M) + # 分子与分母都携带观测量:分子在第 0 份副本的比特上有非恒等 Pauli, + # 分母只有辅助比特(最后一个比特)上的 X + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # 分子:Tr(O ρ^M) + return (2.0, 1.0) # 分母:Tr(ρ^M) expectation, variance = vd.run_vd( diff --git a/docs/api/en/python/6_error_mitigation/3_unified.md b/docs/api/en/python/6_error_mitigation/3_unified.md index cfc15745..bbfba3ec 100644 --- a/docs/api/en/python/6_error_mitigation/3_unified.md +++ b/docs/api/en/python/6_error_mitigation/3_unified.md @@ -161,7 +161,7 @@ When the `RunArgs` passed to `run()` do not match the method given at constructi Zero-noise extrapolation: `run()` constructs the mitigation circuits according to the configured fold levels, calls the callback once per circuit, with the observable being the `Hamiltonian` passed in and the number of shots taken from `RunArgs.zne(shots=...)`. Only the 1st element of the pair returned by the callback is used, and the variance is discarded. `get_mitigated()` fits the extrapolation input, and the `variance` of the result is `None`. -Virtual distillation: `run()` first constructs the copy-swap circuit, then calls the callback twice on the same circuit — the numerator first, then the denominator. The numerator receives the observable expanded by the number of copies and `shots_numerator`, and the denominator receives `None` and `shots_denominator`. `get_mitigated()` computes the ratio according to the combination formula of [Virtual distillation](2_virtual_distillation.md), and the `variance` of the result has a value. +Virtual distillation: `run()` first constructs the copy-swap circuit, then calls the callback twice on the same circuit — the numerator first, then the denominator. The numerator receives the observable expanded to the full width (the original observable on the first copy ⊗ `X` on the ancillary qubit) and `shots_numerator`, and the denominator receives the observable with only `X` on the ancillary qubit and `shots_denominator`. `get_mitigated()` computes the ratio according to the combination formula of [Virtual distillation](2_virtual_distillation.md), and the `variance` of the result has a value. ### Example diff --git a/docs/api/en/rust/6_error_mitigation/0_overview.md b/docs/api/en/rust/6_error_mitigation/0_overview.md index 053dabe8..700e830f 100644 --- a/docs/api/en/rust/6_error_mitigation/0_overview.md +++ b/docs/api/en/rust/6_error_mitigation/0_overview.md @@ -24,7 +24,7 @@ This module does not include a simulation or execution backend; all mitigation m pub type Estimator<'a> = dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64) + 'a; ``` -The parameters are, in order, the circuit to execute, the observable to estimate and the number of shots, and the return value is `(expectation value, variance)`. A denominator circuit carries no observable, so the second parameter is `None`; `ZNEMitigation::run_em_sequence` does not specify a number of shots, so the third parameter is `None`. Public methods uniformly accept it as `&Estimator<'_>`, that is, the trait object reference `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`, and both closures and function items can be passed by reference directly. Only virtual distillation uses the variance in the return value; zero-noise extrapolation takes only the expectation value. +The parameters are, in order, the circuit to execute, the observable to estimate and the number of shots, and the return value is `(expectation value, variance)`. Virtual distillation passes a concrete observable for both the numerator and the denominator — the denominator observable is `X` on the ancillary qubit of the copy-swap circuit; `ZNEMitigation::run_em_sequence` does not specify a number of shots, so the third parameter is `None`. Public methods uniformly accept it as `&Estimator<'_>`, that is, the trait object reference `&dyn Fn(&Circuit, Option<&Hamiltonian>, Option) -> (f64, f64)`, and both closures and function items can be passed by reference directly. Only virtual distillation uses the variance in the return value; zero-noise extrapolation takes only the expectation value. ### Mitigation pipeline state @@ -100,8 +100,8 @@ assert_eq!(mitigated.expectation, 0.5); | **Gate folding** | A construction that rewrites a circuit as `U -> U (U† U)^level`; folding only the specified gates is called selective folding. | | **Virtual distillation** | A method that estimates `Tr(O ρ^M) / Tr(ρ^M)` through copies and a copy-swap circuit, where `M` is the number of copies. | | **Copy** | The number of copies of the base circuit prepared in parallel in virtual distillation; the minimum is 2. | -| **copy-swap circuit** | A circuit that places multiple copies of the base circuit side by side on non-overlapping qubit ranges and inserts a bitwise SWAP between the first copy and each of the others; its width equals the number of copies multiplied by the width of the base circuit. | -| **Numerator circuit / denominator circuit** | The two statistics circuits of virtual distillation. The numerator circuit carries the expanded observable; the denominator circuit carries no observable, and the estimator receives `None`. | +| **copy-swap circuit** | A circuit that places multiple copies of the base circuit side by side on non-overlapping qubit ranges, prepares one trailing ancillary qubit in `|+⟩` with a Hadamard gate, and applies a ladder of bitwise SWAPs between adjacent copies, all controlled by the ancillary qubit; its width equals the number of copies multiplied by the width of the base circuit, plus one. | +| **Numerator circuit / denominator circuit** | The two statistics circuits of virtual distillation. The numerator circuit carries the expanded observable (the original observable on the first copy and `X` on the ancillary qubit); the denominator circuit carries `X` on the ancillary qubit with identities elsewhere. | | **Mitigation pipeline** | The sequential flow represented by `ErrorMitigation`: `new` creates it, `run` collects the raw data, and `get_mitigated` performs the method-specific post-processing. | --- @@ -159,7 +159,8 @@ let mut mitigation = ErrorMitigation::new( ) .unwrap(); -// 分子线路带可观测量,分母线路不带 +// 分子与分母都携带可观测量:分子在第 0 份副本的比特上有非恒等 Pauli, +// 分母只有辅助比特上的 X mitigation .run( &hamiltonian, @@ -168,7 +169,12 @@ mitigation shots_denominator: 2, }, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { @@ -232,13 +238,18 @@ let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))] let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); -// 两份基底线路加一对逐比特 SWAP +// 两份基底线路加 1 个辅助比特:辅助比特上的 H 与一次受控 SWAP let copy_swap = vd.build_copy_swap_circuit().unwrap(); -assert_eq!(copy_swap.width(), 2); +assert_eq!(copy_swap.width(), 3); let (mu_vd, var_vd) = vd .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { diff --git a/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md b/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md index 182d5c90..24881f94 100644 --- a/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md +++ b/docs/api/en/rust/6_error_mitigation/2_virtual_distillation.md @@ -32,7 +32,7 @@ Other methods: - `fn new(circuit: Circuit, copies: usize) -> Result`: the creation entry point; it takes ownership of the circuit. The template circuit can only be passed through `new`, and there is no corresponding read interface. - `fn set_copies(&mut self, copies: usize) -> Result<(), ErrorMitigationError>`: updates the number of copies. On failure the original value stays unchanged. - `fn build_copy_swap_circuit(&self) -> Result`: constructs the copy-swap circuit from the base circuit. -- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`: runs the denominator circuit and returns `(mean, variance)`. +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: runs the denominator circuit and returns `(mean, variance)`. - `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: runs the numerator circuit and returns `(mean, variance)`. - `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: runs the complete flow and returns the mitigated `(expectation value, variance)`. @@ -55,10 +55,10 @@ Raises: ### `build_copy_swap_circuit(&self) -> Result` -Constructs the copy-swap circuit, which consists of three parts: first the base circuit is decomposed into gates, then `copies` copies of the base circuit are prepared side by side — the `i`-th copy is shifted as a whole onto the qubit range starting at `i × base width`, and the copies do not overlap — and finally a `SWAP` is inserted bit by bit between the first copy and each of the others. +Constructs the copy-swap circuit — the Hadamard-test circuit of the virtual distillation protocol — which consists of three parts: first the base circuit is decomposed into gates, then `copies` copies of the base circuit are prepared side by side — the `i`-th copy is shifted as a whole onto the qubit range starting at `i × base width`, and the copies do not overlap — and finally one ancillary qubit (the last qubit) is prepared in `|+⟩` by a Hadamard gate, followed by a ladder of `SWAP`s between adjacent copies, each controlled by the ancillary qubit (CSWAP), implementing the derangement of the copies. -- The circuit width is `copies × base width`, where the base width is the width of the decomposed circuit. -- The number of operations is `copies` sets of base operations plus `(copies - 1) × base width` `SWAP`s. +- The circuit width is `copies × base width + 1`, where the base width is the width of the decomposed circuit and the last qubit is the ancillary qubit. +- The number of operations is `copies` sets of base operations plus one `H` and `(copies - 1) × base width` controlled `SWAP`s. Returns: @@ -70,8 +70,8 @@ Raises: ### Numerator and denominator -- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), CircuitError>`: estimates the denominator `Tr(ρ^M)` on the copy-swap circuit. The observable parameter the estimator receives is `None` and the number of shots is `Some(shots)`; for the callback form see [Overview](0_overview.md). -- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: estimates the numerator `Tr(O ρ^M)` on the copy-swap circuit. The observable is first expanded inside the module to the width of the copy-swap circuit: the original Pauli terms keep their original qubit indices, phase and coefficients unchanged, and `Z` is filled in on the newly added higher-index qubits; the expanded result is passed in through the observable parameter of the estimator, and the number of shots is `Some(shots)`. +- `fn run_denominator_circuit(&self, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: estimates the denominator `Tr(ρ^M)` on the copy-swap circuit. The estimator receives `Some(denominator observable)` — `X` on the ancillary qubit (the last qubit) with identities everywhere else — and the number of shots is `Some(shots)`; for the callback form see [Overview](0_overview.md). +- `fn run_numerator_circuit(&self, hamiltonian: &Hamiltonian, shots: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: estimates the numerator `Tr(O ρ^M)` on the copy-swap circuit. The observable is first expanded inside the module to the width of the copy-swap circuit: the original Pauli terms keep their original qubit indices (i.e. they act on the first copy), phase and coefficients unchanged, the remaining copies are padded with `I`, and an `X` is appended on the ancillary qubit (the last qubit); the expanded result is passed in through the observable parameter of the estimator, and the number of shots is `Some(shots)`. - `fn run_vd(&self, hamiltonian: &Hamiltonian, shots_numerator: usize, shots_denominator: usize, estimator: &Estimator<'_>) -> Result<(f64, f64), ErrorMitigationError>`: first validates that the number of qubits of the Hamiltonian equals the width of the base circuit, then executes the numerator circuit and the denominator circuit in turn, and finally combines them into the mitigated result. The numerator and the denominator use their own number of shots. Raises for `run_vd`: @@ -94,11 +94,12 @@ let q0 = Qubit::new(0); let mut circuit = Circuit::new(1); circuit.x(q0).unwrap(); -// 两份单比特副本:X 作用于比特 0、X 作用于比特 1,再加一对逐比特 SWAP +// 两份单比特副本加 1 个辅助比特:X 作用于比特 0、X 作用于比特 1, +// 辅助比特(比特 2)上的 H,以及一次由辅助比特控制的 SWAP let vd = VirtualDistillation::new(circuit, 2).unwrap(); let copy_swap = vd.build_copy_swap_circuit().unwrap(); -assert_eq!(copy_swap.width(), 2); -assert_eq!(copy_swap.operations().len(), 3); +assert_eq!(copy_swap.width(), 3); +assert_eq!(copy_swap.operations().len(), 4); ``` ```rust @@ -127,11 +128,17 @@ let mut pauli = PauliString::new(1); pauli.set_pauli(0, Pauli::Z); let hamiltonian = Hamiltonian::from_list(vec![(pauli, Complex64::new(1.0, 0.0))]).unwrap(); -// 分子线路带可观测量,分母线路不带;两者使用各自的 shot 数 +// 分子与分母都携带可观测量,按内容区分:分子在第 0 份副本的比特上有 +// 非恒等 Pauli,分母只有辅助比特上的 X;两者使用各自的 shot 数 let vd = VirtualDistillation::new(Circuit::new(1), 2).unwrap(); let (mu_vd, var_vd) = vd .run_vd(&hamiltonian, 3, 2, &|_circuit, hamiltonian_arg, shots| { - if hamiltonian_arg.is_some() { + let observable = hamiltonian_arg.unwrap(); + let is_numerator = observable + .terms + .iter() + .any(|(term, _)| term.get_pauli(0) != Pauli::I); + if is_numerator { assert_eq!(shots, Some(3)); (1.5, 0.25) } else { @@ -175,6 +182,6 @@ There is no `Default` implementation, so `copies` must be given explicitly. Impl | `ErrorMitigationError::HamiltonianQubitCountMismatch` | The number of qubits of the Hamiltonian received by `run_vd` is inconsistent with the width of the base circuit. | | `ErrorMitigationError::ZeroDenominatorMean` | The mean of the denominator circuit is zero. | | `ErrorMitigationError::Circuit` | Carries `CircuitError::QubitCountMismatch`, indicating that the width of the expanded observable is inconsistent with the width of the copy-swap circuit. | -| `CircuitError` | The circuit error returned directly by `build_copy_swap_circuit` and `run_denominator_circuit`. | +| `CircuitError` | The circuit error returned directly by `build_copy_swap_circuit`; `run_denominator_circuit` wraps it into `ErrorMitigationError::Circuit`. | For the complete definition of the error type, see [Unified pipeline](3_unified.md). diff --git a/docs/api/en/rust/6_error_mitigation/3_unified.md b/docs/api/en/rust/6_error_mitigation/3_unified.md index 67c4d340..99ac0ab4 100644 --- a/docs/api/en/rust/6_error_mitigation/3_unified.md +++ b/docs/api/en/rust/6_error_mitigation/3_unified.md @@ -77,7 +77,7 @@ Parameters: Execution details differ by method: - Zero-noise extrapolation: constructs the folded circuits according to the configured fold levels, calls the estimator one by one with the observable parameter `Some(hamiltonian)` and the number of shots `RunArgs::Zne::shots`, and takes only the expectation value from the return value. The folded circuits, the expectation values, the noise factors and the execution arguments are kept together. -- Virtual distillation: constructs the copy-swap circuit and expands the observable to the width of that circuit; the estimator of the numerator circuit receives `Some(expanded observable)` and `Some(shots_numerator)`, and the denominator circuit receives `None` and `Some(shots_denominator)`. The means and variances of the numerator and the denominator are kept together. +- Virtual distillation: constructs the copy-swap circuit and expands the observable to the width of that circuit; the estimator of the numerator circuit receives `Some(expanded observable)` — the original observable on the first copy together with `X` on the ancillary qubit — and `Some(shots_numerator)`, and the denominator circuit receives `Some(denominator observable)` — `X` on the ancillary qubit with identities elsewhere — and `Some(shots_denominator)`. The means and variances of the numerator and the denominator are kept together. Returns: diff --git a/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md index 2a8aae16..5f679932 100644 --- a/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/0_overview.md @@ -88,7 +88,7 @@ Estimator = Callable[ | 参数 | 含义 | |------|------| | `run_circuit` | 待执行的线路(可能是折叠线路或 copy-swap 线路) | -| `observable` | 待估计的 `Hamiltonian`;分母线路时为 `None` | +| `observable` | 待估计的 `Hamiltonian`;VD 的分子与分母调用都会携带(分母观测量为辅助比特上的 `X`) | | `shots` | 本次执行的 shot 数;部分 API 可为 `None` | | 返回值 | `(expectation, variance)` | diff --git a/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md index 9544d1e8..55ec311f 100644 --- a/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/2_virtual_distillation.md @@ -1,6 +1,6 @@ # 虚拟蒸馏(Virtual Distillation) -虚拟蒸馏(Virtual Distillation, VD)通过多份密度矩阵拷贝与 **copy-swap** 线路,估计: +虚拟蒸馏(Virtual Distillation, VD)通过多份密度矩阵拷贝与 **copy-swap** 线路(即 Huggins 等人 PRX 11, 041036 的 Hadamard-test 线路),估计: ```text Tr(O ρ^M) / Tr(ρ^M) @@ -29,12 +29,19 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) vd = em.VirtualDistillation(circuit, copies=2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * 基础宽度 + 1(最后一个比特是辅助比特) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # denominator: Tr(ρ^M) - return (1.5, 0.25) # numerator: Tr(O ρ^M) + # 分子与分母都携带 Hamiltonian;分子在第 0 份副本的比特上有非恒等 + # Pauli,分母只在辅助比特(最后一个比特)上携带 X。 + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # numerator: Tr(O ρ^M) + return (2.0, 1.0) # denominator: Tr(ρ^M) mean, var = vd.run_vd(hamiltonian, shots_numerator=3, shots_denominator=2, estimator=estimator) print("expectation:", mean) # 0.75 @@ -52,8 +59,8 @@ vd = em.VirtualDistillation(circuit, copies=2) copy_swap = vd.build_copy_swap_circuit() ``` -- 输出线路宽度为 `copies` × 基础线路宽度; -- 内部通过 pairwise `SWAP` 耦合各拷贝寄存器; +- 输出线路宽度为 `copies` × 基础线路宽度 **+ 1**,最后一个比特是辅助比特; +- 辅助比特先经 `H` 门制备到 `|+⟩`,然后在相邻副本之间施加**由辅助比特控制**的 `SWAP` 梯(CSWAP),实现各副本的轮换(derangement); - 原始 `circuit` 不会被修改。 可通过 `set_copies(copies)` 更新拷贝数,再重新构造线路。 @@ -64,11 +71,11 @@ copy_swap = vd.build_copy_swap_circuit() | API | 说明 | |-----|------| -| `run_denominator_circuit(shots, estimator)` | 估计 `Tr(ρ^M)`;`observable=None` | -| `run_numerator_circuit(hamiltonian, shots, estimator)` | 估计 `Tr(O ρ^M)` | +| `run_denominator_circuit(shots, estimator)` | 估计 `Tr(ρ^M)`;观测量为辅助比特上的 `X` | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | 估计 `Tr(O ρ^M)`;观测量为第 0 份副本上的 `O` ⊗ 辅助比特上的 `X` | | `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator)` | 一次完成分子/分母并返回比值 | -`estimator` 在分母调用时收到 `observable=None`,在分子调用时收到具体 `Hamiltonian`。 +`estimator` 在分子与分母调用中都收到具体 `Hamiltonian`:分子观测量在第 0 份副本的比特上非平凡,并在辅助比特上携带 `X`;分母观测量只在辅助比特上携带 `X`(其余为恒等)。 --- @@ -76,8 +83,8 @@ copy_swap = vd.build_copy_swap_circuit() | `copies` | 线路宽度 | 典型效果 | |----------|----------|----------| -| `2` | 2 比特 | 最低配置,开销较小 | -| `3` | 3 比特 | 更强蒸馏,开销更高 | +| `2` | 3 比特(2 份副本 + 1 个辅助比特) | 最低配置,开销较小 | +| `3` | 4 比特(3 份副本 + 1 个辅助比特) | 更强蒸馏,开销更高 | `M` 越大,越突出主导本征态成分,但制备与测量成本显著增加。 @@ -92,9 +99,14 @@ mitigation = em.ErrorMitigation( ) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) @@ -110,7 +122,7 @@ VD 的 `MitigatedResult.variance` 通常有值。 ## 说明 - VD 适用于希望放大主导本征态、降低混合噪声影响的场景; -- `Hamiltonian` 会在内部按拷贝数扩展,无需手动复制 Pauli 项; +- 分子观测量会在内部扩展为「第 0 份副本上的 `O` ⊗ 其余副本恒等 ⊗ 辅助比特上的 `X`」,分母观测量为辅助比特上的 `X`,无需手动扩展 Pauli 项; - 正式实验应分别记录 numerator 与 denominator 的 shot 预算。 --- diff --git a/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md b/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md index 1da35aa8..f0e7872f 100644 --- a/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md +++ b/docs/documentation/cn/1_cqlib/6_error_mitigation/3_unified_api.md @@ -86,10 +86,15 @@ mitigation = em.ErrorMitigation( ) def estimator(run_circuit, observable, shots): - assert run_circuit.width == 2 - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + assert run_circuit.width == 3 # 2 份副本 × 1 比特 + 1 个辅助比特 + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) @@ -137,7 +142,8 @@ em.MitigationMethod.virtual_distillation(em.VirtualDistillationConfig(2)) ```python def estimator(run_circuit, observable, shots): # run_circuit: 可能是折叠线路或 copy-swap 线路 - # observable: Hamiltonian 或 None(VD 分母) + # observable: Hamiltonian(VD 的分子与分母都会携带;分母观测量为 + # 辅助比特上的 X) # shots: int 或 None return (expectation, variance) ``` @@ -146,7 +152,7 @@ def estimator(run_circuit, observable, shots): - 返回值必须是 `(float, float)`; - ZNE 的 `run_em_sequence_with_shots` 会把 `shots` 传给 estimator; -- VD 分母路径上 `observable is None`; +- VD 的分子与分母路径上 `observable` 都是 `Hamiltonian`,按内容区分——分子在第 0 份副本的比特上有非恒等 Pauli,分母只在辅助比特(最后一个比特)上携带 `X`; - estimator 内部异常会原样向上抛出。 --- diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md b/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md index fa255ee4..bd382e4f 100644 --- a/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/0_overview.md @@ -88,7 +88,7 @@ Estimator = Callable[ | Parameter | Meaning | |------|------| | `run_circuit` | The circuit to be executed (possibly a folded circuit or a copy-swap circuit) | -| `observable` | The `Hamiltonian` to be estimated; `None` for the denominator circuit | +| `observable` | The `Hamiltonian` to be estimated; both VD numerator and denominator calls carry one (the denominator observable is `X` on the ancillary qubit) | | `shots` | The shot count of this execution; may be `None` in some APIs | | Return value | `(expectation, variance)` | diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md b/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md index 1d5b463c..74e10c28 100644 --- a/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/2_virtual_distillation.md @@ -1,6 +1,6 @@ # Virtual Distillation (VD) -Virtual distillation (VD) estimates the following through multiple copies of the density matrix and a **copy-swap** circuit: +Virtual distillation (VD) estimates the following through multiple copies of the density matrix and a **copy-swap** circuit (the Hadamard-test circuit of Huggins et al., PRX 11, 041036): ```text Tr(O ρ^M) / Tr(ρ^M) @@ -29,12 +29,20 @@ hamiltonian = Hamiltonian.from_list([(PauliString.from_str("Z"), 1.0)]) vd = em.VirtualDistillation(circuit, copies=2) copy_swap = vd.build_copy_swap_circuit() -print(copy_swap.width) # 2 +print(copy_swap.width) # 3 = copies * base_width + 1 (the last qubit is the ancilla) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) # denominator: Tr(ρ^M) - return (1.5, 0.25) # numerator: Tr(O ρ^M) + # Both the numerator and the denominator carry a Hamiltonian; the + # numerator also acts on the first copy's qubits, the denominator only + # carries X on the ancillary (last) qubit. + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) # numerator: Tr(O ρ^M) + return (2.0, 1.0) # denominator: Tr(ρ^M) mean, var = vd.run_vd(hamiltonian, shots_numerator=3, shots_denominator=2, estimator=estimator) print("expectation:", mean) # 0.75 @@ -52,8 +60,8 @@ vd = em.VirtualDistillation(circuit, copies=2) copy_swap = vd.build_copy_swap_circuit() ``` -- The output circuit width is `copies` × the base circuit width; -- The copy registers are coupled internally through pairwise `SWAP`; +- The output circuit width is `copies` × the base circuit width **+ 1**; the last qubit is an ancillary qubit; +- The ancillary qubit is first prepared in `|+⟩` by an `H` gate, then adjacent copies are coupled by a ladder of `SWAP`s, each **controlled by the ancillary qubit** (CSWAP), which implements the derangement of the copies; - The original `circuit` is not modified. The number of copies can be updated through `set_copies(copies)`, and the circuit can then be reconstructed. @@ -64,11 +72,11 @@ The number of copies can be updated through `set_copies(copies)`, and the circui | API | Description | |-----|------| -| `run_denominator_circuit(shots, estimator)` | Estimates `Tr(ρ^M)`; `observable=None` | -| `run_numerator_circuit(hamiltonian, shots, estimator)` | Estimates `Tr(O ρ^M)` | +| `run_denominator_circuit(shots, estimator)` | Estimates `Tr(ρ^M)`; the observable is `X` on the ancillary qubit | +| `run_numerator_circuit(hamiltonian, shots, estimator)` | Estimates `Tr(O ρ^M)`; the observable is `O` on the first copy ⊗ `X` on the ancillary qubit | | `run_vd(hamiltonian, shots_numerator, shots_denominator, estimator)` | Completes the numerator and denominator in one call and returns their ratio | -`estimator` receives `observable=None` in the denominator call and a concrete `Hamiltonian` in the numerator call. +`estimator` receives a concrete `Hamiltonian` in both calls: the numerator observable acts non-trivially on the first copy's qubits and carries `X` on the ancillary qubit, while the denominator observable carries only `X` on the ancillary qubit (identities elsewhere). --- @@ -76,8 +84,8 @@ The number of copies can be updated through `set_copies(copies)`, and the circui | `copies` | Circuit width | Typical effect | |----------|----------|----------| -| `2` | 2 qubits | Minimum configuration, lower overhead | -| `3` | 3 qubits | Stronger distillation, higher overhead | +| `2` | 3 qubits (2 copies + 1 ancilla) | Minimum configuration, lower overhead | +| `3` | 4 qubits (3 copies + 1 ancilla) | Stronger distillation, higher overhead | The larger `M` is, the more the dominant eigenstate component is emphasized, but the preparation and measurement cost increases significantly. @@ -92,9 +100,14 @@ mitigation = em.ErrorMitigation( ) def estimator(run_circuit, observable, shots): - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) @@ -110,7 +123,7 @@ The `MitigatedResult.variance` of VD usually has a value. ## Notes - VD suits scenarios where the dominant eigenstate is to be amplified and the impact of mixed noise reduced; -- The `Hamiltonian` is expanded internally according to the number of copies, so Pauli terms do not need to be copied manually; +- The numerator observable is expanded internally to `O` on the first copy ⊗ identities ⊗ `X` on the ancillary qubit, and the denominator observable is `X` on the ancillary qubit, so Pauli terms do not need to be expanded manually; - Formal experiments should record the shot budget of the numerator and the denominator separately. --- diff --git a/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md b/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md index b370fd5d..6e15f0e5 100644 --- a/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md +++ b/docs/documentation/en/1_cqlib/6_error_mitigation/3_unified_api.md @@ -70,10 +70,15 @@ mitigation = em.ErrorMitigation( ) def estimator(run_circuit, observable, shots): - assert run_circuit.width == 2 - if observable is None: - return (2.0, 1.0) - return (1.5, 0.25) + assert run_circuit.width == 3 # 2 copies × 1 qubit + 1 ancilla + is_numerator = any( + qubit < observable.num_qubits - 1 + for term, _ in observable.terms + for qubit in term.support() + ) + if is_numerator: + return (1.5, 0.25) + return (2.0, 1.0) mitigation.run(hamiltonian, em.RunArgs.virtual_distillation(3, 2), estimator) result = mitigation.get_mitigated(em.ProcessArgs.virtual_distillation()) @@ -121,7 +126,9 @@ em.MitigationMethod.virtual_distillation(em.VirtualDistillationConfig(2)) ```python def estimator(run_circuit, observable, shots): # run_circuit: may be a folded circuit or a copy-swap circuit - # observable: a Hamiltonian or None (VD denominator) + # observable: a Hamiltonian (for VD both the numerator and the + # denominator carry one; the denominator observable is X + # on the ancillary qubit) # shots: an int or None return (expectation, variance) ``` @@ -130,7 +137,7 @@ Confirm the following when implementing: - The return value must be `(float, float)`; - The `run_em_sequence_with_shots` of ZNE passes `shots` to the estimator; -- On the VD denominator path, `observable is None`; +- On both VD paths `observable` is a `Hamiltonian`; the numerator and denominator are told apart by content — the numerator acts non-trivially on the first copy's qubits, the denominator carries only `X` on the ancillary (last) qubit; - Exceptions inside the estimator are propagated upwards as-is. --- From 875ded10dad79285730ca01ff93bfbc9e7e566dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Tue, 29 Sep 2026 14:45:40 +0800 Subject: [PATCH 3/6] fix(device): reject zero-qubit device factory construction Device::line, bidirectional_line, ring, star, grid, from_edges, and line_from_qubits previously accepted zero qubits and returned an empty device. Add DeviceError::EmptyDevice and validate the two construction entry points (line_from_qubits and from_u32_edges) so all factories reject empty input. --- crates/cqlib-core/src/device/device_impl.rs | 10 ++++++ crates/cqlib-core/src/device/device_test.rs | 36 +++++++++++++++++++++ crates/cqlib-core/src/device/error.rs | 5 +++ 3 files changed, 51 insertions(+) diff --git a/crates/cqlib-core/src/device/device_impl.rs b/crates/cqlib-core/src/device/device_impl.rs index 49987693..68ffa95a 100644 --- a/crates/cqlib-core/src/device/device_impl.rs +++ b/crates/cqlib-core/src/device/device_impl.rs @@ -472,10 +472,17 @@ impl Device { /// /// The device contains every supplied physical qubit, all of which are /// online. Couplings follow the supplied order: `qubits[i] -> qubits[i + 1]`. + /// + /// # Errors + /// + /// Returns [`DeviceError::EmptyDevice`] if `physical_qubits` is empty. pub fn line_from_qubits( name: impl Into, physical_qubits: Vec, ) -> Result { + if physical_qubits.is_empty() { + return Err(DeviceError::EmptyDevice); + } let qubits = physical_qubits.iter().copied().collect::>(); let topology = Topology::line(physical_qubits).map_err(DeviceError::InvalidTopology)?; Self::new(name, qubits, topology) @@ -569,6 +576,9 @@ impl Device { num_qubits: u32, edges: &[(u32, u32)], ) -> Result { + if num_qubits == 0 { + return Err(DeviceError::EmptyDevice); + } let physical_qubits = (0..num_qubits).map(PhysicalQubit::new).collect::>(); let qubits = physical_qubits.iter().copied().collect::>(); let coupling_map = edges diff --git a/crates/cqlib-core/src/device/device_test.rs b/crates/cqlib-core/src/device/device_test.rs index 463f611e..3d6681c1 100644 --- a/crates/cqlib-core/src/device/device_test.rs +++ b/crates/cqlib-core/src/device/device_test.rs @@ -359,6 +359,42 @@ fn grid_device_uses_row_major_bidirectional_nearest_neighbors() { ); } +#[test] +fn device_factories_reject_zero_qubits() { + assert_eq!( + Device::line("bad", 0).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::line_from_qubits("bad", vec![]).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::bidirectional_line("bad", 0).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::ring("bad", 0).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::star("bad", 0, 0).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::grid("bad", 0, 3).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::grid("bad", 3, 0).unwrap_err(), + DeviceError::EmptyDevice + ); + assert_eq!( + Device::from_edges("bad", 0, &[]).unwrap_err(), + DeviceError::EmptyDevice + ); +} + #[test] fn from_edges_creates_explicit_directed_couplings() { let device = Device::from_edges("custom", 3, &[(0, 2), (2, 1)]).unwrap(); diff --git a/crates/cqlib-core/src/device/error.rs b/crates/cqlib-core/src/device/error.rs index 5491a48b..675aafb1 100644 --- a/crates/cqlib-core/src/device/error.rs +++ b/crates/cqlib-core/src/device/error.rs @@ -22,6 +22,8 @@ use thiserror::Error; #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum DeviceError { + /// The device must contain at least one physical qubit. + EmptyDevice, InvalidOnlineQubit(PhysicalQubit), QubitNotInDevice(PhysicalQubit), QubitNotInTopology(PhysicalQubit), @@ -153,6 +155,9 @@ impl std::error::Error for DeviceValidationError {} impl fmt::Display for DeviceError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::EmptyDevice => { + write!(f, "Device must contain at least one physical qubit") + } Self::InvalidOnlineQubit(q) => { write!( f, From 20839402405b4e534542c87aeccbab1c66603665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Tue, 29 Sep 2026 14:55:22 +0800 Subject: [PATCH 4/6] docs(tianyan): fix wait/wait_raw parameter names to timeout and poll_interval --- .../cn/1_cqlib/7_tianyan/0_overview.md | 6 +++--- .../cn/1_cqlib/7_tianyan/3_task_result.md | 14 +++++++------- .../cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md | 2 +- .../cn/1_cqlib/7_tianyan/5_readout_mitigation.md | 10 +++++----- .../en/1_cqlib/7_tianyan/0_overview.md | 6 +++--- .../en/1_cqlib/7_tianyan/3_task_result.md | 14 +++++++------- .../en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md | 2 +- .../en/1_cqlib/7_tianyan/5_readout_mitigation.md | 10 +++++----- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md b/docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md index e342c80b..c9944c26 100644 --- a/docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/0_overview.md @@ -80,7 +80,7 @@ backend = platform.get_backend("tianyan-287") qcis = "H Q1\nM Q1" task = backend.run([qcis], shots=1000) -results = task.wait(timeout_secs=120.0, poll_interval_secs=5.0) +results = task.wait(timeout=120.0, poll_interval=5.0) result = results[0] print(result.task_id) @@ -124,8 +124,8 @@ classDiagram +shots +submitted_at +status() - +wait(timeout_secs, poll_interval_secs) - +wait_raw(timeout_secs, poll_interval_secs) + +wait(timeout, poll_interval) + +wait_raw(timeout, poll_interval) } class ExecutionResult { +task_id diff --git a/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md b/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md index d2bcf7ce..984e1fd8 100644 --- a/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/3_task_result.md @@ -92,8 +92,8 @@ for result in partial_results: ```python results = task.wait( - timeout_secs=120.0, - poll_interval_secs=5.0, + timeout=120.0, + poll_interval=5.0, ) for result in results: @@ -106,8 +106,8 @@ for result in results: | 参数 | 说明 | |---|---| -| `timeout_secs` | 最大等待秒数 | -| `poll_interval_secs` | 轮询间隔秒数,默认 `5.0` | +| `timeout` | 最大等待秒数 | +| `poll_interval` | 轮询间隔秒数,默认 `5.0` | `wait()` 在阻塞等待时会释放 Python GIL,因此不会阻塞其他 Python 线程运行。 @@ -117,8 +117,8 @@ for result in results: ```python raw_results = task.wait_raw( - timeout_secs=120.0, - poll_interval_secs=5.0, + timeout=120.0, + poll_interval=5.0, ) ``` @@ -180,7 +180,7 @@ print(len(task.task_ids)) # 120 | 现象 | 常见原因 | 处理方式 | |---|---|---| | 提交失败 | 后端不可用、QCIS 格式错误、API Key 无效或网络问题 | 检查 `backend.status`、QCIS 文本和认证状态 | -| `wait` 超时 | 队列等待时间长或任务未完成 | 增大 `timeout_secs`,或改用 `status()` 分批查询 | +| `wait` 超时 | 队列等待时间长或任务未完成 | 增大 `timeout`,或改用 `status()` 分批查询 | | 返回结果少于提交线路数 | 使用了 `status()`,它只返回已完成结果 | 使用 `wait()` 等待全部完成 | | counts 为空 | 平台结果尚未完成或解析失败 | 检查任务状态和原始错误信息 | | 概率和 counts 不一致 | 概率由 counts 归一化得到,矫正模式可能改变 counts | 对比 `wait()` 和 `wait_raw()` | diff --git a/docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md b/docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md index 5a7b1730..124c005e 100644 --- a/docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/4_qcis_ir_workflow.md @@ -52,7 +52,7 @@ platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) backend = platform.get_backend("tianyan-287") task = backend.run([qcis_text], shots=1000) -results = task.wait(timeout_secs=120.0) +results = task.wait(timeout=120.0) print(results[0].counts) ``` diff --git a/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md b/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md index a356909d..e26cad86 100644 --- a/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md +++ b/docs/documentation/cn/1_cqlib/7_tianyan/5_readout_mitigation.md @@ -63,7 +63,7 @@ task = backend.run_with_mode( ```python task = backend.run(["H Q1\nM Q1"], shots=1000) -results = task.wait(timeout_secs=120.0) +results = task.wait(timeout=120.0) ``` `auto` 模式会在满足条件时自动应用读取误差矫正。 @@ -74,14 +74,14 @@ results = task.wait(timeout_secs=120.0) ```python task = backend.run_raw(["H Q1\nM Q1"], shots=1000) -raw_results = task.wait(timeout_secs=120.0) +raw_results = task.wait(timeout=120.0) ``` 或者在已有任务上使用 `wait_raw`: ```python task = backend.run(["H Q1\nM Q1"], shots=1000) -raw_results = task.wait_raw(timeout_secs=120.0) +raw_results = task.wait_raw(timeout=120.0) ``` ## 5. 显式指定矫正模式 @@ -135,8 +135,8 @@ qcis = "H Q1\nM Q1" task = backend.run([qcis], shots=1000) -calibrated = task.wait(timeout_secs=120.0) -raw = task.wait_raw(timeout_secs=120.0) +calibrated = task.wait(timeout=120.0) +raw = task.wait_raw(timeout=120.0) print("矫正后:", calibrated[0].counts, calibrated[0].probabilities) print("原始值:", raw[0].counts, raw[0].probabilities) diff --git a/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md b/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md index c19564a4..613466ad 100644 --- a/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md +++ b/docs/documentation/en/1_cqlib/7_tianyan/0_overview.md @@ -80,7 +80,7 @@ backend = platform.get_backend("tianyan-287") qcis = "H Q1\nM Q1" task = backend.run([qcis], shots=1000) -results = task.wait(timeout_secs=120.0, poll_interval_secs=5.0) +results = task.wait(timeout=120.0, poll_interval=5.0) result = results[0] print(result.task_id) @@ -124,8 +124,8 @@ classDiagram +shots +submitted_at +status() - +wait(timeout_secs, poll_interval_secs) - +wait_raw(timeout_secs, poll_interval_secs) + +wait(timeout, poll_interval) + +wait_raw(timeout, poll_interval) } class ExecutionResult { +task_id diff --git a/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md b/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md index c7caa3ae..72a76a47 100644 --- a/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md +++ b/docs/documentation/en/1_cqlib/7_tianyan/3_task_result.md @@ -92,8 +92,8 @@ Suitable for implementing polling logic manually, or for refreshing task status ```python results = task.wait( - timeout_secs=120.0, - poll_interval_secs=5.0, + timeout=120.0, + poll_interval=5.0, ) for result in results: @@ -106,8 +106,8 @@ Parameter description: | Parameter | Description | |---|---| -| `timeout_secs` | Maximum number of seconds to wait | -| `poll_interval_secs` | Polling interval in seconds, default `5.0` | +| `timeout` | Maximum number of seconds to wait | +| `poll_interval` | Polling interval in seconds, default `5.0` | `wait()` releases the Python GIL while blocking, so it does not block other Python threads from running. @@ -117,8 +117,8 @@ By default, `wait()` decides whether to apply readout error correction according ```python raw_results = task.wait_raw( - timeout_secs=120.0, - poll_interval_secs=5.0, + timeout=120.0, + poll_interval=5.0, ) ``` @@ -180,7 +180,7 @@ It is recommended to check the following before submission: | Symptom | Common cause | Handling | |---|---|---| | Submission fails | Backend unavailable, QCIS format error, invalid API Key or network problem | Check `backend.status`, the QCIS text and the authentication status | -| `wait` times out | Long queue wait time or the task has not completed | Increase `timeout_secs`, or use `status()` to query in batches | +| `wait` times out | Long queue wait time or the task has not completed | Increase `timeout`, or use `status()` to query in batches | | Fewer results returned than submitted circuits | `status()` was used, which returns only completed results | Use `wait()` to wait for all of them to complete | | counts is empty | The platform result has not completed or parsing failed | Check the task status and the raw error message | | Probabilities and counts are inconsistent | Probabilities are normalized from counts, and the correction mode may change counts | Compare `wait()` with `wait_raw()` | diff --git a/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md b/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md index 7a9eb06a..af112903 100644 --- a/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md +++ b/docs/documentation/en/1_cqlib/7_tianyan/4_qcis_ir_workflow.md @@ -52,7 +52,7 @@ platform = TianyanPlatform.login(os.environ["TIANYAN_API_KEY"]) backend = platform.get_backend("tianyan-287") task = backend.run([qcis_text], shots=1000) -results = task.wait(timeout_secs=120.0) +results = task.wait(timeout=120.0) print(results[0].counts) ``` diff --git a/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md b/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md index 157308d0..9a03aaff 100644 --- a/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md +++ b/docs/documentation/en/1_cqlib/7_tianyan/5_readout_mitigation.md @@ -63,7 +63,7 @@ task = backend.run_with_mode( ```python task = backend.run(["H Q1\nM Q1"], shots=1000) -results = task.wait(timeout_secs=120.0) +results = task.wait(timeout=120.0) ``` The `auto` mode applies readout error correction automatically when the conditions are met. @@ -74,14 +74,14 @@ To obtain raw counts only without any correction, use `run_raw`: ```python task = backend.run_raw(["H Q1\nM Q1"], shots=1000) -raw_results = task.wait(timeout_secs=120.0) +raw_results = task.wait(timeout=120.0) ``` Or use `wait_raw` on an existing task: ```python task = backend.run(["H Q1\nM Q1"], shots=1000) -raw_results = task.wait_raw(timeout_secs=120.0) +raw_results = task.wait_raw(timeout=120.0) ``` ## 5. Specifying the correction mode explicitly @@ -135,8 +135,8 @@ qcis = "H Q1\nM Q1" task = backend.run([qcis], shots=1000) -calibrated = task.wait(timeout_secs=120.0) -raw = task.wait_raw(timeout_secs=120.0) +calibrated = task.wait(timeout=120.0) +raw = task.wait_raw(timeout=120.0) print("corrected:", calibrated[0].counts, calibrated[0].probabilities) print("raw:", raw[0].counts, raw[0].probabilities) From f1c2491038f24e50653b725a8a5ed0a3b60706c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Tue, 29 Sep 2026 15:59:13 +0800 Subject: [PATCH 5/6] fix(cqlib): remove unsupported crates.io category --- crates/cqlib/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cqlib/Cargo.toml b/crates/cqlib/Cargo.toml index d1c96364..7eb80358 100644 --- a/crates/cqlib/Cargo.toml +++ b/crates/cqlib/Cargo.toml @@ -21,7 +21,6 @@ categories = [ "science", "simulation", "algorithms", - "science::physics", "data-structures" ] From a21ee769d857358f9243ed0b61af7468cf2f7c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=81=A5=E5=81=A5?= Date: Tue, 29 Sep 2026 16:07:32 +0800 Subject: [PATCH 6/6] chore(release): bump versions to 0.1.0-beta.2 / 2.0.0-beta2 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- crates/binding-c/Cargo.toml | 2 +- crates/binding-python/Cargo.toml | 4 ++-- crates/cqlib-core/README.md | 4 ++-- crates/cqlib/Cargo.toml | 2 +- docs/releasing.md | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee324102..a6eac5bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,7 +165,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "binding-c" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" dependencies = [ "cbindgen", "cqlib-core", @@ -177,7 +177,7 @@ dependencies = [ [[package]] name = "binding-python" -version = "2.0.0-beta1" +version = "2.0.0-beta2" dependencies = [ "chrono", "cqlib-core", @@ -464,14 +464,14 @@ dependencies = [ [[package]] name = "cqlib" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" dependencies = [ "cqlib-core", ] [[package]] name = "cqlib-core" -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" dependencies = [ "approx", "bitvec", diff --git a/Cargo.toml b/Cargo.toml index 84695230..de2a69a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.0-beta.1" +version = "0.1.0-beta.2" edition = "2024" rust-version = "1.89" license = "Apache-2.0" diff --git a/crates/binding-c/Cargo.toml b/crates/binding-c/Cargo.toml index 7c61cde5..d7b538f2 100644 --- a/crates/binding-c/Cargo.toml +++ b/crates/binding-c/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true crate-type = ["rlib", "staticlib", "cdylib"] [dependencies] -cqlib-core = { path = "../cqlib-core", version = "0.1.0-beta.1" } +cqlib-core = { path = "../cqlib-core", version = "0.1.0-beta.2" } libc = "0.2" ndarray.workspace = true num-complex.workspace = true diff --git a/crates/binding-python/Cargo.toml b/crates/binding-python/Cargo.toml index 0e8c4838..2ac30fc5 100644 --- a/crates/binding-python/Cargo.toml +++ b/crates/binding-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "binding-python" -version = "2.0.0-beta1" +version = "2.0.0-beta2" edition.workspace = true rust-version.workspace = true license-file.workspace = true @@ -14,7 +14,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] pyo3 = { version = "0.29", features = ["abi3-py310", "num-complex", "chrono"] } chrono = "0.4" -cqlib-core = { path = "../cqlib-core", version = '0.1.0-beta.1' } +cqlib-core = { path = "../cqlib-core", version = '0.1.0-beta.2' } num-complex.workspace = true numpy.workspace = true rustworkx-core.workspace = true diff --git a/crates/cqlib-core/README.md b/crates/cqlib-core/README.md index 913e85e3..f6bf3f7c 100644 --- a/crates/cqlib-core/README.md +++ b/crates/cqlib-core/README.md @@ -14,13 +14,13 @@ Add the crate to your project: ```toml [dependencies] -cqlib-core = "0.1.0-beta.1" +cqlib-core = "0.1.0-beta.2" ``` Or use Cargo: ```shell -cargo add cqlib-core@0.1.0-beta.1 +cargo add cqlib-core@0.1.0-beta.2 ``` ## Quick start diff --git a/crates/cqlib/Cargo.toml b/crates/cqlib/Cargo.toml index 7eb80358..735273f3 100644 --- a/crates/cqlib/Cargo.toml +++ b/crates/cqlib/Cargo.toml @@ -26,4 +26,4 @@ categories = [ [dependencies] -cqlib-core = { path = "../cqlib-core", version = "0.1.0-beta.1" } +cqlib-core = { path = "../cqlib-core", version = "0.1.0-beta.2" } diff --git a/docs/releasing.md b/docs/releasing.md index bf40e612..8cbcd033 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -40,9 +40,9 @@ Tags use the Python version to identify a release batch: - Stable releases: `vX.Y.Z`. - Prereleases: `vX.Y.Z-alpha.N`, `vX.Y.Z-beta.N`, or `vX.Y.Z-rc.N`, with N starting at 1. -- Current tag: `v1.4.0-beta.1`. -- Current Python source version: `1.4.0-beta1`, normalized to `1.4.0b1` for PEP 440 and wheel filenames. -- Current Rust/C version: `0.1.0-beta.1`; both inherit the workspace version. +- Current tag: `v2.0.0-beta.2`. +- Current Python source version: `2.0.0-beta2`, normalized to `2.0.0b2` for PEP 440 and wheel filenames. +- Current Rust/C version: `0.1.0-beta.2`; both inherit the workspace version. Python and Rust/C versions evolve independently and do not need matching numbers. The Cargo.toml files at a given tag record the version mapping for that