Algorithm Search⌘K

AKS sorting network in Python

A sorting network of depth O(log n) built from expander-based halvers, with a very large constant.

File
aks_sorting_network.py, 206 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
aks_sorting_network.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aks-sorting-network/. The authors are listed in prov/9EWBX0.jsonl.

"""Seiferas's version of the AKS sorting network.

The network keeps its registers in bags, one bag for each node of a complete
binary tree, and repeats one stage: separate the items of each bag, send a
few items from each end back to the parent, and send the two halves of the
rest down to the children. The capacities shrink with time, and at the end
every item lies in a small subtree to which it is native.

The separator is pluggable. The default is an exact separator (a Batcher
odd-even merge network on the bag), which is correct for every n but not of
constant depth. The functions expander_halver and halver_error build and
measure the constant-depth epsilon-halvers that the O(log n) depth needs.
"""

import random
from fractions import Fraction
from functools import lru_cache
from itertools import product
from typing import Callable, Protocol, TypeVar


class Ordered(Protocol):
    def __lt__(self, other: "Ordered", /) -> bool: ...


T = TypeVar("T", bound=Ordered)

# Seiferas's parameters (A = 10, lambda = 1/100, nu = 13/20); with an exact
# separator (epsilon = 0) they satisfy all of his constraints.
A = Fraction(10)
LAM = Fraction(1, 100)
NU = Fraction(13, 20)

Comparator = tuple[int, int]  # (i, j): the minimum goes to register i
Separator = Callable[[list[int]], list[Comparator]]


