Algorithm Search⌘K

Carry-lookahead adder in Python

Computes carries from generate and propagate signals in logarithmic depth.

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

"""A gate-level model of the hierarchical carry-lookahead adder.

A wire is a pair (bit, arrival time). Every AND, OR and XOR gate has delay
1, whatever its fan-in. The lookahead units have r children each, so the
largest fan-in is r + 1. The function returns the sum bits, the carry out,
the gate counts and the depth of the circuit, so that tests can check both
the values and the cost claims of the page.
"""

from dataclasses import dataclass, field

Wire = tuple[int, int]


@dataclass
class Counts:
    """Gates used by the circuit, and the largest fan-in seen."""

    and_gates: int = 0
    or_gates: int = 0
    xor_gates: int = 0
    max_fan_in: int = 0
    levels: int = 0


@dataclass
class Node:
    """A node of the lookahead tree: its (G, P) pair and its children."""

    g: Wire
    p: Wire | None
    children: list["Node"] = field(default_factory=list)
    leaf: int = -1
    carry_in: Wire | None = None


def _and(ws: list[Wire], k: Counts) -> Wire:
    if len(ws) == 1:
        return ws[0]
    k.and_gates += 1
    k.max_fan_in = max(k.max_fan_in, len(ws))
    return (int(all(w[0] for w in ws)), max(w[1] for w in ws) + 1)


def _or(ws: list[Wire], k: Counts) -> Wire:
    if len(ws) == 1:
        return ws[0]
    k.or_gates += 1
    k.max_fan_in = max(k.max_fan_in, len(ws))
    return (int(any(w[0] for w in ws)), max(w[1] for w in ws) + 1)


def _xor(x: Wire, y: Wire, k: Counts) -> Wire:
    k.xor_gates += 1
    return (x[0] ^ y[0], max(x[1], y[1]) + 1)


def _lookahead(ch: list[Node], c: Wire | None, k: Counts) -> Wire:
    """g[m-1] or p[m-1] g[m-2] or ... or p[m-1] ... p[0] c, for m = len(ch)."""
    m = len(ch)
    terms = [ch[m - 1].g]
    for j in range(m - 2, -1, -1):
        terms.append(_and([ch[t].p for t in range(m - 1, j, -1)] + [ch[j].g], k))
    if c is not None:
        terms.append(_and([ch[t].p for t in range(m - 1, -1, -1)] + [c], k))
    return _or(terms, k)


def cla_add(a: list[int], b: list[int], c0: int = 0, r: int = 4):
    """Add two n-bit numbers given low bit first.

    Returns (sum bits, carry out, counts, depth).
    """
    n = len(a)
    assert n == len(b) and n >= 1 and r >= 2
    k = Counts()
    ins_a = [(x, 0) for x in a]
    ins_b = [(y, 0) for y in b]
    cin: Wire = (c0, 0)
    level = []
    for i in range(n):
        g = _and([ins_a[i], ins_b[i]], k)  # @line L1
        p = _xor(ins_a[i], ins_b[i], k)  # @line L1
        level.append(Node(g, p, leaf=i))  # @line L2
    h = 0  # @line L2
    while True:  # @line L3
        h += 1  # @line L4
        groups = [level[j:j + r] for j in range(0, len(level), r)]  # @line L4
        root = len(groups) == 1
        nxt = []
        for ch in groups:  # @line L5
            if root:
                nxt.append(Node((0, 0), None, ch))
            else:
                big_g = _lookahead(ch, None, k)  # @line L5
                big_p = _and([w.p for w in ch], k)  # @line L5
                nxt.append(Node(big_g, big_p, ch))
        level = nxt
        if root:  # @line L6
            break
    k.levels = h
    top = level[0]
    top.carry_in = cin  # @line L7
    stack = [top]
    carries: list[Wire] = [cin] * n
    while stack:  # @line L8
        node = stack.pop()
        if node.leaf >= 0:
            carries[node.leaf] = node.carry_in
            continue
        ch = node.children
        ch[0].carry_in = node.carry_in  # @line L9
        for t in range(1, len(ch)):
            ch[t].carry_in = _lookahead(ch[:t], node.carry_in, k)  # @line L9
        stack.extend(ch)
    cout = _lookahead(top.children, cin, k)  # @line L10
    leaves = _leaves(top)
    s = [_xor(leaves[i].p, carries[i], k) for i in range(n)]  # @line L11
    depth = max([w[1] for w in s] + [cout[1]])
    return [w[0] for w in s], cout[0], k, depth  # @line L12


