Algorithm Search⌘K

Ribbon filter in Python

Space-efficient static approximate set built from solving banded linear systems over GF(2).

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

"""Standard ribbon filter (Dillinger and Walzer 2021): banded on-the-fly elimination."""

import hashlib
import math
from collections.abc import Iterable

Key = str | bytes | int


class RibbonFilter:
    """A static filter: a solution Z of one xor equation per key, with r-bit values.

    Key x has a start s(x) in [0, m - w], a w-bit coefficient row c(x) with bit 0
    set, and an r-bit fingerprint fp(x); bit j of c(x) stands for row s(x) + j
    of Z. The filter stores Z with xor of Z[s + j] over the set bits j equal to
    fp(x) for each x in S. A key not in S is accepted with probability 2^-r.
    """

    def __init__(
        self,
        keys: Iterable[Key],
        r: int = 8,
        w: int = 64,
        overhead: float = 0.05,
        seed: int = 0,
        m: int | None = None,
    ) -> None:
        if r < 1 or w < 1 or overhead < 0:
            raise ValueError("need r >= 1, w >= 1 and overhead >= 0")
        self.r, self.w = r, w
        ks = list(keys)
        self.n = len(ks)
        self.m = m if m else max(w, math.ceil((1 + overhead) * self.n) + w)  # @line L1
        self.seed = seed
        while True:  # @line L2: new hash functions until every row fits
            rows = self._band(ks)
            if rows is not None:
                break
            self.seed += 1
        C, B = rows
        self.Z = [0] * self.m
        for i in range(self.m - 1, -1, -1):  # @line L8: back substitution
            if C[i]:
                z, c, j = B[i], C[i] >> 1, 1
                while c:
                    if c & 1:
                        z ^= self.Z[i + j]  # @line L9
                    c >>= 1
                    j += 1
                self.Z[i] = z

    def _hash(self, key: Key) -> tuple[int, int, int]:
        """The start s, the w-bit row c with bit 0 set, and the r-bit fingerprint."""
        d = hashlib.blake2b(repr(key).encode(), digest_size=24, key=self.seed.to_bytes(8, "little")).digest()
        a, b, f = (int.from_bytes(d[i : i + 8], "little") for i in range(0, 24, 8))
        s = a % (self.m - self.w + 1)
        c = (b % (1 << self.w)) | 1
        return s, c, f % (1 << self.r)

    def _band(self, keys: list[Key]) -> tuple[list[int], list[int]] | None:
        """Banded elimination: C[i] is 0 or has bit 0 set (leading column i)."""
        C, B = [0] * self.m, [0] * self.m
        for x in keys:
            s, c, b = self._hash(x)
            while True:  # @line L3
                if C[s] == 0:  # @line L4
                    C[s], B[s] = c, b
                    break
                c ^= C[s]  # @line L5
                b ^= B[s]
                if c == 0:  # @line L6: the row is a sum of stored rows
                    if b != 0:
                        return None
                    break
                j = (c & -c).bit_length() - 1  # @line L7
                s += j
                c >>= j
        return C, B

    def query(self, key: Key) -> bool:
        """False if key is surely not in S; True if it may be."""
        s, c, f = self._hash(key)
        acc, j = 0, 0
        while c:  # @line L10
            if c & 1:
                acc ^= self.Z[s + j]
            c >>= 1
            j += 1
        return acc == f

    def bits(self) -> int:
        """The size of Z in bits."""
        return self.m * self.r
test_ribbon_filter.py 73 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/ribbon-filter/. The authors are listed in prov/K1BXC2.jsonl.

import random

import pytest

from ribbon_filter import Key, RibbonFilter

TABLE: dict[Key, tuple[int, int, int]] = {
    "a": (0, 0b1011, 5),
    "b": (0, 0b0111, 9),
    "c": (1, 0b0101, 3),
    "d": (2, 0b1001, 12),
    "e": (0, 0b1011, 6),  # the row of a with another fingerprint
    "x": (1, 0b0011, 15),
    "y": (2, 0b0111, 1),
}


class FixedRibbon(RibbonFilter):
    def _hash(self, key: Key) -> tuple[int, int, int]:
        return TABLE[key]


def test_example() -> None:
    f = FixedRibbon("abcd", r=4, w=4, m=6)
    C, B = f._band(list("abcd"))
    assert C == [0b1011, 0b0101, 0b0011, 0b0101, 0, 0]
    assert B == [5, 3, 12, 0, 0, 0]
    assert f.Z == [6, 3, 12, 0, 0, 0]
    assert all(f.query(k) for k in "abcd")
    assert f.query("x")  # 3 ^ 12 = 15, a false positive
    assert not f.query("y")
    assert f._band(list("abcda")) is not None  # a repeated key is a redundant row
    assert f._band(list("abcde")) is None  # an inconsistent row


def test_edges() -> None:
    f = RibbonFilter([])
    assert f.m == 64 and not any(f.Z)
    g = RibbonFilter([3, 3, 3], r=16, w=8)
    assert g.query(3)
    with pytest.raises(ValueError):
        RibbonFilter([1], r=0)


def test_random_against_set() -> None:
    rng = random.Random(2021)
    for _ in range(500):
        n = rng.randint(0, 150)
        r = rng.randint(1, 12)
        w = rng.choice([8, 16, 32, 64])
        keys = rng.sample(range(10**6), n)
        f = RibbonFilter(keys, r=r, w=w, overhead=0.15, seed=rng.randrange(1000))
        assert f.m >= w and all(0 <= z < 1 << r for z in f.Z)
        assert all(f.query(x) for x in keys)
        for x in keys:
            s, c, fp = f._hash(x)
            assert c & 1 and s + c.bit_length() <= f.m
            acc = 0
            for j in range(w):
                if c >> j & 1:
                    acc ^= f.Z[s + j]
            assert acc == fp


def test_rate() -> None:
    keys = [f"k{i}" for i in range(4000)]
    f = RibbonFilter(keys, r=8, seed=3)
    assert f.bits() / len(keys) < 1.07 * 8
    fp = sum(f.query(f"z{i}") for i in range(30000))
    assert abs(fp / 30000 - 2**-8) < 0.0015

What the next level needs

  • the tests did not run in this build

Read the full page about Ribbon filter