Deepen structural graph analysis
This commit is contained in:
parent
67924e5b9e
commit
2f293a6490
3 changed files with 581 additions and 33 deletions
20
README.md
20
README.md
|
|
@ -78,9 +78,13 @@ 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
|
||||
|
||||
|
|
@ -88,6 +92,9 @@ The crate exposes `analyze(&graph)` for layout-independent structure reads:
|
|||
|
||||
- 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
|
||||
|
|
@ -107,10 +114,15 @@ for group in result.fold_groups {
|
|||
}
|
||||
```
|
||||
|
||||
Fold groups currently cover weak components, strongly connected components, 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.
|
||||
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.
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
|
|||
154
RESEARCH.md
Normal file
154
RESEARCH.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Research Notes
|
||||
|
||||
This crate is moving toward a staged graph anatomy pipeline:
|
||||
|
||||
```text
|
||||
graph -> analysis -> coarse bodies -> constraints -> 3D relaxation -> fold groups
|
||||
```
|
||||
|
||||
The useful lesson from the literature is blunt: do not ask one force soup to
|
||||
discover every structure at once. Extract graph structure first, then make the
|
||||
solver express it.
|
||||
|
||||
## Landed
|
||||
|
||||
These features are implemented in the crate now:
|
||||
|
||||
- SCC detection for cyclic bodies.
|
||||
- weak components for broad spatial islands.
|
||||
- articulation points and bridges for hinge-like connectors.
|
||||
- biconnected components for blocks that should fold as more durable bodies.
|
||||
- k-core numbers and shells for dense nucleus/periphery structure.
|
||||
- local clustering coefficients for triangle-rich neighborhoods.
|
||||
- deterministic label-propagation communities.
|
||||
- fold groups for weak components, SCCs, biconnected blocks, communities,
|
||||
core shells, and cycles.
|
||||
|
||||
## Paper-Derived Design Moves
|
||||
|
||||
### Constrained Stress
|
||||
|
||||
Dwyer, Koren, and Marriott show that stress majorization can be augmented with
|
||||
linear ordering/separation constraints. That gives us a more principled future
|
||||
solver than hand-added forces when rank, spacing, or no-overlap constraints need
|
||||
to become harder.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Add a `StressConfig`.
|
||||
- Convert Sugiyama ranks, ordering, and component separation into weighted
|
||||
constraints.
|
||||
- Use force relaxation as the interactive fast path and constrained stress as
|
||||
the higher-quality settle pass.
|
||||
|
||||
Source:
|
||||
https://doi.org/10.1016/j.disc.2007.12.103
|
||||
|
||||
### Multilevel Layout
|
||||
|
||||
Walshaw's multilevel force-directed method repeatedly coarsens the graph, lays
|
||||
out the coarse graph, then expands and refines. This is exactly the missing
|
||||
piece for large graphs and foldable bodies: layout the anatomy before the cells.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Coarsen by SCC, community, biconnected block, or k-core shell.
|
||||
- Lay out coarse bodies first.
|
||||
- Expand each body into local coordinates.
|
||||
- Relax with cross-body hinge and boundary forces.
|
||||
|
||||
Source:
|
||||
https://doi.org/10.7155/jgaa.00070
|
||||
|
||||
### Continuous Layout Ergonomics
|
||||
|
||||
ForceAtlas2 is not magic; it is a practical assembly of force choices, adaptive
|
||||
speed, Barnes-Hut approximation, and usability knobs. The useful part for this
|
||||
crate is adaptive step control and Barnes-Hut for large interactive layouts.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Replace fixed cooling with adaptive speed.
|
||||
- Add Barnes-Hut or grid approximation for repulsion.
|
||||
- Expose live-tick APIs for Bevy interaction instead of only batch layout.
|
||||
|
||||
Source:
|
||||
https://doi.org/10.1371/journal.pone.0098679
|
||||
|
||||
### Communities
|
||||
|
||||
Louvain and Leiden are the serious modularity route. Leiden improves Louvain by
|
||||
guaranteeing better-connected communities. The crate currently uses deterministic
|
||||
label propagation because it is small, fast, dependency-free, and good enough as
|
||||
a first structural signal. It is not the final boss.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Keep label propagation as the cheap default.
|
||||
- Add optional Louvain/Leiden-style modularity optimization later.
|
||||
- Use community stability across multiple passes as a visual confidence score.
|
||||
|
||||
Sources:
|
||||
https://arxiv.org/abs/0803.0476
|
||||
https://www.nature.com/articles/s41598-019-41695-z
|
||||
https://doi.org/10.1103/PhysRevE.76.036106
|
||||
|
||||
### Core/Periphery
|
||||
|
||||
k-core decomposition is cheap and useful for visualization. Dense cores should
|
||||
read as nuclei; low-core leaves should read as peripheral atmosphere, not equal
|
||||
citizens in the same spatial argument.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Use core number to scale anchoring, node mass, and opacity hints.
|
||||
- Add `FoldGroupKind::CoreShell` interactions in Bevy.
|
||||
- Give high-core regions their own collision shell.
|
||||
|
||||
Sources:
|
||||
https://arxiv.org/abs/cs/0504107
|
||||
https://www.nature.com/articles/30918
|
||||
|
||||
### Edge Bundling
|
||||
|
||||
Edge bundling is powerful but dangerous. It can reduce clutter while also making
|
||||
the underlying graph harder to recover. The right version here is structural and
|
||||
inspectable: bundles should be generated from known bodies, edge roles, and
|
||||
hierarchy paths, not vague visual mush.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Add edge control points derived from fold groups.
|
||||
- Bundle bridge/cross/back edges differently.
|
||||
- Preserve unbundled edge identity for picking and inspection.
|
||||
- Treat bundling as renderer geometry, not graph truth.
|
||||
|
||||
Sources:
|
||||
https://www.microsoft.com/en-us/research/publication/improving-layered-graph-layouts-with-edgebundling/
|
||||
https://arxiv.org/abs/2108.05467
|
||||
|
||||
### Dynamic Mental Map
|
||||
|
||||
Mental-map preservation is more task-dependent than the usual folklore admits.
|
||||
The useful move is not "never move nodes"; it is preserving stable landmarks
|
||||
when the user needs orientation, while allowing meaningful rearrangement when
|
||||
new structure appears.
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- Add stable node anchors from previous layouts.
|
||||
- Preserve fold group centers more strongly than individual leaf nodes.
|
||||
- Expose a `LayoutMemory` object for incremental Bevy updates.
|
||||
|
||||
Source:
|
||||
https://doi.org/10.1016/j.ijhcs.2013.08.004
|
||||
|
||||
## Next High-Value Crate Work
|
||||
|
||||
1. Add multilevel coarsening and expansion.
|
||||
2. Add a live iterative solver API for Bevy.
|
||||
3. Add Barnes-Hut/grid repulsion.
|
||||
4. Add edge routing and structural bundling output.
|
||||
5. Add optional constrained-stress settling.
|
||||
|
||||
That is the machine with teeth. Everything else is decoration wearing a badge.
|
||||
440
src/lib.rs
440
src/lib.rs
|
|
@ -4,7 +4,8 @@
|
|||
//! ordering, and edge direction pressure. A deterministic force solver then
|
||||
//! relaxes those constraints alongside springs, repulsion, and collision.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{BinaryHeap, VecDeque};
|
||||
|
||||
/// Index of a node in a [`Graph`].
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
|
|
@ -140,6 +141,12 @@ pub struct Layout3dConfig {
|
|||
pub cycle_fold_strength: f32,
|
||||
/// Force anchoring high-centrality nodes closer to their component center.
|
||||
pub centrality_anchor_strength: f32,
|
||||
/// Force pulling label-propagation communities into local regions.
|
||||
pub community_cohesion_strength: f32,
|
||||
/// Force giving dense core nodes stronger horizontal anchoring.
|
||||
pub core_anchor_strength: f32,
|
||||
/// Force keeping high-clustering neighborhoods compact.
|
||||
pub clustering_cohesion_strength: f32,
|
||||
}
|
||||
|
||||
impl Default for Layout3dConfig {
|
||||
|
|
@ -156,6 +163,9 @@ impl Default for Layout3dConfig {
|
|||
bridge_hinge_strength: 0.025,
|
||||
cycle_fold_strength: 0.035,
|
||||
centrality_anchor_strength: 0.02,
|
||||
community_cohesion_strength: 0.025,
|
||||
core_anchor_strength: 0.018,
|
||||
clustering_cohesion_strength: 0.02,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -210,12 +220,17 @@ pub struct Layout3d {
|
|||
pub struct GraphAnalysis {
|
||||
pub weak_components: Vec<Component>,
|
||||
pub strongly_connected_components: Vec<Component>,
|
||||
pub biconnected_components: Vec<Component>,
|
||||
pub communities: Vec<Component>,
|
||||
pub core_shells: Vec<Component>,
|
||||
pub articulation_points: Vec<NodeId>,
|
||||
pub bridges: Vec<EdgeId>,
|
||||
pub node_metrics: Vec<NodeMetrics>,
|
||||
pub edge_roles: Vec<EdgeRole>,
|
||||
pub node_to_weak_component: Vec<usize>,
|
||||
pub node_to_strong_component: Vec<usize>,
|
||||
pub node_to_community: Vec<usize>,
|
||||
pub max_core: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -230,6 +245,10 @@ pub struct NodeMetrics {
|
|||
pub out_degree: usize,
|
||||
pub undirected_degree: usize,
|
||||
pub degree_centrality: f32,
|
||||
pub core_number: usize,
|
||||
pub local_clustering_coefficient: f32,
|
||||
pub community_id: usize,
|
||||
pub biconnected_component_count: usize,
|
||||
pub is_articulation: bool,
|
||||
pub is_cyclic: bool,
|
||||
}
|
||||
|
|
@ -259,6 +278,9 @@ pub struct FoldGroup {
|
|||
pub enum FoldGroupKind {
|
||||
WeakComponent,
|
||||
StrongComponent,
|
||||
BiconnectedComponent,
|
||||
Community,
|
||||
CoreShell,
|
||||
Cycle,
|
||||
}
|
||||
|
||||
|
|
@ -379,15 +401,26 @@ pub fn analyze(graph: &Graph) -> GraphAnalysis {
|
|||
let undirected = undirected_adjacency(graph);
|
||||
let weak_components = weak_components(&undirected);
|
||||
let (sccs, node_to_scc) = strongly_connected_components(&directed);
|
||||
let (articulation_points, bridges) = articulation_points_and_bridges(&undirected);
|
||||
let (articulation_points, bridges, biconnected_components) =
|
||||
articulation_points_bridges_and_blocks(&undirected);
|
||||
let core_numbers = core_numbers(&undirected);
|
||||
let max_core = core_numbers.iter().copied().max().unwrap_or(0);
|
||||
let core_shells = core_shells(&core_numbers);
|
||||
let local_clustering = local_clustering_coefficients(&undirected);
|
||||
let (communities, node_to_community) = label_propagation_communities(&undirected, 16);
|
||||
let biconnected_counts = biconnected_component_counts(n, &biconnected_components);
|
||||
let edge_roles = classify_edges(graph, &node_to_scc, &bridges);
|
||||
let node_metrics = node_metrics(
|
||||
let node_metrics = node_metrics(MetricsInput {
|
||||
graph,
|
||||
&undirected,
|
||||
&articulation_points,
|
||||
&sccs,
|
||||
&node_to_scc,
|
||||
);
|
||||
undirected: &undirected,
|
||||
articulation_points: &articulation_points,
|
||||
sccs: &sccs,
|
||||
node_to_scc: &node_to_scc,
|
||||
core_numbers: &core_numbers,
|
||||
local_clustering: &local_clustering,
|
||||
node_to_community: &node_to_community,
|
||||
biconnected_counts: &biconnected_counts,
|
||||
});
|
||||
let mut node_to_weak_component = vec![0usize; n];
|
||||
for component in &weak_components {
|
||||
for node in &component.nodes {
|
||||
|
|
@ -398,12 +431,17 @@ pub fn analyze(graph: &Graph) -> GraphAnalysis {
|
|||
GraphAnalysis {
|
||||
weak_components,
|
||||
strongly_connected_components: sccs,
|
||||
biconnected_components,
|
||||
communities,
|
||||
core_shells,
|
||||
articulation_points,
|
||||
bridges,
|
||||
node_metrics,
|
||||
edge_roles,
|
||||
node_to_weak_component,
|
||||
node_to_strong_component: node_to_scc,
|
||||
node_to_community,
|
||||
max_core,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -579,9 +617,9 @@ fn strongly_connected_components(adjacency: &[Vec<usize>]) -> (Vec<Component>, V
|
|||
(tarjan.components, tarjan.node_to_component)
|
||||
}
|
||||
|
||||
fn articulation_points_and_bridges(
|
||||
fn articulation_points_bridges_and_blocks(
|
||||
adjacency: &[Vec<(usize, EdgeId)>],
|
||||
) -> (Vec<NodeId>, Vec<EdgeId>) {
|
||||
) -> (Vec<NodeId>, Vec<EdgeId>, Vec<Component>) {
|
||||
struct Dfs<'a> {
|
||||
adjacency: &'a [Vec<(usize, EdgeId)>],
|
||||
time: usize,
|
||||
|
|
@ -590,6 +628,8 @@ fn articulation_points_and_bridges(
|
|||
low: Vec<usize>,
|
||||
articulation: Vec<bool>,
|
||||
bridges: Vec<EdgeId>,
|
||||
edge_stack: Vec<EdgeId>,
|
||||
blocks: Vec<Component>,
|
||||
}
|
||||
|
||||
impl Dfs<'_> {
|
||||
|
|
@ -605,17 +645,24 @@ fn articulation_points_and_bridges(
|
|||
continue;
|
||||
}
|
||||
if self.visited[next] {
|
||||
if self.discovery[next] < self.discovery[node] {
|
||||
self.edge_stack.push(edge);
|
||||
}
|
||||
self.low[node] = self.low[node].min(self.discovery[next]);
|
||||
continue;
|
||||
}
|
||||
|
||||
child_count += 1;
|
||||
self.edge_stack.push(edge);
|
||||
self.visit(next, Some(edge));
|
||||
self.low[node] = self.low[node].min(self.low[next]);
|
||||
|
||||
if parent_edge.is_some() && self.low[next] >= self.discovery[node] {
|
||||
self.articulation[node] = true;
|
||||
}
|
||||
if self.low[next] >= self.discovery[node] {
|
||||
self.pop_block(edge);
|
||||
}
|
||||
if self.low[next] > self.discovery[node] {
|
||||
self.bridges.push(edge);
|
||||
}
|
||||
|
|
@ -625,6 +672,26 @@ fn articulation_points_and_bridges(
|
|||
self.articulation[node] = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_block(&mut self, stop_edge: EdgeId) {
|
||||
let mut nodes = Vec::new();
|
||||
while let Some(edge) = self.edge_stack.pop() {
|
||||
let (source, target) = endpoints_for_edge(self.adjacency, edge);
|
||||
nodes.push(NodeId(source));
|
||||
nodes.push(NodeId(target));
|
||||
if edge == stop_edge {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nodes.sort_by_key(|node| node.0);
|
||||
nodes.dedup();
|
||||
if !nodes.is_empty() {
|
||||
self.blocks.push(Component {
|
||||
id: self.blocks.len(),
|
||||
nodes,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let n = adjacency.len();
|
||||
|
|
@ -636,6 +703,8 @@ fn articulation_points_and_bridges(
|
|||
low: vec![0; n],
|
||||
articulation: vec![false; n],
|
||||
bridges: Vec::new(),
|
||||
edge_stack: Vec::new(),
|
||||
blocks: Vec::new(),
|
||||
};
|
||||
|
||||
for node in 0..n {
|
||||
|
|
@ -651,7 +720,180 @@ fn articulation_points_and_bridges(
|
|||
.filter_map(|(idx, &is_articulation)| is_articulation.then_some(NodeId(idx)))
|
||||
.collect();
|
||||
dfs.bridges.sort_by_key(|edge| edge.0);
|
||||
(articulation_points, dfs.bridges)
|
||||
(articulation_points, dfs.bridges, dfs.blocks)
|
||||
}
|
||||
|
||||
fn endpoints_for_edge(adjacency: &[Vec<(usize, EdgeId)>], edge: EdgeId) -> (usize, usize) {
|
||||
for (source, edges) in adjacency.iter().enumerate() {
|
||||
for &(target, candidate) in edges {
|
||||
if candidate == edge && source <= target {
|
||||
return (source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (source, edges) in adjacency.iter().enumerate() {
|
||||
for &(target, candidate) in edges {
|
||||
if candidate == edge {
|
||||
return (source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
fn core_numbers(adjacency: &[Vec<(usize, EdgeId)>]) -> Vec<usize> {
|
||||
let n = adjacency.len();
|
||||
let mut degree = adjacency.iter().map(Vec::len).collect::<Vec<_>>();
|
||||
let mut core = vec![0usize; n];
|
||||
let mut removed = vec![false; n];
|
||||
let mut heap = BinaryHeap::new();
|
||||
|
||||
for (node, &node_degree) in degree.iter().enumerate() {
|
||||
heap.push(Reverse((node_degree, node)));
|
||||
}
|
||||
|
||||
while let Some(Reverse((candidate_degree, node))) = heap.pop() {
|
||||
if removed[node] || candidate_degree != degree[node] {
|
||||
continue;
|
||||
}
|
||||
|
||||
removed[node] = true;
|
||||
core[node] = candidate_degree;
|
||||
|
||||
for &(next, _) in &adjacency[node] {
|
||||
if !removed[next] && degree[next] > candidate_degree {
|
||||
degree[next] -= 1;
|
||||
heap.push(Reverse((degree[next], next)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core
|
||||
}
|
||||
|
||||
fn core_shells(core_numbers: &[usize]) -> Vec<Component> {
|
||||
let max_core = core_numbers.iter().copied().max().unwrap_or(0);
|
||||
let mut shells = vec![Vec::new(); max_core + 1];
|
||||
for (idx, &core) in core_numbers.iter().enumerate() {
|
||||
shells[core].push(NodeId(idx));
|
||||
}
|
||||
|
||||
shells
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, nodes)| (!nodes.is_empty()).then_some(Component { id, nodes }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn local_clustering_coefficients(adjacency: &[Vec<(usize, EdgeId)>]) -> Vec<f32> {
|
||||
let n = adjacency.len();
|
||||
let mut marks = vec![false; n];
|
||||
let mut coefficients = vec![0.0f32; n];
|
||||
|
||||
for node in 0..n {
|
||||
let neighbors = adjacency[node]
|
||||
.iter()
|
||||
.map(|&(neighbor, _)| neighbor)
|
||||
.collect::<Vec<_>>();
|
||||
let degree = neighbors.len();
|
||||
if degree < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for &neighbor in &neighbors {
|
||||
marks[neighbor] = true;
|
||||
}
|
||||
|
||||
let mut links = 0usize;
|
||||
for &neighbor in &neighbors {
|
||||
for &(candidate, _) in &adjacency[neighbor] {
|
||||
if marks[candidate] {
|
||||
links += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &neighbor in &neighbors {
|
||||
marks[neighbor] = false;
|
||||
}
|
||||
|
||||
coefficients[node] = links as f32 / (degree * (degree - 1)) as f32;
|
||||
}
|
||||
|
||||
coefficients
|
||||
}
|
||||
|
||||
fn label_propagation_communities(
|
||||
adjacency: &[Vec<(usize, EdgeId)>],
|
||||
max_iterations: usize,
|
||||
) -> (Vec<Component>, Vec<usize>) {
|
||||
let n = adjacency.len();
|
||||
let mut labels = (0..n).collect::<Vec<_>>();
|
||||
let mut counts = Vec::<(usize, usize)>::new();
|
||||
|
||||
for _ in 0..max_iterations {
|
||||
let mut changed = false;
|
||||
for node in 0..n {
|
||||
counts.clear();
|
||||
for &(neighbor, _) in &adjacency[node] {
|
||||
let label = labels[neighbor];
|
||||
if let Some((_, count)) = counts.iter_mut().find(|(seen, _)| *seen == label) {
|
||||
*count += 1;
|
||||
} else {
|
||||
counts.push((label, 1));
|
||||
}
|
||||
}
|
||||
|
||||
if counts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
counts.sort_by_key(|&(label, count)| (Reverse(count), label));
|
||||
let best_label = counts[0].0;
|
||||
if labels[node] != best_label {
|
||||
labels[node] = best_label;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut unique = labels.clone();
|
||||
unique.sort_unstable();
|
||||
unique.dedup();
|
||||
|
||||
let mut node_to_community = vec![0usize; n];
|
||||
let mut communities = unique
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, _)| Component {
|
||||
id,
|
||||
nodes: Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (node, label) in labels.iter().enumerate() {
|
||||
let community = unique.binary_search(label).unwrap_or(0);
|
||||
node_to_community[node] = community;
|
||||
communities[community].nodes.push(NodeId(node));
|
||||
}
|
||||
|
||||
(communities, node_to_community)
|
||||
}
|
||||
|
||||
fn biconnected_component_counts(n: usize, components: &[Component]) -> Vec<usize> {
|
||||
let mut counts = vec![0usize; n];
|
||||
for component in components {
|
||||
for node in &component.nodes {
|
||||
counts[node.0] += 1;
|
||||
}
|
||||
}
|
||||
counts
|
||||
}
|
||||
|
||||
fn classify_edges(graph: &Graph, node_to_scc: &[usize], bridges: &[EdgeId]) -> Vec<EdgeRole> {
|
||||
|
|
@ -676,35 +918,41 @@ fn classify_edges(graph: &Graph, node_to_scc: &[usize], bridges: &[EdgeId]) -> V
|
|||
roles
|
||||
}
|
||||
|
||||
fn node_metrics(
|
||||
graph: &Graph,
|
||||
undirected: &[Vec<(usize, EdgeId)>],
|
||||
articulation_points: &[NodeId],
|
||||
sccs: &[Component],
|
||||
node_to_scc: &[usize],
|
||||
) -> Vec<NodeMetrics> {
|
||||
let n = graph.node_count();
|
||||
struct MetricsInput<'a> {
|
||||
graph: &'a Graph,
|
||||
undirected: &'a [Vec<(usize, EdgeId)>],
|
||||
articulation_points: &'a [NodeId],
|
||||
sccs: &'a [Component],
|
||||
node_to_scc: &'a [usize],
|
||||
core_numbers: &'a [usize],
|
||||
local_clustering: &'a [f32],
|
||||
node_to_community: &'a [usize],
|
||||
biconnected_counts: &'a [usize],
|
||||
}
|
||||
|
||||
fn node_metrics(input: MetricsInput<'_>) -> Vec<NodeMetrics> {
|
||||
let n = input.graph.node_count();
|
||||
let mut in_degree = vec![0usize; n];
|
||||
let mut out_degree = vec![0usize; n];
|
||||
for &(source, target) in graph.edges() {
|
||||
for &(source, target) in input.graph.edges() {
|
||||
out_degree[source.0] += 1;
|
||||
in_degree[target.0] += 1;
|
||||
}
|
||||
|
||||
let mut articulation = vec![false; n];
|
||||
for node in articulation_points {
|
||||
for node in input.articulation_points {
|
||||
articulation[node.0] = true;
|
||||
}
|
||||
|
||||
let mut cyclic_scc = vec![false; sccs.len()];
|
||||
for component in sccs {
|
||||
let mut cyclic_scc = vec![false; input.sccs.len()];
|
||||
for component in input.sccs {
|
||||
if component.nodes.len() > 1 {
|
||||
cyclic_scc[component.id] = true;
|
||||
}
|
||||
}
|
||||
for &(source, target) in graph.edges() {
|
||||
for &(source, target) in input.graph.edges() {
|
||||
if source == target {
|
||||
cyclic_scc[node_to_scc[source.0]] = true;
|
||||
cyclic_scc[input.node_to_scc[source.0]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -713,10 +961,14 @@ fn node_metrics(
|
|||
.map(|idx| NodeMetrics {
|
||||
in_degree: in_degree[idx],
|
||||
out_degree: out_degree[idx],
|
||||
undirected_degree: undirected[idx].len(),
|
||||
degree_centrality: undirected[idx].len() as f32 / centrality_denominator,
|
||||
undirected_degree: input.undirected[idx].len(),
|
||||
degree_centrality: input.undirected[idx].len() as f32 / centrality_denominator,
|
||||
core_number: input.core_numbers[idx],
|
||||
local_clustering_coefficient: input.local_clustering[idx],
|
||||
community_id: input.node_to_community[idx],
|
||||
biconnected_component_count: input.biconnected_counts[idx],
|
||||
is_articulation: articulation[idx],
|
||||
is_cyclic: cyclic_scc[node_to_scc[idx]],
|
||||
is_cyclic: cyclic_scc[input.node_to_scc[idx]],
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -903,6 +1155,7 @@ fn relax_3d(
|
|||
) {
|
||||
let n = graph.node_count();
|
||||
let mut forces = vec![(0.0f32, 0.0f32, 0.0f32); n];
|
||||
let undirected = undirected_adjacency(graph);
|
||||
|
||||
for iter in 0..config.base.iterations {
|
||||
forces.fill((0.0, 0.0, 0.0));
|
||||
|
|
@ -910,7 +1163,7 @@ fn relax_3d(
|
|||
apply_repulsion_3d(positions, &mut forces, config);
|
||||
apply_springs_3d(graph, positions, &mut forces, config);
|
||||
apply_constraints_3d(constraints, positions, &mut forces, config);
|
||||
apply_structural_forces_3d(graph, analysis, positions, &mut forces, config);
|
||||
apply_structural_forces_3d(graph, &undirected, analysis, positions, &mut forces, config);
|
||||
apply_grounding_3d(positions, &mut forces, config);
|
||||
|
||||
let cooling = 1.0 - (iter as f32 / config.base.iterations.max(1) as f32);
|
||||
|
|
@ -925,6 +1178,7 @@ fn relax_3d(
|
|||
|
||||
fn apply_structural_forces_3d(
|
||||
graph: &Graph,
|
||||
undirected: &[Vec<(usize, EdgeId)>],
|
||||
analysis: &GraphAnalysis,
|
||||
positions: &[(f32, f32, f32)],
|
||||
forces: &mut [(f32, f32, f32)],
|
||||
|
|
@ -942,7 +1196,15 @@ fn apply_structural_forces_3d(
|
|||
forces,
|
||||
config.weak_component_cohesion_strength,
|
||||
);
|
||||
apply_component_cohesion(
|
||||
&analysis.communities,
|
||||
positions,
|
||||
forces,
|
||||
config.community_cohesion_strength,
|
||||
);
|
||||
apply_centrality_anchors(analysis, positions, forces, config);
|
||||
apply_core_anchors(analysis, positions, forces, config);
|
||||
apply_clustering_cohesion(graph, undirected, analysis, positions, forces, config);
|
||||
apply_cycle_folds(analysis, positions, forces, config);
|
||||
apply_bridge_hinges(graph, analysis, positions, forces, config);
|
||||
}
|
||||
|
|
@ -998,6 +1260,59 @@ fn apply_centrality_anchors(
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_core_anchors(
|
||||
analysis: &GraphAnalysis,
|
||||
positions: &[(f32, f32, f32)],
|
||||
forces: &mut [(f32, f32, f32)],
|
||||
config: &Layout3dConfig,
|
||||
) {
|
||||
if config.core_anchor_strength <= 0.0 || analysis.max_core == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for component in &analysis.weak_components {
|
||||
let center = component_center(&component.nodes, positions);
|
||||
for node in &component.nodes {
|
||||
let idx = node.0;
|
||||
let core_ratio =
|
||||
analysis.node_metrics[idx].core_number as f32 / analysis.max_core as f32;
|
||||
let strength = config.core_anchor_strength * core_ratio * core_ratio;
|
||||
forces[idx].0 += (center[0] - positions[idx].0) * strength;
|
||||
forces[idx].2 += (center[2] - positions[idx].2) * strength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_clustering_cohesion(
|
||||
graph: &Graph,
|
||||
adjacency: &[Vec<(usize, EdgeId)>],
|
||||
analysis: &GraphAnalysis,
|
||||
positions: &[(f32, f32, f32)],
|
||||
forces: &mut [(f32, f32, f32)],
|
||||
config: &Layout3dConfig,
|
||||
) {
|
||||
if config.clustering_cohesion_strength <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for node in 0..graph.node_count() {
|
||||
let coefficient = analysis.node_metrics[node].local_clustering_coefficient;
|
||||
if coefficient <= 0.0 || adjacency[node].len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut nodes = adjacency[node]
|
||||
.iter()
|
||||
.map(|&(neighbor, _)| NodeId(neighbor))
|
||||
.collect::<Vec<_>>();
|
||||
nodes.push(NodeId(node));
|
||||
let center = component_center(&nodes, positions);
|
||||
let strength = config.clustering_cohesion_strength * coefficient;
|
||||
forces[node].0 += (center[0] - positions[node].0) * strength;
|
||||
forces[node].2 += (center[2] - positions[node].2) * strength;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_cycle_folds(
|
||||
analysis: &GraphAnalysis,
|
||||
positions: &[(f32, f32, f32)],
|
||||
|
|
@ -1109,6 +1424,53 @@ fn build_fold_groups(analysis: &GraphAnalysis, positions: &[(f32, f32, f32)]) ->
|
|||
));
|
||||
}
|
||||
|
||||
for component in &analysis.biconnected_components {
|
||||
if component.nodes.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let parent = component
|
||||
.nodes
|
||||
.first()
|
||||
.map(|node| analysis.node_to_weak_component[node.0]);
|
||||
groups.push(fold_group(
|
||||
groups.len(),
|
||||
FoldGroupKind::BiconnectedComponent,
|
||||
component.nodes.clone(),
|
||||
positions,
|
||||
parent,
|
||||
));
|
||||
}
|
||||
|
||||
for component in &analysis.communities {
|
||||
if component.nodes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let parent = component
|
||||
.nodes
|
||||
.first()
|
||||
.map(|node| analysis.node_to_weak_component[node.0]);
|
||||
groups.push(fold_group(
|
||||
groups.len(),
|
||||
FoldGroupKind::Community,
|
||||
component.nodes.clone(),
|
||||
positions,
|
||||
parent,
|
||||
));
|
||||
}
|
||||
|
||||
for component in &analysis.core_shells {
|
||||
if component.nodes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
groups.push(fold_group(
|
||||
groups.len(),
|
||||
FoldGroupKind::CoreShell,
|
||||
component.nodes.clone(),
|
||||
positions,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
for component in &analysis.strongly_connected_components {
|
||||
let is_cycle = component
|
||||
.nodes
|
||||
|
|
@ -1471,6 +1833,14 @@ mod tests {
|
|||
assert!(analysis.articulation_points.contains(&c));
|
||||
assert!(analysis.bridges.contains(&bridge));
|
||||
assert_eq!(analysis.edge_roles[bridge.0], EdgeRole::Bridge);
|
||||
assert!(analysis.biconnected_components.len() >= 2);
|
||||
assert!(analysis.max_core >= 1);
|
||||
assert!(analysis.node_metrics[a.0].core_number >= 2);
|
||||
assert!(analysis.node_metrics[a.0].local_clustering_coefficient > 0.0);
|
||||
assert_eq!(
|
||||
analysis.node_metrics[c.0].biconnected_component_count,
|
||||
analysis.biconnected_components.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1503,5 +1873,17 @@ mod tests {
|
|||
.iter()
|
||||
.any(|group| group.kind == FoldGroupKind::Cycle)
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.fold_groups
|
||||
.iter()
|
||||
.any(|group| group.kind == FoldGroupKind::Community)
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.fold_groups
|
||||
.iter()
|
||||
.any(|group| group.kind == FoldGroupKind::CoreShell)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue