Algorithm Search⌘K

SRT division in Python

Digit-recurrence division choosing quotient digits from a small lookup table, one digit per step.

File
srt_division.py, 98 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
srt_division.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/srt-division/. The authors are listed in prov/732WBN.jsonl.

"""Radix-4 SRT division with digit set {-2, ..., 2} and a derived selection table.

Fixed-point convention: x and d lie in [1/2, 1) and are given as integers
X = x * 2^F and D = d * 2^F. The partial remainder w satisfies |w| <= (2/3) d.
The quotient digit is chosen from 4 bits of d (0.1xxx) and 4w truncated to
3 fractional bits, exactly as a hardware table lookup would.
"""

import math
from fractions import Fraction

RHO = Fraction(2, 3)  # redundancy factor a/(r-1) = 2/3
D_BITS = 4  # divisor estimate 0.1xxx: 4 bits after the binary point
Y_BITS = 3  # fractional bits of the remainder estimate


def build_table(d_bits: int = D_BITS, y_bits: int = Y_BITS) -> dict[tuple[int, int], int]:
    """For each (divisor cell, remainder cell) pick a digit k that keeps |4w - k d| <= rho d.

    A cell is the set of exact (y, d) with d in [i/2^d_bits, (i+1)/2^d_bits) and
    y in [j/2^y_bits, (j+1)/2^y_bits) and |y| <= (8/3) d. Raises if some cell has no digit.
    """
    table = {}
    for i in range(2 ** (d_bits - 1), 2**d_bits):
        dlo, dhi = Fraction(i, 2**d_bits), Fraction(i + 1, 2**d_bits)
        lim = math.ceil(4 * RHO * dhi * 2**y_bits) + 1
        for j in range(-lim - 1, lim + 1):
            ylo, yhi = Fraction(j, 2**y_bits), Fraction(j + 1, 2**y_bits)
            if ylo > 4 * RHO * dhi or yhi <= -4 * RHO * dhi:
                continue  # unreachable cell
            feasible = []
            for k in range(-2, 3):
                low_ok = k == -2 or ylo >= max((k - RHO) * dlo, (k - RHO) * dhi)
                high_ok = k == 2 or yhi <= min((k + RHO) * dlo, (k + RHO) * dhi)
                if low_ok and high_ok:
                    feasible.append(k)
            if not feasible:
                raise ValueError(f"no digit for cell {(i, j)}")
            table[(i, j)] = min(feasible, key=lambda k: (abs(k), -k))
    return table


TABLE = build_table()


def srt_divide_fixed(X: int, D: int, F: int, n: int, table=None) -> tuple[int, int]:
    """n radix-4 SRT steps on x = X/2^F and d = D/2^F, both in [1/2, 1).

    The remainder is held as W = w * 2^(F+2), so w0 = x/4 is exact.
    Returns (Q, W) with x/(4d) = Q/4^n + w/(4^n d) and 0 <= w < d.
    """
    table = TABLE if table is None else table
    d_index = D >> (F - D_BITS)  # 0.1xxx, the 4 leading bits of d
    D4 = 4 * D  # d in the units of W
    W, Q = X, 0  # @line L1
    for _ in range(n):  # @line L2
        y_index = (4 * W) >> (F + 2 - Y_BITS)  # @line L3
        q = table[(d_index, y_index)]  # @line L4
        W = 4 * W - q * D4  # @line L5
        Q = 4 * Q + q  # @line L6
    if W < 0:  # @line L7
        Q, W = Q - 1, W + D4
    return Q, W  # @line L8


def srt_float_divide(a: float, b: float) -> float:
    """a / b correctly rounded (ties to even) via radix-4 SRT, for positive normal results."""
    if not (a > 0 and b > 0 and math.isfinite(a) and math.isfinite(b)):
        raise ValueError("positive finite operands expected")
    ma, ea = math.frexp(a)  # ma in [1/2, 1)
    mb, eb = math.frexp(b)
    F = 53
    X, D = int(ma * 2**F), int(mb * 2**F)
    n = 28  # 56 quotient bits of x/(4d); x/d = 4 Q / 4^n + ...
    Q, W = srt_divide_fixed(X, D, F, n)  # W > 0 iff the division is inexact
    # x/d = Q * 2^(2 - 2n) + tail, with tail in [0, 2^(2 - 2n)); sticky = tail > 0
    shift = Q.bit_length() - 53
    keep, rest = Q >> shift, Q & ((1 << shift) - 1)
    half = 1 << (shift - 1)
    if rest > half or (rest == half and (W != 0 or keep % 2 == 1)):
        keep += 1
    return math.ldexp(keep, shift + 2 - 2 * n + ea - eb)


