Hodge Laplacians, simplicial complexes (up to 2-simplices), and the 3-way gradient + curl + harmonic Hodge decomposition of edge flows. Pure scipy.sparse, no C++ dependencies, no optional backends.
The topological-signal-processing ecosystem on PyPI has gaps the audit for our Phase 7 work called out explicitly:
toposig— does not exist (hallucinated by LLM ideation)HodgeLaplacian(PyPI) — fake package, do not installtoponetx— real but v0.x with volatile API surface; not reliable for productiongraph-hodge— real, but graph-only, no simplicial complex support
hodgex fills the narrow, stable slice: build a 2-complex, compute its boundary matrices and Hodge Laplacians, project signals onto the low-frequency spectral basis, and get the canonical 3-way edge-flow decomposition. Everything is testable against hand-checkable fixtures (K5, C6, K4-with-filled-triangles).
pip install hodgexPython ≥ 3.9, numpy ≥ 1.23, scipy ≥ 1.10.
import numpy as np
from hodgex import SimplicialComplex2, hodge_laplacian_0, hodge_laplacian_1, low_freq_projection, hodge_decompose_edge
# Build K5 (complete graph on 5 vertices) and fill every triangle
sc = SimplicialComplex2.from_edges(
n_vertices=5,
edges=[(i, j) for i in range(5) for j in range(i + 1, 5)],
).fill_all_triangles()
# Boundary matrices: B1 is (V, E), B2 is (E, T)
B1, B2 = sc.boundary_matrices()
# Hodge Laplacians
L0 = hodge_laplacian_0(B1) # vertex Laplacian = classical graph Laplacian
L1 = hodge_laplacian_1(B1, B2) # edge Laplacian (the "real" Hodge object)
# Spectral smoothing: project a vertex signal onto the k lowest eigenvectors of L0
signal = np.array([1., 2., 3., 4., 100.]) # vertex 4 is an outlier
smoothed = low_freq_projection(signal, L0, k=2)
# 3-way Hodge decomposition of an edge flow
edge_flow = np.random.default_rng(0).standard_normal(B1.shape[1])
grad, curl, harm = hodge_decompose_edge(edge_flow, B1, B2)
assert np.allclose(grad + curl + harm, edge_flow)| Function | Returns |
|---|---|
SimplicialComplex2(n, edges, triangles) |
an oriented 2-complex; rejects self-loops, enforces triangle ⊂ edges |
.fill_all_triangles() |
mutates to add every 3-clique as a triangle (fills C_n's cycles) |
.boundary_matrices() |
returns (B1, B2) as scipy.sparse.csr_matrix |
hodge_laplacian_0(B1) |
L0 = B1 @ B1.T — classical graph Laplacian |
hodge_laplacian_1(B1, B2) |
L1 = B1.T @ B1 + B2 @ B2.T — edge Laplacian |
hodge_laplacian_2(B2) |
L2 = B2.T @ B2 |
low_freq_projection(signal, L, k) |
topology-aware smoothing: project onto k smallest eigenvectors |
kernel_dimension(L) |
Betti number (β_0 from L0, β_1 from L1) via low-eigenvalue counting |
hodge_decompose_edge(flow, B1, B2) |
(grad, curl, harm) — sums exactly to flow, mutually L2-orthogonal |
An oriented 2-complex has a chain complex C_2 → C_1 → C_0 with boundary maps ∂_2 = B2 (triangles → edges) and ∂_1 = B1 (edges → vertices). By construction ∂_1 ∘ ∂_2 = 0 — hodgex asserts this at build time.
The Hodge Laplacian at dimension k combines the two adjacent boundaries:
L0 = ∂_1 ∂_1ᵀ (vertices: the combinatorial graph Laplacian)
L1 = ∂_1ᵀ ∂_1 + ∂_2 ∂_2ᵀ (edges: down-Laplacian + up-Laplacian)
L2 = ∂_2ᵀ ∂_2 (triangles)
The Hodge decomposition theorem on C_1 (edge signals):
C_1 = im(∂_1ᵀ) ⊕ im(∂_2) ⊕ ker(L1)
≡ gradient ⊕ curl ⊕ harmonic
Every edge flow splits uniquely into:
- a gradient part:
∂_1ᵀ vfor some vertex potentialv(conservative flow) - a curl part:
∂_2 tfor some triangle circulationt(vortex flow) - a harmonic part: in the kernel of L1 — neither a gradient nor a curl (persistent cycles)
The three subspaces are mutually L²-orthogonal. The dimension of the harmonic subspace equals β_1, the first Betti number — the number of independent 1-cycles not filled in by triangles.
Key subtlety: on a connected graph, the kernel of L0 is only the constant vector (β_0 = 1). So the "harmonic component of a vertex signal" is trivially the signal's mean. The interesting content of vertex-signal smoothing is the low-eigenvalue projection onto L0 — low_freq_projection(signal, L0, k) returns exactly this. The genuine 3-way gradient + curl + harmonic split only appears on edge signals (1-forms); hence hodge_decompose_edge.
16 unit tests cover:
| Test | Checks |
|---|---|
| K5 spectrum | L0 eigenvalues exactly [0, 5, 5, 5, 5] |
| K5 triangle count | fill_all_triangles produces 10 triangles (C(5,3)) |
| C6 β_1 | one independent cycle on a 6-gon |
| C3 unfilled | β_1 = 1 |
| C3 filled | β_1 = 0 (triangle kills the cycle) |
| K4 filled | β_1 = 0 |
| Two disjoint edges | β_0 = 2 (matches connected components) |
| Chain complex | B1 @ B2 = 0 exactly at machine precision |
| Low-freq k=1 | returns the mean broadcast |
| Low-freq k monotone | more eigenvectors → smaller reconstruction error |
| Hodge decomp sum | grad + curl + harm == flow |
| Harmonic on C6 | pure-cycle flow projects onto the 1-dim L1 kernel |
| Kernel dimension helper | matches dense eigvalsh count |
| Input validation | self-loops, missing triangle edges, out-of-range vertices all raise |
Run with pytest.
v0.2 (planned):
- Weighted Hodge Laplacian (per-edge weights for non-combinatorial cases)
- 3-simplex (tetrahedron) support, capped — compute grows as O(C(n, 4))
- Persistent homology wrapper (bridge to ripser for the Phase 7B path)
- GPU-backed eigendecomposition via
cupy.sparseoptional extra
Built for a signal-denoising application (L0 spectral filtering on a correlation-network backbone). Sister library to phawkes (Hawkes processes), fisherrao (information geometry), tailcor (tail-contagion decomposition), diebold-yilmaz (spillover index). Same "small, tested, publishable" ethos.
- Pierre Samson (@darw007d) — idea, use-case, design decisions
- Claude Opus (Anthropic) — implementation and tests
- Lim, L.-H. (2020). Hodge Laplacians on graphs. SIAM Review 62(3), 685-715.
- Schaub, M.T. et al. (2020). Signal processing on higher-order networks: Livin' on the edge... and beyond. Signal Processing 187.
- Eckmann, B. (1944). Harmonische Funktionen und Randwertaufgaben in einem Komplex. Commentarii Mathematici Helvetici 17.
MIT — see LICENSE.