Carry-select adder in Python
Computes blocks of sums for both carry-in values and selects the correct one.
- File
carry_select_adder.py, 122 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/carry-select-adder/. The authors are listed in prov/0D2Z2R.jsonl.
"""The carry-select adder, simulated gate by gate.
Bit lists are least significant bit first. Every signal is a pair (value,
arrival time); AND, OR and XOR gates have two inputs and unit delay. The
constant carries 0 and 1 of the two copies of each block are folded in, so
the gate count is that of a real circuit. The block sizes are a parameter.
"""
from dataclasses import dataclass
Wire = tuple[int, int]
ZERO: Wire = (0, 0)
ONE: Wire = (1, 0)
@dataclass
class Stats:
gates: int = 0
def _and(x: Wire, y: Wire, st: Stats) -> Wire:
st.gates += 1
return x[0] & y[0], max(x[1], y[1]) + 1
def _or(x: Wire, y: Wire, st: Stats) -> Wire:
st.gates += 1
return x[0] | y[0], max(x[1], y[1]) + 1
def _xor(x: Wire, y: Wire, st: Stats) -> Wire:
st.gates += 1
return x[0] ^ y[0], max(x[1], y[1]) + 1
def _ripple(g: list[Wire], p: list[Wire], c: Wire | int, st: Stats) -> tuple[list[Wire], Wire]:
"""Ripple through one block; c is a wire, or the constant 0 or 1."""
sums = []
for gi, pi in zip(g, p):
if c == 0:
sums.append(pi)
c = gi
elif c == 1:
sums.append(_xor(pi, ONE, st))
c = _or(gi, pi, st)
else:
sums.append(_xor(pi, c, st))
c = _or(gi, _and(pi, c, st), st)
return sums, c
def carry_select_add(a: list[int], b: list[int], c_in: int, sizes: list[int], st: Stats | None = None):
"""Add two n-bit lists with blocks of the given sizes, low block first.
Returns (n + 1 result bits with the carry out last, depth).
"""
if st is None:
st = Stats()
n = len(a)
if len(b) != n or n == 0 or sum(sizes) != n or min(sizes) < 1:
raise ValueError("bad widths or block sizes")
A = [(x & 1, 0) for x in a]
B = [(x & 1, 0) for x in b]
g = [_and(A[i], B[i], st) for i in range(n)] # @line L1
p = [_xor(A[i], B[i], st) for i in range(n)] # @line L1
k0 = sizes[0]
s, carry = _ripple(g[:k0], p[:k0], (c_in & 1, 0), st) # @line L2
starts = [sum(sizes[:j]) for j in range(len(sizes))]
pre = []
for j in range(1, len(sizes)): # @line L3
lo, hi = starts[j], starts[j] + sizes[j]
s0, c0 = _ripple(g[lo:hi], p[lo:hi], 0, st) # @line L4
s1, c1 = _ripple(g[lo:hi], p[lo:hi], 1, st) # @line L4
diff = [_xor(x, y, st) for x, y in zip(s0, s1)]
pre.append((s0, diff, c0, c1))
for s0, diff, c0, c1 in pre: # @line L5
cb = carry # @line L6
s += [_xor(x, _and(cb, d, st), st) for x, d in zip(s0, diff)] # @line L7
carry = _or(c0, _and(c1, cb, st), st) # @line L8
out = s + [carry]
return [w[0] for w in out], max(w[1] for w in out) # @line L9
def uniform_sizes(n: int, k: int) -> list[int]:
"""Blocks of size k, with a shorter last block if k does not divide n."""
return [k] * (n // k) + ([n % k] if n % k else [])
def growing_sizes(n: int, k0: int = 1) -> list[int]:
"""Block sizes k0, k0, k0 + 1, k0 + 2, ..., the last block cut to fit.
With these sizes the two results of each block are ready by the time its
carry-in arrives, so each block adds only the two gates of its carry
selection to the critical path.
"""
sizes, total, k = [], 0, k0
while total < n:
take = min(k, n - total)
sizes.append(take)
total += take
if len(sizes) >= 2:
k += 1
return sizes
def to_bits(x: int, n: int) -> list[int]:
return [(x >> i) & 1 for i in range(n)]
def from_bits(bits: list[int]) -> int:
return sum(bit << i for i, bit in enumerate(bits))
def add(x: int, y: int, n: int, c_in: int = 0, sizes: list[int] | None = None) -> int:
"""x + y + c_in for 0 <= x, y < 2^n, through the circuit."""
if sizes is None:
sizes = uniform_sizes(n, max(1, round(n ** 0.5)))
bits, _ = carry_select_add(to_bits(x, n), to_bits(y, n), c_in, sizes)
return from_bits(bits)
test_carry_select_adder.py 84 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-select-adder/. The authors are listed in prov/0D2Z2R.jsonl.
import random
from carry_select_adder import (
Stats,
add,
carry_select_add,
from_bits,
growing_sizes,
to_bits,
uniform_sizes,
)
def _depth(n: int, sizes: list[int]) -> int:
_, d = carry_select_add([1] * n, [0] * n, 1, sizes)
return d
def test_example() -> None:
bits, depth = carry_select_add(to_bits(0xB6D3, 16), to_bits(0x69E5, 16), 0, [4, 4, 4, 4])
assert from_bits(bits) == 0x120B8
assert depth == 15
def test_exhaustive_small() -> None:
for n in range(1, 6):
schedules = [[n], uniform_sizes(n, 1), uniform_sizes(n, 2), growing_sizes(n)]
for sizes in schedules:
for x in range(1 << n):
for y in range(1 << n):
for c in (0, 1):
assert add(x, y, n, c, sizes) == x + y + c
def test_edges() -> None:
for n in (1, 2, 5, 16, 17, 64, 100):
top = (1 << n) - 1
for sizes in (uniform_sizes(n, 3), growing_sizes(n), [n]):
assert add(0, 0, n, 0, sizes) == 0
assert add(top, 0, n, 1, sizes) == 1 << n
assert add(top, 1, n, 0, sizes) == 1 << n
assert add(top, top, n, 1, sizes) == 2 * top + 1
def test_random() -> None:
rng = random.Random(1962)
for _ in range(800):
n = rng.randint(1, 120)
sizes, left = [], n
while left:
k = rng.randint(1, left)
sizes.append(k)
left -= k
x, y, c = rng.getrandbits(n), rng.getrandbits(n), rng.randint(0, 1)
assert add(x, y, n, c, sizes) == x + y + c
def test_depth_formulas() -> None:
for n in range(2, 70):
assert _depth(n, [n]) == 2 * n + 1
for k in range(1, n):
sizes = uniform_sizes(n, k)
assert _depth(n, sizes) == 2 * k + 2 * len(sizes) - 1
for k0 in (1, 2, 3):
sizes = growing_sizes(n, k0)
if len(sizes) >= 2:
assert _depth(n, sizes) == 2 * k0 + 2 * (len(sizes) - 1) + 1
def test_square_root_depth() -> None:
for m in range(2, 24):
n = m * m
assert _depth(n, uniform_sizes(n, m)) == 4 * m - 1
assert _depth(n, growing_sizes(n)) < 2 * (2 * n) ** 0.5 + 5
def test_linear_size() -> None:
for n in (16, 64, 256, 1024):
st = Stats()
carry_select_add([0] * n, [0] * n, 0, uniform_sizes(n, 4), st)
assert st.gates <= 11 * n
What the next level needs
- the tests did not run in this build