Array multiplier in Python
Hardware multiplier arranged as a grid of adders, the circuit form of schoolbook multiplication.
- File
array_multiplier.py, 94 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/array-multiplier/. The authors are listed in prov/4XX4WG.jsonl.
"""A gate-level model of the carry-save array multiplier.
Each wire is a pair (bit, arrival time), or None for a wire that is the
constant 0 by construction. An AND gate has delay 1, and a full adder or a
half adder has delay 1 on both outputs. A cell with three live inputs is a
full adder, with two it is a half adder, and with one it is a plain wire.
"""
from dataclasses import dataclass
Wire = tuple[int, int] | None
@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
def _cell(x: Wire, y: Wire, z: Wire, counts: Counts) -> tuple[Wire, Wire]:
"""Add up to three bits of one weight; return (sum, carry)."""
live = [w for w in (x, y, z) if w is not None]
if not live:
return None, None
if len(live) == 1:
return live[0], None
if len(live) == 2:
counts.half_adders += 1
else:
counts.full_adders += 1
total = sum(w[0] for w in live)
t = max(w[1] for w in live) + 1
return (total & 1, t), (total >> 1, t)
def array_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). Cost: n^2 AND gates and
n(n - 1) adder cells for n >= 2, simulated in Theta(n^2) steps.
"""
n = len(a)
if n < 1 or len(b) != n:
raise ValueError("operands must have the same length n >= 1")
counts = Counts()
pp: list[list[Wire]] = []
for j in range(n): # @line L1
pp.append([(a[i] & b[j], 1) for i in range(n)])
counts.and_gates += n
s: list[Wire] = list(pp[0]) # @line L2
c: list[Wire] = [None] * n
p: list[Wire] = [None] * (2 * n)
for j in range(1, n): # @line L3
p[j - 1] = s[0] # @line L4
new_s: list[Wire] = []
new_c: list[Wire] = []
for i in range(n): # @line L5
upper = s[i + 1] if i + 1 < n else None
si, ci = _cell(pp[j][i], upper, c[i], counts)
new_s.append(si)
new_c.append(ci)
s, c = new_s, new_c # @line L6
p[n - 1] = s[0] # @line L7
carry: Wire = None # @line L8
for i in range(1, n): # @line L9
p[n - 1 + i], carry = _cell(s[i], c[i - 1], carry, counts) # @line L10
p[2 * n - 1] = carry # @line L11
bits = [w[0] if w is not None else 0 for w in p]
depth = max((w[1] for w in p if w is not None), default=0)
return bits, counts, depth
def to_bits(x: int, n: int) -> list[int]:
"""The n low bits of x >= 0, low bit first."""
return [(x >> i) & 1 for i in range(n)]
def from_bits(bits: list[int]) -> int:
"""The value of a bit list, low bit first."""
return sum(bit << i for i, bit in enumerate(bits))
def multiply(x: int, y: int, n: int) -> int:
"""x * y for 0 <= x, y < 2^n, computed by the array multiplier."""
bits, _, _ = array_multiply(to_bits(x, n), to_bits(y, n))
return from_bits(bits)
test_array_multiplier.py 67 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/array-multiplier/. The authors are listed in prov/4XX4WG.jsonl.
import random
from array_multiplier import array_multiply, from_bits, multiply, to_bits
def test_example() -> None:
bits, _, _ = array_multiply([1, 0, 1, 1], [1, 1, 0, 1])
assert bits == [1, 1, 1, 1, 0, 0, 0, 1]
assert from_bits(bits) == 143
def test_exhaustive_small() -> None:
for n in range(1, 6):
for x in range(1 << n):
for y in range(1 << n):
assert multiply(x, y, n) == x * y
def test_edges() -> None:
for n in (1, 2, 3, 8, 16, 33):
top = (1 << n) - 1
assert multiply(0, 0, n) == 0
assert multiply(top, 0, n) == 0
assert multiply(0, top, n) == 0
assert multiply(1, top, n) == top
assert multiply(top, top, n) == top * top
assert multiply(1 << (n - 1), 1 << (n - 1), n) == 1 << (2 * n - 2)
def test_cell_counts_and_depth() -> None:
for n in range(1, 13):
_, counts, depth = array_multiply([1] * n, [1] * n)
assert counts.and_gates == n * n
if n >= 2:
assert counts.full_adders == n * n - 2 * n
assert counts.half_adders == n
assert depth == 2 * n - 1
def test_depth_does_not_depend_on_values() -> None:
rng = random.Random(7)
for n in (4, 9):
for _ in range(20):
x, y = rng.randrange(1 << n), rng.randrange(1 << n)
_, _, depth = array_multiply(to_bits(x, n), to_bits(y, n))
assert depth == 2 * n - 1
def test_random_against_python() -> None:
rng = random.Random(4004)
for _ in range(600):
n = rng.randint(1, 24)
x, y = rng.randrange(1 << n), rng.randrange(1 << n)
assert multiply(x, y, n) == x * y
def test_bad_lengths() -> None:
for a, b in (([], []), ([1], [1, 0])):
try:
array_multiply(a, b)
except ValueError:
pass
else:
raise AssertionError("expected ValueError")
What the next level needs
- the tests did not run in this build