Quadratic sieve in Python
Finds the smooth values of f(x) = x^2 - N near sqrt(N) with a sieve, as for primes, and combines them mod 2 into a congruence of squares. Heuristically L_N[1/2, 1].
- File
quadratic_sieve.py, 238 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/quadratic-sieve/. The authors are listed in prov/5TWHZJ.jsonl.
from math import exp, gcd, isqrt, log, log2
def primes_up_to(b: int) -> list[int]:
"""The primes p <= b, by the sieve of Eratosthenes in O(b log log b) time."""
if b < 2:
return []
sieve = bytearray([1]) * (b + 1)
sieve[0] = sieve[1] = 0
for i in range(2, isqrt(b) + 1):
if sieve[i]:
sieve[i * i :: i] = bytearray(len(range(i * i, b + 1, i)))
return [i for i in range(b + 1) if sieve[i]]
def sqrt_mod(a: int, p: int) -> int:
"""A square root of a modulo an odd prime p, for a quadratic residue a, by Tonelli-Shanks."""
a %= p
if a == 0:
return 0
q, s = p - 1, 0
while q % 2 == 0:
q, s = q // 2, s + 1
z = 2
while pow(z, (p - 1) // 2, p) != p - 1:
z += 1
m, c, t, r = s, pow(z, q, p), pow(a, q, p), pow(a, (q + 1) // 2, p)
while t != 1:
i, t2 = 0, t
while t2 != 1:
t2, i = t2 * t2 % p, i + 1
b = pow(c, 1 << (m - i - 1), p)
m, c, t, r = i, b * b % p, t * b * b % p, r * b % p
return r
def exponents(v: int, base: list[int]) -> list[int] | None:
"""The exponents of v over the factor base, or None if v is not smooth."""
e = [0] * len(base)
if v < 0:
e[0], v = 1, -v
for j in range(1, len(base)):
p = base[j]
while v % p == 0:
v //= p
e[j] += 1
return e if v == 1 else None
def dependencies(vectors: list[int]) -> list[int]:
"""Subsets (as bit masks over the rows) whose vectors add to zero over GF(2),
by Gaussian elimination on bit masks. One subset per linearly dependent row."""
pivots: dict[int, tuple[int, int]] = {}
found = []
for j, v in enumerate(vectors):
h = 1 << j
while v:
top = v.bit_length() - 1
if top not in pivots:
pivots[top] = (v, h)
break
pv, ph = pivots[top]
v, h = v ^ pv, h ^ ph
if v == 0:
found.append(h)
return found
def default_bound(n: int) -> int:
"""The smoothness bound 3 L_n[1/2, 1/2] = 3 exp((1/2) (ln n ln ln n)^(1/2)), at least 30.
The factor 3 suits this implementation, which tests candidates by trial division."""
ln = log(n)
return max(30, int(3 * exp(0.5 * (ln * log(ln)) ** 0.5)))
def quadratic_sieve(n: int, bound: int = 0, block: int = 0, extra: int = 10, blocks: int = 0) -> int:
"""Pomerance's quadratic sieve with the single polynomial f(t) = (s + t)^2 - n.
Precondition: n >= 3 odd and not a perfect square. The factor base holds
-1, 2 and the odd primes p <= bound with (n/p) = 1, extended past bound
until it has at least 16 entries. It sieves blocks of
`block` values of t on both sides of 0, with s = isqrt(n), until it has
len(base) + extra smooth values f(t), or `blocks` pairs of blocks; then
combines them by linear algebra over GF(2).
Returns a proper factor of n, or 1 on failure (always 1 for a prime n).
Cost: heuristically L_n[1/2, 1 + o(1)] with bound about L_n[1/2, 1/2].
"""
if n < 3 or n % 2 == 0:
raise ValueError("need an odd n >= 3")
s = isqrt(n)
if s * s == n:
raise ValueError("n must not be a perfect square")
if bound <= 0:
bound = default_bound(n)
if block <= 0:
block = max(200, 10 * bound)
if blocks <= 0:
blocks = 50
base, roots = [-1, 2], [(2, 1)] # @line L1
for p in primes_up_to(max(bound, 1000))[1:]:
if p > bound and len(base) >= 16:
break
if n % p == 0:
return p if p < n else 1
if pow(n % p, (p - 1) // 2, p) == 1:
r = sqrt_mod(n, p)
base.append(p)
roots.append((p, r))
roots.append((p, p - r))
logs = {p: log2(p) for p in base[1:]}
slack = 2 * log2(bound) + 2
rels: list[tuple[int, list[int]]] = []
seen: set[int] = set()
j = 0
while len(rels) < len(base) + extra and j < blocks: # @line L2
for lo in (j * block, -(j + 1) * block): # @line L3
lo = max(lo, 1 - s)
hi = lo + block if lo >= 0 else min(lo + block, -j * block)
if hi <= lo:
continue
sieve = [0.0] * (hi - lo)
for p, r in roots: # @line L4
lg = logs[p]
for i in range((r - s - lo) % p, hi - lo, p):
sieve[i] += lg
for i, acc in enumerate(sieve): # @line L5
x = s + lo + i
v = x * x - n
if acc >= log2(abs(v)) - slack and x not in seen:
e = exponents(v, base) # @line L6
if e is not None:
seen.add(x)
rels.append((x, e))
j += 1
vectors = [sum((c & 1) << k for k, c in enumerate(e)) for _, e in rels]
for mask in dependencies(vectors): # @line L7
x, total = 1, [0] * len(base)
for k, (xk, e) in enumerate(rels):
if mask >> k & 1:
x = x * xk % n
total = [a + c for a, c in zip(total, e)]
y = 1
for p, c in zip(base[1:], total[1:]):
y = y * pow(p, c // 2, n) % n
g = gcd(x - y, n) # @line L8
if 1 < g < n:
return g
return 1 # @line L9
_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
def is_probable_prime(n: int) -> bool:
"""Strong probable-prime test to the 13 prime bases up to 41.
It is a proof of primality for n < 3317044064679887385961981 (about 3.3 * 10^24)."""
if n < 2:
return False
for p in _BASES:
if n % p == 0:
return n == p
d, s = n - 1, 0
while d % 2 == 0:
d, s = d // 2, s + 1
for a in _BASES:
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(s - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
def iroot(m: int, e: int) -> int:
"""The integer e-th root floor(m^(1/e)) of m >= 0, by Newton's method."""
if m < 2:
return m
x = 1 << ((m.bit_length() + e - 1) // e)
while True:
y = ((e - 1) * x + m // x ** (e - 1)) // e
if y >= x:
return x
x = y
def _split(m: int) -> int:
"""A proper factor of a composite m with no prime factor below 100.
A perfect power r^e is split by r. Otherwise the quadratic sieve runs with
growing smoothness bounds. Trial division is the last resort."""
for e in range(2, m.bit_length() // 6 + 2):
r = iroot(m, e)
if r**e == m:
return r
b = default_bound(m)
for _ in range(4):
g = quadratic_sieve(m, b)
if g > 1:
return g
b *= 2
d = 101
while m % d:
d += 2
return d
def factorize(n: int) -> list[int]:
"""Prime factors of n >= 1 in non-decreasing order, with multiplicity.
Trial division removes the primes below 100, then each composite part is
split by the quadratic sieve. A loop with an explicit stack replaces recursion."""
if n < 1:
raise ValueError("n must be a positive integer")
out: list[int] = []
for d in range(2, 100):
while n % d == 0:
out.append(d)
n //= d
stack = [n]
while stack:
m = stack.pop()
if m == 1:
continue
if is_probable_prime(m):
out.append(m)
continue
g = _split(m)
stack.extend((g, m // g))
return sorted(out)
test_quadratic_sieve.py 178 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/quadratic-sieve/. The authors are listed in prov/5TWHZJ.jsonl.
import math
import random
import pytest
from quadratic_sieve import (
dependencies,
exponents,
factorize,
primes_up_to,
quadratic_sieve,
sqrt_mod,
)
CARMICHAEL = [561, 1105, 1729, 2465, 2821, 6601, 8911, 10585, 15841, 29341, 41041, 46657,
52633, 62745, 63973, 75361, 101101, 115921, 126217, 162401, 172081, 188461,
252601, 278545, 294409, 314821, 334153, 340561, 399001, 410041, 449065,
488881, 512461, 3215031751]
def is_prime(n: int) -> bool:
"""Deterministic Miller-Rabin for n < 3.3 * 10^24, written here independently."""
assert n < 3317044064679887385961981
if n < 2:
return False
small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
for p in small:
if n % p == 0:
return n == p
d, s = n - 1, 0
while d % 2 == 0:
d, s = d // 2, s + 1
for a in small:
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(s - 1):
x = x * x % n
if x == n - 1:
break
else:
return False
return True
def random_prime(rng: random.Random, bits: int) -> int:
while True:
p = rng.getrandbits(bits) | (1 << (bits - 1)) | 1
if is_prime(p):
return p
def check(n: int) -> list[int]:
f = factorize(n)
assert math.prod(f) == n
assert all(is_prime(p) for p in f)
assert f == sorted(f)
return f
def test_example() -> None:
n = 9073
base = [-1, 2, 3, 7, 11, 13]
assert [p for p in primes_up_to(20)[1:] if pow(n % p, (p - 1) // 2, p) == 1] == base[2:]
assert [min(sqrt_mod(n, p), p - sqrt_mod(n, p)) for p in base[2:]] == [1, 1, 3, 5]
smooth = [x for x in range(85, 105) if exponents(x * x - n, base)]
assert smooth == [85, 89, 91, 95, 96, 97, 99, 102, 103]
assert (-1848) * (-792) * 336 == (2**5 * 3**2 * 7 * 11) ** 2
x, y = 85 * 91 * 97 % n, 2**5 * 3**2 * 7 * 11 % n
assert (x, y) == (6309, 4030)
assert math.gcd(x - y, n) == 43
assert quadratic_sieve(n, 20) in (43, 211)
def test_sqrt_mod() -> None:
for p in primes_up_to(3000)[1:]:
for a in range(0, p, max(1, p // 50)):
if a == 0 or pow(a, (p - 1) // 2, p) == 1:
r = sqrt_mod(a, p)
assert r * r % p == a
def test_dependencies() -> None:
rng = random.Random(7)
for _ in range(200):
rows = [rng.getrandbits(12) for _ in range(rng.randrange(1, 20))]
found = dependencies(rows)
for mask in found:
acc = 0
for j, v in enumerate(rows):
if mask >> j & 1:
acc ^= v
assert mask and acc == 0
assert len(found) >= len(rows) - 12
def test_returns_divisor() -> None:
rng = random.Random(1982)
for _ in range(200):
n = rng.randrange(10**4, 10**8) | 1
if math.isqrt(n) ** 2 == n:
continue
g = quadratic_sieve(n)
assert g == 1 or (1 < g < n and n % g == 0)
if is_prime(n):
assert g == 1
def test_sieve_splits_small() -> None:
# Every odd composite n < 10^5 that is not a square and has no prime
# factor below 100 is split on the first try (every third one is checked).
count = 0
for n in range(10201, 100001, 2):
if any(n % p == 0 for p in range(3, 101, 2)) or is_prime(n):
continue
if math.isqrt(n) ** 2 == n:
continue
count += 1
if count % 3 == 0:
g = quadratic_sieve(n)
assert 1 < g < n and n % g == 0, n
def test_edges() -> None:
for n in (0, 1, 2, 4, 9, 10201):
with pytest.raises(ValueError):
quadratic_sieve(n)
with pytest.raises(ValueError):
factorize(0)
assert factorize(1) == []
assert factorize(2) == [2]
assert factorize(3) == [3]
assert factorize(2**61 - 1) == [2**61 - 1]
assert factorize(2**40) == [2] * 40
assert factorize(101**2) == [101, 101]
assert factorize(101**3) == [101] * 3
assert factorize(10007**3) == [10007] * 3
assert factorize(1000003**2) == [1000003, 1000003]
def test_all_small() -> None:
for n in range(1, 100001):
f = factorize(n)
assert math.prod(f) == n
assert all(is_prime(p) for p in f)
def test_carmichael() -> None:
for n in CARMICHAEL + [2199733160881]:
check(n)
def test_random_semiprimes() -> None:
rng = random.Random(1403)
for bits in range(4, 25):
p, q = random_prime(rng, bits), random_prime(rng, bits)
assert check(p * q) == sorted([p, q])
def test_close_primes() -> None:
p = 1000003
for _ in range(4):
q = p + 2
while not is_prime(q):
q += 2
assert check(p * q) == [p, q]
p = q
def test_random_smooth() -> None:
rng = random.Random(2047)
primes = [p for p in range(101, 3000) if is_prime(p)]
for k in range(1, 9):
f = sorted(rng.choice(primes) for _ in range(k))
assert check(math.prod(f)) == f
What the next level needs
- the tests did not run in this build