"""********************************************************************************
* 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 collections.abc import Callable
import numpy as np
import numpy.typing as npt
from numpy.polynomial import Chebyshev
from qrisp.algorithms.gqsp.qsvt import QSVT
from qrisp.block_encodings import BlockEncoding
[docs]
def dalzell_inversion(A: BlockEncoding, prep_b: Callable, t: float, eps: float, kappa: float) -> BlockEncoding:
r"""Performs the `Dalzell quantum algorithm <https://arxiv.org/pdf/2406.12086>`_
to solve the Quantum Linear System Problem (QSLP) $A\vec{x}=\vec{b}$, using kernel reflection.
When applied to a state $\ket{0}$, the algorithm prepares a state $\tilde{x}\propto A^{-1}\ket{b}$
within target precision $\epsilon$ of the ideal solution $\ket{x}$.
See also `Dalzell's talk <https://www.youtube.com/watch?v=OwqhdCioj4Y>`_ for more details.
.. warning::
The returned BlockEncoding must be applied to operands in state $\ket{0}$ (and not in state $\ket{b}$).
Instead of solving the system $Ax=b$ directly, we consider the augmented system $A_t x_t = b'$, i.e.,
.. math::
A_t = \begin{pmatrix} A & 0 \\ 0 & t^{-1} \end{pmatrix} \begin{pmatrix} x \\ t \end{pmatrix} = \begin{pmatrix} b \\ 1 \end{pmatrix} = b'
Equivalently, we can write the augmented system as $A\ket{x_t}=\ket{b'}$ where
.. math::
A_t = \ket{0}_a\bra{0}_a \otimes A + t^{-1} \ket{1}_a\bra{1}_a \otimes \ket{0}_s\bra{0}_s,\\
\ket{b'} \propto \ket{0}_a\ket{b}_s + \ket{1}_a\ket{0}_s, \quad \ket{x_t} \propto \|x\|_2\ket{0}_a\ket{x}_s + t\ket{1}_a\ket{0}_s
Here, $\ket{\cdot}_a$ denotes the state of the (1-qubit) ancilla variable, and $\ket{\cdot}_s$ denotes the state of the system variable.
If $A_t x_t = b'$, then
.. math::
G_tx_t=(\mathbb{I} - \ket{b'}\bra{b'})A_t x_t = (\mathbb{I} - \ket{b'}\bra{b'})\ket{b'} = 0
Hence, the solution state $\ket{x_t}$ is a kernel state (sigular value 0) of the operator $G_t=(\mathbb{I} - \ket{b'}\bra{b'})A_t$.
The **kernel reflection** operator $K(G_t)$ reflects about the solution state $\ket{x_t}$ to the augmented system,
and can be implemented via QSVT using a polynomial approximation of the kernel reflection function
$K(\sigma)\colon [0,1] \to [-1,1]$ which maps the sigular value 0 to 1 and all other singular values to -1.
The algorithm consists of the following steps:
- Introduce extra basis state $\ket{e_n}=\ket{1}_a\ket{0}_s$ and prepare it as the initial state.
- Choose $t\simeq \|x\|_2$ and form the augmented linear system $A_tx_t=b'$.
- Apply the kernel reflection operator $K(G_t)$, which **reflects about the solution state to the augmented system**.
- Project the resulting state onto the $\ket{0}_a$ subspace to obtain $\ket{x}$.
The projection step can be implemented by post-selecting on the ancilla qubit being in state $\ket{0}$ after applying the kernel reflection.
The success probability of the algorithm depends on the choice of $t$.
If $t$ is chosen to be on the order of $\|x\|_2$, the algorithm succeeds with constant probability and only requires a constant number of repetitions.
.. image:: /_static/chebyshev_kernel_reflection.png
:align: center
Parameters
----------
A : BlockEncoding
The block-encoded matrix to be inverted. It is assumed that
the eigenvalues of $A/\alpha$ lie within $D_{\kappa} = [-1, -1/\kappa] \cup [1/\kappa, 1]$.
prep_b : Callable
A function ``prep_b(*operands)`` preparing the right hand side $\ket{b}$.
t : float
An estimate $t$ for the norm $\|x\|_2=\|(A/\alpha)^{-1}b\|_2$ for normalized $\|b\|_2=1$.
The success probability depends on the the ratio $t/\|x\|_2$.
The optimal choice is $t=\|x\|_2$. The estimate must lie in the interval $[1, \kappa]$. See below for more details.
eps : float
The target precision $\epsilon$.
kappa : float
An upper bound for the condition number $\kappa$ of $A$.
This value defines the "gap" around zero such that all sigular values (eigenvalues) of $A/\alpha$
lie in the domain $D_{\kappa}$.
Returns
-------
BlockEncoding
A new BlockEncoding instance representing the kernel reflection operator.
Notes
-----
- **Complexity**: The query complexitiy of the algorithm is determined by the polynomial degree and scales as $\mathcal O(\kappa\log(1/\epsilon))$.
The success probability of the algorithm depends on the choice of $t$ relative to $\|x\|_2$:
- If the norm is known up to a constant factor, the algorithm succeeds with constant probability, hence only requires a constant number of repetitions.
- If the norm is unknown, `methods for norm estimation <https://arxiv.org/pdf/2406.12086>`_ can be used to find a suitable $t$ with only a logarithmic overhead in the overall complexity.
For further insights, see `Constant Factor Analysis of Optimal Quantum Linear Solvers in Practice <https://arxiv.org/abs/2604.22185>`_,
and make sure to use :meth:`.resources() <qrisp.block_encodings.BlockEncoding.resources>` to compare the resources of different inversion methods in practice.
- The polynomial is applied to operator $G_t=(\mathbb I - \ket{b'}\bra{b'})A_t$.
Each application of $G_t$ requires 1 call to the block-encoding oracle for $A$
and 2 calls to the state preparation oracle for $b$.
Hence, in contrast to other :meth:`inversion` methods that only prepare the initial state once,
the overall circuit complexity scales multiplicatively with the complexity of the state preparation oracle.
Examples
--------
::
import numpy as np
A = np.array([[ 0.78, -0.01, -0.16, -0.1 ],
[-0.01, 0.57, -0.03, 0.08],
[-0.16, -0.03, 0.69, -0.15],
[-0.1 , 0.08, -0.15, 0.88]])
b = np.array([1, 1, 1, 1])
kappa = np.linalg.cond(A)
print("Condition number of A: ", kappa)
# Condition number of A: 2.020268873491503
::
from qrisp import *
from qrisp.block_encodings import BlockEncoding
from qrisp.gqsp import dalzell_inversion
BA = BlockEncoding.from_array(A)
def prep_b(operand):
prepare(operand, b)
BA_inv = dalzell_inversion(BA, prep_b, 2.0, 0.01, 3.0)
@terminal_sampling
def main():
# Prepare operand variable in state |0>
operand = QuantumFloat(2)
ancillas = BA_inv.apply(operand)
return operand, *ancillas
res_dict = main()
# Post-selection on ancillas being in |0> state
filtered_dict = {k[0]: p for k, p in res_dict.items() \
if all(x == 0 for x in k[1:])}
success_prob = sum(filtered_dict.values())
print("Success probability:", success_prob)
filtered_dict = {k: p / success_prob for k, p in filtered_dict.items()}
amps = np.sqrt([filtered_dict.get(i, 0) for i in range(len(b))])
::
c = (np.linalg.inv(A) @ b) / np.linalg.norm(np.linalg.inv(A) @ b)
print("QUANTUM SIMULATION\n", amps, "\nCLASSICAL SOLUTION\n", c)
# QUANTUM SIMULATION
# [0.51718522 0.43564501 0.60746181 0.41680095]
# CLASSICAL SOLUTION
# [0.51643917 0.4380885 0.60597747 0.41732523]
Alternatively, use ``apply_rus`` to directly obtain the solution state without post-selection:
::
@terminal_sampling
def main():
operand = BA_inv.apply_rus(lambda: QuantumFloat(2))()
return operand
res_dict = main()
amps = np.sqrt([res_dict.get(i, 0) for i in range(len(b))])
print(amps)
# [0.51816163 0.43295659 0.60721322 0.4187472 ]
"""
from qrisp import QuantumBool, control, h, x
def prep_b_ext(*args):
ext = args[0]
ops_A = args[1:]
h(ext)
with control(ext[0], ctrl_state=0):
prep_b(*ops_A)
# Define BlockEncoding of A_t
# A_t = |0><0| ⊗ A + (1 / t) |1><1| ⊗ |0><0|
P0 = BlockEncoding.from_projector(0, 0)
P10 = BlockEncoding.from_projector((1, 0), (1, 0))
A_t = P0.kron(A) + (A.alpha / t) * P10
# Define BlockEncoding of the kernel projector I - |b_t><b_t|
P = BlockEncoding.from_projector(prep_b_ext, kernel=True, num_ops=2)
# Define BlockEncoding of G_t = (I - |b_t><b_t|) A_t
G_t = P @ A_t
# Kernel Reflection
# Rescale kappa to account for larger normalization factor of A_t
kappa_t = kappa * (1.0 + 1.0 / t)
p = _kernel_reflection_cheb(1.0 / kappa_t, eps)
KR = QSVT(G_t, p, kind="Chebyshev", parity="even", rescale=False)
def new_unitary(*args):
anc_ext = args[0]
ancs_ = args[1 : 1 + KR.num_ancs]
args_ = args[1 + KR.num_ancs :]
x(anc_ext)
KR.unitary(*ancs_, anc_ext, *args_)
new_anc_templates = [QuantumBool().template()] + KR._anc_templates
return BlockEncoding(KR.alpha, new_anc_templates, new_unitary, num_ops=KR.num_ops - 1)
def _kernel_reflection_cheb(delta: float, eps: float = 1e-3) -> npt.NDArray[np.float64]:
r"""Constructs the Chebyshev polynomial for the Kernel Reflection
Polynomial $K_{\delta, \ell}(x)$ from `Dalzell (2024) <https://arxiv.org/pdf/2406.12086>`_ Eq 62.
Parameters
----------
delta : float
The spectral gap threshold ($0 < \delta < 1$).
eps : float
The maximum allowed error on the interval $[\delta, 1]$. Defaults to 1e-3.
Returns
-------
ndarray
1-D array containing the coefficients of the Chebyshev series representing the smooth, bounded
approximation of the kernel reflection, ordered from lowest order term to highest.
"""
# 1. Calculate the required degree parameter ell for the
# Kernel Reflection Polynomial to achieve a target error eps.
# Formula 51 (Dalzell 2024, arXiv:2406.12086):
ell = int(np.ceil((1 / (2 * delta)) * np.log(2 / eps)))
# 2. Calculate the Kernel Reflection Polynomial.
# Formula 62 (Dalzell 2024, arXiv:2406.12086):
# 2.1 Define the inner quadratic mapping z(x) in the Chebyshev basis:
# z(x) = (1 + delta^2 - 2x^2) / (1 - delta^2)
# Using the identity x^2 = 0.5*T_0(x) + 0.5*T_2(x), write z(x) exactly as:
c_0 = (delta**2) / (1 - delta**2)
c_2 = -1.0 / (1 - delta**2)
# Instantiate z(x) as a Chebyshev series object:
z_cheb = Chebyshev([c_0, 0.0, c_2])
# 2.2 Define the outer Chebyshev polynomial T_ell(z).
coeffs_T_ell = [0.0] * ell + [1.0]
T_ell = Chebyshev(coeffs_T_ell)
# 2.3 Compose the polynomials: T_ell_z(x) = T_ell(z(x)).
T_ell_z = T_ell(z_cheb)
# 2.4 Calculate the normalization constant.
# Evaluate T_ell at the scalar value z_0 = (1 + delta^2) / (1 - delta^2).
z_0 = (1 + delta**2) / (1 - delta**2)
T_ell_z0 = T_ell(z_0)
# 2.5 Construct the final normalized Kernel Reflection Polynomial.
K_cheb = -1.0 + 2.0 * (T_ell_z + 1.0) / (T_ell_z0 + 1.0)
return K_cheb.coef