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
# 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