Algorithm Search⌘K

Harvey-van der Hoeven multiplication in Python

Integer multiplication in O(n log n) bit operations, matching the conjectured optimum.

File
harvey_van_der_hoeven_multiplication.py, 189 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
harvey_van_der_hoeven_multiplication.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/harvey-van-der-hoeven-multiplication/. The authors are listed in prov/H0NKE4.jsonl.

"""A toy structural model of the first stage of the Harvey-van der Hoeven algorithm.

An integer product becomes a cyclic convolution of length S = s_1 * ... * s_d
for distinct primes s_i. The Chinese remainder theorem turns it into a
d-dimensional cyclic convolution of size s_1 x ... x s_d (Agarwal and Cooley),
which is done by a d-dimensional DFT, one dimension at a time. Each DFT of
prime length s is turned into a cyclic convolution of length s - 1 by Rader's
algorithm, the step of the paper's conditional variant. The arithmetic is
exact, modulo a prime q = 1 (mod S), instead of approximate complex numbers.
Gaussian resampling, the synthetic FFTs and the recursion of the real
algorithm are not modelled: the convolutions of length s - 1 are done
directly and only counted.
"""

from dataclasses import dataclass
from math import prod


@dataclass
class Counts:
    """Operations of the toy, by kind."""

    prime_dfts: int = 0
    rader_convolutions: int = 0
    pointwise_products: int = 0