def _oem(m: int) -> list[tuple[int, int]]:
    """Comparators (p, q), p < q, of odd-even merge sort on positions 0 .. m - 1."""
    size = 1
    while size < m:
        size *= 2
    net: list[tuple[int, int]] = []

    def merge(lo: int, w: int, r: int) -> None:
        s = 2 * r
        if s < w:
            merge(lo, w, s)
            merge(lo + r, w, s)
            net.extend((i, i + r) for i in range(lo + r, lo + w - r, s))
        else:
            net.append((lo, lo + r))

    def sort(lo: int, w: int) -> None:
        if w > 1:
            sort(lo, w // 2)
            sort(lo + w // 2, w // 2)
            merge(lo, w, 1)

    sort(0, size)
    return [(p, q) for p, q in net if q < m]


def exact_separator(regs: list[int]) -> list[Comparator]:
    """Sort the registers regs, in this order, with an odd-even merge network."""
    return [(regs[p], regs[q]) for p, q in _oem(len(regs))]


def aks_network(n: int, separate: Separator = exact_separator) -> tuple[list[Comparator], list[int]]:
    """Return (comparators, order) of the network for n = 2^k >= 2 registers.

    After the comparators run, out[r] = register order[r] holds the item of
    rank r.
    """
    levels = n.bit_length() - 1  # lg n
    leaf = levels - 1  # depth of the bags of pairs
    net: list[Comparator] = []
    bags: dict[tuple[int, int], list[int]] = {(0, 0): list(range(n))}  # @line L1
    t = 0
    while n * NU**t * A**leaf >= 1 / LAM:  # @line L2
        new: dict[tuple[int, int], list[int]] = {}
        for (d, i), regs in bags.items():  # @line L3
            if not regs:
                continue
            b = n * NU**t * A**d  # @line L4
            net.extend(separate(regs))  # @line L5
            rest = regs
            if d > 0:
                k = int(LAM * b)  # @line L6
                if 2 * k >= len(regs):
                    up, rest = regs, []
                else:
                    up, rest = regs[:k] + regs[len(regs) - k :], regs[k : len(regs) - k]
                if len(rest) % 2 == 1:  # @line L7
                    mid = len(rest) // 2
                    up, rest = up + [rest[mid]], rest[:mid] + rest[mid + 1 :]
                new.setdefault((d - 1, i // 2), []).extend(up)
            if rest:
                if d == leaf:
                    raise AssertionError("a leaf bag must evacuate items downward")
                h = len(rest) // 2  # @line L8
                new.setdefault((d + 1, 2 * i), []).extend(rest[:h])
                new.setdefault((d + 1, 2 * i + 1), []).extend(rest[h:])
        bags = new
        t += 1  # @line L9
    k = 0
    while (1 / LAM) * (1 / A) ** (k + 1) >= 1:
        k += 1
    top = max(leaf - k, 0)
    width = n >> top
    order = [0] * n
    for i in range(1 << top):  # @line L10
        regs = [r for (d, j), rs in bags.items() if d >= top and j >> (d - top) == i for r in rs]
        if any(rs for (d, _), rs in bags.items() if d < top):
            raise AssertionError("a bag above the final subtrees is not empty")
        if len(regs) != width:
            raise AssertionError("a final subtree holds the wrong number of items")
        net.extend(exact_separator(regs))
        order[i * width : (i + 1) * width] = regs
    return net, order


@lru_cache(maxsize=None)
def _cached_network(n: int) -> tuple[tuple[Comparator, ...], tuple[int, ...]]:
    net, order = aks_network(n)
    return tuple(net), tuple(order)


def stages(n: int) -> int:
    """Return the number of separation stages of the network for n = 2^k registers."""
    leaf = n.bit_length() - 2
    t = 0
    while n * NU**t * A**leaf >= 1 / LAM:
        t += 1
    return t


def aks_sorting_network(a: list[T]) -> None:
    """Sort a in place, in non-decreasing order, with the bag network of Seiferas.

    The length is padded to a power of 2 (at least 2) with a top element that
    is larger than every item.
    """
    n = len(a)
    if n < 2:
        return
    size = 2
    while size < n:
        size *= 2
    top = object()
    x: list[object] = list(a) + [top] * (size - n)

    def less(u: object, v: object) -> bool:
        if v is top:
            return u is not top
        if u is top:
            return False
        return u < v  # type: ignore[operator]

    net, order = _cached_network(size)
    for i, j in net:
        if less(x[j], x[i]):  # @line L11
            x[i], x[j] = x[j], x[i]
    a[:] = [x[r] for r in order[:n]]  # type: ignore[misc]


def expander_halver(m: int, d: int, seed: int) -> list[list[Comparator]]:
    """Return d layers; layer s compares wire u with wire m + p_s(u) for a random permutation p_s.

    The union of the d perfect matchings is a d-regular bipartite graph
    between the halves 0 .. m - 1 and m .. 2m - 1, and each comparator puts
    the minimum on the first half.
    """
    rng = random.Random(seed)
    layers = []
    for _ in range(d):
        p = list(range(m))
        rng.shuffle(p)
        layers.append([(u, m + p[u]) for u in range(m)])
    return layers


def halver_error(layers: list[list[Comparator]], m: int) -> Fraction:
    """Return the smallest epsilon for which the layers form an epsilon-halver on 2m wires.

    By the zero-one principle it is enough to try all 0-1 inputs: for z <= m
    zeros, count the zeros that end in the second half, and for z >= m, the
    ones that end in the first half.
    """
    worst = Fraction(0)
    for bits in product((0, 1), repeat=2 * m):
        x = list(bits)
        for layer in layers:
            for i, j in layer:
                if x[i] > x[j]:
                    x[i], x[j] = x[j], x[i]
        zeros = x.count(0)
        if 0 < zeros <= m:
            worst = max(worst, Fraction(x[m:].count(0), zeros))
        ones = 2 * m - zeros
        if 0 < ones <= m:
            worst = max(worst, Fraction(x[:m].count(1), ones))
    return worst
test_aks_sorting_network.py 78 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aks-sorting-network/. The authors are listed in prov/9EWBX0.jsonl.

import random

from aks_sorting_network import aks_network, aks_sorting_network, expander_halver, halver_error, stages


def run_zero_one(net: list[tuple[int, int]], order: list[int], n: int) -> bool:
    """Run all 2^n inputs at once: bit v of wire[k] is item k of input v."""
    m = 1 << n
    wire = []
    for k in range(n):
        h = 1 << k
        wire.append((((1 << h) - 1) << h) * (((1 << m) - 1) // ((1 << (2 * h)) - 1)))
    for i, j in net:
        wire[i], wire[j] = wire[i] & wire[j], wire[i] | wire[j]
    out = [wire[r] for r in order]
    return all(out[k] & ~out[k + 1] == 0 for k in range(n - 1))


def test_example() -> None:
    a = [11, 3, 14, 0, 9, 6, 15, 1, 12, 5, 8, 2, 13, 7, 10, 4]
    aks_sorting_network(a)
    assert a == list(range(16))


def test_stage_counts() -> None:
    assert [stages(1 << k) for k in range(1, 11)] == [0, 0, 5, 12, 19, 26, 33, 40, 47, 54]


def test_all_zero_one_inputs() -> None:
    for n in (2, 4, 8, 16):
        net, order = aks_network(n)
        assert run_zero_one(net, order, n)
        assert sorted(order) == list(range(n))


def test_permutations_large() -> None:
    rng = random.Random(7)
    for n in (32, 64, 128, 256):
        net, order = aks_network(n)
        for _ in range(20):
            x = list(range(n))
            rng.shuffle(x)
            for i, j in net:
                if x[j] < x[i]:
                    x[i], x[j] = x[j], x[i]
            assert [x[r] for r in order] == list(range(n))


def test_edges() -> None:
    for a in ([], [1], [2, 1], [1, 2], [3, 3, 3], [5, 4, 3, 2, 1], list(range(40, 0, -1)), ["b", "a", "c"]):
        b = list(a)
        aks_sorting_network(b)
        assert b == sorted(a)


def test_halvers() -> None:
    # A single matching between the halves is a 1/2-halver and no better.
    assert halver_error(expander_halver(4, 1, 1), 4) == 0.5
    # The complete bipartite graph K_{m,m}, as m matchings, is an exact halver.
    m = 4
    layers = [[(u, m + (u + s) % m) for u in range(m)] for s in range(m)]
    assert halver_error(layers, m) == 0
    # A layer only moves zeros down and ones up, so more layers never increase the error.
    errors = [halver_error(expander_halver(6, d, 3), 6) for d in range(1, 5)]
    assert errors == sorted(errors, reverse=True)


def test_random() -> None:
    rng = random.Random(2024)
    for _ in range(600):
        n = rng.randrange(0, 70)
        a = [rng.randrange(-20, 20) for _ in range(n)]
        b = list(a)
        aks_sorting_network(b)
        assert b == sorted(a)

What the next level needs

  • the tests did not run in this build

Read the full page about AKS sorting network