Algorithm Search⌘K

AKS primality test in Python

The first unconditional deterministic polynomial-time primality test. It checks (X + a)^n ≡ X^n + a modulo X^r - 1 and n for a few a and one small r.

File
aks.py, 169 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
aks.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aks-primality-test/. The authors are listed in prov/JR8RT8.jsonl.

"""The AKS primality test (Agrawal, Kayal and Saxena, 2004 version).

aks(n) returns True exactly when n is prime. It is deterministic, never
wrong, and runs in time polynomial in log n with no unproved hypothesis.

Cost, with lg n binary digits: r = O((lg n)^5) by the AKS bound, and in
practice r is a little above (lg n)^2. Step L8 is run for at most
sqrt(phi(r)) lg n values of a, and each needs O(lg n) products of polynomials
of degree below r with coefficients modulo n. With fast multiplication that
is O~(r^(3/2) (lg n)^3) bit operations, O~((lg n)^(21/2)) in the worst case.
Integers are of arbitrary precision (Python int). Polynomial products use
Kronecker substitution: the coefficients are packed into one big integer.

The logarithms lg n = log2 n are computed with 60 decimal digits. For n not
a power of 2 the numbers (lg n)^2 and sqrt(phi(r)) lg n are not integers, so
the floors below are exact at this precision for every n in practical range.
"""

import decimal
import math


