Source code for qarp.algorithms._primitives.termwise_hadamard_test

"""TermwiseHadamardTest primitive.

Composition wrapper: runs one :class:`HadamardTest` per operator term,
optionally weighted by per-term coefficients.  When ``operator`` is a
``QubitOperator``, it is expanded into per-term ``PauliBlock`` objects via
:class:`PauliBlockFactory` and the per-term coefficients are used as weights.
"""

from typing import List, Optional, Union

import qarpx as qx
from qarp.operators import QubitOperator

from ..._types import Shots
from ...blocks import AnyBlock
from ...factories._pauli_block_factory import PauliBlockFactory
from .hadamard_test import HadamardTest
from .primitive_algorithm import PrimitiveAlgorithm
from .target import Target


[docs] class TermwiseHadamardTest(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[Union[AnyBlock, List[AnyBlock], QubitOperator]] = None, ket: Optional[AnyBlock] = None, coefficients: Optional[List[float]] = None, real: bool = True, imaginary: bool = True, n_shots: Optional[Union[int, Shots]] = None, ): """ Args: bra: Optional state preparation block for ⟨ψ|. operator: Block, list of Blocks, or QubitOperator (expanded into per-term Blocks). ``None`` with an explicit ``bra`` (not ``ket``) measures the bare overlap ``<bra|ket>`` (``Target.OVERLAP``). ket: State preparation block for ``|ψ⟩``. coefficients: Per-term weights for the list path; they are the sole source of weights there, so pass the term magnitudes explicitly. A block's own ``.coefficient`` is not consulted in the list path (omitting ``coefficients`` weights every term by ``1.0``). Rejected if ``operator`` is a QubitOperator, whose per-term magnitudes are used automatically. real: If True, include real-part sub-circuits. imaginary: If True, include imaginary-part sub-circuits. n_shots: Number of 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=None, n_shots=n_shots, target=Target.SAMPLING) # Normalize operator to a list (or QubitOperator deferred to build). if isinstance(operator, qx.Block): self.operator: Union[List[AnyBlock], QubitOperator] = [operator] else: self.operator = operator # type: ignore[assignment] self.bra = bra self.ket = ket self.coefficients = coefficients self.real = real self.imaginary = imaginary self.sub_algorithms: List[HadamardTest] = [] self.n_qubits: Optional[int] = None self.result_sum: Optional[complex] = None self.result_list: List[complex] = [] def _validate_inputs(self) -> None: if self.operator is None: if self.coefficients is not None: raise ValueError( "coefficients were given but no operator; pass the operator they weight" ) # Only an explicit bra != ket may go without an operator (an overlap); # a forgotten operator must not become the expectation of the identity. if self._bra_is_default or self.bra is self.ket: raise ValueError("operator must be provided") return if not isinstance(self.operator, (list, QubitOperator)): raise TypeError("operator must be Block, list of Blocks, or QubitOperator") if isinstance(self.operator, list): if not self.operator: raise ValueError("operator list cannot be empty") for i, op in enumerate(self.operator): if not isinstance(op, qx.Block): raise TypeError(f"operator[{i}] must be a Block instance") if isinstance(self.operator, QubitOperator) and self.coefficients is not None: raise ValueError("coefficients should not be provided when operator is a QubitOperator") if ( isinstance(self.operator, list) and self.coefficients is not None and len(self.coefficients) != len(self.operator) ): raise ValueError("coefficients must match the length of operator list")
[docs] def build(self) -> "TermwiseHadamardTest": self._validate_inputs() # The constructor stores a SAMPLING placeholder (the operator may still # be an unexpanded QubitOperator there); the real target — a weighted # EV or transition-amplitude sum — is only known here. target = self.infer_target() self.n_qubits = self.ket.n_qubits # Expand QubitOperator → per-term PauliBlocks (with coefficients). if isinstance(self.operator, QubitOperator): pauli_blocks = PauliBlockFactory.from_qubit_operator( self.operator, include_coefficients=True, n_qubits=self.n_qubits ) self.operator = list(pauli_blocks) # The blocks carry arg(coefficient) as a phase in their circuits, so # the classical weight is the magnitude only; weighting by the full # complex coefficient would apply the phase twice. self.coefficients = [abs(op.coefficient) for op in self.operator] # An overlap has no operator terms: one operator-less child, which # HadamardTest builds as <bra|ket> directly (run() weights it by 1.0). terms: list = [None] if target is Target.OVERLAP else self.operator self.sub_algorithms = [] self.sub_blocks = [] for op in terms: child = HadamardTest( bra=self.bra, operator=op, ket=self.ket, real=self.real, imaginary=self.imaginary, n_shots=self.n_shots, ) child.build() self.sub_algorithms.append(child) self.sub_blocks.extend(child.sub_blocks) return self
[docs] def run(self, results: list) -> complex: if not self.sub_algorithms: raise ValueError("TermwiseHadamardTest must be built before running") if self.coefficients is None: self.coefficients = [1.0] * len(self.sub_algorithms) per_term: List[complex] = [] circuit_idx = 0 for child in self.sub_algorithms: n_per_child = len(child.sub_blocks) child_results = results[circuit_idx : circuit_idx + n_per_child] circuit_idx += n_per_child per_term.append(child.run(child_results)) weighted = [r * c for r, c in zip(per_term, self.coefficients, strict=True)] self.result_list = weighted self.result_sum = sum(weighted) return self.result_sum
[docs] def get_hadamard_test(self, index: int) -> HadamardTest: if not self.sub_algorithms: raise ValueError("TermwiseHadamardTest must be built first") return self.sub_algorithms[index]
def __len__(self) -> int: return len(self.operator) if isinstance(self.operator, list) else 0 @property def n_operators(self) -> int: return len(self.operator) if isinstance(self.operator, list) else 0 @property def n_sub_algorithms(self) -> int: return len(self.sub_algorithms) @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"TermwiseHadamardTest(n_operators={self.n_operators}, " f"real={self.real}, imaginary={self.imaginary}, n_shots={self.n_shots})" )