Skip to content

Design: graph

Why CSR arrays, not a Graph class

A Graph jitclass would add construction ceremony and force one blessed representation. CSR (indptr, indices) is already the lingua franca (scipy.sparse.csr_matrix uses the same layout), costs two plain arrays, and passes across the @njit boundary for free. Every algorithm takes the arrays directly; edges_to_csr builds them from the edge lists users actually start with, and returns an order array so any per-edge payload (weights, labels) can be aligned with one fancy-index — no zipped structs needed in nopython.

Why traversal bounds-checks indices

Kernels compile without bounds checking, so a malformed CSR (an indices entry outside [0, n)) would write through a wild pointer — exactly the silent-corruption class the 0.1.2 audit fixed in histogram. The per-edge check is one predictable branch; the algorithms raise ValueError instead of corrupting memory. indptr monotonicity is trusted (checking it per node would duplicate work the construction already guarantees).

Why Dijkstra carries its own heap

collections.PriorityQueue stores bare priorities — no payload — and Dijkstra needs (distance, node) pairs. Rather than a pair-encoding hack (packing node ids into float mantissas loses exactness), the kernel keeps two parallel arrays as a binary heap with lazy deletion: each successful relaxation pushes, stale entries are skipped on pop via the done mask. Each edge relaxes at most once, so the heap is allocated once at m + 1 — zero allocation inside the loop, no decrease-key machinery.

NaN weights are rejected up front — the same lesson as weighted_sampling: NaN passes a plain w < 0 check and would silently corrupt distances. +inf is legal (the edge is effectively absent).

Why iterative DFS with a preorder-preserving stack

Recursion in nopython is a native-stack crash risk, not a catchable exception. The explicit stack pushes neighbors in reverse CSR order so the produced preorder is identical to the recursive formulation — reference implementations in tests stay trivially comparable.

Why Kahn (not DFS) for topological_sort

Kahn's algorithm gives cycle detection for free (processed count < n) and a deterministic, explainable order: ready nodes leave in ascending index. DFS-based toposort would need the same explicit-stack care for a less useful ordering guarantee.

Why union returns a bool

UnionFind.union returning "did a merge happen" makes the common Kruskal/cycle-detection loop a one-liner and costs nothing. Union by size (not rank) keeps a real component_size query for free.