Source code for qarp.blocks._primitives.interferometric_state_block

"""Blocks for preparing and measuring an interferometric transition state.

``InterferometricStateBlock`` prepares the measurement-free state

    (|0> |phi> + |1> |psi>) / sqrt(2),

with the requested ancilla quadrature encoded in the transition circuit.  The
separate ``InterferometricMeasurementBlock`` adds local basis rotations and
terminal measurements for a qubit-wise commuting Pauli group.  Keeping these
two concerns separate makes the state reusable for other primitives (for
example, custom observables, amplitude estimation, or tomography) while
retaining the grouped transition-measurement convenience wrapper.
"""

from typing import Dict, Optional

import qarpx as qx

from .._block import AnyBlock, CompositeBlockBase, SimpleBlock
from .hadamard_test_block import HadamardTestBlock
from .identity_block import IdentityBlock


def _validate_states(bra: AnyBlock, ket: AnyBlock) -> None:
    """Validate the state-preparation blocks shared by both public classes."""
    if not isinstance(bra, qx.Block):
        raise TypeError("bra must be a Block instance")
    if not isinstance(ket, qx.Block):
        raise TypeError("ket must be a Block instance")
    if bra.n_qubits != ket.n_qubits:
        raise ValueError(
            f"bra ({bra.n_qubits} qubits) and ket ({ket.n_qubits} qubits) "
            "must act on the same number of qubits"
        )


[docs] class InterferometricStateBlock(CompositeBlockBase): """Prepare a measurement-free interferometric transition state. Qubit 0 is the interferometric ancilla and qubits ``1..n`` are the state register. The branch synthesis is delegated to :class:`HadamardTestBlock`, including its branch-sharing optimization for similar state-preparation circuits. No basis rotations or measurements are appended, so the block can be embedded in a larger circuit or measured by a caller-specific primitive. """ def __init__( self, bra: AnyBlock, ket: AnyBlock, estimate_imaginary: bool = False, name: str = "InterferometricState", ): _validate_states(bra, ket) super().__init__(n_qubits=1 + ket.n_qubits, name=name) self.bra = bra self.ket = ket self.estimate_imaginary = estimate_imaginary
[docs] def build_vanilla(self) -> None: full_qubits = list(range(self.n_qubits)) # Leave all qubits, including the ancilla, unmeasured. The selected # quadrature is prepared by HadamardTestBlock itself. transition = HadamardTestBlock( state=IdentityBlock(self.ket.n_qubits), unitary=self.ket, unitary_dagger=self.bra, estimate_imaginary=self.estimate_imaginary, measure=False, name="TransitionInterferometer", ) transition.target_qubits = full_qubits self.add_wired_child(transition)
def __repr__(self) -> str: part = "imaginary" if self.estimate_imaginary else "real" return f"InterferometricStateBlock(estimate_{part}=True)"
[docs] class InterferometricMeasurementBlock(CompositeBlockBase): """Measure an interferometric transition state in a QWC Pauli basis. This wrapper first prepares :class:`InterferometricStateBlock`, then rotates state-register qubits into the requested local ``X``, ``Y`` or ``Z`` basis and measures the ancilla and all data qubits. ``basis`` maps state-register qubits (indexed from zero, excluding the ancilla) to Pauli letters; omitted qubits remain in the computational Z basis. """ def __init__( self, bra: AnyBlock, ket: AnyBlock, basis: Optional[Dict[int, str]] = None, estimate_imaginary: bool = False, name: str = "InterferometricMeasurement", ): _validate_states(bra, ket) n_state = ket.n_qubits normalized_basis = dict(basis or {}) for qubit, pauli in normalized_basis.items(): if not isinstance(qubit, int) or qubit < 0 or qubit >= n_state: raise ValueError(f"basis qubit {qubit!r} is outside the state register") if pauli not in {"X", "Y", "Z"}: raise ValueError(f"invalid QWC basis {pauli!r}; expected 'X', 'Y' or 'Z'") super().__init__(n_qubits=1 + n_state, name=name) self.bra = bra self.ket = ket self.basis = normalized_basis self.estimate_imaginary = estimate_imaginary
[docs] def build_vanilla(self) -> None: full_qubits = list(range(self.n_qubits)) state = InterferometricStateBlock( bra=self.bra, ket=self.ket, estimate_imaginary=self.estimate_imaginary, ) state.target_qubits = full_qubits self.add_wired_child(state) # QWC groups need only local basis changes: X -> H and # Y -> Sdg followed by H. The state-register offset is one because # the ancilla occupies qubit 0. basis_change = SimpleBlock(self.n_qubits, name="QWCBasisChange") for qubit, pauli in sorted(self.basis.items()): state_qubit = qubit + 1 if pauli == "X": basis_change.h(state_qubit) elif pauli == "Y": basis_change.sdg(state_qubit) basis_change.h(state_qubit) basis_change.target_qubits = full_qubits self.add_wired_child(basis_change) # Measure ancilla into cbit 0 and data qubit q+1 into cbit q+1. Thus # the engine's integer outcome has the layout expected by the grouped # transition post-processor: ancilla is bit 0 and data is outcome >> 1. measure = SimpleBlock(self.n_qubits, name="TransitionMeasure") measure.measure([(qubit, qubit) for qubit in full_qubits]) measure.target_qubits = full_qubits self.add_wired_child(measure) self.n_cbits = self.n_qubits
def __repr__(self) -> str: part = "imaginary" if self.estimate_imaginary else "real" return f"InterferometricMeasurementBlock(estimate_{part}=True, basis={self.basis!r})"