Algorithm Search⌘K

Dadda multiplier in Python

Hardware multiplier that reduces partial products with a minimal number of adders.

File
dadda_multiplier.py, 141 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
dadda_multiplier.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dadda-multiplier/. The authors are listed in prov/Q7R1ED.jsonl.

"""A gate-level model of the Dadda multiplier.

Each wire is a pair (bit, arrival time). An AND gate, a half adder and a full
adder each have delay 1 on all outputs. The partial product bits are kept in
columns by weight; Dadda's rule reduces the columns in stages to the heights
of the sequence 2, 3, 4, 6, 9, 13, ... and a ripple-carry adder adds the last
two rows.
"""

from dataclasses import dataclass

Wire = tuple[int, int]


@dataclass
class Counts:
    """The number of cells of each kind that the circuit uses."""

    and_gates: int = 0
    full_adders: int = 0
    half_adders: int = 0
    final_adder_width: int = 0
    stages: int = 0
    reduction_depth: int = 0


def dadda_heights(limit: int) -> list[int]:
    """Return the Dadda sequence 2, 3, 4, 6, 9, ... of the values below limit."""
    seq = [2]
    while seq[-1] * 3 // 2 < limit:
        seq.append(seq[-1] * 3 // 2)
    return [d for d in seq if d < limit]


def _full(x: Wire, y: Wire, z: Wire) -> tuple[Wire, Wire]:
    t = max(x[1], y[1], z[1]) + 1
    total = x[0] + y[0] + z[0]
    return (total & 1, t), (total >> 1, t)


def _half(x: Wire, y: Wire) -> tuple[Wire, Wire]:
    t = max(x[1], y[1]) + 1
    total = x[0] + y[0]
    return (total & 1, t), (total >> 1, t)


def dadda_multiply(a: list[int], b: list[int]) -> tuple[list[int], Counts, int]:
    """Multiply two n-bit unsigned numbers given as bit lists, low bit first.

    Precondition: len(a) == len(b) == n >= 1 and every entry is 0 or 1.
    Returns the 2n product bits (low first), the cell counts and the depth
    (the arrival time of the last product bit). The final adder is a ripple
    carry adder, so the depth is that of the ripple; counts.reduction_depth
    is the arrival time of the last bit of the two rows before it.
    """
    n = len(a)
    if n < 1 or len(b) != n:
        raise ValueError("operands must have the same length n >= 1")
    counts = Counts()
    width = 2 * n
    cols: list[list[Wire]] = [[] for _ in range(width + 1)]
    for i in range(n):  # @line L1
        for j in range(n):
            cols[i + j].append((a[i] & b[j], 1))
            counts.and_gates += 1
    targets = dadda_heights(n)  # @line L2
    for d in reversed(targets):  # @line L3
        if max(len(c) for c in cols) <= d:
            continue
        counts.stages += 1
        new_cols: list[list[Wire]] = [[] for _ in range(width + 1)]
        for k in range(width):  # @line L4
            bits = cols[k]
            excess = len(bits) + len(new_cols[k]) - d
            nf, nh = (excess // 2, excess % 2) if excess > 0 else (0, 0)
            if 3 * nf + 2 * nh > len(bits):
                raise AssertionError("Dadda rule ran out of bits")
            pos = 0
            for _ in range(nf):  # @line L5
                s, c = _full(bits[pos], bits[pos + 1], bits[pos + 2])
                pos += 3
                new_cols[k].append(s)
                new_cols[k + 1].append(c)
                counts.full_adders += 1
            for _ in range(nh):  # @line L6
                s, c = _half(bits[pos], bits[pos + 1])
                pos += 2
                new_cols[k].append(s)
                new_cols[k + 1].append(c)
                counts.half_adders += 1
            new_cols[k].extend(bits[pos:])  # @line L7
        if new_cols[width]:
            raise AssertionError("a carry left the product width")
        cols = new_cols
    counts.reduction_depth = max((w[1] for c in cols for w in c), default=0)
    out: list[int] = []  # @line L8
    depth = 0
    carry: Wire | None = None
    first = None
    for k in range(width):
        live = list(cols[k]) + ([carry] if carry is not None else [])
        if len(live) >= 2 and first is None:
            first = k
        if len(live) == 0:
            out.append(0)
            carry = None
            continue
        if len(live) == 1:
            out.append(live[0][0])
            depth = max(depth, live[0][1])
            carry = None
            continue
        if len(live) == 2:
            s, carry = _half(live[0], live[1])
        else:
            s, carry = _full(live[0], live[1], live[2])
        out.append(s[0])
        depth = max(depth, s[1])
    if carry is not None and carry[0]:
        raise AssertionError("product overflowed 2n bits")
    last = max((k for k in range(width) if len(cols[k]) == 2), default=-1)
    counts.final_adder_width = 0 if first is None else last - first + 1
    return out, counts, depth  # @line L9


def to_bits(x: int, n: int) -> list[int]:
    """The n low bits of x, least significant first."""
    return [(x >> i) & 1 for i in range(n)]


def from_bits(bits: list[int]) -> int:
    """The value of a bit list, least significant first."""
    return sum(bit << i for i, bit in enumerate(bits))


def stage_count(n: int) -> int:
    """The number of reduction stages: the Dadda heights below n."""
    return len(dadda_heights(n)) if n > 2 else 0
test_dadda_multiplier.py 71 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dadda-multiplier/. The authors are listed in prov/Q7R1ED.jsonl.

import random

import pytest

from dadda_multiplier import dadda_heights, dadda_multiply, from_bits, stage_count, to_bits


def mul(x: int, y: int, n: int):
    bits, counts, depth = dadda_multiply(to_bits(x, n), to_bits(y, n))
    assert len(bits) == 2 * n
    return from_bits(bits), counts, depth


def test_example_4_bits():
    p, c, depth = mul(13, 11, 4)
    assert p == 143
    assert (c.full_adders, c.half_adders, c.stages) == (3, 3, 2)
    assert c.final_adder_width == 6
    assert c.reduction_depth == 3
    assert depth == 7


def test_heights_sequence():
    assert dadda_heights(64) == [2, 3, 4, 6, 9, 13, 19, 28, 42, 63]
    assert dadda_heights(2) == []
    assert dadda_heights(3) == [2]


def test_exhaustive_small():
    for n in range(1, 6):
        for x in range(1 << n):
            for y in range(1 << n):
                assert mul(x, y, n)[0] == x * y


def test_random():
    rng = random.Random(20250605)
    for _ in range(600):
        n = rng.randint(1, 24)
        x, y = rng.getrandbits(n), rng.getrandbits(n)
        assert mul(x, y, n)[0] == x * y


def test_counts_and_depth():
    # Proved: stages = number of Dadda heights below n, reduction depth = stages + 1.
    # FA = n^2 - 4n + 3 follows from the final shape; that shape and HA = n - 1 are observed.
    for n in list(range(3, 41)) + [48, 64]:
        top = (1 << n) - 1
        p, c, depth = mul(top, top, n)
        assert p == top * top
        assert c.and_gates == n * n
        assert c.full_adders == n * n - 4 * n + 3
        assert c.half_adders == n - 1
        assert c.stages == stage_count(n)
        assert c.reduction_depth == c.stages + 1
        assert c.final_adder_width == 2 * n - 2
        assert depth == 2 * n - 1  # ripple-carry final adder


def test_edges():
    for n in (1, 2, 7, 16, 33):
        top = (1 << n) - 1
        for x, y in ((0, 0), (0, top), (top, 1), (top, top), (1 << (n - 1), 1 << (n - 1))):
            assert mul(x, y, n)[0] == x * y
    with pytest.raises(ValueError):
        dadda_multiply([], [])
    with pytest.raises(ValueError):
        dadda_multiply([1, 0], [1])

What the next level needs

  • the tests did not run in this build

Read the full page about Dadda multiplier