Skip to main content

rust_igraph/algorithms/layout/
sugiyama.rs

1//! Sugiyama layered layout (ALGO-LO-005).
2//!
3//! Hierarchical layout for directed (acyclic) graphs. Reference:
4//! K. Sugiyama, S. Tagawa, M. Toda, "Methods for Visual Understanding
5//! of Hierarchical Systems", IEEE Trans. SMC 11(2), 1981.
6//!
7//! Coordinate assignment uses the Brandes-Köpf method:
8//! U. Brandes, B. Köpf, "Fast and Simple Horizontal Coordinate
9//! Assignment", LNCS 2265:31-44, 2002.
10
11use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
12use std::collections::VecDeque;
13
14/// Result of a Sugiyama layout computation.
15#[derive(Debug, Clone)]
16pub struct SugiyamaResult {
17    /// Positions `[x, y]` for each vertex in the original graph (indices 0..vcount).
18    pub positions: Vec<[f64; 2]>,
19    /// Positions of dummy vertices added for long edges (indices vcount..).
20    pub dummy_positions: Vec<[f64; 2]>,
21    /// The extended graph containing both original and dummy vertices.
22    /// Each edge spans exactly one layer and points downward.
23    pub extended_graph: Graph,
24    /// For each edge in `extended_graph`, the index of the corresponding
25    /// original edge. Multiple extended edges map to the same original edge
26    /// when dummy nodes split a long edge.
27    pub extended_to_orig_eids: Vec<u32>,
28}
29
30/// Parameters for the Sugiyama layout.
31#[derive(Debug, Clone)]
32pub struct SugiyamaParams {
33    /// Preferred horizontal gap between vertices in the same layer.
34    pub hgap: f64,
35    /// Vertical distance between layers.
36    pub vgap: f64,
37    /// Maximum number of iterations for crossing minimization.
38    pub maxiter: u32,
39}
40
41impl Default for SugiyamaParams {
42    fn default() -> Self {
43        Self {
44            hgap: 1.0,
45            vgap: 1.0,
46            maxiter: 100,
47        }
48    }
49}
50
51/// Compute the Sugiyama hierarchical layout.
52///
53/// Designed for directed acyclic graphs where vertices are assigned to
54/// layers. Vertices on the same layer share the same Y coordinate. The
55/// algorithm minimizes edge crossings via the barycenter heuristic and
56/// assigns X coordinates using the Brandes-Köpf method.
57///
58/// For graphs with cycles, a DFS-based feedback arc set is computed and
59/// those edges are reversed before layering.
60///
61/// If `layers` is `None`, layer assignment is computed automatically using
62/// longest-path layering.
63///
64/// # Arguments
65///
66/// * `graph` — input graph (directed preferred; undirected is treated as
67///   all edges pointing both ways).
68/// * `layers` — optional per-vertex layer assignment (0-indexed). If
69///   provided, must have length equal to `graph.vcount()`.
70/// * `params` — layout parameters (gaps, iterations).
71///
72/// # Examples
73///
74/// ```
75/// use rust_igraph::{Graph, layout_sugiyama, SugiyamaParams};
76///
77/// // Simple DAG: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
78/// let mut g = Graph::new(4, true).unwrap();
79/// g.add_edge(0, 1).unwrap();
80/// g.add_edge(0, 2).unwrap();
81/// g.add_edge(1, 3).unwrap();
82/// g.add_edge(2, 3).unwrap();
83///
84/// let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
85/// assert_eq!(result.positions.len(), 4);
86/// // Vertex 0 should be at top (y=0), vertex 3 at bottom (y=2)
87/// assert!((result.positions[0][1]).abs() < 1e-10);
88/// assert!((result.positions[3][1] - 2.0).abs() < 1e-10);
89/// ```
90pub fn layout_sugiyama(
91    graph: &Graph,
92    layers: Option<&[u32]>,
93    params: &SugiyamaParams,
94) -> IgraphResult<SugiyamaResult> {
95    let n = graph.vcount() as usize;
96
97    if n == 0 {
98        return Ok(SugiyamaResult {
99            positions: Vec::new(),
100            dummy_positions: Vec::new(),
101            extended_graph: Graph::new(0, graph.is_directed())?,
102            extended_to_orig_eids: Vec::new(),
103        });
104    }
105
106    // Step 1: Determine layer assignment
107    let mut layer_assign = if let Some(l) = layers {
108        if l.len() != n {
109            return Err(IgraphError::InvalidArgument(
110                "layer vector length must equal vertex count".into(),
111            ));
112        }
113        l.to_vec()
114    } else {
115        compute_layers(graph)?
116    };
117
118    // Normalize layers: eliminate empty layers and make contiguous 0..k
119    let layer_to_y = normalize_layers(&mut layer_assign, params.vgap);
120
121    // Step 2: Build extended graph with dummy nodes for long edges
122    let ext = build_extended_graph(graph, &layer_assign)?;
123
124    // Step 3: Compute layout for each connected component
125    let mut positions = vec![[0.0_f64; 2]; n];
126    let mut dummy_positions: Vec<[f64; 2]> = Vec::new();
127
128    // For simplicity, process all vertices together (single-component path)
129    let total_nodes = ext.node_count;
130    let ext_layers = ext.layers.clone();
131
132    // Build layering structure
133    let num_layers = ext_layers.iter().copied().max().unwrap_or(0) as usize + 1;
134    let mut layering: Vec<Vec<usize>> = vec![Vec::new(); num_layers];
135    for (v, &lyr) in ext_layers.iter().enumerate() {
136        layering[lyr as usize].push(v);
137    }
138
139    // Assign initial Y coordinates
140    let mut layout_y = vec![0.0_f64; total_nodes];
141    for (v, &lyr) in ext_layers.iter().enumerate() {
142        let y_idx = lyr as usize;
143        layout_y[v] = if y_idx < layer_to_y.len() {
144            layer_to_y[y_idx]
145        } else {
146            f64::from(lyr) * params.vgap
147        };
148    }
149
150    // Step 4: Order nodes horizontally (crossing minimization)
151    let mut layout_x = vec![0.0_f64; total_nodes];
152    order_horizontally(
153        &ext.edges,
154        total_nodes,
155        &layering,
156        &mut layout_x,
157        params.maxiter,
158    );
159
160    // Step 5: Assign horizontal coordinates (Brandes-Köpf simplified)
161    place_horizontally(
162        &ext.edges,
163        total_nodes,
164        &layering,
165        &mut layout_x,
166        &ext_layers,
167        params.hgap,
168        n,
169    );
170
171    // Collect results
172    for v in 0..n {
173        positions[v] = [layout_x[v], layout_y[v]];
174    }
175    for v in n..total_nodes {
176        dummy_positions.push([layout_x[v], layout_y[v]]);
177    }
178
179    // Build the actual extended Graph
180    let mut ext_graph = Graph::new(total_nodes as u32, graph.is_directed())?;
181    for &(src, tgt) in &ext.edges {
182        ext_graph.add_edge(src as VertexId, tgt as VertexId)?;
183    }
184
185    Ok(SugiyamaResult {
186        positions,
187        dummy_positions,
188        extended_graph: ext_graph,
189        extended_to_orig_eids: ext.edge_to_orig,
190    })
191}
192
193// ═══════════════════════════════════════════════════════════════════
194// Layer assignment
195// ═══════════════════════════════════════════════════════════════════
196
197fn compute_layers(graph: &Graph) -> IgraphResult<Vec<u32>> {
198    let n = graph.vcount() as usize;
199    let directed = graph.is_directed();
200
201    // For directed graphs: use longest-path layering after removing cycles
202    // For undirected: use BFS-based layering from a source vertex
203    if directed {
204        longest_path_layering(graph)
205    } else {
206        // BFS from vertex 0 (or highest-degree vertex)
207        let mut layers = vec![0u32; n];
208        let mut visited = vec![false; n];
209        let mut queue = VecDeque::new();
210
211        let root = select_highest_degree(graph);
212        visited[root] = true;
213        queue.push_back(root);
214
215        while let Some(v) = queue.pop_front() {
216            let neighbors = all_neighbors(graph, v as VertexId);
217            for nei in neighbors {
218                let nei_idx = nei as usize;
219                if !visited[nei_idx] {
220                    visited[nei_idx] = true;
221                    layers[nei_idx] = layers[v]
222                        .checked_add(1)
223                        .ok_or_else(|| IgraphError::InvalidArgument("layer overflow".into()))?;
224                    queue.push_back(nei_idx);
225                }
226            }
227        }
228
229        // Handle disconnected vertices
230        for i in 0..n {
231            if !visited[i] {
232                layers[i] = 0;
233            }
234        }
235
236        Ok(layers)
237    }
238}
239
240fn longest_path_layering(graph: &Graph) -> IgraphResult<Vec<u32>> {
241    let n = graph.vcount() as usize;
242    let ecount = graph.ecount();
243
244    // First, find a feedback arc set via DFS and compute a topological order
245    // ignoring back edges
246    let mut in_degree = vec![0u32; n];
247    let mut adj_out: Vec<Vec<usize>> = vec![Vec::new(); n];
248    let mut back_edges = vec![false; ecount];
249
250    // DFS to find back edges
251    let mut color = vec![0u8; n]; // 0=white, 1=gray, 2=black
252    let mut stack: Vec<(usize, usize)> = Vec::new(); // (vertex, neighbor_index)
253
254    for start in 0..n {
255        if color[start] != 0 {
256            continue;
257        }
258        stack.push((start, 0));
259        color[start] = 1;
260
261        while let Some((v, idx)) = stack.last_mut() {
262            let v_id = *v;
263            let out_edges = out_edges_of(graph, v_id as VertexId);
264            if *idx >= out_edges.len() {
265                color[v_id] = 2;
266                stack.pop();
267            } else {
268                let (eid, tgt) = out_edges[*idx];
269                *idx += 1;
270                let tgt_idx = tgt as usize;
271                if color[tgt_idx] == 1 {
272                    // Back edge — mark for removal
273                    back_edges[eid] = true;
274                } else if color[tgt_idx] == 0 {
275                    color[tgt_idx] = 1;
276                    stack.push((tgt_idx, 0));
277                }
278            }
279        }
280    }
281
282    // Build DAG adjacency (ignoring back edges)
283    for eid in 0..ecount {
284        if back_edges[eid] {
285            continue;
286        }
287        if let Ok((src, tgt)) = graph.edge(eid as u32) {
288            let src_idx = src as usize;
289            let tgt_idx = tgt as usize;
290            adj_out[src_idx].push(tgt_idx);
291            in_degree[tgt_idx] = in_degree[tgt_idx]
292                .checked_add(1)
293                .ok_or_else(|| IgraphError::InvalidArgument("in-degree overflow".into()))?;
294        }
295    }
296
297    // Longest-path layering on the DAG
298    // layer[v] = max(layer[pred] + 1) for all predecessors
299    let mut layers = vec![0u32; n];
300    let mut queue: VecDeque<usize> = VecDeque::new();
301    let mut remaining_in = in_degree.clone();
302
303    // Start from sources (in_degree == 0)
304    for v in 0..n {
305        if remaining_in[v] == 0 {
306            queue.push_back(v);
307        }
308    }
309
310    while let Some(v) = queue.pop_front() {
311        for &w in &adj_out[v] {
312            let new_layer = layers[v]
313                .checked_add(1)
314                .ok_or_else(|| IgraphError::InvalidArgument("layer overflow".into()))?;
315            if new_layer > layers[w] {
316                layers[w] = new_layer;
317            }
318            remaining_in[w] -= 1;
319            if remaining_in[w] == 0 {
320                queue.push_back(w);
321            }
322        }
323    }
324
325    // If there are unreachable vertices (isolated or in unprocessed cycles),
326    // assign them layer 0
327    Ok(layers)
328}
329
330fn normalize_layers(layers: &mut [u32], vgap: f64) -> Vec<f64> {
331    if layers.is_empty() {
332        return Vec::new();
333    }
334
335    // Sort unique layer values and create contiguous mapping
336    let mut unique: Vec<u32> = layers.to_vec();
337    unique.sort_unstable();
338    unique.dedup();
339
340    let mut layer_to_y = Vec::with_capacity(unique.len());
341    for (new_idx, &old_val) in unique.iter().enumerate() {
342        layer_to_y.push(f64::from(old_val) * vgap);
343        for l in layers.iter_mut() {
344            if *l == old_val {
345                *l = new_idx as u32;
346            }
347        }
348    }
349
350    // Re-derive layer_to_y based on new contiguous indices
351    let num_layers = unique.len();
352    let mut result = vec![0.0; num_layers];
353    for i in 0..num_layers {
354        result[i] = i as f64 * vgap;
355    }
356    result
357}
358
359// ═══════════════════════════════════════════════════════════════════
360// Extended graph (dummy nodes for long edges)
361// ═══════════════════════════════════════════════════════════════════
362
363struct ExtendedGraph {
364    node_count: usize,
365    edges: Vec<(usize, usize)>,
366    edge_to_orig: Vec<u32>,
367    layers: Vec<u32>,
368}
369
370fn build_extended_graph(graph: &Graph, layers: &[u32]) -> IgraphResult<ExtendedGraph> {
371    let n = graph.vcount() as usize;
372    let ecount = graph.ecount() as u32;
373    let mut next_node = n;
374    let mut edges: Vec<(usize, usize)> = Vec::new();
375    let mut edge_to_orig: Vec<u32> = Vec::new();
376    let mut node_layers: Vec<u32> = layers.to_vec();
377
378    for eid in 0..ecount {
379        let (src, tgt) = graph.edge(eid)?;
380        let src_idx = src as usize;
381        let tgt_idx = tgt as usize;
382        let src_layer = layers[src_idx];
383        let tgt_layer = layers[tgt_idx];
384
385        if src_layer == tgt_layer {
386            // Same-layer edge: include directly
387            edges.push((src_idx, tgt_idx));
388            edge_to_orig.push(eid);
389        } else {
390            // Determine direction: edge should go from lower to higher layer
391            let (from, to, from_layer, to_layer) = if src_layer < tgt_layer {
392                (src_idx, tgt_idx, src_layer, tgt_layer)
393            } else {
394                (tgt_idx, src_idx, tgt_layer, src_layer)
395            };
396
397            if to_layer - from_layer == 1 {
398                // Spans exactly one layer — no dummy needed
399                edges.push((from, to));
400                edge_to_orig.push(eid);
401            } else {
402                // Insert dummy nodes
403                let mut prev = from;
404                for lyr in (from_layer + 1)..to_layer {
405                    let dummy = next_node;
406                    next_node += 1;
407                    node_layers.push(lyr);
408                    edges.push((prev, dummy));
409                    edge_to_orig.push(eid);
410                    prev = dummy;
411                }
412                edges.push((prev, to));
413                edge_to_orig.push(eid);
414            }
415        }
416    }
417
418    Ok(ExtendedGraph {
419        node_count: next_node,
420        edges,
421        edge_to_orig,
422        layers: node_layers,
423    })
424}
425
426// ═══════════════════════════════════════════════════════════════════
427// Crossing minimization (barycenter heuristic)
428// ═══════════════════════════════════════════════════════════════════
429
430fn order_horizontally(
431    edges: &[(usize, usize)],
432    node_count: usize,
433    layering: &[Vec<usize>],
434    layout_x: &mut [f64],
435    maxiter: u32,
436) {
437    let num_layers = layering.len();
438    if num_layers <= 1 {
439        // Single layer — just assign sequential positions
440        for (i, &v) in layering
441            .first()
442            .map_or(&[][..], |l| l.as_slice())
443            .iter()
444            .enumerate()
445        {
446            layout_x[v] = i as f64;
447        }
448        return;
449    }
450
451    // Build adjacency: for each node, predecessors (in higher layer) and successors
452    let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
453    let mut successors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
454    for &(src, tgt) in edges {
455        successors[src].push(tgt);
456        predecessors[tgt].push(src);
457    }
458
459    // Initial ordering: sequential within each layer
460    let mut layer_order: Vec<Vec<usize>> = layering.to_vec();
461    for (layer_idx, layer) in layer_order.iter().enumerate() {
462        for (pos, &v) in layer.iter().enumerate() {
463            layout_x[v] = pos as f64;
464        }
465        let _ = layer_idx;
466    }
467
468    // Iterate: sweep down then up, reordering by barycenters
469    let mut changed = true;
470    let mut iter = 0u32;
471    while changed && iter < maxiter {
472        changed = false;
473
474        // Sweep downward: for each layer (top to bottom), sort by
475        // barycenter of predecessors
476        for layer_idx in 1..num_layers {
477            let layer = &layer_order[layer_idx];
478            let layer_size = layer.len();
479            if layer_size == 0 {
480                continue;
481            }
482
483            let mut barycenters: Vec<(f64, usize)> = Vec::with_capacity(layer_size);
484            for &v in layer {
485                let preds = &predecessors[v];
486                let bc = if preds.is_empty() {
487                    layout_x[v]
488                } else {
489                    let sum: f64 = preds.iter().map(|&p| layout_x[p]).sum();
490                    sum / preds.len() as f64
491                };
492                barycenters.push((bc, v));
493            }
494
495            barycenters.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
496
497            let new_order: Vec<usize> = barycenters.iter().map(|&(_, v)| v).collect();
498            if new_order != layer_order[layer_idx] {
499                changed = true;
500                for (pos, &v) in new_order.iter().enumerate() {
501                    layout_x[v] = pos as f64;
502                }
503                layer_order[layer_idx] = new_order;
504            }
505        }
506
507        // Sweep upward: for each layer (bottom to top), sort by
508        // barycenter of successors
509        for layer_idx in (0..num_layers.saturating_sub(1)).rev() {
510            let layer = &layer_order[layer_idx];
511            let layer_size = layer.len();
512            if layer_size == 0 {
513                continue;
514            }
515
516            let mut barycenters: Vec<(f64, usize)> = Vec::with_capacity(layer_size);
517            for &v in layer {
518                let succs = &successors[v];
519                let bc = if succs.is_empty() {
520                    layout_x[v]
521                } else {
522                    let sum: f64 = succs.iter().map(|&s| layout_x[s]).sum();
523                    sum / succs.len() as f64
524                };
525                barycenters.push((bc, v));
526            }
527
528            barycenters.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
529
530            let new_order: Vec<usize> = barycenters.iter().map(|&(_, v)| v).collect();
531            if new_order != layer_order[layer_idx] {
532                changed = true;
533                for (pos, &v) in new_order.iter().enumerate() {
534                    layout_x[v] = pos as f64;
535                }
536                layer_order[layer_idx] = new_order;
537            }
538        }
539
540        iter += 1;
541    }
542}
543
544// ═══════════════════════════════════════════════════════════════════
545// Horizontal coordinate assignment (simplified Brandes-Köpf)
546// ═══════════════════════════════════════════════════════════════════
547
548fn place_horizontally(
549    edges: &[(usize, usize)],
550    node_count: usize,
551    layering: &[Vec<usize>],
552    layout_x: &mut [f64],
553    _node_layers: &[u32],
554    hgap: f64,
555    _no_of_real_nodes: usize,
556) {
557    // Build predecessor list
558    let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
559    for &(src, tgt) in edges {
560        predecessors[tgt].push(src);
561    }
562
563    // Run 4 alignments (up-left, up-right, down-left, down-right) and
564    // take median X for each vertex
565    let mut all_xs: [Vec<f64>; 4] = [
566        vec![0.0; node_count],
567        vec![0.0; node_count],
568        vec![0.0; node_count],
569        vec![0.0; node_count],
570    ];
571
572    for combo in 0..4u8 {
573        let reverse = combo / 2 == 1;
574        let align_right = combo % 2 == 1;
575
576        // Vertical alignment
577        let mut roots = (0..node_count).collect::<Vec<_>>();
578        let mut align = (0..node_count).collect::<Vec<_>>();
579
580        vertical_alignment(
581            edges,
582            node_count,
583            layering,
584            layout_x,
585            reverse,
586            align_right,
587            &mut roots,
588            &mut align,
589            &predecessors,
590        );
591
592        // Horizontal compaction
593        horizontal_compaction(
594            node_count,
595            layering,
596            &roots,
597            &align,
598            hgap,
599            &mut all_xs[combo as usize],
600        );
601    }
602
603    // Align the 4 coordinate sets and take the median
604    let mut mins = [0.0_f64; 4];
605    let mut maxs = [0.0_f64; 4];
606    for i in 0..4 {
607        mins[i] = all_xs[i].iter().copied().fold(f64::INFINITY, f64::min);
608        maxs[i] = all_xs[i].iter().copied().fold(f64::NEG_INFINITY, f64::max);
609    }
610
611    // Find narrowest alignment
612    let mut best = 0;
613    let mut best_width = f64::INFINITY;
614    for i in 0..4 {
615        let w = maxs[i] - mins[i];
616        if w < best_width {
617            best_width = w;
618            best = i;
619        }
620    }
621
622    // Shift alignments to match
623    for i in 0..4 {
624        if i == best {
625            continue;
626        }
627        let diff = if i % 2 == 0 {
628            mins[best] - mins[i]
629        } else {
630            maxs[best] - maxs[i]
631        };
632        for x in &mut all_xs[i] {
633            *x += diff;
634        }
635    }
636
637    // Take median of 4 values for each vertex
638    for v in 0..node_count {
639        let mut vals = [all_xs[0][v], all_xs[1][v], all_xs[2][v], all_xs[3][v]];
640        vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
641        layout_x[v] = (vals[1] + vals[2]) * 0.5;
642    }
643}
644
645fn vertical_alignment(
646    edges: &[(usize, usize)],
647    node_count: usize,
648    layering: &[Vec<usize>],
649    layout_x: &[f64],
650    reverse: bool,
651    align_right: bool,
652    roots: &mut [usize],
653    align: &mut [usize],
654    predecessors: &[Vec<usize>],
655) {
656    let num_layers = layering.len();
657
658    // Build successors
659    let mut successors: Vec<Vec<usize>> = vec![Vec::new(); node_count];
660    for &(src, tgt) in edges {
661        successors[src].push(tgt);
662    }
663
664    // Initialize: each vertex is its own root and aligned to itself
665    for i in 0..node_count {
666        roots[i] = i;
667        align[i] = i;
668    }
669
670    // Process layers
671    let layer_range: Vec<usize> = if reverse {
672        (0..num_layers.saturating_sub(1)).rev().collect()
673    } else {
674        (1..num_layers).collect()
675    };
676
677    for &layer_idx in &layer_range {
678        let layer = &layering[layer_idx];
679        let mut r: i64 = if align_right { i64::MAX } else { -1 };
680
681        let vertex_iter: Vec<usize> = if align_right {
682            layer.iter().copied().rev().collect()
683        } else {
684            layer.clone()
685        };
686
687        for vertex in vertex_iter {
688            if align[vertex] != vertex {
689                continue;
690            }
691
692            // Get upper/lower neighbors depending on direction
693            let neighbors: &Vec<usize> = if reverse {
694                &successors[vertex]
695            } else {
696                &predecessors[vertex]
697            };
698
699            if neighbors.is_empty() {
700                continue;
701            }
702
703            // Sort neighbors by their X position
704            let mut sorted_neis: Vec<usize> = neighbors.clone();
705            sorted_neis.sort_by(|&a, &b| {
706                layout_x[a]
707                    .partial_cmp(&layout_x[b])
708                    .unwrap_or(std::cmp::Ordering::Equal)
709            });
710
711            // Find median neighbor(s)
712            let n_neis = sorted_neis.len();
713            let medians = if n_neis == 1 {
714                vec![sorted_neis[0]]
715            } else if n_neis % 2 == 1 {
716                vec![sorted_neis[n_neis / 2]]
717            } else if align_right {
718                vec![sorted_neis[n_neis / 2], sorted_neis[n_neis / 2 - 1]]
719            } else {
720                vec![sorted_neis[n_neis / 2 - 1], sorted_neis[n_neis / 2]]
721            };
722
723            // Try to align with median
724            for median in medians {
725                if align[vertex] != vertex {
726                    break;
727                }
728                let pos = layout_x[median] as i64;
729                if (align_right && r > pos) || (!align_right && r < pos) {
730                    align[median] = vertex;
731                    roots[vertex] = roots[median];
732                    align[vertex] = roots[median];
733                    r = pos;
734                }
735            }
736        }
737    }
738}
739
740fn horizontal_compaction(
741    node_count: usize,
742    layering: &[Vec<usize>],
743    roots: &[usize],
744    align: &[usize],
745    hgap: f64,
746    xs: &mut [f64],
747) {
748    // Build vertex_to_the_left
749    let mut vertex_to_left: Vec<usize> = (0..node_count).collect();
750    for layer in layering {
751        if layer.len() <= 1 {
752            continue;
753        }
754        for i in 1..layer.len() {
755            vertex_to_left[layer[i]] = layer[i - 1];
756        }
757    }
758
759    let mut sinks: Vec<usize> = (0..node_count).collect();
760    let mut shifts = vec![f64::INFINITY; node_count];
761
762    // Initialize xs to -1 (unplaced)
763    for x in xs.iter_mut() {
764        *x = -1.0;
765    }
766
767    // Place blocks starting from roots
768    for i in 0..node_count {
769        if roots[i] == i {
770            place_block(
771                i,
772                &vertex_to_left,
773                roots,
774                align,
775                &mut sinks,
776                &mut shifts,
777                hgap,
778                xs,
779            );
780        }
781    }
782
783    // Calculate absolute coordinates
784    let old_xs = xs.to_vec();
785    for i in 0..node_count {
786        let root = roots[i];
787        xs[i] = old_xs[root];
788        let shift = shifts[sinks[root]];
789        if shift < f64::INFINITY {
790            xs[i] += shift;
791        }
792    }
793}
794
795fn place_block(
796    v: usize,
797    vertex_to_left: &[usize],
798    roots: &[usize],
799    align: &[usize],
800    sinks: &mut [usize],
801    shifts: &mut [f64],
802    hgap: f64,
803    xs: &mut [f64],
804) {
805    if xs[v] >= 0.0 {
806        return;
807    }
808
809    xs[v] = 0.0;
810
811    let mut w = v;
812    loop {
813        let u_left = vertex_to_left[w];
814        if u_left != w {
815            let u = roots[u_left];
816            place_block(u, vertex_to_left, roots, align, sinks, shifts, hgap, xs);
817
818            let u_sink = sinks[u];
819            let v_sink = sinks[v];
820            if v_sink == v {
821                sinks[v] = u_sink;
822            }
823
824            let current_v_sink = sinks[v];
825            if current_v_sink != u_sink {
826                let candidate = xs[v] - xs[u] - hgap;
827                if shifts[u_sink] > candidate {
828                    shifts[u_sink] = candidate;
829                }
830            } else if xs[v] < xs[u] + hgap {
831                xs[v] = xs[u] + hgap;
832            }
833        }
834
835        w = align[w];
836        if w == v {
837            break;
838        }
839    }
840}
841
842// ═══════════════════════════════════════════════════════════════════
843// Helpers
844// ═══════════════════════════════════════════════════════════════════
845
846fn select_highest_degree(graph: &Graph) -> usize {
847    let n = graph.vcount() as usize;
848    if n == 0 {
849        return 0;
850    }
851    let mut best = 0;
852    let mut best_deg = 0;
853    for v in 0..n {
854        if let Ok(d) = graph.degree(v as VertexId) {
855            if d > best_deg {
856                best_deg = d;
857                best = v;
858            }
859        }
860    }
861    best
862}
863
864fn all_neighbors(graph: &Graph, v: VertexId) -> Vec<VertexId> {
865    let mut result = Vec::new();
866    let ecount = graph.ecount();
867    for eid in 0..ecount as u32 {
868        if let Ok((src, tgt)) = graph.edge(eid) {
869            if src == v && tgt != v {
870                result.push(tgt);
871            } else if tgt == v && src != v {
872                result.push(src);
873            }
874        }
875    }
876    result
877}
878
879fn out_edges_of(graph: &Graph, v: VertexId) -> Vec<(usize, VertexId)> {
880    let mut result = Vec::new();
881    let ecount = graph.ecount();
882    for eid in 0..ecount as u32 {
883        if let Ok((src, tgt)) = graph.edge(eid) {
884            if graph.is_directed() {
885                if src == v && tgt != v {
886                    result.push((eid as usize, tgt));
887                }
888            } else if src == v && tgt != v {
889                result.push((eid as usize, tgt));
890            } else if tgt == v && src != v {
891                result.push((eid as usize, src));
892            }
893        }
894    }
895    result
896}
897
898// ═══════════════════════════════════════════════════════════════════
899// Tests
900// ═══════════════════════════════════════════════════════════════════
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905
906    fn simple_dag() -> Graph {
907        // 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
908        let mut g = Graph::new(4, true).unwrap();
909        g.add_edge(0, 1).unwrap();
910        g.add_edge(0, 2).unwrap();
911        g.add_edge(1, 3).unwrap();
912        g.add_edge(2, 3).unwrap();
913        g
914    }
915
916    #[test]
917    fn test_sugiyama_basic_dag() {
918        let g = simple_dag();
919        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
920        assert_eq!(result.positions.len(), 4);
921        // Vertex 0 at top layer
922        assert!(result.positions[0][1].abs() < 1e-10);
923        // Vertex 3 at bottom layer (layer 2)
924        assert!((result.positions[3][1] - 2.0).abs() < 1e-10);
925        // Vertices 1 and 2 at middle layer
926        assert!((result.positions[1][1] - 1.0).abs() < 1e-10);
927        assert!((result.positions[2][1] - 1.0).abs() < 1e-10);
928    }
929
930    #[test]
931    fn test_sugiyama_empty() {
932        let g = Graph::new(0, true).unwrap();
933        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
934        assert!(result.positions.is_empty());
935    }
936
937    #[test]
938    fn test_sugiyama_single_vertex() {
939        let g = Graph::new(1, true).unwrap();
940        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
941        assert_eq!(result.positions.len(), 1);
942        assert!(result.positions[0][0].abs() < 1e-10);
943        assert!(result.positions[0][1].abs() < 1e-10);
944    }
945
946    #[test]
947    fn test_sugiyama_linear_chain() {
948        // 0 -> 1 -> 2 -> 3
949        let mut g = Graph::new(4, true).unwrap();
950        g.add_edge(0, 1).unwrap();
951        g.add_edge(1, 2).unwrap();
952        g.add_edge(2, 3).unwrap();
953        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
954        // Each vertex should be on a successive layer
955        for i in 0..4 {
956            assert!(
957                (result.positions[i][1] - i as f64).abs() < 1e-10,
958                "vertex {i} y={}, expected {i}",
959                result.positions[i][1]
960            );
961        }
962    }
963
964    #[test]
965    fn test_sugiyama_with_layers() {
966        let g = simple_dag();
967        let layers = vec![0, 1, 1, 2];
968        let result = layout_sugiyama(&g, Some(&layers), &SugiyamaParams::default()).unwrap();
969        assert_eq!(result.positions.len(), 4);
970        assert!(result.positions[0][1].abs() < 1e-10);
971        assert!((result.positions[3][1] - 2.0).abs() < 1e-10);
972    }
973
974    #[test]
975    fn test_sugiyama_invalid_layers() {
976        let g = simple_dag();
977        let layers = vec![0, 1]; // Wrong length
978        let result = layout_sugiyama(&g, Some(&layers), &SugiyamaParams::default());
979        assert!(result.is_err());
980    }
981
982    #[test]
983    fn test_sugiyama_long_edge_dummies() {
984        // 0 -> 1, 0 -> 2, with layers [0, 2, 1]
985        // Edge 0->1 spans 2 layers, needs a dummy
986        let mut g = Graph::new(3, true).unwrap();
987        g.add_edge(0, 1).unwrap();
988        g.add_edge(0, 2).unwrap();
989        let layers = vec![0, 2, 1];
990        let result = layout_sugiyama(&g, Some(&layers), &SugiyamaParams::default()).unwrap();
991        assert_eq!(result.positions.len(), 3);
992        // Should have one dummy for the 0->1 edge
993        assert_eq!(result.dummy_positions.len(), 1);
994        // Dummy should be at layer 1
995        assert!((result.dummy_positions[0][1] - 1.0).abs() < 1e-10);
996    }
997
998    #[test]
999    fn test_sugiyama_diamond_symmetry() {
1000        let g = simple_dag();
1001        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
1002        // Vertices 1 and 2 should be symmetric around vertex 0's x
1003        let mid_x = result.positions[0][0];
1004        let left = result.positions[1][0] - mid_x;
1005        let right = result.positions[2][0] - mid_x;
1006        assert!(
1007            (left + right).abs() < 1e-10,
1008            "not symmetric: left={left}, right={right}"
1009        );
1010    }
1011
1012    #[test]
1013    fn test_sugiyama_no_overlap_same_layer() {
1014        let g = simple_dag();
1015        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
1016        // Vertices 1 and 2 are on the same layer — should not overlap
1017        let x1 = result.positions[1][0];
1018        let x2 = result.positions[2][0];
1019        assert!((x1 - x2).abs() >= 1.0 - 1e-10, "overlap: x1={x1}, x2={x2}");
1020    }
1021
1022    #[test]
1023    fn test_sugiyama_undirected() {
1024        let mut g = Graph::with_vertices(4);
1025        g.add_edge(0, 1).unwrap();
1026        g.add_edge(1, 2).unwrap();
1027        g.add_edge(2, 3).unwrap();
1028        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
1029        assert_eq!(result.positions.len(), 4);
1030        // All positions should be finite
1031        for p in &result.positions {
1032            assert!(p[0].is_finite());
1033            assert!(p[1].is_finite());
1034        }
1035    }
1036
1037    #[test]
1038    fn test_sugiyama_with_cycle() {
1039        // 0 -> 1 -> 2 -> 0 (cycle)
1040        let mut g = Graph::new(3, true).unwrap();
1041        g.add_edge(0, 1).unwrap();
1042        g.add_edge(1, 2).unwrap();
1043        g.add_edge(2, 0).unwrap();
1044        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
1045        assert_eq!(result.positions.len(), 3);
1046        // Should produce valid layout despite cycle
1047        for p in &result.positions {
1048            assert!(p[0].is_finite());
1049            assert!(p[1].is_finite());
1050        }
1051    }
1052
1053    #[test]
1054    fn test_sugiyama_custom_gaps() {
1055        let g = simple_dag();
1056        let params = SugiyamaParams {
1057            hgap: 2.0,
1058            vgap: 3.0,
1059            maxiter: 50,
1060        };
1061        let result = layout_sugiyama(&g, None, &params).unwrap();
1062        // Vertex 3 should be at y = 2 * vgap = 6.0
1063        assert!((result.positions[3][1] - 6.0).abs() < 1e-10);
1064        // Vertices 1 and 2 should be at least hgap=2.0 apart
1065        let x1 = result.positions[1][0];
1066        let x2 = result.positions[2][0];
1067        assert!(
1068            (x1 - x2).abs() >= 2.0 - 1e-10,
1069            "gap too small: |{x1} - {x2}| = {}",
1070            (x1 - x2).abs()
1071        );
1072    }
1073
1074    #[test]
1075    fn test_sugiyama_extended_graph() {
1076        let g = simple_dag();
1077        let result = layout_sugiyama(&g, None, &SugiyamaParams::default()).unwrap();
1078        // Extended graph should have at least as many vertices as original
1079        assert!(result.extended_graph.vcount() >= g.vcount());
1080        // Each extended edge should map to a valid original edge
1081        for &orig_eid in &result.extended_to_orig_eids {
1082            assert!(orig_eid < g.ecount() as u32);
1083        }
1084    }
1085}