Algorithm Search⌘K

APR-CL primality test in Python

A deterministic primality test that checks powers of Jacobi sums in rings of roots of unity. It runs in time (log n)^(O(log log log n)) and proves primes of hundreds of digits.

File
apr_cl.py, 458 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
apr_cl.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/apr-cl-primality-test/. The authors are listed in prov/MW7HYF.jsonl.
"""The APR-CL primality test with Jacobi sums (Adleman, Pomerance, Rumely,
Cohen and Lenstra), in the form of the page.

The test uses an even t with t/2 odd and primes q_1 < ... < q_j with
q_i - 1 | t and s = q_1 ... q_j > sqrt(n). For each prime q | s and each
odd prime p | q - 1 it checks that a power of the Jacobi sum J(p, q) is a
root of unity modulo n, in the ring Z[zeta_m] with m = p^k exactly dividing
q - 1. For p = 2 it checks (-q)^((n - 1)/2) = +1 or -1. If all checks pass,
every prime factor of n is a power of n modulo s, and a search over the
t powers finds a factor of each composite n.

aprcl(n) returns True or False, and None only if the search for an extra
prime in line L16 gives up, which does not happen for any n tested.
"""

from __future__ import annotations

from functools import lru_cache
from math import gcd, isqrt, lcm
from typing import NamedTuple

# Values of t, each 2 times an odd number, in increasing order. The
# largest gives s > 10^295, so the table covers n below about 10^590.
T_TABLE = (
    2, 6, 30, 90, 210, 630, 2310, 6930, 30030, 90090, 117810, 139230,
    270270, 450450, 1531530, 2645370, 4594590, 7657650, 29099070,
)
# Extra primes q tried for one odd p in line L16 before the test gives up.
EXTRA_TRIES = 60

Poly = list[int]


class Plan(NamedTuple):
    """The data of line L2: the primes q dividing s, s itself, and t."""

    primes: tuple[int, ...]
    s: int
    t: int


def small_is_prime(n: int) -> bool:
    """Trial division; used only for numbers below 10^8 (t + 1 and q)."""
    if n < 2:
        return False
    if n % 2 == 0:
        return n == 2
    d = 3
    while d * d <= n:
        if n % d == 0:
            return False
        d += 2
    return True


def prime_factors(n: int) -> list[int]:
    """The distinct prime factors of a small n > 0, by trial division."""
    out = []
    d = 2
    while d * d <= n:
        if n % d == 0:
            out.append(d)
            while n % d == 0:
                n //= d
        d += 1
    if n > 1:
        out.append(n)
    return out


def divisors(n: int) -> list[int]:
    ds = [1]
    for p in prime_factors(n):
        e = 0
        m = n
        while m % p == 0:
            m //= p
            e += 1
        ds = [d * p**i for d in ds for i in range(e + 1)]
    return sorted(ds)


def valuation(p: int, n: int) -> int:
    k = 0
    while n % p == 0:
        n //= p
        k += 1
    return k


@lru_cache(maxsize=None)
def candidate_primes(t: int) -> tuple[int, ...]:
    """All primes q with q - 1 | t, in increasing order."""
    return tuple(d + 1 for d in divisors(t) if small_is_prime(d + 1))


def plan(n: int) -> Plan:
    """Line L2: the first t of the table with enough primes, and the
    shortest list of the smallest such primes with s^2 > n."""
    for t in T_TABLE:
        s = 1
        chosen: list[int] = []
        for q in candidate_primes(t):
            chosen.append(q)
            s *= q
            if s * s > n:
                return Plan(tuple(chosen), s, lcm(*(q - 1 for q in chosen)))
    raise ValueError("n is too large for the table of t")


