Seysen reduction in Python
Basis reduction that minimizes a global size measure on the basis and its dual.
- File
seysen_reduction.py, 86 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/seysen-reduction/. The authors are listed in prov/WRVXKA.jsonl.
from fractions import Fraction
from math import floor
from typing import Sequence, Union
Number = Union[int, Fraction]
Basis = list[list[Number]]
def _dot(u: Sequence[Number], v: Sequence[Number]) -> Number:
return sum((a * b for a, b in zip(u, v)), 0)
def _inverse(a: list[list[Fraction]]) -> list[list[Fraction]]:
"""Exact inverse by Gauss-Jordan elimination; ValueError if a is singular."""
n = len(a)
m = [list(row) + [Fraction(int(i == j)) for j in range(n)] for i, row in enumerate(a)]
for c in range(n):
p = next((r for r in range(c, n) if m[r][c] != 0), None)
if p is None:
raise ValueError("the rows are linearly dependent")
m[c], m[p] = m[p], m[c]
piv = m[c][c]
m[c] = [x / piv for x in m[c]]
for r in range(n):
if r != c and m[r][c] != 0:
f = m[r][c]
m[r] = [x - f * y for x, y in zip(m[r], m[c])]
return [row[n:] for row in m]
def seysen_measure(basis: Sequence[Sequence[Number]]) -> Fraction:
"""S(B) = sum_i ||b_i||^2 ||d_i||^2, where d_i is the dual basis."""
b = [list(r) for r in basis]
g = [[Fraction(_dot(u, v)) for v in b] for u in b]
h = _inverse(g)
return sum((g[i][i] * h[i][i] for i in range(len(b))), Fraction(0))
def seysen_reduce_counted(basis: Sequence[Sequence[Number]]) -> tuple[Basis, int]:
"""Seysen reduction with exact arithmetic; returns (reduced basis, number of updates).
Input: n linearly independent rows of equal length, int or Fraction entries.
Output: a basis of the same lattice such that for every ordered pair i != j,
|lambda*_ij| <= 1/2, so no update b_j <- b_j + l b_i with integer l lowers S.
Each update lowers S(B) det(G); for an integer basis this is a positive integer,
so there are at most (S_0 - n) det(G) updates.
Raises ValueError for ragged or linearly dependent rows.
"""
b: Basis = [list(r) for r in basis]
n = len(b)
if n == 0:
return [], 0
if any(len(r) != len(b[0]) for r in b):
raise ValueError("all rows must have the same length")
g = [[Fraction(_dot(u, v)) for v in b] for u in b] # @line L1
h = _inverse(g) # h[i][j] = <d_i, d_j>
steps = 0
while True: # @line L2
changed = False # @line L3
for i in range(n): # @line L4
for j in range(n):
if i == j:
continue
star = (h[i][j] / h[j][j] - g[i][j] / g[i][i]) / 2 # @line L5
lam = floor(star + Fraction(1, 2)) # @line L6
if lam * lam - 2 * lam * star < 0: # @line L7
b[j] = [x + lam * y for x, y in zip(b[j], b[i])] # @line L8
# @line L9: b_j += lam b_i and d_i -= lam d_j, on both Gram matrices
g[j] = [x + lam * y for x, y in zip(g[j], g[i])]
for r in range(n):
g[r][j] += lam * g[r][i]
h[i] = [x - lam * y for x, y in zip(h[i], h[j])]
for r in range(n):
h[r][i] -= lam * h[r][j]
steps += 1
changed = True # @line L10
if not changed: # @line L11
return b, steps # @line L12
def seysen_reduce(basis: Sequence[Sequence[Number]]) -> Basis:
"""Seysen-reduced basis of the lattice generated by the rows of `basis`."""
return seysen_reduce_counted(basis)[0]
test_seysen_reduction.py 198 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/seysen-reduction/. The authors are listed in prov/WRVXKA.jsonl.
import itertools
import random
from fractions import Fraction
from math import floor, isqrt
import pytest
from seysen_reduction import seysen_measure, seysen_reduce, seysen_reduce_counted
def dot(u, v):
return sum((a * b for a, b in zip(u, v)), 0)
def gram(b):
return [[Fraction(dot(x, y)) for y in b] for x in b]
def inverse(a):
n = len(a)
m = [list(row) + [Fraction(int(i == j)) for j in range(n)] for i, row in enumerate(a)]
for c in range(n):
p = next(r for r in range(c, n) if m[r][c] != 0)
m[c], m[p] = m[p], m[c]
piv = m[c][c]
m[c] = [x / piv for x in m[c]]
for r in range(n):
if r != c and m[r][c] != 0:
f = m[r][c]
m[r] = [x - f * y for x, y in zip(m[r], m[c])]
return [row[n:] for row in m]
def det(a):
m = [[Fraction(x) for x in row] for row in a]
n, d = len(m), Fraction(1)
for c in range(n):
p = next((r for r in range(c, n) if m[r][c] != 0), None)
if p is None:
return Fraction(0)
if p != c:
m[c], m[p] = m[p], m[c]
d = -d
d *= m[c][c]
for r in range(c + 1, n):
f = m[r][c] / m[c][c]
m[r] = [x - f * y for x, y in zip(m[r], m[c])]
return d
def same_lattice(b, c):
"""True if c = U b with U an integer matrix of determinant +-1."""
if len(b) != len(c):
return False
gi = inverse(gram(b))
u = []
for row in c:
r = [Fraction(dot(row, x)) for x in b]
coeff = [sum((r[j] * gi[j][i] for j in range(len(b))), Fraction(0)) for i in range(len(b))]
if any(x.denominator != 1 for x in coeff):
return False
if [sum((coeff[i] * b[i][t] for i in range(len(b))), Fraction(0)) for t in range(len(b[0]))] != list(map(Fraction, row)):
return False
u.append(coeff)
return abs(det(u)) == 1
def measure(b):
g = gram(b)
h = inverse(g)
return sum((g[i][i] * h[i][i] for i in range(len(b))), Fraction(0))
def is_seysen_reduced(b):
"""No update b_j <- b_j + l b_i with integer l != 0 lowers S: |lambda*_ij| <= 1/2."""
g = gram(b)
h = inverse(g)
base = measure(b)
for i in range(len(b)):
for j in range(len(b)):
if i != j:
star = (h[i][j] / h[j][j] - g[i][j] / g[i][i]) / 2
if abs(star) > Fraction(1, 2):
return False
# and directly: no small l lowers the measure
for lam in (-2, -1, 1, 2):
c = [list(r) for r in b]
c[j] = [x + lam * y for x, y in zip(c[j], c[i])]
if measure(c) < base:
return False
return True
def check(b):
c, steps = seysen_reduce_counted(b)
assert same_lattice(b, c)
assert is_seysen_reduced(c)
s0, s1 = measure(b), measure(c)
assert s1 <= s0 and s1 >= len(b)
assert seysen_measure(c) == s1
if all(isinstance(x, int) for r in b for x in r):
dg = det(gram(b))
assert (s0 * dg).denominator == 1 and (s1 * dg).denominator == 1
assert steps <= (s0 - s1) * dg
return c, steps
def minima_2d(b):
"""lambda_1^2 and lambda_2^2 of a rank 2 lattice by a proved box search."""
gi = inverse(gram(b))
r2 = max(dot(u, u) for u in b)
ranges = [range(-isqrt(floor(r2 * gi[i][i])), isqrt(floor(r2 * gi[i][i])) + 1) for i in range(2)]
vecs = sorted(
(dot(v, v), x)
for x in itertools.product(*ranges)
if any(x)
for v in [[x[0] * b[0][t] + x[1] * b[1][t] for t in range(len(b[0]))]]
)
s1, x1 = vecs[0]
s2 = next(s for s, x in vecs if x[0] * x1[1] - x[1] * x1[0] != 0)
return s1, s2
def random_basis(rng, n, m, lo, hi):
while True:
b = [[rng.randint(lo, hi) for _ in range(m)] for _ in range(n)]
if det(gram(b)) != 0:
return b
def skew(rng, b, steps):
b = [list(r) for r in b]
for _ in range(steps):
i, j = rng.sample(range(len(b)), 2)
k = rng.choice([-3, -2, -1, 1, 2, 3])
b[i] = [p + k * q for p, q in zip(b[i], b[j])]
return b
def test_worked_examples():
c, steps = check([[5, 3], [8, 5]])
assert c == [[-1, 0], [0, -1]] and steps == 3
assert measure([[5, 3], [8, 5]]) == 6052
c, steps = check([[1, 1, 1], [-1, 0, 2], [3, 5, 6]])
assert c == [[-1, 0, -1], [-2, 0, 1], [0, -1, 0]] and steps == 7
assert measure(c) == Fraction(29, 9)
def test_dimension_one_and_empty():
assert seysen_reduce_counted([[3, -4]]) == ([[3, -4]], 0)
assert seysen_reduce_counted([]) == ([], 0)
def test_orthogonal_is_fixed():
b = [[2, 0, 0], [0, 3, 0], [0, 0, 1]]
assert seysen_reduce_counted(b) == (b, 0)
assert seysen_measure(b) == 3
def test_nearly_dependent():
check([[1, 0], [1000, 1]])
check([[1, 1, 0], [1, 1, 1], [100, 101, 0]])
check([[1, 0, 0], [0, 1, 0], [99, 100, 1]])
def test_fraction_entries():
check([[Fraction(1, 2), Fraction(1, 3)], [Fraction(5, 4), Fraction(2, 3)]])
def test_two_dimensions_reaches_minima():
rng = random.Random(5)
for _ in range(150):
b = skew(rng, random_basis(rng, 2, rng.choice([2, 3]), -9, 9), 4)
c, _ = check(b)
c.sort(key=lambda v: dot(v, v))
g = gram(c)
assert abs(g[0][1]) * 2 <= g[0][0] <= g[1][1] # Lagrange-Gauss reduced
assert (g[0][0], g[1][1]) == minima_2d(c) # c is a basis of the same lattice
def test_random_bases():
rng = random.Random(20251106)
for n in range(2, 7):
for _ in range(30 if n < 6 else 15):
b = random_basis(rng, n, n + rng.randint(0, 1), -5, 5)
if n <= 4:
b = skew(rng, b, 3)
check(b)
def test_errors():
with pytest.raises(ValueError):
seysen_reduce([[1, 2], [2, 4]])
with pytest.raises(ValueError):
seysen_reduce([[1, 2], [1]])
What the next level needs
- the tests did not run in this build