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