Source code for qarp.blocks._primitives.qsvt_block

from typing import List, Optional, Union

import numpy as np

import qarpx as qx
from qarp.operators import QubitOperator

from .._block import CompositeBlockBase, SimpleBlock
from .block_encoding_block import BlockEncodingBlock


[docs] class QSVTBlock(CompositeBlockBase): r"""Pattern B composite: Quantum Singular Value Transformation of an operator ``A`` driven by a sequence of projector-controlled-phase angles. For a polynomial of degree ``d = len(P_angles) - 1`` (per the QSVT convention), the assembled circuit is: Even ``d``:: Π_φ₀ · BE† · Π_φ₁ · BE · Π_φ₂ · BE† · … · Π_φ_d Odd ``d``:: Π_φ₀ · BE · Π_φ₁ · BE† · Π_φ₂ · BE · … · BE · Π_φ_d where ``BE`` block-encodes ``A / λ`` and ``Π_φ`` is a ``ProjectedControlPhaseBlock`` rotating the LCU-control subspace. Inputs are validated to be square / Hermitian / real-block-encoding (the standard QSVT preconditions). """ def __init__( self, A: Union[np.ndarray, QubitOperator], P_angles: List[float], target_qubits: Optional[List[int]] = None, name: str = "QSVT", ): # ── Input validation: QSVTBlock requires a square, Hermitian, real A ── if isinstance(A, np.ndarray): if A.ndim != 2 or A.shape[0] != A.shape[1]: raise NotImplementedError( "Current QSVT implementation only accepts square matrices/ops" ) if not np.allclose(A, A.conj().T): raise NotImplementedError( "Current QSVT implementation only accepts Hermitian matrices/ops" ) elif not isinstance(A, QubitOperator): raise TypeError("Expected np.ndarray or QubitOperator in QSVTBlock") # Build a BE up-front to get the lambda and full-unitary matrix for # the real-block-encoding check; we discard the built circuit and # rebuild fresh BE / BE† inside ``build_vanilla``. be_probe = BlockEncodingBlock(A) be_probe.build() be_FU = np.array(qx.QarpSimulator().unitary_matrix(be_probe.flatten(), be_probe.n_qubits)) lambda_factor = be_probe.lambda_factor() if np.linalg.norm(np.imag(be_FU)) > 1e-8: raise NotImplementedError("Current QSVT implementation only accepts real matrices/ops") if np.abs(lambda_factor - 1) > 1e-8: print( "Warning: lambda in QSVT is not 1. It is proceeded with A -> A / lambda. " f"lambda = {lambda_factor}" ) # Stash inputs (post-rescaling) for ``build_vanilla``. self.A = A / lambda_factor if isinstance(A, np.ndarray) else A self.optimal_angles = list(P_angles) self.nqubits = be_probe.n_qubits # Determine the target-subspace dimension for the PCP projectors. if isinstance(self.A, np.ndarray): self.Adim = self.A.shape[0] else: # max qubit index in any term of A; if A is a constant, default to 1. max_q = max((i for term in self.A.terms for i, _ in term), default=-1) self.Adim = 2 ** (1 + max_q) super().__init__( n_qubits=self.nqubits, target_qubits=target_qubits, name=name, )
[docs] def build_vanilla(self) -> None: n_full = self.n_qubits full_targets = list(range(n_full)) # ``Adim`` is the user-facing matrix dimension (may be non-power-of-2 # for ndarray inputs). The BE pads to ``2^ceil(log2(Adim))`` target # qubits, so the ancilla register has the matching size. n_anc = n_full - int(np.ceil(np.log2(self.Adim))) N_anc = 2**n_anc N_full = 2**n_full # Helper to add a "QSVT projector": phase ``e^{+iφ}`` on # ``ancilla = |0…0⟩`` indices, ``e^{-iφ}`` on the complement. In # qarpx LSB convention, ``ancilla = 0`` indices are strided # (`i % 2^n_anc == 0`), so we cannot reuse ``ProjectedControlPhaseBlock`` # — its "first ``dim`` states" semantics matches MSB layouts only. def _add_pcp(k: int) -> None: phi = self.optimal_angles[k] plus = np.exp(1j * phi) minus = np.exp(-1j * phi) diag = [plus if (i % N_anc == 0) else minus for i in range(N_full)] pcp = SimpleBlock(n_full, name=f"PCP_{k}") pcp.diagonal_unitary(diag) pcp.target_qubits = full_targets self.add_wired_child(pcp) # Helper to add a fresh BE (or BE†) on the full register. def _add_be(dagger: bool = False) -> None: be = BlockEncodingBlock(self.A) be.build() if dagger: # Lazy Python-wrapper dagger (deepcopy + flag): `add_child` # materialises it into concrete daggered commands, keeping # the child deepcopy-safe. be_use = be.dagger() else: be_use = be be_use.target_qubits = full_targets self.add_child(be_use) d = len(self.optimal_angles) - 1 if d % 2 == 0: # Even degree: Π · BE† · Π · BE · Π · BE† · … · Π for k in range(d // 2): _add_pcp(2 * k) _add_be(dagger=True) _add_pcp(2 * k + 1) _add_be(dagger=False) _add_pcp(d) else: # Odd degree: Π · BE · Π · BE† · … · BE · Π _add_pcp(0) for k in range((d - 1) // 2): _add_be(dagger=True) _add_pcp(2 * k + 1) _add_be(dagger=False) _add_pcp(2 * k + 2) _add_be(dagger=False) _add_pcp(d)