use std::collections::{HashMap, HashSet};
use super::graph::LayoutGraph;
use super::rank;
#[derive(Debug, Clone)]
struct OrderEntry {
v: usize,
barycenter: Option<f64>,
weight: Option<f64>,
}
#[derive(Debug, Clone)]
struct SortResult {
vs: Vec<usize>,
barycenter: Option<f64>,
weight: Option<f64>,
}
#[derive(Debug, Clone)]
struct ResolvedEntry {
vs: Vec<usize>,
i: usize,
barycenter: Option<f64>,
weight: Option<f64>,
}
fn compute_barycenters(
graph: &LayoutGraph,
movable: &[usize],
edges: &[(usize, usize, f64)],
fixed_layer_nodes: &[usize],
) -> Vec<OrderEntry> {
movable
.iter()
.map(|&node| {
let neighbors: Vec<(usize, f64)> = edges
.iter()
.filter(|&&(_, to, _)| to == node)
.map(|&(from, _, w)| (from, w))
.filter(|&(n, _)| fixed_layer_nodes.contains(&n))
.collect();
if neighbors.is_empty() {
OrderEntry {
v: node,
barycenter: None,
weight: None,
}
} else {
let weighted_sum: f64 = neighbors
.iter()
.map(|&(n, w)| w * graph.order[n] as f64)
.sum();
let total_weight: f64 = neighbors.iter().map(|&(_, w)| w).sum();
OrderEntry {
v: node,
barycenter: Some(weighted_sum / total_weight),
weight: Some(total_weight),
}
}
})
.collect()
}
struct ConstraintGraph {
out_edges: HashMap<usize, Vec<usize>>,
in_edges: HashMap<usize, Vec<usize>>,
edge_set: HashSet<(usize, usize)>,
}
impl ConstraintGraph {
fn new() -> Self {
Self {
out_edges: HashMap::new(),
in_edges: HashMap::new(),
edge_set: HashSet::new(),
}
}
fn add_edge(&mut self, from: usize, to: usize) {
if !self.edge_set.insert((from, to)) {
return; }
self.out_edges.entry(from).or_default().push(to);
self.in_edges.entry(to).or_default().push(from);
}
fn edges(&self) -> Vec<(usize, usize)> {
self.out_edges
.iter()
.flat_map(|(&from, tos)| tos.iter().map(move |&to| (from, to)))
.collect()
}
}
fn resolve_conflicts(entries: &[OrderEntry], cg: &ConstraintGraph) -> Vec<ResolvedEntry> {
struct MappedEntry {
indegree: usize,
in_entries: Vec<usize>, out_entries: Vec<usize>,
vs: Vec<usize>,
i: usize,
barycenter: Option<f64>,
weight: Option<f64>,
merged: bool,
}
let mut mapped: Vec<MappedEntry> = entries
.iter()
.enumerate()
.map(|(i, e)| MappedEntry {
indegree: 0,
in_entries: Vec::new(),
out_entries: Vec::new(),
vs: vec![e.v],
i,
barycenter: e.barycenter,
weight: e.weight,
merged: false,
})
.collect();
let mut node_to_entry: HashMap<usize, usize> = HashMap::new();
for (idx, entry) in entries.iter().enumerate() {
node_to_entry.insert(entry.v, idx);
}
for (from, to) in cg.edges() {
if let (Some(&from_idx), Some(&to_idx)) = (node_to_entry.get(&from), node_to_entry.get(&to))
{
mapped[to_idx].indegree += 1;
mapped[from_idx].out_entries.push(to_idx);
}
}
let mut source_set: Vec<usize> = mapped
.iter()
.enumerate()
.filter(|(_, e)| e.indegree == 0)
.map(|(i, _)| i)
.collect();
let mut result_order: Vec<usize> = Vec::new();
while let Some(v_idx) = source_set.pop() {
result_order.push(v_idx);
let in_list: Vec<usize> = mapped[v_idx].in_entries.clone();
for &u_idx in in_list.iter().rev() {
if mapped[u_idx].merged {
continue;
}
let u_bc = mapped[u_idx].barycenter;
let v_bc = mapped[v_idx].barycenter;
if u_bc.is_none() || v_bc.is_none() || u_bc.unwrap() >= v_bc.unwrap() {
let u_vs = mapped[u_idx].vs.clone();
let u_bc = mapped[u_idx].barycenter;
let u_w = mapped[u_idx].weight;
let u_i = mapped[u_idx].i;
let mut sum = 0.0_f64;
let mut weight = 0.0_f64;
if let (Some(bc), Some(w)) = (mapped[v_idx].barycenter, mapped[v_idx].weight) {
sum += bc * w;
weight += w;
}
if let (Some(bc), Some(w)) = (u_bc, u_w) {
sum += bc * w;
weight += w;
}
let mut new_vs = u_vs;
new_vs.extend(&mapped[v_idx].vs);
mapped[v_idx].vs = new_vs;
mapped[v_idx].barycenter = if weight > 0.0 {
Some(sum / weight)
} else {
None
};
mapped[v_idx].weight = if weight > 0.0 { Some(weight) } else { None };
mapped[v_idx].i = mapped[v_idx].i.min(u_i);
mapped[u_idx].merged = true;
}
}
let out_list: Vec<usize> = mapped[v_idx].out_entries.clone();
for &w_idx in &out_list {
mapped[w_idx].in_entries.push(v_idx);
mapped[w_idx].indegree -= 1;
if mapped[w_idx].indegree == 0 {
source_set.push(w_idx);
}
}
}
result_order
.iter()
.filter(|&&idx| !mapped[idx].merged)
.map(|&idx| ResolvedEntry {
vs: mapped[idx].vs.clone(),
i: mapped[idx].i,
barycenter: mapped[idx].barycenter,
weight: mapped[idx].weight,
})
.collect()
}
fn sort_entries(entries: &[ResolvedEntry], bias_right: bool, graph: &LayoutGraph) -> SortResult {
let (mut sortable, mut unsortable): (Vec<&ResolvedEntry>, Vec<&ResolvedEntry>) =
entries.iter().partition(|e| e.barycenter.is_some());
unsortable.sort_by(|a, b| b.i.cmp(&a.i));
sortable.sort_by(|a, b| {
a.barycenter
.unwrap()
.partial_cmp(&b.barycenter.unwrap())
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
let mo_a = a.vs.iter().filter_map(|&v| graph.model_order[v]).min();
let mo_b = b.vs.iter().filter_map(|&v| graph.model_order[v]).min();
mo_a.cmp(&mo_b)
})
.then_with(|| {
if bias_right {
b.i.cmp(&a.i)
} else {
a.i.cmp(&b.i)
}
})
});
let mut vs: Vec<usize> = Vec::new();
let mut vs_index: usize = 0;
let mut sum: f64 = 0.0;
let mut weight: f64 = 0.0;
consume_unsortable_entries(&mut vs, &mut unsortable, &mut vs_index);
for entry in &sortable {
vs_index += entry.vs.len();
vs.extend(&entry.vs);
sum += entry.barycenter.unwrap() * entry.weight.unwrap();
weight += entry.weight.unwrap();
consume_unsortable_entries(&mut vs, &mut unsortable, &mut vs_index);
}
SortResult {
vs,
barycenter: if weight > 0.0 {
Some(sum / weight)
} else {
None
},
weight: if weight > 0.0 { Some(weight) } else { None },
}
}
fn consume_unsortable_entries(
vs: &mut Vec<usize>,
unsortable: &mut Vec<&ResolvedEntry>,
vs_index: &mut usize,
) {
while let Some(entry) = unsortable.last() {
if entry.i <= *vs_index {
let entry = unsortable.pop().unwrap();
vs.extend(&entry.vs);
*vs_index += 1; } else {
break;
}
}
}
fn get_children_at_rank(graph: &LayoutGraph, parent: Option<usize>, rank: i32) -> Vec<usize> {
let mut children = Vec::new();
for n in 0..graph.node_ids.len() {
if graph.parents[n] != parent {
continue;
}
if graph.is_position_node(n) && graph.ranks[n] == rank {
children.push(n);
} else if graph.compound_nodes.contains(&n) {
let min_r = graph.min_rank.get(&n).copied().unwrap_or(i32::MAX);
let max_r = graph.max_rank.get(&n).copied().unwrap_or(i32::MIN);
if min_r <= rank && rank <= max_r {
children.push(n);
}
}
}
children
}
fn get_borders_at_rank(
graph: &LayoutGraph,
parent: Option<usize>,
rank: i32,
) -> (Option<usize>, Option<usize>) {
let parent = match parent {
Some(p) => p,
None => return (None, None),
};
let left = graph.border_left.get(&parent);
let right = graph.border_right.get(&parent);
if left.is_none() || right.is_none() {
return (None, None);
}
let min_r = match graph.min_rank.get(&parent) {
Some(&r) => r,
None => return (None, None),
};
let rank_offset = (rank - min_r) as usize;
let bl = left.and_then(|v| v.get(rank_offset).copied());
let br = right.and_then(|v| v.get(rank_offset).copied());
match (bl, br) {
(Some(bl), Some(br)) => (Some(bl), Some(br)),
_ => (None, None),
}
}
fn is_compound_with_children_at_rank(graph: &LayoutGraph, node: usize, rank: i32) -> bool {
if !graph.compound_nodes.contains(&node) {
return false;
}
let min_r = graph.min_rank.get(&node).copied();
let max_r = graph.max_rank.get(&node).copied();
match (min_r, max_r) {
(Some(min), Some(max)) => min <= rank && rank <= max,
_ => false,
}
}
fn merge_barycenters(entry: &mut OrderEntry, other_bc: f64, other_weight: f64) {
if let (Some(bc), Some(w)) = (entry.barycenter, entry.weight) {
entry.barycenter = Some((bc * w + other_bc * other_weight) / (w + other_weight));
entry.weight = Some(w + other_weight);
} else {
entry.barycenter = Some(other_bc);
entry.weight = Some(other_weight);
}
}
fn expand_subgraphs(entries: &mut [ResolvedEntry], subgraph_results: &HashMap<usize, SortResult>) {
for entry in entries.iter_mut() {
let mut new_vs = Vec::new();
for &v in &entry.vs {
if let Some(sub) = subgraph_results.get(&v) {
new_vs.extend(&sub.vs);
} else {
new_vs.push(v);
}
}
entry.vs = new_vs;
}
}
fn get_border_predecessor_order(
graph: &LayoutGraph,
border: usize,
edges: &[(usize, usize, f64)],
) -> Option<f64> {
edges
.iter()
.filter(|&&(_, to, _)| to == border)
.map(|&(from, _, _)| graph.order[from] as f64)
.next()
}
fn sort_subgraph(
graph: &LayoutGraph,
parent: Option<usize>,
rank: i32,
edges: &[(usize, usize, f64)],
fixed_layer_nodes: &[usize],
cg: &ConstraintGraph,
bias_right: bool,
) -> SortResult {
let children = get_children_at_rank(graph, parent, rank);
let (bl, br) = get_borders_at_rank(graph, parent, rank);
let movable: Vec<usize> = if let (Some(bl_node), Some(br_node)) = (bl, br) {
children
.iter()
.copied()
.filter(|&n| n != bl_node && n != br_node)
.collect()
} else {
children
};
let mut barycenters = compute_barycenters(graph, &movable, edges, fixed_layer_nodes);
let mut subgraph_results: HashMap<usize, SortResult> = HashMap::new();
for entry in &mut barycenters {
if is_compound_with_children_at_rank(graph, entry.v, rank) {
let sub_result = sort_subgraph(
graph,
Some(entry.v),
rank,
edges,
fixed_layer_nodes,
cg,
bias_right,
);
if let Some(sub_bc) = sub_result.barycenter {
merge_barycenters(entry, sub_bc, sub_result.weight.unwrap_or(0.0));
}
subgraph_results.insert(entry.v, sub_result);
}
}
let mut resolved = resolve_conflicts(&barycenters, cg);
expand_subgraphs(&mut resolved, &subgraph_results);
let mut result = sort_entries(&resolved, bias_right, graph);
if let (Some(bl_node), Some(br_node)) = (bl, br) {
let mut vs = vec![bl_node];
vs.extend(&result.vs);
vs.push(br_node);
result.vs = vs;
let bl_pred_order = get_border_predecessor_order(graph, bl_node, edges);
let br_pred_order = get_border_predecessor_order(graph, br_node, edges);
if let (Some(bl_ord), Some(br_ord)) = (bl_pred_order, br_pred_order) {
if result.barycenter.is_none() {
result.barycenter = Some(0.0);
result.weight = Some(0.0);
}
let bc = result.barycenter.unwrap();
let w = result.weight.unwrap();
result.barycenter = Some((bc * w + bl_ord + br_ord) / (w + 2.0));
result.weight = Some(w + 2.0);
}
}
result
}
fn add_subgraph_constraints(graph: &LayoutGraph, cg: &mut ConstraintGraph, sorted_vs: &[usize]) {
let mut prev: HashMap<Option<usize>, usize> = HashMap::new();
'outer: for &v in sorted_vs {
let mut child_opt = graph.parents[v]; while let Some(child_idx) = child_opt {
let parent = graph.parents[child_idx]; if let Some(&prev_child) = prev.get(&parent) {
prev.insert(parent, child_idx);
if prev_child != child_idx {
cg.add_edge(prev_child, child_idx);
continue 'outer; }
} else {
prev.insert(parent, child_idx);
}
child_opt = parent;
}
}
}
fn debug_order() -> bool {
super::debug::order_enabled()
}
fn debug_dump_order(graph: &LayoutGraph, label: &str) {
if !debug_order() {
return;
}
let layers = rank::by_rank(graph);
eprintln!("[order] {label}");
for (rank, layer) in layers.iter().enumerate() {
let mut nodes: Vec<(usize, &str)> = layer
.iter()
.map(|&idx| (graph.order[idx], graph.node_ids[idx].0.as_str()))
.collect();
nodes.sort_by_key(|&(ord, _)| ord);
let names: Vec<String> = nodes
.iter()
.map(|(ord, name)| format!("{name}={ord}"))
.collect();
eprintln!("[order] rank {rank}: [{}]", names.join(", "));
}
}
pub(crate) fn effective_edges_weighted_filtered(graph: &LayoutGraph) -> Vec<(usize, usize, f64)> {
graph
.edges
.iter()
.enumerate()
.filter_map(|(idx, &(from, to, _))| {
if graph.excluded_edges.contains(&idx) {
return None;
}
let weight = graph.edge_weights[idx];
let (from, to) = if graph.reversed_edges.contains(&idx) {
(to, from)
} else {
(from, to)
};
if !graph.is_position_node(from) || !graph.is_position_node(to) {
return None;
}
Some((from, to, weight))
})
.collect()
}
fn init_order(graph: &mut LayoutGraph, layers: &[Vec<usize>]) {
let edges = effective_edges_weighted_filtered(graph);
let n = graph.node_ids.len();
let mut successors: Vec<Vec<usize>> = vec![Vec::new(); n];
for &(from, to, _) in &edges {
successors[from].push(to);
}
for succs in &mut successors {
succs.sort_by(|&a, &b| graph.model_order[a].cmp(&graph.model_order[b]));
}
let mut start_nodes: Vec<usize> = layers.iter().flatten().copied().collect();
start_nodes.sort_by(|&a, &b| {
graph.ranks[a]
.cmp(&graph.ranks[b])
.then_with(|| graph.model_order[a].cmp(&graph.model_order[b]))
});
let mut visited = vec![false; n];
let max_rank = graph.ranks.iter().max().copied().unwrap_or(0) as usize;
let mut layer_next_idx: Vec<usize> = vec![0; max_rank + 1];
for &root in &start_nodes {
if visited[root] {
continue;
}
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if visited[node] {
continue;
}
visited[node] = true;
let rank = graph.ranks[node] as usize;
graph.order[node] = layer_next_idx[rank];
layer_next_idx[rank] += 1;
for &succ in successors[node].iter().rev() {
if !visited[succ] {
stack.push(succ);
}
}
}
}
}
fn layers_sorted_by_order(layers: &[Vec<usize>], graph: &LayoutGraph) -> Vec<Vec<usize>> {
let mut layers: Vec<Vec<usize>> = layers.to_vec();
for layer in &mut layers {
layer.sort_by_key(|&node| graph.order[node]);
}
layers
}
pub fn run(graph: &mut LayoutGraph, enable_greedy_switch: bool) {
run_with_options(graph, enable_greedy_switch, false);
}
pub fn run_with_options(
graph: &mut LayoutGraph,
enable_greedy_switch: bool,
always_compound_ordering: bool,
) {
let layers = rank::by_rank_filtered(graph, |node| graph.is_position_node(node));
if layers.len() < 2 {
return;
}
init_order(graph, &layers);
debug_dump_order(graph, "after init_order");
let layers = layers_sorted_by_order(&layers, graph);
let edges = effective_edges_weighted_filtered(graph);
let mut best_cc = usize::MAX;
let mut best_order: Vec<usize> = Vec::new();
let mut i: usize = 0;
let mut last_best: usize = 0;
let use_compound_sweeps = always_compound_ordering || !graph.compound_nodes.is_empty();
while last_best < 4 {
let bias_right = (i % 4) >= 2;
if use_compound_sweeps {
let downward = !i.is_multiple_of(2); sweep_compound(graph, &layers, &edges, bias_right, downward);
} else if i.is_multiple_of(2) {
sweep_up(graph, &layers, &edges, bias_right);
} else {
sweep_down(graph, &layers, &edges, bias_right);
}
let cc = count_all_crossings(graph, &layers, &edges);
if debug_order() {
let dir = if i.is_multiple_of(2) { "up" } else { "down" };
eprintln!(
"[order] iter {i}: sweep_{dir}, bias_right={bias_right}, cc={cc}, best_cc={best_cc}"
);
debug_dump_order(graph, &format!("after iter {i}"));
}
if cc < best_cc {
last_best = 0;
best_cc = cc;
best_order = graph.order.clone();
if debug_order() {
eprintln!("[order] iter {i}: NEW BEST cc={cc}");
}
}
i += 1;
last_best += 1;
}
if !best_order.is_empty() {
graph.order = best_order;
}
if enable_greedy_switch {
if debug_order() {
let before = count_all_crossings(graph, &layers, &edges);
greedy_switch(graph, &layers, &edges);
let after = count_all_crossings(graph, &layers, &edges);
eprintln!("[order] greedy_switch: {before} -> {after} crossings");
} else {
greedy_switch(graph, &layers, &edges);
}
}
debug_dump_order(graph, "final");
}
fn sweep_compound(
graph: &mut LayoutGraph,
layers: &[Vec<usize>],
edges: &[(usize, usize, f64)],
bias_right: bool,
downward: bool,
) {
let mut cg = ConstraintGraph::new();
let rank_order: Vec<usize> = if downward {
(1..layers.len()).collect()
} else {
(0..layers.len() - 1).rev().collect()
};
for &layer_idx in &rank_order {
if layers[layer_idx].is_empty() {
continue;
}
let fixed_layer = if downward {
&layers[layer_idx - 1]
} else {
&layers[layer_idx + 1]
};
let free_rank = graph.ranks[layers[layer_idx][0]];
let layer_edges: Vec<(usize, usize, f64)> = if downward {
edges
.iter()
.filter(|&&(from, to, _)| {
fixed_layer.contains(&from) && graph.ranks[to] == free_rank
})
.copied()
.collect()
} else {
edges
.iter()
.filter(|&&(from, to, _)| {
fixed_layer.contains(&to) && graph.ranks[from] == free_rank
})
.map(|&(from, to, w)| (to, from, w))
.collect()
};
let result = sort_subgraph(
graph,
None,
free_rank,
&layer_edges,
fixed_layer,
&cg,
bias_right,
);
for (order, &node) in result.vs.iter().enumerate() {
graph.order[node] = order;
}
add_subgraph_constraints(graph, &mut cg, &result.vs);
}
}
fn sweep_down(
graph: &mut LayoutGraph,
layers: &[Vec<usize>],
edges: &[(usize, usize, f64)],
bias_right: bool,
) {
for i in 1..layers.len() {
let fixed = &layers[i - 1];
let free = &layers[i];
reorder_layer(graph, fixed, free, edges, true, bias_right);
}
}
fn sweep_up(
graph: &mut LayoutGraph,
layers: &[Vec<usize>],
edges: &[(usize, usize, f64)],
bias_right: bool,
) {
for i in (0..layers.len() - 1).rev() {
let fixed = &layers[i + 1];
let free = &layers[i];
reorder_layer(graph, fixed, free, edges, false, bias_right);
}
}
fn reorder_layer(
graph: &mut LayoutGraph,
fixed: &[usize],
free: &[usize],
edges: &[(usize, usize, f64)],
downward: bool,
bias_right: bool,
) {
let mut sortable: Vec<(usize, f64, usize)> = Vec::new(); let mut unsortable: Vec<(usize, usize)> = Vec::new();
for (original_pos, &node) in free.iter().enumerate() {
let neighbor_weights: Vec<(usize, f64)> = if downward {
edges
.iter()
.filter(|&&(_, to, _)| to == node)
.map(|&(from, _, w)| (from, w))
.filter(|&(n, _)| fixed.contains(&n))
.collect()
} else {
edges
.iter()
.filter(|&&(from, _, _)| from == node)
.map(|&(_, to, w)| (to, w))
.filter(|&(n, _)| fixed.contains(&n))
.collect()
};
if neighbor_weights.is_empty() {
unsortable.push((node, original_pos));
} else {
let weighted_sum: f64 = neighbor_weights
.iter()
.map(|&(n, w)| w * graph.order[n] as f64)
.sum();
let total_weight: f64 = neighbor_weights.iter().map(|&(_, w)| w).sum();
let barycenter = weighted_sum / total_weight;
sortable.push((node, barycenter, original_pos));
}
}
sortable.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| graph.model_order[a.0].cmp(&graph.model_order[b.0]))
.then_with(|| {
if bias_right {
b.2.cmp(&a.2)
} else {
a.2.cmp(&b.2)
}
})
});
unsortable.sort_by(|a, b| b.1.cmp(&a.1));
let mut result: Vec<usize> = Vec::with_capacity(free.len());
let mut vs_index: usize = 0;
fn consume_unsortable(
result: &mut Vec<usize>,
unsortable: &mut Vec<(usize, usize)>,
vs_index: &mut usize,
) {
while let Some(&(_, orig_pos)) = unsortable.last() {
if orig_pos <= *vs_index {
let (node, _) = unsortable.pop().unwrap();
result.push(node);
*vs_index += 1;
} else {
break;
}
}
}
consume_unsortable(&mut result, &mut unsortable, &mut vs_index);
for &(node, _, _) in &sortable {
result.push(node);
vs_index += 1;
consume_unsortable(&mut result, &mut unsortable, &mut vs_index);
}
while let Some((node, _)) = unsortable.pop() {
result.push(node);
}
for (new_pos, &node) in result.iter().enumerate() {
graph.order[node] = new_pos;
}
}
#[cfg(test)]
fn count_pair_crossings(
graph: &LayoutGraph,
node_a: usize,
node_b: usize,
layers: &[Vec<usize>],
edges: &[(usize, usize, f64)],
) -> usize {
let rank_a = graph.ranks[node_a];
let layer_idx = layers
.iter()
.position(|layer| layer.first().is_some_and(|&n| graph.ranks[n] == rank_a));
let Some(layer_idx) = layer_idx else {
return 0;
};
let mut crossings = 0;
if layer_idx > 0 {
crossings += count_pair_crossings_one_side(graph, node_a, node_b, edges, true);
}
if layer_idx + 1 < layers.len() {
crossings += count_pair_crossings_one_side(graph, node_a, node_b, edges, false);
}
crossings
}
#[cfg(test)]
fn count_pair_crossings_one_side(
graph: &LayoutGraph,
node_a: usize,
node_b: usize,
edges: &[(usize, usize, f64)],
is_upper: bool,
) -> usize {
let neighbors_a = get_pair_neighbors(graph, node_a, edges, is_upper);
let neighbors_b = get_pair_neighbors(graph, node_b, edges, is_upper);
let mut crossings = 0;
for &na_order in &neighbors_a {
for &nb_order in &neighbors_b {
if na_order > nb_order {
crossings += 1;
}
}
}
crossings
}
#[cfg(test)]
fn get_pair_neighbors(
graph: &LayoutGraph,
node: usize,
edges: &[(usize, usize, f64)],
is_upper: bool,
) -> Vec<usize> {
let node_rank = graph.ranks[node];
edges
.iter()
.filter_map(|&(from, to, _)| {
if is_upper {
if to == node && graph.ranks[from] < node_rank {
Some(graph.order[from])
} else {
None
}
} else {
if from == node && graph.ranks[to] > node_rank {
Some(graph.order[to])
} else {
None
}
}
})
.collect()
}
struct AdjacencyIndex {
upper: Vec<Vec<usize>>,
lower: Vec<Vec<usize>>,
}
impl AdjacencyIndex {
fn build(graph: &LayoutGraph, edges: &[(usize, usize, f64)]) -> Self {
let n = graph.node_ids.len();
let mut upper = vec![Vec::new(); n];
let mut lower = vec![Vec::new(); n];
for &(from, to, _) in edges {
if graph.ranks[from] < graph.ranks[to] {
lower[from].push(to); upper[to].push(from); }
}
AdjacencyIndex { upper, lower }
}
fn count_pair_crossings(&self, graph: &LayoutGraph, node_a: usize, node_b: usize) -> usize {
let mut crossings = 0;
for &na in &self.upper[node_a] {
let na_order = graph.order[na];
for &nb in &self.upper[node_b] {
if na_order > graph.order[nb] {
crossings += 1;
}
}
}
for &na in &self.lower[node_a] {
let na_order = graph.order[na];
for &nb in &self.lower[node_b] {
if na_order > graph.order[nb] {
crossings += 1;
}
}
}
crossings
}
}
fn greedy_switch(graph: &mut LayoutGraph, layers: &[Vec<usize>], edges: &[(usize, usize, f64)]) {
let adj = AdjacencyIndex::build(graph, edges);
let mut improved = true;
while improved {
improved = false;
for layer in layers {
let mut sorted: Vec<usize> = layer.clone();
sorted.sort_by_key(|&n| graph.order[n]);
for i in 0..sorted.len().saturating_sub(1) {
let node_a = sorted[i];
let node_b = sorted[i + 1];
if graph.parents[node_a] != graph.parents[node_b] {
continue;
}
let current = adj.count_pair_crossings(graph, node_a, node_b);
let swapped = adj.count_pair_crossings(graph, node_b, node_a);
if swapped < current {
graph.order.swap(node_a, node_b);
sorted.swap(i, i + 1);
improved = true;
}
}
}
}
}
pub(crate) fn count_all_crossings(
graph: &LayoutGraph,
layers: &[Vec<usize>],
edges: &[(usize, usize, f64)],
) -> usize {
let mut total = 0;
for i in 0..layers.len().saturating_sub(1) {
total += count_crossings_between(graph, &layers[i], &layers[i + 1], edges);
}
total
}
fn count_crossings_between(
graph: &LayoutGraph,
layer1: &[usize],
layer2: &[usize],
edges: &[(usize, usize, f64)],
) -> usize {
let mut edge_positions: Vec<(usize, usize)> = Vec::new();
for &(from, to, _) in edges {
if layer1.contains(&from) && layer2.contains(&to) {
edge_positions.push((graph.order[from], graph.order[to]));
} else if layer1.contains(&to) && layer2.contains(&from) {
edge_positions.push((graph.order[to], graph.order[from]));
}
}
let mut crossings = 0;
for i in 0..edge_positions.len() {
for j in i + 1..edge_positions.len() {
let (u1, v1) = edge_positions[i];
let (u2, v2) = edge_positions[j];
if (u1 < u2 && v1 > v2) || (u1 > u2 && v1 < v2) {
crossings += 1;
}
}
}
crossings
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engines::graph::algorithms::layered::graph::DiGraph;
use crate::engines::graph::algorithms::layered::{LayoutConfig, NodeId};
fn setup_graph_and_run(
nodes: &[&str],
edges_list: &[(&str, &str)],
) -> (LayoutGraph, Vec<Vec<usize>>) {
let mut graph: DiGraph<()> = DiGraph::new();
for &n in nodes {
graph.add_node(n, ());
}
for &(from, to) in edges_list {
graph.add_edge(from, to);
}
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
(lg, layers)
}
#[test]
fn test_order_no_crossings() {
let (mut lg, _) = setup_graph_and_run(&["A", "B", "C"], &[("A", "B"), ("B", "C")]);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_all_crossings(&lg, &layers, &edges), 0);
}
#[test]
fn test_order_reduces_crossings() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let crossings = count_all_crossings(&lg, &layers, &edges);
assert_eq!(crossings, 0);
}
#[test]
fn test_crossing_minimize_fan_out_with_back_edge() {
let mut graph: DiGraph<()> = DiGraph::new();
for id in ["A", "B", "C", "D", "E", "F", "G", "H", "I"] {
graph.add_node(id, ());
}
graph.add_edge("A", "B");
graph.add_edge("B", "C");
graph.add_edge("B", "D");
graph.add_edge("C", "E");
graph.add_edge("E", "A"); graph.add_edge("D", "G");
graph.add_edge("D", "H");
graph.add_edge("G", "I");
graph.add_edge("H", "I");
graph.add_edge("I", "F");
graph.add_edge("E", "F");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
crate::engines::graph::algorithms::layered::acyclic::run(&mut lg);
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
crate::engines::graph::algorithms::layered::normalize::run(
&mut lg,
&std::collections::HashMap::new(),
false,
);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let crossings = count_all_crossings(&lg, &layers, &edges);
assert_eq!(crossings, 0, "Expected 0 crossings after ordering");
}
#[test]
fn test_bias_right_changes_order() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
for layer in &layers {
for (idx, &node) in layer.iter().enumerate() {
lg.order[node] = idx;
}
}
let edges = effective_edges_weighted_filtered(&lg);
let fixed = &layers[0]; let free = &layers[1];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
reorder_layer(&mut lg, fixed, free, &edges, true, false);
let left_order_b = lg.order[b];
let left_order_c = lg.order[c];
for (idx, &node) in free.iter().enumerate() {
lg.order[node] = idx;
}
reorder_layer(&mut lg, fixed, free, &edges, true, true);
let right_order_b = lg.order[b];
let right_order_c = lg.order[c];
assert!(
left_order_b < left_order_c,
"B should be before C with left bias"
);
assert!(
right_order_b < right_order_c,
"B should still be before C with right bias (model_order wins over bias)"
);
}
#[test]
fn test_init_order_groups_connected() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
graph.add_edge("B", "D");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let layers = rank::by_rank(&lg);
for layer in &layers {
let mut orders: Vec<usize> = layer.iter().map(|&n| lg.order[n]).collect();
orders.sort();
let expected: Vec<usize> = (0..layer.len()).collect();
assert_eq!(
orders, expected,
"Orders should be consecutive starting from 0"
);
}
}
#[test]
fn test_init_order_disconnected() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let layers = rank::by_rank(&lg);
for layer in &layers {
let mut orders: Vec<usize> = layer.iter().map(|&n| lg.order[n]).collect();
orders.sort();
let expected: Vec<usize> = (0..layer.len()).collect();
assert_eq!(orders, expected);
}
}
#[test]
fn test_adaptive_selects_best() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let crossings = count_all_crossings(&lg, &layers, &edges);
assert_eq!(
crossings, 0,
"Adaptive loop should find zero-crossing ordering"
);
}
#[test]
fn test_adaptive_converges() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_node("E", ());
graph.add_node("F", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
graph.add_edge("B", "D");
graph.add_edge("C", "E");
graph.add_edge("D", "F");
graph.add_edge("E", "F");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
for layer in &layers {
let mut orders: Vec<usize> = layer.iter().map(|&n| lg.order[n]).collect();
orders.sort();
let expected: Vec<usize> = (0..layer.len()).collect();
assert_eq!(
orders, expected,
"Orders should be consecutive in each layer"
);
}
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_all_crossings(&lg, &layers, &edges), 0);
}
#[test]
fn test_adaptive_single_layer() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
}
#[test]
fn test_order_with_disconnected() {
let (mut lg, _) = setup_graph_and_run(
&["A", "B", "C", "D"],
&[
("A", "C"),
("B", "D"),
],
);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
assert!(!layers.is_empty());
}
#[test]
fn test_unsortable_nodes_preserve_position() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_node("E", ());
graph.add_edge("A", "C");
graph.add_edge("B", "E");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let d = lg.node_index[&NodeId::from("D")];
let c = lg.node_index[&NodeId::from("C")];
let e = lg.node_index[&NodeId::from("E")];
let mut orders = vec![lg.order[c], lg.order[d], lg.order[e]];
orders.sort();
assert_eq!(orders, vec![0, 1, 2]);
}
#[test]
fn test_all_unsortable_preserves_order() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_all_crossings(&lg, &layers, &edges), 0);
}
#[test]
fn test_all_sortable_unchanged() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
graph.add_edge("B", "D");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_all_crossings(&lg, &layers, &edges), 0);
}
#[test]
fn test_reorder_layer_unsortable_interleaving() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("Y", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_edge("X", "A");
graph.add_edge("Y", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let x = lg.node_index[&NodeId::from("X")];
let y = lg.node_index[&NodeId::from("Y")];
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
lg.order[x] = 0;
lg.order[y] = 1;
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 2;
let edges = effective_edges_weighted_filtered(&lg);
let fixed = vec![x, y];
let free = vec![a, b, c];
reorder_layer(&mut lg, &fixed, &free, &edges, true, false);
assert_eq!(lg.order[a], 0);
assert_eq!(lg.order[b], 1);
assert_eq!(lg.order[c], 2);
}
#[test]
fn test_weighted_barycenter_uniform_weights() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
assert!(lg.edge_weights.iter().all(|&w| w == 1.0));
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_all_crossings(&lg, &layers, &edges), 0);
}
#[test]
fn test_weighted_barycenter_nonuniform() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("Y", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_edge("X", "A");
graph.add_edge("Y", "A");
graph.add_edge("Y", "B");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let x = lg.node_index[&NodeId::from("X")];
let a = lg.node_index[&NodeId::from("A")];
for (idx, &(from, to, _)) in lg.edges.iter().enumerate() {
let (eff_from, eff_to) = if lg.reversed_edges.contains(&idx) {
(to, from)
} else {
(from, to)
};
if eff_from == x && eff_to == a {
lg.edge_weights[idx] = 3.0;
}
}
let y = lg.node_index[&NodeId::from("Y")];
let b = lg.node_index[&NodeId::from("B")];
lg.order[x] = 0;
lg.order[y] = 1;
lg.order[a] = 0;
lg.order[b] = 1;
let edges = effective_edges_weighted_filtered(&lg);
let fixed = vec![x, y];
let free = vec![a, b];
reorder_layer(&mut lg, &fixed, &free, &edges, true, false);
assert_eq!(
lg.order[a], 0,
"A (weighted barycenter 0.25) should be first"
);
assert_eq!(lg.order[b], 1, "B (barycenter 1.0) should be second");
}
use crate::engines::graph::algorithms::layered::{border, nesting};
fn build_compound_for_ordering() -> LayoutGraph {
let mut g: DiGraph<()> = DiGraph::new();
g.add_node("X", ());
g.add_node("A", ());
g.add_node("B", ());
g.add_node("sg1", ());
g.add_edge("X", "A");
g.add_edge("A", "B");
g.set_parent("A", "sg1");
g.set_parent("B", "sg1");
let mut lg = LayoutGraph::from_digraph(&g, |_, _| (10.0, 10.0));
nesting::run(&mut lg);
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
nesting::cleanup(&mut lg);
nesting::assign_rank_minmax(&mut lg);
border::add_segments(&mut lg);
lg
}
#[test]
fn test_compound_ordering_borders_at_edges() {
let mut lg = build_compound_for_ordering();
let sg1_idx = lg.node_index[&"sg1".into()];
run(&mut lg, false);
let border_tops: HashSet<usize> = lg.border_top.values().copied().collect();
let border_bottoms: HashSet<usize> = lg.border_bottom.values().copied().collect();
let border_titles: HashSet<usize> = lg.border_title.values().copied().collect();
let is_excluded = |node: usize| {
border_tops.contains(&node)
|| border_bottoms.contains(&node)
|| border_titles.contains(&node)
};
let left_borders = &lg.border_left[&sg1_idx];
let right_borders = &lg.border_right[&sg1_idx];
let min_r = lg.min_rank[&sg1_idx];
let max_r = lg.max_rank[&sg1_idx];
let layers = rank::by_rank(&lg);
let layers = layers_sorted_by_order(&layers, &lg);
for rank in min_r..=max_r {
let rank_offset = (rank - min_r) as usize;
let left_border = left_borders[rank_offset];
let right_border = right_borders[rank_offset];
let layer = layers
.iter()
.find(|l| !l.is_empty() && lg.ranks[l[0]] == rank)
.expect("should find layer for rank");
let sg1_children: Vec<usize> = layer
.iter()
.copied()
.filter(|&n| lg.parents[n] == Some(sg1_idx) && !is_excluded(n))
.collect();
if sg1_children.len() >= 2 {
let min_order = sg1_children.iter().map(|&n| lg.order[n]).min().unwrap();
let max_order = sg1_children.iter().map(|&n| lg.order[n]).max().unwrap();
assert_eq!(
lg.order[left_border], min_order,
"Left border should have min order among sg1 children at rank {rank}"
);
assert_eq!(
lg.order[right_border], max_order,
"Right border should have max order among sg1 children at rank {rank}"
);
}
}
}
#[test]
fn test_compound_ordering_children_contiguous() {
let mut g: DiGraph<()> = DiGraph::new();
g.add_node("X", ());
g.add_node("A", ());
g.add_node("B", ());
g.add_node("C", ());
g.add_node("D", ());
g.add_node("sg1", ());
g.add_node("sg2", ());
g.add_edge("X", "A");
g.add_edge("X", "C");
g.add_edge("A", "B");
g.add_edge("C", "D");
g.set_parent("A", "sg1");
g.set_parent("B", "sg1");
g.set_parent("C", "sg2");
g.set_parent("D", "sg2");
let mut lg = LayoutGraph::from_digraph(&g, |_, _| (10.0, 10.0));
nesting::run(&mut lg);
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
nesting::cleanup(&mut lg);
nesting::assign_rank_minmax(&mut lg);
border::add_segments(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let layers = layers_sorted_by_order(&layers, &lg);
let sg1_idx = lg.node_index[&"sg1".into()];
let sg2_idx = lg.node_index[&"sg2".into()];
let mut border_nodes: HashSet<usize> = HashSet::new();
for nodes in lg.border_left.values() {
border_nodes.extend(nodes.iter().copied());
}
for nodes in lg.border_right.values() {
border_nodes.extend(nodes.iter().copied());
}
border_nodes.extend(lg.border_top.values().copied());
border_nodes.extend(lg.border_bottom.values().copied());
border_nodes.extend(lg.border_title.values().copied());
for layer in &layers {
let sg1_children: Vec<usize> = layer
.iter()
.copied()
.filter(|&n| lg.parents[n] == Some(sg1_idx) && !border_nodes.contains(&n))
.collect();
let sg2_children: Vec<usize> = layer
.iter()
.copied()
.filter(|&n| lg.parents[n] == Some(sg2_idx) && !border_nodes.contains(&n))
.collect();
if sg1_children.len() >= 2 {
let orders: Vec<usize> = sg1_children.iter().map(|&n| lg.order[n]).collect();
let span = orders.iter().max().unwrap() - orders.iter().min().unwrap() + 1;
assert_eq!(
span,
sg1_children.len(),
"sg1 children should be contiguous in layer"
);
}
if sg2_children.len() >= 2 {
let orders: Vec<usize> = sg2_children.iter().map(|&n| lg.order[n]).collect();
let span = orders.iter().max().unwrap() - orders.iter().min().unwrap() + 1;
assert_eq!(
span,
sg2_children.len(),
"sg2 children should be contiguous in layer"
);
}
}
}
#[test]
fn test_simple_graph_ordering_unchanged() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
graph.add_edge("B", "D");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(
count_all_crossings(&lg, &layers, &edges),
0,
"Simple diamond should have zero crossings"
);
}
#[test]
fn test_compute_barycenters_with_connections() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("Y", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_edge("X", "A");
graph.add_edge("Y", "A");
graph.add_edge("Y", "B");
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let x = lg.node_index[&NodeId::from("X")];
let y = lg.node_index[&NodeId::from("Y")];
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let mut lg = lg;
lg.order[x] = 0;
lg.order[y] = 1;
let edges: Vec<(usize, usize, f64)> = vec![(x, a, 1.0), (y, a, 1.0), (y, b, 1.0)];
let fixed = vec![x, y];
let movable = vec![a, b];
let result = compute_barycenters(&lg, &movable, &edges, &fixed);
assert_eq!(result.len(), 2);
assert_eq!(result[0].v, a);
assert!((result[0].barycenter.unwrap() - 0.5).abs() < 1e-9);
assert!((result[0].weight.unwrap() - 2.0).abs() < 1e-9);
assert_eq!(result[1].v, b);
assert!((result[1].barycenter.unwrap() - 1.0).abs() < 1e-9);
assert!((result[1].weight.unwrap() - 1.0).abs() < 1e-9);
}
#[test]
fn test_compute_barycenters_no_connections() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("D", ());
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let x = lg.node_index[&NodeId::from("X")];
let d = lg.node_index[&NodeId::from("D")];
let edges: Vec<(usize, usize, f64)> = vec![];
let fixed = vec![x];
let movable = vec![d];
let result = compute_barycenters(&lg, &movable, &edges, &fixed);
assert_eq!(result.len(), 1);
assert_eq!(result[0].v, d);
assert!(result[0].barycenter.is_none());
assert!(result[0].weight.is_none());
}
#[test]
fn test_compute_barycenters_weighted() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("Y", ());
graph.add_node("A", ());
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let x = lg.node_index[&NodeId::from("X")];
let y = lg.node_index[&NodeId::from("Y")];
let a = lg.node_index[&NodeId::from("A")];
lg.order[x] = 0;
lg.order[y] = 1;
let edges: Vec<(usize, usize, f64)> = vec![(x, a, 3.0), (y, a, 1.0)];
let fixed = vec![x, y];
let movable = vec![a];
let result = compute_barycenters(&lg, &movable, &edges, &fixed);
assert_eq!(result.len(), 1);
assert!((result[0].barycenter.unwrap() - 0.25).abs() < 1e-9);
assert!((result[0].weight.unwrap() - 4.0).abs() < 1e-9);
}
#[test]
fn test_resolve_conflicts_no_constraints() {
let entries = vec![
OrderEntry {
v: 10,
barycenter: Some(1.0),
weight: Some(1.0),
},
OrderEntry {
v: 11,
barycenter: Some(2.0),
weight: Some(1.0),
},
OrderEntry {
v: 12,
barycenter: Some(3.0),
weight: Some(1.0),
},
];
let cg = ConstraintGraph::new();
let result = resolve_conflicts(&entries, &cg);
assert_eq!(result.len(), 3);
for r in &result {
assert_eq!(r.vs.len(), 1);
assert_eq!(r.vs[0], r.vs[0]); }
let mut vs: Vec<usize> = result.iter().map(|r| r.vs[0]).collect();
vs.sort();
assert_eq!(vs, vec![10, 11, 12]);
}
#[test]
fn test_resolve_conflicts_compatible_constraint() {
let entries = vec![
OrderEntry {
v: 10,
barycenter: Some(1.0),
weight: Some(1.0),
},
OrderEntry {
v: 11,
barycenter: Some(2.0),
weight: Some(1.0),
},
];
let mut cg = ConstraintGraph::new();
cg.add_edge(10, 11);
let result = resolve_conflicts(&entries, &cg);
assert_eq!(result.len(), 2);
assert_eq!(result[0].vs, vec![10]);
assert_eq!(result[1].vs, vec![11]);
}
#[test]
fn test_resolve_conflicts_conflicting_merges() {
let entries = vec![
OrderEntry {
v: 10,
barycenter: Some(3.0),
weight: Some(1.0),
},
OrderEntry {
v: 11,
barycenter: Some(1.0),
weight: Some(1.0),
},
];
let mut cg = ConstraintGraph::new();
cg.add_edge(10, 11);
let result = resolve_conflicts(&entries, &cg);
assert_eq!(result.len(), 1);
assert_eq!(result[0].vs, vec![10, 11]);
assert!((result[0].barycenter.unwrap() - 2.0).abs() < 1e-9);
assert!((result[0].weight.unwrap() - 2.0).abs() < 1e-9);
assert_eq!(result[0].i, 0); }
#[test]
fn test_resolve_conflicts_undefined_barycenter_merges() {
let entries = vec![
OrderEntry {
v: 10,
barycenter: None,
weight: None,
},
OrderEntry {
v: 11,
barycenter: Some(1.0),
weight: Some(1.0),
},
];
let mut cg = ConstraintGraph::new();
cg.add_edge(10, 11);
let result = resolve_conflicts(&entries, &cg);
assert_eq!(result.len(), 1);
assert_eq!(result[0].vs, vec![10, 11]);
}
#[test]
fn test_resolve_conflicts_unsortable_passthrough() {
let entries = vec![OrderEntry {
v: 10,
barycenter: None,
weight: None,
}];
let cg = ConstraintGraph::new();
let result = resolve_conflicts(&entries, &cg);
assert_eq!(result.len(), 1);
assert_eq!(result[0].vs, vec![10]);
assert_eq!(result[0].i, 0);
assert!(result[0].barycenter.is_none());
}
fn re(vs: Vec<usize>, i: usize, bc: Option<f64>, w: Option<f64>) -> ResolvedEntry {
ResolvedEntry {
vs,
i,
barycenter: bc,
weight: w,
}
}
fn dummy_graph_for_sort(max_node: usize) -> LayoutGraph {
let mut graph: DiGraph<()> = DiGraph::new();
let names: Vec<String> = (0..=max_node).map(|i| format!("n{i}")).collect();
for name in &names {
graph.add_node(name.as_str(), ());
}
LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0))
}
#[test]
fn test_sort_entries_all_sortable() {
let lg = dummy_graph_for_sort(12);
let entries = vec![
re(vec![10], 0, Some(2.0), Some(1.0)),
re(vec![11], 1, Some(0.5), Some(1.0)),
re(vec![12], 2, Some(1.5), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(result.vs, vec![11, 12, 10]);
}
#[test]
fn test_sort_entries_interleave_unsortable() {
let lg = dummy_graph_for_sort(12);
let entries = vec![
re(vec![10], 0, Some(0.5), Some(1.0)),
re(vec![11], 1, None, None),
re(vec![12], 2, Some(1.5), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(result.vs, vec![10, 11, 12]);
}
#[test]
fn test_sort_entries_bias_right_tie_break() {
let lg = dummy_graph_for_sort(12);
let entries = vec![
re(vec![10], 0, Some(1.0), Some(1.0)),
re(vec![11], 1, Some(1.0), Some(1.0)),
];
let result_left = sort_entries(&entries, false, &lg);
assert_eq!(result_left.vs, vec![10, 11]);
let result_right = sort_entries(&entries, true, &lg);
assert_eq!(result_right.vs, vec![10, 11]);
}
#[test]
fn test_sort_entries_aggregate_barycenter() {
let lg = dummy_graph_for_sort(12);
let entries = vec![
re(vec![10], 0, Some(1.0), Some(2.0)),
re(vec![11], 1, Some(3.0), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert!((result.barycenter.unwrap() - 5.0 / 3.0).abs() < 1e-9);
assert!((result.weight.unwrap() - 3.0).abs() < 1e-9);
}
#[test]
fn test_sort_entries_multi_node_entries() {
let lg = dummy_graph_for_sort(12);
let entries = vec![
re(vec![10, 11], 0, Some(1.0), Some(1.0)),
re(vec![12], 2, None, None),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(result.vs, vec![10, 11, 12]);
}
#[test]
fn test_sort_entries_model_order_tie_break() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("B", ()); graph.add_node("C", ());
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let entries = vec![
re(vec![b], 0, Some(1.0), Some(1.0)),
re(vec![c], 1, Some(1.0), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(
result.vs[0], b,
"B (lower model_order) should be first with bias_right=false"
);
assert_eq!(result.vs[1], c);
let result = sort_entries(&entries, true, &lg);
assert_eq!(
result.vs[0], b,
"B (lower model_order) should be first even with bias_right=true"
);
assert_eq!(result.vs[1], c);
}
#[test]
fn test_sort_entries_model_order_does_not_override_barycenter() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("D", ()); graph.add_node("C", ());
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let d = lg.node_index[&NodeId::from("D")];
let c = lg.node_index[&NodeId::from("C")];
let entries = vec![
re(vec![d], 0, Some(2.0), Some(1.0)),
re(vec![c], 1, Some(0.5), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(
result.vs[0], c,
"C (lower barycenter) should be first despite higher model_order"
);
assert_eq!(result.vs[1], d);
}
#[test]
fn test_sort_entries_multi_node_entry_uses_min_model_order() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("B", ()); graph.add_node("C", ()); graph.add_node("D", ());
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
let entries = vec![
re(vec![b, c], 0, Some(1.0), Some(1.0)),
re(vec![d], 1, Some(1.0), Some(1.0)),
];
let result = sort_entries(&entries, false, &lg);
assert_eq!(result.vs[0], b);
assert_eq!(result.vs[1], c);
assert_eq!(result.vs[2], d);
}
#[test]
fn test_sort_subgraph_flat_no_compound() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("X", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_edge("X", "A");
graph.add_edge("X", "B");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let x = lg.node_index[&NodeId::from("X")];
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
lg.order[x] = 0;
lg.order[a] = 0;
lg.order[b] = 1;
let edges = effective_edges_weighted_filtered(&lg);
let fixed = vec![x];
let cg = ConstraintGraph::new();
let result = sort_subgraph(&lg, None, lg.ranks[a], &edges, &fixed, &cg, false);
assert_eq!(result.vs.len(), 2);
assert!(result.vs.contains(&a));
assert!(result.vs.contains(&b));
}
#[test]
fn test_sort_subgraph_with_borders() {
let mut lg = build_compound_for_ordering();
let sg1_idx = lg.node_index[&"sg1".into()];
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
init_order(&mut lg, &layers);
let layers = layers_sorted_by_order(&layers, &lg);
let edges = effective_edges_weighted_filtered(&lg);
let min_r = lg.min_rank[&sg1_idx];
let max_r = lg.max_rank[&sg1_idx];
for rank in min_r..=max_r {
let rank_offset = (rank - min_r) as usize;
let bl = lg.border_left[&sg1_idx][rank_offset];
let br = lg.border_right[&sg1_idx][rank_offset];
let layer_idx = layers
.iter()
.position(|l| !l.is_empty() && lg.ranks[l[0]] == rank)
.unwrap();
if layer_idx == 0 {
continue; }
let fixed = &layers[layer_idx - 1];
let cg = ConstraintGraph::new();
let result = sort_subgraph(&lg, Some(sg1_idx), rank, &edges, fixed, &cg, false);
if result.vs.len() >= 2 {
assert_eq!(
result.vs[0], bl,
"Border left should be first in sort_subgraph result"
);
assert_eq!(
*result.vs.last().unwrap(),
br,
"Border right should be last in sort_subgraph result"
);
}
}
}
#[test]
fn test_add_subgraph_constraints_siblings() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("sg1", ());
graph.add_node("sg2", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.set_parent("A", "sg1");
graph.set_parent("B", "sg2");
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let sg1 = lg.node_index[&NodeId::from("sg1")];
let sg2 = lg.node_index[&NodeId::from("sg2")];
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let mut cg = ConstraintGraph::new();
add_subgraph_constraints(&lg, &mut cg, &[a, b]);
let edges = cg.edges();
assert!(
edges.contains(&(sg1, sg2)),
"Should have constraint sg1 -> sg2, got: {:?}",
edges
);
}
#[test]
fn test_add_subgraph_constraints_same_subgraph_no_edge() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("sg1", ());
graph.add_node("A", ());
graph.add_node("B", ());
graph.set_parent("A", "sg1");
graph.set_parent("B", "sg1");
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let mut cg = ConstraintGraph::new();
add_subgraph_constraints(&lg, &mut cg, &[a, b]);
assert!(
cg.edges().is_empty(),
"Should have no constraints for same-subgraph siblings"
);
}
#[test]
fn test_add_subgraph_constraints_no_parent() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
let lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let mut cg = ConstraintGraph::new();
add_subgraph_constraints(&lg, &mut cg, &[a, b]);
assert!(cg.edges().is_empty());
}
#[test]
fn test_crossing_count_k33_no_regression() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A1", "A2", "A3", "B1", "B2", "B3"] {
graph.add_node(n, ());
}
for a in ["A1", "A2", "A3"] {
for b in ["B1", "B2", "B3"] {
graph.add_edge(a, b);
}
}
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let cc = count_all_crossings(&lg, &layers, &edges);
assert!(
cc <= 9,
"K_3,3 crossings should not exceed baseline of 9, got {cc}"
);
assert!(cc >= 1, "K_3,3 must have at least 1 crossing");
}
#[test]
fn test_crossing_count_fan_no_regression() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["X", "Y", "C", "D", "E"] {
graph.add_node(n, ());
}
graph.add_edge("X", "D");
graph.add_edge("X", "C");
graph.add_edge("Y", "C");
graph.add_edge("Y", "E");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let cc = count_all_crossings(&lg, &layers, &edges);
assert_eq!(cc, 0, "Fan-out crossings should be 0, got {cc}");
}
#[test]
fn test_crossing_count_deep_no_regression() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D", "E", "F", "G", "H"] {
graph.add_node(n, ());
}
graph.add_edge("A", "C");
graph.add_edge("A", "D");
graph.add_edge("B", "C");
graph.add_edge("B", "E");
graph.add_edge("C", "F");
graph.add_edge("D", "G");
graph.add_edge("E", "H");
graph.add_edge("D", "H");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let cc = count_all_crossings(&lg, &layers, &edges);
assert!(
cc <= 1,
"Deep graph crossings should not exceed baseline of 1, got {cc}"
);
}
#[test]
fn test_order_timing_baseline() {
use std::time::Instant;
let sizes = [10, 20, 50, 100];
for &n in &sizes {
let mut graph: DiGraph<()> = DiGraph::new();
for i in 0..n {
graph.add_node(format!("L0_{i}"), ());
graph.add_node(format!("L1_{i}"), ());
}
for i in 0..n {
for j in 0..3_usize.min(n) {
graph.add_edge(format!("L0_{i}"), format!("L1_{}", (i + j) % n));
}
}
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let start = Instant::now();
run(&mut lg, false);
let elapsed = start.elapsed();
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
let cc = count_all_crossings(&lg, &layers, &edges);
eprintln!(
"order::run() n={n}: {:.3}ms, crossings={}",
elapsed.as_secs_f64() * 1000.0,
cc
);
}
}
#[test]
fn test_count_pair_crossings_no_crossings() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("A", "C");
graph.add_edge("B", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_pair_crossings(&lg, a, b, &layers, &edges), 0);
assert_eq!(count_pair_crossings(&lg, b, a, &layers, &edges), 1);
}
#[test]
fn test_count_pair_crossings_with_crossings() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_pair_crossings(&lg, a, b, &layers, &edges), 1);
assert_eq!(count_pair_crossings(&lg, b, a, &layers, &edges), 0);
}
#[test]
fn test_count_pair_crossings_two_sided() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["X", "Y", "A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("X", "A");
graph.add_edge("Y", "B");
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let x = lg.node_index[&NodeId::from("X")];
let y = lg.node_index[&NodeId::from("Y")];
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[x] = 0;
lg.order[y] = 1;
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_pair_crossings(&lg, a, b, &layers, &edges), 1);
assert_eq!(count_pair_crossings(&lg, b, a, &layers, &edges), 1);
}
#[test]
fn test_count_pair_crossings_no_edges() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C"] {
graph.add_node(n, ());
}
graph.add_edge("A", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let layers = rank::by_rank(&lg);
let edges = effective_edges_weighted_filtered(&lg);
assert_eq!(count_pair_crossings(&lg, a, b, &layers, &edges), 0);
assert_eq!(count_pair_crossings(&lg, b, a, &layers, &edges), 0);
}
#[test]
fn test_greedy_switch_reduces_crossings() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
let before = count_all_crossings(&lg, &layers, &edges);
assert_eq!(before, 1, "Should have 1 crossing before greedy switch");
greedy_switch(&mut lg, &layers, &edges);
let after = count_all_crossings(&lg, &layers, &edges);
assert_eq!(after, 0, "Should have 0 crossings after greedy switch");
}
#[test]
fn test_greedy_switch_no_change_when_optimal() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("A", "C");
graph.add_edge("B", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
let order_before: Vec<usize> = lg.order.clone();
greedy_switch(&mut lg, &layers, &edges);
assert_eq!(
lg.order, order_before,
"Order should not change when optimal"
);
}
#[test]
fn test_greedy_switch_preserves_consecutive_orders() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D", "E", "F"] {
graph.add_node(n, ());
}
graph.add_edge("A", "D");
graph.add_edge("A", "F");
graph.add_edge("B", "E");
graph.add_edge("C", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
greedy_switch(&mut lg, &layers, &edges);
for layer in &layers {
let mut orders: Vec<usize> = layer.iter().map(|&n| lg.order[n]).collect();
orders.sort();
let expected: Vec<usize> = (0..layer.len()).collect();
assert_eq!(
orders, expected,
"Orders must be consecutive within each layer"
);
}
}
#[test]
fn test_greedy_switch_never_increases_crossings() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D", "E", "F", "G", "H"] {
graph.add_node(n, ());
}
graph.add_edge("A", "E");
graph.add_edge("A", "F");
graph.add_edge("B", "G");
graph.add_edge("B", "E");
graph.add_edge("C", "H");
graph.add_edge("D", "F");
graph.add_edge("D", "G");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
let before = count_all_crossings(&lg, &layers, &edges);
greedy_switch(&mut lg, &layers, &edges);
let after = count_all_crossings(&lg, &layers, &edges);
assert!(
after <= before,
"Greedy switch must never increase crossings: before={}, after={}",
before,
after
);
}
#[test]
fn test_greedy_switch_converges() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A1", "A2", "A3", "B1", "B2", "B3"] {
graph.add_node(n, ());
}
for a in ["A1", "A2", "A3"] {
for b in ["B1", "B2", "B3"] {
graph.add_edge(a, b);
}
}
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
greedy_switch(&mut lg, &layers, &edges);
let cc = count_all_crossings(&lg, &layers, &edges);
assert!(cc >= 1, "K_3,3 must have at least 1 crossing");
}
#[test]
fn test_greedy_switch_skips_different_parents() {
let mut g: DiGraph<()> = DiGraph::new();
g.add_node("X", ());
g.add_node("A", ());
g.add_node("B", ());
g.add_node("C", ());
g.add_node("D", ());
g.add_node("sg1", ());
g.add_node("sg2", ());
g.add_edge("X", "A");
g.add_edge("X", "C");
g.add_edge("A", "B");
g.add_edge("C", "D");
g.set_parent("A", "sg1");
g.set_parent("B", "sg1");
g.set_parent("C", "sg2");
g.set_parent("D", "sg2");
let mut lg = LayoutGraph::from_digraph(&g, |_, _| (10.0, 10.0));
nesting::run(&mut lg);
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
nesting::cleanup(&mut lg);
nesting::assign_rank_minmax(&mut lg);
border::add_segments(&mut lg);
run(&mut lg, false);
let layers = rank::by_rank(&lg);
let sorted_layers = layers_sorted_by_order(&layers, &lg);
let sg1_idx = lg.node_index[&"sg1".into()];
let sg2_idx = lg.node_index[&"sg2".into()];
let mut border_nodes: HashSet<usize> = HashSet::new();
for nodes in lg.border_left.values() {
border_nodes.extend(nodes.iter().copied());
}
for nodes in lg.border_right.values() {
border_nodes.extend(nodes.iter().copied());
}
border_nodes.extend(lg.border_top.values().copied());
border_nodes.extend(lg.border_bottom.values().copied());
border_nodes.extend(lg.border_title.values().copied());
for layer in &sorted_layers {
let sg1_children: Vec<usize> = layer
.iter()
.copied()
.filter(|&n| lg.parents[n] == Some(sg1_idx) && !border_nodes.contains(&n))
.collect();
let sg2_children: Vec<usize> = layer
.iter()
.copied()
.filter(|&n| lg.parents[n] == Some(sg2_idx) && !border_nodes.contains(&n))
.collect();
if sg1_children.len() >= 2 {
let orders: Vec<usize> = sg1_children.iter().map(|&n| lg.order[n]).collect();
let span = orders.iter().max().unwrap() - orders.iter().min().unwrap() + 1;
assert_eq!(
span,
sg1_children.len(),
"sg1 children should remain contiguous after greedy switch"
);
}
if sg2_children.len() >= 2 {
let orders: Vec<usize> = sg2_children.iter().map(|&n| lg.order[n]).collect();
let span = orders.iter().max().unwrap() - orders.iter().min().unwrap() + 1;
assert_eq!(
span,
sg2_children.len(),
"sg2 children should remain contiguous after greedy switch"
);
}
}
}
#[test]
fn test_greedy_switch_flat_graph_no_parent_check_interference() {
let mut graph: DiGraph<()> = DiGraph::new();
for n in ["A", "B", "C", "D"] {
graph.add_node(n, ());
}
graph.add_edge("A", "D");
graph.add_edge("B", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let layers = rank::by_rank_filtered(&lg, |node| lg.is_position_node(node));
let edges = effective_edges_weighted_filtered(&lg);
let before = count_all_crossings(&lg, &layers, &edges);
greedy_switch(&mut lg, &layers, &edges);
let after = count_all_crossings(&lg, &layers, &edges);
assert!(after <= before);
}
#[test]
fn test_init_order_respects_model_order_for_same_rank() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("B", ());
graph.add_node("A", ());
graph.add_node("X", ());
graph.add_node("Y", ());
graph.add_edge("B", "Y");
graph.add_edge("A", "X");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
assert!(
lg.order[b] < lg.order[a],
"B (model_order 0) should come before A (model_order 1) at same rank. Got B={}, A={}",
lg.order[b],
lg.order[a]
);
}
#[test]
fn test_init_order_rank_still_primary() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("B", ()); graph.add_node("A", ()); graph.add_edge("A", "B");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
assert_eq!(lg.order[a], 0); assert_eq!(lg.order[b], 0); }
#[test]
fn test_init_order_fan_out_declaration_order() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("C", ()); graph.add_node("B", ()); graph.add_node("D", ()); graph.add_edge("A", "C");
graph.add_edge("A", "B");
graph.add_edge("A", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let c = lg.node_index[&NodeId::from("C")];
let b = lg.node_index[&NodeId::from("B")];
let d = lg.node_index[&NodeId::from("D")];
assert!(
lg.order[c] < lg.order[b],
"C (declared first) should precede B. Got C={}, B={}",
lg.order[c],
lg.order[b]
);
assert!(
lg.order[b] < lg.order[d],
"B (declared second) should precede D. Got B={}, D={}",
lg.order[b],
lg.order[d]
);
}
#[test]
fn test_init_order_fan_out_edge_order() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_node("D", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
graph.add_edge("A", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let layers = rank::by_rank(&lg);
init_order(&mut lg, &layers);
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
assert_eq!(lg.order[b], 0, "B should be first");
assert_eq!(lg.order[c], 1, "C should be second");
assert_eq!(lg.order[d], 2, "D should be third");
}
#[test]
fn test_reorder_layer_model_order_tie_break() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("C", ());
graph.add_edge("A", "B");
graph.add_edge("A", "C");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
lg.order[a] = 0;
lg.order[b] = 0;
lg.order[c] = 1;
let edges = effective_edges_weighted_filtered(&lg);
let fixed = vec![a];
let free = vec![b, c];
reorder_layer(&mut lg, &fixed, &free, &edges, true, false);
assert_eq!(
lg.order[b], 0,
"B (lower model_order) should be first. Got B={}, C={}",
lg.order[b], lg.order[c]
);
assert_eq!(lg.order[c], 1);
lg.order[b] = 0;
lg.order[c] = 1;
reorder_layer(&mut lg, &fixed, &free, &edges, true, true);
assert_eq!(
lg.order[b], 0,
"B (lower model_order) should still be first even with bias_right=true. Got B={}, C={}",
lg.order[b], lg.order[c]
);
}
#[test]
fn test_reorder_layer_model_order_does_not_override_barycenter() {
let mut graph: DiGraph<()> = DiGraph::new();
graph.add_node("A", ());
graph.add_node("B", ());
graph.add_node("D", ()); graph.add_node("C", ()); graph.add_edge("A", "C");
graph.add_edge("B", "D");
let mut lg = LayoutGraph::from_digraph(&graph, |_, _| (10.0, 10.0));
rank::run(&mut lg, &LayoutConfig::default());
rank::normalize(&mut lg);
let a = lg.node_index[&NodeId::from("A")];
let b = lg.node_index[&NodeId::from("B")];
let c = lg.node_index[&NodeId::from("C")];
let d = lg.node_index[&NodeId::from("D")];
lg.order[a] = 0;
lg.order[b] = 1;
lg.order[c] = 0;
lg.order[d] = 1;
let edges = effective_edges_weighted_filtered(&lg);
let fixed = vec![a, b];
let free = vec![c, d];
reorder_layer(&mut lg, &fixed, &free, &edges, true, false);
assert_eq!(
lg.order[c], 0,
"C (barycenter=0) should be first despite higher model_order"
);
assert_eq!(
lg.order[d], 1,
"D (barycenter=1) should be second despite lower model_order"
);
}
}