def primitive_root(q: int) -> int:
    """The least primitive root of the prime q."""
    if q == 2:
        return 1
    fs = prime_factors(q - 1)
    g = 2
    while any(pow(g, (q - 1) // f, q) == 1 for f in fs):
        g += 1
    return g


@lru_cache(maxsize=4096)
def jacobi_sums(q: int) -> dict[int, tuple[int, ...]]:
    """For each odd prime p | q - 1, with m = p^k exactly dividing q - 1, the
    Jacobi sum J(p, q) = sum of chi(x) chi(1 - x) over x in F_q, as the
    coefficients of 1, z, ..., z^(m-1) with z a primitive m-th root of 1.
    Here chi(g^i) = z^i for the least primitive root g."""
    g = primitive_root(q)
    order = q - 1
    log = [0] * q
    x = 1
    for i in range(order):
        log[x] = i
        x = x * g % q
    # e(i) = i + log(1 - g^i) for 1 <= i <= q - 2; count e(i) mod L.
    ms = {p: p ** valuation(p, order) for p in prime_factors(order) if p > 2}
    if not ms:
        return {}
    big = lcm(*ms.values())
    counts = [0] * big
    x = 1
    for i in range(1, order):
        x = x * g % q
        counts[(i + log[q + 1 - x]) % big] += 1
    return {p: tuple(sum(counts[j::m]) for j in range(m)) for p, m in ms.items()}


def unit_w(m: int, p: int) -> int:
    """w = sum over y in (Z/m)^* of floor(2y/m) / y modulo m. The proof
    needs p not dividing w; plan_pairs checks it."""
    return sum((2 * y // m) * pow(y, -1, m) for y in range(1, m) if y % p) % m


# Arithmetic in Z[zeta_m] = Z[x]/(Phi_m(x)) modulo n. An element is the
# list of its phi(m) coefficients on 1, x, ..., x^(phi(m) - 1).


def reduce_cyclotomic(a: Poly, m: int, p: int) -> Poly:
    """The image in Z[x]/(Phi_m) of a polynomial a of degree below m, with
    Phi_m(x) = sum of x^(i m/p) for 0 <= i < p. Coefficients are not
    reduced modulo n."""
    a = list(a) + [0] * (m - len(a))
    step = m // p
    deg = m - step
    for i in range(m - 1, deg - 1, -1):
        c = a[i]
        if c:
            a[i] = 0
            for j in range(p - 1):
                a[i - deg + j * step] -= c
    return a[:deg]


class Ring:
    """Z[zeta_m] modulo n, for m = p^k with p prime and gcd(m, n) = 1."""

    def __init__(self, m: int, p: int, n: int) -> None:
        self.m = m
        self.p = p
        self.n = n
        self.deg = m - m // p

    def one(self) -> Poly:
        return [1] + [0] * (self.deg - 1)

    def from_cyclic(self, a: Poly) -> Poly:
        """The image of an element of Z[x]/(x^m - 1), given by m coefficients."""
        return [c % self.n for c in reduce_cyclotomic(a, self.m, self.p)]

    def mul(self, a: Poly, b: Poly) -> Poly:
        """Schoolbook product, then x^m = 1, then Phi_m, then mod n."""
        d, m = self.deg, self.m
        c = [0] * (2 * d - 1)
        if a is b:
            for i in range(d):
                ai = a[i]
                if ai:
                    c[2 * i] += ai * ai
                    ai2 = 2 * ai
                    for j in range(i + 1, d):
                        c[i + j] += ai2 * a[j]
        else:
            for i in range(d):
                ai = a[i]
                if ai:
                    for j in range(d):
                        c[i + j] += ai * b[j]
        if len(c) > m:
            for i in range(m, len(c)):
                c[i - m] += c[i]
            del c[m:]
        return [v % self.n for v in reduce_cyclotomic(c, m, self.p)]

    def power(self, a: Poly, e: int) -> Poly:
        """a^e, by binary powering for small e and with a window of 4
        binary digits for large e."""
        result = self.one()
        if e < 64:
            base = a
            while e:
                if e & 1:
                    result = self.mul(result, base)
                e >>= 1
                if e:
                    base = self.mul(base, base)
            return result
        table = [result, a]
        for _ in range(14):
            table.append(self.mul(table[-1], a))
        started = False
        for shift in range((e.bit_length() + 3) // 4 * 4 - 4, -4, -4):
            if started:
                for _ in range(4):
                    result = self.mul(result, result)
            digit = (e >> shift) & 15
            if digit:
                result = self.mul(result, table[digit]) if started else table[digit]
                started = True
        return result

    def sigma(self, a: Poly, c: int) -> Poly:
        """The automorphism zeta -> zeta^c, for c prime to m."""
        m = self.m
        out = [0] * m
        for i, v in enumerate(a):
            out[i * c % m] = v
        return self.from_cyclic(out)

    def root_index(self, a: Poly) -> int | None:
        """The h in [0, m) with a = zeta^h, or None if there is none."""
        for h in range(self.m):
            unit = [0] * self.m
            unit[h] = 1
            if self.from_cyclic(unit) == a:
                return h
        return None


def jacobi_power(n: int, q: int, p: int) -> int | None:
    """Lines L9 to L11 for the pair (p, q): S = J(p, q)^(alpha_n) modulo n,
    where alpha_n = sum over y of floor(n y/m) sigma_y^(-1). Returns the h
    with S = zeta^h, or None if S is not a root of unity."""
    coeffs = jacobi_sums(q)[p]  # @line L9
    m = len(coeffs)
    ring = Ring(m, p, n)
    j = ring.from_cyclic(list(coeffs))
    units = [y for y in range(1, m) if y % p]
    # S = (J^theta)^(n // m) * J^(alpha_r), theta = sum y sigma_y^(-1).
    s1 = ring.one()
    for y in units:
        s1 = ring.mul(s1, ring.power(ring.sigma(j, pow(y, -1, m)), y))
    r = n % m
    s3 = ring.one()
    for y in units:
        f = r * y // m
        if f:
            s3 = ring.mul(s3, ring.power(ring.sigma(j, pow(y, -1, m)), f))
    big_s = ring.mul(ring.power(s1, n // m), s3)  # @line L10
    return ring.root_index(big_s)


def is_square(n: int) -> bool:
    return n >= 0 and isqrt(n) ** 2 == n


def jacobi_symbol(a: int, n: int) -> int:
    """The Jacobi symbol (a/n) for odd n > 0."""
    a %= n
    result = 1
    while a:
        while a % 2 == 0:
            a //= 2
            if n % 8 in (3, 5):
                result = -result
        a, n = n, a
        if a % 4 == 3 and n % 4 == 3:
            result = -result
        a %= n
    return result if n == 1 else 0


def check_units(primes: tuple[int, ...]) -> None:
    for q in primes:
        for p in prime_factors(q - 1):
            if p > 2:
                m = p ** valuation(p, q - 1)
                if unit_w(m, p) % p == 0:
                    raise ValueError("p divides w(m); use another Jacobi sum")


def make_plan(n: int, primes: tuple[int, ...] | None) -> Plan:
    """The plan of line L2, or a given list of primes, checked: each q is 2
    or a prime with q = 3 mod 4, and s^2 > n."""
    if primes is None:
        return plan(n)
    for q in primes:
        if not small_is_prime(q) or (q > 2 and q % 4 != 3):
            raise ValueError("each q must be 2 or a prime q = 3 mod 4")
    s = 1
    for q in primes:
        s *= q
    if s * s <= n:
        raise ValueError("s^2 must exceed n")
    return Plan(tuple(sorted(primes)), s, lcm(*(q - 1 for q in primes)))


def divisor_search(n: int, s: int, t: int) -> int | None:
    """Lines L17 to L19: the first n^i mod s, 1 <= i < t, that is a proper
    factor of n, or None."""
    nm = n % s
    r = 1
    for _ in range(1, t):  # @line L17
        r = r * nm % s  # @line L18
        if 1 < r < n and n % r == 0:  # @line L19
            return r
    return None


def aprcl(n: int, primes: tuple[int, ...] | None = None) -> bool | None:
    """True if n is prime, False if not. None only if line L16 gives up.
    The optional primes replace the table choice of line L2 (for tests)."""
    if n < 2:  # @line L1
        return False
    primes, s, t = make_plan(n, primes)  # @line L2
    check_units(primes)
    small = set(primes) | set(prime_factors(t))
    if n in small:  # @line L3
        return True
    if gcd(n, t * s) > 1:  # @line L4
        return False
    odd_ps = [p for p in prime_factors(t) if p > 2]
    done = {p: pow(n, p - 1, p * p) != 1 for p in odd_ps}  # @line L5
    done[2] = n % 4 == 3  # @line L6
    for q in primes:  # @line L7
        if q == 2:
            continue
        for p in prime_factors(q - 1):  # @line L8
            if p == 2:
                continue
            h = jacobi_power(n, q, p)
            if h is None:  # @line L11
                return False
            if h % p:  # @line L12
                done[p] = True
        e = pow(-q % n, (n - 1) // 2, n)  # @line L13
        if e not in (1, n - 1):  # @line L14
            return False
        if e == n - 1:  # @line L15
            done[2] = True
    for p in sorted(done):  # @line L16
        if done[p]:
            continue
        verdict = extra_check(n, p, set(primes))
        if verdict is not True:
            return verdict
    return divisor_search(n, s, t) is None  # @line L20


def extra_check(n: int, p: int, used: set[int]) -> bool | None:
    """Line L16 for one p: True when the condition L_p is now proved,
    False when n is shown composite, None when the tries run out."""
    if p == 2:
        # Here n = 1 mod 4. Find z with Jacobi symbol (z/n) = -1; for a
        # prime n, Euler's criterion gives z^((n-1)/2) = -1.
        if is_square(n):
            return False
        z = 2
        while True:
            j = jacobi_symbol(z, n)
            if j == 0:
                return False
            if j == -1:
                return pow(z, (n - 1) // 2, n) == n - 1
            z += 1
    tries = 0
    q = 2 * p + 1
    while tries < EXTRA_TRIES:
        if q not in used and small_is_prime(q):
            if n % q == 0:
                return n == q
            tries += 1
            h = jacobi_power(n, q, p)
            if h is None:
                return False
            if h % p:
                return True
        q += 2 * p
    return None


class Report(NamedTuple):
    """The data that the worked example shows."""

    primes: tuple[int, ...]
    s: int
    t: int
    roots: dict[tuple[int, int], int]
    signs: dict[int, int]
    factor: int | None


def trace(n: int) -> Report:
    """Run the main checks for n and record the h of each pair, the sign
    (-q)^((n-1)/2) of each q, and the factor that lines L17 to L19 find
    (None if there is none)."""
    primes, s, t = plan(n)
    roots: dict[tuple[int, int], int] = {}
    signs: dict[int, int] = {}
    for q in primes:
        if q == 2:
            continue
        for p in prime_factors(q - 1):
            if p > 2:
                h = jacobi_power(n, q, p)
                roots[(p, q)] = -1 if h is None else h
        e = pow(-q % n, (n - 1) // 2, n)
        signs[q] = -1 if e == n - 1 else e
    return Report(primes, s, t, roots, signs, divisor_search(n, s, t))


def gauss_jacobi_check(q: int, p: int) -> bool:
    """J(p, q) times its complex conjugate is q in Z[zeta_m]: a check of the
    Jacobi sums used in the tests."""
    coeffs = list(jacobi_sums(q)[p])
    m = len(coeffs)
    conj = [0] * m
    for i, v in enumerate(coeffs):
        conj[(-i) % m] = v
    full = [0] * m
    for i, a in enumerate(coeffs):
        if a:
            for k, b in enumerate(conj):
                full[(i + k) % m] += a * b
    red = reduce_cyclotomic(full, m, p)
    return red == [q] + [0] * (len(red) - 1)

apr_cl_primality_test.py 42 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/apr-cl-primality-test/. The authors are listed in prov/MW7HYF.jsonl.
"""Entry point named after the slug. The code is in apr_cl.py,
because the tools treat a file whose name ends in _test as a test file."""

from apr_cl import (
    T_TABLE,
    Plan,
    Report,
    aprcl,
    candidate_primes,
    divisor_search,
    extra_check,
    gauss_jacobi_check,
    jacobi_power,
    jacobi_sums,
    jacobi_symbol,
    make_plan,
    plan,
    primitive_root,
    trace,
    unit_w,
)

__all__ = [
    "T_TABLE",
    "Plan",
    "Report",
    "aprcl",
    "candidate_primes",
    "divisor_search",
    "extra_check",
    "gauss_jacobi_check",
    "jacobi_power",
    "jacobi_sums",
    "jacobi_symbol",
    "make_plan",
    "plan",
    "primitive_root",
    "trace",
    "unit_w",
]
test_apr_cl_primality_test.py 317 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/apr-cl-primality-test/. The authors are listed in prov/MW7HYF.jsonl.

import random
from math import gcd, lcm, prod

import pytest

from apr_cl_primality_test import (
    T_TABLE,
    aprcl,
    candidate_primes,
    divisor_search,
    extra_check,
    gauss_jacobi_check,
    jacobi_power,
    jacobi_sums,
    jacobi_symbol,
    make_plan,
    plan,
    primitive_root,
    trace,
    unit_w,
)

LIMIT = 100_000

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,
]
# Strong pseudoprimes: to base 2, to bases 2 and 3, to 2, 3, 5, and so on.
STRONG_PSEUDOPRIMES = [
    2047, 3277, 4033, 4681, 8321, 1373653, 25326001, 3215031751,
    2152302898747, 3474749660383, 341550071728321, 3825123056546413051,
    318665857834031151167461, 3317044064679887385961981,
]


def sieve(limit: int) -> bytearray:
    is_p = bytearray([1]) * (limit + 1)
    is_p[0] = is_p[1] = 0
    for i in range(2, int(limit**0.5) + 1):
        if is_p[i]:
            is_p[i * i :: i] = bytearray(len(is_p[i * i :: i]))
    return is_p


def trial_division_is_prime(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 miller_rabin(n: int, rng: random.Random, rounds: int = 30) -> bool:
    """A reference probable prime test with random bases."""
    if n < 2:
        return False
    for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
        if n % p == 0:
            return n == p
    d, r = n - 1, 0
    while d % 2 == 0:
        d //= 2
        r += 1
    for _ in range(rounds):
        x = pow(rng.randrange(2, n - 1), d, n)
        if x in (1, n - 1):
            continue
        for _ in range(r - 1):
            x = x * x % n
            if x == n - 1:
                break
        else:
            return False
    return True


def next_prime(n: int, rng: random.Random) -> int:
    while not miller_rabin(n, rng):
        n += 1
    return n


def test_matches_sieve_up_to_limit() -> None:
    is_p = sieve(LIMIT)
    for n in range(LIMIT + 1):
        assert aprcl(n) is bool(is_p[n]), n


def test_small_inputs_and_even_numbers() -> None:
    assert aprcl(-7) is False
    assert aprcl(0) is False
    assert aprcl(1) is False
    assert aprcl(2) is True
    assert aprcl(3) is True
    assert aprcl(4) is False
    for n in (2**64, 2**127, 10**200, 2 * (2**127 - 1)):
        assert aprcl(n) is False


def test_prime_powers() -> None:
    rng = random.Random(1)
    p100 = next_prime(10**99, rng)
    for base, top in ((3, 40), (5, 30), (7, 25), (31, 12), (65537, 5)):
        for e in range(2, top):
            assert aprcl(base**e) is False
    assert aprcl(p100**2) is False
    assert aprcl((2**127 - 1) ** 2) is False
    assert aprcl(1000003**3) is False


def test_carmichael_numbers() -> None:
    for n in CARMICHAEL:
        assert aprcl(n) is False, n
    # A Chernick number (6k+1)(12k+1)(18k+1) of about 180 digits.
    rng = random.Random(2)
    k = 10**59 + 3
    while not all(miller_rabin(a * k + 1, rng) for a in (6, 12, 18)):
        k += 1
    n = (6 * k + 1) * (12 * k + 1) * (18 * k + 1)
    assert pow(2, n - 1, n) == 1
    assert aprcl(n) is False


def test_strong_pseudoprimes() -> None:
    for n in STRONG_PSEUDOPRIMES:
        assert aprcl(n) is False, n


def test_large_primes() -> None:
    rng = random.Random(3)
    for n in (
        2**61 - 1,
        2**89 - 1,
        2**127 - 1,
        10**100 + 267,
        2**521 - 1,
        next_prime(10**199, rng),
    ):
        assert aprcl(n) is True, n


def test_large_composites() -> None:
    rng = random.Random(4)
    p = next_prime(rng.randrange(10**149, 10**150), rng)
    q = next_prime(rng.randrange(10**149, 10**150), rng)
    assert aprcl(p * q) is False
    assert aprcl(2**523 - 1) is False
    assert aprcl(2**512 + 1) is False
    assert aprcl((2**127 - 1) * (2**89 - 1)) is False
    for _ in range(25):
        n = rng.randrange(10**299, 10**300) | 1
        if not miller_rabin(n, rng):
            assert aprcl(n) is False


def test_random_against_miller_rabin() -> None:
    rng = random.Random(5)
    seen_prime = 0
    for digits in (12, 20, 30, 45, 60):
        for _ in range(30):
            n = rng.randrange(10 ** (digits - 1), 10**digits)
            if rng.random() < 0.5:
                n = next_prime(n, rng)
            expected = miller_rabin(n, rng)
            seen_prime += expected
            assert aprcl(n) is expected, n
    assert seen_prime > 50


def test_plan_conditions() -> None:
    for digits in (1, 5, 30, 100, 300, 580):
        n = 10**digits + 1
        primes, s, t = plan(n)
        assert s == prod(primes) and s * s > n
        assert prod(primes[:-1]) ** 2 <= n
        assert t == lcm(*(q - 1 for q in primes))
        assert all(q == 2 or q % 4 == 3 for q in primes)
        assert all(t % (q - 1) == 0 for q in primes)
    with pytest.raises(ValueError):
        plan(10**700)
    with pytest.raises(ValueError):
        make_plan(10**6, (2, 3, 7))
    with pytest.raises(ValueError):
        make_plan(100, (2, 5, 7))


def test_table_is_increasing_and_t_has_one_factor_two() -> None:
    assert list(T_TABLE) == sorted(T_TABLE)
    sizes = []
    for t in T_TABLE:
        assert t % 2 == 0 and t % 4 != 0
        primes = candidate_primes(t)
        assert all(trial_division_is_prime(q) for q in primes if q < 10**6)
        sizes.append(prod(primes))
    assert sizes == sorted(sizes)
    assert sizes[-1] > 10**295


def test_units_w_not_divisible_by_p() -> None:
    for p, k in ((3, 4), (5, 3), (7, 2), (11, 2), (13, 2), (17, 1), (19, 1), (23, 1)):
        for e in range(1, k + 1):
            assert unit_w(p**e, p) % p != 0
    assert unit_w(3, 3) == 2 and unit_w(5, 5) == 1 and unit_w(9, 3) == 5


def test_jacobi_sums_have_norm_q() -> None:
    for q in candidate_primes(450450):
        if q > 3:
            for p in jacobi_sums(q):
                assert gauss_jacobi_check(q, p), (q, p)


def test_jacobi_sum_sizes() -> None:
    for q in (7, 11, 31, 43, 211, 2311):
        sums = jacobi_sums(q)
        for p, coeffs in sums.items():
            m = len(coeffs)
            assert (q - 1) % m == 0 and ((q - 1) // m) % p != 0
            assert sum(coeffs) == q - 2


def test_root_of_unity_for_primes() -> None:
    """For a prime n, S = chi(n)^(-w n): h = -w n ind(n) mod m."""
    rng = random.Random(6)
    for _ in range(40):
        n = next_prime(rng.randrange(10**6, 10**30), rng)
        for q in (7, 11, 31, 43, 71, 127, 211):
            if n % q == 0:
                continue
            g = primitive_root(q)
            ind = next(i for i in range(q - 1) if pow(g, i, q) == n % q)
            for p, coeffs in jacobi_sums(q).items():
                if p == 2:
                    continue
                m = len(coeffs)
                expected = (-unit_w(m, p) * n * ind) % m
                assert jacobi_power(n, q, p) == expected


def test_extra_check() -> None:
    is_p = sieve(20_000)
    uses = 0
    for n in range(5, 20_000, 2):
        if not is_p[n]:
            continue
        for p in (3, 5, 7):
            if n % p and pow(n, p - 1, p * p) == 1:
                uses += 1
                assert extra_check(n, p, set()) is True
        if n % 4 == 1:
            assert extra_check(n, 2, set()) is True
    assert uses > 100
    assert extra_check(25 * 41, 2, set()) is False
    assert extra_check(49 * 49, 2, set()) is False


def test_divisor_search_against_brute_force() -> None:
    rng = random.Random(7)
    for _ in range(300):
        primes = tuple(sorted(rng.sample([3, 7, 11, 19, 23, 31, 43, 47], 3))) + (2,)
        s = prod(primes)
        t = lcm(*(q - 1 for q in primes))
        n = rng.randrange(3, s * s)
        found = None
        for i in range(1, t):
            r = pow(n, i, s)
            if 1 < r < n and n % r == 0:
                found = r
                break
        assert divisor_search(n, s, t) == found


def test_divisor_search_finds_planted_factor() -> None:
    """If a prime r is a power n^i modulo s, the search finds a factor."""
    s, t = 2 * 3 * 7 * 11 * 31, 30
    hits = 0
    for r in (13, 17, 19, 29, 37, 41, 43, 61, 67, 71):
        for c in range(r + 2, 2 * s, 2):
            n = r * c
            if n < s * s and gcd(n, s * t) == 1:
                if any(pow(n, i, s) == r for i in range(1, t)):
                    hits += 1
                    assert divisor_search(n, s, t) is not None
    assert hits > 50


def test_jacobi_symbol() -> None:
    for n in range(3, 400, 2):
        for a in range(0, 60):
            if gcd(a, n) > 1:
                assert jacobi_symbol(a, n) == 0
            elif trial_division_is_prime(n):
                assert jacobi_symbol(a, n) == (1 if pow(a, (n - 1) // 2, n) == 1 else -1)


def test_worked_example() -> None:
    rep = trace(1999)
    assert rep.primes == (2, 3, 7, 11) and rep.s == 462 and rep.t == 30
    assert jacobi_sums(7)[3] == (2, 0, 3)
    assert jacobi_sums(11)[5] == (2, 4, 0, 1, 2)
    assert primitive_root(7) == 3 and primitive_root(11) == 2
    assert rep.roots == {(3, 7): 1, (5, 11): 3}
    assert rep.signs == {3: 1, 7: 1, 11: -1}
    assert pow(1999, 2, 9) == 1 and pow(1999, 4, 25) == 1
    assert rep.factor is None and aprcl(1999) is True
    assert 1999 % 462 == 151
    bad = trace(1891)
    assert bad.roots[(3, 7)] == -1 and aprcl(1891) is False

What the next level needs

  • the tests did not run in this build

Read the full page about APR-CL primality test