Algorithm Search⌘K

Carry-skip adder in Python

Skips propagation across a block when all its bits propagate the incoming carry.

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

"""The carry-skip (carry-bypass) adder, simulated gate by gate.

Bit lists are least significant bit first. Every signal is a triple
(value, settle time, depth). The depth is the topological one: one more than
the deepest input. The settle time follows controlling values: an AND gate
whose output is 0 settles one unit after its earliest 0 input, an OR gate
whose output is 1 one unit after its earliest 1 input, and otherwise a gate
settles one unit after its latest input. The settle time depends on the data;
its maximum over all inputs is the delay of the adder.
"""

from dataclasses import dataclass

Wire = tuple[int, int, int]


@dataclass
class Stats:
    gates: int = 0


def _and(x: Wire, y: Wire, st: Stats) -> Wire:
    st.gates += 1
    d = max(x[2], y[2]) + 1
    if x[0] and y[0]:
        return 1, max(x[1], y[1]) + 1, d
    return 0, min(w[1] for w in (x, y) if w[0] == 0) + 1, d


def _or(x: Wire, y: Wire, st: Stats) -> Wire:
    st.gates += 1
    d = max(x[2], y[2]) + 1
    if x[0] or y[0]:
        return 1, min(w[1] for w in (x, y) if w[0] == 1) + 1, d
    return 0, max(x[1], y[1]) + 1, d


def _xor(x: Wire, y: Wire, st: Stats) -> Wire:
    st.gates += 1
    return x[0] ^ y[0], max(x[1], y[1]) + 1, max(x[2], y[2]) + 1


def _not(x: Wire, st: Stats) -> Wire:
    st.gates += 1
    return 1 - x[0], x[1] + 1, x[2] + 1


def _and_tree(ws: list[Wire], st: Stats) -> Wire:
    while len(ws) > 1:
        nxt = [_and(ws[i], ws[i + 1], st) for i in range(0, len(ws) - 1, 2)]
        if len(ws) % 2:
            nxt.append(ws[-1])
        ws = nxt
    return ws[0]


def carry_skip_add(a: list[int], b: list[int], c_in: int, sizes: list[int], st: Stats | None = None):
    """Add two n-bit lists with ripple blocks of the given sizes, low block first.

    Returns (n + 1 result bits with the carry out last, delay, depth): the
    settle time of the slowest output on this input, and the depth of the circuit.
    """
    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, 0) for x in a]
    B = [(x & 1, 0, 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
    starts = [sum(sizes[:j]) for j in range(len(sizes))]
    big_p = [None] + [_and_tree(p[lo:lo + k], st) for lo, k in zip(starts[1:], sizes[1:])]  # @line L2
    c: Wire = (c_in & 1, 0, 0)  # @line L3
    s: list[Wire] = []
    for j, (lo, k) in enumerate(zip(starts, sizes)):  # @line L4
        x = c  # @line L5
        for i in range(lo, lo + k):  # @line L6
            s.append(_xor(p[i], c, st))  # @line L6
            c = _or(g[i], _and(p[i], c, st), st)  # @line L6
        if j >= 1:  # @line L7
            c = _or(_and(big_p[j], x, st), _and(_not(big_p[j], st), c, st), st)  # @line L7
    out = s + [c]
    return [w[0] for w in out], max(w[1] for w in out), max(w[2] for w in out)  # @line L8


def uniform_sizes(n: int, k: int) -> list[int]:
    return [k] * (n // k) + ([n % k] if n % k else [])


def worst_input(n: int) -> tuple[int, int, int]:
    """Operands (a, b, c_in) of the slowest case found: the carry-in 1 is
    killed at position 0, and the carry 0 that leaves position 0 must then
    pass every position, because all other positions propagate."""
    return (1 << n) - 2, 0, 1


def delay_bound(sizes: list[int]) -> int:
    """The upper bound on the delay proved on the page, for these block sizes.

    T is the time by which the carry into the next block has settled.
    """
    best = 2 * sizes[0]
    t = 2 * sizes[0] + 1
    for k in sizes[1:]:
        best = max(best, t + 2 * k - 1)
        t = max(t + 2, 2 * k + 3)
    return max(best, t)


def depth_formula(n: int, blocks: int) -> int:
    """The depth of the circuit: the ripple path through all n positions,
    plus two gates for each skip multiplexer on it."""
    return 2 * n + 2 * blocks - 1


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 / 2) ** 0.5)))
    bits, _, _ = carry_skip_add(to_bits(x, n), to_bits(y, n), c_in, sizes)
    return from_bits(bits)
test_carry_skip_adder.py 101 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-skip-adder/. The authors are listed in prov/FWJCMW.jsonl.

import itertools
import random

from carry_skip_adder import (
    Stats,
    add,
    carry_skip_add,
    delay_bound,
    depth_formula,
    from_bits,
    to_bits,
    uniform_sizes,
    worst_input,
)


def _max_delay(n: int, sizes: list[int]) -> int:
    """The largest settle time over all inputs. Only the kind of each position
    (kill, propagate, generate) and the carry-in matter for the timing."""
    worst = 0
    for kinds in itertools.product((0, 1, 2), repeat=n):
        a = [1 if t >= 1 else 0 for t in kinds]
        b = [1 if t == 2 else 0 for t in kinds]
        for c in (0, 1):
            worst = max(worst, carry_skip_add(a, b, c, sizes)[1])
    return worst


def test_example() -> None:
    bits, delay, depth = carry_skip_add(to_bits(0x9A3C, 16), to_bits(0x65C4, 16), 0, [4, 4, 4, 4])
    assert from_bits(bits) == 0x9A3C + 0x65C4
    assert delay == 15 and depth == 39


def test_exhaustive_small() -> None:
    for n in range(1, 6):
        for sizes in ([n], uniform_sizes(n, 1), uniform_sizes(n, 2), uniform_sizes(n, 3)):
            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, 7, 16, 33, 100):
        top = (1 << n) - 1
        for sizes in (uniform_sizes(n, 4), [n]):
            assert add(0, 0, n, 0, sizes) == 0
            assert add(top, 0, n, 1, sizes) == 1 << n
            assert add(top, top, n, 1, sizes) == 2 * top + 1
            assert add(top - 1, 0, n, 1, sizes) == top


def test_random() -> None:
    rng = random.Random(1961)
    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_delay_bound_is_exact_for_small_n() -> None:
    rng = random.Random(7)
    for n in range(1, 7):
        schedules = [uniform_sizes(n, k) for k in range(1, n + 1)]
        for _ in range(2):
            sizes, left = [], n
            while left:
                k = rng.randint(1, left)
                sizes.append(k)
                left -= k
            schedules.append(sizes)
        for sizes in schedules:
            assert _max_delay(n, sizes) == delay_bound(sizes)


def test_worst_input_and_depth() -> None:
    for n in (8, 16, 64, 256):
        a, b, c = worst_input(n)
        for k in range(1, 20):
            sizes = uniform_sizes(n, k)
            _, delay, depth = carry_skip_add(to_bits(a, n), to_bits(b, n), c, sizes)
            assert delay == delay_bound(sizes)
            assert depth == depth_formula(n, len(sizes))


def test_square_root_delay() -> None:
    for n in (64, 256, 1024):
        best = min(delay_bound(uniform_sizes(n, k)) for k in range(1, n + 1))
        assert best <= 4 * (2 * n) ** 0.5
        st = Stats()
        carry_skip_add([0] * n, [0] * n, 0, uniform_sizes(n, 8), st)
        assert st.gates <= 7 * n

What the next level needs

  • the tests did not run in this build

Read the full page about Carry-skip adder