def _leaves(top: Node) -> list[Node]:
    out, stack = [], [top]
    while stack:
        node = stack.pop()
        if node.leaf >= 0:
            out.append(node)
        else:
            stack.extend(node.children)
    out.sort(key=lambda x: x.leaf)
    return out


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, c0: int = 0, r: int = 4) -> int:
    """x + y + c0 for 0 <= x, y < 2^n, through the circuit."""
    s, cout, _, _ = cla_add(to_bits(x, n), to_bits(y, n), c0, r)
    return from_bits(s) + (cout << n)
test_carry_lookahead_adder.py 85 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-lookahead-adder/. The authors are listed in prov/ZGRFB8.jsonl.

import random

from carry_lookahead_adder import add, cla_add, from_bits, to_bits


def _levels(n: int, r: int) -> int:
    h, size = 0, n
    while True:
        h += 1
        size = -(-size // r)
        if size == 1:
            return h


def test_example() -> None:
    s, cout, _, depth = cla_add(to_bits(0xA7, 8), to_bits(0x59, 8))
    assert s == [0] * 8 and cout == 1
    assert from_bits(s) + (cout << 8) == 256
    assert depth == 7


def test_exhaustive_small() -> None:
    for r in (2, 3, 4):
        for n in range(1, 6):
            for x in range(1 << n):
                for y in range(1 << n):
                    for c0 in (0, 1):
                        assert add(x, y, n, c0, r) == x + y + c0


def test_edges() -> None:
    for r in (2, 4, 5):
        for n in (1, 2, 4, 15, 16, 17, 64, 100):
            top = (1 << n) - 1
            assert add(0, 0, n, 0, r) == 0
            assert add(top, 0, n, 1, r) == 1 << n
            assert add(top, top, n, 1, r) == 2 * top + 1
            assert add(top, 1, n, 0, r) == 1 << n
            assert add(0x5555555555555555555555555 & top, 0xAAAAAAAAAAAAAAAAAAAAAAAAA & top, n, 1, r) == top + 1


def test_random() -> None:
    rng = random.Random(20261107)
    for _ in range(600):
        n = rng.randint(1, 96)
        r = rng.choice((2, 3, 4, 8))
        x, y = rng.getrandbits(n), rng.getrandbits(n)
        c0 = rng.randint(0, 1)
        assert add(x, y, n, c0, r) == x + y + c0


def test_depth_and_fan_in() -> None:
    for r in (2, 3, 4, 8):
        for n in list(range(1, 40)) + [63, 64, 65, 127, 128, 129, 256, 257]:
            x = (1 << n) - 1
            _, _, k, depth = cla_add(to_bits(x, n), to_bits(0, n), 1, r)
            h = _levels(n, r)
            assert k.levels == h
            assert depth <= 4 * h
            assert k.max_fan_in <= r + 1


def test_exact_depth_at_powers() -> None:
    for r in (2, 4):
        n = r
        while n <= 1024:
            _, _, k, depth = cla_add([1] * n, [0] * n, 1, r)
            expect = 4 * k.levels - (1 if r == 2 and k.levels >= 2 else 0)
            assert depth == expect
            n *= r


def test_linear_size() -> None:
    for n in (2, 4, 8, 16, 64, 256, 1024):
        _, _, k, _ = cla_add([0] * n, [0] * n, 0, 2)
        assert k.and_gates + k.or_gates + k.xor_gates == 8 * n - 5
    for n in (4, 16, 64, 256, 1024):
        _, _, k, _ = cla_add([0] * n, [0] * n, 0, 4)
        assert 3 * (k.and_gates + k.or_gates + k.xor_gates) == 23 * n - 14
    for n in range(1, 130):
        _, _, k, _ = cla_add([0] * n, [0] * n, 0, 4)
        assert k.and_gates + k.or_gates + k.xor_gates <= 8 * n

What the next level needs

  • the tests did not run in this build

Read the full page about Carry-lookahead adder