Source code for qarp.algorithms._primitives.mirror_test
"""MirrorTest primitive.
The mirror test estimates ``|⟨bra|U|ket⟩|²`` (or ``|⟨bra|ket⟩|²`` when
``operator=None``) by appending the mirror sequence ``ket · U · bra†`` to
``|0…0⟩`` and reading the probability of the all-zeros outcome.
Pattern: subclasses :class:`PrimitiveAlgorithm` (the qarpx-backed base).
``build()`` populates ``sub_blocks`` with one composite qarpx block and a
final all-qubit ``MeasureBlock``. ``run(results)`` reads the engine's
:class:`qx.SamplingResult` and returns ``P(all zeros)``.
"""
from typing import Optional, Self, Union
import qarpx as qx
from ..._types import Shots
from ...blocks import AnyBlock, SimpleBlock
from ...blocks._block import CompositeBlockBase
from .primitive_algorithm import PrimitiveAlgorithm
from .target import Target
[docs]
class MirrorTest(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: State preparation block for ``⟨ψ|``.
operator: Optional unitary ``U`` to apply between ``ket`` and ``bra†``.
ket: State preparation block for ``|φ⟩``.
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
self.n_qubits: Optional[int] = 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()
# Determine the working register size from the inputs.
self.ket.build()
self.bra.build()
if self.operator is not None:
self.operator.build()
self.n_qubits = max(
self.ket.n_qubits,
self.bra.n_qubits,
self.operator.n_qubits if self.operator is not None else 0,
)
# Compose: ket · (operator)? · bra† · measure-all.
full_qubits = list(range(self.n_qubits))
composite = CompositeBlockBase(n_qubits=self.n_qubits, name="MirrorTest")
ket_built = self.ket.build()
ket_built.target_qubits = full_qubits
composite.add_child(ket_built)
if self.operator is not None:
op_built = self.operator.build()
op_built.target_qubits = full_qubits
composite.add_child(op_built)
# Lazy Python-wrapper dagger (deepcopy + flag): `add_child` folds the
# pending flag into concrete daggered commands via
# `_materialise_pending_ops`, keeping the child deepcopy-safe.
bra_dag = self.bra.build().dagger()
bra_dag.target_qubits = full_qubits
composite.add_child(bra_dag)
# Measure every qubit into the corresponding cbit so the engine emits
# one outcome integer per shot.
measure_layer = SimpleBlock(self.n_qubits, name="measure")
measure_layer.measure([(q, q) for q in range(self.n_qubits)])
measure_layer.build()
measure_layer.target_qubits = full_qubits
composite.add_child(measure_layer)
composite.build()
self.sub_blocks = [composite]
return self
[docs]
def run(self, results: list) -> float:
"""``P(all zeros)`` = the squared overlap estimate.
The single-circuit engine pipeline returns one
:class:`qx.SamplingResult` per ``sub_blocks`` entry; we read its
``counts`` and divide by ``n_shots``. All-zeros corresponds to
outcome integer ``0`` regardless of register size.
"""
sr = results[0]
n_shots = sr.n_shots
zero_count = sr.counts.get(0, 0)
self.result = zero_count / n_shots
return self.result
def __repr__(self) -> str:
return f"MirrorTest(target={self.target}, n_shots={self.n_shots})"