Algorithm Search⌘K

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

Read the full page about Carry-save addition