Source code for qrisp.algorithms.shor.shors_algorithm

# ********************************************************************************
# * 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 Shor's integer factorization algorithm via quantum order finding."""

import math
from collections.abc import Callable
from typing import TYPE_CHECKING

from sympy import Rational, continued_fraction_convergents, continued_fraction_iterator

from qrisp import QuantumFloat, QuantumModulus, control, h
from qrisp.alg_primitives import QFT

if TYPE_CHECKING:
    from qrisp.interface.measurement_result import DecodedMeasurementResult
from qrisp.alg_primitives.arithmetic.modular_arithmetic import find_optimal_m, modinv

depths = []
cnot_count = []
qubits = []


def _find_optimal_a(N: int) -> list[int]:
    # N.bit_length() would be off by one for N an exact power of two; this
    # is the exact-integer equivalent of ceil(log2(N)), safe for N far
    # beyond float64/int64 range (unlike np.log2/np.ceil).
    n = (N - 1).bit_length() if N > 1 else 0
    proposals = []

    # Search through the first O(1) possibilities to find a good a
    for a in range(2, min(100, N - 1)):
        # We only append non-trivial proposals
        if math.gcd(a, N) == 1:
            proposals.append(a)

    cost_dic = {}
    for a in proposals:
        m_values = []
        for k in range(2 * n + 1):
            inpl_multiplier = pow(a, 2**k, N)

            if inpl_multiplier == 1:
                continue

            # find_optimal_m is a function that determines the lowest possible
            # Montgomery shift for a given number. The higher the montgomery shift,
            # the more qubits and the more effort is needed.
            m_values.append(find_optimal_m(inpl_multiplier, N))
            m_values.append(find_optimal_m(modinv((-inpl_multiplier) % N, N), N))

        cost_dic[a] = sum(m_values) + max(m_values) * 1e-5

    proposals.sort(key=lambda a: cost_dic[a])

    return proposals


def _find_order(a: int, N: int, inpl_adder: Callable | None = None, mes_kwargs: dict | None = None) -> int:
    if mes_kwargs is None:
        mes_kwargs = {}
    orig_a = a
    qg = QuantumModulus(N, inpl_adder)
    qg[:] = 1
    qpe_res = QuantumFloat(2 * qg.size + 1, exponent=-(2 * qg.size + 1))
    h(qpe_res)
    for qb in qpe_res:
        with control(qb):
            qg *= a
            a = (a * a) % N
    QFT(qpe_res, inv=True, inpl_adder=inpl_adder)

    mes_res = qpe_res.get_measurement(**mes_kwargs)

    return _extract_order(mes_res, orig_a, N)


def _extract_order(mes_res: "DecodedMeasurementResult", a: int, N: int) -> int:
    # Caps the accumulated-candidate search so it stays polynomial (not
    # exponential) in the number of outcomes examined, even adversarially.
    # Two or three independent QPE measurements are, in practice, almost
    # always enough to recover the true order.
    max_candidates = 64

    accumulated_r_values: list[int] = []

    approximations = list(mes_res.keys())

    try:
        approximations.remove(0)
    except ValueError:
        pass

    while approximations:
        r_values = _get_r_values(approximations.pop(0))

        for r in r_values:
            if pow(a, r, N) == 1:
                return r

        # Combine this outcome's candidates with every LCM accumulated from
        # all outcomes seen so far, so an order that only emerges from
        # combining three or more outcomes (e.g. 30 = lcm(2, 3, 5), where no
        # pairwise combination alone divides 30) is still found.
        new_candidates = []
        for prev_r in accumulated_r_values:
            for r in r_values:
                combined = math.lcm(r, prev_r)
                if pow(a, combined, N) == 1:
                    return combined
                if combined not in accumulated_r_values and combined not in new_candidates:
                    new_candidates.append(combined)

        accumulated_r_values.extend(r_values)
        accumulated_r_values.extend(new_candidates)
        if len(accumulated_r_values) > max_candidates:
            accumulated_r_values = accumulated_r_values[-max_candidates:]

    # Noisy or finite-shot measurements can produce outcomes that never yield
    # a valid order; let the caller decide whether to retry with another base
    # instead of crashing on a bare IndexError from the exhausted list.
    raise RuntimeError(f"could not recover the order of {a} mod {N} from the given measurement outcomes")


def _get_r_values(approx: int | float) -> list[int]:
    rationals = continued_fraction_convergents(continued_fraction_iterator(Rational(approx)))
    return [rat.q for rat in rationals if 1 < rat.q]


[docs] def shors_alg(N: int, inpl_adder: Callable | None = None, mes_kwargs: dict | None = None) -> int: """Performs `Shor's factorization algorithm <https://arxiv.org/abs/quant-ph/9508027>`_ on a given integer N. The adder used for factorization can be customized. To learn more about this feature, please read :ref:`QuantumModulus` Parameters ---------- N : integer The integer to be factored. inpl_adder : callable, optional A function that performs in-place addition. The default is None. mes_kwargs : dict, optional A dictionary of keyword arguments for :meth:`get_measurement <qrisp.QuantumVariable.get_measurement>`. This especially allows you to specify an execution backend. The default is {}. Returns ------- res : integer A factor of N. Examples -------- We factor 65: >>> from qrisp.shor import shors_alg >>> shors_alg(65) 5 """ if not N % 2: return 2 if mes_kwargs is None: mes_kwargs = {} a_proposals = _find_optimal_a(N) for a in a_proposals: K = math.gcd(a, N) if K != 1: res = K break try: r = _find_order(a, N, inpl_adder, mes_kwargs) except RuntimeError: continue if r % 2: continue # gcd(x, N) == gcd(x mod N, N) for any integer x, so reducing a**(r//2) # modulo N before the +1 is mathematically exact and avoids computing # a**(r//2) in full precision, which can be an enormous number for # large r. g = math.gcd(pow(a, r // 2, N) + 1, N) if g not in [N, 1]: res = g break else: raise RuntimeError( f"Shor's algorithm failed to find a nontrivial factor of {N} using the given candidate bases" ) return res