Source code for qarp.blocks._primitives.hadamard_test_block

from copy import deepcopy
from typing import List, Optional

import qarpx as qx

from .._block import (
    AnyBlock,
    CompositeBlockBase,
    ControlledBlock,
    MeasureBlock,
    SimpleBlock,
)


[docs] class HadamardTestBlock(CompositeBlockBase): """Hadamard test for ``⟨ψ|U|ψ⟩`` using one ancilla qubit. Pattern B (composite). Layout: qubit 0 is the ancilla; qubits ``1 .. state.n_qubits`` hold the state register. The circuit: 1. ``H`` on ancilla. 2. ``state`` preparation on the state register. 3. ``C-U`` on ``(ancilla, state)`` controlled on ``ancilla = |1⟩``. 4. (optional) ``C-U†`` on ``(ancilla, state)`` controlled on ``ancilla = |0⟩``. 5. (optional) ``Sdg`` on ancilla — switches the post-measurement basis so the ancilla expectation gives the imaginary part of ``⟨ψ|U|ψ⟩``. 6. ``H`` on ancilla. 7. (optional) ``Measure`` ancilla into cbit 0. The ``unitary`` (and ``unitary_dagger``, if provided) must be qarpx blocks whose flattened command stream uses gates that ``ControlledBlock`` can lift to single-controlled form (X, Y, Z, H, S, Sdg, T, Tdg, Rx, Ry, Rz, P, U, CX, SWAP, GPhase, Barrier). Other gates would need to be decomposed first. """ def __init__( self, state: AnyBlock, unitary: AnyBlock, unitary_dagger: Optional[AnyBlock] = None, estimate_imaginary: bool = False, measure: bool = False, target_qubits: Optional[List[int]] = None, name: str = "HadamardTest", ): """Build the Hadamard test circuit. Args: state: Block preparing the ket state on the state register. unitary: The unitary operator U to test. Must have the same ``n_qubits`` as ``state``. unitary_dagger: Optional U†. When provided, the test estimates ``⟨ψ|U|ψ⟩ - ⟨ψ|U†|ψ⟩``-style quantities by also conjugating with the negative-control wrapping of U†. estimate_imaginary: If True, applies an Sdg before the final H so the ancilla expectation gives ``Im⟨ψ|U|ψ⟩``. measure: If True, measures the ancilla into cbit 0. target_qubits: standard Block kwarg. """ if not isinstance(state, qx.Block): raise TypeError("state must be a Block instance") if not isinstance(unitary, qx.Block): raise TypeError("unitary must be a Block instance") if unitary_dagger is not None and not isinstance(unitary_dagger, qx.Block): raise TypeError("unitary_dagger must be a Block instance or None") if state.n_qubits != unitary.n_qubits: raise ValueError( f"state ({state.n_qubits} qubits) and unitary ({unitary.n_qubits} qubits) " "must act on the same number of qubits" ) if unitary_dagger is not None and unitary_dagger.n_qubits != unitary.n_qubits: raise ValueError("unitary_dagger and unitary must have the same n_qubits") n_state = state.n_qubits super().__init__( n_qubits=1 + n_state, target_qubits=target_qubits, name=name, ) self.state = state self.unitary = unitary self.unitary_dagger = unitary_dagger self.estimate_imaginary = estimate_imaginary self.measure_at_end = measure # bool flag — distinct from `self.measure(q,c)`
[docs] def build_vanilla(self) -> None: ancilla = 0 state_qubits = list(range(1, self.n_qubits)) # 1. H on ancilla ancilla_h_pre = SimpleBlock(1, name="AncillaH_pre") ancilla_h_pre.h(0) ancilla_h_pre.target_qubits = [ancilla] self.add_wired_child(ancilla_h_pre) # 2. State preparation on state register. Deepcopy first so we don't # mutate the caller's shared ``state`` block (assigning target_qubits # to ``self.state.build()`` would otherwise rewrite its qubit frame # for any downstream caller that reused ``state`` directly). state_built = deepcopy(self.state.build()) state_built.target_qubits = state_qubits self.add_child(state_built) # 3/4. Controlled branches. For a transition/overlap test the two # branch unitaries are usually instances of the same ansatz (with # different parameters). Controlling both complete circuits makes # every shared entangling gate a Toffoli-like operation. Synthesize # the multiplexor command-by-command instead: gates that are exactly # common to both branches are applied once, unconditionally, while # only the differing runs are controlled. The resulting unitary is # unchanged because the branch projectors are orthogonal. if self.unitary_dagger is None: unitary_built = deepcopy(self.unitary.build()) c_u = ControlledBlock(unitary_built, num_controls=1, ctrl_state=[True], name="C-U") c_u.build() c_u.target_qubits = [ancilla] + state_qubits self.add_child(c_u) else: self._append_controlled_multiplexor( false_branch=self.unitary_dagger, true_branch=self.unitary, ancilla=ancilla, state_qubits=state_qubits, ) # 5. Optional Sdg for imaginary-part estimation if self.estimate_imaginary: sdg = SimpleBlock(1, name="AncillaSdg") sdg.sdg(0) sdg.target_qubits = [ancilla] self.add_wired_child(sdg) # 6. Final H on ancilla ancilla_h_post = SimpleBlock(1, name="AncillaH_post") ancilla_h_post.h(0) ancilla_h_post.target_qubits = [ancilla] self.add_wired_child(ancilla_h_post) # 7. Optional measurement of ancilla into cbit 0 if self.measure_at_end: self.add_child(MeasureBlock(ancilla, 0))
@staticmethod def _flat_commands(block: AnyBlock) -> List[qx.Command]: """Return a private, local-frame copy of a block's commands.""" built = deepcopy(block.build()) built.target_qubits = list(range(block.n_qubits)) return list(built.flatten()) @staticmethod def _commands_match(left, right) -> bool: """Return true only for exactly identical branch commands.""" if left is None or right is None: return False # Do not use Command.approx_equal here: sharing a nearly equal # rotation would change the transition amplitude rather than merely # change its compiled representation. try: left_cbits = list(getattr(left, "cbits", [])) right_cbits = list(getattr(right, "cbits", [])) except TypeError: return False if left_cbits != right_cbits: return False try: params_match = list(left.params) == list(right.params) except (TypeError, RuntimeError): params_match = False if not params_match: return False if left.gate != right.gate or list(left.qubits) != list(right.qubits): return False return True @staticmethod def _command_block(command, n_qubits: int, name: str) -> SimpleBlock: """Materialise one or more already-built commands in a leaf block.""" block = SimpleBlock(n_qubits, name=name) block.set_commands(command if isinstance(command, list) else [command]) block.build() return block def _append_controlled_multiplexor( self, false_branch: AnyBlock, true_branch: AnyBlock, ancilla: int, state_qubits: List[int], ) -> None: """Append ``|0><0|⊗false + |1><1|⊗true`` efficiently.""" false_commands = self._flat_commands(false_branch) true_commands = self._flat_commands(true_branch) n_state = len(state_qubits) n_steps = max(len(false_commands), len(true_commands)) false_run: List[qx.Command] = [] true_run: List[qx.Command] = [] common_run: List[qx.Command] = [] def flush_common_run() -> None: if common_run: common = self._command_block( common_run, n_state, name="Shared-branch-run", ) common.target_qubits = state_qubits self.add_wired_child(common) common_run.clear() def flush_controlled_runs() -> None: if false_run: false_block = self._command_block(false_run, n_state, name="C-Udg-run") controlled_false = ControlledBlock( false_block, num_controls=1, ctrl_state=[False], name="C-Udg" ) controlled_false.build() controlled_false.target_qubits = [ancilla] + state_qubits self.add_child(controlled_false) false_run.clear() if true_run: true_block = self._command_block(true_run, n_state, name="C-U-run") controlled_true = ControlledBlock( true_block, num_controls=1, ctrl_state=[True], name="C-U" ) controlled_true.build() controlled_true.target_qubits = [ancilla] + state_qubits self.add_child(controlled_true) true_run.clear() for index in range(n_steps): false_command = false_commands[index] if index < len(false_commands) else None true_command = true_commands[index] if index < len(true_commands) else None if self._commands_match(false_command, true_command): flush_controlled_runs() common_run.append(false_command) else: flush_common_run() if false_command is not None: false_run.append(false_command) if true_command is not None: true_run.append(true_command) flush_common_run() flush_controlled_runs() def __repr__(self) -> str: part = "Im" if self.estimate_imaginary else "Re" return ( f"HadamardTestBlock(estimate_{part.lower()}={self.estimate_imaginary}, " f"measure={self.measure_at_end})" )