Source code for qarp.algorithms._primitives.shadows.pauli

"""``PauliShadow`` — the random-Pauli (local-Clifford) shadow ensemble.

Each setting draws an independent measurement axis per qubit; the per-setting
circuit is ``ket -> (per-qubit basis rotation) -> measure-all``.  The inversion
lives in :class:`~.kernels.PauliKernel`.
"""

from __future__ import annotations

import numpy as np

from ....blocks import AnyBlock, SimpleBlock
from ....blocks._block import CompositeBlockBase
from .base import ShadowProtocol
from .kernels import PauliKernel


[docs] class PauliShadow(ShadowProtocol): """Random-Pauli classical shadows (Huang–Kueng–Preskill, 2020).""" def _sample_settings(self, rng: np.random.Generator, n_settings: int, n_qubits: int): # 0/1/2 = X/Y/Z, one axis per qubit per setting. return rng.integers(0, 3, size=(n_settings, n_qubits), dtype=np.int8) def _kernel(self, n_qubits: int) -> PauliKernel: return PauliKernel(n_qubits) def _basis_change_block(self, setting) -> SimpleBlock: """Per-qubit rotation into the sampled measurement basis (measure Z after): ``X -> H`` ; ``Y -> S† then H`` ; ``Z -> nothing``. Its unitary ``R`` obeys ``R† Z R = P_axis`` on each qubit, so measuring Z in the rotated frame is measuring the axis the :class:`PauliKernel` then inverts. That identity is the seam between circuit and kernel and is pinned exactly in ``test_pauli.py`` (a sign error here is otherwise invisible to sampled ⟨Z⟩/⟨X⟩ checks). """ n = self.n_qubits basis = SimpleBlock(n, name="basis_change") for qubit in range(n): axis = int(setting[qubit]) if axis == 0: # X basis.h(qubit) elif axis == 1: # Y basis.sdg(qubit) basis.h(qubit) # axis == 2 (Z): identity basis.target_qubits = list(range(n)) basis.mark_built() return basis def _setting_block(self, setting) -> AnyBlock: n = self.n_qubits full = list(range(n)) composite = CompositeBlockBase(n_qubits=n, name="PauliShadowSetting") ket_built = self.ket.build() ket_built.target_qubits = full composite.add_child(ket_built) composite.add_child(self._basis_change_block(setting)) measure = SimpleBlock(n, name="measure") measure.measure([(qubit, qubit) for qubit in range(n)]) measure.target_qubits = full composite.add_wired_child(measure) composite.build() return composite