Algorithm Search⌘K

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
carry_select_adder.pyRawGitHub
# 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

Read the full page about Carry-select adder