Source code for qarp.algorithms._primitives.interferometric_test
"""InterferometricTest primitive.
A composition wrapper that estimates ``Re⟨ψ|U|ψ⟩`` and ``Im⟨ψ|U|ψ⟩`` by
running the configured ``sampling_algorithm`` (MirrorTest or SWAPTest) on
three different state preparations: the bare ``ket``, a GHZ-like
"real" combination, and a dephased GHZ-like "imaginary" combination.
"""
from typing import Optional, Self, Union
import qarpx as qx
from ..._types import Shots
from ...blocks import AnyBlock, ComputationalBasisStateBlock, GHZLikeStateBlock
from .mirror_test import MirrorTest
from .primitive_algorithm import PrimitiveAlgorithm
from .swap_test import SWAPTest
from .target import Target
[docs]
class InterferometricTest(PrimitiveAlgorithm):
gradient_kind = "expectation" # every circuit's statistic is bilinear in its state
supported_targets = frozenset({Target.EXPECTATION_VALUE})
def __init__(
self,
bra: Optional[ComputationalBasisStateBlock] = None,
operator: Optional[AnyBlock] = None,
ket: Optional[ComputationalBasisStateBlock] = None,
real: bool = True,
imaginary: bool = True,
sampling_algorithm: Optional[PrimitiveAlgorithm] = None,
n_shots: Optional[Union[int, Shots]] = None,
):
"""
Args:
bra: Optional bra state preparation block.
operator: The unitary operator U to test.
ket: State preparation block for |ψ⟩ (must be a
``ComputationalBasisStateBlock`` and non-zero).
real: If True, include the real-part sub-circuit.
imaginary: If True, include the imaginary-part sub-circuit.
sampling_algorithm: ``MirrorTest()`` or ``SWAPTest()`` (default: ``MirrorTest()``).
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")
if sampling_algorithm is None:
sampling_algorithm = MirrorTest()
if not isinstance(sampling_algorithm, (MirrorTest, SWAPTest)):
raise ValueError("sampling_algorithm must be MirrorTest() or SWAPTest()")
super().__init__(
ket=ket, bra=bra, operator=operator, n_shots=n_shots, target=Target.EXPECTATION_VALUE
)
self.real = real
self.imaginary = imaginary
self.sampling_algorithm = sampling_algorithm
self.result_real: Optional[float] = None
self.result_imaginary: Optional[float] = None
self.result: Optional[complex] = None
self._validate_inputs()
if not isinstance(ket, ComputationalBasisStateBlock):
raise TypeError("ket must be a ComputationalBasisStateBlock instance")
ket.build()
self.n_qubits = ket.n_qubits
if ket.basis_state == [0] * self.n_qubits:
raise ValueError(
"ket must not be the all-zero state (orthogonality with the reference would fail)"
)
self.reference = ComputationalBasisStateBlock([0] * self.n_qubits)
def _validate_inputs(self) -> None:
if not isinstance(self.ket, ComputationalBasisStateBlock):
raise TypeError("ket must be a ComputationalBasisStateBlock instance")
if self.bra is not None and not isinstance(self.bra, ComputationalBasisStateBlock):
raise TypeError("bra must be a ComputationalBasisStateBlock 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")
def _configure(self, sa: PrimitiveAlgorithm, *, bra: AnyBlock, ket: AnyBlock) -> AnyBlock:
"""Set up ``sa`` (MirrorTest or SWAPTest) for one sub-block build."""
sa.bra = bra
sa.operator = self.operator
sa.ket = ket
sa.n_shots = self.n_shots
sa.build()
return sa.sub_blocks[0]
[docs]
def build(self) -> Self:
self._validate_inputs()
self.sub_blocks = []
# 1. Reference: bra = ket = ket (gives ⟨ψ|U|ψ⟩ probability).
self.sub_blocks.append(self._configure(self.sampling_algorithm, bra=self.ket, ket=self.ket))
ghzlike_real = None
if self.real:
ghzlike_real = GHZLikeStateBlock(self.ket.basis_state)
self.sub_blocks.append(
self._configure(self.sampling_algorithm, bra=ghzlike_real, ket=ghzlike_real)
)
if self.imaginary:
if ghzlike_real is None:
ghzlike_real = GHZLikeStateBlock(self.ket.basis_state)
ghzlike_imag = GHZLikeStateBlock(self.ket.basis_state, dephase=True)
self.sub_blocks.append(
self._configure(self.sampling_algorithm, bra=ghzlike_imag, ket=ghzlike_real)
)
return self
[docs]
def run(self, results: list) -> complex:
idx = 0
# Reference probability via the sampling_algorithm's own post-processor.
p_ref = self.sampling_algorithm.run([results[idx]])
idx += 1
real_part = 0.0
imaginary_part = 0.0
if self.real:
p_real = self.sampling_algorithm.run([results[idx]])
real_part = 2 * p_real - 0.5 * (1 + p_ref)
idx += 1
if self.imaginary:
p_imag = self.sampling_algorithm.run([results[idx]])
imaginary_part = -2 * p_imag + 0.5 * (1 + p_ref)
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"InterferometricTest(target={self.target}, real={self.real}, "
f"imaginary={self.imaginary}, "
f"sampling_algorithm={type(self.sampling_algorithm).__name__}, "
f"n_shots={self.n_shots})"
)