Carry-save addition in Python
Adds three numbers into sum and carry vectors in constant time per digit, deferring carry propagation.
- File
carry_save_addition.py, 108 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-save-addition/. The authors are listed in prov/KMMETW.jsonl.
"""Carry-save addition on digit lists in base beta, least significant digit first.
csa reduces three numbers to two with no carry propagation: each output digit
depends only on the three input digits in the same position. A sum of many
numbers is kept as a pair (S, C) and resolved by one ordinary addition at the end.
"""
def csa(x: list[int], y: list[int], z: list[int], base: int) -> tuple[list[int], list[int]]:
"""Return (s, c) with x + y + z = s + c, all of the same length as the inputs.
The inputs must have the same length and digits in [0, base); the top
digit of each must be small enough that the sum fits, which the callers
ensure by padding with zeros.
"""
n = len(x)
s = [0] * n
c = [0] * n
for i in range(n): # @line L1: every position at once, in a circuit
t = x[i] + y[i] + z[i] # @line L1
s[i] = t % base # @line L2
if i + 1 < n:
c[i + 1] = t // base # @line L2: the carry goes to the next position, not further
elif t >= base:
raise OverflowError("the sum does not fit; pad the operands")
c[0] = 0 # @line L3
return s, c # @line L4
def propagate_add(x: list[int], y: list[int], base: int) -> list[int]:
"""An ordinary carry-propagate addition, used once at the end."""
out, carry = [], 0
for u, v in zip(x, y):
t = u + v + carry
out.append(t % base)
carry = t // base
if carry:
raise OverflowError("the sum does not fit")
return out
def carry_save_sum(numbers: list[list[int]], base: int) -> tuple[list[int], int]:
"""Add m numbers of equal length with a chain of carry-save adders.
Returns the digits of the sum and the number of csa steps, m - 2 for m >= 2.
The operands must be padded so that the total fits.
"""
n = len(numbers[0])
zero = [0] * n
big_s, big_c = numbers[0], zero # @line L5
if len(numbers) == 1:
return propagate_add(big_s, big_c, base), 0
big_c = numbers[1]
steps = 0
for x in numbers[2:]: # @line L6
big_s, big_c = csa(big_s, big_c, x, base) # @line L6
steps += 1
return propagate_add(big_s, big_c, base), steps # @line L7
def carry_save_tree(numbers: list[list[int]], base: int) -> tuple[list[int], int]:
"""Add m numbers with levels of parallel carry-save adders (a Wallace tree).
Each level groups the numbers in threes and replaces every group by two.
Returns the digits of the sum and the number of levels.
"""
level = list(numbers)
levels = 0
while len(level) > 2:
nxt = []
full = len(level) - len(level) % 3
for j in range(0, full, 3):
s, c = csa(level[j], level[j + 1], level[j + 2], base)
nxt += [s, c]
nxt += level[full:]
level = nxt
levels += 1
if len(level) == 1:
level.append([0] * len(level[0]))
return propagate_add(level[0], level[1], base), levels
def to_digits(x: int, n: int, base: int) -> list[int]:
out = []
for _ in range(n):
x, r = divmod(x, base)
out.append(r)
if x:
raise OverflowError("the number does not fit in n digits")
return out
def from_digits(d: list[int], base: int) -> int:
v = 0
for digit in reversed(d):
v = v * base + digit
return v
def width_for(m: int, n: int, base: int) -> int:
"""Digits enough for the sum of m numbers of n digits."""
w, cap = n, base ** n
while (cap - 1) * m >= base ** w:
w += 1
return w
test_carry_save_addition.py 107 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/carry-save-addition/. The authors are listed in prov/KMMETW.jsonl.
import random
import pytest
from carry_save_addition import (
carry_save_sum,
carry_save_tree,
csa,
from_digits,
to_digits,
width_for,
)
def _levels(m: int) -> int:
"""The least L with d_L >= m, where d_0 = 2 and d_{j+1} = floor(3 d_j / 2)."""
d, level = 2, 0
while d < m:
d, level = 3 * d // 2, level + 1
return level
def test_example_decimal() -> None:
x, y, z = (to_digits(v, 4, 10) for v in (487, 659, 328))
s, c = csa(x, y, z, 10)
assert s == [4, 5, 3, 0] and c == [0, 2, 1, 1]
assert from_digits(s, 10) + from_digits(c, 10) == 1474
def test_example_binary() -> None:
x, y, z = (to_digits(v, 6, 2) for v in (13, 11, 7))
s, c = csa(x, y, z, 2)
assert from_digits(s, 2) == 1 and from_digits(c, 2) == 30
def test_exhaustive_small() -> None:
for base in (2, 3, 4):
n = 3
top = base ** (n - 1)
for x in range(top):
for y in range(top):
for z in range(top):
xs, ys, zs = (to_digits(v, n, base) for v in (x, y, z))
s, c = csa(xs, ys, zs, base)
assert from_digits(s, base) + from_digits(c, base) == x + y + z
assert all(0 <= d < base for d in s)
assert all(0 <= d <= 2 for d in c) and c[0] == 0
def test_random_csa_invariant() -> None:
rng = random.Random(1964)
for _ in range(1500):
base = rng.choice((2, 3, 7, 10, 16, 256, 2**16))
n = rng.randint(1, 30)
vals = [rng.randrange(base ** n) for _ in range(3)]
xs, ys, zs = (to_digits(v, n + 1, base) for v in vals)
s, c = csa(xs, ys, zs, base)
assert from_digits(s, base) + from_digits(c, base) == sum(vals)
assert max(c) <= 2
def test_many_operands() -> None:
rng = random.Random(2026)
for _ in range(600):
base = rng.choice((2, 10, 2**8, 2**32))
n = rng.randint(1, 12)
m = rng.randint(1, 40)
vals = [rng.randrange(base ** n) for _ in range(m)]
w = width_for(m, n, base)
nums = [to_digits(v, w, base) for v in vals]
total, steps = carry_save_sum(nums, base)
assert from_digits(total, base) == sum(vals)
assert steps == max(0, m - 2)
total, levels = carry_save_tree(nums, base)
assert from_digits(total, base) == sum(vals)
assert levels == _levels(m)
def test_edges() -> None:
for base in (2, 10, 2**16):
n = 5
top = base ** n - 1
w = width_for(3, n, base)
xs = to_digits(top, w, base)
s, c = csa(xs, xs, xs, base)
assert from_digits(s, base) + from_digits(c, base) == 3 * top
zero = to_digits(0, w, base)
s, c = csa(zero, zero, zero, base)
assert s == zero and c == zero
total, _ = carry_save_sum([xs] * 3, base)
assert from_digits(total, base) == 3 * top
def test_overflow_is_reported() -> None:
with pytest.raises(OverflowError):
csa([9], [9], [9], 10)
def test_tree_level_counts() -> None:
assert [_levels(m) for m in (2, 3, 4, 6, 9, 13, 19, 28, 42, 63)] == list(range(10))
for m in range(1, 200):
nums = [[1, 0, 0, 0]] * m
_, levels = carry_save_tree(nums, 1000)
assert levels == _levels(m)
What the next level needs
- the tests did not run in this build