def is_prime(n: int) -> bool:
    """Deterministic Miller-Rabin, valid for n < 3.3 * 10^24."""
    if n < 2:
        return False
    small = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
    for p in small:
        if n % p == 0:
            return n == p
    d, r = n - 1, 0
    while d % 2 == 0:
        d //= 2
        r += 1
    for a in small:
        x = pow(a, 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 _prime_factors(n: int) -> list[int]:
    out, p = [], 2
    while p * p <= n:
        if n % p == 0:
            out.append(p)
            while n % p == 0:
                n //= p
        p += 1
    if n > 1:
        out.append(n)
    return out


def primitive_root(p: int) -> int:
    """The smallest generator of the multiplicative group modulo the prime p."""
    factors = _prime_factors(p - 1)
    g = 1
    while True:
        g += 1
        if all(pow(g, (p - 1) // f, p) != 1 for f in factors):
            return g


class Plan:
    """The sizes s_i, the modulus q and the roots of unity."""

    def __init__(self, sizes: tuple[int, ...], b: int):
        if len(set(sizes)) != len(sizes) or not all(is_prime(s) for s in sizes):
            raise ValueError("the sizes must be distinct primes")
        self.sizes, self.b = tuple(sizes), b
        self.S = prod(sizes)
        self.chunks = (self.S + 1) // 2  # 2 * chunks - 1 <= S: no cyclic wrap
        bound = self.chunks * ((1 << b) - 1) ** 2
        q = (bound // self.S + 1) * self.S + 1
        while not is_prime(q):
            q += self.S
        self.q = q  # a prime, q = 1 (mod S), q > every coefficient of the product
        gq = primitive_root(q)
        self.roots = [pow(gq, (q - 1) // s, q) for s in sizes]  # primitive s_i-th roots
        self.gens = [primitive_root(s) for s in sizes]
        self.strides = [prod(sizes[i + 1 :]) for i in range(len(sizes))]


def cyclic_convolution(a: list[int], c: list[int], q: int) -> list[int]:
    """Cyclic convolution of two vectors of the same length, mod q, done directly."""
    n = len(a)
    out = [0] * n
    for i, x in enumerate(a):
        if x:
            for j, y in enumerate(c):
                k = i + j - n if i + j >= n else i + j
                out[k] = (out[k] + x * y) % q
    return out


def prime_dft(x: list[int], root: int, g: int, q: int, counts: Counts) -> list[int]:
    """DFT of prime length s with the root `root`, by Rader's algorithm."""
    s = len(x)
    counts.prime_dfts += 1
    out = [0] * s
    out[0] = sum(x) % q  # @line L10
    if s == 2:
        out[1] = (x[0] - x[1]) % q
        return out
    ginv = pow(g, -1, s)
    a = [x[pow(g, k, s)] for k in range(s - 1)]  # @line L11
    c = [pow(root, pow(ginv, k, s), q) for k in range(s - 1)]
    conv = cyclic_convolution(a, c, q)
    counts.rader_convolutions += 1
    for p in range(s - 1):  # @line L12
        out[pow(ginv, p, s)] = (x[0] + conv[p]) % q
    return out


def dft_d(cells: list[int], plan: Plan, sign: int, counts: Counts) -> list[int]:
    """d-dimensional DFT of an array stored in row-major order, one dimension at a time."""
    q, S = plan.q, plan.S
    cells = list(cells)
    for i, s in enumerate(plan.sizes):  # @line L7
        stride = plan.strides[i]
        root = pow(plan.roots[i], sign, q)
        for base in range(S):  # @line L8
            if (base // stride) % s:
                continue  # base is the start of a line along dimension i
            idx = [base + stride * k for k in range(s)]
            line = prime_dft([cells[t] for t in idx], root, plan.gens[i], q, counts)
            for t, val in zip(idx, line):
                cells[t] = val
    return cells  # @line L9


def crt_cell(j: int, plan: Plan) -> int:
    """The row-major index of the cell (j mod s_1, ..., j mod s_d)."""
    return sum((j % s) * st for s, st in zip(plan.sizes, plan.strides))


def hvdh_toy_multiply(u: int, v: int, plan: Plan, counts: Counts | None = None) -> int:
    """Return u * v for 0 <= u, v < 2^(b * chunks)."""
    counts = counts if counts is not None else Counts()
    b, S, q = plan.b, plan.S, plan.q
    if u < 0 or v < 0:
        raise ValueError("operands must be nonnegative")
    if max(u.bit_length(), v.bit_length()) > b * plan.chunks:
        raise ValueError("operands too long for this plan")
    mask = (1 << b) - 1
    cell = [crt_cell(j, plan) for j in range(S)]

    def to_cells(z: int) -> list[int]:
        arr = [0] * S
        for j in range(plan.chunks):  # @line L1
            arr[cell[j]] = z & mask  # @line L2
            z >>= b
        return arr

    fu = dft_d(to_cells(u), plan, 1, counts)  # @line L3
    fv = dft_d(to_cells(v), plan, 1, counts)
    prodcells = []
    for x, y in zip(fu, fv):  # @line L4
        prodcells.append(x * y % q)
        counts.pointwise_products += 1
    back = dft_d(prodcells, plan, -1, counts)  # @line L5
    inv_s = pow(S, -1, q)
    result = 0
    for j in range(S - 1, -1, -1):  # @line L6
        result = (result << b) + back[cell[j]] * inv_s % q
    return result


def expected_counts(sizes: tuple[int, ...]) -> tuple[int, int]:
    """(prime DFTs, Rader convolutions) of one d-dimensional DFT."""
    S = prod(sizes)
    dfts = sum(S // s for s in sizes)
    convs = sum(S // s for s in sizes if s > 2)
    return dfts, convs
test_harvey_van_der_hoeven_multiplication.py 110 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/harvey-van-der-hoeven-multiplication/. The authors are listed in prov/H0NKE4.jsonl.

import random
from math import prod

import pytest

from harvey_van_der_hoeven_multiplication import (
    Counts,
    Plan,
    crt_cell,
    expected_counts,
    hvdh_toy_multiply,
    is_prime,
    prime_dft,
    primitive_root,
)

SIZES = [(2,), (3,), (5,), (2, 3), (3, 5), (2, 5, 7), (3, 5, 7), (5, 7, 11), (2, 3, 5, 7)]


def test_is_prime_small():
    sieve = [True] * 2000
    sieve[0] = sieve[1] = False
    for i in range(2, 2000):
        if sieve[i]:
            for j in range(i * i, 2000, i):
                sieve[j] = False
    assert all(is_prime(n) == sieve[n] for n in range(2000))
    assert is_prime(998244353) and not is_prime(998244353 * 7)


def test_primitive_root():
    for p in (3, 5, 7, 11, 13, 1831):
        g = primitive_root(p)
        assert len({pow(g, k, p) for k in range(p - 1)}) == p - 1


@pytest.mark.parametrize("s", [2, 3, 5, 7, 11, 13])
def test_prime_dft_matches_definition(s):
    plan = Plan((s,), 6)
    q, root, g = plan.q, plan.roots[0], plan.gens[0]
    assert pow(root, s, q) == 1 and (s == 1 or pow(root, 1, q) != 1)
    rng = random.Random(s)
    for sign in (1, -1):
        w = pow(root, sign, q)
        for _ in range(10):
            x = [rng.randrange(q) for _ in range(s)]
            want = [sum(x[n] * pow(w, n * k, q) for n in range(s)) % q for k in range(s)]
            assert prime_dft(x, w, g, q, Counts()) == want


@pytest.mark.parametrize("sizes", SIZES)
def test_crt_map_is_a_bijection(sizes):
    plan = Plan(sizes, 4)
    assert sorted(crt_cell(j, plan) for j in range(plan.S)) == list(range(plan.S))


@pytest.mark.parametrize("sizes", SIZES)
def test_counts(sizes):
    plan = Plan(sizes, 4)
    counts = Counts()
    hvdh_toy_multiply(3, 5, plan, counts)
    dfts, convs = expected_counts(sizes)
    assert (counts.prime_dfts, counts.rader_convolutions, counts.pointwise_products) == (3 * dfts, 3 * convs, prod(sizes))


def test_example_from_the_page():
    plan = Plan((3, 5), 4)
    assert plan.q == 1831 and plan.chunks == 8
    counts = Counts()
    assert hvdh_toy_multiply(0xBEEF, 0xCAFE, plan, counts) == 0x97660722
    assert (counts.prime_dfts, counts.rader_convolutions, counts.pointwise_products) == (24, 24, 15)


def test_edge_cases():
    for sizes in SIZES:
        for b in (1, 3, 8):
            plan = Plan(sizes, b)
            n = b * plan.chunks
            top = (1 << n) - 1
            for u, v in [(0, 0), (0, top), (1, 1), (1, top), (top, top), (1 << (n - 1), top)]:
                assert hvdh_toy_multiply(u, v, plan) == u * v


def test_random_against_python_ints():
    rng = random.Random(2019)
    plans = [Plan(sizes, b) for sizes in SIZES[:7] for b in (1, 5, 8)]
    checked = 0
    for plan in plans:
        n = plan.b * plan.chunks
        for _ in range(25):
            u = rng.getrandbits(rng.randrange(1, n + 1))
            v = rng.getrandbits(rng.randrange(1, n + 1))
            assert hvdh_toy_multiply(u, v, plan) == u * v
            checked += 1
    assert checked >= 500


def test_rejects_bad_input():
    with pytest.raises(ValueError):
        Plan((3, 3), 4)
    with pytest.raises(ValueError):
        Plan((4, 5), 4)
    plan = Plan((3, 5), 4)
    with pytest.raises(ValueError):
        hvdh_toy_multiply(1 << 32, 1, plan)
    with pytest.raises(ValueError):
        hvdh_toy_multiply(-1, 1, plan)

What the next level needs

  • the tests did not run in this build

Read the full page about Harvey-van der Hoeven multiplication