Skip to content

nlft_qsp.qsp

Definitions of quantum signal processing protocols and public API for QSP solvers.

Classes:

Name Description
ChebyshevQSPPhaseFactors

Phase factors for a Chebyshev QSP protocol.

GQSPPhaseFactors

Phase factors for a Generalized QSP protocol.

PhaseFactors

Set of phase factors for a general Quantum Signal Processing protocol.

QSVTPhaseFactors

Phase factors for a QSVT/Reflection QSP protocol.

XQSPPhaseFactors

Phase factors for a XQSP protocol.

YQSPPhaseFactors

Phase factors for a YQSP protocol.

Functions:

Name Description
analytic_to_laurent

Converts the given polynomial \(p_0 + p_1 z + ... + p_n z^n\) into a definite-parity Laurent polynomial \(p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n\). If \(n\) is not given, then the degree of \(P\) will be considered.

chebqsp_approximate

DEPRECATED: use ChebyshevQSPPhaseFactors.approximate() instead.

chebqsp_solve

DEPRECATED: use ChebyshevQSPPhaseFactors.solve() instead.

chebyshev_to_laurent

Returns the Laurent polynomial equivalent to the Chebyshev expansion.

gqsp_solve

DEPRECATED: use GQSPPhaseFactors.solve() instead.

is_definite_parity

Returns whether the polynomial has the parity of \(n\). If \(n\) is not defined, then \(n\) is the index of last coefficient of \(P\).

laurent_to_analytic

Converts the given definite-parity Laurent polynomial \(p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n\) into an analytic polynomial \(p_0 + p_1 z + ... + p_n z^n\). If \(n\) is not given, then \(n\) = the effective degree of \(P\).

phase_prefactor

Computes the phase prefactor for the nonlinear Fourier sequence coefficient \(F\).

qsp_variant

Class decorator to register QSP variants. Modes are [a]nalytic, [l]aurent, [c]hebyshev.

qsvt_approximate

DEPRECATED: use QSVTPhaseFactors.approximate() instead.

qsvt_solve

DEPRECATED: use QSVTPhaseFactors.solve() instead.

xqsp_solve

DEPRECATED: use XQSPPhaseFactors.solve() instead.

xqsp_solve_laurent

DEPRECATED: use XQSPPhaseFactors.solve_laurent() instead.

yqsp_solve

DEPRECATED: use YQSPPhaseFactors.solve() instead.

yqsp_solve_laurent

DEPRECATED: use YQSPPhaseFactors.solve_laurent() instead.

ChebyshevQSPPhaseFactors

Bases: XQSPPhaseFactors

Phase factors for a Chebyshev QSP protocol.

\[ e^{i\phi_0 Z} \tilde{x} e^{i\phi_1 Z} \tilde{x} \cdots \tilde{x} e^{i\phi_n Z} = \begin{pmatrix} P(x) & iQ(x) \sqrt{1 - x^2} \\ iQ^*(x) \sqrt{1 - x^2} & P^*(x) \end{pmatrix} \]

where the signal operator \(\tilde{x}\) is

\[ \tilde{x} = \begin{pmatrix} x & i\sqrt{1 - x^2} \\ i\sqrt{1 - x^2} & x \end{pmatrix} \]
Note

This is the ansatz of arXiv:2105.02859 Theorem 9, but the polynomial construction is implemented by conjugating XQSP with a Hadamard gate.

Methods:

Name Description
approximate

Approximate the given callable object \(f\) (which takes \(x \in [-1, 1]\) and returns a real number)

solve

