Toom-Cook multiplication in Python
Splits integers into k parts, evaluates at points and interpolates, generalizing Karatsuba.
- File
toom_cook_multiplication.py, 231 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/toom-cook-multiplication/. The authors are listed in prov/KFMRJ2.jsonl.
"""Toom-3 multiplication of limb arrays, with Bodrato's evaluation and
interpolation sequence at the points 0, 1, -1, -2 and infinity.
Limb arrays are lists of ints in [0, 2^w), least significant limb first.
Signed intermediate values are pairs (sign, magnitude) with sign in {1, -1}
and a trimmed magnitude limb array. A generic Toom-k on Python ints shows the
general method.
"""
from fractions import Fraction
def _trim(a: list[int]) -> list[int]:
a = list(a)
while a and a[-1] == 0:
a.pop()
return a
def _cmp(a: list[int], b: list[int]) -> int:
a, b = _trim(a), _trim(b)
if len(a) != len(b):
return -1 if len(a) < len(b) else 1
for x, y in zip(reversed(a), reversed(b)):
if x != y:
return -1 if x < y else 1
return 0
def _add_mag(a: list[int], b: list[int], w: int) -> list[int]:
mask = (1 << w) - 1
if len(a) < len(b):
a, b = b, a
out, carry = [], 0
for i, x in enumerate(a):
t = x + (b[i] if i < len(b) else 0) + carry
out.append(t & mask)
carry = t >> w
if carry:
out.append(carry)
return _trim(out)
def _sub_mag(a: list[int], b: list[int], w: int) -> list[int]:
"""a - b for magnitudes with a >= b."""
mask = (1 << w) - 1
out, borrow = [], 0
for i, x in enumerate(a):
t = x - (b[i] if i < len(b) else 0) - borrow
borrow = 1 if t < 0 else 0
out.append(t & mask)
if borrow:
raise ValueError("negative magnitude")
return _trim(out)
def s_add(x: tuple, y: tuple, w: int) -> tuple:
(sx, a), (sy, b) = x, y
if sx == sy:
return sx, _add_mag(a, b, w)
c = _cmp(a, b)
if c >= 0:
return (sx if c > 0 else 1), _sub_mag(a, b, w)
return sy, _sub_mag(b, a, w)
def s_sub(x: tuple, y: tuple, w: int) -> tuple:
return s_add(x, (-y[0], y[1]), w)
def s_mul_small(x: tuple, d: int, w: int) -> tuple:
mask = (1 << w) - 1
out, carry = [], 0
for v in x[1]:
t = v * d + carry
out.append(t & mask)
carry = t >> w
while carry:
out.append(carry & mask)
carry >>= w
return x[0], _trim(out)
def s_div_exact(x: tuple, d: int, w: int) -> tuple:
"""x / d for a small d that divides x exactly (long division by one limb)."""
out, r = [0] * len(x[1]), 0
for i in range(len(x[1]) - 1, -1, -1):
t = (r << w) | x[1][i]
out[i], r = divmod(t, d)
if r:
raise ValueError("division is not exact")
return x[0], _trim(out)
def _schoolbook(a: list[int], b: list[int], w: int) -> list[int]:
mask = (1 << w) - 1
c = [0] * (len(a) + len(b))
for i, x in enumerate(a):
carry = 0
for j, y in enumerate(b):
t = c[i + j] + x * y + carry
c[i + j] = t & mask
carry = t >> w
c[i + len(b)] = carry
return c
def _pad(a: list[int], n: int) -> list[int]:
return list(a) + [0] * (n - len(a))
class Stats:
def __init__(self) -> None:
self.calls = 0
self.leaves = 0
self.max_depth = 0
def toom3(a: list[int], b: list[int], w: int = 8, threshold: int = 3, st: Stats | None = None, depth: int = 0) -> list[int]:
"""Product of two limb arrays of the same length n, as 2n limbs.
Five recursive products of k + 1 limbs, where k = ceil(n / 3); the depth is
about log3(n / threshold).
"""
n = len(a)
if n != len(b):
raise ValueError("toom3() needs operands of the same length")
if w < 3:
raise ValueError("the values at the points need w >= 3")
if st is not None:
st.calls += 1
st.max_depth = max(st.max_depth, depth)
if n <= max(threshold, 2): # @line L1
if st is not None:
st.leaves += 1
return _schoolbook(a, b, w)
k = (n + 2) // 3 # @line L2: a = a2 x^2 + a1 x + a0 with x = beta^k
a0, a1, a2 = a[:k], a[k:2 * k], a[2 * k:]
b0, b1, b2 = b[:k], b[k:2 * k], b[2 * k:]
def evaluate(c0, c1, c2): # @line L3: values at 0, 1, -1, -2, infinity
p0, q1, q2 = (1, _trim(c0)), (1, _trim(c1)), (1, _trim(c2))
p = s_add(p0, q2, w)
p1 = s_add(p, q1, w)
pm1 = s_sub(p, q1, w)
pm2 = s_sub(s_mul_small(s_add(pm1, q2, w), 2, w), p0, w)
return [p0, p1, pm1, pm2, q2]
ea, eb = evaluate(a0, a1, a2), evaluate(b0, b1, b2)
r = []
for (sa, ma), (sb, mb) in zip(ea, eb): # @line L4: five products of k + 1 limbs
prod = toom3(_pad(ma, k + 1), _pad(mb, k + 1), w, threshold, st, depth + 1)
r.append((sa * sb, _trim(prod)))
v0, v1, vm1, vm2, vinf = r
r0, r4 = v0, vinf # @line L5: interpolation (Bodrato's sequence)
r3 = s_div_exact(s_sub(vm2, v1, w), 3, w)
r1 = s_div_exact(s_sub(v1, vm1, w), 2, w)
r2 = s_sub(vm1, v0, w)
r3 = s_add(s_div_exact(s_sub(r2, r3, w), 2, w), s_mul_small(r4, 2, w), w)
r2 = s_sub(s_add(r2, r1, w), r4, w)
r1 = s_sub(r1, r3, w)
c = [0] * (2 * n + 4 * k + 2) # @line L6: c = r0 + r1 x + r2 x^2 + r3 x^3 + r4 x^4
for i, (sg, mag) in enumerate((r0, r1, r2, r3, r4)):
if mag and sg < 0:
raise ValueError("negative coefficient")
shifted = [0] * (i * k) + mag
c = _add_mag(c, shifted, w)
return _pad(c, 2 * n)[: 2 * n]
def toom3_multiply(a: list[int], b: list[int], w: int = 8, threshold: int = 3) -> list[int]:
"""Product of limb arrays of any lengths, as len(a) + len(b) limbs (pads the shorter)."""
m, n = len(a), len(b)
if m == 0 or n == 0:
return [0] * (m + n)
L = max(m, n)
c = toom3(_pad(a, L), _pad(b, L), w, threshold)
return c[: m + n]
def toom_k(x: int, y: int, k: int, digits: int, threshold: int = 1, counter: list[int] | None = None) -> int:
"""Generic Toom-k on nonnegative Python ints of at most `digits` limbs of 8 binary digits.
Splits into k pieces of ceil(digits / k) limbs, evaluates at the 2k - 1 points
0, 1, -1, 2, -2, ... and infinity, multiplies recursively and interpolates
exactly with rational Lagrange coefficients.
"""
if digits <= max(threshold, k + 1) or k < 2:
if counter is not None:
counter[0] += 1
return x * y
h = -(-digits // k)
X = 1 << (8 * h)
xs = [(x >> (8 * h * i)) & (X - 1) for i in range(k)]
ys = [(y >> (8 * h * i)) & (X - 1) for i in range(k)]
pts = [0]
j = 1
while len(pts) < 2 * k - 2:
pts.append(j)
if len(pts) < 2 * k - 2:
pts.append(-j)
j += 1
vals = []
for t in pts:
u = sum(c * t**i for i, c in enumerate(xs))
v = sum(c * t**i for i, c in enumerate(ys))
s = (u < 0) != (v < 0)
size = -(-max(abs(u).bit_length(), abs(v).bit_length(), 1) // 8) # h + O(log k) limbs
pr = toom_k(abs(u), abs(v), k, size, threshold, counter)
vals.append(-pr if s else pr)
top = toom_k(xs[-1], ys[-1], k, h, threshold, counter) # the value at infinity
# remove the leading term, then interpolate the remaining 2k - 2 coefficients
m = 2 * k - 2
rest = [vals[i] - top * pts[i] ** m for i in range(m)]
coef = [Fraction(0)] * m
for i in range(m):
num, den = [Fraction(1)], Fraction(1)
for j2 in range(m):
if j2 != i:
num = [Fraction(0)] + num
for t in range(len(num) - 1):
num[t] -= pts[j2] * num[t + 1]
den *= pts[i] - pts[j2]
for t in range(m):
coef[t] += rest[i] * num[t] / den
coefs = [int(c) for c in coef] + [top]
assert all(c.denominator == 1 for c in coef)
return sum(c << (8 * h * i) for i, c in enumerate(coefs))
test_toom_cook_multiplication.py 133 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/toom-cook-multiplication/. The authors are listed in prov/KFMRJ2.jsonl.
import math
import random
import pytest
import toom_cook_multiplication as tc
from toom_cook_multiplication import Stats, s_div_exact, toom3, toom3_multiply, toom_k
def limbs(x: int, w: int, n: int) -> list[int]:
return [(x >> (w * i)) & ((1 << w) - 1) for i in range(n)]
def value(d: list[int], w: int) -> int:
v = 0
for x in reversed(d):
v = (v << w) | x
return v
def test_example_by_hand() -> None:
a2, a1, a0, b2, b1, b0 = 12, 34, 56, 78, 90, 12
def ev(c2, c1, c0):
p = c0 + c2
return [c0, p + c1, p - c1, 2 * (p - c1 + c2) - c0, c2]
assert ev(a2, a1, a0) == [56, 102, 34, 36, 12]
assert ev(b2, b1, b0) == [12, 180, 0, 144, 78]
v0, v1, vm1, vm2, vinf = [x * y for x, y in zip(ev(a2, a1, a0), ev(b2, b1, b0))]
assert (v0, v1, vm1, vm2, vinf) == (672, 18360, 0, 5184, 936)
r3 = (vm2 - v1) // 3
r1 = (v1 - vm1) // 2
r2 = vm1 - v0
assert (r3, r1, r2) == (-4392, 9180, -672)
r3 = (r2 - r3) // 2 + 2 * vinf
r2 = r2 + r1 - vinf
r1 = r1 - r3
assert [v0, r1, r2, r3, vinf] == [672, 5448, 7572, 3732, 936]
assert sum(c * 100**i for i, c in enumerate([v0, r1, r2, r3, vinf])) == 97408265472 == 123456 * 789012
def test_example_on_limbs() -> None:
# the same product with 8-bit limbs
x, y = 123456, 789012
assert value(toom3(limbs(x, 8, 3), limbs(y, 8, 3), 8, 2), 8) == x * y
@pytest.mark.parametrize("w", [4, 8])
def test_random(w: int) -> None:
rng = random.Random(100 + w)
for n in range(1, 31):
for t in range(8):
x, y = rng.randrange(1 << (w * n)), rng.randrange(1 << (w * n))
th = 2 if t % 2 else 4
out = toom3(limbs(x, w, n), limbs(y, w, n), w, th)
assert len(out) == 2 * n
assert value(out, w) == x * y
@pytest.mark.parametrize("w", [4, 8])
def test_edges(w: int) -> None:
B = 1 << w
for n in (1, 2, 3, 4, 5, 6, 7, 9, 10, 27, 28):
top = B**n - 1
for x in (0, 1, top, B ** (n - 1), top - 1, B ** (n // 2)):
for y in (0, 1, top, B ** (n - 1)):
assert value(toom3(limbs(x, w, n), limbs(y, w, n), w, 2), w) == x * y
def test_unequal_lengths() -> None:
rng = random.Random(5)
for _ in range(200):
m, n = rng.randint(0, 20), rng.randint(0, 20)
x, y = rng.randrange(1 << (8 * m)) if m else 0, rng.randrange(1 << (8 * n)) if n else 0
out = toom3_multiply(limbs(x, 8, m), limbs(y, 8, n), 8, 2)
assert len(out) == m + n
assert value(out, 8) == x * y
def test_counts_and_depth() -> None:
counted = [0]
orig = tc._schoolbook
def counting(a, b, w):
counted[0] += len(a) * len(b)
return orig(a, b, w)
tc._schoolbook = counting
try:
results = {}
for n in (729, 2187):
counted[0] = 0
st = Stats()
toom3([255] * n, [255] * n, 8, 8, st)
results[n] = (counted[0], st.max_depth, st.leaves)
finally:
tc._schoolbook = orig
assert results[729] == (78125, 5, 5**5)
assert results[2187] == (390625, 6, 5**6)
for n in (10, 100, 1000):
st = Stats()
toom3([1] * n, [1] * n, 8, 2, st)
assert st.max_depth <= math.ceil(math.log(2 * n, 3)) + 1
def test_exact_division_check() -> None:
with pytest.raises(ValueError):
s_div_exact((1, [7]), 3, 8)
with pytest.raises(ValueError):
toom3([1], [1, 2], 8)
with pytest.raises(ValueError):
toom3([1, 2, 3], [1, 2, 3], 2)
def test_generic_toom_k() -> None:
rng = random.Random(9)
for k in (2, 3, 4, 5):
for d in (1, 4, 9, 30, 70):
for _ in range(6):
x, y = rng.randrange(1 << (8 * d)), rng.randrange(1 << (8 * d))
assert toom_k(x, y, k, d) == x * y
top = (1 << 240) - 1
assert toom_k(top, top, k, 30) == top * top
assert toom_k(0, top, k, 30) == 0
def test_exponents() -> None:
e = [math.log(2 * k - 1, k) for k in (2, 3, 4, 5)]
assert [round(v, 3) for v in e] == [1.585, 1.465, 1.404, 1.365]
What the next level needs
- the tests did not run in this build