BlockEncoding.svt#

BlockEncoding.svt(p: ArrayLike, kind: Literal['Polynomial', 'Chebyshev'] = 'Polynomial', parity: Literal['even', 'odd'] = 'odd') BlockEncoding#

Returns a BlockEncoding representing a singular value transformation (SVT) of the operator.

For a block-encoded operator \(A\) with 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. This is achieved using the Quantum Singular Value Transform (QSVT).

Parameters:
pArrayLike

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".

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 svt() to obtain a BlockEncoding of \(p(A)=U p(\Sigma) V^{\dagger}\) for an odd parity polynomial.

from qrisp import QuantumFloat, prepare, terminal_sampling
from qrisp.block_encodings import BlockEncoding

def U0(qv): pass
def U1(qv): qv-=1
BE = BlockEncoding.from_lcu(np.array([3,1]), [U0,U1])

BE_poly = BE.svt(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]