Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/binding-c/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 18 additions & 10 deletions crates/binding-c/src/error_mitigation/virtual_distillation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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),
}
}

Expand Down
17 changes: 12 additions & 5 deletions crates/binding-c/tests/error_mitigation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,20 +493,26 @@ 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,
_shots: usize,
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 {
Expand Down Expand Up @@ -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!(
Expand Down
5 changes: 3 additions & 2 deletions crates/binding-c/tests/parity_compile_em_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion crates/binding-c/tests/test_parity_compile_em.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions crates/binding-python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -55,22 +63,41 @@ 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,
hamiltonian: Hamiltonian,
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,
Expand All @@ -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: ...
Expand Down
45 changes: 33 additions & 12 deletions crates/binding-python/tests/error_mitigation/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]] = []

Expand All @@ -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)
Expand All @@ -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),
]


Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion crates/cqlib-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ categories = [
"science",
"simulation",
"algorithms",
"science::physics",
"data-structures"
]
include = [
Expand Down
Loading
Loading