Source code for qrisp.alg_primitives.iterative_qpe

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

"""Implements the iterative quantum phase estimation (IQPE) algorithm using a single ancilla qubit."""

# Step j of the iterative (or semiclassical) phase estimation applies U^(2^(t-1-j))
# controlled on a fresh qubit, removes the phase of the bits measured before with a
# rotation, and measures bit j of the outcome y, from the least significant one. The
# estimated phase is y / 2**t, and the outcome has the same distribution as the
# measurement of a t-qubit phase register after the inverse QFT (Griffiths and Niu,
# Phys. Rev. Lett. 76, 3228, 1996).

from __future__ import annotations

import operator
from collections.abc import Callable
from typing import Any

import jax.numpy as jnp
import numpy as np
from jax import Array
from jax.core import Tracer

from qrisp import QuantumBool, control, h, measure, reset, rz
from qrisp.alg_primitives.arithmetic.jasp_arithmetic.jasp_bigintiger import DTYPE
from qrisp.alg_primitives.arithmetic.jasp_arithmetic.jasp_mod_tools import _LIMB_BITS
from qrisp.jasp import jrange, q_fori_loop

# The module is star-imported into qrisp, which should not receive its imports
__all__ = ["IQPE"]

# IQPE applies U up to 2**(precision - 1) times, counted with 64-bit integers
_MAX_IQPE_PRECISION = 63


