Algorithm Search⌘K

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
general_number_field_sieve.pyRawGitHub
# 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

Read the full page about General number field sieve