Source code for qarp.algorithms._primitives.hadamard_test
"""HadamardTest primitive.
Wraps :class:`qarp.blocks.HadamardTestBlock` (the qarpx composite)
and post-processes the engine's :class:`qx.SamplingResult` to extract
``Re⟨ψ|U|ψ⟩`` and/or ``Im⟨ψ|U|ψ⟩``.
Submits one or two ``sub_blocks`` (real / imaginary) and combines their
ancilla statistics in :meth:`run`.
"""
from typing import Optional, Self, Union
import qarpx as qx
from ..._types import Shots
from ...blocks import AnyBlock, IdentityBlock
from ...blocks._block import CompositeBlockBase
from ...blocks._primitives import HadamardTestBlock
from .primitive_algorithm import PrimitiveAlgorithm
from .target import Target
[docs]
class HadamardTest(PrimitiveAlgorithm):
gradient_kind = "expectation" # every circuit's statistic is bilinear in its state
supported_targets = frozenset(
{Target.EXPECTATION_VALUE, Target.OVERLAP, Target.TRANSITION_AMPLITUDE}
)
def __init__(
self,
bra: Optional[AnyBlock] = None,
operator: Optional[AnyBlock] = None,
ket: Optional[AnyBlock] = None,
real: bool = True,
imaginary: bool = True,
n_shots: Optional[Union[int, Shots]] = None,
):
"""
Args:
bra: Optional state preparation block for ⟨ψ|.
operator: The unitary operator U to test.
ket: State preparation block for |ψ⟩.
real: If True, sub_blocks include the real-part circuit.
imaginary: If True, sub_blocks include the imaginary-part circuit.
n_shots: Number of measurement shots; ``None`` defers to the engine default.
"""
if not real and not imaginary:
raise ValueError("At least one of real or imaginary must be True")
super().__init__(
ket=ket, bra=bra, operator=operator, n_shots=n_shots, target=Target.SAMPLING
)
self.real = real
self.imaginary = imaginary
self.result_real: Optional[float] = None
self.result_imaginary: Optional[float] = None
self.result: Optional[complex] = None
def _validate_inputs(self) -> None:
if not isinstance(self.ket, qx.Block):
raise TypeError("ket must be a Block instance")
if self.bra is not None and not isinstance(self.bra, qx.Block):
raise TypeError("bra must be a Block instance or None")
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()
n_qubits = self.ket.n_qubits
target = self.infer_target()
if target == Target.EXPECTATION_VALUE:
state = self.ket
unitary = self.operator
unitary_dagger = None
elif target == Target.OVERLAP:
state = IdentityBlock(n_qubits)
unitary = self.ket
unitary_dagger = self.bra
elif target == Target.TRANSITION_AMPLITUDE:
state = IdentityBlock(n_qubits)
ko = CompositeBlockBase(n_qubits=n_qubits, name="ket+operator")
ket_built = self.ket.build()
ket_built.target_qubits = list(range(n_qubits))
op_built = self.operator.build()
op_built.target_qubits = list(range(n_qubits))
ko.add_child(ket_built)
ko.add_child(op_built)
ko.build()
unitary = ko
unitary_dagger = self.bra
else:
raise RuntimeError(f"Unhandled target {target}")
self.sub_blocks = []
if self.real:
blk_re = HadamardTestBlock(
state=state,
unitary=unitary,
unitary_dagger=unitary_dagger,
estimate_imaginary=False,
measure=True,
)
blk_re.build()
self.sub_blocks.append(blk_re)
if self.imaginary:
blk_im = HadamardTestBlock(
state=state,
unitary=unitary,
unitary_dagger=unitary_dagger,
estimate_imaginary=True,
measure=True,
)
blk_im.build()
self.sub_blocks.append(blk_im)
return self
@staticmethod
def _ancilla_zero_prob(sr) -> float:
"""``P(ancilla=0) = (sum of counts where outcome bit 0 == 0) / n_shots``."""
zero_count = sum(c for outcome, c in sr.counts.items() if (outcome & 1) == 0)
return zero_count / sr.n_shots
[docs]
def run(self, results: list) -> complex:
idx = 0
real_part = 0.0
imaginary_part = 0.0
if self.real:
real_part = 2 * self._ancilla_zero_prob(results[idx]) - 1
idx += 1
if self.imaginary:
imaginary_part = 2 * self._ancilla_zero_prob(results[idx]) - 1
if self.real and self.imaginary:
self.result_real = real_part
self.result_imaginary = imaginary_part
self.result = complex(real_part, imaginary_part)
return self.result
if self.real:
self.result_real = real_part
return real_part
self.result_imaginary = imaginary_part
return imaginary_part
@property
def expectation_type(self) -> str:
if self.real and self.imaginary:
return "complex"
if self.real:
return "real"
return "imaginary"
def __repr__(self) -> str:
return (
f"HadamardTest(target={self.target}, real={self.real}, "
f"imaginary={self.imaginary}, n_shots={self.n_shots})"
)