def trace(x: Fraction, d: Fraction, n: int) -> list[tuple[Fraction, Fraction, int]]:
    """Rows (4w, its truncated estimate, chosen digit) of the first n steps, in exact rationals."""
    rows, w = [], x / 4
    d_index = math.floor(d * 2**D_BITS)
    for _ in range(n):
        y = 4 * w
        j = math.floor(y * 2**Y_BITS)
        q = TABLE[(d_index, j)]
        rows.append((y, Fraction(j, 2**Y_BITS), q))
        w = y - q * d
    return rows
test_srt_division.py 79 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/srt-division/. The authors are listed in prov/732WBN.jsonl.

import math
import random
from fractions import Fraction

import pytest

from srt_division import RHO, TABLE, build_table, srt_divide_fixed, srt_float_divide, trace


def test_example_trace():
    rows = trace(Fraction(3, 4), Fraction(5, 8), 6)
    assert [q for _, _, q in rows] == [1, 1, -1, 1, -1, 1]
    Q, W = srt_divide_fixed(3 << 8, 5 << 7, 10, 8)
    assert (Q, W) == (19660, 2048)
    assert Fraction(Q, 4**8) + Fraction(W, 2**12) / (4**8 * Fraction(5, 8)) == Fraction(3, 10)


def test_table_size_and_coarser_estimates_fail():
    assert len(TABLE) == 275
    with pytest.raises(ValueError):
        build_table(4, 2)  # two fractional bits of 4w are not enough
    with pytest.raises(ValueError):
        build_table(3, 3)  # nor are two bits of d after the leading one


def test_remainder_stays_bounded():
    rng = random.Random(11)
    for _ in range(400):
        x = Fraction(rng.randrange(2**30, 2**31), 2**31)
        d = Fraction(rng.randrange(2**30, 2**31), 2**31)
        w = x / 4
        for y, est, q in trace(x, d, 20):
            assert est <= y < est + Fraction(1, 8)
            w = y - q * d
            assert abs(w) <= RHO * d


def test_fixed_point_identity():
    rng = random.Random(12)
    F = 40
    for _ in range(2000):
        X, D = rng.randrange(2 ** (F - 1), 2**F), rng.randrange(2 ** (F - 1), 2**F)
        n = rng.randrange(1, 30)
        Q, W = srt_divide_fixed(X, D, F, n)
        x, d, w = Fraction(X, 2**F), Fraction(D, 2**F), Fraction(W, 2 ** (F + 2))
        assert 0 <= w < d
        assert x / (4 * d) == Fraction(Q, 4**n) + w / (4**n * d)


def test_float_division_correctly_rounded():
    rng = random.Random(13)
    for _ in range(20000):
        a = math.ldexp(rng.uniform(0.5, 1.0), rng.randint(-400, 400))
        b = math.ldexp(rng.uniform(0.5, 1.0), rng.randint(-400, 400))
        assert srt_float_divide(a, b) == a / b
    for a, b in [(1.0, 3.0), (3.0, 1.5), (1.0, 1.0), (2.0 - 2.0**-52, 1.0 + 2.0**-52), (4195835.0, 3145727.0)]:
        assert srt_float_divide(a, b) == a / b


def test_missing_table_entries_give_wrong_quotients():
    # Pentium-style fault: five rarely used cells near the top of the table return 0 instead of 2
    bad = dict(TABLE)
    for cell in [(11, 13), (12, 15), (13, 15), (14, 15), (15, 19)]:
        assert bad[cell] == 2
        bad[cell] = 0
    rng = random.Random(14)
    wrong = 0
    for _ in range(3000):
        X, D = rng.randrange(2**52, 2**53), rng.randrange(2**52, 2**53)
        exact = (X * 4**28) // (4 * D)
        assert srt_divide_fixed(X, D, 53, 28)[0] == exact
        try:
            wrong += srt_divide_fixed(X, D, 53, 28, table=bad)[0] != exact
        except KeyError:  # the remainder left the range the table covers
            wrong += 1
    assert 0 < wrong < 3000

What the next level needs

  • the tests did not run in this build

Read the full page about SRT division