Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/steady-graphs-harden.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@statelyai/graph': minor
---

Add unweighted-distance and explicit connectivity APIs. Harden weighted arithmetic, low-link traversal, transform callbacks, and bulk deletion.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const neighbors = getNeighbors(graph, 'a'); // adjacent nodes
const roots = getSources(graph); // nodes with no incoming edges
```

Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once. `deleteEntities` accepts any iterable of IDs, collects it before mutation, and filters nodes/edges in one pass.

Every mutable CRUD operation has an immutable counterpart:

Expand Down Expand Up @@ -209,7 +209,7 @@ const parsed = GraphSchema.parse(unknownValue);

<!-- algorithm functions exported from src/algorithms.ts -->

Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, ordered shortest simple paths, simple paths, all-pairs shortest paths, A*, bidirectional Dijkstra), Eulerian paths/circuits, centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector, Katz), community detection (Louvain, label propagation, Girvan-Newman, greedy modularity, modularity scoring), flow & cuts (`getMaxFlow`, `getMinCut`), bipartite analysis (`isBipartite`, Hopcroft–Karp `getMaximumBipartiteMatching`), k-cores (`getCoreNumbers`, `getKCore`), graph coloring (`getGraphColoring`, `isValidColoring`), planarity testing (`isPlanar`), approximate TSP tours (`getTSPTour`) and Steiner trees (`getSteinerTree`), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, dominator trees, transitive reduction, isomorphism, topological sort, minimum spanning tree, and seeded graph generators (`createCompleteGraph`, `createGridGraph`, `createRandomGraph`, `createWattsStrogatzGraph`, `createBarabasiAlbertGraph`). Many algorithms have lazy generator variants (`gen*`) for early exit. See [docs/algorithms.md](./docs/algorithms.md) for the full reference.
Includes traversal (BFS, DFS, preorder/postorder), unweighted hop distances, pathfinding (shortest path, ordered shortest simple paths, simple paths, all-pairs shortest paths, A*, bidirectional Dijkstra), Eulerian paths/circuits, centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector, Katz), community detection (Louvain, label propagation, Girvan-Newman, greedy modularity, modularity scoring), flow & cuts (`getMaxFlow`, `getMinCut`), bipartite analysis (`isBipartite`, Hopcroft–Karp `getMaximumBipartiteMatching`), k-cores (`getCoreNumbers`, `getKCore`), graph coloring (`getGraphColoring`, `isValidColoring`), planarity testing (`isPlanar`), approximate TSP tours (`getTSPTour`) and Steiner trees (`getSteinerTree`), cycle detection, weak/strong connectivity and components, bridges, articulation points, biconnected components, dominator trees, transitive reduction, isomorphism, topological sort, minimum spanning tree, and seeded graph generators (`createCompleteGraph`, `createGridGraph`, `createRandomGraph`, `createWattsStrogatzGraph`, `createBarabasiAlbertGraph`). Many algorithms have lazy generator variants (`gen*`) for early exit. See [docs/algorithms.md](./docs/algorithms.md) for the full reference.

Hot algorithm loops (centrality, components) run on an internal compressed-sparse-row snapshot — cached and invalidated transparently like the rest of the index — so they stay fast on large graphs without changing the plain-JSON model. Algorithm results are differential-tested against graphology on seeded random graphs.

Expand All @@ -224,6 +224,8 @@ import {
getCycles,
getTopologicalSort,
getConnectedComponents,
getUnweightedDistances,
isStronglyConnected,
getMinimumSpanningTree,
getPageRank,
getLouvainCommunities,
Expand Down Expand Up @@ -267,6 +269,8 @@ getShortestPath(graph, {
}); // shortest path from any matching source
getTopologicalSort(graph); // topological order (or null)
getConnectedComponents(graph); // connected components
getUnweightedDistances(graph, 'a'); // reachable node IDs → hop counts
isStronglyConnected(graph); // every node reaches every other node
getMinimumSpanningTree(graph, { getWeight: (e) => e.weight ?? 1 }); // MST
getPageRank(graph); // link analysis scores
getLouvainCommunities(graph); // community detection (Louvain)
Expand Down Expand Up @@ -409,6 +413,7 @@ Beyond classic graph algorithms, the library also includes utilities for evolvin
- `getDiff()`, `getPatches()`, `getPatchedGraph()` (immutable), and `updateGraphWithPatches()` (mutable) for graph change tracking
- `genRandomWalk()`, `genWeightedRandomWalk()`, and coverage helpers for model-based testing and simulation
- `getSubgraph()`, `getFilteredGraph()`, `getMappedGraph()`, `getNeighborhood()`, `getReversedGraph()`, and `getLineGraph()` for structural transforms
- Mapping/filtering transforms capture node and edge collections before callbacks run, so callback-driven structural mutation cannot produce a torn result
- `getGraphUnion()`, `getGraphIntersection()`, `getGraphDifference()`, `getGraphSymmetricDifference()`, `getDisjointUnion()`, and `getGraphComplement()` for graph set operations

Binary set operations match nodes and edges by stable ID, require matching graph modes, and retain graph metadata from the left operand. Union and intersection use right-side entity data when IDs conflict. Disjoint union keeps left IDs and deterministically remaps right-side collisions.
Expand Down
17 changes: 10 additions & 7 deletions docs/algorithms.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ Active BFS, DFS, and postorder generators snapshot graph structure when iteratio
|---|---|---|---|
| `hasPath(graph, sourceId, targetId)` | Reachability | O(n + m) | BFS, mode-aware. `hasPath(g, a, a)` is `true`. |
| `isConnected(graph)` | Single weak component? | O(n + m) | Empty graph is connected. |
| `isWeaklyConnected(graph)` | Single weak component? | O(n + m) | Explicit alias for weak connectivity; ignores edge direction. |
| `isStronglyConnected(graph)` | Every node reaches every other node? | O(n + m) | Empty graph is strongly connected; non-directed edges are mutual. |
| `getUnweightedDistances(graph, sourceId, opts?)` | Reachable node IDs → minimum hop count | O(n + m) | `direction` is outgoing (default), incoming, or undirected. Unknown source returns an empty map. |
| `isTree(graph)` | Connected + acyclic + exactly `n − 1` edges | O(n + m) | Directed diamonds and parallel edges are not trees. Empty/single-node graphs are trees. |
| `getConnectedComponents(graph)` | Weakly-connected components | O(n + m) | Every edge connects regardless of mode/direction. CSR-backed. |
| `getStronglyConnectedComponents(graph)` | SCCs (Tarjan) | O(n + m) | Non-directed edges count as mutual reachability. Recursive — deep graphs may hit stack limits. |
| `getBridges(graph)` | Edges whose removal disconnects | O(n + m) | Treats the graph as undirected. Result sorted by id. |
| `getArticulationPoints(graph)` | Cut vertices | O(n + m) | Undirected semantics; sorted by id. |
| `getBiconnectedComponents(graph)` | Biconnected components (node arrays) | O(n + m) | Articulation points appear in multiple components. |
| `getBridges(graph)` | Edges whose removal disconnects | O(n + m) | Iterative, stack-safe low-link traversal over the undirected projection. Result sorted by id. |
| `getArticulationPoints(graph)` | Cut vertices | O(n + m) | Iterative undirected semantics; sorted by id. |
| `getBiconnectedComponents(graph)` | Biconnected components (node arrays) | O(n + m) | Handles parallel edges and self-loop singleton components; articulation points may repeat. |

## Cycles & DAG

Expand All @@ -57,7 +60,7 @@ Active BFS, DFS, and postorder generators snapshot graph structure when iteratio
| `genSimplePaths(graph, opts?)` / `getSimplePaths(...)` / `getSimplePath(...)` | All (or first) simple paths from `from` (optionally to `to`) | exponential (output-sensitive) | `from` accepts a node ID or predicate; predicates independently fan out from every matching node in graph order. DFS with backtracking; without `to`, every non-empty simple path is yielded. |
| `getJoinedPath(headPath, tailPath)` | Concatenated `GraphPath` | O(steps) | Throws unless head ends where tail starts. |

**Negative-weight contract.** Dijkstra, A*, and the bidirectional/early-exit searches may legitimately finish without ever scanning a negative edge — so they assert "no negative weights" **up front**: O(1) via the CSR's cached `firstNegativeEdge` flag for the default weight, or one O(m) sweep when a custom `getWeight` is supplied. They throw with a pointer to `{ algorithm: 'bellman-ford' }` (O(n·m), handles negative edges; negative *cycles* still throw).
**Numeric contract.** Every shortest-path weight and intermediate cost must remain finite; `NaN`, infinities, and arithmetic overflow throw. Dijkstra, A*, and bidirectional/early-exit searches also assert "no negative weights" **up front**: O(1) via cached CSR flags for default weights, or one O(m) sweep for custom `getWeight`. Bellman–Ford handles finite negative edges; negative cycles still throw.

## Path sets & coverage

Expand Down Expand Up @@ -93,7 +96,7 @@ whose output can itself be O(m²).

| Function | Computes | Complexity | Notes |
|---|---|---|---|
| `getMinimumSpanningTree(graph, opts?)` | New `Graph` containing the MST/forest edges | Prim (default) O(m log m); `algorithm: 'kruskal'` O(m log m) | Mode-aware: non-directed edges are candidates in both directions; directed edges only source→target (Prim). All nodes are kept; weight from `opts.getWeight ?? edge.weight ?? 1`. |
| `getMinimumSpanningTree(graph, opts?)` | New `Graph` containing the MST/forest edges | Prim (default) O(m log m); `algorithm: 'kruskal'` O(m log m) | Mode-aware: non-directed edges are candidates in both directions; directed edges only source→target (Prim). All nodes are kept; weights must be finite. |

## Centrality

Expand Down Expand Up @@ -126,7 +129,7 @@ All community algorithms treat the graph as **undirected** regardless of mode. O

| Function | Computes | Complexity | Notes |
|---|---|---|---|
| `getMaxFlow(graph, { from, to, getCapacity? })` | `{ value, flows, cutEdges }` | Edmonds–Karp O(n·m²) | Capacity defaults to `edge.weight ?? 1`; negative capacity throws. Directed edges carry flow source→target only; non-directed edges become two independent opposite arcs each with full capacity. `flows` is net flow per edge id (positive = source→target). Self-loops carry nothing. |
| `getMaxFlow(graph, { from, to, getCapacity? })` | `{ value, flows, cutEdges }` | Edmonds–Karp O(n·m²) | Capacity defaults to `edge.weight ?? 1`; non-finite/negative capacity and total-flow overflow throw. Directed edges carry flow source→target only; non-directed edges become two independent opposite arcs each with full capacity. `flows` is net flow per edge id (positive = source→target). Self-loops carry nothing. |
| `getMinCut(graph, { source, sink, getCapacity? })` | `{ value, cutEdges, partition }` | same solver | Max-flow-min-cut: `partition.source` = residual-reachable side (in `graph.nodes` order); `Σ cap(cutEdges) === value`. |

## Bipartite
Expand Down Expand Up @@ -179,7 +182,7 @@ Walk generators yield `GraphStep`s lazily and honor effective edge modes (non-di

## Performance notes

- **CSR snapshot.** Hot loops (BFS/DFS, components, shortest paths, centrality, cores, bipartite) run on a compressed-sparse-row snapshot of the graph's *traversable arcs* (`src/algorithms/csr.ts`): flat `Int32Array`s addressed by node position, so traversal pays no string hashing or Map lookups. Directed edges contribute one arc; non-directed edges contribute both. It also caches a `firstNegativeEdge` flag for O(1) negative-weight assertions.
- **CSR snapshot.** Hot loops (BFS/DFS, components, shortest paths, centrality, cores, bipartite) run on a compressed-sparse-row snapshot of the graph's *traversable arcs* (`src/algorithms/csr.ts`): flat `Int32Array`s addressed by node position, so traversal pays no string hashing or Map lookups. Directed edges contribute one arc; non-directed edges contribute both. It caches invalid/negative default-weight flags for O(1) assertions.
- **Auto-invalidation.** The CSR is cached per `GraphIndex` and revalidated against the index `version` and `graph.mode` — O(1) per access. Replacing `nodes`/`edges` arrays, length changes, and all `add*`/`delete*`/`update*` API mutations are detected automatically.
- **`invalidateIndex(graph)`** is only needed after *direct in-place field mutation* (e.g. `edge.sourceId = 'x'`, `node.parentId = 'y'`), which is not O(1)-detectable. The same staleness contract covers both the index and the CSR.
- Algorithm results are differential-tested against graphology on seeded random graphs; see [./benchmarks.md](./benchmarks.md) for throughput comparisons.
3 changes: 3 additions & 0 deletions src/algorithms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ export {
dfs,
isAcyclic,
getConnectedComponents,
getUnweightedDistances,
getTopologicalSort,
hasPath,
isConnected,
isWeaklyConnected,
isStronglyConnected,
isTree,
} from './algorithms/traversal';

Expand Down
Loading
Loading