rust-igraph
A pure-Rust port of igraph, the network-analysis library. Targets full API parity with igraph C v1.0.x (≈ 850 public functions), validated continuously against three official implementations:
- igraph C —
tests/unit/*.c+*.out - python-igraph —
tests/test_*.py - R-igraph (
rigraph) —tests/testthat/test-*.R
Status: 308 algorithm work units complete across Phases 1–7 and 9. 1,297 public APIs, 8,577 tests (unit + integration + doctest + proptest). WASM-compatible (
wasm32-unknown-unknown). See the master plan for the roadmap, and the algorithm tracker for per-algorithm progress.
What's implemented
| Category | Highlights |
|---|---|
| Traversal | BFS, DFS, shortest paths (Dijkstra, Bellman-Ford, Johnson, Floyd-Warshall, A*), widest paths |
| Centrality | PageRank, betweenness, closeness, harmonic, eigenvector, HITS, constraint |
| Community | Louvain, Leiden, Infomap, Spinglass, label propagation, fluid communities, fast greedy, edge betweenness, walktrap, leading eigenvector, Voronoi |
| Connectivity | Components, articulation points, bridges, biconnected components, cohesive blocks |
| Flow | Max flow (Dinic), min cut, Gomory-Hu tree, vertex/edge connectivity, all s-t cuts |
| Isomorphism | VF2 (graph + subgraph), BLISS canonical labeling, LAD subgraph isomorphism |
| Coloring | Greedy vertex coloring (CN + DSatur), chordality, bipartite matching |
| Constructors | 40+ graph generators (Erdős-Rényi, Barabási-Albert, Watts-Strogatz, SBM, lattices, famous graphs, ...) |
| I/O | 8 formats: GML, GraphML, DOT, Pajek, NCOL, LGL, DL, LEDA — all attribute-aware |
| Operators | Union, intersection, difference, complement, simplify, permute, line graph |
Why another Rust graph library?
petgraph is excellent for general-purpose graph work, but its API does
not express what igraph_t expresses (rich attributes, vertex/edge
selectors, full C API parity). Users coming from igraph's C, Python, or R
bindings need a Rust home where:
- function names mirror
igraph_* - numerical results match python-igraph within tight tolerance
- the build is WASM-friendly by default (no
unsafe, no system deps)
License
GPL-2.0-or-later, matching upstream igraph. The architecture decision record explains why.
How this site is built
mdBook builds the prose sections; cargo doc builds the API rustdoc; CI
publishes both to GitHub Pages. To build locally:
cargo install mdbook
mdbook serve --open
Quick start
Installation
Add rust-igraph to your Cargo.toml:
[dependencies]
rust-igraph = "0.6"
Your first graph
use rust_igraph::{Graph, bfs, pagerank, louvain}; fn main() -> Result<(), Box<dyn std::error::Error>> { // Build a small social network let g = Graph::from_edges( &[(0,1), (0,2), (1,2), (1,3), (2,3), (3,4), (4,5), (5,6), (6,4)], false, // undirected None, // auto-infer vertex count )?; println!("{g}"); // => Undirected graph with 7 vertices and 9 edges // BFS traversal from vertex 0 let order = bfs(&g, 0)?; println!("BFS order: {order:?}"); // PageRank centrality let pr = pagerank(&g)?; let (top_v, top_score) = pr.iter() .enumerate() .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) .unwrap(); println!("Most central vertex: {top_v} (PageRank = {top_score:.4})"); // Community detection let communities = louvain(&g)?; println!("Communities: {:?}", communities.membership); println!("Modularity: {:.4}", communities.modularity); Ok(()) }
Method-style API
The same operations are available as methods on Graph:
use rust_igraph::Graph; fn main() -> Result<(), Box<dyn std::error::Error>> { let g = Graph::from_edges( &[(0,1), (0,2), (1,2), (1,3), (2,3), (3,4), (4,5), (5,6), (6,4)], false, None, )?; // All of these work directly on the graph let pr = g.pagerank()?; let bc = g.betweenness()?; let communities = g.louvain()?; let connected = g.is_connected()?; let diameter = g.diameter()?; println!("Connected: {connected}"); println!("Diameter: {diameter:?}"); println!("Modularity: {:.4}", communities.modularity); Ok(()) }
Graph construction options
#![allow(unused)] fn main() { use rust_igraph::{Graph, GraphBuilder}; // From a flat edge list let g = Graph::from_edges(&[(0,1), (1,2), (2,0)], false, None).unwrap(); // Fluent builder pattern let g = GraphBuilder::undirected() .vertices(5) .edges(&[(0,1), (1,2), (2,3), (3,4), (4,0)]) .build() .unwrap(); // Classic generators let er = Graph::erdos_renyi(1000, 0.01, 42).unwrap(); // random let ba = Graph::barabasi_albert(1000, 3, 42).unwrap(); // scale-free let ws = Graph::watts_strogatz(1000, 6, 0.1, 42).unwrap(); // small-world }
Running the examples
The repository includes 116 runnable examples:
# Clone and run
git clone https://github.com/Totoro-jam/rust-igraph
cd rust-igraph
cargo run --example quickstart
cargo run --example social_network_demo
cargo run --example community_detection_demo
cargo run --example method_api_demo
cargo run --example layout_demo
cargo run --example file_io_demo
Where to read next
- Tutorial — walks through all major features with runnable code snippets.
- API documentation — full rustdoc reference for every function, struct, and enum.
- Examples directory — 116 self-contained programs covering every algorithm category.
Tutorial
This chapter walks through the core features of rust-igraph by building
a small network analysis pipeline. Every code block is a self-contained
snippet you can paste into a Rust file with use rust_igraph::prelude::*;.
Creating graphs
The simplest way to create a graph is from an edge list:
#![allow(unused)] fn main() { use rust_igraph::Graph; // Undirected triangle let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false, None).unwrap(); assert_eq!(g.vcount(), 3); assert_eq!(g.ecount(), 3); }
For more control, use the GraphBuilder:
#![allow(unused)] fn main() { use rust_igraph::GraphBuilder; let g = GraphBuilder::undirected() .vertices(5) .edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]) .build() .unwrap(); }
There are also 40+ named constructors for common graph families:
#![allow(unused)] fn main() { use rust_igraph::{full_graph, ring_graph, star_graph, StarMode, erdos_renyi_gnp}; let complete = full_graph(5, false, false).unwrap(); // K_5 let cycle = ring_graph(10, false, false, false).unwrap(); // C_10 let hub = star_graph(8, StarMode::Undirected, 0).unwrap(); // star with center 0 let random = erdos_renyi_gnp(100, 0.05, false, false, 42).unwrap(); // G(100, 0.05) }
Basic properties
#![allow(unused)] fn main() { use rust_igraph::{Graph, density, is_connected, ConnectednessMode, diameter}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(3,0),(2,4),(4,5)], false, None ).unwrap(); println!("Vertices: {}", g.vcount()); // 6 println!("Edges: {}", g.ecount()); // 6 println!("Directed: {}", g.is_directed()); // false println!("Density: {:.4}", density(&g).unwrap().unwrap_or(0.0)); println!("Connected: {}", is_connected(&g, ConnectednessMode::Weak).unwrap()); println!("Diameter: {:?}", diameter(&g).unwrap()); }
Centrality measures
#![allow(unused)] fn main() { use rust_igraph::{Graph, pagerank, betweenness, closeness}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(1,3),(2,3),(3,4),(4,5),(5,6),(6,4)], false, None ).unwrap(); let pr = pagerank(&g).unwrap(); let bc = betweenness(&g).unwrap(); let cl = closeness(&g).unwrap(); // Find the most central vertex let (top_v, top_score) = pr.iter() .enumerate() .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) .unwrap(); println!("Highest PageRank: vertex {} ({:.4})", top_v, top_score); }
Community detection
#![allow(unused)] fn main() { use rust_igraph::{Graph, louvain, leiden}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(3,4),(3,5),(4,5),(2,3)], false, None ).unwrap(); let result = louvain(&g).unwrap(); println!("Communities: {:?}", result.membership); println!("Modularity: {:.4}", result.modularity); }
Available community detection algorithms: Louvain, Leiden, label propagation, fluid communities, fast greedy, edge betweenness, walktrap, and leading eigenvector.
Shortest paths
#![allow(unused)] fn main() { use rust_igraph::{Graph, distances, dijkstra_distances}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(0,3),(1,3)], false, None ).unwrap(); // Unweighted distances from vertex 0 let dist = distances(&g, 0).unwrap(); println!("Distances from 0: {:?}", dist); // [Some(0), Some(1), Some(2), Some(1)] // Weighted shortest paths let weights = vec![1.0, 2.0, 1.0, 5.0, 1.0]; let wdist = dijkstra_distances(&g, 0, &weights).unwrap(); println!("Weighted distances from 0: {:?}", wdist); }
Graph attributes
Vertices, edges, and the graph itself can carry typed attributes:
#![allow(unused)] fn main() { use rust_igraph::{Graph, AttributeValue}; let mut g = Graph::from_edges(&[(0,1),(1,2)], false, None).unwrap(); // Vertex attributes g.set_vertex_attribute("name", 0, AttributeValue::String("Alice".into())).unwrap(); g.set_vertex_attribute("name", 1, AttributeValue::String("Bob".into())).unwrap(); g.set_vertex_attribute("name", 2, AttributeValue::String("Carol".into())).unwrap(); // Edge attributes g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5)).unwrap(); g.set_edge_attribute("weight", 1, AttributeValue::Numeric(2.3)).unwrap(); // Graph-level attributes g.set_graph_attribute("title", AttributeValue::String("My Network".into())); // Read back if let Some(name) = g.vertex_attribute("name", 0) { println!("Vertex 0: {}", name); // "Alice" } }
File I/O
The easiest way to read and write graphs is with from_file / to_file,
which auto-detect the format from the file extension:
#![allow(unused)] fn main() { use rust_igraph::Graph; // Auto-detects GML from the extension let g = Graph::from_file("network.gml").unwrap(); println!("{g}"); // Write as GraphML — extension determines format g.to_file("network.graphml").unwrap(); }
Supported extensions: .gml, .graphml/.xml, .dot/.gv, .net/.pajek,
.ncol, .lgl, .leda/.lgr, .dl, .edges/.edgelist/.txt/.csv.
For more control (e.g. writing to a buffer), use the stream-based functions:
#![allow(unused)] fn main() { use rust_igraph::{Graph, AttributeValue, write_gml, read_gml}; let mut g = Graph::from_edges(&[(0,1),(1,2),(2,0)], false, None).unwrap(); g.set_graph_attribute("name", AttributeValue::String("triangle".into())); // Write to an in-memory buffer let mut buf = Vec::new(); write_gml(&g, &mut buf).unwrap(); // Read back — attributes survive the round-trip let g2 = read_gml(buf.as_slice()).unwrap(); assert_eq!(g2.vcount(), 3); assert_eq!( g2.graph_attribute("name").and_then(AttributeValue::as_str), Some("triangle") ); }
Supported formats: GML, GraphML, DOT (Graphviz), Pajek, NCOL, LGL, DL (UCINET), and LEDA.
Graph isomorphism
#![allow(unused)] fn main() { use rust_igraph::{full_graph, ring_graph, isomorphic}; let g1 = full_graph(4, false, false).unwrap(); let g2 = full_graph(4, false, false).unwrap(); let g3 = ring_graph(4, false, false, false).unwrap(); assert!(isomorphic(&g1, &g2).unwrap()); // K_4 ≅ K_4 assert!(!isomorphic(&g1, &g3).unwrap()); // K_4 ≇ C_4 }
BLISS canonical labeling
The BLISS engine provides canonical forms, automorphism counting, and vertex-color-aware isomorphism:
#![allow(unused)] fn main() { use rust_igraph::{Graph, canonical_permutation, count_automorphisms, isomorphic_bliss, permute_vertices}; let g1 = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, None).unwrap(); let g2 = Graph::from_edges(&[(2,0),(0,3),(3,1),(1,2)], false, None).unwrap(); // Canonical permutation — same for isomorphic graphs after relabeling let p1 = canonical_permutation(&g1, None).unwrap(); let p2 = canonical_permutation(&g2, None).unwrap(); let c1 = permute_vertices(&g1, &p1).unwrap(); let c2 = permute_vertices(&g2, &p2).unwrap(); assert_eq!(c1.ecount(), c2.ecount()); // Automorphism group order: |Aut(C_4)| = 8 let aut = count_automorphisms(&g1, None).unwrap(); assert!((aut - 8.0).abs() < 1e-9); // BLISS-backed isomorphism with mapping let result = isomorphic_bliss(&g1, &g2, None, None).unwrap(); assert!(result.iso); }
Graph operators
#![allow(unused)] fn main() { use rust_igraph::Graph; let a = Graph::from_edges(&[(0,1),(1,2)], false, None).unwrap(); let b = Graph::from_edges(&[(1,2),(2,3)], false, None).unwrap(); let u = &a | &b; // union let i = &a & &b; // intersection println!("Union: {} vertices, {} edges", u.vcount(), u.ecount()); println!("Intersection: {} vertices, {} edges", i.vcount(), i.ecount()); }
Graph layouts
rust-igraph includes 16 layout engines for 2D and 3D graph visualization:
#![allow(unused)] fn main() { use rust_igraph::{Graph, FrParams, KkParams, layout_fruchterman_reingold, layout_circle, layout_kamada_kawai}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(3,0),(0,2),(1,3)], false, None ).unwrap(); // Force-directed layout (Fruchterman-Reingold) let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap(); for (i, &(x, y)) in coords.iter().enumerate() { println!("v{i}: ({x:.2}, {y:.2})"); } // Circle layout (deterministic) let circle = layout_circle(&g, None); assert_eq!(circle.len(), g.vcount() as usize); // Kamada-Kawai (energy-based) let n = g.vcount() as usize; let kk = layout_kamada_kawai(&g, None, &KkParams::default_for(n), None).unwrap(); assert_eq!(kk.len(), n); }
Available engines: Fruchterman-Reingold, Kamada-Kawai, DrL, Sugiyama, GEM, Davidson-Harel, GraphOpt, MDS, LGL, UMAP, Reingold-Tilford, circle, star, grid, random, and sphere.
Iterating over a graph
#![allow(unused)] fn main() { use rust_igraph::Graph; let g = Graph::from_edges(&[(0,1),(1,2),(2,0)], false, None).unwrap(); // Iterate over edges for (src, tgt) in &g { println!("{} -- {}", src, tgt); } // Iterate over vertex ids for v in g.vertex_ids() { let deg = g.degree(v).unwrap(); println!("Vertex {}: degree {}", v, deg); } }
Method API vs free functions
Most algorithms are available both as free functions and as methods on
Graph:
#![allow(unused)] fn main() { use rust_igraph::{Graph, pagerank}; let g = Graph::from_edges(&[(0,1),(1,2),(2,0)], false, None).unwrap(); // Free function style let pr1 = pagerank(&g).unwrap(); // Method style let pr2 = g.pagerank().unwrap(); // Same result assert_eq!(pr1, pr2); }
Next steps
- Browse the API documentation for the full list of functions
- Run
cargo run --example social_network_demofor a comprehensive demo - Check the 116 examples in the
examples/directory
Using rust-igraph in the Browser (WASM)
rust-igraph compiles to WebAssembly, letting you run graph algorithms entirely client-side — no server needed. This chapter walks through the full path from building the WASM module to integrating it into your web application.
Prerequisites
You need:
- Rust (1.85+) with the
wasm32-unknown-unknowntarget - wasm-pack (the build tool that wraps
wasm-bindgen) - Node.js (18+) for your frontend toolchain
# Install the WASM target
rustup target add wasm32-unknown-unknown
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# or via cargo:
cargo install wasm-pack
Building the WASM module
The igraph-wasm crate in crates/igraph-wasm/ provides the browser
bindings. Build it with wasm-pack:
# Clone the repo (if you haven't already)
git clone https://github.com/Totoro-jam/rust-igraph
cd rust-igraph
# Build for browser (ES module output)
wasm-pack build crates/igraph-wasm \
--target web \
--out-dir ../../pkg \
--out-name igraph_wasm
This produces a pkg/ directory containing:
| File | Purpose |
|---|---|
igraph_wasm.js | JavaScript glue (ES module) |
igraph_wasm_bg.wasm | Compiled WebAssembly binary |
igraph_wasm.d.ts | TypeScript type definitions |
package.json | npm package metadata |
Build targets
wasm-pack supports different output targets:
# For ES module usage (Vite, webpack, browsers with <script type="module">)
wasm-pack build crates/igraph-wasm --target web
# For bundlers (webpack, Rollup — uses import())
wasm-pack build crates/igraph-wasm --target bundler
# For Node.js (CommonJS)
wasm-pack build crates/igraph-wasm --target nodejs
For most web projects, --target web is the right choice.
Quick start — vanilla HTML
The simplest way to use the WASM module is directly in a <script> tag:
<!DOCTYPE html>
<html>
<head>
<title>rust-igraph WASM Demo</title>
</head>
<body>
<pre id="output"></pre>
<script type="module">
// Import and initialize the WASM module
import init, { WasmGraph } from './pkg/igraph_wasm.js';
async function main() {
// Must call init() before using any WASM functions
await init();
// Create a graph from edge pairs: [src0, dst0, src1, dst1, ...]
const edges = new Uint32Array([0,1, 1,2, 2,3, 3,0, 0,2, 1,3]);
const graph = WasmGraph.fromEdges(edges, false); // undirected
console.log(`Vertices: ${graph.vcount()}, Edges: ${graph.ecount()}`);
// Run PageRank
const prJson = graph.pagerank();
const pr = JSON.parse(prJson);
document.getElementById('output').textContent =
`PageRank scores: ${pr.scores.map(s => s.toFixed(4)).join(', ')}`;
// Run community detection
const commJson = graph.louvain();
const comm = JSON.parse(commJson);
console.log('Communities:', comm.membership);
console.log('Modularity:', comm.modularity.toFixed(4));
// Always free the graph when done
graph.free();
}
main();
</script>
</body>
</html>
Serve this with any static file server (e.g. python3 -m http.server)
and open it in your browser.
Integration with Vite + React
For a production-quality setup with Vite and React:
1. Build the WASM module into your project
wasm-pack build crates/igraph-wasm \
--target web \
--out-dir ../../my-app/public/wasm \
--out-name igraph_wasm
Placing the output in public/wasm/ ensures Vite copies the files as-is
without hashing (WASM files need stable paths for dynamic import).
2. Create a Web Worker
Running graph algorithms in a Web Worker keeps the UI responsive. Create
src/worker.ts:
// TypeScript interface matching the WASM exports
interface WasmGraphInstance {
bfs(root: number): string;
dfs(root: number): string;
pagerank(): string;
louvain(): string;
betweenness(): string;
closeness(): string;
connectedComponents(): string;
layoutFr(niter: number): string;
layoutKamadaKawai(): string;
layoutCircle(): string;
layoutRandom(seed: number): string;
layoutGrid(width: number): string;
layoutStar(center: number): string;
coreness(): string;
eccentricity(): string;
density(): string;
radius(): string;
meanDistance(): string;
meanDegree(): string;
assortativityDegree(): string;
constraint(): string;
reciprocity(): string;
vcount(): number;
ecount(): number;
free(): void;
}
let WasmGraph: {
fromEdges(edges: Uint32Array, directed: boolean): WasmGraphInstance;
} | null = null;
async function initWasm(): Promise<boolean> {
try {
const workerUrl = self.location.href;
const root = workerUrl.replace(/\/[^/]*$/, '');
const wasmModule = await import(
/* @vite-ignore */ `${root}/wasm/igraph_wasm.js`
);
await wasmModule.default();
WasmGraph = wasmModule.WasmGraph;
return true;
} catch (e) {
console.error('WASM init failed:', e);
return false;
}
}
// Message handler
self.onmessage = async (e: MessageEvent) => {
const { type, ...params } = e.data;
if (type === 'init') {
const ok = await initWasm();
self.postMessage({ type: 'ready', ok });
return;
}
if (type === 'run' && WasmGraph) {
const edges = new Uint32Array(params.edges);
const graph = WasmGraph.fromEdges(edges, params.directed);
try {
const t0 = performance.now();
const resultJson = graph.pagerank(); // or any algorithm
const elapsed = performance.now() - t0;
self.postMessage({
type: 'result',
data: JSON.parse(resultJson),
elapsed,
});
} finally {
graph.free();
}
}
};
3. Use the worker from React
import { useEffect, useRef, useState } from 'react';
function App() {
const workerRef = useRef<Worker | null>(null);
const [ready, setReady] = useState(false);
const [result, setResult] = useState<any>(null);
useEffect(() => {
const worker = new Worker(
new URL('./worker.ts', import.meta.url),
{ type: 'module' }
);
worker.onmessage = (e) => {
if (e.data.type === 'ready') setReady(e.data.ok);
if (e.data.type === 'result') setResult(e.data.data);
};
worker.postMessage({ type: 'init' });
workerRef.current = worker;
return () => worker.terminate();
}, []);
const runPageRank = () => {
workerRef.current?.postMessage({
type: 'run',
edges: [0,1, 1,2, 2,3, 3,0, 0,2],
directed: false,
});
};
return (
<div>
<button onClick={runPageRank} disabled={!ready}>
Run PageRank
</button>
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</div>
);
}
Integration with Node.js
For server-side or CLI usage with Node.js:
wasm-pack build crates/igraph-wasm --target nodejs --out-dir ../../pkg
const { WasmGraph } = require('./pkg/igraph_wasm.js');
const edges = new Uint32Array([0,1, 1,2, 2,0, 2,3, 3,4]);
const graph = WasmGraph.fromEdges(edges, false);
const pr = JSON.parse(graph.pagerank());
console.log('PageRank:', pr.scores);
graph.free();
WasmGraph API reference
All algorithm methods return JSON strings. Parse them with
JSON.parse() to get the result objects described below.
Construction
| Method | Description |
|---|---|
WasmGraph.fromEdges(edges: Uint32Array, directed: boolean) | Create from flat edge pairs [u0,v0, u1,v1, ...] |
new WasmGraph(directed: boolean) | Create an empty graph |
graph.addEdge(u: number, v: number) | Add a single edge |
Graph generators (static constructors)
| Method | Description |
|---|---|
WasmGraph.erdosRenyi(n, p, seed) | Erdos-Renyi G(n,p) random graph |
WasmGraph.fullGraph(n) | Complete graph K_n |
WasmGraph.cycleGraph(n) | Cycle graph C_n |
WasmGraph.ringGraph(n, circular) | Ring (cycle if circular=true, path if false) |
WasmGraph.barabasiAlbert(n, m, seed) | Barabasi-Albert preferential attachment |
WasmGraph.wattsStrogatz(n, k, p, seed) | Watts-Strogatz small-world |
Properties
| Method | Return type | Description |
|---|---|---|
graph.vcount() | number | Number of vertices |
graph.ecount() | number | Number of edges |
Centrality algorithms
| Method | Result fields | Description |
|---|---|---|
graph.pagerank() | { scores: number[] } | PageRank centrality |
graph.betweenness() | { scores: number[] } | Betweenness centrality |
graph.closeness() | { scores: number[] } | Closeness centrality |
graph.eigenvectorCentrality() | { scores: number[] } | Eigenvector centrality |
graph.harmonicCentrality() | { scores: number[] } | Harmonic centrality |
graph.katzCentrality() | { scores: number[] } | Katz centrality |
graph.hubAndAuthorityScores() | { hub: number[], authority: number[] } | HITS algorithm |
graph.edgeBetweenness() | { scores: number[] } | Edge betweenness centrality |
Community detection
| Method | Result fields | Description |
|---|---|---|
graph.louvain() | { membership: number[], modularity: number } | Louvain modularity |
graph.leiden() | { membership: number[], quality: number, nb_clusters: number } | Leiden algorithm |
graph.infomap() | { membership: number[], codelength: number } | Infomap |
graph.spinglass() | { membership: number[], modularity: number, nb_clusters: number } | Spinglass |
graph.labelPropagation() | { membership: number[], nb_clusters: number } | Label propagation |
graph.walktrap() | { membership: number[], nb_clusters: number, modularity: number } | Walktrap |
graph.fastGreedy() | { membership: number[], nb_clusters: number, modularity: number } | Fast greedy |
graph.leadingEigenvector() | { membership: number[], modularity: number } | Leading eigenvector |
graph.edgeBetweennessCommunity() | { membership: number[], nb_clusters: number } | Edge betweenness community |
graph.fluidCommunities(k: number) | { membership: number[], nb_clusters: number } | Fluid communities |
Traversal & paths
| Method | Result fields | Description |
|---|---|---|
graph.bfs(root: number) | { order: number[] } | Breadth-first search |
graph.dfs(root: number) | { order: number[] } | Depth-first search |
graph.dijkstra(source: number, weights: Float64Array) | { distances: number[] } | Dijkstra shortest paths |
graph.shortestPath(source, target) | { path: number[] } | Shortest path between two vertices |
graph.randomWalk(start, steps, seed) | { vertices: number[] } | Random walk from a start vertex |
graph.topologicalSort() | { order: number[] } | Topological sort (directed graphs) |
graph.maxFlow(source: number, target: number) | { value: number } | Maximum flow |
graph.diameter() | { diameter: number | null } | Graph diameter (longest shortest path) |
Structure
| Method | Result fields | Description |
|---|---|---|
graph.connectedComponents() | { membership: number[], count: number } | Weakly connected components |
graph.stronglyConnectedComponents() | { membership: number[], count: number } | Strongly connected components |
graph.graphStats() | { vcount, ecount, is_directed, is_connected, diameter, girth, triangles, is_bipartite } | Aggregate statistics |
graph.articulationPoints() | { vertices: number[] } | Cut vertices |
graph.bridges() | { edges: [number,number][], count: number } | Bridge edges |
graph.degreeSequence() | { degrees: number[] } | Degree of each vertex |
graph.vertexColoring() | { colors: number[], chromatic: number } | Greedy vertex coloring |
graph.transitivity() | { value: number } | Global clustering coefficient |
graph.triadCensus() | { counts: number[] } | 16-type triad census (directed) |
Isomorphism
| Method | Result fields | Description |
|---|---|---|
graph.canonicalPermutation() | { permutation: number[] } | BLISS canonical labeling |
graph.countAutomorphisms() | { count: number } | Automorphism group order |
graph.isomorphicBliss(other) | { isomorphic: boolean, mapping: number[] } | BLISS isomorphism test |
Graph Metrics
| Method | Result fields | Description |
|---|---|---|
graph.coreness() | { cores: number[] } | k-core decomposition |
graph.eccentricity() | { values: number[] } | Per-vertex eccentricity |
graph.density() | { density: number | null } | Edge density |
graph.radius() | { radius: number | null } | Graph radius (min eccentricity) |
graph.meanDistance() | { mean_distance: number | null } | Average shortest-path length |
graph.meanDegree() | { mean_degree: number | null } | Average degree |
graph.assortativityDegree() | { assortativity: number | null } | Degree assortativity coefficient |
graph.constraint() | { scores: number[] } | Burt's constraint (structural holes) |
graph.reciprocity() | { reciprocity: number | null } | Edge reciprocity (directed) |
Layout
| Method | Result fields | Description |
|---|---|---|
graph.layoutFr(niter: number) | { coords: [number,number][] } | Fruchterman-Reingold force-directed layout |
graph.layoutKamadaKawai() | { coords: [number,number][] } | Kamada-Kawai spring layout |
graph.layoutCircle() | { coords: [number,number][] } | Circular layout |
graph.layoutRandom(seed: number) | { coords: [number,number][] } | Random layout |
graph.layoutGrid(width: number) | { coords: [number,number][] } | Grid layout (0 = auto width) |
graph.layoutStar(center: number) | { coords: [number,number][] } | Star layout with center vertex |
Memory management
Always call graph.free() when you're done with a graph instance.
WASM memory is not garbage-collected by JavaScript — if you don't call
free(), the graph's memory will leak until the page is closed or the
worker is terminated.
const graph = WasmGraph.fromEdges(edges, false);
try {
const result = JSON.parse(graph.pagerank());
// use result...
} finally {
graph.free(); // always free!
}
Performance tips
-
Use Web Workers — graph algorithms can be CPU-intensive. Running them on the main thread blocks the UI. Always offload to a worker.
-
Reuse graph instances — if you run multiple algorithms on the same graph, create the
WasmGraphonce and call multiple methods before freeing.const graph = WasmGraph.fromEdges(edges, false); const pr = JSON.parse(graph.pagerank()); const bc = JSON.parse(graph.betweenness()); const layout = JSON.parse(graph.layoutFr(300)); graph.free(); -
Use typed arrays — edges must be passed as
Uint32Array, not regular arrays. Dijkstra weights useFloat64Array. Typed arrays avoid serialization overhead across the JS/WASM boundary. -
Bundle size — the WASM binary is ~400 KB gzipped. Use
wasm-opt -O3(included inwasm-pack build --release) for production builds.
Troubleshooting
"WASM module not found" / 404 errors
The WASM files must be served with the correct MIME type
(application/wasm). Most development servers handle this automatically.
If you're using Vite, place the WASM output in public/ so it's served
as-is:
wasm-pack build crates/igraph-wasm \
--target web \
--out-dir ../../my-app/public/wasm
"Cannot use import statement outside a module"
The --target web output is an ES module. Use <script type="module">
or configure your bundler to handle .js imports correctly.
CORS errors when loading WASM
If loading from file://, CORS policies block WASM. Use a local server:
python3 -m http.server 8080
# or
npx serve .
Memory issues with large graphs
WASM has a default memory limit. For graphs with millions of edges,
you may need to increase it in .cargo/config.toml:
[target.wasm32-unknown-unknown]
rustflags = ["-C", "link-args=-z stack-size=8388608"]
Algorithms that require undirected graphs
Some algorithms (spinglass, fast greedy, fluid communities) require
undirected graphs. If you pass a directed graph, the WASM call will
throw a JavaScript Error with a descriptive message.
Live demo
Try the interactive playground
to see all 64 algorithms running in the browser via WASM. The
playground source code at website/playground/ serves as a full
reference implementation.
Next steps
- Browse the API documentation for the full Rust API
- Check the playground source for a production-quality React + WASM integration
- Run
cargo check --target wasm32-unknown-unknownto verify your own code is WASM-compatible
Cookbook
Practical patterns for common graph analysis tasks. Each recipe is
self-contained — paste it into a file and run with
cargo run --example <name>, or use the snippets directly in your project.
Find the most important nodes
Combine multiple centrality measures to identify key vertices:
#![allow(unused)] fn main() { use rust_igraph::{Graph, pagerank, betweenness, harmonic_centrality}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(1,3),(2,3),(3,4),(4,5),(5,6),(6,4),(6,7),(7,8),(8,9),(9,7)], false, None ).unwrap(); let pr = pagerank(&g).unwrap(); let bc = betweenness(&g).unwrap(); let hc = harmonic_centrality(&g).unwrap(); // Rank vertices by PageRank let mut ranked: Vec<(usize, f64)> = pr.iter().copied().enumerate().collect(); ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); println!("Top vertices by PageRank:"); for &(v, score) in ranked.iter().take(3) { println!(" v{v}: PR={score:.4} betweenness={:.2} harmonic={:.4}", bc[v], hc[v]); } }
Compare community detection algorithms
Run multiple algorithms and measure agreement:
#![allow(unused)] fn main() { use rust_igraph::{Graph, louvain, leiden, label_propagation, compare_communities, CommunityComparison}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(3,4),(3,5),(4,5),(2,3),(6,7),(6,8),(7,8),(5,6)], false, None ).unwrap(); let c_louvain = louvain(&g).unwrap(); let c_leiden = leiden(&g).unwrap(); let c_lpa = label_propagation(&g).unwrap(); println!("Louvain: {} communities, modularity {:.4}", *c_louvain.membership.iter().max().unwrap() + 1, c_louvain.modularity); println!("Leiden: {} communities, modularity {:.4}", *c_leiden.membership.iter().max().unwrap() + 1, c_leiden.modularity); // Compare partitions using Normalized Mutual Information let nmi = compare_communities( &c_louvain.membership, &c_leiden.membership, CommunityComparison::NormalizedMutualInformation, ).unwrap(); println!("NMI(Louvain, Leiden) = {nmi:.4}"); let ari = compare_communities( &c_louvain.membership, &c_lpa.membership, CommunityComparison::AdjustedRand, ).unwrap(); println!("ARI(Louvain, LPA) = {ari:.4}"); }
Build a weighted graph and find shortest paths
#![allow(unused)] fn main() { use rust_igraph::{Graph, dijkstra_distances, dijkstra_paths}; let g = Graph::from_edges( &[(0,1),(0,2),(1,3),(2,3),(3,4),(2,4)], false, None ).unwrap(); let weights = vec![1.0, 4.0, 2.0, 1.0, 3.0, 2.0]; // Distance vector from source 0 let dist = dijkstra_distances(&g, 0, &weights).unwrap(); for (v, d) in dist.iter().enumerate() { match d { Some(d) => println!(" 0 -> {v}: distance {d:.1}"), None => println!(" 0 -> {v}: unreachable"), } } // Full path reconstruction let paths = dijkstra_paths(&g, 0, &weights).unwrap(); if let Some(parent) = paths.parents[4] { println!("Vertex 4 reached via vertex {parent}"); } }
Analyze graph connectivity
#![allow(unused)] fn main() { use rust_igraph::{Graph, connected_components, articulation_points, bridges}; let g = Graph::from_edges( &[(0,1),(1,2),(2,0),(2,3),(3,4),(4,5),(5,3)], false, None ).unwrap(); let cc = connected_components(&g).unwrap(); println!("Components: {}", cc.count); let ap = articulation_points(&g).unwrap(); println!("Articulation points: {:?}", ap); let br = bridges(&g).unwrap(); println!("Bridges: {:?}", br); }
Generate and analyze random graphs
#![allow(unused)] fn main() { use rust_igraph::{Graph, density, connected_components, erdos_renyi_gnp, barabasi_game_bag, watts_strogatz_game}; // Erdos-Renyi: G(n, p) let er = erdos_renyi_gnp(100, 0.05, false, false, 42).unwrap(); let er_cc = connected_components(&er).unwrap(); println!("ER(100, 0.05): {} edges, {} components", er.ecount(), er_cc.count); // Barabasi-Albert: preferential attachment let ba = barabasi_game_bag(100, 3, false, false, 42).unwrap(); println!("BA(100, m=3): {} edges, density {:.4}", ba.ecount(), density(&ba).unwrap().unwrap_or(0.0)); // Watts-Strogatz: small-world let ws = watts_strogatz_game(100, 4, 0.1, false, false, 42).unwrap(); println!("WS(100, k=4, p=0.1): {} edges", ws.ecount()); }
You can also use the convenience methods on Graph:
#![allow(unused)] fn main() { use rust_igraph::Graph; let er = Graph::erdos_renyi(100, 0.05, 42).unwrap(); let ba = Graph::barabasi_albert(100, 3, 42).unwrap(); let ws = Graph::watts_strogatz(100, 4, 0.1, 42).unwrap(); }
Round-trip graph with attributes through GML
#![allow(unused)] fn main() { use rust_igraph::{Graph, AttributeValue, write_gml, read_gml}; let mut g = Graph::from_edges(&[(0,1),(1,2),(2,0)], false, None).unwrap(); // Add vertex names for (i, name) in ["Alice", "Bob", "Carol"].iter().enumerate() { g.set_vertex_attribute("name", i as u32, AttributeValue::String((*name).into())).unwrap(); } // Add edge weights g.set_edge_attribute("weight", 0, AttributeValue::Numeric(1.5)).unwrap(); g.set_edge_attribute("weight", 1, AttributeValue::Numeric(2.0)).unwrap(); g.set_edge_attribute("weight", 2, AttributeValue::Numeric(0.8)).unwrap(); // Serialize and deserialize let mut buf = Vec::new(); write_gml(&g, &mut buf).unwrap(); let g2 = read_gml(buf.as_slice()).unwrap(); assert_eq!(g2.vcount(), 3); assert_eq!( g2.vertex_attribute("name", 0).and_then(AttributeValue::as_str), Some("Alice") ); }
Check graph isomorphism with BLISS
#![allow(unused)] fn main() { use rust_igraph::{Graph, isomorphic_bliss, canonical_permutation, count_automorphisms, permute_vertices}; let g1 = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, None).unwrap(); // Same graph with vertices relabeled let g2 = Graph::from_edges(&[(2,0),(0,3),(3,1),(1,2)], false, None).unwrap(); let result = isomorphic_bliss(&g1, &g2, None, None).unwrap(); assert!(result.iso); println!("Isomorphic: {}", result.iso); if !result.map12.is_empty() { println!("Mapping g1->g2: {:?}", result.map12); } // Canonical form: two isomorphic graphs get the same canonical labeling let perm1 = canonical_permutation(&g1, None).unwrap(); let perm2 = canonical_permutation(&g2, None).unwrap(); let c1 = permute_vertices(&g1, &perm1).unwrap(); let c2 = permute_vertices(&g2, &perm2).unwrap(); // c1 and c2 have the same edge set // Count automorphisms let aut = count_automorphisms(&g1, None).unwrap(); println!("|Aut(C_4)| = {aut}"); // 8 }
Compute a minimum spanning tree
#![allow(unused)] fn main() { use rust_igraph::{Graph, minimum_spanning_tree, MstAlgorithm}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(1,3),(2,3),(3,4)], false, None ).unwrap(); let weights = vec![4.0, 2.0, 1.0, 3.0, 5.0, 1.0]; let mst_edges = minimum_spanning_tree(&g, Some(&weights), MstAlgorithm::Automatic).unwrap(); println!("MST edges: {:?}", mst_edges); let total_weight: f64 = mst_edges.iter() .map(|&e| weights[e as usize]) .sum(); println!("Total MST weight: {total_weight}"); }
Network flow
#![allow(unused)] fn main() { use rust_igraph::{Graph, max_flow_value}; // A simple flow network (directed) let g = Graph::from_edges( &[(0,1),(0,2),(1,3),(2,3),(1,2)], true, // directed None, ).unwrap(); let capacity = vec![3.0, 2.0, 2.0, 3.0, 1.0]; let flow = max_flow_value(&g, 0, 3, Some(&capacity)).unwrap(); println!("Max flow from 0 to 3: {flow}"); }
Detect communities with Infomap and Spinglass
#![allow(unused)] fn main() { use rust_igraph::{Graph, infomap, spinglass, compare_communities, CommunityComparison}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(3,4),(3,5),(4,5),(2,3),(6,7),(6,8),(7,8),(5,6)], false, None ).unwrap(); // Infomap — information-theoretic (map equation) let im = infomap(&g).unwrap(); println!("Infomap: {} modules, codelength {:.4}", *im.membership.iter().max().unwrap() + 1, im.codelength); // Spinglass — Potts model simulated annealing let sp = spinglass(&g, None).unwrap(); println!("Spinglass: {} communities, modularity {:.4}", *sp.membership.iter().max().unwrap() + 1, sp.modularity); // Compare the two partitions let nmi = compare_communities( &im.membership, &sp.membership, CommunityComparison::NormalizedMutualInformation, ).unwrap(); println!("NMI(Infomap, Spinglass) = {nmi:.4}"); }
Analyze triadic structure (motif census)
#![allow(unused)] fn main() { use rust_igraph::{Graph, triad_census, dyad_census}; // Directed graph for triad analysis let g = Graph::from_edges( &[(0,1),(1,2),(2,0),(0,3),(3,4),(4,0),(1,4)], true, None ).unwrap(); let tc = triad_census(&g).unwrap(); println!("Triad census (16 types):"); for (i, &count) in tc.counts.iter().enumerate() { if count > 0 { println!(" Type {:02}: {count}", i); } } let dc = dyad_census(&g).unwrap(); println!("Dyad census: mutual={}, asymmetric={}, null={}", dc.mutual, dc.asymmetric, dc.null_count); }
Build spatial proximity graphs
#![allow(unused)] fn main() { use rust_igraph::{delaunay_graph, gabriel_graph}; // 2D point cloud let points = vec![ vec![0.0, 0.0], vec![1.0, 0.0], vec![0.5, 0.866], vec![2.0, 0.5], vec![1.5, 1.5], ]; // Delaunay triangulation — connects nearest point triples let dt = delaunay_graph(&points).unwrap(); println!("Delaunay: {} vertices, {} edges", dt.vcount(), dt.ecount()); // Gabriel graph — subset of Delaunay, no third point inside // the diametral circle of any edge let gg = gabriel_graph(&points).unwrap(); println!("Gabriel: {} vertices, {} edges", gg.vcount(), gg.ecount()); }
K-core decomposition and trussness
Identify the dense backbone of a network:
#![allow(unused)] fn main() { use rust_igraph::{Graph, coreness, trussness}; let g = Graph::from_edges( &[(0,1),(0,2),(1,2),(1,3),(2,3),(3,4),(3,5),(4,5),(4,6),(5,6),(6,7)], false, None ).unwrap(); // Coreness: each vertex gets its maximum k such that it belongs to a k-core let cores = coreness(&g).unwrap(); println!("Vertex coreness: {:?}", cores); let max_core = *cores.iter().max().unwrap(); let core_members: Vec<usize> = cores.iter().enumerate() .filter(|&(_, &c)| c == max_core) .map(|(i, _)| i) .collect(); println!("{max_core}-core members: {core_members:?}"); // Trussness: edge-based analog (each edge in a k-truss has >= k-2 triangles) let trusses = trussness(&g).unwrap(); println!("Edge trussness: {:?}", trusses); }
Clique analysis
Find cliques and measure their structure:
#![allow(unused)] fn main() { use rust_igraph::{Graph, clique_number, largest_cliques, maximal_cliques}; let g = Graph::from_edges( &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3),(3,4),(4,5),(5,6),(4,6)], false, None ).unwrap(); // Clique number: size of the largest clique let omega = clique_number(&g).unwrap(); println!("Clique number (ω): {omega}"); // All largest cliques (there may be several of size ω) let big = largest_cliques(&g).unwrap(); println!("Largest cliques:"); for c in &big { println!(" {:?}", c); } // All maximal cliques (cannot be extended by adding a vertex) let all = maximal_cliques(&g).unwrap(); println!("{} maximal cliques found", all.len()); }
Graph algebra: products, union, complement
Build complex graphs from simple building blocks:
#![allow(unused)] fn main() { use rust_igraph::{Graph, cartesian_product, tensor_product, complementer, line_graph, union, intersection}; let path3 = Graph::from_edges(&[(0,1),(1,2)], false, None).unwrap(); let k2 = Graph::from_edges(&[(0,1)], false, None).unwrap(); // Cartesian product: P3 □ K2 gives a ladder graph (6 vertices, 7 edges) let ladder = cartesian_product(&path3, &k2).unwrap(); println!("Ladder: {} vertices, {} edges", ladder.vcount(), ladder.ecount()); // Tensor product (categorical product) let tp = tensor_product(&path3, &k2).unwrap(); println!("Tensor product: {} vertices, {} edges", tp.vcount(), tp.ecount()); // Complement: edges become non-edges and vice versa let comp = complementer(&path3, false).unwrap(); println!("Complement of P3: {} edges (K3 minus P3 = 1 edge)", comp.ecount()); // Line graph: vertices are edges of the original, adjacent if they share a vertex let lg = line_graph(&path3).unwrap(); println!("Line graph of P3: {} vertices, {} edges", lg.vcount(), lg.ecount()); // Union: overlay two graphs on the same vertex set let g1 = Graph::from_edges(&[(0,1),(1,2)], false, None).unwrap(); let g2 = Graph::from_edges(&[(2,3),(3,4)], false, None).unwrap(); let combined = union(&g1, &g2).unwrap(); println!("Union: {} vertices, {} edges", combined.vcount(), combined.ecount()); }
Random walks
Simulate a random walk on a graph:
#![allow(unused)] fn main() { use rust_igraph::{Graph, random_walk, DijkstraMode}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(3,4),(4,0),(0,2),(1,3),(2,4)], false, None ).unwrap(); // Unweighted random walk: 15 steps from vertex 0 let (vertices, edges) = random_walk(&g, None, 0, DijkstraMode::Out, 15, 42).unwrap(); println!("Walk: {:?}", vertices); // Weighted random walk (biases toward heavier edges) let weights = vec![1.0, 1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0]; let (v2, _) = random_walk(&g, Some(&weights), 0, DijkstraMode::Out, 15, 7).unwrap(); println!("Weighted walk: {:?}", v2); }
Distance metrics: eccentricity, radius, diameter
Understand graph-level distance structure:
#![allow(unused)] fn main() { use rust_igraph::{Graph, eccentricity, diameter, radius, girth}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(3,4),(4,5),(5,0),(0,3),(1,4),(2,5)], false, None ).unwrap(); // Eccentricity: max distance from each vertex to any other let ecc = eccentricity(&g).unwrap(); for (v, &e) in ecc.iter().enumerate() { println!(" v{v}: eccentricity = {e}"); } // Diameter: max eccentricity (longest shortest path) let d = diameter(&g).unwrap(); println!("Diameter: {:?}", d); // Radius: min eccentricity (tightest center) let r = radius(&g).unwrap(); println!("Radius: {:?}", r); // Girth: length of the shortest cycle let girth_val = girth(&g).unwrap(); println!("Girth: {:?}", girth_val); }
Layout for visualization export
#![allow(unused)] fn main() { use rust_igraph::{Graph, FrParams, layout_fruchterman_reingold, layout_circle}; let g = Graph::from_edges( &[(0,1),(1,2),(2,3),(3,4),(4,0),(0,2),(1,3)], false, None ).unwrap(); // Force-directed layout let coords = layout_fruchterman_reingold(&g, &FrParams::default()).unwrap(); // Output as CSV for use in external tools println!("vertex,x,y"); for (v, &(x, y)) in coords.iter().enumerate() { println!("{v},{x:.6},{y:.6}"); } // Circle layout (deterministic, good for small graphs) let circle = layout_circle(&g, None); for (v, &(x, y)) in circle.iter().enumerate() { println!("v{v}: ({x:.4}, {y:.4})"); } }
Project layout
rust-igraph/
├── Cargo.toml workspace root
├── CLAUDE.md project-level rules for AI agents
├── CONTRIBUTING.md alpha-stage external policy
├── DEVELOPMENT.md setup + AWU workflow (maintainer notes)
├── CHANGELOG.md Keep a Changelog
├── SECURITY.md vulnerability reporting
├── LICENSE GPL-2.0-or-later
├── README.md high-level overview
├── deny.toml cargo-deny: license + security policy
│
├── src/
│ ├── lib.rs crate root + re-exports
│ ├── core/ Graph, Vector, Matrix, error types
│ └── algorithms/ every algorithm implementation
│
├── tests/
│ └── conformance/{c,py,r}/<algo>/ 3-source fixtures (gitignored: no, tracked)
│
├── benches/ criterion baselines
├── examples/ runnable demos
├── fixtures/ standard graphs (karate, dolphins, ...)
├── scripts/
│ ├── oracle.py live python-igraph oracle bridge
│ ├── bench_compare.py criterion vs python-igraph diff
│ └── test_extract/ conformance extractors (from_c/py/r)
├── templates/ Step-3 skeletons for the AWU SOP
│
├── book/ this mdBook site
├── docs/plans/ planning + design docs
│
├── .codefuse/tracking/ trackers committed alongside code
│ ├── ALGORITHMS.md AWU status (single source of truth)
│ ├── ARCHITECTURE.md ADR index
│ ├── CONFORMANCE.md 3-source coverage matrix
│ ├── AI_PROMPTS.md cookbook of working prompts
│ ├── RESUME.md session-by-session notes
│ └── perf/<ALGO-XXX>.json criterion baselines
│
├── .claude/ AI infrastructure (committed)
│ ├── agents/ 7 sub-agents
│ ├── skills/ 9 /awu-* skills + helpers
│ └── hooks/ git guardrails + auto-fmt
├── .githooks/ repo-hosted git hooks (Co-Authored-By)
│
├── references/ gitignored: igraph C / py / R clones
└── .github/workflows/ CI + Pages deployment
The Master Plan
The rust-igraph master plan lives on GitHub and is updated continuously:
View the Master Plan on GitHub
It covers:
- Phase-by-phase algorithm porting roadmap
- Release milestones and exit criteria
- Architectural decisions and constraints
- v0.6.0 roadmap with three workstreams: algorithms, docs/ecosystem, and engineering quality
Architecture Decisions
Architecture Decision Records (ADRs) for rust-igraph are maintained on GitHub:
View Architecture Decisions on GitHub
Key decisions include:
- GPL-2.0-or-later license (matching upstream igraph)
- Zero
unsafeblocks policy - Single dependency (
thiserror) constraint - Graph data structure design (
Vec<Vec<u32>>adjacency lists) - Three-source conformance testing approach
Algorithm Tracker (AWU)
Every algorithm in rust-igraph is tracked as an Algorithm Work Unit (AWU). The full tracker is on GitHub:
View Algorithm Tracker on GitHub
Current status: 308 AWUs complete across traversal, centrality, community detection, connectivity, flow, isomorphism, generators, layouts, I/O, and more.
Each AWU follows a 9-step SOP: recon, interface design, skeleton, implementation, unit tests, conformance extraction, property tests, benchmarks, and documentation.
Conformance Matrix
rust-igraph is cross-validated against three official igraph implementations. The full conformance matrix is on GitHub:
View Conformance Matrix on GitHub
Conformance testing ensures that rust-igraph produces numerically identical results to:
- igraph C (v0.10.x) — the reference implementation
- python-igraph (v0.11.x) — Python bindings
- R-igraph (rigraph) — R bindings
Test fixtures are extracted from upstream test suites and stored in tests/conformance/{c,py,r}/.
Contributing
rust-igraph welcomes contributions! The full contributing guide is on GitHub:
View Contributing Guide on GitHub
Quick start
- Fork and clone the repository
- Run
cargo test --workspaceto verify your setup - Pick an unassigned AWU from the algorithm tracker
- Follow the 9-step AWU SOP described in the master plan
- Open a PR with
ALGO-XXX-NNNin the title
Key rules
- No
unsafeblocks - No new dependencies without an ADR
- All public APIs need rustdoc with a doctest
- Floating-point comparisons use tolerance helpers, never
==
Development Notes
Detailed development setup and workflow notes are on GitHub:
View Development Notes on GitHub
Essential commands
# Build and test
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
# Full test suite (including conformance and proptests)
cargo test --workspace --all-features
# WASM compatibility check
cargo check --target wasm32-unknown-unknown
Project structure
src/core/ # Graph, Vector, Matrix, attributes
src/algorithms/ # All algorithm implementations
tests/ # Integration, conformance, property tests
fixtures/ # Standard graph datasets
scripts/ # Oracle and test extraction tools
AI Workflow
rust-igraph uses AI-assisted development with Claude Code. The workflow configuration is on GitHub:
The AI workflow includes:
- AWU skills (
/awu-start,/awu-translate,/awu-test, etc.) for systematic algorithm porting - Sub-agents for specialized tasks (recon, translation, testing, benchmarking)
- Three-source conformance extraction from igraph C, Python, and R test suites
- Property-based testing with proptest for algorithmic invariants
All AI-generated code goes through the same quality gates as human-written code: clippy, fmt, full test suite, and conformance checks.
Resume After a Break
If you're picking up development after a break, the resume guide is on GitHub:
Quick checklist
git pulland check for new commits- Review the algorithm tracker for current status
- Check
.codefuse/tracking/for any in-progress AWUs - Run
cargo test --workspaceto verify your local build - Pick up from the last incomplete AWU or start a new one
API rustdoc
The complete public API documentation is auto-generated by cargo doc and
published to GitHub Pages on every push to main:
- rust-igraph — https://Totoro-jam.github.io/rust-igraph/rust_igraph/
Build locally:
cargo doc --no-deps --open
v0.6.0: 1,297 public functions spanning traversal, shortest paths, centrality, community detection, connectivity, flow, isomorphism, graph generators, layouts, spatial algorithms, and I/O. See the algorithm tracker for per-algorithm status.