Algorithm Search⌘K

Elias-Fano encoding in Python

A succinct representation of a monotone integer sequence that supports access and successor queries.

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

from collections.abc import Sequence

W = 64
Q = 64  # sampling rate of the select samples on H


class _HighBits:
    """The bit vector H with select1 and select0 by sampled positions.

    Every Q-th 1 and every Q-th 0 has its position stored; a select jumps to
    the sample and scans words, which is O(1) when the samples are dense
    enough and the rank/select dictionary of the page gives worst-case O(1).
    """

    def __init__(self, ones: list[int], length: int) -> None:
        self.length = length
        self.words = [0] * (length // W + 1)
        for p in ones:
            self.words[p // W] |= 1 << (p % W)
        self.samples: dict[int, list[int]] = {1: [], 0: []}
        count = {1: 0, 0: 0}
        for p in range(length):
            bit = (self.words[p // W] >> (p % W)) & 1
            if count[bit] % Q == 0:
                self.samples[bit].append(p)
            count[bit] += 1
        self.count = count

    def _word(self, k: int, bit: int) -> int:
        x = self.words[k]
        if bit == 0:
            x = ~x & ((1 << W) - 1)
            if k == self.length // W:
                x &= (1 << (self.length % W)) - 1
        return x

    def select(self, j: int, bit: int) -> int:
        """Return the position of the j-th bit equal to `bit`, for j >= 1."""
        if not 1 <= j <= self.count[bit]:
            raise IndexError(j)
        p = self.samples[bit][(j - 1) // Q]
        j -= (j - 1) // Q * Q  # p is the position of the wanted bit number 1
        k = p // W
        x = self._word(k, bit) & ~((1 << (p % W)) - 1)
        while x.bit_count() < j:
            j -= x.bit_count()
            k += 1
            x = self._word(k, bit)
        for _ in range(j - 1):
            x &= x - 1
        return k * W + (x & -x).bit_length() - 1


class EliasFano:
    """A static nondecreasing sequence of integers in [0, u), stored as low bits
    in plain form and high bits as unary gaps in a bit vector H."""

    def __init__(self, xs: Sequence[int], u: int) -> None:
        n = len(xs)
        if u < 1:
            raise ValueError("u must be positive")
        for i, x in enumerate(xs):
            if not 0 <= x < u or (i and xs[i - 1] > x):
                raise ValueError("the sequence must be nondecreasing in [0, u)")
        self.n, self.u = n, u
        ell = (u // n).bit_length() - 1 if 0 < n <= u else 0  # @line L1
        self.ell = ell
        self.low = 0  # the low parts, packed into one integer of n * ell bits
        ones = []
        for i, x in enumerate(xs):  # @line L2
            self.low |= (x & ((1 << ell) - 1)) << (i * ell)  # @line L3
            ones.append((x >> ell) + i)  # @line L4
        self.H = _HighBits(ones, n + ((u - 1) >> ell) + 1)

    def __len__(self) -> int:
        return self.n

    def _low(self, i: int) -> int:
        return (self.low >> (i * self.ell)) & ((1 << self.ell) - 1)

    def access(self, i: int) -> int:
        """Return x_i, for 0 <= i < n."""
        if not 0 <= i < self.n:
            raise IndexError(i)
        return ((self.H.select(i + 1, 1) - i) << self.ell) | self._low(i)  # @line L5

    def next_geq(self, x: int) -> int:
        """Return the smallest i with x_i >= x, or n if there is none, for 0 <= x < u."""
        if not 0 <= x < self.u:
            raise IndexError(x)
        h = x >> self.ell  # @line L6
        p = 0 if h == 0 else self.H.select(h, 0) + 1  # @line L7
        i = p - h  # @line L8
        while i < self.n and self.access(i) < x:  # @line L9
            i += 1
        return i  # @line L10

    def size_in_bits(self) -> int:
        """Return n * ell + |H|, the space without the select samples."""
        return self.n * self.ell + self.H.length
test_elias_fano_encoding.py 62 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/elias-fano-encoding/. The authors are listed in prov/XGWQ3Q.jsonl.

import bisect
import random

import pytest

from elias_fano_encoding import EliasFano


def check_all(xs: list[int], u: int) -> None:
    ef = EliasFano(xs, u)
    n = len(xs)
    assert [ef.access(i) for i in range(n)] == xs
    for x in range(u):
        assert ef.next_geq(x) == bisect.bisect_left(xs, x)
    if 0 < n <= u:
        assert ef.size_in_bits() < n * ef.ell + 3 * n + 2


def test_page_example() -> None:
    xs = [3, 4, 7, 13, 14, 15, 21, 43]
    ef = EliasFano(xs, 64)
    assert ef.ell == 3
    h = "".join(str((ef.H.words[0] >> p) & 1) for p in range(16))
    assert h == "1110111010001000"
    assert ef.size_in_bits() == 40
    assert ef.access(6) == 21 and ef.access(7) == 43
    assert ef.H.select(1, 0) == 3 and ef.H.select(3, 0) == 9
    assert ef.next_geq(14) == 4
    assert ef.next_geq(30) == 7
    assert ef.next_geq(44) == 8
    check_all(xs, 64)


def test_edges() -> None:
    check_all([], 1)
    check_all([], 10)
    check_all([0], 1)
    check_all([5] * 20, 6)  # u < n, ell = 0
    check_all([0, 999], 1000)
    check_all(list(range(300)), 300)
    with pytest.raises(ValueError):
        EliasFano([3, 2], 10)
    with pytest.raises(ValueError):
        EliasFano([10], 10)
    with pytest.raises(IndexError):
        EliasFano([1], 10).access(1)


def test_random_against_bisect() -> None:
    rng = random.Random(1974)
    for _ in range(500):
        u = rng.randrange(1, 2000)
        n = rng.randrange(0, 300)
        xs = sorted(rng.randrange(u) for _ in range(n))
        ef = EliasFano(xs, u)
        assert [ef.access(i) for i in range(n)] == xs
        for _ in range(40):
            x = rng.randrange(u)
            assert ef.next_geq(x) == bisect.bisect_left(xs, x)

What the next level needs

  • the tests did not run in this build

Read the full page about Elias-Fano encoding