Returns the set of phase factors for a Chebyshev QSP protocol implementing the polynomial \(P(x)\) (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

Source code in nlft_qsp/qsp.py
@serializable(
    type_tag="@qspx/phase_factors/chebqsp",
    fields={"phi": "phi"}
)
@qsp_variant('cheb', modes=['c'], display_name='Chebyshev QSP')
class ChebyshevQSPPhaseFactors(XQSPPhaseFactors):
    r"""Phase factors for a Chebyshev QSP protocol.

    $$ e^{i\phi_0 Z} \tilde{x} e^{i\phi_1 Z} \tilde{x} \cdots \tilde{x} e^{i\phi_n Z} = \begin{pmatrix} P(x) & iQ(x) \sqrt{1 - x^2} \\ iQ^*(x) \sqrt{1 - x^2} & P^*(x) \end{pmatrix} $$

    where the signal operator $\tilde{x}$ is

    $$ \tilde{x} = \begin{pmatrix} x & i\sqrt{1 - x^2} \\ i\sqrt{1 - x^2} & x \end{pmatrix} $$

    Note:
        This is the ansatz of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859) Theorem 9, but the polynomial
        construction is implemented by conjugating XQSP with a Hadamard gate."""
    def processing_operator(self, k: int): # exp(i phi[k] Z)
        return np.exp(1j*self.phi[k]), 0

    def protocol_conjugation(self, P, Q): # Return the polynomials (P', Q') = H (P, Q) H
        return ((P + P.conjugate()) + (Q - Q.conjugate()))/2, ((P - P.conjugate()) - (Q + Q.conjugate()))/2

    def processing_operator_conjugation(self, a, b):
        return np.real(a) + 1j*np.imag(b), 1j*np.imag(a) - np.real(b)

    def duplicate(self):
        return ChebyshevQSPPhaseFactors(self.phi)

    def iX(self):
        raise ValueError("Multiplying by iX is not possible.")

    def iZ(self):
        return super.iX() # applying iZ is equivalent to applying iX, by the Hadamard conjugation

    @classmethod
    def solve(cls, T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> "ChebyshevQSPPhaseFactors":
        r"""Returns the set of phase factors for a Chebyshev QSP protocol implementing the polynomial $P(x)$ (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

        The target polynomial will be $T(x) = \sum_{k = 0}^n c_k T_k(x)$ (if `T` is a `ChebyshevTExpansion`) or $T(x) = \sum_{k = 0}^n c_k x^k$ (if `T` is a `Polynomial`), where $T_k(x)$ are the Chebyshev polynomials of the first kind.

        Args:
            T: a Chebyshev expansion object or the desired Polynomial $P(x)$ (that will be converted to the Chebyshev basis).

        Raises:
            ValueError: If the target polynomial does not have definite parity or is not real.

        Note:
            `T` can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases."""
        if isinstance(T, list):
            T = ChebyshevTExpansion(T)

        if isinstance(T, Polynomial):
            T = ChebyshevTExpansion.from_polynomial(T)

        if not T.is_real():
            raise ValueError("Only real polynomials are supported.")

        xqsp = XQSPPhaseFactors.solve_laurent(T.to_laurent())
        return ChebyshevQSPPhaseFactors(xqsp.phi)

    @classmethod
    def approximate(cls, f: Callable, deg: int) -> "ChebyshevQSPPhaseFactors":
        r"""Approximate the given callable object $f$ (which takes $x \in [-1, 1]$ and returns a real number)
        and returns the Chebyshev QSP phase factors implementing an approximating polynomial of degree `deg`
        (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

        Note: The parity of `deg` should coincide with the parity of $f$, otherwise the Chebyshev approximator might give numerical errors."""
        return cls.solve(chebyshev_approximate(f, deg))

approximate(f: Callable, deg: int) -> ChebyshevQSPPhaseFactors classmethod

Approximate the given callable object \(f\) (which takes \(x \in [-1, 1]\) and returns a real number) and returns the Chebyshev QSP phase factors implementing an approximating polynomial of degree deg (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

Note: The parity of deg should coincide with the parity of \(f\), otherwise the Chebyshev approximator might give numerical errors.

Source code in nlft_qsp/qsp.py
@classmethod
def approximate(cls, f: Callable, deg: int) -> "ChebyshevQSPPhaseFactors":
    r"""Approximate the given callable object $f$ (which takes $x \in [-1, 1]$ and returns a real number)
    and returns the Chebyshev QSP phase factors implementing an approximating polynomial of degree `deg`
    (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

    Note: The parity of `deg` should coincide with the parity of $f$, otherwise the Chebyshev approximator might give numerical errors."""
    return cls.solve(chebyshev_approximate(f, deg))

solve(T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> ChebyshevQSPPhaseFactors classmethod

Returns the set of phase factors for a Chebyshev QSP protocol implementing the polynomial \(P(x)\) (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

The target polynomial will be \(T(x) = \sum_{k = 0}^n c_k T_k(x)\) (if T is a ChebyshevTExpansion) or \(T(x) = \sum_{k = 0}^n c_k x^k\) (if T is a Polynomial), where \(T_k(x)\) are the Chebyshev polynomials of the first kind.

Parameters:

Name Type Description Default
T list[complex_type] | Polynomial | ChebyshevTExpansion

a Chebyshev expansion object or the desired Polynomial \(P(x)\) (that will be converted to the Chebyshev basis).

required

Raises:

Type Description
ValueError

If the target polynomial does not have definite parity or is not real.

Note

T can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases.

Source code in nlft_qsp/qsp.py
@classmethod
def solve(cls, T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> "ChebyshevQSPPhaseFactors":
    r"""Returns the set of phase factors for a Chebyshev QSP protocol implementing the polynomial $P(x)$ (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

    The target polynomial will be $T(x) = \sum_{k = 0}^n c_k T_k(x)$ (if `T` is a `ChebyshevTExpansion`) or $T(x) = \sum_{k = 0}^n c_k x^k$ (if `T` is a `Polynomial`), where $T_k(x)$ are the Chebyshev polynomials of the first kind.

    Args:
        T: a Chebyshev expansion object or the desired Polynomial $P(x)$ (that will be converted to the Chebyshev basis).

    Raises:
        ValueError: If the target polynomial does not have definite parity or is not real.

    Note:
        `T` can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases."""
    if isinstance(T, list):
        T = ChebyshevTExpansion(T)

    if isinstance(T, Polynomial):
        T = ChebyshevTExpansion.from_polynomial(T)

    if not T.is_real():
        raise ValueError("Only real polynomials are supported.")

    xqsp = XQSPPhaseFactors.solve_laurent(T.to_laurent())
    return ChebyshevQSPPhaseFactors(xqsp.phi)

GQSPPhaseFactors

Bases: PhaseFactors

Phase factors for a Generalized QSP protocol.

\[ e^{i\lambda Z} e^{i\phi_0 X} e^{i\theta_0 Z} W(z) e^{i\phi_1 X} e^{i\theta_1 Z} W(z) \cdots W(z) e^{i\phi_n X} e^{i\theta_n Z} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} \]

where \(W(z) = \mathrm{diag}(z, 1)\) (mode='analytic') or \(W(z) = \mathrm{diag}(z, z^{-1})\) (mode='laurent').

Note

This class follows the convention of arXiv:2503.03026, Theorem 2, which is different from the original GQSP convention. If the convention of arXiv:2308.01501 Theorem 3 is desired, then one should use the to_mw_gqsp() method.

Methods:

Name Description
from_nlfs

Computes the GQSP phase factors for a given NLFT sequence.

phase_offset

Returns the phase of the leading coefficient of P, where (P, Q) is the pair of polynomials generated by this set.

solve

Returns the set of phase factors for a Generalized QSP protocol producing the given polynomial. See here for an overview of the QSP variants.

solve_laurent

Returns the set of phase factors for a Generalized QSP protocol producing the given definite-parity polynomial. See here for an overview of the QSP variants.

to_mw_gqsp

Converts the GQSP phase factors to the convention of arXiv:2308.01501 Theorem 3.

to_xqsp

Converts the QSP phase factors into XQSP phase factors.

to_yqsp

Converts the QSP phase factors into YQSP phase factors.

Source code in nlft_qsp/qsp.py
@serializable(
    type_tag="@qspx/phase_factors/gqsp",
    fields={"phi": "phi", "lbd": "lbd", "theta": "theta"}
)
@qsp_variant('g', modes=['a', 'l'], display_name='Generalized QSP')
class GQSPPhaseFactors(PhaseFactors):
    r"""Phase factors for a Generalized QSP protocol.

    $$ e^{i\lambda Z} e^{i\phi_0 X} e^{i\theta_0 Z} W(z) e^{i\phi_1 X} e^{i\theta_1 Z} W(z) \cdots W(z) e^{i\phi_n X} e^{i\theta_n Z} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} $$

    where $W(z) = \mathrm{diag}(z, 1)$ (`mode='analytic'`) or $W(z) = \mathrm{diag}(z, z^{-1})$ (`mode='laurent'`).

    Note:
        This class follows the convention of [arXiv:2503.03026](https://arxiv.org/abs/2503.03026), Theorem 2, which is different from the original GQSP convention. If the convention of [arXiv:2308.01501](https://arxiv.org/abs/2308.01501) Theorem 3 is desired, then one should use the `to_mw_gqsp()` method."""
    def __init__(self, phi: list[float_type], lbd: float_type=0, theta: list[float_type]=None):
        self.lbd = lbd
        self.phi = list(phi)

        if theta is None:
            theta = [0] * len(phi)

        self.theta = list(theta)

        if len(theta) < len(phi):
            theta += [0] * (len(phi) - len(theta))

        if len(theta) > len(phi):
            phi += [0] * (len(theta) - len(phi))

    def duplicate(self):
        return GQSPPhaseFactors(self.phi, self.lbd, self.theta)

    def processing_operator(self, k: int):
        if k == 0: # exp(i lbd Z) exp(i phi X) exp(i theta Z)
            return np.exp(1j*(self.lbd + self.theta[k]))*np.cos(self.phi[k]), \
                1j*np.exp(1j*(self.lbd - self.theta[k]))*np.sin(self.phi[k])
        else: # exp(i phi X) exp(i theta Z)
            return np.exp(1j*self.theta[k])*np.cos(self.phi[k]), 1j*np.exp(-1j*self.theta[k])*np.sin(self.phi[k])

    def degree(self):
        return len(self.phi) - 1

    def iX(self):
        pf = self.duplicate()
        pf.theta[-1] = -pf.theta[-1]
        pf.phi[-1] += np.pi/2
        return pf

    def iY(self):
        return self.iZ().iX()

    def iZ(self):
        pf = self.duplicate()
        pf.theta[-1] += np.pi/2
        return pf

    def phase_offset(self) -> float_type:
        """Returns the phase of the leading coefficient of P, where (P, Q) is the pair of polynomials generated by this set."""
        return self.lbd + sum(self.theta)

    def to_xqsp(self):
        """Converts the QSP phase factors into XQSP phase factors.

        Raises:
            ValueError: If the phase factors do not lie in the XQSP subalgebra."""
        F = self.to_nlfs()
        if not F.is_imaginary():
            raise ValueError("The phase factors are not reducible to XQSP.")

        return XQSPPhaseFactors.from_nlfs(F)

    def to_yqsp(self):
        """Converts the QSP phase factors into YQSP phase factors.

        Raises:
            ValueError: If the phase factors do not lie in the YQSP subalgebra."""
        F = self.to_nlfs()
        if not F.is_real():
            raise ValueError("The phase factors are not reducible to YQSP.")

        return YQSPPhaseFactors.from_nlfs(F)

    def to_nlfs(self) -> NonLinearFourierSequence:
        n = self.degree()
        alpha = self.phase_offset() # phase of the leading coefficient of P

        psi = [0] * (n+1) # prefactors

        psi[0] = self.lbd - alpha/2
        for k in range(n):
            psi[k+1] = self.theta[k] + psi[k]

        phi = self.phi
        return NonLinearFourierSequence([1j*np.tan(phik)*np.exp(2j*psik) for phik, psik in zip(phi, psi)])

    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence, alpha: float_type = 0) -> PhaseFactors:
        r"""Computes the GQSP phase factors for a given NLFT sequence.
        If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(e^{i \alpha} z^n a, b)$ in the analytic picture.

        Args:
            F (NonLinearFourierSequence): The sequence to be converted to phase factors.
            alpha (float_type): In the pair of polynomials $(P, Q)$ generated by the returned phase factors, $P$ will be multiplied by `e^{i \alpha}`.

        Note:
            The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
        psi = [phase_prefactor(Fk) for Fk in F.coeffs]
        lbd = psi[0]

        phi = [np.arctan(np.imag(Fk * np.exp(-2j * psik))) for Fk, psik in zip(F, psi)]

        psi += [0] # we add psi[n+1] to compute theta
        theta = [psi[k+1] - psi[k] for k in range(len(phi))]

        lbd += alpha/2
        theta[-1] += alpha/2
        return GQSPPhaseFactors(phi, lbd, theta)

    def to_mw_gqsp(self) -> tuple[list[float_type], list[float_type], float_type]:
        r"""Converts the GQSP phase factors to the convention of [arXiv:2308.01501](https://arxiv.org/abs/2308.01501) Theorem 3.

        In particular, a protocol should be constructed of the form

        $$ R(\theta_0, \phi_0, \lambda) \tilde{w} R(\theta_1, \phi_1, 0) \tilde{w} \cdots \tilde{w} R(\phi_n, \theta_n, 0) = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} $$

        where the matrix $R$ is of the form

        $$ R(\theta, \phi, \lambda) = \begin{pmatrix} e^{i(\lambda + \phi)} \cos \theta & e^{i\phi} \sin \theta \\ e^{i\lambda} \sin \theta & -\cos \theta \end{pmatrix} $$

        Returns:
            The phase factors \vec{\theta}, \vec{\phi} (as lists) and \lambda, in this order."""
        n = self.degree()

        alpha = - self.lbd - sum(self.theta) - (n+1) * np.pi # needed to counterbalance the global phase

        mw_theta = [self.phi[k] for k in range(n+1)]
        mw_phi = [2 * self.lbd - np.pi/2 + alpha] + [2 * self.theta[k-1] - np.pi for k in range(1, n+1)]
        mw_lbd = 2 * self.theta[n] - np.pi/2 + alpha

        return mw_theta, mw_phi, mw_lbd

    @classmethod
    def solve(cls, P: Polynomial, convention='qsp') -> "GQSPPhaseFactors":
        r"""Returns the set of phase factors for a Generalized QSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

            The support_start of $P$ will be ignored."""
        if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
            raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

        match convention:
            case "qsp":
                P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
            case "nlft":
                P = Polynomial(P.coeffs, 0)
            case _:
                raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

        F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

        if convention != "qsp":
            return GQSPPhaseFactors.from_nlfs(F)
        return GQSPPhaseFactors.from_nlfs(F).iX()

    @classmethod
    def solve_laurent(cls, P: Polynomial, convention='qsp') -> "GQSPPhaseFactors":
        r"""Returns the set of phase factors for a Generalized QSP protocol producing the given definite-parity polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Raises:
            ValueError: If $P$ does not have definite parity.

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.
            """
        if not is_definite_parity(P):
            raise ValueError("Laurent polynomial is not of definite parity.")

        return cls.solve(laurent_to_analytic(P), convention)

from_nlfs(F: NonLinearFourierSequence, alpha: float_type = 0) -> PhaseFactors classmethod

Computes the GQSP phase factors for a given NLFT sequence. If \(\mathrm{NLFT}(F) = (a, b)\), then the returned phase factors will implement \((e^{i \alpha} z^n a, b)\) in the analytic picture.

Parameters:

Name Type Description Default
F NonLinearFourierSequence

The sequence to be converted to phase factors.

required
alpha float_type

In the pair of polynomials \((P, Q)\) generated by the returned phase factors, \(P\) will be multiplied by e^{i \alpha}.

0
Note

The support start of \(F\) is ignored, so the support of \(b\) is assumed to start at \(0\).

Source code in nlft_qsp/qsp.py
@classmethod
def from_nlfs(cls, F: NonLinearFourierSequence, alpha: float_type = 0) -> PhaseFactors:
    r"""Computes the GQSP phase factors for a given NLFT sequence.
    If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(e^{i \alpha} z^n a, b)$ in the analytic picture.

    Args:
        F (NonLinearFourierSequence): The sequence to be converted to phase factors.
        alpha (float_type): In the pair of polynomials $(P, Q)$ generated by the returned phase factors, $P$ will be multiplied by `e^{i \alpha}`.

    Note:
        The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
    psi = [phase_prefactor(Fk) for Fk in F.coeffs]
    lbd = psi[0]

    phi = [np.arctan(np.imag(Fk * np.exp(-2j * psik))) for Fk, psik in zip(F, psi)]

    psi += [0] # we add psi[n+1] to compute theta
    theta = [psi[k+1] - psi[k] for k in range(len(phi))]

    lbd += alpha/2
    theta[-1] += alpha/2
    return GQSPPhaseFactors(phi, lbd, theta)

phase_offset() -> float_type

Returns the phase of the leading coefficient of P, where (P, Q) is the pair of polynomials generated by this set.

Source code in nlft_qsp/qsp.py
def phase_offset(self) -> float_type:
    """Returns the phase of the leading coefficient of P, where (P, Q) is the pair of polynomials generated by this set."""
    return self.lbd + sum(self.theta)

solve(P: Polynomial, convention='qsp') -> GQSPPhaseFactors classmethod

Returns the set of phase factors for a Generalized QSP protocol producing the given polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'
Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

The support_start of \(P\) will be ignored.

Source code in nlft_qsp/qsp.py
@classmethod
def solve(cls, P: Polynomial, convention='qsp') -> "GQSPPhaseFactors":
    r"""Returns the set of phase factors for a Generalized QSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
    A complementary $Q$ will be computed with Weiss' algorithm.

    Args:
        convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

    Note:
        The sup norm of $P$ should be bounded by $1 - \eta < 1$.
        The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

        The support_start of $P$ will be ignored."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

    match convention:
        case "qsp":
            P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

    F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if convention != "qsp":
        return GQSPPhaseFactors.from_nlfs(F)
    return GQSPPhaseFactors.from_nlfs(F).iX()

solve_laurent(P: Polynomial, convention='qsp') -> GQSPPhaseFactors classmethod

Returns the set of phase factors for a Generalized QSP protocol producing the given definite-parity polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'

Raises:

Type Description
ValueError

If \(P\) does not have definite parity.

Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

Source code in nlft_qsp/qsp.py
@classmethod
def solve_laurent(cls, P: Polynomial, convention='qsp') -> "GQSPPhaseFactors":
    r"""Returns the set of phase factors for a Generalized QSP protocol producing the given definite-parity polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
    A complementary $Q$ will be computed with Weiss' algorithm.

    Args:
        convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

    Raises:
        ValueError: If $P$ does not have definite parity.

    Note:
        The sup norm of $P$ should be bounded by $1 - \eta < 1$.
        The time required by the algorithm to compute the phase factors will scale with $1/\eta$.
        """
    if not is_definite_parity(P):
        raise ValueError("Laurent polynomial is not of definite parity.")

    return cls.solve(laurent_to_analytic(P), convention)

to_mw_gqsp() -> tuple[list[float_type], list[float_type], float_type]

Converts the GQSP phase factors to the convention of arXiv:2308.01501 Theorem 3.

In particular, a protocol should be constructed of the form

\[ R(\theta_0, \phi_0, \lambda) \tilde{w} R(\theta_1, \phi_1, 0) \tilde{w} \cdots \tilde{w} R(\phi_n, \theta_n, 0) = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} \]

where the matrix \(R\) is of the form

\[ R(\theta, \phi, \lambda) = \begin{pmatrix} e^{i(\lambda + \phi)} \cos \theta & e^{i\phi} \sin \theta \\ e^{i\lambda} \sin \theta & -\cos \theta \end{pmatrix} \]

Returns:

Type Description
tuple[list[float_type], list[float_type], float_type]

The phase factors \vec{\theta}, \vec{\phi} (as lists) and \lambda, in this order.

Source code in nlft_qsp/qsp.py
def to_mw_gqsp(self) -> tuple[list[float_type], list[float_type], float_type]:
    r"""Converts the GQSP phase factors to the convention of [arXiv:2308.01501](https://arxiv.org/abs/2308.01501) Theorem 3.

    In particular, a protocol should be constructed of the form

    $$ R(\theta_0, \phi_0, \lambda) \tilde{w} R(\theta_1, \phi_1, 0) \tilde{w} \cdots \tilde{w} R(\phi_n, \theta_n, 0) = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} $$

    where the matrix $R$ is of the form

    $$ R(\theta, \phi, \lambda) = \begin{pmatrix} e^{i(\lambda + \phi)} \cos \theta & e^{i\phi} \sin \theta \\ e^{i\lambda} \sin \theta & -\cos \theta \end{pmatrix} $$

    Returns:
        The phase factors \vec{\theta}, \vec{\phi} (as lists) and \lambda, in this order."""
    n = self.degree()

    alpha = - self.lbd - sum(self.theta) - (n+1) * np.pi # needed to counterbalance the global phase

    mw_theta = [self.phi[k] for k in range(n+1)]
    mw_phi = [2 * self.lbd - np.pi/2 + alpha] + [2 * self.theta[k-1] - np.pi for k in range(1, n+1)]
    mw_lbd = 2 * self.theta[n] - np.pi/2 + alpha

    return mw_theta, mw_phi, mw_lbd

to_xqsp()

Converts the QSP phase factors into XQSP phase factors.

Raises:

Type Description
ValueError

If the phase factors do not lie in the XQSP subalgebra.

Source code in nlft_qsp/qsp.py
def to_xqsp(self):
    """Converts the QSP phase factors into XQSP phase factors.

    Raises:
        ValueError: If the phase factors do not lie in the XQSP subalgebra."""
    F = self.to_nlfs()
    if not F.is_imaginary():
        raise ValueError("The phase factors are not reducible to XQSP.")

    return XQSPPhaseFactors.from_nlfs(F)

to_yqsp()

Converts the QSP phase factors into YQSP phase factors.

Raises:

Type Description
ValueError

If the phase factors do not lie in the YQSP subalgebra.

Source code in nlft_qsp/qsp.py
def to_yqsp(self):
    """Converts the QSP phase factors into YQSP phase factors.

    Raises:
        ValueError: If the phase factors do not lie in the YQSP subalgebra."""
    F = self.to_nlfs()
    if not F.is_real():
        raise ValueError("The phase factors are not reducible to YQSP.")

    return YQSPPhaseFactors.from_nlfs(F)

PhaseFactors

Set of phase factors for a general Quantum Signal Processing protocol. It also provides methods to construct polynomials generated by QSP protocols. Each subclass of this class represents a different QSP ansatz.

Methods:

Name Description
degree

Returns the degree of the polynomials generated by the QSP protocol.

iX

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iX\) on the right, where \(X\) is the Pauli matrix.

iY

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iY\) on the right, where \(Y\) is the Pauli matrix.

iZ

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iZ\) on the right, where \(Z\) is the Pauli matrix.

polynomials

Returns the pair of polynomials \((P, Q)\) generated by the given set of phase factors.

polynomials_bounds

Returns the pair of polynomials \((P, Q) = A_{inf} \tilde{v} A_{inf+1} \tilde{v} ... \tilde{v} A_{sup-1} \tilde{v} A_{sup}\),

processing_operator

Returns the \(k\)-th signal processing operator according to the given QSP variant.

processing_operator_conjugation

This applies a conjugation to each signal processing operator returned by processing_operator(), e.g., with the Hadamard gate.

protocol_conjugation

Given \((P, Q)\), this applies a final conjugation to the whole protocol, e.g., with the Hadamard gate.

signal_operator

Returns the pair of polynomials given by multiplying \((P_1, Q_1) W(z) (P_2, Q_2)\), where W(z) is the signal operator.

to_nlfs

Returns the nonlinear Fourier sequence generating \((z^{-n} P, Q)\),

Source code in nlft_qsp/qsp.py
class PhaseFactors:
    """Set of phase factors for a general Quantum Signal Processing protocol.
    It also provides methods to construct polynomials generated by QSP protocols.
    Each subclass of this class represents a different QSP ansatz."""
    def duplicate(self):
        raise NotImplementedError()

    def processing_operator(self, k: int):
        r"""Returns the $k$-th signal processing operator according to the given QSP variant."""
        raise NotImplementedError()

    def signal_operator(self, P1: Polynomial, Q1: Polynomial, P2: Polynomial, Q2: Polynomial) -> tuple[Polynomial, Polynomial]:
        """Returns the pair of polynomials given by multiplying $(P_1, Q_1) W(z) (P_2, Q_2)$, where `W(z)` is the signal operator.

        Note:
            This might not reflect the signal operator as expressed in the original papers. Some basis transformations are implicitly made for computational efficiency."""
        # W(z) = diag(z, z^(-1))
        zP1 = P1.shift(1)  # z*P1
        zQ1 = Q1.shift(-1) # z^(-1)*Q1
        return zP1 * P2 - zQ1 * Q2.conjugate(), zP1 * Q2 + zQ1 * P2.conjugate()

    def protocol_conjugation(self, P, Q):
        r"""Given $(P, Q)$, this applies a final conjugation to the whole protocol, e.g., with the Hadamard gate."""
        return P, Q

    def processing_operator_conjugation(self, a, b):
        """This applies a conjugation to each signal processing operator returned by `processing_operator()`, e.g., with the Hadamard gate.
        This is done mainly to make `processing_operator()` return the operators as expressed by the original ansatze in the papers, while keeping computational efficiency and numerical stability."""
        return a, b

    def degree(self):
        """Returns the degree of the polynomials generated by the QSP protocol."""
        raise NotImplementedError()

    def iX(self):
        r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iX$ on the right, where $X$ is the Pauli matrix.
        This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (iQ, iP)$.
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

        Raises:
            ValueError: if multiplying by $iX$ does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iX is not possible.")

    def iY(self):
        r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iY$ on the right, where $Y$ is the Pauli matrix.
        This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (-Q, P)$.
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

        Raises:
            ValueError: if multiplying by $iY$ does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iY is not possible.")

    def iZ(self):
        r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iZ$ on the right, where $Z$ is the Pauli matrix.
        This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (iP, -iQ)$.
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

        Raises:
            ValueError: if multiplying by $iZ$ does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iZ is not possible.")

    def polynomials_bounds(self, inf: int, sup: int) -> tuple[Polynomial, Polynomial]:
        r"""Returns the pair of polynomials $(P, Q) = A_{inf} \tilde{v} A_{inf+1} \tilde{v} ... \tilde{v} A_{sup-1} \tilde{v} A_{sup}$,
        where $\tilde{v} = \mathrm{diag}(z, z^{-1})$ is the signal operator.

        Note:
            This assumes the Laurent picture ($\tilde{v} = \mathrm{diag}(z, z^{-1})$). For the analytic picture $\tilde{w} = \mathrm{diag}(z, 1)$ use `polynomials()`."""
        if sup - inf < 0:
            return Polynomial([1]), Polynomial([0])
        if sup - inf <= 0:
            p, q = self.processing_operator(inf)
            p, q = self.processing_operator_conjugation(p, q)
            return Polynomial([p]), Polynomial([q])

        mid = (sup + inf) // 2
        P1, Q1 = self.polynomials_bounds(inf, mid)
        P2, Q2 = self.polynomials_bounds(mid+1, sup)

        return self.signal_operator(P1, Q1, P2, Q2) # (P1, Q1) W(z) (P2, Q2)

    def polynomials(self, inf: int = 0, sup: int = -1, mode: str = 'analytic') -> tuple[Polynomial, Polynomial]:
        """Returns the pair of polynomials $(P, Q)$ generated by the given set of phase factors.
        The polynomials are computed with a divide-and-conquer strategy (see [here](https://arxiv.org/abs/2410.06409)).

        Args:
            mode (str): Either `'analytic'` or `'laurent'`, indicating whether an analytic or a Laurent QSP protocol should be composed.
        """
        if sup < 0:
            sup = self.degree()

        Pl, Ql = self.polynomials_bounds(inf, sup)
        Pl, Ql = self.protocol_conjugation(Pl, Ql)
        match mode:
            case 'analytic': # convert from Laurent to analytic picture
                return laurent_to_analytic(Pl), laurent_to_analytic(Ql)
            case 'laurent':
                return Pl, Ql
            case _:
                raise ValueError("mode can only be 'analytic' or 'laurent'.")

    def to_nlfs(self) -> NonLinearFourierSequence:
        """Returns the nonlinear Fourier sequence generating $(z^{-n} P, Q)$,
        where $(P, Q)$ is the pair of polynomial generated by the given set of GQSP phase factors.

        Note: if the phase factors are not canonical, then the phase of the leading coefficient of $P$ is adjusted
        so that it becomes real and positive, and $(z^{-n} P, Q)$ is in the image of the NLFT."""
        raise NotImplementedError()

degree()

Returns the degree of the polynomials generated by the QSP protocol.

Source code in nlft_qsp/qsp.py
def degree(self):
    """Returns the degree of the polynomials generated by the QSP protocol."""
    raise NotImplementedError()

iX()

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iX\) on the right, where \(X\) is the Pauli matrix. This will make the generated polynomials undergo the transformation \((P, Q) \rightarrow (iQ, iP)\). This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

Raises:

Type Description
ValueError

if multiplying by \(iX\) does not preserve the subalgebra of the phase factors.

Source code in nlft_qsp/qsp.py
def iX(self):
    r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iX$ on the right, where $X$ is the Pauli matrix.
    This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (iQ, iP)$.
    This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

    Raises:
        ValueError: if multiplying by $iX$ does not preserve the subalgebra of the phase factors."""
    raise ValueError("Multiplying by iX is not possible.")

iY()

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iY\) on the right, where \(Y\) is the Pauli matrix. This will make the generated polynomials undergo the transformation \((P, Q) \rightarrow (-Q, P)\). This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

Raises:

Type Description
ValueError

if multiplying by \(iY\) does not preserve the subalgebra of the phase factors.

Source code in nlft_qsp/qsp.py
def iY(self):
    r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iY$ on the right, where $Y$ is the Pauli matrix.
    This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (-Q, P)$.
    This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

    Raises:
        ValueError: if multiplying by $iY$ does not preserve the subalgebra of the phase factors."""
    raise ValueError("Multiplying by iY is not possible.")

iZ()

Returns a new QSP protocol, obtained by multiplying the given QSP protocol by \(iZ\) on the right, where \(Z\) is the Pauli matrix. This will make the generated polynomials undergo the transformation \((P, Q) \rightarrow (iP, -iQ)\). This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

Raises:

Type Description
ValueError

if multiplying by \(iZ\) does not preserve the subalgebra of the phase factors.

Source code in nlft_qsp/qsp.py
def iZ(self):
    r"""Returns a new QSP protocol, obtained by multiplying the given QSP protocol by $iZ$ on the right, where $Z$ is the Pauli matrix.
    This will make the generated polynomials undergo the transformation $(P, Q) \rightarrow (iP, -iQ)$.
    This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.

    Raises:
        ValueError: if multiplying by $iZ$ does not preserve the subalgebra of the phase factors."""
    raise ValueError("Multiplying by iZ is not possible.")

polynomials(inf: int = 0, sup: int = -1, mode: str = 'analytic') -> tuple[Polynomial, Polynomial]

Returns the pair of polynomials \((P, Q)\) generated by the given set of phase factors. The polynomials are computed with a divide-and-conquer strategy (see here).

Parameters:

Name Type Description Default
mode str

Either 'analytic' or 'laurent', indicating whether an analytic or a Laurent QSP protocol should be composed.

'analytic'
Source code in nlft_qsp/qsp.py
def polynomials(self, inf: int = 0, sup: int = -1, mode: str = 'analytic') -> tuple[Polynomial, Polynomial]:
    """Returns the pair of polynomials $(P, Q)$ generated by the given set of phase factors.
    The polynomials are computed with a divide-and-conquer strategy (see [here](https://arxiv.org/abs/2410.06409)).

    Args:
        mode (str): Either `'analytic'` or `'laurent'`, indicating whether an analytic or a Laurent QSP protocol should be composed.
    """
    if sup < 0:
        sup = self.degree()

    Pl, Ql = self.polynomials_bounds(inf, sup)
    Pl, Ql = self.protocol_conjugation(Pl, Ql)
    match mode:
        case 'analytic': # convert from Laurent to analytic picture
            return laurent_to_analytic(Pl), laurent_to_analytic(Ql)
        case 'laurent':
            return Pl, Ql
        case _:
            raise ValueError("mode can only be 'analytic' or 'laurent'.")

polynomials_bounds(inf: int, sup: int) -> tuple[Polynomial, Polynomial]

Returns the pair of polynomials \((P, Q) = A_{inf} \tilde{v} A_{inf+1} \tilde{v} ... \tilde{v} A_{sup-1} \tilde{v} A_{sup}\), where \(\tilde{v} = \mathrm{diag}(z, z^{-1})\) is the signal operator.

Note

This assumes the Laurent picture (\(\tilde{v} = \mathrm{diag}(z, z^{-1})\)). For the analytic picture \(\tilde{w} = \mathrm{diag}(z, 1)\) use polynomials().

Source code in nlft_qsp/qsp.py
def polynomials_bounds(self, inf: int, sup: int) -> tuple[Polynomial, Polynomial]:
    r"""Returns the pair of polynomials $(P, Q) = A_{inf} \tilde{v} A_{inf+1} \tilde{v} ... \tilde{v} A_{sup-1} \tilde{v} A_{sup}$,
    where $\tilde{v} = \mathrm{diag}(z, z^{-1})$ is the signal operator.

    Note:
        This assumes the Laurent picture ($\tilde{v} = \mathrm{diag}(z, z^{-1})$). For the analytic picture $\tilde{w} = \mathrm{diag}(z, 1)$ use `polynomials()`."""
    if sup - inf < 0:
        return Polynomial([1]), Polynomial([0])
    if sup - inf <= 0:
        p, q = self.processing_operator(inf)
        p, q = self.processing_operator_conjugation(p, q)
        return Polynomial([p]), Polynomial([q])

    mid = (sup + inf) // 2
    P1, Q1 = self.polynomials_bounds(inf, mid)
    P2, Q2 = self.polynomials_bounds(mid+1, sup)

    return self.signal_operator(P1, Q1, P2, Q2) # (P1, Q1) W(z) (P2, Q2)

processing_operator(k: int)

Returns the \(k\)-th signal processing operator according to the given QSP variant.

Source code in nlft_qsp/qsp.py
def processing_operator(self, k: int):
    r"""Returns the $k$-th signal processing operator according to the given QSP variant."""
    raise NotImplementedError()

processing_operator_conjugation(a, b)

This applies a conjugation to each signal processing operator returned by processing_operator(), e.g., with the Hadamard gate. This is done mainly to make processing_operator() return the operators as expressed by the original ansatze in the papers, while keeping computational efficiency and numerical stability.

Source code in nlft_qsp/qsp.py
def processing_operator_conjugation(self, a, b):
    """This applies a conjugation to each signal processing operator returned by `processing_operator()`, e.g., with the Hadamard gate.
    This is done mainly to make `processing_operator()` return the operators as expressed by the original ansatze in the papers, while keeping computational efficiency and numerical stability."""
    return a, b

protocol_conjugation(P, Q)

Given \((P, Q)\), this applies a final conjugation to the whole protocol, e.g., with the Hadamard gate.

Source code in nlft_qsp/qsp.py
def protocol_conjugation(self, P, Q):
    r"""Given $(P, Q)$, this applies a final conjugation to the whole protocol, e.g., with the Hadamard gate."""
    return P, Q

signal_operator(P1: Polynomial, Q1: Polynomial, P2: Polynomial, Q2: Polynomial) -> tuple[Polynomial, Polynomial]

Returns the pair of polynomials given by multiplying \((P_1, Q_1) W(z) (P_2, Q_2)\), where W(z) is the signal operator.

Note

This might not reflect the signal operator as expressed in the original papers. Some basis transformations are implicitly made for computational efficiency.

Source code in nlft_qsp/qsp.py
def signal_operator(self, P1: Polynomial, Q1: Polynomial, P2: Polynomial, Q2: Polynomial) -> tuple[Polynomial, Polynomial]:
    """Returns the pair of polynomials given by multiplying $(P_1, Q_1) W(z) (P_2, Q_2)$, where `W(z)` is the signal operator.

    Note:
        This might not reflect the signal operator as expressed in the original papers. Some basis transformations are implicitly made for computational efficiency."""
    # W(z) = diag(z, z^(-1))
    zP1 = P1.shift(1)  # z*P1
    zQ1 = Q1.shift(-1) # z^(-1)*Q1
    return zP1 * P2 - zQ1 * Q2.conjugate(), zP1 * Q2 + zQ1 * P2.conjugate()

to_nlfs() -> NonLinearFourierSequence

Returns the nonlinear Fourier sequence generating \((z^{-n} P, Q)\), where \((P, Q)\) is the pair of polynomial generated by the given set of GQSP phase factors.

Note: if the phase factors are not canonical, then the phase of the leading coefficient of \(P\) is adjusted so that it becomes real and positive, and \((z^{-n} P, Q)\) is in the image of the NLFT.

Source code in nlft_qsp/qsp.py
def to_nlfs(self) -> NonLinearFourierSequence:
    """Returns the nonlinear Fourier sequence generating $(z^{-n} P, Q)$,
    where $(P, Q)$ is the pair of polynomial generated by the given set of GQSP phase factors.

    Note: if the phase factors are not canonical, then the phase of the leading coefficient of $P$ is adjusted
    so that it becomes real and positive, and $(z^{-n} P, Q)$ is in the image of the NLFT."""
    raise NotImplementedError()

QSVTPhaseFactors

Bases: ChebyshevQSPPhaseFactors

Phase factors for a QSVT/Reflection QSP protocol.

\[ e^{i\phi_0 Z} \tilde{r} e^{i\phi_1 Z} \tilde{r} \cdots \tilde{r} e^{i\phi_n Z} = \begin{pmatrix} P(x) & \cdot \\ \cdot & \cdot \end{pmatrix} \]

with the Hermitian signal operator \(\tilde{r}\):

\[ \tilde{r} = \begin{pmatrix} x & \sqrt{1 - x^2} \\ \sqrt{1 - x^2} & -x \end{pmatrix} \]
Note

This is the ansatz of Corollary 8 arXiv:1806.01838, but the polynomial construction is implemented by implicitly adjusting the phase factors from Chebyshev QSP, see arXiv:2105.02859, (A5).

Methods:

Name Description
approximate

Approximate the given callable object \(f\) (which takes \(x \in [-1, 1]\) and returns a real number)

from_chebqsp

Returns a set of QSVT phase factors constructing the same left chebyshev expansion as the given QSP protocol.

solve

Returns the set of phase factors for a QSVT protocol implementing the polynomial \(P(x)\) (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

to_chebqsp

Returns a set of Chebyshev QSP phase factors constructing the same left chebyshev expansion as the given QSVT protocol.

Source code in nlft_qsp/qsp.py
@serializable(
    type_tag="@qspx/phase_factors/qsvt",
    fields={"phi": "phi"}
)
@qsp_variant('qsvt', modes=['c'], display_name='QSVT')
class QSVTPhaseFactors(ChebyshevQSPPhaseFactors):
    r"""Phase factors for a QSVT/Reflection QSP protocol.

    $$ e^{i\phi_0 Z} \tilde{r} e^{i\phi_1 Z} \tilde{r} \cdots \tilde{r} e^{i\phi_n Z} = \begin{pmatrix} P(x) & \cdot \\ \cdot & \cdot \end{pmatrix} $$

    with the Hermitian signal operator $\tilde{r}$:

    $$ \tilde{r} = \begin{pmatrix} x & \sqrt{1 - x^2} \\ \sqrt{1 - x^2} & -x \end{pmatrix} $$

    Note:
        This is the ansatz of Corollary 8 [arXiv:1806.01838](https://arxiv.org/abs/1806.01838), but the polynomial
        construction is implemented by implicitly adjusting the phase factors from Chebyshev QSP, see [arXiv:2105.02859](https://arxiv.org/abs/2105.02859), (A5)."""
    def protocol_conjugation(self, P, Q):
        d = self.degree()
        return ((P + P.conjugate()) + (Q - Q.conjugate()))*((-1j)**(d+1)/2), \
               ((P - P.conjugate()) - (Q + Q.conjugate()))*((-1j)**d/2)

    def processing_operator_conjugation(self, a, b):
        a *= 1j # R(x) = -I*exp(I*pi*Z/4)*W(x)*exp(I*pi*Z/4)
        return np.real(a) + 1j*np.imag(b), 1j*np.imag(a) - np.real(b)

    def duplicate(self):
        return QSVTPhaseFactors(self.phi)

    @classmethod
    def from_chebqsp(cls, pf: ChebyshevQSPPhaseFactors):
        """Returns a set of QSVT phase factors constructing the same left chebyshev expansion as the given QSP protocol."""
        d = pf.degree()
        phi = [0] * (d+1)

        phi[0] = pf.phi[0] + (2*d - 1)*np.pi/4
        for k in range(1, d):
            phi[k] = pf.phi[k] - np.pi/2
        phi[d] = pf.phi[d] - np.pi/4

        return QSVTPhaseFactors(phi)

    def to_chebqsp(self) -> ChebyshevQSPPhaseFactors:
        """Returns a set of Chebyshev QSP phase factors constructing the same left chebyshev expansion as the given QSVT protocol."""
        d = self.degree()
        phi = [0] * (d+1)

        phi[0] = self.phi[0] - (2*d - 1)*np.pi/4
        for k in range(1, d):
            phi[k] = self.phi[k] + np.pi/2
        phi[d] = self.phi[d] + np.pi/4

        return ChebyshevQSPPhaseFactors(phi)

    @classmethod
    def solve(cls, T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> "QSVTPhaseFactors":
        r"""Returns the set of phase factors for a QSVT protocol implementing the polynomial $P(x)$ (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

        The target polynomial will be $T(x) = \sum_{k = 0}^n c_k T_k(x)$ (if `T` is a `ChebyshevTExpansion`) or $T(x) = \sum_{k = 0}^n c_k x^k$ (if `T` is a `Polynomial`), where $T_k(x)$ are the Chebyshev polynomials of the first kind.

        Args:
            T: a Chebyshev expansion object or the desired Polynomial $P(x)$ (that will be converted to the Chebyshev basis).

        Raises:
            ValueError: If the target polynomial does not have definite parity or is not real.

        Note:
            `T` can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases."""
        return QSVTPhaseFactors.from_chebqsp(ChebyshevQSPPhaseFactors.solve(T))

    @classmethod
    def approximate(cls, f: Callable, deg: int) -> "QSVTPhaseFactors":
        r"""Approximate the given callable object $f$ (which takes $x \in [-1, 1]$ and returns a real number)
            and returns the QSVT phase factors implementing an approximating polynomial of degree `deg`
            (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

            Note: The parity of `deg` should coincide with the parity of $f$, otherwise the Chebyshev approximator might give numerical errors."""
        return cls.solve(chebyshev_approximate(f, deg))

approximate(f: Callable, deg: int) -> QSVTPhaseFactors classmethod

Approximate the given callable object \(f\) (which takes \(x \in [-1, 1]\) and returns a real number) and returns the QSVT phase factors implementing an approximating polynomial of degree deg (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

Note: The parity of deg should coincide with the parity of \(f\), otherwise the Chebyshev approximator might give numerical errors.

Source code in nlft_qsp/qsp.py
@classmethod
def approximate(cls, f: Callable, deg: int) -> "QSVTPhaseFactors":
    r"""Approximate the given callable object $f$ (which takes $x \in [-1, 1]$ and returns a real number)
        and returns the QSVT phase factors implementing an approximating polynomial of degree `deg`
        (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

        Note: The parity of `deg` should coincide with the parity of $f$, otherwise the Chebyshev approximator might give numerical errors."""
    return cls.solve(chebyshev_approximate(f, deg))

from_chebqsp(pf: ChebyshevQSPPhaseFactors) classmethod

Returns a set of QSVT phase factors constructing the same left chebyshev expansion as the given QSP protocol.

Source code in nlft_qsp/qsp.py
@classmethod
def from_chebqsp(cls, pf: ChebyshevQSPPhaseFactors):
    """Returns a set of QSVT phase factors constructing the same left chebyshev expansion as the given QSP protocol."""
    d = pf.degree()
    phi = [0] * (d+1)

    phi[0] = pf.phi[0] + (2*d - 1)*np.pi/4
    for k in range(1, d):
        phi[k] = pf.phi[k] - np.pi/2
    phi[d] = pf.phi[d] - np.pi/4

    return QSVTPhaseFactors(phi)

solve(T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> QSVTPhaseFactors classmethod

Returns the set of phase factors for a QSVT protocol implementing the polynomial \(P(x)\) (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

The target polynomial will be \(T(x) = \sum_{k = 0}^n c_k T_k(x)\) (if T is a ChebyshevTExpansion) or \(T(x) = \sum_{k = 0}^n c_k x^k\) (if T is a Polynomial), where \(T_k(x)\) are the Chebyshev polynomials of the first kind.

Parameters:

Name Type Description Default
T list[complex_type] | Polynomial | ChebyshevTExpansion

a Chebyshev expansion object or the desired Polynomial \(P(x)\) (that will be converted to the Chebyshev basis).

required

Raises:

Type Description
ValueError

If the target polynomial does not have definite parity or is not real.

Note

T can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases.

Source code in nlft_qsp/qsp.py
@classmethod
def solve(cls, T: list[complex_type] | Polynomial | ChebyshevTExpansion) -> "QSVTPhaseFactors":
    r"""Returns the set of phase factors for a QSVT protocol implementing the polynomial $P(x)$ (as the real part of the top-left polynomial, see Theorem 9 of [arXiv:2105.02859](https://arxiv.org/abs/2105.02859)).

    The target polynomial will be $T(x) = \sum_{k = 0}^n c_k T_k(x)$ (if `T` is a `ChebyshevTExpansion`) or $T(x) = \sum_{k = 0}^n c_k x^k$ (if `T` is a `Polynomial`), where $T_k(x)$ are the Chebyshev polynomials of the first kind.

    Args:
        T: a Chebyshev expansion object or the desired Polynomial $P(x)$ (that will be converted to the Chebyshev basis).

    Raises:
        ValueError: If the target polynomial does not have definite parity or is not real.

    Note:
        `T` can also be a list of complex numbers. This will be regarded as coefficients in the Chebyshev basis. Passing the list directly is discouraged and will be removed in future releases."""
    return QSVTPhaseFactors.from_chebqsp(ChebyshevQSPPhaseFactors.solve(T))

to_chebqsp() -> ChebyshevQSPPhaseFactors

Returns a set of Chebyshev QSP phase factors constructing the same left chebyshev expansion as the given QSVT protocol.

Source code in nlft_qsp/qsp.py
def to_chebqsp(self) -> ChebyshevQSPPhaseFactors:
    """Returns a set of Chebyshev QSP phase factors constructing the same left chebyshev expansion as the given QSVT protocol."""
    d = self.degree()
    phi = [0] * (d+1)

    phi[0] = self.phi[0] - (2*d - 1)*np.pi/4
    for k in range(1, d):
        phi[k] = self.phi[k] + np.pi/2
    phi[d] = self.phi[d] + np.pi/4

    return ChebyshevQSPPhaseFactors(phi)

XQSPPhaseFactors

Bases: PhaseFactors

Phase factors for a XQSP protocol.

\[ e^{i\phi_0 X} W(z) e^{i\phi_1 X} W(z) \cdots W(z) e^{i\phi_n X} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} \]

where \(W(z) = \mathrm{diag}(z, 1)\) (mode='analytic') or \(W(z) = \mathrm{diag}(z, z^{-1})\) (mode='laurent').

Methods:

Name Description
from_nlfs

Computes the XQSP phase factors for a given imaginary NLFT sequence.

solve

Returns the set of phase factors for a XQSP protocol producing the given polynomial. See here for an overview of the QSP variants.

solve_laurent

Returns the set of phase factors for a XQSP protocol producing the given definite-parity Laurent polynomial. See here for an overview of the QSP variants.

Source code in nlft_qsp/qsp.py
@serializable(
    type_tag="@qspx/phase_factors/xqsp",
    fields={"phi": "phi"}
)
@qsp_variant('x', modes=['l', 'a'], display_name='XQSP')
class XQSPPhaseFactors(PhaseFactors):
    r"""Phase factors for a XQSP protocol.

    $$ e^{i\phi_0 X} W(z) e^{i\phi_1 X} W(z) \cdots W(z) e^{i\phi_n X} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} $$

    where $W(z) = \mathrm{diag}(z, 1)$ (`mode='analytic'`) or $W(z) = \mathrm{diag}(z, z^{-1})$ (`mode='laurent'`).
    """
    def __init__(self, phi: list[float_type]):
        self.phi = list(phi)

    def duplicate(self):
        return XQSPPhaseFactors(self.phi)

    def processing_operator(self, k: int): # exp(i phi[k] X)
        return np.cos(self.phi[k]), 1j*np.sin(self.phi[k])

    def degree(self):
        return len(self.phi) - 1

    def iX(self):
        pf = self.duplicate()
        pf.phi[-1] += np.pi/2
        return pf

    def to_nlfs(self) -> NonLinearFourierSequence:
        return NonLinearFourierSequence([1j*np.tan(phik) for phik in self.phi])

    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
        r"""Computes the XQSP phase factors for a given imaginary NLFT sequence.
        If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(z^n a, b)$ in the analytic picture.

        Args:
            F (NonLinearFourierSequence): The imaginary sequence to be converted to phase factors.

        Raises:
            ValueError: if $F$ is not imaginary.

        Note:
            The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
        if not F.is_imaginary():
            raise ValueError("The nonlinear Fourier sequence must be imaginary in order to be turned into a XQSP protocol.")

        return XQSPPhaseFactors([np.arctan(np.imag(Fk)) for Fk in F.coeffs])

    @classmethod
    def solve(cls, P: Polynomial, convention='qsp') -> "XQSPPhaseFactors":
        r"""Returns the set of phase factors for a XQSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
            A complementary $Q$ will be computed with Weiss' algorithm.

            Args:
                convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

            Raises:
                ValueError: If $P$ does not lie in the XQSP subalgebra.

            Note:
                The sup norm of $P$ should be bounded by $1 - \eta < 1$.
                The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

                The support_start of $P$ will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent XQSP, use `XQSPPhaseFactors.solve_laurent()`."""
        if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
            raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

        match convention:
            case "qsp":
                P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
            case "nlft":
                P = Polynomial(P.coeffs, 0)
            case _:
                raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

        F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

        if convention != "qsp":
            return XQSPPhaseFactors.from_nlfs(F)
        return XQSPPhaseFactors.from_nlfs(F).iX()

    @classmethod
    def solve_laurent(cls, P: Polynomial, convention='qsp') -> "XQSPPhaseFactors":
        r"""Returns the set of phase factors for a XQSP protocol producing the given definite-parity Laurent polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Raises:
            ValueError: If $P$ does not lie in the X-constrained subalgebra or $P$ has not definite-parity.

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

            The support_start of $P$ will be ignored. In order to obtain phase factors for Laurent XQSP, first convert the polynomial into analytic form."""
        if not is_definite_parity(P):
            raise ValueError("Laurent polynomial is not of definite parity.")

        return cls.solve(laurent_to_analytic(P), convention)

from_nlfs(F: NonLinearFourierSequence) -> PhaseFactors classmethod

Computes the XQSP phase factors for a given imaginary NLFT sequence. If \(\mathrm{NLFT}(F) = (a, b)\), then the returned phase factors will implement \((z^n a, b)\) in the analytic picture.

Parameters:

Name Type Description Default
F NonLinearFourierSequence

The imaginary sequence to be converted to phase factors.

required

Raises:

Type Description
ValueError

if \(F\) is not imaginary.

Note

The support start of \(F\) is ignored, so the support of \(b\) is assumed to start at \(0\).

Source code in nlft_qsp/qsp.py
@classmethod
def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
    r"""Computes the XQSP phase factors for a given imaginary NLFT sequence.
    If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(z^n a, b)$ in the analytic picture.

    Args:
        F (NonLinearFourierSequence): The imaginary sequence to be converted to phase factors.

    Raises:
        ValueError: if $F$ is not imaginary.

    Note:
        The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
    if not F.is_imaginary():
        raise ValueError("The nonlinear Fourier sequence must be imaginary in order to be turned into a XQSP protocol.")

    return XQSPPhaseFactors([np.arctan(np.imag(Fk)) for Fk in F.coeffs])

solve(P: Polynomial, convention='qsp') -> XQSPPhaseFactors classmethod

Returns the set of phase factors for a XQSP protocol producing the given polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'

Raises:

Type Description
ValueError

If \(P\) does not lie in the XQSP subalgebra.

Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

The support_start of \(P\) will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent XQSP, use XQSPPhaseFactors.solve_laurent().

Source code in nlft_qsp/qsp.py
@classmethod
def solve(cls, P: Polynomial, convention='qsp') -> "XQSPPhaseFactors":
    r"""Returns the set of phase factors for a XQSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Raises:
            ValueError: If $P$ does not lie in the XQSP subalgebra.

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

            The support_start of $P$ will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent XQSP, use `XQSPPhaseFactors.solve_laurent()`."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

    match convention:
        case "qsp":
            P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

    F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if convention != "qsp":
        return XQSPPhaseFactors.from_nlfs(F)
    return XQSPPhaseFactors.from_nlfs(F).iX()

solve_laurent(P: Polynomial, convention='qsp') -> XQSPPhaseFactors classmethod

Returns the set of phase factors for a XQSP protocol producing the given definite-parity Laurent polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'

Raises:

Type Description
ValueError

If \(P\) does not lie in the X-constrained subalgebra or \(P\) has not definite-parity.

Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

The support_start of \(P\) will be ignored. In order to obtain phase factors for Laurent XQSP, first convert the polynomial into analytic form.

Source code in nlft_qsp/qsp.py
@classmethod
def solve_laurent(cls, P: Polynomial, convention='qsp') -> "XQSPPhaseFactors":
    r"""Returns the set of phase factors for a XQSP protocol producing the given definite-parity Laurent polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
    A complementary $Q$ will be computed with Weiss' algorithm.

    Args:
        convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

    Raises:
        ValueError: If $P$ does not lie in the X-constrained subalgebra or $P$ has not definite-parity.

    Note:
        The sup norm of $P$ should be bounded by $1 - \eta < 1$.
        The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

        The support_start of $P$ will be ignored. In order to obtain phase factors for Laurent XQSP, first convert the polynomial into analytic form."""
    if not is_definite_parity(P):
        raise ValueError("Laurent polynomial is not of definite parity.")

    return cls.solve(laurent_to_analytic(P), convention)

YQSPPhaseFactors

Bases: PhaseFactors

Phase factors for a YQSP protocol.

\[ e^{i\phi_0 Y} W(z) e^{i\phi_1 Y} W(z) \cdots W(z) e^{i\phi_n Y} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} \]

where \(W(z) = \mathrm{diag}(z, 1)\) (mode='analytic') or \(W(z) = \mathrm{diag}(z, z^{-1})\) (mode='laurent').

Methods:

Name Description
from_nlfs

Computes the YQSP phase factors for a given real NLFT sequence.

solve

Returns the set of phase factors for a YQSP protocol producing the given polynomial. See here for an overview of the QSP variants.

solve_laurent

Returns the set of phase factors for a YQSP protocol producing the given definite-parity polynomial. See here for an overview of the QSP variants.

Source code in nlft_qsp/qsp.py
@serializable(
    type_tag="@qspx/phase_factors/yqsp",
    fields={"phi": "phi"}
)
@qsp_variant('y', modes=['l', 'a'], display_name='YQSP')
class YQSPPhaseFactors(PhaseFactors):
    r"""Phase factors for a YQSP protocol.

        $$ e^{i\phi_0 Y} W(z) e^{i\phi_1 Y} W(z) \cdots W(z) e^{i\phi_n Y} = \begin{pmatrix} P(z) & Q(z) \\ \cdot & \cdot \end{pmatrix} $$

        where $W(z) = \mathrm{diag}(z, 1)$ (`mode='analytic'`) or $W(z) = \mathrm{diag}(z, z^{-1})$ (`mode='laurent'`).
        """
    def __init__(self, phi: list[float_type]):
        self.phi = list(phi)

    def duplicate(self):
        return YQSPPhaseFactors(self.phi)

    def processing_operator(self, k: int): # exp(i phi[k] Y)
        return np.cos(self.phi[k]), np.sin(self.phi[k])

    def degree(self):
        return len(self.phi) - 1

    def iY(self):
        pf = self.duplicate()
        pf.phi[-1] += np.pi/2
        return pf

    def to_nlfs(self) -> NonLinearFourierSequence:
        return NonLinearFourierSequence([np.tan(phik) for phik in self.phi])

    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
        r"""Computes the YQSP phase factors for a given real NLFT sequence.
        If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(z^n a, b)$ in the analytic picture.

        Args:
            F (NonLinearFourierSequence): The real sequence to be converted to phase factors.

        Raises:
            ValueError: if $F$ is not real.

        Note:
            The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
        if not F.is_real():
            raise ValueError("The Non-Linear Fourier sequence must be real in order to be turned into a YQSP protocol.")

        return YQSPPhaseFactors([np.arctan(np.real(Fk)) for Fk in F.coeffs])

    @classmethod
    def solve(cls, P: Polynomial, convention='qsp') -> "YQSPPhaseFactors":
        r"""Returns the set of phase factors for a YQSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Raises:
            ValueError: If $P$ does not lie in the YQSP subalgebra.

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

            The support_start of $P$ will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent YQSP, use `YQSPPhaseFactors.solve_laurent()`."""
        if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
            raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

        match convention:
            case "qsp":
                P = -Polynomial(P.coeffs, 0) # (Q, -P) -> (P, Q)
            case "nlft":
                P = Polynomial(P.coeffs, 0)
            case _:
                raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

        F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

        if convention != "qsp":
            return YQSPPhaseFactors.from_nlfs(F)
        return YQSPPhaseFactors.from_nlfs(F).iY()

    @classmethod
    def solve_laurent(cls, P: Polynomial, convention='qsp') -> "YQSPPhaseFactors":
        r"""Returns the set of phase factors for a YQSP protocol producing the given definite-parity polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
        A complementary $Q$ will be computed with Weiss' algorithm.

        Args:
            convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

        Raises:
            ValueError: If $P$ does not lie in the YQSP subalgebra or $P$ has not definite-parity.

        Note:
            The sup norm of $P$ should be bounded by $1 - \eta < 1$.
            The time required by the algorithm to compute the phase factors will scale with $1/\eta$.
            """
        if not is_definite_parity(P):
            raise ValueError("Laurent polynomial is not of definite parity.")

        return cls.solve(laurent_to_analytic(P), convention)

from_nlfs(F: NonLinearFourierSequence) -> PhaseFactors classmethod

Computes the YQSP phase factors for a given real NLFT sequence. If \(\mathrm{NLFT}(F) = (a, b)\), then the returned phase factors will implement \((z^n a, b)\) in the analytic picture.

Parameters:

Name Type Description Default
F NonLinearFourierSequence

The real sequence to be converted to phase factors.

required

Raises:

Type Description
ValueError

if \(F\) is not real.

Note

The support start of \(F\) is ignored, so the support of \(b\) is assumed to start at \(0\).

Source code in nlft_qsp/qsp.py
@classmethod
def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
    r"""Computes the YQSP phase factors for a given real NLFT sequence.
    If $\mathrm{NLFT}(F) = (a, b)$, then the returned phase factors will implement $(z^n a, b)$ in the analytic picture.

    Args:
        F (NonLinearFourierSequence): The real sequence to be converted to phase factors.

    Raises:
        ValueError: if $F$ is not real.

    Note:
        The support start of $F$ is ignored, so the support of $b$ is assumed to start at $0$."""
    if not F.is_real():
        raise ValueError("The Non-Linear Fourier sequence must be real in order to be turned into a YQSP protocol.")

    return YQSPPhaseFactors([np.arctan(np.real(Fk)) for Fk in F.coeffs])

solve(P: Polynomial, convention='qsp') -> YQSPPhaseFactors classmethod

Returns the set of phase factors for a YQSP protocol producing the given polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'

Raises:

Type Description
ValueError

If \(P\) does not lie in the YQSP subalgebra.

Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

The support_start of \(P\) will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent YQSP, use YQSPPhaseFactors.solve_laurent().

Source code in nlft_qsp/qsp.py
@classmethod
def solve(cls, P: Polynomial, convention='qsp') -> "YQSPPhaseFactors":
    r"""Returns the set of phase factors for a YQSP protocol producing the given polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
    A complementary $Q$ will be computed with Weiss' algorithm.

    Args:
        convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

    Raises:
        ValueError: If $P$ does not lie in the YQSP subalgebra.

    Note:
        The sup norm of $P$ should be bounded by $1 - \eta < 1$.
        The time required by the algorithm to compute the phase factors will scale with $1/\eta$.

        The support_start of $P$ will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent YQSP, use `YQSPPhaseFactors.solve_laurent()`."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")

    match convention:
        case "qsp":
            P = -Polynomial(P.coeffs, 0) # (Q, -P) -> (P, Q)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")

    F = _riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if convention != "qsp":
        return YQSPPhaseFactors.from_nlfs(F)
    return YQSPPhaseFactors.from_nlfs(F).iY()

solve_laurent(P: Polynomial, convention='qsp') -> YQSPPhaseFactors classmethod

Returns the set of phase factors for a YQSP protocol producing the given definite-parity polynomial. See here for an overview of the QSP variants. A complementary \(Q\) will be computed with Weiss' algorithm.

Parameters:

Name Type Description Default
convention str

Whether the phase factors should produce \((P, Q)\) ('qsp'), or \((Q, P)\) ('nlft').

'qsp'

Raises:

Type Description
ValueError

If \(P\) does not lie in the YQSP subalgebra or \(P\) has not definite-parity.

Note

The sup norm of \(P\) should be bounded by \(1 - \eta < 1\). The time required by the algorithm to compute the phase factors will scale with \(1/\eta\).

Source code in nlft_qsp/qsp.py
@classmethod
def solve_laurent(cls, P: Polynomial, convention='qsp') -> "YQSPPhaseFactors":
    r"""Returns the set of phase factors for a YQSP protocol producing the given definite-parity polynomial. See [here](https://arxiv.org/abs/2503.03026) for an overview of the QSP variants.
    A complementary $Q$ will be computed with Weiss' algorithm.

    Args:
        convention (str): Whether the phase factors should produce $(P, Q)$ (`'qsp'`), or $(Q, P)$ (`'nlft'`).

    Raises:
        ValueError: If $P$ does not lie in the YQSP subalgebra or $P$ has not definite-parity.

    Note:
        The sup norm of $P$ should be bounded by $1 - \eta < 1$.
        The time required by the algorithm to compute the phase factors will scale with $1/\eta$.
        """
    if not is_definite_parity(P):
        raise ValueError("Laurent polynomial is not of definite parity.")

    return cls.solve(laurent_to_analytic(P), convention)

analytic_to_laurent(P: Polynomial, n: int = -1) -> Polynomial

Converts the given polynomial \(p_0 + p_1 z + ... + p_n z^n\) into a definite-parity Laurent polynomial \(p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n\). If \(n\) is not given, then the degree of \(P\) will be considered.

Note

No check is done on whether \(P\) is actually analytic, any negative-degree coefficients is implicitly discarded.

Source code in nlft_qsp/qsp.py
def analytic_to_laurent(P: Polynomial, n: int = -1) -> Polynomial:
    """Converts the given polynomial $p_0 + p_1 z + ... + p_n z^n$ into a definite-parity Laurent polynomial $p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n$. If $n$ is not given, then the degree of $P$ will be considered.

    Note:
        No check is done on whether $P$ is actually analytic, any negative-degree coefficients is implicitly discarded."""
    if n < 0:
        n = P.support().stop - 1

    Pl = Polynomial([0], support_start=-n)
    for k in range(n+1):
        Pl[2*k - n] = P[k]

    return Pl

chebqsp_approximate(f, deg: int) -> ChebyshevQSPPhaseFactors

DEPRECATED: use ChebyshevQSPPhaseFactors.approximate() instead.

Source code in nlft_qsp/qsp.py
def chebqsp_approximate(f, deg: int) -> ChebyshevQSPPhaseFactors:
    r"""DEPRECATED: use `ChebyshevQSPPhaseFactors.approximate()` instead."""
    return ChebyshevQSPPhaseFactors.approximate(f, deg)

chebqsp_solve(T: list[complex_type] | ChebyshevTExpansion) -> ChebyshevQSPPhaseFactors

DEPRECATED: use ChebyshevQSPPhaseFactors.solve() instead.

Source code in nlft_qsp/qsp.py
def chebqsp_solve(T: list[complex_type] | ChebyshevTExpansion) -> ChebyshevQSPPhaseFactors:
    r"""DEPRECATED: use `ChebyshevQSPPhaseFactors.solve()` instead."""
    return ChebyshevQSPPhaseFactors.solve(T)

chebyshev_to_laurent(c: list[complex_type]) -> Polynomial

Returns the Laurent polynomial equivalent to the Chebyshev expansion.

Source code in nlft_qsp/qsp.py
def chebyshev_to_laurent(c: list[complex_type]) -> Polynomial:
    """Returns the Laurent polynomial equivalent to the Chebyshev expansion."""
    P = Polynomial(c)
    return (P + P.conjugate())/2

gqsp_solve(P: Polynomial, mode='qsp') -> GQSPPhaseFactors

DEPRECATED: use GQSPPhaseFactors.solve() instead.

Source code in nlft_qsp/qsp.py
def gqsp_solve(P: Polynomial, mode='qsp') -> GQSPPhaseFactors:
    r"""DEPRECATED: use `GQSPPhaseFactors.solve()` instead."""
    return GQSPPhaseFactors.solve(P, mode)

is_definite_parity(P: Polynomial, n: int = -1) -> bool

Returns whether the polynomial has the parity of \(n\). If \(n\) is not defined, then \(n\) is the index of last coefficient of \(P\).

Source code in nlft_qsp/qsp.py
def is_definite_parity(P: Polynomial, n: int = -1) -> bool:
    r"""Returns whether the polynomial has the parity of $n$. If $n$ is not defined, then $n$ is the index of last coefficient of $P$."""
    if n < 0:
        n = P.support().stop - 1

    for k in P.support():
        if (k - n) % 2 != 0 and np.abs(P[k]) > bd.machine_threshold():
            return False

    return True

laurent_to_analytic(P: Polynomial, n: int = -1) -> Polynomial

Converts the given definite-parity Laurent polynomial \(p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n\) into an analytic polynomial \(p_0 + p_1 z + ... + p_n z^n\). If \(n\) is not given, then \(n\) = the effective degree of \(P\).

Note

No check is done on whether \(P\) is of definite-parity. The other coefficients are implicitly discarded.

Source code in nlft_qsp/qsp.py
def laurent_to_analytic(P: Polynomial, n: int = -1) -> Polynomial:
    """Converts the given definite-parity Laurent polynomial $p_0 z^{-n} + p_1 z^{-n+2} + ... + p_n z^n$ into an analytic polynomial $p_0 + p_1 z + ... + p_n z^n$. If $n$ is not given, then $n$ = the effective degree of $P$.

    Note:
        No check is done on whether $P$ is of definite-parity. The other coefficients are implicitly discarded.
    """
    if n < 0:
        n = max(abs(P.support().start), abs(P.support().stop - 1))

    return Polynomial([P[2*k - n] for k in range(n+1)])

phase_prefactor(F: complex_type) -> float_type

Computes the phase prefactor for the nonlinear Fourier sequence coefficient \(F\).

Source code in nlft_qsp/qsp.py
def phase_prefactor(F: complex_type) -> float_type:
    """Computes the phase prefactor for the nonlinear Fourier sequence coefficient $F$."""

    if np.abs(F) < bd.machine_threshold():
        return 0

    if np.abs(np.imag(F)) < bd.machine_threshold():
        return -(np.pi/4)

    return -np.arctan(np.real(F)/np.imag(F))/2

qsp_variant(variant_tag: str, modes: list, display_name: str)

Class decorator to register QSP variants. Modes are [a]nalytic, [l]aurent, [c]hebyshev.

Source code in nlft_qsp/qsp.py
def qsp_variant(variant_tag: str, modes: list, display_name: str):
    """Class decorator to register QSP variants. Modes are [a]nalytic, [l]aurent, [c]hebyshev."""

    def decorator(cls: Type[T]) -> Type[T]:
        cls._variant_tag = variant_tag
        cls._variant_modes = modes
        cls._variant_display_name = display_name
        VARIANTS_REGISTRY[variant_tag] = cls
        return cls

    return decorator

qsvt_approximate(f, deg: int) -> QSVTPhaseFactors

DEPRECATED: use QSVTPhaseFactors.approximate() instead.

Source code in nlft_qsp/qsp.py
def qsvt_approximate(f, deg: int) -> QSVTPhaseFactors:
    r"""DEPRECATED: use `QSVTPhaseFactors.approximate()` instead."""
    return QSVTPhaseFactors.approximate(f, deg)

qsvt_solve(T: list[complex_type] | ChebyshevTExpansion) -> QSVTPhaseFactors

DEPRECATED: use QSVTPhaseFactors.solve() instead.

Source code in nlft_qsp/qsp.py
def qsvt_solve(T: list[complex_type] | ChebyshevTExpansion) -> QSVTPhaseFactors:
    r"""DEPRECATED: use `QSVTPhaseFactors.solve()` instead."""
    return QSVTPhaseFactors.solve(T)

xqsp_solve(P: Polynomial, mode='qsp') -> XQSPPhaseFactors

DEPRECATED: use XQSPPhaseFactors.solve() instead.

Source code in nlft_qsp/qsp.py
def xqsp_solve(P: Polynomial, mode='qsp') -> XQSPPhaseFactors:
    r"""DEPRECATED: use `XQSPPhaseFactors.solve()` instead."""
    return XQSPPhaseFactors.solve(P, mode)

xqsp_solve_laurent(P: Polynomial, mode='qsp') -> XQSPPhaseFactors

DEPRECATED: use XQSPPhaseFactors.solve_laurent() instead.

Source code in nlft_qsp/qsp.py
def xqsp_solve_laurent(P: Polynomial, mode='qsp') -> XQSPPhaseFactors:
    r"""DEPRECATED: use `XQSPPhaseFactors.solve_laurent()` instead."""
    return XQSPPhaseFactors.solve_laurent(P, mode)

yqsp_solve(P: Polynomial, mode='qsp') -> YQSPPhaseFactors

DEPRECATED: use YQSPPhaseFactors.solve() instead.

Source code in nlft_qsp/qsp.py
def yqsp_solve(P: Polynomial, mode='qsp') -> YQSPPhaseFactors:
    r"""DEPRECATED: use `YQSPPhaseFactors.solve()` instead."""
    return YQSPPhaseFactors.solve(P, mode)

yqsp_solve_laurent(P: Polynomial, mode='qsp') -> YQSPPhaseFactors

DEPRECATED: use YQSPPhaseFactors.solve_laurent() instead.

Source code in nlft_qsp/qsp.py
def yqsp_solve_laurent(P: Polynomial, mode='qsp') -> YQSPPhaseFactors:
    r"""DEPRECATED: use `YQSPPhaseFactors.solve_laurent()` instead."""
    return YQSPPhaseFactors.solve_laurent(P, mode)