Coppersmith-Winograd algorithm in Python
The 1990 bound ω < 2.375477 (about 2.376 originally) from a tensor of border rank q+2 and the laser method; its algorithms are galactic and never used in practice.
- File
coppersmith_winograd_algorithm.py, 216 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/coppersmith-winograd-algorithm/. The authors are listed in prov/YF5DJV.jsonl.
"""The basic construction of Coppersmith and Winograd.
The Coppersmith-Winograd tensor for q >= 0 lives in R^(q+2) (x) R^(q+2) (x) R^(q+2):
T_q = sum_{i=1..q} (x0 yi zi + xi y0 zi + xi yi z0) + x0 y0 z_{q+1} + x0 y_{q+1} z0 + x_{q+1} y0 z0.
As a bilinear map phi(x, y) with output index k (the index of z):
phi_0 = sum_i xi yi + x0 y_{q+1} + x_{q+1} y0, phi_i = x0 yi + xi y0, phi_{q+1} = x0 y0.
The main function computes phi with q + 2 products of polynomials in a
central indeterminate eps, the approximate algorithm of border rank q + 2
and error degree 3, and reads off the coefficient of eps^3. This is the
small tensor at the start of the laser method; the matrix multiplication
algorithm built from it is galactic and is not implemented here.
"""
import math
from fractions import Fraction
from typing import Optional
Poly = list[int] # coefficients of 1, eps, eps^2, ...
def _add(a: Poly, b: Poly) -> Poly:
n = max(len(a), len(b))
return [(a[i] if i < len(a) else 0) + (b[i] if i < len(b) else 0) for i in range(n)]
def _scale(c: int, a: Poly) -> Poly:
return [c * v for v in a]
def _shift(k: int, a: Poly) -> Poly:
return [0] * k + a
def _mul(a: Poly, b: Poly) -> Poly:
out = [0] * (len(a) + len(b) - 1)
for i, u in enumerate(a):
if u:
for j, v in enumerate(b):
out[i + j] += u * v
return out
def _coef(a: Poly, k: int) -> int:
return a[k] if k < len(a) else 0
def _check(x: list[int], y: list[int], q: int) -> None:
if q < 0:
raise ValueError("q must be nonnegative")
if len(x) != q + 2 or len(y) != q + 2:
raise ValueError("x and y must have length q + 2")
def coppersmith_winograd_algorithm(
x: list[int], y: list[int], q: int, mod: Optional[int] = None
) -> list[int]:
"""Return phi(x, y) for the CW tensor T_q, computed with q + 2 products in R[eps].
Precondition: len(x) == len(y) == q + 2 and q >= 0. The entries may be
in any ring; only x-times-y products are formed, so commutativity of the
entries is not used. Cost: q + 2 products of polynomials of degree at
most 3 in eps and O(q) additions of such polynomials.
"""
_check(x, y, q)
x0, y0 = x[0], y[0]
prods: list[Poly] = [[]] # 1-based
for i in range(1, q + 1): # @line L1
prods.append(_mul([x0, x[i]], [y0, y[i]]))
big_x = sum(x[1 : q + 1]) # @line L2
big_y = sum(y[1 : q + 1])
prods.append(_mul([x0, 0, big_x], [y0, 0, big_y])) # @line L3
prods.append(_mul([x0, 0, 0, x[q + 1]], [y0, 0, 0, y[q + 1]])) # @line L4
p_sum: Poly = [0]
for i in range(1, q + 1):
p_sum = _add(p_sum, prods[i])
c0 = _add(_add(_shift(1, p_sum), _scale(-1, prods[q + 1])), _mul([1, -q], prods[q + 2])) # @line L5
cs = [c0]
for i in range(1, q + 1): # @line L6
cs.append(_shift(2, _add(prods[i], _scale(-1, prods[q + 1]))))
cs.append(_shift(3, prods[q + 2])) # @line L7
out = [_coef(c, 3) for c in cs] # @line L8
if mod is not None:
out = [v % mod for v in out]
return out
def count_products(q: int) -> int:
"""Number of products in R[eps] made by the main function: q + 2."""
if q < 0:
raise ValueError("q must be nonnegative")
return q + 2
def decomposition(q: int) -> list[tuple[list[Poly], list[Poly], list[Poly]]]:
"""The q + 2 rank-one terms u (x) v (x) w over R[eps] with sum eps^3 T_q + O(eps^4).
Each vector is a list of q + 2 polynomials in eps. The w vectors are
truncated to degree 3, which does not change the coefficients of
eps^0, ..., eps^3.
"""
if q < 0:
raise ValueError("q must be nonnegative")
d = q + 2
def vec(entries: dict[int, Poly]) -> list[Poly]:
return [entries.get(k, [0]) for k in range(d)]
terms = []
for i in range(1, q + 1):
u = vec({0: [1], i: [0, 1]})
w = vec({0: [0, 1], i: [0, 0, 1]})
terms.append((u, u, w))
s = vec({**{i: [0, 0, 1] for i in range(1, q + 1)}, 0: [1]})
terms.append((s, s, [_scale(-1, p) for p in s]))
t = vec({0: [1], q + 1: [0, 0, 0, 1]})
terms.append((t, t, vec({0: [1, -q], q + 1: [0, 0, 0, 1]})))
return terms
def cw_exact_by_interpolation(
x: list[int], y: list[int], q: int, mod: Optional[int] = None
) -> list[int]:
"""Exact phi(x, y) from 7 evaluations of the approximate algorithm, 7(q + 2) scalar products.
With the truncated w, f(eps) = eps^(-3) * (sum of the terms at eps) is a
polynomial of degree at most 6 in eps with f(0) = phi(x, y). It is
evaluated at eps = 1..7 and interpolated at 0. Over Z the arithmetic is
in Q; with mod, mod must be a prime larger than 7.
"""
_check(x, y, q)
if mod is not None and (mod <= 7 or any(mod % p == 0 for p in range(2, int(mod**0.5) + 1))):
raise ValueError("mod must be a prime larger than 7")
terms = decomposition(q)
points = list(range(1, 8))
def ev(p: Poly, e: int) -> int:
return sum(c * e**k for k, c in enumerate(p))
values = []
for e in points:
out = [0] * (q + 2)
for u, v, w in terms:
m = sum(ev(u[a], e) * x[a] for a in range(q + 2)) * sum(ev(v[b], e) * y[b] for b in range(q + 2))
for k in range(q + 2):
out[k] += m * ev(w[k], e)
values.append(out)
result = []
for k in range(q + 2):
acc = Fraction(0)
for t, e in enumerate(points):
lam = Fraction(1)
for s, f in enumerate(points):
if s != t:
lam *= Fraction(-f, e - f)
acc += lam * Fraction(values[t][k], e**3)
if mod is None:
assert acc.denominator == 1
result.append(int(acc))
else:
result.append(acc.numerator * pow(acc.denominator, -1, mod) % mod)
return result
def cw_approximate(x: list[float], y: list[float], q: int, eps: float) -> list[float]:
"""The approximate algorithm at a fixed eps in floating point: phi(x, y) + O(eps)."""
_check(x, y, q)
terms = decomposition(q)
def ev(p: Poly) -> float:
return sum(c * eps**k for k, c in enumerate(p))
out = [0.0] * (q + 2)
for u, v, w in terms:
m = sum(ev(u[a]) * x[a] for a in range(q + 2)) * sum(ev(v[b]) * y[b] for b in range(q + 2))
for k in range(q + 2):
out[k] += m * ev(w[k])
return [o / eps**3 for o in out]
def _entropy(*ps: float) -> float:
return -sum(p * math.log2(p) for p in ps if p > 0)
def easy_bound(q: int) -> float:
"""Bound on omega from the easy CW tensor with q >= 2: log_q(4 (q + 2)^3 / 27)."""
if q < 2:
raise ValueError("q must be at least 2")
return math.log(4 * (q + 2) ** 3 / 27, q)
def complete_bound(q: int, alpha: float) -> float:
"""Laser bound from the complete CW tensor and weight alpha in (0, 1/3]."""
if q < 2 or not 0 < alpha <= 1 / 3:
raise ValueError("need q >= 2 and 0 < alpha <= 1/3")
h = _entropy(2 / 3 - alpha, 2 * alpha, 1 / 3 - alpha)
return (math.log2(q + 2) - h) / (alpha * math.log2(q))
def best_complete_bound(q: int) -> tuple[float, float]:
"""Minimize complete_bound over alpha; returns (bound, alpha)."""
lo, hi = 1e-9, 1 / 3
for _ in range(200): # the function is unimodal in alpha on this interval
m1, m2 = lo + (hi - lo) / 3, hi - (hi - lo) / 3
if complete_bound(q, m1) <= complete_bound(q, m2):
hi = m2
else:
lo = m1
a = (lo + hi) / 2
return complete_bound(q, a), a
test_coppersmith_winograd_algorithm.py 176 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/coppersmith-winograd-algorithm/. The authors are listed in prov/YF5DJV.jsonl.
import random
import pytest
from coppersmith_winograd_algorithm import (
best_complete_bound,
complete_bound,
count_products,
coppersmith_winograd_algorithm as cw,
cw_approximate,
cw_exact_by_interpolation,
decomposition,
easy_bound,
)
def schoolbook(a, b, mod=None):
n, m, p = len(a), len(b), len(b[0]) if b else 0
c = [[0] * p for _ in range(n)]
for i in range(n):
for j in range(p):
s = 0
for k in range(m):
s += a[i][k] * b[k][j]
c[i][j] = s % mod if mod is not None else s
return c
def phi(x, y, q, mod=None):
"""Direct evaluation of the CW bilinear map with 3q + 3 products."""
out = [sum(x[i] * y[i] for i in range(1, q + 1)) + x[0] * y[q + 1] + x[q + 1] * y[0]]
out += [x[0] * y[i] + x[i] * y[0] for i in range(1, q + 1)]
out.append(x[0] * y[0])
return [v % mod for v in out] if mod is not None else out
def rand(rng, d, lo=-9, hi=9):
return [rng.randint(lo, hi) for _ in range(d)]
def test_example() -> None:
assert cw([1, 2, 3, 4], [5, 6, 7, 8], 2) == [61, 16, 22, 5]
assert cw_exact_by_interpolation([1, 2, 3, 4], [5, 6, 7, 8], 2) == [61, 16, 22, 5]
def test_small_q() -> None:
assert cw([3, 4], [5, 6], 0) == [3 * 6 + 4 * 5, 15]
assert cw([2, 3, 4], [5, 6, 7], 1) == [3 * 6 + 2 * 7 + 4 * 5, 2 * 6 + 3 * 5, 10]
assert count_products(0) == 2 and count_products(6) == 8
def test_zero_and_unit() -> None:
for q in range(0, 6):
z = [0] * (q + 2)
assert cw(z, z, q) == [0] * (q + 2)
e0 = [1] + [0] * (q + 1)
assert cw(e0, e0, q) == [0] * (q + 1) + [1]
def test_random_over_z() -> None:
rng = random.Random(20260)
for _ in range(600):
q = rng.randint(0, 9)
x, y = rand(rng, q + 2), rand(rng, q + 2)
assert cw(x, y, q) == phi(x, y, q)
@pytest.mark.parametrize("mod", [2, 3, 5, 7])
def test_random_mod(mod: int) -> None:
rng = random.Random(mod)
for _ in range(200):
q = rng.randint(0, 8)
x, y = rand(rng, q + 2, 0, mod - 1), rand(rng, q + 2, 0, mod - 1)
assert cw(x, y, q, mod) == phi(x, y, q, mod)
def test_interpolation_exact() -> None:
rng = random.Random(7)
for _ in range(150):
q = rng.randint(0, 6)
x, y = rand(rng, q + 2), rand(rng, q + 2)
assert cw_exact_by_interpolation(x, y, q) == phi(x, y, q)
for mod in (11, 13, 101):
for _ in range(60):
q = rng.randint(0, 6)
x, y = rand(rng, q + 2, 0, mod - 1), rand(rng, q + 2, 0, mod - 1)
assert cw_exact_by_interpolation(x, y, q, mod) == phi(x, y, q, mod)
for bad in (7, 9, 2):
with pytest.raises(ValueError):
cw_exact_by_interpolation([1, 1], [1, 1], 0, bad)
def test_decomposition_is_eps3_T() -> None:
"""Sum of the q + 2 rank-one terms has eps^0..eps^2 parts zero and eps^3 part T_q."""
for q in range(0, 7):
d = q + 2
terms = decomposition(q)
assert len(terms) == q + 2
coef = {}
for u, v, w in terms:
for a in range(d):
for b in range(d):
for c in range(d):
for i, ua in enumerate(u[a]):
for j, vb in enumerate(v[b]):
for k, wc in enumerate(w[c]):
if ua * vb * wc and i + j + k <= 3:
key = (i + j + k, a, b, c)
coef[key] = coef.get(key, 0) + ua * vb * wc
target = {}
for i in range(1, q + 1):
for t in ((0, i, i), (i, 0, i), (i, i, 0)):
target[t] = 1
for t in ((0, 0, q + 1), (0, q + 1, 0), (q + 1, 0, 0)):
target[t] = 1
for (deg, a, b, c), val in coef.items():
if deg < 3:
assert val == 0
else:
assert val == target.get((a, b, c), 0)
assert sum(1 for k, v in coef.items() if k[0] == 3 and v) == len(target) == 3 * q + 3
def test_contains_matrix_products() -> None:
"""Pieces of T_q are matrix products; check them against the schoolbook loop."""
rng = random.Random(3)
for _ in range(100):
q = rng.randint(1, 8)
u, v = rand(rng, q), rand(rng, q)
# <1, q, 1>: x0 = y0 = 0 and x_{q+1} = y_{q+1} = 0 leaves the inner product in phi_0.
out = cw([0] + u + [0], [0] + v + [0], q)
assert out[0] == schoolbook([u], [[t] for t in v])[0][0]
# <1, 1, q>: only x0 = a nonzero on the x side gives a times y in phi_1..phi_q.
a = rng.randint(-9, 9)
out = cw([a] + [0] * (q + 1), [0] + v + [0], q)
assert out[1 : q + 1] == schoolbook([[a]], [v])[0]
def test_float_error_is_order_eps() -> None:
x, y, q = [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], 2
exact = phi(x, y, q)
errs = []
for eps in (1e-1, 1e-2, 1e-3):
approx = cw_approximate(x, y, q, eps)
errs.append(max(abs(a - b) for a, b in zip(approx, exact)))
assert errs[0] > errs[1] > errs[2]
assert errs[1] < 1.5 and errs[2] < 0.2
assert abs(errs[1] / errs[2] - 10) < 1.5
def test_bounds() -> None:
assert abs(easy_bound(8) - 2.40363) < 1e-4
assert easy_bound(4) == pytest.approx(2.5)
assert min(range(2, 30), key=easy_bound) == 8
b, a = best_complete_bound(6)
assert abs(b - 2.38719) < 1e-4 and abs(a - 0.31733) < 1e-3
assert b < 2.388
assert min(range(2, 30), key=lambda q: best_complete_bound(q)[0]) == 6
assert abs(best_complete_bound(2)[0] - 2.69856) < 1e-4
for q in range(2, 20):
assert 2 < best_complete_bound(q)[0] <= complete_bound(q, 1 / 3) + 1e-12
assert best_complete_bound(q)[0] <= easy_bound(q) + 1e-12
def test_bad_input() -> None:
with pytest.raises(ValueError):
cw([1, 2], [1, 2, 3], 1)
with pytest.raises(ValueError):
cw([], [], -2)
with pytest.raises(ValueError):
easy_bound(1)
with pytest.raises(ValueError):
complete_bound(6, 0.5)
What the next level needs
- the tests did not run in this build