Source code for qarp.algorithms._primitives.swap_test

"""SWAPTest primitive.

Wraps :class:`qarp.blocks.SWAPTestBlock` (the qarpx composite block
that implements the SWAP test) and post-processes the engine's
:class:`qx.SamplingResult` to produce the ``|⟨bra|ket⟩|²`` estimate.

If an ``operator`` is provided, the ket register is replaced with
``ket · operator`` (so the test estimates ``|⟨bra|U|ket⟩|²``).
"""

from typing import Optional, Self, Union

import qarpx as qx

from ..._types import Shots
from ...blocks import AnyBlock
from ...blocks._block import CompositeBlockBase
from ...blocks._primitives import SWAPTestBlock
from .primitive_algorithm import PrimitiveAlgorithm
from .target import Target


[docs] class SWAPTest(PrimitiveAlgorithm): gradient_kind = "expectation" # every circuit's statistic is bilinear in its state returns_probability = True # run() is |⟨bra|ket⟩|², not the amplitude supported_targets = frozenset({Target.OVERLAP}) def __init__( self, bra: Optional[AnyBlock] = None, operator: Optional[AnyBlock] = None, ket: Optional[AnyBlock] = None, n_shots: Optional[Union[int, Shots]] = None, ): """ Args: bra: Block preparing the bra state ``|ψ⟩``. operator: Optional unitary ``U`` applied to the ket register. ket: Block preparing the ket state ``|φ⟩``. n_shots: Number of measurement shots; ``None`` defers to the engine default. """ super().__init__( ket=ket, bra=bra, operator=operator, n_shots=n_shots, target=Target.OVERLAP ) self.result: Optional[float] = None def _validate_inputs(self) -> None: if not isinstance(self.bra, qx.Block): raise TypeError("bra must be a Block instance") if not isinstance(self.ket, qx.Block): raise TypeError("ket must be a Block instance") if self.operator is not None and not isinstance(self.operator, qx.Block): raise TypeError("operator must be a Block instance or None")
[docs] def build(self) -> Self: self._validate_inputs() # If operator is provided, the ket register effectively prepares # ``ket`` then applies ``operator`` — wrap them in a composite. if self.operator is not None: n_state = self.ket.n_qubits ket_built = self.ket.build() ket_built.target_qubits = list(range(n_state)) op_built = self.operator.build() op_built.target_qubits = list(range(n_state)) ket_register = CompositeBlockBase(n_qubits=n_state, name="ket+operator") ket_register.add_child(ket_built) ket_register.add_child(op_built) ket_register.build() else: ket_register = self.ket.build() block = SWAPTestBlock(bra=self.bra, ket=ket_register, measure=True) block.build() self.sub_blocks = [block] return self
[docs] def run(self, results: list) -> float: """``2·P(ancilla=0) − 1 = |⟨bra|ket⟩|²``. ``SamplingResult.counts`` is keyed on the full all-qubit outcome integer; the ancilla is qubit 0 so its measured value is bit 0. """ sr = results[0] n_shots = sr.n_shots zero_count = sum(c for outcome, c in sr.counts.items() if (outcome & 1) == 0) p0 = zero_count / n_shots self.result = 2 * p0 - 1 return self.result
def __repr__(self) -> str: return f"SWAPTest(target={self.target}, n_shots={self.n_shots})"