A metric-free computational topology engine.
graphos represents a finite stratified signed incidence structure C = (N₀, …, N_n ; ∂₁, …, ∂_n) — cell counts per stratum, boundary operators ∂_k with entries in {−1, 0, +1}, and the chain maps its operations induce. A k-cell is an index: no realization, coordinate or metric is stored, and none is needed to state or check anything below. Complexes may be mixed-dimensional, polytopal, non-regular, non-manifold and non-orientable; validity is queried rather than presupposed.
The formal specification — objects, morphisms, the operation calculus, the
laws and the tests that witness them — is THEORY.md. Geometry,
metrics, Hodge stars and PDE solvers belong to the companion project
exokal.
The surface, in the vocabulary of THEORY.md:
| Term | Meaning in graphos |
|---|---|
| k-cell | A cell of stratum k; an index, carrying no realization |
| complex | C = (N₀, …, N_n ; ∂₁, …, ∂_n); a chain complex exactly when ∂∘∂ = 0, which d_squared_is_zero decides |
| boundary operator ∂_k | ∂_k ∈ {−1, 0, +1}^{N_{k−1} × N_k} in signed CSR; row σ holds the (k−1)-faces τ of σ with [σ : τ] |
| attaching a cell | attach_cell — a k-cell along the (k−1)-chain ∂σ, the CW paradigm |
| mesh ingestion | from_edges, from_polygons (vertex cycles), from_polyhedra (face lists), from_simplices(d, …) in any dimension. Intermediate strata are derived, orientation conventions are deterministic, and ∂∘∂ = 0 holds by construction |
| chain map | f_* : C_k(C) → C_k(C′), induced on generators by every operation; a generator may be sent to 0 |
| coproduct | disjoint_union(a, b) — A ⊔ B, strata concatenated; the factors may differ in dimension |
| quotient | quotient(c, identifications) — C/~; an orientation flip propagates into st(σ) |
| parallel cells | find_parallel_cells(c, k) — k-cells whose boundary chains agree as signed sets up to a uniform flip |
| pushout A ⊔_C B | pushout(a, b, vertex_identifications) — glued along the subcomplex C the identified vertices generate; deduplication extends C upward and makes the interface conforming |
| star deletion | star_deletion(c, marker) — removes st(S), leaving the largest subcomplex meeting no marked cell |
| cutting along a subcomplex | cut_along(c, interface_marker) — for x ∈ cl(S), the sides are the incidence-connected components of st(x) ∖ cl(S), each receiving a copy of x. cl(S) survives as a detached subcomplex; a one-sided rim is not copied, giving the crack front, and a junction gets one copy per side. No geometric side test enters |
| coboundary δ_k | coboundary(c, k) — δ_k = ∂_{k+1}^T; applied to a k-cochain it is the discrete exterior derivative |
| incidence I(k, j) | incidence(c, k, j) — per k-cell σ, the j-cells of cl(σ) (j < k), of st(σ) (j > k), or σ itself. Unsigned by necessity: a multi-level composite of incidence numbers telescopes to 0 by ∂∘∂ = 0 |
| connected components | connected_components(c, k, via[, exclude]) — components of k-cells adjacent through a common via-cell; excluding marked connectors gives the sides of a cut. connected_components(c) is β₀ of the whole complex |
| product A × B | product(a, b) — ∂(α × β) = ∂α × β + (−1)^{dim α} α × ∂β. product(mesh, segment) is extrusion; χ(A × B) = χ(A) · χ(B); the factor structure survives in blocks |
| lifting identifications | lift_identifications(c, vertex_pairs) — extends a vertex pairing upward by boundary-chain matching, reporting rel_sign. With quotient, a periodic identification in two calls: a square becomes a cylinder, a twisted pairing a Möbius band |
| barycentric subdivision | barycentric_subdivision(c) — sd(C), the order complex of the face poset: one vertex per cell, one k-simplex per strict chain of k+1 cells. Simplicial from any complex. The carrier is the refinement relation; the signed carrier ∏ᵢ [cᵢ : cᵢ₊₁] is the subdivision chain map, so refinement transfers orientation |
| dual complex | dual(frozen) → DualView — the face poset order-reversed, zero-copy: the dual k-cell is the primal (n−k)-cell and ∂^dual_k = δ_{n−k}. The combinatorial half of the DEC dual mesh |
| orientation | orient(c, k) — coherently orients the maximal cells of stratum k, so every interior facet is induced with opposite incidence numbers, or reports non-orientability. The orientation classes are returned: coherence leaves exactly one free sign per class, which only a geometric notion of outward can settle |
| Betti numbers (Z₂) | betti_numbers_z2(c) — β_k = N_k − rank ∂_k − rank ∂_{k+1} by reduction mod 2. These fix the dimension of the harmonic space, hence well-posedness of mixed formulations on multiply-connected domains |
| manifoldness | check_manifold(c) — the decidable necessary conditions, with an offending-cell marker: purity, facet coface counts, connectivity of st(σ) for dim σ ≤ n−2 |
| agglomeration | agglomerate(c, labels) — merges top cells into aggregates with boundary Σ ∂(members); interior facets cancel exactly when the stratum is coherently oriented, and an incoherent one is detected |
| replacement | replace(c, region, patch, vertex_glue) — excision and amalgamation fused: excise st(S), glue a patch along the frontier through a vertex correspondence lifted by boundary-chain matching. Imprints an interface through a cell without rebuilding the complex |
| coarsening | coarsen(c, labels[, protected]) — agglomeration by dimensional descent: top cells, then interface patches, then 1-chains; vertices only relabel. A patch merges only if coherently orientable, coefficient-clean, open and coface-consistent, else its members stay singletons. protected cells are declared barriers, since a metric-free engine cannot locate a corner; protecting the coarse lattice frame inverts tensor refinement exactly |
| relative homology | betti_numbers_z2(c, rel) — β_k(K, A; Z₂) with A = cl(marked), making the excision isomorphism H_k(K, cl st S) ≅ H_k(K ∖ st S, frontier S) computable on both sides |
| join A ∗ B | join(a, b) — ∂(α ∗ β) = ∂α ∗ β + (−1)^{dim α+1} α ∗ ∂β, both factors embedded. join(point, X) is the cone, join(S⁰, X) the suspension, and cl(st σ) ≅ σ ∗ lk(σ) becomes computable on both sides |
| neighbourhood markers | star_of, closure_of, link_of, frontier_of — st(S), cl(S), lk(S) = cl(st S) ∖ st(cl S), and the frontier cl(st S) ∖ st(S), which bounds the void a pushout patch glues to |
| elementary collapse | free_faces(c) — τ with one proper coface, occurring once in ∂σ — and collapse(c), which removes free pairs until none remains. Every step is a simple homotopy equivalence, so the homotopy type is preserved |
| frozen complex | freeze(c) → FrozenComplex — the immutable object queries sit on: ∂ and δ in device-capable storage (exec::Array, the CHAI seam), with host rows and kernel views |
| star / closure / link | FrozenComplex::star/closure/link(k, cell) — st(σ), cl(σ), lk(σ) = cl(st σ) ∖ st(cl σ), per cell |
| marker | Marker — a selection of cells, evaluated locally and meaningful collectively (mark, mark_where(k, pred)); the argument form of every operation on a subcomplex |
| closed subcomplex | subcomplex(c, marker) — cl(S) as its own complex, with both chain maps. Interface domains, material regions, ∂K, and the k-skeleton are all instances |
| facet classification | classify_facets(c) — partitions the facets by top-coface count: maximal (0, a detached interface — no proper coface, not free in the Whitehead sense, which is free_faces), boundary (1, ∂K), interior (2), nonmanifold (3+, a junction). With subcomplex it extracts ∂K |
A decision that requires geometry — that two vertices are the same point —
enters as an explicit Identification; everything downstream of it is
combinatorial.
- Metric-free. No coordinate, length, area or normal enters graphos. A complex is integer index spaces with signed CSR incidence.
- Out-of-place operations. Complex in, complex and chain map out; cochains are transported by gathering through the chain map.
- Flat arrays. The face poset is CSR strata, never a pointer graph — the precondition for device portability and zero-copy views.
- Portability seams. The RAJA-CHAI-Umpire triplet connects at three
points:
exec::forall/exec::*_scan(exec/forall.hpp) — kernel phases dispatch through RAJA policies (GRAPHOS_ENABLE_RAJA), with a serial fallback of identical semantics.star_deletionandsubcomplexare in full kernel form — mark → cascade → scan → scatter — and are the shape the remaining operations are being moved to.exec::Buffer<T>(exec/memory.hpp) — kernel scratch drawn from Umpire pools (GRAPHOS_ENABLE_UMPIRE), plain heap otherwise.exec::Array<T>(exec/array.hpp) — persistent storage for frozen complexes,chai::ManagedArray-backed underGRAPHOS_ENABLE_CHAI(host fallback otherwise).Complexis the mutable builder;freeze()produces the immutableFrozenComplexwhose ∂ and δ arrays andCsrViews a device kernel captures. Device policies (CUDA/HIP/SYCL) do not exist yet — see the Roadmap.
Read-only questions live in queries/, apart from the transformations in
ops/:
| Question | Where |
|---|---|
| Boundary ∂_k(σ) / coboundary δ_k(σ) rows | Complex::boundary, coboundary(), FrozenComplex::boundary_row/coboundary_row (core) |
| Star, closure, link, excision frontier (per cell and per set) | FrozenComplex::star/closure/link, queries/neighborhood.hpp |
| Generalized adjacency (facet-adjacent, node-adjacent, any via-stratum) | queries/adjacency.hpp |
| Facet co-degree: boundary / interior / nonmanifold / free | queries/facets.hpp (classify_facets) |
| Closedness ∂K = ∅ (and ∂∂K = ∅ by composition) | queries/facets.hpp (is_closed) |
| Purity, facet condition, link connectivity | queries/manifold.hpp (check_manifold) |
| Link sphere/ball property per vertex (exact for dim ≤ 3) | queries/manifold.hpp (classify_vertex_links) |
| χ, Betti numbers (absolute and relative, Z₂) | euler_characteristic (core), queries/homology.hpp |
| b₀ / component labels (with excluded connectors) | queries/components.hpp |
| Orientability of a stratum, and its orientation classes | ops/orient.hpp (orient().orientable, .class_of) — an operation, since it also constructs |
| Amalgamation: ∂σ ∩ ∂τ and its Z₂-acyclicity, the excess cl σ ∩ cl τ ∖ cl(∂σ ∩ ∂τ), and whether σ ∪ τ is a cell | queries/amalgamation.hpp |
Every public operation is specified as collective; the present serial
implementation is the P = 1 case. Distribution — global IDs, ParMETIS
partitioning, halo exchange — is intended to land inside freeze() and the
operations without an API change. None of it exists yet (see the Roadmap);
what follows is the contract it will honour.
Three contracts, in PETSc vocabulary:
| Contract | Meaning | Operations |
|---|---|---|
| Collective | All ranks call it, same order; result is globally consistent | from_edges/from_polygons/from_polyhedra/from_simplices, disjoint_union, product, join, cut_along, star_deletion, subcomplex, classify_facets, connected_components, barycentric_subdivision, orient, betti_numbers_z2, check_manifold, star_of/closure_of/link_of/frontier_of, free_faces, collapse, freeze, count, validate, d_squared_is_zero, euler_characteristic |
| Logically collective | All ranks participate; arguments are supplied per-rank for locally owned cells | quotient, pushout, lift_identifications (identifications), agglomerate, coarsen (labels, protections), replace (region, glue) |
| Local | Per-rank, no communication; correct within the ghost ring | star, closure, link, incidence, dual, row access, views |
Two disciplines follow:
- Same order on every rank. A distributed program must call the collective operations in one order — never branch a topology-changing call on rank-local data. This is the only way distribution appears in user code.
- Selection by marking, not by index list. Operations take a
Marker, marked locally;mark_where(k, predicate)is the canonical form. The predicate runs on each rank over its own cells, so the same program text is meaningful at any rank count.
freeze(c) takes a complex and nothing else. The ghost-ring depth that bounds
the Local queries is a property of the distributed layer, and arrives as an
argument when that layer does.
-DGRAPHOS_BUILD_PYTHON=ON builds graphos._core (pybind11, pinned) into
python/graphos/; run with PYTHONPATH=python. The whole calculus and query
surface carries the same names: a marker takes a Python predicate
(marker.mark_where(1, lambda e: ...)), identifications and glue are
(from, to[, sign]) tuples, and chain maps return as NumPy arrays.
Lifetime follows the builder/frozen split — accessors on Complex and on
operation results return copies, while FrozenComplex.boundary/coboundary
return zero-copy views whose base keeps the complex alive.
python/tests/test_graphos.py re-asserts the laws through the bindings as the
python.test_graphos ctest entry.
Read-only views duck-type the NetworkX protocol directly over the CSR
arrays: hasse_graph(frozen) — the face poset, nodes (dim, index) and arcs
σ → τ carrying [σ : τ] as an edge attribute — incidence_graph(c, k, j) for
I(k, j), and adjacency_graph(c, k, via[, exclude]) for adjacency through a
via-stratum. Each has .to_networkx() for guaranteed interop.
The views carry __networkx_backend__ = "graphos" and the package registers
the backend through the packaging entry point, with discovery metadata in the
source tree so PYTHONPATH=python works too. A plain
nx.connected_components(adjacency_graph(...)) then dispatches to the C++
engine — the sides-of-a-cut computation included, through the exclude
marker — and an algorithm with no native implementation falls back through
conversion when nx.config.fallback_to_nx is set.
python/tests/test_networkx.py covers protocol, dispatch and fallback.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8
ctest --test-dir build
cmake --install build --prefix ~/opt/graphosConsume the installed package with find_package(graphos CONFIG REQUIRED)
and link graphos::graphos. Full instructions — including the
RAJA/Umpire/CHAI stack via the bundled Spack environment
(spack.yaml), GPU variants, and troubleshooting — are in
INSTALL.md.
Requires CMake ≥ 3.20 and a C++20 compiler; the default build has no other
dependencies (header-only, graphos::graphos CMake target).
Unit tests mirror the include tree: one file per header, one ctest entry per
file (tests/core/test_complex.cpp ↔ include/graphos/core/complex.hpp,
registered as core.test_complex), on a zero-dependency harness in
tests/graphos_test.hpp with shared complexes in tests/fixtures.hpp. Run
one suite with ctest --test-dir build -R ops.test_cut.
Beyond the unit suites:
- Property-based fuzzing (
property.test_properties): random complexes through random operation sequences, asserting the laws after every step — the structural invariants, ∂∘∂ = 0, Euler–Poincaré, chain-map validity, and homotopy invariance under collapse. - Scaling assertions (
property.test_scaling): each operation's runtime against its complexity model on 8×-grown complexes, so a change of complexity class fails. - Performance guards (
property.test_performance): stored bytes/cell asserted against the CSR memory model (structural bloat fails deterministically), and bulk-op throughput asserted against the machine's own measured memcpy bandwidth — a constant-factor regression fails without hardcoding machine-specific numbers. Floors assert only in optimized, uninstrumented builds (sanitized/debug builds report only). - Multilevel hierarchies (
property.test_multilevel): three levels of quad (2D) and hex (3D) meshes built by tensor refinement (productof path complexes); the cross-level topology is asserted throughagglomerate— parent-label coarsening reproduces the next level's structure, every seam facet connects exactly the parents of its fine cofaces, and level-by-level coarsening equals one-step coarsening with identical chain maps. - Benchmarks (
bench/graphos_bench [n ...]): structured simplicial grids, per-operation wall time and throughput — the evidence behind any performance claim. - Sanitizers:
-DGRAPHOS_SANITIZE=address,undefinedapplies ASan/UBSan to everything in the tree. - CI (
.github/workflows/ci.yml): aformatcheck (clang-format --dry-run --Werror, version pinned), a dependency-freebaselinematrix (Linux gcc Release, Linux clang Debug + sanitizers, macOS Release), and four staged jobs that answer whether the installation is reliable —1. tplsbuilds camp/RAJA/Umpire/CHAI from source (scripts/build_tpls.sh, cached on the script's own hash),2. librarybuilds and installs against them,3. testsruns ctest and then compiles a fresh consumer against the install alone, and4. containerruns the same four stages in Dockerfile from a bare image. Each phase is a separate job, so a red run names its cause.
Portability options:
cmake -S . -B build -DGRAPHOS_ENABLE_RAJA=ON -DGRAPHOS_FETCH_TPL=ONGRAPHOS_FETCH_TPL downloads and builds the third-party libs via
FetchContent; without it they are located with find_package. CHAI is
reached through find_package only, so it comes from the Spack environment
or from scripts/build_tpls.sh.
Shipped since this list was last written: the Python bindings and the NetworkX 3.x backend (§ Python bindings), and the staged installation CI with its container (§ Build, test, install).
- Kernel form for the operation calculus. Two of the fifteen operations
go through the exec seam today —
star_deletionandsubcomplex, in the mark → cascade → scan → scatter shape. The other thirteen are serial host code:cut_along(side labelling as parallel label propagation) andquotientare the load-bearing ones, thenagglomerate,coarsen,orient,subdivisionand the rest. Also 3-dimensional cut coverage, branching interfaces included. - Device execution. No CUDA/HIP/SYCL policy exists yet;
exec::foralldispatches to RAJA host policies or to plain loops. Device policies over the frozen storage, and a CHAI-enabled CI build, follow (1) — there is little to move to a device until the operations are in kernel form. - Fixed-arity strided storage. A complex of a single cell type needs no
offset array, only a stride — the specialization
BoundaryOperatoranticipates. - Distributed layer. Global IDs, METIS/ParMETIS partitioning and MPI halo
exchange inside
freeze()and the operations. The contracts in § Collective semantics are written for it; today every one is the P = 1 case, and no MPI or ParMETIS call exists in the tree.