This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
epiphany-graph-rs/README.md
2026-05-07 04:45:45 +01:00

237 lines
7.8 KiB
Markdown

# epiphany-graph-rs
Hybrid graph layout for DAGs that still want to breathe.
This crate treats Sugiyama as a constraint generator instead of a final layout.
The Sugiyama pass assigns ranks and same-rank order; the force pass relaxes
those constraints with springs, repulsion, collision, and edge direction
pressure.
The result is a spectrum:
- force-dominant for organic structure
- Sugiyama-dominant for hierarchical DAGs
- hybrid for the usual graph reality, where purity gets mugged in the hallway
## Current Pipeline
1. Analyze graph anatomy: weak components, SCCs, bridges, articulation points,
edge roles, degree metrics, and cyclic nodes.
2. Assign ranks from directed edges using a longest-path DAG pass.
3. Preserve partial hierarchy for cyclic leftovers with local edge pressure.
4. Order nodes inside each rank with repeated barycenter sweeps.
5. Generate rank, order, and edge-direction constraints.
6. Relax the graph with deterministic force iterations.
7. Emit fold groups for renderer-side collapse, expansion, highlighting, or
protein-ish folding.
## Example
```rust
use epiphany_graph_rs::{layout, Graph, LayoutConfig};
let mut graph = Graph::new();
let a = graph.add_node(1.0);
let b = graph.add_node(1.0);
let c = graph.add_node(1.0);
graph.add_edge(a, b);
graph.add_edge(b, c);
let result = layout(&graph, &LayoutConfig::default());
for node in result.nodes {
println!("{:?}: ({}, {}) rank={}", node.id, node.x, node.y, node.rank);
}
```
## 3D Layout
Use `layout_3d` when the renderer has a real spatial scene, such as Bevy.
Hierarchy stays on `Y`; organic spread happens across `X/Z`; grounding and
depth forces keep the graph shallow instead of letting it become decorative fog.
```rust
use epiphany_graph_rs::{layout_3d, Graph, Layout3dConfig};
let mut graph = Graph::new();
let root = graph.add_node(1.0);
let child = graph.add_node(1.0);
graph.add_edge(root, child);
let result = layout_3d(&graph, &Layout3dConfig::default());
for node in result.nodes {
let [x, y, z] = node.as_xyz();
println!("{:?}: ({x}, {y}, {z}) rank={}", node.id, node.rank);
}
```
For Bevy, map `NodeLayout3d::as_xyz()` directly into `Vec3::new(x, y, z)`.
The crate does not depend on Bevy just to borrow its vector type; the layout
core stays small and renderer-agnostic.
## Realtime Solver
Use `Layout3dSolver` when Bevy needs to iterate layout over frames:
```rust
use epiphany_graph_rs::{Graph, Layout3dConfig, Layout3dSolver};
let graph = Graph::new();
let mut solver = Layout3dSolver::new(graph, Layout3dConfig::default());
// In a Bevy system, run a small number of iterations per frame.
solver.tick(2);
for position in solver.positions() {
let (x, y, z) = *position;
// Transform::from_translation(Vec3::new(x, y, z))
}
```
The solver caches graph analysis, Sugiyama constraints, ranks, order, adjacency,
positions, and force buffers. Use `with_initial_positions` to warm-start after a
graph edit. That keeps the layout from starting over like it suffered a small
bureaucratic head injury.
`Layout3dConfig::repulsion_mode` controls the expensive part:
- `RepulsionMode::BarnesHut` is the default realtime path.
- `RepulsionMode::SpatialGrid` keeps a simpler local approximation available.
- `RepulsionMode::Exact` keeps all-pairs repulsion for small graphs or quality
comparisons.
Barnes-Hut uses an octree and `barnes_hut_theta` as the opening-angle knob.
Lower theta values are more accurate; higher values are faster. Large graphs
still want multilevel coarsening, because even good approximation does not make
every visual decision worth rendering individually.
Additional scale knobs split local and global behavior:
- `barnes_hut_near_radius` forces Barnes-Hut to recurse near the target instead
of aggregating local neighborhoods.
- `near_repulsion_scale` controls exact pairwise/local repulsion.
- `far_repulsion_scale` controls Barnes-Hut aggregate far-field repulsion.
## Accuracy Sweeps
Use the repulsion accuracy sweep to compare approximation settings against exact
all-pairs repulsion on the same positions:
```rust
use epiphany_graph_rs::{
repulsion_accuracy_sweep, Layout3dConfig, RepulsionAccuracyCandidate,
};
let positions = [
[0.0, 0.0, 0.0],
[120.0, 20.0, 40.0],
[-80.0, 10.0, -60.0],
];
let reports = repulsion_accuracy_sweep(
&positions,
&Layout3dConfig::default(),
&[
RepulsionAccuracyCandidate::barnes_hut(0.4),
RepulsionAccuracyCandidate::barnes_hut(0.7),
RepulsionAccuracyCandidate::barnes_hut(1.0),
RepulsionAccuracyCandidate::barnes_hut_tuned(1.0, 240.0, 0.85),
RepulsionAccuracyCandidate::spatial_grid(180.0, 1),
],
);
for report in reports {
println!(
"{:?}: mean_rel={} rms_rel={} max_rel={} elapsed={:?}",
report.candidate.repulsion_mode,
report.mean_relative_error,
report.rms_relative_error,
report.max_relative_error,
report.elapsed
);
}
```
For a live Bevy-style solver, call `solver_repulsion_accuracy_sweep(&solver,
&candidates)` to evaluate the current positions without advancing the layout.
This is the tuning harness: sweep theta, grid radius, and later force-category
scale knobs until the error/time curve stops paying rent.
Use `organ_battleground` for structure-aware force experiments:
```bash
.\scripts\fetch_tuning_datasets.ps1
cargo run --release --example organ_battleground
```
It compares raw node approximations and body-force candidates on synthetic
fixtures plus SNAP email/Gnutella datasets when available.
Use `evaluate_layout_quality(&graph, &layout)` to score fold compactness, group
separation, rank error, edge length variance, and bridge length variance for a
layout snapshot.
## Graph Analysis And Folding
The crate exposes `analyze(&graph)` for layout-independent structure reads:
- weak components
- strongly connected components
- biconnected components
- label-propagation communities
- k-core shells
- articulation points
- bridge edges
- per-node degree metrics
- local clustering coefficients
- edge roles
- cyclic node flags
`layout_3d` uses that same analysis to add structural pressure:
- SCC cohesion folds cycles and tight feedback regions into local bodies.
- weak component cohesion keeps broad regions visually legible.
- community cohesion gives dense modules local territory.
- k-core anchoring lets dense nuclei read differently from peripheral leaves.
- local clustering cohesion tightens triangle-rich neighborhoods.
- bridge hinges keep connector edges readable between bodies.
- centrality anchoring keeps important nodes from drifting into decorative exile.
- cycle folding uses depth so feedback does not have to lose every argument
against hierarchy.
The result includes `fold_groups`, which are renderer-facing bodies:
```rust
for group in result.fold_groups {
println!(
"{:?}: center={:?} radius={} nodes={}",
group.kind,
group.center,
group.radius,
group.nodes.len()
);
}
```
Fold groups currently cover weak components, strongly connected components,
biconnected components, communities, core shells, and cycle bodies. They are
meant to be useful handles for Bevy interactions: collapse a component, pulse a
cycle, dim a subtree, or unfold a dense region without asking the raw node soup
for permission.
See [RESEARCH.md](RESEARCH.md) for the paper trail and the next deeper cuts:
multilevel coarsening, constrained stress, edge bundling, adaptive solvers, and
layout memory. See [TUNING_DOCTRINE.md](TUNING_DOCTRINE.md) for measured
performance/accuracy tradeoffs and current tuning policy.
## Notes
The first release favors a small deterministic core over a sprawling layout
cathedral. The public API exposes generated constraints so callers can inspect,
reuse, tune, or replace the force relaxation phase later.
The realtime solver defaults to Barnes-Hut repulsion. Exact pairwise repulsion
is still available for small graphs and quality comparisons.