def _semiclassical_phase_estimation(
    args: Any,
    apply_power: Callable[[Any, Array], None],
    precision: int | Tracer,
    ctrl_method: str | None = None,
    outcome_limbs: bool = False,
) -> tuple[Array, Array]:
    """Estimate the phase of a unitary ``U`` with one control qubit at a time, measured after every step.

    Parameters
    ----------
    args : Any
        The arguments on which ``U`` acts, passed on to ``apply_power``.
    apply_power : Callable
        ``apply_power(args, k)`` applies ``U`` to the power ``2**k``, for an integer
        ``k`` that is traced.
    precision : int or Tracer
        The number ``t`` of bits of the outcome.
    ctrl_method : str, optional
        The method of the controlled ``U``, see :meth:`.control <qrisp.Operation.control>`.
        The default is None.
    outcome_limbs : bool, optional
        If True, the outcome ``y`` is also assembled into its ``ceil(t / 32)`` 32-bit
        limbs, which requires a precision that is not traced. The default is False,
        which computes only the estimated phase.

    Returns
    -------
    tuple
        The estimated phase ``y / 2**t`` as a float, exact for up to 53 bits, and the
        limbs of the outcome ``y`` (the digits of a BigInteger), of dtype ``uint32``,
        or no limbs if ``outcome_limbs`` is False.

    Raises
    ------
    TypeError
        If ``outcome_limbs`` is True and the precision is traced or not an integer.

    """
    # The number of limbs sets the shape of an array, so it must be known while tracing
    num_limbs = -(-operator.index(precision) // _LIMB_BITS) if outcome_limbs else 0

    def step(j: Array, carry: tuple) -> tuple:
        """Measure bit ``j`` of the outcome.

        Parameters
        ----------
        j : Array
            The step, from 0 to ``precision - 1``.
        carry : tuple
            The phase of the bits measured before (as a fraction of a turn), the
            arguments of ``U`` and the limbs of the outcome.

        Returns
        -------
        tuple
            The carry after the step.

        """
        phase, args, digits = carry
        control_qubit = QuantumBool()
        h(control_qubit)
        with control(control_qubit[0], ctrl_method=ctrl_method):
            apply_power(args, precision - 1 - j)
        # For a phase y / 2**t, the power 2**(t - 1 - j) gives the phase 0.y_j y_(j-1) ... y_0
        # in binary, and ``phase`` holds 0.y_(j-1) ... y_0. The rotation removes half of it,
        # which leaves the phase y_j / 2: the Hadamard gate maps it to |y_j>. At step 0
        # the angle is 0: the rotation is kept, so that every step has the same circuit.
        rz(-np.pi * phase, control_qubit)
        h(control_qubit)
        bit = measure(control_qubit)
        # A released qubit must be in |0>, since interpreters that reuse it (such as the
        # Catalyst and static-register interpreters) do not reset it
        reset(control_qubit)
        control_qubit.delete()

        if num_limbs:
            limb = j // _LIMB_BITS
            shift = jnp.asarray(j % _LIMB_BITS, dtype=DTYPE)
            digits = digits.at[limb].set(digits[limb] | (jnp.asarray(bit, dtype=DTYPE) << shift))
        return ((phase + bit) / 2, args, digits)

    initial = (jnp.asarray(0.0), args, jnp.zeros(num_limbs, dtype=DTYPE))
    phase, _, digits = q_fori_loop(0, precision, step, initial)
    return phase, digits


# The public signature keeps its six parameters, which callers may pass positionally
[docs] def IQPE( # noqa: PLR0913, PLR0917 args: Any, U: Callable, precision: int | Tracer, iter_spec: bool = False, ctrl_method: str | None = None, kwargs: dict | None = None, ) -> Array: r"""Estimate the phase of a unitary with the iterative quantum phase estimation algorithm. The algorithm is described in `this paper <https://arxiv.org/pdf/quant-ph/0610214>`_. The unitary to estimate is expected to be given as Python function, which is called on ``args``. Instead of a variable of ``precision`` qubits, the algorithm uses a single control qubit at a time. Step $j$ applies $U^{2^{t - 1 - j}}$ controlled on a fresh qubit, where $t$ is the precision, corrects the phase of the bits measured before, and measures bit $j$ of the estimate, from the least significant one. Parameters ---------- args : Any The state on which the phase estimation is performed, for example a QuantumVariable or a list of QuantumVariables. It is passed to ``U`` as its first argument. U : Callable The unitary, as a Python function that is called as ``U(args, **kwargs)``, with the keyword ``iter`` as well if ``iter_spec`` is True. precision : int or Tracer The precision of the estimation. iter_spec : bool, optional If set to ``True``, ``U`` will be called with the additional keyword ``iter = i`` where ``i`` is the amount of iterations to perform (instead of simply calling ``U`` for ``i`` times). The default is False. ctrl_method : str, optional Allows to specify which method should be used to generate the controlled U circuit. For more information check :meth:`.control <qrisp.Operation.control>`. The default is None. kwargs : dict, optional A dictionary of keyword arguments to pass to ``U``. The default is None, which passes no keyword arguments. Returns ------- float The estimated phase as a fraction of $2 \pi$. Raises ------ TypeError If ``precision`` is not an integer. ValueError If ``precision`` is larger than 63. A traced precision, for example an argument of a jaspified function, is not checked, since its value is only known when the program runs. Notes ----- Without ``iter_spec``, the first step calls ``U`` $2^{t-1}$ times and the last one once, $2^t - 1$ calls in total. The number of iterations $2^{t-1}$ is a 64-bit integer, also with ``iter_spec``, so the precision is at most 63. The estimate is a float, exact for a precision of up to 53 bits. For the order finding of Shor's algorithm with thousands of bits, see :func:`~qrisp.shor.semiclassical_order_finding`. Examples -------- We define a function that applies two rotations onto its input and estimate the applied phase: >>> import numpy as np >>> from qrisp import IQPE, QuantumFloat, h, rx, x >>> from qrisp.jasp import make_jaspr >>> def f(): ... def U(qv): ... rx(2 * np.pi / 2**3, qv[0]) ... rx(2 * np.pi / 2**2, qv[1]) ... qv = QuantumFloat(2) ... x(qv) ... h(qv) ... return IQPE(qv, U, precision=4) >>> jaspr = make_jaspr(f)() Each qubit is in the state $\lvert - \rangle$, on which $R_X(2 \pi x)$ has the eigenvalue $e^{i \pi x}$, so the phase is $(1/8 + 1/4) / 2 = 0.1875$: >>> jaspr() # doctest: +SKIP Array(0.1875, dtype=float64) """ # A traced precision is only known when the program runs, so it cannot be checked here if not isinstance(precision, Tracer) and operator.index(precision) > _MAX_IQPE_PRECISION: raise ValueError(f"The precision of IQPE is at most {_MAX_IQPE_PRECISION}, got {precision}") kwargs = {} if kwargs is None else kwargs def apply_power(args: Any, k: Array) -> None: """Apply ``U`` to the power ``2**k``. Parameters ---------- args : Any The arguments of ``U``. k : Array The exponent of the power of two. """ if iter_spec: U(args, iter=2**k, **kwargs) else: for _ in jrange(2**k): U(args, **kwargs) return _semiclassical_phase_estimation(args, apply_power, precision, ctrl_method=ctrl_method)[0]