Algorithm Search⌘K

Compressed sparse column in Python

Column-major analogue of CSR, storing in-neighbours of each vertex.

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

from collections.abc import Iterable


class CompressedSparseColumn:
    """A static directed graph on 0, ..., n - 1 stored by in-neighbours.

    The tails of the edges into v are src[coff[v]:coff[v + 1]], in increasing
    order. Building from CSR arrays takes Theta(n + m) time; in_neighbors takes
    Theta(d-(v)), in_degree O(1) and adjacent O(log d-(v)).
    """

    def __init__(self, n: int, off: list[int], adj: list[int]) -> None:
        """BUILD-FROM-CSR: off and adj are the CSR arrays of the same graph."""
        if n < 0 or len(off) != n + 1 or off[0] != 0 or off[n] != len(adj):
            raise ValueError("bad CSR arrays")
        self.n = n
        cnt = [0] * (n + 1)  # @line L1
        for v in adj:
            cnt[v + 1] += 1  # @line L2
        coff = [0] * (n + 1)
        for v in range(1, n + 1):
            coff[v] = coff[v - 1] + cnt[v]  # @line L3
        pos = coff[:n]  # @line L4
        src = [0] * len(adj)
        for u in range(n):  # @line L5
            for i in range(off[u], off[u + 1]):  # @line L6
                v = adj[i]  # @line L7
                src[pos[v]] = u
                pos[v] += 1
        self.coff = coff
        self.src = src

    @classmethod
    def from_edges(cls, n: int, edges: Iterable[tuple[int, int]]) -> "CompressedSparseColumn":
        """Build the (unsorted-row) CSR of the edges by a counting sort, then the CSC."""
        es = list(edges)
        for u, v in es:
            if not (0 <= u < n and 0 <= v < n):
                raise IndexError("vertex out of range")
        if len(set(es)) != len(es):
            raise ValueError("repeated edge")
        off = [0] * (n + 1)
        for u, _ in es:
            off[u + 1] += 1
        for u in range(1, n + 1):
            off[u] += off[u - 1]
        pos = off[:n]
        adj = [0] * len(es)
        for u, v in es:
            adj[pos[u]] = v
            pos[u] += 1
        return cls(n, off, adj)

    def in_neighbors(self, v: int) -> list[int]:
        if not 0 <= v < self.n:
            raise IndexError("vertex out of range")
        return [self.src[i] for i in range(self.coff[v], self.coff[v + 1])]  # @line L8

    def in_degree(self, v: int) -> int:
        if not 0 <= v < self.n:
            raise IndexError("vertex out of range")
        return self.coff[v + 1] - self.coff[v]  # @line L9

    def adjacent(self, u: int, v: int) -> bool:
        """Is (u, v) an edge?"""
        if not (0 <= u < self.n and 0 <= v < self.n):
            raise IndexError("vertex out of range")
        lo, hi = self.coff[v], self.coff[v + 1]  # @line L10
        while lo < hi:  # @line L11
            mid = (lo + hi) // 2  # @line L12
            if self.src[mid] < u:  # @line L13
                lo = mid + 1
            else:
                hi = mid
        return lo < self.coff[v + 1] and self.src[lo] == u  # @line L14

    def edges(self) -> list[tuple[int, int]]:
        return [(self.src[i], v) for v in range(self.n) for i in range(self.coff[v], self.coff[v + 1])]

    def pull_sum(self, x: list[float]) -> list[float]:
        """y = A^T x: y[v] is the sum of x[u] over the edges (u, v)."""
        return [sum(x[self.src[i]] for i in range(self.coff[v], self.coff[v + 1])) for v in range(self.n)]

    def check_invariant(self) -> None:
        coff, src, n = self.coff, self.src, self.n
        assert len(coff) == n + 1 and coff[0] == 0 and coff[n] == len(src)
        for v in range(n):
            assert coff[v] <= coff[v + 1]
            col = src[coff[v] : coff[v + 1]]
            assert all(col[i] < col[i + 1] for i in range(len(col) - 1))
            assert all(0 <= u < n for u in col)
test_compressed_sparse_column.py 58 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/compressed-sparse-column/. The authors are listed in prov/RG5BXY.jsonl.

import random

import pytest

from compressed_sparse_column import CompressedSparseColumn


def test_example() -> None:
    g = CompressedSparseColumn(5, [0, 2, 3, 4, 6, 6], [1, 2, 2, 3, 1, 4])
    g.check_invariant()
    assert g.coff == [0, 0, 2, 4, 5, 6]
    assert g.src == [0, 3, 0, 1, 2, 3]
    assert g.in_neighbors(1) == [0, 3]
    assert g.in_degree(0) == 0
    assert g.adjacent(1, 2)
    assert not g.adjacent(2, 1)
    assert g.pull_sum([1, 1, 1, 1, 1]) == [0, 2, 2, 1, 1]


def test_edge_cases() -> None:
    g = CompressedSparseColumn(0, [0], [])
    assert g.coff == [0] and g.edges() == []
    h = CompressedSparseColumn.from_edges(3, [])
    assert h.coff == [0, 0, 0, 0] and not h.adjacent(0, 1)
    loop = CompressedSparseColumn.from_edges(1, [(0, 0)])
    assert loop.in_neighbors(0) == [0] and loop.adjacent(0, 0)
    with pytest.raises(ValueError):
        CompressedSparseColumn(2, [0, 1], [1])
    with pytest.raises(ValueError):
        CompressedSparseColumn.from_edges(2, [(0, 1), (0, 1)])
    with pytest.raises(IndexError):
        CompressedSparseColumn.from_edges(2, [(0, 5)])
    with pytest.raises(IndexError):
        h.in_degree(3)


def test_random_against_set_model() -> None:
    rng = random.Random(5888)
    for _ in range(600):
        n = rng.randint(0, 8)
        pairs = [(u, v) for u in range(n) for v in range(n)]
        model = rng.sample(pairs, rng.randint(0, len(pairs)))
        g = CompressedSparseColumn.from_edges(n, model)
        g.check_invariant()
        assert sorted(g.edges()) == sorted(model)
        s = set(model)
        x = [rng.randint(-5, 5) for _ in range(n)]
        y = g.pull_sum(x)
        for v in range(n):
            ins = sorted(u for u, b in s if b == v)
            assert g.in_neighbors(v) == ins
            assert g.in_degree(v) == len(ins)
            assert y[v] == sum(x[u] for u in ins)
            for u in range(n):
                assert g.adjacent(u, v) == ((u, v) in s)

What the next level needs

  • the tests did not run in this build

Read the full page about Compressed sparse column