Dixon p-adic lifting in Python
Solves an integer linear system by lifting a solution modulo a prime power to a rational solution.
- File
dixon_p_adic_lifting.py, 125 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/dixon-p-adic-lifting/. The authors are listed in prov/YG36BM.jsonl.
from fractions import Fraction
from math import isqrt
from typing import Iterable, Iterator, Sequence
def _is_prime(n: int) -> bool:
"""Deterministic Miller-Rabin for n < 3.3 * 10^24."""
if n < 2:
return False
small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)
for q in small:
if n % q == 0:
return n == q
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 _default_primes() -> Iterator[int]:
"""Primes below 2^31, largest first."""
q = 2**31 - 1
while q > 2:
if _is_prime(q):
yield q
q -= 2
def inverse_mod_p(a: Sequence[Sequence[int]], p: int) -> list[list[int]] | None:
"""Inverse of a modulo the prime p by Gauss-Jordan elimination, or None if det a = 0 mod p."""
n = len(a)
m = [[x % p for x in row] + [int(i == j) for j in range(n)] for i, row in enumerate(a)]
for c in range(n):
piv = next((i for i in range(c, n) if m[i][c]), None)
if piv is None:
return None
m[c], m[piv] = m[piv], m[c]
inv = pow(m[c][c], -1, p)
m[c] = [x * inv % p for x in m[c]]
for i in range(n):
if i != c and m[i][c]:
f = m[i][c]
m[i] = [(x - f * y) % p for x, y in zip(m[i], m[c])]
return [row[n:] for row in m]
def rational_reconstruction(u: int, m: int, bound_n: int, bound_d: int) -> Fraction | None:
"""Return the fraction s/t with s = t u (mod m), |s| <= bound_n and 0 < t <= bound_d.
Requires 2 bound_n bound_d < m; then the fraction is unique if it exists.
Runs the extended Euclidean algorithm on (m, u) until the remainder is at most bound_n.
"""
r0, r1, t0, t1 = m, u % m, 0, 1
while r1 > bound_n:
q = r0 // r1
r0, r1, t0, t1 = r1, r0 - q * r1, t1, t0 - q * t1
if t1 == 0 or abs(t1) > bound_d:
return None
return Fraction(r1, t1)
def dixon_solve(a: Sequence[Sequence[int]], b: Sequence[int],
primes: Iterable[int] | None = None) -> list[Fraction] | None:
"""Solve a x = b for a square integer matrix a; return the rational solution, or None if a is singular.
Cost: O(n^3) operations modulo p for the inverse, then O(n^2) per p-adic digit for
k = O(n (b + log n) / log p) digits, and n rational reconstructions:
O(n^3 (b + log n)^2) bit operations with schoolbook arithmetic and a word-size p.
"""
n = len(a)
if n == 0:
return []
col2 = [sum(a[i][j] ** 2 for i in range(n)) for j in range(n)]
prod = 1
for c in col2:
prod *= c
had = isqrt(prod) + 1 # |det a| < had, by Hadamard on the columns
p, inv, tried = 0, None, 1
for q in primes if primes is not None else _default_primes(): # @line L1
inv = inverse_mod_p(a, q)
if inv is not None:
p = q
break
tried *= q
if tried > had:
return None # det a = 0 modulo a product larger than |det a|
if inv is None:
raise ValueError("ran out of primes")
bound_d = had # @line L2
bound_n = isqrt(prod * sum(x * x for x in b)) + 1
k, pk = 0, 1 # @line L3
while pk <= 2 * bound_n * bound_d:
k, pk = k + 1, pk * p
r = list(b) # @line L4
xbar = [0] * n
pi = 1
for _ in range(k): # @line L5
y = [sum(inv[i][j] * r[j] for j in range(n)) % p for i in range(n)] # @line L6
xbar = [xbar[i] + y[i] * pi for i in range(n)] # @line L7
pi *= p
new = []
for i in range(n): # @line L8
t = r[i] - sum(a[i][j] * y[j] for j in range(n))
assert t % p == 0
new.append(t // p)
r = new
x = []
for j in range(n): # @line L9
f = rational_reconstruction(xbar[j], pk, bound_n, bound_d) # @line L10
assert f is not None
x.append(f)
return x # @line L11
test_dixon_p_adic_lifting.py 93 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dixon-p-adic-lifting/. The authors are listed in prov/YG36BM.jsonl.
import random
from fractions import Fraction
from itertools import permutations
from dixon_p_adic_lifting import dixon_solve, inverse_mod_p, rational_reconstruction
SMALL_PRIMES = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
def det_leibniz(a):
n = len(a)
total = 0
for p in permutations(range(n)):
inv = sum(1 for i in range(n) for j in range(i + 1, n) if p[i] > p[j])
t = 1
for i in range(n):
t *= a[p[i]][i]
total += -t if inv % 2 else t
return total
def check(a, b, x):
n = len(a)
assert all(sum(a[i][j] * x[j] for j in range(n)) == b[i] for i in range(n))
d = det_leibniz(a)
assert all((x[j] * d).denominator == 1 for j in range(n)) # Cramer: det(A) x is integral
def test_example() -> None:
m = [[2, 1, 0], [1, 3, 1], [0, 1, 4]]
assert dixon_solve(m, [1, 0, 7]) == [1, -1, 2]
assert dixon_solve(m, [1, 2, 3], primes=[7]) == [Fraction(1, 3), Fraction(1, 3), Fraction(2, 3)]
def test_edges() -> None:
assert dixon_solve([], []) == []
assert dixon_solve([[3]], [2]) == [Fraction(2, 3)]
assert dixon_solve([[0]], [1]) is None
assert dixon_solve([[1, 2], [2, 4]], [1, 2]) is None
assert dixon_solve([[0, 1], [1, 0]], [5, 7]) == [7, 5] # needs a swap modulo p
assert dixon_solve([[1, 0], [0, 0]], [0, 0], primes=SMALL_PRIMES) is None
assert dixon_solve([[2, 0], [0, 3]], [0, 0]) == [0, 0]
def test_unlucky_primes() -> None:
# det = 3 * 5 * 7 = 105, so the first three primes divide the determinant
a = [[3, 0, 0], [1, 5, 0], [2, 4, 7]]
for _ in range(3):
assert inverse_mod_p(a, 3) is None
x = dixon_solve(a, [1, 1, 1], primes=SMALL_PRIMES)
check(a, [1, 1, 1], x)
def test_rational_reconstruction() -> None:
m = 10007
for s in range(-30, 31):
for t in range(1, 31):
if Fraction(s, t).denominator != t:
continue
u = s * pow(t, -1, m) % m
assert rational_reconstruction(u, m, 30, 30) == Fraction(s, t)
def test_random() -> None:
rng = random.Random(1982)
for trial in range(600):
n = rng.randint(1, 5)
a = [[rng.randint(-6, 6) for _ in range(n)] for _ in range(n)]
if rng.random() < 0.15:
a[rng.randrange(n)] = [0] * n
if rng.random() < 0.15 and n > 1:
i, j = rng.sample(range(n), 2)
a[i] = [2 * x for x in a[j]]
b = [rng.randint(-9, 9) for _ in range(n)]
primes = SMALL_PRIMES if trial % 2 else None
x = dixon_solve(a, b, primes=primes)
if det_leibniz(a) == 0:
assert x is None
else:
check(a, b, x)
def test_large_entries() -> None:
rng = random.Random(7)
for _ in range(20):
n = 6
a = [[rng.randint(-10**30, 10**30) for _ in range(n)] for _ in range(n)]
b = [rng.randint(-10**30, 10**30) for _ in range(n)]
x = dixon_solve(a, b)
assert all(sum(a[i][j] * x[j] for j in range(n)) == b[i] for i in range(n))
What the next level needs
- the tests did not run in this build