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