Source code for uniqc.algorithms.workflows.classical_shadow_workflow
"""High-level classical-shadow workflow.
Wraps :func:`uniqc.algorithms.core.measurement.classical_shadow.classical_shadow`
and :func:`shadow_expectation` so that the user can request multiple Pauli
expectations from a single shadow dataset in one call.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from uniqc.algorithms.core.measurement.classical_shadow import (
ShadowSnapshot,
classical_shadow,
shadow_expectation,
)
from uniqc.circuit_builder import Circuit
__all__ = ["ShadowWorkflowResult", "run_classical_shadow_workflow"]
[docs]
@dataclass
class ShadowWorkflowResult:
"""Outcome of :func:`run_classical_shadow_workflow`.
Attributes:
snapshots: List of :class:`ShadowSnapshot` produced by the shadow run.
expectations: Mapping ``pauli_string -> estimated <P>``.
n_snapshots: Number of shadow snapshots collected (= ``shots``).
"""
snapshots: list[ShadowSnapshot]
expectations: dict[str, float] = field(default_factory=dict)
n_snapshots: int = 0
[docs]
def run_classical_shadow_workflow(
circuit: Circuit,
pauli_observables: Sequence[str],
*,
shots: int = 1000,
n_shadow: int | None = None,
qubits: list[int] | None = None,
) -> ShadowWorkflowResult:
"""Collect a classical-shadow dataset and estimate multiple observables.
Args:
circuit: Quantum circuit. Must contain measurements on every qubit
that participates in any of ``pauli_observables``.
pauli_observables: Pauli strings to estimate. All must have the same
length and that length must equal the circuit's qubit count.
shots: Shots per snapshot forwarded to ``classical_shadow``.
n_shadow: Optional number of distinct snapshots forwarded to
``classical_shadow``. ``None`` defaults to that function's
built-in default.
qubits: Optional qubit subset forwarded to ``classical_shadow``.
Returns:
:class:`ShadowWorkflowResult` with both the raw snapshots and the
per-observable estimates.
Example:
>>> from uniqc.circuit_builder import Circuit
>>> from uniqc.algorithms.workflows import classical_shadow_workflow as csw
>>> c = Circuit(); c.h(0); c.cx(0, 1); c.measure(0); c.measure(1)
>>> r = csw.run_classical_shadow_workflow(
... c, ["ZZ", "XX"], shots=500
... ) # doctest: +SKIP
>>> abs(r.expectations["ZZ"] - 1.0) < 0.5 # doctest: +SKIP
True
"""
if not pauli_observables:
raise ValueError("pauli_observables must contain at least one Pauli string")
lengths = {len(p) for p in pauli_observables}
if len(lengths) != 1:
raise ValueError(f"All Pauli observables must have the same length, got: {sorted(lengths)}")
kwargs: dict = {"shots": shots}
if n_shadow is not None:
kwargs["n_shadow"] = n_shadow
if qubits is not None:
kwargs["qubits"] = qubits
snapshots = classical_shadow(circuit, **kwargs)
expectations: dict[str, float] = {}
for pauli in pauli_observables:
expectations[pauli] = float(shadow_expectation(snapshots, pauli))
return ShadowWorkflowResult(
snapshots=snapshots,
expectations=expectations,
n_snapshots=len(snapshots),
)
[docs]
def run_classical_shadow_workflow_example() -> ShadowWorkflowResult:
"""Bell-state shadow estimation for ZZ and XX observables."""
c = Circuit()
c.h(0)
c.cx(0, 1)
c.measure(0)
c.measure(1)
return run_classical_shadow_workflow(c, ["ZZ", "XX"], shots=500)