Source code for qrisp.algorithms.gqsp.pseudo_inversion

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

import numpy as np
import numpy.typing as npt
from numpy.polynomial import Chebyshev
from scipy.special import erf

from qrisp.algorithms.cks import cks_coeffs, cks_params
from qrisp.algorithms.gqsp.helper_functions import chebyshev_approx
from qrisp.algorithms.gqsp.qsvt import QSVT
from qrisp.block_encodings import BlockEncoding


[docs] def pseudo_inversion( A: BlockEncoding, eps: float, theta: float, delta: float | None = None, ) -> BlockEncoding: r"""Returns a BlockEncoding approximating the threshold `matrix pseudoinverse <https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse>`_ of the operator. Let $A$ be a matrix with `Singular Value Decomposition <https://en.wikipedia.org/wiki/Singular_value_decomposition>`_ .. math:: A = U\Sigma V^{\dagger} = \sum_i \sigma_i u_iv_i^{\dagger} where $\sigma_i$ are the singular values, $u_i$ are the left singular vectors, and $v_i$ are the right singular vectors. Given a threshold $\theta>0$, define a scalar thresholding function $f_{\theta}(\sigma)$ that acts on the singular values: .. math:: f_{\theta}(\sigma)=\begin{cases} \frac{1}{\sigma} & \sigma\geq\theta \\ 0 & \sigma<\theta \end{cases} The threshold pseudo inverse $A_{\theta}^{+}$ is constructed by applying this function to the singular values and recombining the matrix: .. math:: A_{\theta}^{+} = V f_{\theta}(\Sigma)U^{\dagger} = \sum_{\sigma_i\geq\theta}\frac{1}{\sigma_i}v_iu_i^{\dagger} For a block-encoded matrix $A$ with normalization factor $\alpha$, this function returns a BlockEncoding of an operator $\tilde{A}_{\alpha\cdot\theta}^{+}$ such that $\|\tilde{A}_{\alpha\cdot\theta}^{+} - A_{\alpha\cdot\theta}^{+}\| \leq \epsilon$. The pseudo inverse is implemented via Quantum Singular Value Transform (QSVT) using a polynomial approximation of $1/x$ over the domain $D_{\theta} = [-1, -\theta] \cup [\theta, 1]$, and a smoothed rectangle filter over the domain $D_{\theta}' = [-\theta, \theta]$. .. image:: /_static/chebyshev_pseudo_inversion.png :align: center Parameters ---------- A : BlockEncoding The block-encoded matrix to be pseudo inverted. It is assumed that the relevant singular values of $A/\alpha$ lie within $D_{\theta}$. eps : float The target precision $\epsilon$. theta : float This threshold value defines the boundaries of the "gap" around zero $[-\theta, \theta]\subset [-1,1]$ where the function $1/x$ is not approximated. delta : float The width of the transition region. The function will smoothly decay from 1 to 0 over the intervals $[-\theta, -\theta + 2\delta]$ and $[\theta - 2\delta, \theta]$. Defaults to $\delta = \theta/4$. Returns ------- BlockEncoding A new BlockEncoding instance representing an approximation of the pseudo inverse $A_{\alpha\cdot\theta}^{+}$. Notes ----- - **Complexity**: The polynomial degree scales as :math:`\mathcal{O}(\log(1/(\epsilon\theta))/\theta)`. References ---------- - Childs et al. (2017) `Quantum algorithm for systems of linear equations with exponentially improved dependence on precision <https://arxiv.org/pdf/1511.02306>`_. - Gilyén et al. (2019) `Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics <https://dl.acm.org/doi/abs/10.1145/3313276.3316366>`_. Examples -------- First, define a matrix $A$ and a right-hand side vector $\vec{b}$. :: import numpy as np N = 4 A = 1.4 * np.eye(N, k=1) + 1.1 * np.eye(N, k=-1) A[0, N-1] = 1.1 A[N-1, 0] = 1.4 b = np.array([0, 1, 1, 1]) print(A) #[[0. 1.4 0. 1.1] # [1.1 0. 1.4 0. ] # [0. 1.1 0. 1.4] # [1.4 0. 1.1 0. ]] _, S, _ = np.linalg.svd(A) print("Singular values of A: ", S) # Singular values of A: [2.5 2.5 0.3 0.3] Generate a block-encoding of $A$ and use :meth:`pseudo_inversion` to find a block-encoding approximating $A_{1}^{+}$. :: from qrisp import * from qrisp.block_encodings import BlockEncoding from qrisp.gqsp import pseudo_inversion # Define efficient block-encoding. def f0(x): x+=1 def f1(x): x-=1 BE = BlockEncoding.from_lcu(np.array([1.4, 1.1]), [f0, f1]) # alpha = 1.4 + 1.1 = 2.5 # Choose threshold theta > 0.3 / 2.5 = 0.12 # to cut off smallest singular values. BE_inv = pseudo_inversion(BE, eps=0.01, theta=0.4, delta=0.1) # Prepares operand variable in state |b> def prep_b(): operand = QuantumFloat(2) prepare(operand, b) return operand @terminal_sampling def main(): operand = prep_b() ancillas = BE_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_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))]) print(amps) Finally, compare the quantum simulation result with the classical solution: :: def threshold_pseudoinverse(A, threshold): U, S, Vh = np.linalg.svd(A, full_matrices=False) S_inv = np.zeros_like(S) valid_mask = S >= threshold S_inv[valid_mask] = 1.0 / S[valid_mask] return (Vh.conj().T * S_inv) @ U.conj().T c = (threshold_pseudoinverse(A, 0.4 * 2.5) @ b) c = c / np.linalg.norm(c) print("QUANTUM SIMULATION\n", amps, "\nCLASSICAL SOLUTION\n", c) # QUANTUM SIMULATION # [0.63244232 0.31179432 0.63244232 0.32065201] # CLASSICAL SOLUTION # [0.63245553 0.31622777 0.63245553 0.31622777] """ p = _pseudo_inversion_cheb(theta, delta, eps) # Set _rescale=False to apply p(A/α) instead of p(A). A_pseudo_inv = QSVT(A, p, kind="Chebyshev", rescale=False) # Adjust scaling factor since (A/α)^{-1} = αA^{-1}. A_pseudo_inv.alpha = A_pseudo_inv.alpha / A.alpha return A_pseudo_inv
def _smooth_rectangle( x: npt.NDArray[np.float64], t: float, delta: float, ) -> npt.NDArray[np.float64]: r"""Computes a smoothed rectangle (indicator) function using the error function. This function acts as a continuous, differentiable stand-in for a harsh discontinuous step function. It evaluates to approximately 1 inside the interval [-t, t] and transitions to 0 over a specified width. Smoothing the jump prevents the Gibbs phenomenon (wild oscillations) when subsequently fitting this target with a Chebyshev polynomial. Parameters ---------- x : ndarray The input array of values (typically evaluating over the domain [-1, 1]). t : float The half-width of the inner interval. The function will be approximately 1 for x in [-t, t]. delta :float The width of the transition region. The function will smoothly decay from 1 to 0 over the intervals $[-t - \delta, -t + \delta]$ and $[t - \delta, t + \delta]$. Returns ------- ndarray An array of evaluated function values, bounded between 0 and 1, with the same shape as the input array ``x``. """ # kappa dictates the steepness of the transition. # The factor of 2.0 is an empirical choice to ensure the curve settles # completely to 0 or 1 within the delta region. kappa = 2.0 / delta erf_plus = erf(kappa * (x + t)) erf_minus = erf(kappa * (x - t)) return 0.5 * (erf_plus - erf_minus) def _pseudo_inversion_cheb( theta: float, delta: float = None, eps: float = 1e-3, max_N: int = 2024, ) -> npt.NDArray[np.float64]: r"""Constructs a Chebyshev polynomial approximation of the pseudo-inversion. This function creates a polynomial that approximates $1/x$ over the domain $[-1, \theta] \cup [\theta, 1]$ while smoothly dropping to zero around the origin. It achieves this by multiplying an odd Chebyshev approximation of $1/x$ (https://arxiv.org/pdf/1511.02306, Lemma 14) with an even, smooth "inverted rectangle" filter that cuts off the region close to zero (https://arxiv.org/pdf/1806.01838, Lemma 29). Parameters ---------- theta : float This threshold value defines the boundaries of the "gap" around zero $[-\theta, \theta]\subset [-1,1]$ where the function $1/x$ is not approximated. delta : float, optional The width of the transition region for the smooth origin cutoff. If None, it defaults to $\theta / 4$. eps : float, optional The target precision $\epsilon$ for the approximation. Defaults to 1e-3. max_N : int, optional The maximum polynomial degree to evaluate when interpolating the even cutoff polynomial (the smooth rectangle). Defaults to 2048. Returns ------- ndarray 1-D array containing the coefficients of the Chebyshev series representing the smooth, bounded approximation of the pseudo-inverse, ordered from lowest order term to highest. """ if delta is None: delta = theta / 4 t = theta - delta # Define the target function for Chebyshev interpolation. target_func = lambda x: _smooth_rectangle(x, t, delta) cheb_even = 1 - chebyshev_approx(target_func, eps=eps, max_N=max_N) # The inversion polynomial is constructed using cks_params and cks_coeffs. # Since approximating 1/x over the relevant spectral interval [-1, -1/kappa] + [1/kappa, 1] # requires an odd Chebyshev series, cks_coeffs returns an array containing only the odd-degree coefficients. # This array is expanded into a full Chebyshev series by padding even-degree terms with zeros. j_0, beta = cks_params(eps, 1 / theta) coeffs_odd = cks_coeffs(j_0, beta) coeffs_odd = coeffs_odd * (-1) ** np.arange(len(coeffs_odd)) coeffs = np.zeros(2 * len(coeffs_odd)) coeffs[1::2] = coeffs_odd cheb_odd = Chebyshev(coeffs) cheb_pseudo_inverse = cheb_odd * cheb_even return cheb_pseudo_inverse.coef