Source code for qrisp.algorithms.gqsp.qsvt

"""********************************************************************************
* Copyright (c) 2026 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************
"""

from typing import TYPE_CHECKING, Literal

from qrisp.algorithms.gqsp.gqsp_angles import qsvt_angles
from qrisp.algorithms.gqsp.helper_functions import _rescale_poly, poly2cheb
from qrisp.block_encodings import BlockEncoding
from qrisp.core.gate_application_functions import h, mcx, rz
from qrisp.environments import conjugate, invert
from qrisp.jasp import jrange, q_cond
from qrisp.operators import FermionicOperator, QubitOperator
from qrisp.qtypes import QuantumBool

if TYPE_CHECKING:
    from jax.typing import ArrayLike


[docs] def QSVT( A: BlockEncoding | FermionicOperator | QubitOperator, p: "ArrayLike", kind: Literal["Polynomial", "Chebyshev"] = "Polynomial", parity: Literal["odd", "even"] = "odd", rescale: bool = True, ) -> BlockEncoding: r"""Returns a BlockEncoding representing a polynomial transformation of the operator via `Quantum Singular Value Transformation <https://arxiv.org/abs/1806.01838>`_. For a block-encoded operator $A$ with `Singular Value Decomposition <https://en.wikipedia.org/wiki/Singular_value_decomposition>`_ $A = U \Sigma V^{\dagger}$ for unitaries $U, V$, and a (real) polynomial $p(x)$, this method returns a BlockEncoding of either operator: - $p_{odd}(A)=U p_{odd}(\Sigma) V^{\dagger}$ - $p_{even}(A)=V p_{even}(\Sigma) V^{\dagger}$ where $p=p_{odd}+p_{even}$ is decomposed into odd and even parity parts. .. warning:: If the parity is odd, this deviates from :func:`qrisp.algorithms.gqsp.gqsvt.GQSVT`, which returns a BlockEncoding of $p_{odd}(A)=V p_{odd}(\Sigma) U^{\dagger}$, i.e., the Hermitian conjugate. Parameters ---------- A : BlockEncoding | FermionicOperator | QubitOperator The operator to be transformed. Unlike in (G)QET, this operator does not need to be Hermitian. p : ArrayLike 1-D array containing the polynomial coefficients, ordered from lowest order term to highest. kind : {"Polynomial", "Chebyshev"} The basis in which the coefficients are defined. - ``"Polynomial"``: $p(x) = \sum c_i x^i$ - ``"Chebyshev"``: $p(x) = \sum c_i T_i(x)$, where $T_i$ are Chebyshev polynomials of the first kind. Default is ``"Polynomial"``. parity : {"odd", "even"} The parity part of $p=p_{odd}+p_{even}$ to be applied. - ``"odd"``: The odd part $p_{odd}(A)$ is applied. - ``"even"``: The even part $p_{even}(A)$ is applied. Default is ``"odd"``. rescale : bool If True (default), the method returns a block-encoding of $p(A)$. If False, the method returns a block-encoding of $p(A/\alpha)$ where $\alpha$ is the normalization factor for the block-encoding of the operator $A$. Returns ------- BlockEncoding A new BlockEncoding instance representing the transformed operator $p_{odd}(A)$ or $p_{even}(A)$. Examples -------- Define a non-Hermitian matrix $A$ and a vector $\vec{b}$. The matrix $A$ has singular value decomposition $A = U \Sigma V^{\dagger}$ for unitary matrices $U, V$. :: import numpy as np N = 4 A = np.eye(N, k=1) + 3 * np.eye(N) A[N-1,0] = 1 b = np.array([1,0,0,0]) print(A) # [[3. 1. 0. 0.] # [0. 3. 1. 0.] # [0. 0. 3. 1.] # [1. 0. 0. 3.]] Generate a BlockEncoding of $A$ and use QSVT to obtain a BlockEncoding of $p(A)=U p(\Sigma) V^{\dagger}$ for an odd parity polynomial. :: from qrisp import * from qrisp.block_encodings import BlockEncoding from qrisp.gqsp import QSVT def U0(qv): pass def U1(qv): qv-=1 BE = BlockEncoding.from_lcu(np.array([3,1]), [U0,U1]) BE_poly = QSVT(BE, np.array([0.,1.,0.,1.]), parity="odd") # Prepare initial system state |b> def operand_prep(): qv = QuantumFloat(2) prepare(qv, b) return qv @terminal_sampling def main(): operand = BE_poly.apply_rus(operand_prep)() return operand res_dict = main() amps = np.sqrt([res_dict.get(i, 0) for i in range(len(b))]) print(amps) # [0.85184732 0.21296187 0.0709873 0.47324855] Finally, compare the quantum simulation result with the classical solution: :: # Compute the SVD U, S, Vh = np.linalg.svd(A) # Apply polynomial z + z^3 to singular values S_poly = S + S ** 3 # Reconstruct transformed matrix A_poly = U @ np.diag(S_poly) @ Vh res = A_poly @ b / np.linalg.norm(A_poly @ b) print(res) # [0.85184734 0.21296184 0.07098728 0.47324852] .. warning:: For non-Hermitian matrices performing Singular Value Transform is not the same as applying a matrix polynomial. :: A_poly = A + A @ A @ A res = A_poly @ b / np.linalg.norm(A_poly @ b) print(res) # [0.71388113 0.02379604 0.21416434 0.66628906] """ ALLOWED_KINDS = {"Polynomial", "Chebyshev"} if kind not in ALLOWED_KINDS: raise ValueError(f"Invalid kind specified: '{kind}'. Allowed kinds are: {', '.join(ALLOWED_KINDS)}") if isinstance(A, (QubitOperator, FermionicOperator)): A = BlockEncoding.from_operator(A) # Rescaling of the polynomial to account for scaling factor alpha of block-encoding # If rescale=False, the returned block-encoding will implement p(A/alpha) instead of p(A), # where alpha is the normalization factor of the input block-encoding A. if rescale: p = _rescale_poly(A.alpha, p, kind=kind) # If the coefficients are given in the standard polynomial basis, convert them to the Chebyshev basis, # which is used internally for the angle calculation. if kind == "Polynomial": p = poly2cheb(p) phi, new_alpha = qsvt_angles(p, parity=parity) m = len(A._anc_templates) def reflection(args, phase): qubits = sum([arg.reg for arg in args[1 : m + 1]], []) with conjugate(mcx)(qubits, args[0], ctrl_state=0): rz(phase, args[0]) def even(args): A.unitary(*args[1:]) def odd(args): with invert(): A.unitary(*args[1:]) def new_unitary(*args): h(args[0]) d = len(phi) - 1 for i in jrange(0, d): reflection(args, phase=2 * phi[d - i]) q_cond(i % 2 == 0, even, odd, args) reflection(args, phase=2 * phi[0]) h(args[0]) new_anc_templates = [QuantumBool().template()] + A._anc_templates return BlockEncoding(new_alpha, new_anc_templates, new_unitary, num_ops=A.num_ops, is_hermitian=False)