General number field sieve in Python
The fastest known classical method to factor a large integer with no special form. It gets a congruence of squares from relations smooth on two sides. Heuristically L_N[1/3, (64/9)^(1/3)].
- File
general_number_field_sieve.py, 444 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/general-number-field-sieve/. The authors are listed in prov/H28VR1.jsonl.
from math import gcd, isqrt, log2
# Polynomials are lists of integers, lowest degree first. An element of
# Z[alpha] = Z[x]/(f) for a monic f of degree d is a list of d integers.
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 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 evaluate(f: list[int], x: int, mod: int = 0) -> int:
"""f(x), or f(x) mod `mod` if mod > 0, by Horner's rule."""
v = 0
for c in reversed(f):
v = v * x + c
if mod:
v %= mod
return v
def homogeneous(f: list[int], a: int, b: int) -> int:
"""F(a, b) = b^d f(a/b), the norm of a - b alpha for a monic f."""
d = len(f) - 1
return sum(c * a**i * b ** (d - i) for i, c in enumerate(f))
def derivative(f: list[int]) -> list[int]:
return [i * c for i, c in enumerate(f)][1:]
def mulmod(u: list[int], v: list[int], f: list[int], mod: int = 0) -> list[int]:
"""u v in Z[x]/(f), with coefficients reduced modulo `mod` if mod > 0.
Precondition: f is monic of degree d, and u, v have length d."""
d = len(f) - 1
w = [0] * (2 * d - 1)
for i, x in enumerate(u):
if x:
for j, y in enumerate(v):
w[i + j] += x * y
for k in range(2 * d - 2, d - 1, -1):
c = w[k]
if c:
for j in range(d):
w[k - d + j] -= c * f[j]
w = w[:d]
return [x % mod for x in w] if mod else w
def powmod(u: list[int], e: int, f: list[int], mod: int) -> list[int]:
"""u^e in (Z/mod)[x]/(f), by binary exponentiation."""
d = len(f) - 1
r = [1] + [0] * (d - 1)
while e:
if e & 1:
r = mulmod(r, u, f, mod)
u = mulmod(u, u, f, mod)
e >>= 1
return r
def base_m(n: int, d: int, m: int = 0) -> tuple[list[int], int]:
"""The base-m expansion f of n, with f(m) = n, and m.
Precondition: d >= 2 and n > 2^(2 d^2) or so, so that the leading digit is
1. By default m = floor(n^(1/d)); then m^d <= n < (m + 1)^d < 2 m^d."""
if m <= 0:
m = iroot(n, d)
f, x = [], n
for _ in range(d):
f.append(x % m)
x //= m
f.append(x)
return f, m
def balanced_base_m(n: int, d: int, m: int) -> list[int]:
"""The base-m expansion of n with digits in (-m/2, m/2], so f(m) = n."""
f, x = [], n
for _ in range(d):
c = x % m
if c > m // 2:
c -= m
f.append(c)
x = (x - c) // m
f.append(x)
return f
def select_polynomial(n: int, d: int, tries: int = 64) -> tuple[list[int], int]:
"""A monic f of degree d and an m with f(m) = n, chosen among the balanced
base-m expansions for m = floor(n^(1/d)), m - 1, ..., with the smallest
largest coefficient. Smaller coefficients give smaller norms F(a, b)."""
m0 = iroot(n, d)
best: tuple[int, list[int], int] | None = None
for m in range(m0, max(m0 - tries, 1), -1):
f = balanced_base_m(n, d, m)
if f[-1] != 1:
continue
size = max(abs(c) for c in f[:-1])
if best is None or size < best[0]:
best = (size, f, m)
if best is None:
return base_m(n, d, m0)
return best[1], best[2]
def is_inert(f: list[int], p: int) -> bool:
"""True if the monic f of prime degree d is irreducible modulo the prime p.
For a prime d, f is irreducible modulo p if and only if it has no root
modulo p and x^(p^d) = x in F_p[x]/(f)."""
d = len(f) - 1
if any(evaluate(f, r, p) == 0 for r in range(p)):
return False
x = [0, 1] + [0] * (d - 2)
return powmod(x, p**d, f, p) == x
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 factor_bases(f: list[int], bound: int, chars: int):
"""Rational primes p <= bound, algebraic pairs (p, r) with f(r) = 0 mod p,
and `chars` quadratic characters (q, s): primes q > bound with a simple
root s of f modulo q."""
fp = derivative(f)
primes = primes_up_to(bound)
pairs = [(p, r) for p in primes for r in range(p) if evaluate(f, r, p) == 0]
qchars: list[tuple[int, int]] = []
q = bound
while len(qchars) < chars:
q += 1
if all(q % p for p in primes_up_to(isqrt(q))):
for s in range(q):
if evaluate(f, s, q) == 0 and evaluate(fp, s, q) != 0:
qchars.append((q, s))
if len(qchars) == chars:
break
return primes, pairs, qchars
def smooth_exponents(v: int, primes: list[int]) -> list[int] | None:
"""Exponents of |v| over primes, or None if |v| is not smooth over them."""
v = abs(v)
if v == 0:
return None
e = [0] * len(primes)
for i, p in enumerate(primes):
while v % p == 0:
v //= p
e[i] += 1
if v == 1:
return e
return e if v == 1 else None
def find_relations(n, f, m, primes, pairs, need, width, max_b):
"""Pairs (a, b) with gcd(a, b) = 1, b >= 1, |a| <= width, such that a - b m
and F(a, b) are both smooth over primes. Line sieving on both sides, one b
at a time, with logarithms; candidates are confirmed by trial division."""
rels = []
size = 2 * width + 1
for b in range(1, max_b + 1): # @line L3
rat = [0] * size
alg = [0] * size
for p in primes:
lg = p.bit_length()
for i in range((b * m + width) % p, size, p):
rat[i] += lg
for p, r in pairs:
lg = p.bit_length()
for i in range((b * r + width) % p, size, p):
alg[i] += lg
slack = 2 * primes[-1].bit_length() + 2
for i in range(size):
a = i - width
u = a - b * m
v = homogeneous(f, a, b)
if u == 0 or v == 0 or gcd(a, b) != 1:
continue
if rat[i] < abs(u).bit_length() - slack or alg[i] < abs(v).bit_length() - slack:
continue
eu = smooth_exponents(u, primes) # @line L4
if eu is None:
continue
ev = smooth_exponents(v, primes)
if ev is None:
continue
rels.append((a, b, u, eu, v, ev))
if len(rels) >= need:
return rels
return rels
def relation_vector(rel, primes, pairs, index, qchars) -> int:
"""The GF(2) vector of a relation: the sign of a - b m, the rational
exponents, the sign of F(a, b), the exponents of the first degree prime ideals (p, r) with
a = b r mod p, and the quadratic characters (a - b s / q)."""
a, b, u, eu, v, ev = rel
bits = [1 if u < 0 else 0, 1 if v < 0 else 0] + [e & 1 for e in eu]
alg = [0] * len(pairs)
for p, e in zip(primes, ev):
if e:
r = a * pow(b, -1, p) % p
alg[index[(p, r)]] = e & 1
bits += alg
for q, s in qchars:
bits.append(1 if pow((a - b * s) % q, (q - 1) // 2, q) == q - 1 else 0)
return sum(c << k for k, c in enumerate(bits))
def product_in_order(elements: list[list[int]], f: list[int]) -> list[int]:
"""The product of elements of Z[alpha], by a balanced product tree."""
level = elements
while len(level) > 1:
level = [mulmod(level[i], level[i + 1], f) if i + 1 < len(level) else level[i]
for i in range(0, len(level), 2)]
return level[0]
def inert_prime(f: list[int], start: int, tries: int = 2000) -> int:
"""The least prime p > start with p = 3 mod 4 and f irreducible modulo p,
or 0 if none is found among `tries` candidates. Then F_p[x]/(f) is the
field with q = p^d elements, and q = 3 mod 4 when d is odd."""
p = start + 1
for _ in range(tries):
if p % 4 == 3 and all(p % t for t in range(2, isqrt(p) + 1)) and is_inert(f, p):
return p
p += 1
return 0
def algebraic_sqrt(gamma: list[int], f: list[int], p: int) -> list[int] | None:
"""delta in Z[alpha] with delta^2 = gamma, or None.
Precondition: f monic of odd prime degree d, p an inert prime with
p = 3 mod 4 not dividing the norm of gamma. Computes r = gamma^(-1/2) in
F_q with q = p^d by one power, lifts it to Z/p^k by Newton's iteration
r <- r (3 - gamma r^2) / 2, and checks delta = gamma r with symmetric
coefficients exactly. Gives up when p^k passes a bound on the size."""
d = len(f) - 1
q = p**d
g0 = [c % p for c in gamma]
r = powmod(g0, (3 * q - 5) // 4, f, p)
if mulmod(mulmod(r, r, f, p), g0, f, p) != [1] + [0] * (d - 1):
return None
limit = 4 * max(abs(c) for c in gamma) + 4
mod = p
while mod < limit:
mod = mod * mod
g = [c % mod for c in gamma]
t = mulmod(mulmod(r, r, f, mod), g, f, mod)
t = [(-c) % mod for c in t]
t[0] = (t[0] + 3) % mod
half = pow(2, -1, mod)
r = [c * half % mod for c in mulmod(r, t, f, mod)]
delta = mulmod(g, r, f, mod)
delta = [c - mod if c > mod // 2 else c for c in delta]
if mulmod(delta, delta, f) == gamma:
return delta
return None
def default_bound(n: int) -> int:
"""A smoothness bound for this toy sieve, tuned by experiment for n
between 2^20 and 2^64: about 200 at 2^20, 900 at 2^40, 6000 at 2^64."""
return max(150, int(200 * 2 ** ((n.bit_length() - 20) / 9)))
def gnfs(n: int, d: int = 3, bound: int = 0, width: int = 0, chars: int = 0,
extra: int = 10, m: int = 0, max_b: int = 0) -> int:
"""A proper factor of the odd composite n by the general number field sieve.
Precondition: n odd, not a prime power and not a perfect power, d in {3, 5},
and n large enough that the base-m polynomial is monic. Returns a proper
factor of n, or 1 on failure. Cost: heuristically L_n[1/3, (64/9)^(1/3)]
with the right choices of d, bound and the sieve region as n grows; this
toy version uses a fixed small degree."""
f, m = base_m(n, d, m) if m > 0 else select_polynomial(n, d) # @line L1
if f[-1] != 1:
return 1
if bound <= 0:
bound = default_bound(n)
if width <= 0:
width = 3 * bound
if chars <= 0:
chars = 2 * n.bit_length() // 3 + 10
if max_b <= 0:
max_b = 5 * bound
for p in primes_up_to(max(bound, 2 * chars)):
if n % p == 0:
return p if p < n else 1
g = gcd(evaluate(derivative(f), m), n)
if 1 < g < n:
return g
p_inert = inert_prime(f, max(bound, 2 * chars) + 200)
if p_inert == 0:
return 1 # no inert prime: f is probably reducible over Q
primes, pairs, qchars = factor_bases(f, bound, chars) # @line L2
index = {pr: k for k, pr in enumerate(pairs)}
cols = 2 + len(primes) + len(pairs) + len(qchars)
rels = find_relations(n, f, m, primes, pairs, cols + extra, width, max_b)
vectors = [relation_vector(r, primes, pairs, index, qchars) for r in rels] # @line L5
fp = derivative(f)
fpm = evaluate(derivative(f), m, n)
for mask in dependencies(vectors): # @line L6
chosen = [r for k, r in enumerate(rels) if mask >> k & 1]
total = [0] * len(primes)
for _, _, _, eu, _, _ in chosen:
total = [x + y for x, y in zip(total, eu)]
x = fpm # @line L7
for p, e in zip(primes, total):
x = x * pow(p, e // 2, n) % n
elems = [[a, -b] + [0] * (len(f) - 3) for a, b, _, _, _, _ in chosen]
gamma = product_in_order(elems + [fp, fp], f)
delta = algebraic_sqrt(gamma, f, p_inert) # @line L8
if delta is None:
continue
y = evaluate(delta, m, n)
g = gcd(x - y, n) # @line L9
if 1 < g < n:
return g
return 1 # @line L10
_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
_SMALL = primes_up_to(1000)
def _split(m: int) -> int:
"""A proper factor of a composite m with no prime factor below 1000.
A perfect power r^e is split by r. Otherwise the number field sieve runs
with m = floor(m^(1/3)) and then smaller bases and larger bounds. Odd trial
division is the last resort, so the function always returns."""
for e in range(2, m.bit_length() // 9 + 2):
r = iroot(m, e)
if r**e == m:
return r
bound = default_bound(m)
for k in range(4):
g = gnfs(m, 3, bound, extra=10 + 20 * k)
if 1 < g < m:
return g
bound += bound // 2
d = 1001
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 1000, then each composite part is
split by the number field sieve. A loop with an explicit stack replaces
recursion."""
if n < 1:
raise ValueError("n must be a positive integer")
out: list[int] = []
for p in _SMALL:
if p * p > n:
break
while n % p == 0:
out.append(p)
n //= p
stack = [n]
while stack:
m = stack.pop()
if m == 1:
continue
if m < 1000 * 1000 or is_probable_prime(m):
out.append(m)
continue
g = _split(m)
stack.extend((g, m // g))
return sorted(out)
test_general_number_field_sieve.py 287 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/general-number-field-sieve/. The authors are listed in prov/H28VR1.jsonl.
import math
import random
import pytest
from general_number_field_sieve import (
algebraic_sqrt,
balanced_base_m,
base_m,
dependencies,
derivative,
evaluate,
factor_bases,
factorize,
find_relations,
gnfs,
homogeneous,
inert_prime,
is_inert,
mulmod,
product_in_order,
relation_vector,
select_polynomial,
)
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:
# Briggs's example: n = 45113 = 197 * 229 with m = 31.
n = 45113
f, m = base_m(n, 3, 31)
assert (f, m) == ([8, 29, 15, 1], 31)
primes, pairs, chars = factor_bases(f, 100, 10)
assert (len(primes), len(pairs), len(chars)) == (25, 21, 10)
assert pairs[:4] == [(2, 0), (7, 6), (17, 13), (23, 11)]
assert chars[0] == (101, 87)
assert inert_prime(f, 300) == 307
rels = find_relations(n, f, m, primes, pairs, 68, 300, 500)
assert len(rels) == 68 and max(r[1] for r in rels) == 9
chosen = [(-116, 1), (-104, 1), (-32, 1), (-13, 1), (-4, 1), (1, 1), (142, 1), (-17, 3), (56, 3)]
assert all((a, b) in [(r[0], r[1]) for r in rels] for a, b in chosen)
u = math.prod(a - b * m for a, b in chosen)
v = math.prod(homogeneous(f, a, b) for a, b in chosen)
assert u == 161538300**2 and v == 3310622949437167616**2
fp = derivative(f)
gamma = product_in_order([[a, -b, 0] for a, b in chosen] + [fp, fp], f)
assert gamma == [1383514992730880, 788159310461352, 222288412881565]
delta = algebraic_sqrt(gamma, f, 307)
assert delta == [-36380984, -8069491, -1426166]
x = evaluate(fp, m) * 161538300 % n
y = evaluate(delta, m, n)
assert (x, y) == (6045, 9985)
assert (x * x - y * y) % n == 0
assert math.gcd(x - y, n) == 197
assert gnfs(n, m=31, bound=100, width=300, chars=10) == 197
def test_polynomial_selection() -> None:
assert select_polynomial(1040597897477, 3) == ([-966, 956, 122, 1], 10093)
assert select_polynomial(1000003 * 1000033, 3) == ([-1061, -1539, 121, 1], 9960)
rng = random.Random(3)
for _ in range(300):
n = rng.randrange(2**30, 2**80)
for d in (3, 5):
f, m = base_m(n, d)
assert evaluate(f, m) == n and all(0 <= c < m for c in f[:-1])
g = balanced_base_m(n, d, m)
assert evaluate(g, m) == n and all(-m // 2 < c <= m // 2 for c in g[:-1])
f, m = select_polynomial(n, 3)
assert evaluate(f, m) == n and f[-1] == 1
def test_ring_arithmetic() -> None:
rng = random.Random(11)
for _ in range(200):
d = rng.choice((3, 5))
f = [rng.randrange(-50, 51) for _ in range(d)] + [1]
u = [rng.randrange(-99, 100) for _ in range(d)]
v = [rng.randrange(-99, 100) for _ in range(d)]
w = mulmod(u, v, f)
for t in range(-5, 6):
# Evaluation at a root is a ring map, so test with f(t) as modulus.
mod = abs(evaluate(f, t))
if mod > 1:
assert (evaluate(u, t) * evaluate(v, t) - evaluate(w, t)) % mod == 0
def brute_irreducible(f: list[int], p: int) -> bool:
d = len(f) - 1
for k in range(1, d // 2 + 1):
for code in range(p**k):
g = [(code // p**i) % p for i in range(k)] + [1]
r = [c % p for c in f]
for i in range(d, k - 1, -1):
c = r[i]
for j in range(k + 1):
r[i - k + j] = (r[i - k + j] - c * g[j]) % p
if not any(r[:k]):
return False
return True
def test_is_inert() -> None:
rng = random.Random(5)
for _ in range(150):
d = rng.choice((3, 5))
p = rng.choice((2, 3, 5, 7))
f = [rng.randrange(p) for _ in range(d)] + [1]
assert is_inert(f, p) == brute_irreducible(f, p)
def test_algebraic_sqrt() -> None:
rng = random.Random(9)
for _ in range(60):
f = [rng.randrange(-30, 31) for _ in range(3)] + [1]
p = inert_prime(f, 300)
if p == 0:
continue
delta = [rng.randrange(-10**6, 10**6) for _ in range(3)]
gamma = mulmod(delta, delta, f)
if all(c % p == 0 for c in gamma):
continue
r = algebraic_sqrt(gamma, f, p)
assert r is not None and mulmod(r, r, f) == gamma
assert r in (delta, [-c for c in delta])
# A non-square gives None: multiply by alpha, whose norm -f(0) is
# not a square for these f.
if math.isqrt(abs(f[0])) ** 2 != abs(f[0]):
assert algebraic_sqrt(mulmod(gamma, [0, 1, 0], f), f, p) is None
def test_relations() -> None:
n = 1000003 * 1000033
f, m = select_polynomial(n, 3)
primes, pairs, chars = factor_bases(f, 400, 20)
for p, r in pairs:
assert evaluate(f, r, p) == 0
for b in (1, 2, 5):
assert homogeneous(f, b * r + p, b) % p == 0
index = {pr: k for k, pr in enumerate(pairs)}
rels = find_relations(n, f, m, primes, pairs, 150, 1200, 400)
assert len(rels) == 150
for rel in rels:
a, b, u, eu, v, ev = rel
assert math.gcd(a, b) == 1 and u == a - b * m and v == homogeneous(f, a, b)
assert abs(u) == math.prod(p**e for p, e in zip(primes, eu))
assert abs(v) == math.prod(p**e for p, e in zip(primes, ev))
vec = relation_vector(rel, primes, pairs, index, chars)
assert vec.bit_length() <= 2 + len(primes) + len(pairs) + len(chars)
vectors = [relation_vector(r, primes, pairs, index, chars) for r in rels]
for mask in dependencies(vectors)[:5]:
chosen = [r for k, r in enumerate(rels) if mask >> k & 1]
u = math.prod(r[2] for r in chosen)
v = math.prod(r[4] for r in chosen)
assert u > 0 and math.isqrt(u) ** 2 == u
assert v > 0 and math.isqrt(v) ** 2 == v
def test_dependencies() -> None:
rng = random.Random(7)
for _ in range(200):
rows = [rng.getrandbits(12) for _ in range(rng.randrange(1, 20))]
for mask in dependencies(rows):
acc = 0
for j, v in enumerate(rows):
if mask >> j & 1:
acc ^= v
assert mask and acc == 0
assert len(dependencies(rows)) >= len(rows) - 12
def test_returns_divisor() -> None:
rng = random.Random(1990)
wins = 0
for _ in range(12):
p, q = random_prime(rng, 13), random_prime(rng, 14)
n = p * q
g = gnfs(n)
assert g in (1, p, q)
wins += g > 1
assert wins >= 10
def test_edges() -> None:
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(1009**2) == [1009, 1009]
assert factorize(1009**3) == [1009] * 3
assert factorize(1000003**2) == [1000003, 1000003]
assert factorize(10007**3) == [10007] * 3
assert factorize(1009 * 1013) == [1009, 1013]
assert factorize(1000003 * 1000033) == [1000003, 1000033]
assert factorize(3 * 1000003 * 1000033) == [3, 1000003, 1000033]
def test_carmichael() -> None:
for n in CARMICHAEL:
check(n)
assert factorize(3215031751) == [151, 751, 28351]
def test_close_primes() -> None:
rng = random.Random(17)
for bits in (16, 19, 22):
p = random_prime(rng, bits)
q = p + 2
while not is_prime(q):
q += 2
assert check(p * q) == [p, q]
def test_random_semiprimes() -> None:
rng = random.Random(2024)
for bits in (11, 14, 17, 20, 22):
for _ in range(2):
p, q = random_prime(rng, bits), random_prime(rng, bits)
assert check(p * q) == sorted((p, q))
def test_smooth_numbers() -> None:
rng = random.Random(31)
for size in (8, 16, 32, 64, 128):
for _ in range(3):
n = 1
while n.bit_length() < size:
n *= rng.choice((2, 3, 5, 7, 11, 13, 97, 997, 1009, 1013, 2003, 4001))
check(n)
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)
What the next level needs
- the tests did not run in this build