def integer_root(n: int, b: int) -> int:
    """The largest x >= 0 with x^b <= n, for n >= 0 and b >= 1 (Newton's method)."""
    if n < 0 or b < 1:
        raise ValueError("need n >= 0 and b >= 1")
    if n < 2 or b == 1:
        return n
    x = 1 << ((n.bit_length() + b - 1) // b)  # x^b > n
    while True:
        y = ((b - 1) * x + n // x ** (b - 1)) // b
        if y >= x:
            break
        x = y
    while x**b > n:
        x -= 1
    while (x + 1) ** b <= n:
        x += 1
    return x


def is_perfect_power(n: int) -> bool:
    """True when n = a^b for integers a >= 2 and b >= 2."""
    if n < 4:
        return False
    for b in range(2, n.bit_length() + 1):
        if integer_root(n, b) ** b == n:
            return True
    return False


def _lg(n: int) -> decimal.Decimal:
    with decimal.localcontext() as ctx:
        ctx.prec = 60
        return decimal.Decimal(n).ln() / decimal.Decimal(2).ln()


def lg_squared_floor(n: int) -> int:
    """floor((log2 n)^2) for n >= 1."""
    if n & (n - 1) == 0:
        k = n.bit_length() - 1
        return k * k
    with decimal.localcontext() as ctx:
        ctx.prec = 60
        return int(_lg(n) ** 2)


def euler_phi(r: int) -> int:
    """Euler's totient of r >= 1, by trial division."""
    result, m, d = r, r, 2
    while d * d <= m:
        if m % d == 0:
            while m % d == 0:
                m //= d
            result -= result // d
        d += 1
    if m > 1:
        result -= result // m
    return result


def smallest_r(n: int) -> int:
    """The least r >= 2 with gcd(r, n) = 1 and multiplicative order o_r(n) > (log2 n)^2."""
    k_max = lg_squared_floor(n)  # o_r(n) > (lg n)^2 means o_r(n) > k_max
    r = max(2, k_max + 2)  # o_r(n) <= phi(r) <= r - 1, so smaller r cannot work
    while True:
        if math.gcd(r, n) == 1:
            x, k = 1, 0
            while k < k_max:
                x = x * n % r
                k += 1
                if x == 1:
                    break
            else:
                return r
        r += 1


def step_bound(n: int, r: int) -> int:
    """floor(sqrt(phi(r)) * log2 n)."""
    phi = euler_phi(r)
    if n & (n - 1) == 0:
        k = n.bit_length() - 1
        return math.isqrt(phi * k * k)
    with decimal.localcontext() as ctx:
        ctx.prec = 60
        return int(decimal.Decimal(phi).sqrt() * _lg(n))


def _pack(f: list[int], width: int) -> int:
    """One integer with one width-byte slot per coefficient (Kronecker substitution)."""
    return int.from_bytes(b"".join(c.to_bytes(width, "little") for c in f), "little")


def _square_mod(f: list[int], n: int, width: int) -> list[int]:
    """f^2 mod (X^r - 1, n), for a list f of r coefficients in [0, n)."""
    r = len(f)
    p = _pack(f, width)
    raw = (p * p).to_bytes(2 * r * width, "little")
    c = [int.from_bytes(raw[i * width : (i + 1) * width], "little") for i in range(2 * r)]
    return [(c[i] + c[i + r]) % n for i in range(r)]  # X^(r+i) = X^i


def _times_x_plus_a(f: list[int], a: int, n: int) -> list[int]:
    """f * (X + a) mod (X^r - 1, n)."""
    r = len(f)
    return [(a * f[i] + f[i - 1]) % n for i in range(r)]  # f[-1] wraps from X^(r-1)


def power_x_plus_a(a: int, n: int, r: int) -> list[int]:
    """The coefficients of (X + a)^n mod (X^r - 1, n), for n >= 2 and r >= 2.

    Left-to-right binary powering. A slot of the packed square holds a sum of
    at most r products of two numbers below n, so it fits in the width.
    """
    width = (r * (n - 1) ** 2).bit_length() // 8 + 1
    f = [0] * r
    f[0] = a % n
    f[1] = 1
    for bit in bin(n)[3:]:
        f = _square_mod(f, n, width)
        if bit == "1":
            f = _times_x_plus_a(f, a, n)
    return f


def aks(n: int) -> bool:
    """Return True if n is prime and False otherwise (AKS)."""
    if n < 2:  # @line L1
        return False
    if is_perfect_power(n):  # @line L2
        return False
    r = smallest_r(n)  # @line L3
    for a in range(2, min(r, n - 1) + 1):  # @line L4
        if 1 < math.gcd(a, n) < n:
            return False
    if n <= r:  # @line L5
        return True
    ell = step_bound(n, r)  # @line L6
    for a in range(1, ell + 1):  # @line L7
        target = [0] * r
        target[0] = a % n
        target[n % r] = (target[n % r] + 1) % n
        if power_x_plus_a(a, n, r) != target:  # @line L8
            return False
    return True  # @line L9
aks_primality_test.py 26 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aks-primality-test/. The authors are listed in prov/JR8RT8.jsonl.

"""Entry point for the AKS primality test; the code is in aks.py."""

from aks import (
    aks,
    euler_phi,
    integer_root,
    is_perfect_power,
    lg_squared_floor,
    power_x_plus_a,
    smallest_r,
    step_bound,
)

__all__ = [
    "aks",
    "euler_phi",
    "integer_root",
    "is_perfect_power",
    "lg_squared_floor",
    "power_x_plus_a",
    "smallest_r",
    "step_bound",
]
test_aks_primality_test.py 145 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aks-primality-test/. The authors are listed in prov/JR8RT8.jsonl.

import math
import random

from aks_primality_test import (
    aks,
    euler_phi,
    integer_root,
    is_perfect_power,
    lg_squared_floor,
    power_x_plus_a,
    smallest_r,
    step_bound,
)


def is_prime_td(n: int) -> bool:
    if n < 2:
        return False
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d += 1
    return True


def naive_power(a: int, n: int, r: int) -> list[int]:
    """(X + a)^n mod (X^r - 1, n) by n multiplications by X + a."""
    f = [0] * r
    f[0] = 1 % n
    for _ in range(n):
        g = [0] * r
        for i, c in enumerate(f):
            g[i] = (g[i] + a * c) % n
            g[(i + 1) % r] = (g[(i + 1) % r] + c) % n
        f = g
    return f


def order(n: int, r: int) -> int:
    x, k = n % r, 1
    while x != 1:
        x = x * n % r
        k += 1
    return k


def test_small_against_trial_division() -> None:
    for n in range(0, 1000):
        assert aks(n) == is_prime_td(n), n


def test_special_inputs() -> None:
    assert [aks(n) for n in (0, 1, 2, 3, 4)] == [False, False, True, True, False]
    for n in (2**10, 3**7, 5**5, 7**4, 101**2, 1009**3, 2**31 - 2, 10**6):
        assert not aks(n), n  # even numbers and prime powers
    for n in (561, 1105, 1729, 2465, 2821, 6601, 8911, 41041, 62745, 825265):
        assert not aks(n), n  # Carmichael numbers
    for n in (2047, 3277, 4033, 4681, 8321, 1373653, 25326001, 3215031751):
        assert not aks(n), n  # strong pseudoprimes to small bases
    for n in (7919, 65537, 104729, 1000003):
        assert aks(n), n


def test_random_against_trial_division() -> None:
    rng = random.Random(1)
    for _ in range(20):
        n = rng.randrange(2, 2 * 10**5)
        assert aks(n) == is_prime_td(n), n


def test_large_composites() -> None:
    # No factor below r and not perfect powers: rejected at line L8.
    for n in (
        (10**9 + 7) * (10**9 + 9),
        3825123056546413051,
    ):
        assert not aks(n), n
    rng = random.Random(2)
    primes = [p for p in range(10**5, 10**5 + 3000) if is_prime_td(p)]
    for _ in range(5):
        p, q = rng.sample(primes, 2)
        assert not aks(p * q), (p, q)


def test_congruence_holds_for_large_primes() -> None:
    # A full run on these primes is too slow in Python; check some values of a.
    for p, values in ((10**12 + 39, (1, 2, 3)), (2**61 - 1, (1,))):
        r = smallest_r(p)
        for a in values:
            f = power_x_plus_a(a, p, r)
            target = [0] * r
            target[0] = a
            target[p % r] += 1
            assert f == target


def test_huge_perfect_powers() -> None:
    # Numbers of several hundred digits stop at line L2 before the search for r.
    p = 2**521 - 1  # a Mersenne prime of 157 digits
    q = 10**100 + 267  # a prime of 101 digits
    for n in (p**2, p**3, q**5, (p * q) ** 2, 3**700):
        assert not aks(n)


def test_helpers() -> None:
    rng = random.Random(3)
    for _ in range(300):
        n, r = rng.randrange(2, 300), rng.randrange(2, 30)
        a = rng.randrange(0, n)
        assert power_x_plus_a(a, n, r) == naive_power(a, n, r), (a, n, r)
    for n in range(0, 3000):
        for b in (1, 2, 3, 5):
            x = integer_root(n, b)
            assert x**b <= n < (x + 1) ** b
    big = rng.randrange(10**50, 10**51)
    assert integer_root(big**7, 7) == big and integer_root(big**7 - 1, 7) == big - 1
    assert is_perfect_power(big**3) and not is_perfect_power(big**3 + 1)
    assert [n for n in range(50) if is_perfect_power(n)] == [4, 8, 9, 16, 25, 27, 32, 36, 49]
    for r in range(1, 500):
        assert euler_phi(r) == sum(1 for i in range(1, r + 1) if math.gcd(i, r) == 1)
    assert lg_squared_floor(2**61 - 1) == 3720  # floats round lg n up to 61 here
    assert lg_squared_floor(2**61) == 3721
    for n in range(2, 3000):
        assert lg_squared_floor(n) == math.floor(math.log2(n) ** 2)
        r = smallest_r(n)
        k = lg_squared_floor(n)
        assert math.gcd(r, n) == 1 and order(n, r) > k
        assert all(math.gcd(s, n) > 1 or order(n, s) <= k for s in range(2, r))
        if n > r:
            assert step_bound(n, r) == math.floor(math.sqrt(euler_phi(r)) * math.log2(n))


def test_worked_example() -> None:
    assert smallest_r(31) == 29 and step_bound(31, 29) == 26
    for a in range(1, 27):
        f = power_x_plus_a(a, 31, 29)
        assert f == [a, 0, 1] + [0] * 26  # X^31 = X^2 mod X^29 - 1
    n = 74513  # 269 * 277, the least composite that reaches line L8
    assert smallest_r(n) == 263 and step_bound(n, 263) == 261 and n % 263 == 84
    f = power_x_plus_a(1, n, 263)
    assert f[:3] == [54070, 51917, 12374] and all(f)

What the next level needs

  • the tests did not run in this build

Read the full page about AKS primality test