Skip to main content

rust_igraph/algorithms/layout/
reingold_tilford.rs

1//! Reingold-Tilford tree layout (ALGO-LO-004).
2//!
3//! Layered tree drawing algorithm. Reference: Reingold & Tilford,
4//! "Tidier Drawing of Trees", IEEE Trans. Softw. Eng. SE-7(2), 1981.
5//!
6//! Also provides a circular variant that maps the tree layout to
7//! polar coordinates.
8
9use crate::core::graph::EdgeId;
10use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
11use std::collections::VecDeque;
12
13/// Mode for tree edge traversal direction.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum RtMode {
16    /// Follow outgoing edges from parent to children.
17    Out,
18    /// Follow incoming edges (parent is target of edges).
19    In,
20    /// Treat edges as undirected.
21    All,
22}
23
24/// Compute the Reingold-Tilford tree layout.
25///
26/// Places vertices in a layered hierarchy with the root at level 0.
27/// Y-coordinate is the depth in the tree; X-coordinate is computed
28/// using the contour-merging algorithm to keep subtrees compact.
29///
30/// If the graph is not a tree, a BFS spanning tree from `root` is used.
31/// Unreachable vertices are connected to the root before layout.
32///
33/// # Arguments
34///
35/// * `graph` — input graph.
36/// * `root` — root vertex for the tree layout. If `None`, automatically
37///   selects the vertex with highest degree.
38/// * `mode` — edge traversal direction.
39///
40/// Returns `(x, y)` pairs for each vertex.
41///
42/// # Examples
43///
44/// ```
45/// use rust_igraph::{Graph, layout_reingold_tilford, RtMode};
46///
47/// // Binary tree: 0 -> {1, 2}, 1 -> {3, 4}
48/// let mut g = Graph::with_vertices(5);
49/// g.add_edge(0, 1).unwrap();
50/// g.add_edge(0, 2).unwrap();
51/// g.add_edge(1, 3).unwrap();
52/// g.add_edge(1, 4).unwrap();
53///
54/// let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
55/// assert_eq!(pos.len(), 5);
56/// // Root should be at level 0
57/// assert!((pos[0][1]).abs() < 1e-10);
58/// ```
59pub fn layout_reingold_tilford(
60    graph: &Graph,
61    root: Option<VertexId>,
62    mode: RtMode,
63) -> IgraphResult<Vec<[f64; 2]>> {
64    let n = graph.vcount() as usize;
65    if n == 0 {
66        return Ok(Vec::new());
67    }
68
69    let root_v = root.unwrap_or_else(|| select_root(graph, mode));
70    if root_v >= graph.vcount() {
71        return Err(IgraphError::InvalidArgument(
72            "root vertex out of range".into(),
73        ));
74    }
75
76    let root_idx = root_v as usize;
77    let mut vdata = vec![RtVertex::new(); n];
78
79    // Build spanning tree via BFS
80    build_spanning_tree(graph, root_idx, mode, &mut vdata)?;
81
82    // Postorder traversal to compute offsets
83    postorder(&mut vdata, root_idx, n);
84
85    // Calculate final coordinates
86    let mut pos = vec![[0.0_f64; 2]; n];
87    calc_coords(&vdata, &mut pos, root_idx, n, vdata[root_idx].offset);
88
89    Ok(pos)
90}
91
92/// Circular variant of the Reingold-Tilford layout.
93///
94/// First computes the standard RT layout, then maps X to angle and Y
95/// to radius in polar coordinates. The root ends up at the center.
96///
97/// # Examples
98///
99/// ```
100/// use rust_igraph::{Graph, layout_reingold_tilford_circular, RtMode};
101///
102/// let mut g = Graph::with_vertices(7);
103/// for &(u, v) in &[(0,1),(0,2),(1,3),(1,4),(2,5),(2,6)] {
104///     g.add_edge(u, v).unwrap();
105/// }
106///
107/// let pos = layout_reingold_tilford_circular(&g, Some(0), RtMode::All).unwrap();
108/// assert_eq!(pos.len(), 7);
109/// ```
110pub fn layout_reingold_tilford_circular(
111    graph: &Graph,
112    root: Option<VertexId>,
113    mode: RtMode,
114) -> IgraphResult<Vec<[f64; 2]>> {
115    let mut pos = layout_reingold_tilford(graph, root, mode)?;
116    let n = pos.len();
117    if n == 0 {
118        return Ok(pos);
119    }
120
121    let ratio_base = 2.0 * std::f64::consts::PI * (n as f64 - 1.0) / n as f64;
122
123    let minx = pos.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
124    let maxx = pos.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max);
125
126    let ratio = if maxx > minx {
127        ratio_base / (maxx - minx)
128    } else {
129        ratio_base
130    };
131
132    for p in &mut pos {
133        let phi = (p[0] - minx) * ratio;
134        let r = p[1];
135        p[0] = r * phi.cos();
136        p[1] = r * phi.sin();
137    }
138
139    Ok(pos)
140}
141
142// ═══════════════════════════════════════════════════════════════════
143// Internal data structures
144// ═══════════════════════════════════════════════════════════════════
145
146#[derive(Clone)]
147struct RtVertex {
148    parent: i64,
149    level: i64,
150    offset: f64,
151    left_contour: i64,
152    right_contour: i64,
153    offset_to_left_contour: f64,
154    offset_to_right_contour: f64,
155    left_extreme: usize,
156    right_extreme: usize,
157    offset_to_left_extreme: f64,
158    offset_to_right_extreme: f64,
159}
160
161impl RtVertex {
162    fn new() -> Self {
163        Self {
164            parent: -1,
165            level: -1,
166            offset: 0.0,
167            left_contour: -1,
168            right_contour: -1,
169            offset_to_left_contour: 0.0,
170            offset_to_right_contour: 0.0,
171            left_extreme: 0, // will be set to self-index
172            right_extreme: 0,
173            offset_to_left_extreme: 0.0,
174            offset_to_right_extreme: 0.0,
175        }
176    }
177}
178
179// ═══════════════════════════════════════════════════════════════════
180// Helpers
181// ═══════════════════════════════════════════════════════════════════
182
183fn select_root(graph: &Graph, _mode: RtMode) -> VertexId {
184    let n = graph.vcount() as usize;
185    if n == 0 {
186        return 0;
187    }
188    let mut best = 0u32;
189    let mut best_deg = 0usize;
190    for v in 0..n {
191        if let Ok(deg) = graph.degree(v as VertexId) {
192            if deg > best_deg {
193                best_deg = deg;
194                best = v as u32;
195            }
196        }
197    }
198    best
199}
200
201fn build_spanning_tree(
202    graph: &Graph,
203    root: usize,
204    mode: RtMode,
205    vdata: &mut [RtVertex],
206) -> IgraphResult<()> {
207    let n = vdata.len();
208    for i in 0..n {
209        vdata[i].left_extreme = i;
210        vdata[i].right_extreme = i;
211    }
212
213    vdata[root].parent = root as i64;
214    vdata[root].level = 0;
215
216    let mut queue = VecDeque::new();
217    queue.push_back((root, 0i64));
218
219    while let Some((node, dist)) = queue.pop_front() {
220        let neighbors = get_neighbors(graph, node as VertexId, mode);
221        for &nei in &neighbors {
222            let nei_idx = nei as usize;
223            if vdata[nei_idx].parent >= 0 {
224                continue;
225            }
226            vdata[nei_idx].parent = node as i64;
227            vdata[nei_idx].level = dist + 1;
228            queue.push_back((nei_idx, dist + 1));
229        }
230    }
231
232    // Handle unreachable nodes: connect them to root
233    for i in 0..n {
234        if vdata[i].parent < 0 {
235            vdata[i].parent = root as i64;
236            vdata[i].level = 1;
237        }
238    }
239
240    Ok(())
241}
242
243fn get_neighbors(graph: &Graph, v: VertexId, mode: RtMode) -> Vec<VertexId> {
244    let mut result = Vec::new();
245    let ecount = graph.ecount();
246    for eid in 0..ecount as u32 {
247        if let Ok((src, tgt)) = graph.edge(eid) {
248            match mode {
249                RtMode::Out => {
250                    if src == v {
251                        result.push(tgt);
252                    }
253                }
254                RtMode::In => {
255                    if tgt == v {
256                        result.push(src);
257                    }
258                }
259                RtMode::All => {
260                    if src == v {
261                        result.push(tgt);
262                    } else if tgt == v {
263                        result.push(src);
264                    }
265                }
266            }
267        }
268    }
269    result
270}
271
272fn children_of(vdata: &[RtVertex], node: usize, n: usize) -> Vec<usize> {
273    let mut kids = Vec::new();
274    for i in 0..n {
275        if i == node {
276            continue;
277        }
278        if vdata[i].parent == node as i64 {
279            kids.push(i);
280        }
281    }
282    kids
283}
284
285fn postorder(vdata: &mut [RtVertex], node: usize, n: usize) {
286    let children = children_of(vdata, node, n);
287
288    if children.is_empty() {
289        return;
290    }
291
292    // Recursively layout each child's subtree
293    for &child in &children {
294        postorder(vdata, child, n);
295    }
296
297    // Place subtrees next to each other
298    let minsep = 1.0_f64;
299    let mut leftroot: i64 = -1;
300    let mut avg = 0.0_f64;
301
302    for (j, &child) in children.iter().enumerate() {
303        if leftroot >= 0 {
304            let lr = leftroot as usize;
305            // Follow right contour of leftroot and left contour of child
306            let mut lnode: i64 = lr as i64;
307            let mut rnode: i64 = child as i64;
308            let mut rootsep = vdata[lr].offset + minsep;
309            let mut loffset = vdata[lr].offset;
310            let mut roffset = loffset + minsep;
311
312            // Update the right contour of the node being built
313            vdata[node].right_contour = child as i64;
314            vdata[node].offset_to_right_contour = rootsep;
315
316            while lnode >= 0 && rnode >= 0 {
317                let ln = lnode as usize;
318                let rn = rnode as usize;
319
320                // Step right contour of left subtree
321                if vdata[ln].right_contour >= 0 {
322                    loffset += vdata[ln].offset_to_right_contour;
323                    lnode = vdata[ln].right_contour;
324                } else {
325                    // Left subtree ended — threading
326                    if vdata[rn].left_contour >= 0 {
327                        let auxnode = vdata[node].left_extreme;
328                        let newoffset = (vdata[node].offset_to_right_extreme
329                            - vdata[node].offset_to_left_extreme)
330                            + minsep
331                            + vdata[rn].offset_to_left_contour;
332                        vdata[auxnode].left_contour = vdata[rn].left_contour;
333                        vdata[auxnode].right_contour = vdata[rn].left_contour;
334                        vdata[auxnode].offset_to_left_contour = newoffset;
335                        vdata[auxnode].offset_to_right_contour = newoffset;
336
337                        vdata[node].left_extreme = vdata[child].left_extreme;
338                        vdata[node].right_extreme = vdata[child].right_extreme;
339                        vdata[node].offset_to_left_extreme =
340                            vdata[child].offset_to_left_extreme + rootsep;
341                        vdata[node].offset_to_right_extreme =
342                            vdata[child].offset_to_right_extreme + rootsep;
343                    } else {
344                        // Both subtrees end simultaneously
345                        vdata[node].right_extreme = vdata[child].right_extreme;
346                        vdata[node].offset_to_right_extreme =
347                            vdata[child].offset_to_right_extreme + rootsep;
348                    }
349                    lnode = -1;
350                }
351
352                // Step left contour of right subtree
353                let rn = rnode as usize;
354                if rnode >= 0 && vdata[rn].left_contour >= 0 {
355                    roffset += vdata[rn].offset_to_left_contour;
356                    rnode = vdata[rn].left_contour;
357                } else if rnode >= 0 {
358                    // Right subtree ended — threading
359                    if lnode >= 0 {
360                        let auxnode = vdata[child].right_extreme;
361                        let newoffset = loffset - rootsep - vdata[child].offset_to_right_extreme;
362                        vdata[auxnode].left_contour = lnode;
363                        vdata[auxnode].right_contour = lnode;
364                        vdata[auxnode].offset_to_left_contour = newoffset;
365                        vdata[auxnode].offset_to_right_contour = newoffset;
366                    }
367                    rnode = -1;
368                }
369
370                // Push subtrees apart if too close
371                if lnode >= 0 && rnode >= 0 && (roffset - loffset < minsep) {
372                    rootsep += minsep - roffset + loffset;
373                    roffset = loffset + minsep;
374                    vdata[node].offset_to_right_contour = rootsep;
375                }
376            }
377
378            vdata[child].offset = rootsep;
379            vdata[node].offset_to_right_contour = rootsep;
380            avg = (avg * j as f64) / (j as f64 + 1.0) + rootsep / (j as f64 + 1.0);
381            leftroot = child as i64;
382        } else {
383            // First child
384            leftroot = child as i64;
385            vdata[node].left_contour = child as i64;
386            vdata[node].right_contour = child as i64;
387            vdata[node].offset_to_left_contour = 0.0;
388            vdata[node].offset_to_right_contour = 0.0;
389            vdata[node].left_extreme = vdata[child].left_extreme;
390            vdata[node].right_extreme = vdata[child].right_extreme;
391            vdata[node].offset_to_left_extreme = vdata[child].offset_to_left_extreme;
392            vdata[node].offset_to_right_extreme = vdata[child].offset_to_right_extreme;
393            avg = vdata[child].offset;
394        }
395    }
396
397    // Center parent above children
398    vdata[node].offset_to_left_contour -= avg;
399    vdata[node].offset_to_right_contour -= avg;
400    vdata[node].offset_to_left_extreme -= avg;
401    vdata[node].offset_to_right_extreme -= avg;
402    for &child in &children {
403        vdata[child].offset -= avg;
404    }
405}
406
407fn calc_coords(vdata: &[RtVertex], pos: &mut [[f64; 2]], node: usize, n: usize, xpos: f64) {
408    pos[node][0] = xpos;
409    pos[node][1] = vdata[node].level as f64;
410    for i in 0..n {
411        if i == node {
412            continue;
413        }
414        if vdata[i].parent == node as i64 {
415            calc_coords(vdata, pos, i, n, xpos + vdata[i].offset);
416        }
417    }
418}
419
420// ═══════════════════════════════════════════════════════════════════
421// Root selection heuristic
422// ═══════════════════════════════════════════════════════════════════
423
424/// Heuristic for selecting tree-layout roots when multiple candidates
425/// exist within a component.
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum RootChoice {
428    /// Pick the vertex with the highest degree (out- or in-degree for
429    /// directed graphs). Fast — O(n) after sorting.
430    Degree,
431    /// Pick the vertex with the lowest eccentricity ("most central").
432    /// Produces wide, shallow layouts. Slow — O(n²) eccentricity BFS.
433    Eccentricity,
434}
435
436/// Choose "nice" roots for a tree layout.
437///
438/// Returns one root per reachable component so that every vertex is
439/// reachable from at least one root.
440///
441/// - **Undirected** (or `mode == RtMode::All`): one root per weak
442///   connected component — the vertex with the highest degree (or
443///   lowest eccentricity).
444/// - **Directed**: strongly connected components with no incoming
445///   (when `mode == RtMode::Out`) or no outgoing (when `mode ==
446///   RtMode::In`) edges get one root each. Components that *do*
447///   have such edges are omitted — they are reachable from some
448///   root component.
449///
450/// Counterpart of `igraph_roots_for_tree_layout()` from
451/// `references/igraph/src/layout/reingold_tilford.c:536–660`.
452///
453/// # Examples
454///
455/// ```
456/// use rust_igraph::{Graph, roots_for_tree_layout, RtMode, RootChoice};
457///
458/// // Simple undirected tree: 0-1-2-3-4
459/// let mut g = Graph::with_vertices(5);
460/// g.add_edge(0, 1).unwrap();
461/// g.add_edge(1, 2).unwrap();
462/// g.add_edge(2, 3).unwrap();
463/// g.add_edge(3, 4).unwrap();
464///
465/// let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
466/// // One root, should be vertex 1 or 2 or 3 (degree 2 — the hubs of a path)
467/// assert_eq!(roots.len(), 1);
468/// assert!([1, 2, 3].contains(&roots[0]));
469///
470/// // Disconnected: two components
471/// let mut g2 = Graph::with_vertices(4);
472/// g2.add_edge(0, 1).unwrap();
473/// g2.add_edge(2, 3).unwrap();
474/// let roots2 = roots_for_tree_layout(&g2, RtMode::All, RootChoice::Degree).unwrap();
475/// assert_eq!(roots2.len(), 2);
476/// ```
477pub fn roots_for_tree_layout(
478    graph: &Graph,
479    mode: RtMode,
480    heuristic: RootChoice,
481) -> IgraphResult<Vec<VertexId>> {
482    let n = graph.vcount();
483    if n == 0 {
484        return Ok(Vec::new());
485    }
486
487    let effective_mode = if graph.is_directed() {
488        mode
489    } else {
490        RtMode::All
491    };
492
493    let order = build_vertex_order(graph, effective_mode, heuristic)?;
494
495    if effective_mode == RtMode::All {
496        roots_undirected(graph, &order)
497    } else {
498        roots_directed(graph, effective_mode, &order)
499    }
500}
501
502fn build_vertex_order(
503    graph: &Graph,
504    mode: RtMode,
505    heuristic: RootChoice,
506) -> IgraphResult<Vec<VertexId>> {
507    let n = graph.vcount() as usize;
508    match heuristic {
509        RootChoice::Eccentricity => {
510            let ecc_mode = match mode {
511                RtMode::Out => crate::algorithms::paths::radii::EccMode::Out,
512                RtMode::In => crate::algorithms::paths::radii::EccMode::In,
513                RtMode::All => crate::algorithms::paths::radii::EccMode::All,
514            };
515            let ecc = crate::algorithms::paths::radii::eccentricity_with_mode(graph, ecc_mode)?;
516            let mut indices: Vec<VertexId> = (0..n as VertexId).collect();
517            indices.sort_by(|&a, &b| ecc[a as usize].cmp(&ecc[b as usize]));
518            Ok(indices)
519        }
520        RootChoice::Degree => {
521            let deg_mode = match mode {
522                RtMode::Out => crate::algorithms::properties::degree::DegreeMode::Out,
523                RtMode::In => crate::algorithms::properties::degree::DegreeMode::In,
524                RtMode::All => crate::algorithms::properties::degree::DegreeMode::All,
525            };
526            Ok(
527                crate::algorithms::properties::sort_by_degree::sort_vertices_by_degree(
528                    graph,
529                    deg_mode,
530                    crate::algorithms::properties::sort_by_degree::SortOrder::Descending,
531                )?,
532            )
533        }
534    }
535}
536
537fn roots_undirected(graph: &Graph, order: &[VertexId]) -> IgraphResult<Vec<VertexId>> {
538    let comps = crate::algorithms::connectivity::components::connected_components(graph)?;
539    let no_comps = comps.count as usize;
540
541    let mut roots = vec![u32::MAX; no_comps];
542    let mut seen = 0usize;
543
544    for &v in order {
545        let cl = comps.membership[v as usize] as usize;
546        if roots[cl] == u32::MAX {
547            roots[cl] = v;
548            seen += 1;
549            if seen == no_comps {
550                break;
551            }
552        }
553    }
554
555    Ok(roots)
556}
557
558fn roots_directed(graph: &Graph, mode: RtMode, order: &[VertexId]) -> IgraphResult<Vec<VertexId>> {
559    let comps = crate::algorithms::connectivity::strong::strongly_connected_components(graph)?;
560    let no_comps = comps.count as usize;
561
562    let cluster_incoming = cluster_cross_degrees(graph, &comps.membership, no_comps, mode)?;
563
564    let mut roots: Vec<Option<VertexId>> = vec![None; no_comps];
565
566    for &v in order {
567        let cl = comps.membership[v as usize] as usize;
568        if cluster_incoming[cl] == 0 && roots[cl].is_none() {
569            roots[cl] = Some(v);
570        }
571    }
572
573    Ok(roots.into_iter().flatten().collect())
574}
575
576/// For each SCC, count edges arriving from *other* SCCs in the
577/// reverse direction — i.e. if `mode == Out`, count incoming
578/// cross-component edges; if `mode == In`, count outgoing ones.
579fn cluster_cross_degrees(
580    graph: &Graph,
581    membership: &[u32],
582    no_comps: usize,
583    mode: RtMode,
584) -> IgraphResult<Vec<u32>> {
585    let mut degrees = vec![0u32; no_comps];
586    let m = graph.ecount();
587
588    for eid in 0..m as EdgeId {
589        let (from, to) = graph.edge(eid)?;
590        let from_cl = membership[from as usize];
591        let to_cl = membership[to as usize];
592
593        if from_cl != to_cl {
594            let cl = if mode == RtMode::Out { to_cl } else { from_cl };
595            degrees[cl as usize] = degrees[cl as usize].saturating_add(1);
596        }
597    }
598
599    Ok(degrees)
600}
601
602// ═══════════════════════════════════════════════════════════════════
603// Tests
604// ═══════════════════════════════════════════════════════════════════
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    fn binary_tree() -> Graph {
611        // 0 -> {1, 2}, 1 -> {3, 4}, 2 -> {5, 6}
612        let mut g = Graph::with_vertices(7);
613        g.add_edge(0, 1).unwrap();
614        g.add_edge(0, 2).unwrap();
615        g.add_edge(1, 3).unwrap();
616        g.add_edge(1, 4).unwrap();
617        g.add_edge(2, 5).unwrap();
618        g.add_edge(2, 6).unwrap();
619        g
620    }
621
622    #[test]
623    fn test_rt_binary_tree_levels() {
624        let g = binary_tree();
625        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
626        // Root at level 0
627        assert!((pos[0][1]).abs() < 1e-10);
628        // Children at level 1
629        assert!((pos[1][1] - 1.0).abs() < 1e-10);
630        assert!((pos[2][1] - 1.0).abs() < 1e-10);
631        // Grandchildren at level 2
632        assert!((pos[3][1] - 2.0).abs() < 1e-10);
633        assert!((pos[6][1] - 2.0).abs() < 1e-10);
634    }
635
636    #[test]
637    fn test_rt_binary_tree_symmetry() {
638        let g = binary_tree();
639        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
640        // Left subtree mirror of right
641        let root_x = pos[0][0];
642        let left_child_x = pos[1][0];
643        let right_child_x = pos[2][0];
644        // Children should be symmetric around root
645        let diff = (left_child_x - root_x) + (right_child_x - root_x);
646        assert!(diff.abs() < 1e-10, "children not symmetric: diff={diff}");
647    }
648
649    #[test]
650    fn test_rt_path() {
651        // Path: 0 - 1 - 2 - 3
652        let mut g = Graph::with_vertices(4);
653        g.add_edge(0, 1).unwrap();
654        g.add_edge(1, 2).unwrap();
655        g.add_edge(2, 3).unwrap();
656        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
657        // All at same x (linear tree)
658        for i in 0..4 {
659            assert!((pos[i][0] - pos[0][0]).abs() < 1e-10);
660            assert!((pos[i][1] - i as f64).abs() < 1e-10);
661        }
662    }
663
664    #[test]
665    fn test_rt_single_vertex() {
666        let g = Graph::with_vertices(1);
667        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
668        assert_eq!(pos.len(), 1);
669        assert!((pos[0][0]).abs() < 1e-10);
670        assert!((pos[0][1]).abs() < 1e-10);
671    }
672
673    #[test]
674    fn test_rt_empty() {
675        let g = Graph::with_vertices(0);
676        let pos = layout_reingold_tilford(&g, None, RtMode::All).unwrap();
677        assert!(pos.is_empty());
678    }
679
680    #[test]
681    fn test_rt_star() {
682        // Star: 0 connected to 1,2,3,4
683        let mut g = Graph::with_vertices(5);
684        for i in 1..5 {
685            g.add_edge(0, i).unwrap();
686        }
687        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
688        // All leaves at level 1
689        for i in 1..5 {
690            assert!((pos[i][1] - 1.0).abs() < 1e-10);
691        }
692        // Leaves should be evenly spaced (separation 1.0)
693        let mut xs: Vec<f64> = (1..5).map(|i| pos[i][0]).collect();
694        xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
695        for i in 1..xs.len() {
696            assert!((xs[i] - xs[i - 1] - 1.0).abs() < 1e-10);
697        }
698    }
699
700    #[test]
701    fn test_rt_disconnected() {
702        // Two components: 0-1 and 2-3
703        let mut g = Graph::with_vertices(4);
704        g.add_edge(0, 1).unwrap();
705        g.add_edge(2, 3).unwrap();
706        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
707        assert_eq!(pos.len(), 4);
708        // All positions should be finite
709        for p in &pos {
710            assert!(p[0].is_finite());
711            assert!(p[1].is_finite());
712        }
713    }
714
715    #[test]
716    fn test_rt_circular_basic() {
717        let g = binary_tree();
718        let pos = layout_reingold_tilford_circular(&g, Some(0), RtMode::All).unwrap();
719        assert_eq!(pos.len(), 7);
720        // Root is at center (level 0 → radius 0)
721        assert!(pos[0][0].abs() < 1e-10);
722        assert!(pos[0][1].abs() < 1e-10);
723    }
724
725    #[test]
726    fn test_rt_no_overlap() {
727        let g = binary_tree();
728        let pos = layout_reingold_tilford(&g, Some(0), RtMode::All).unwrap();
729        // No two nodes at the same level should overlap in x
730        for i in 0..pos.len() {
731            for j in (i + 1)..pos.len() {
732                if (pos[i][1] - pos[j][1]).abs() < 1e-10 {
733                    assert!(
734                        (pos[i][0] - pos[j][0]).abs() >= 1.0 - 1e-10,
735                        "nodes {i} and {j} overlap: x[{i}]={}, x[{j}]={}",
736                        pos[i][0],
737                        pos[j][0]
738                    );
739                }
740            }
741        }
742    }
743
744    #[test]
745    fn test_rt_auto_root() {
746        let g = binary_tree();
747        let pos = layout_reingold_tilford(&g, None, RtMode::All).unwrap();
748        assert_eq!(pos.len(), 7);
749        // Auto-selects vertex with highest degree (vertex 1, degree 3)
750        assert!((pos[1][1]).abs() < 1e-10);
751    }
752
753    #[test]
754    fn test_rt_invalid_root() {
755        let g = Graph::with_vertices(3);
756        let result = layout_reingold_tilford(&g, Some(99), RtMode::All);
757        assert!(result.is_err());
758    }
759
760    // ---- roots_for_tree_layout ----
761
762    #[test]
763    fn roots_empty_graph() {
764        let g = Graph::with_vertices(0);
765        let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
766        assert!(roots.is_empty());
767    }
768
769    #[test]
770    fn roots_single_vertex() {
771        let g = Graph::with_vertices(1);
772        let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
773        assert_eq!(roots, vec![0]);
774    }
775
776    #[test]
777    fn roots_undirected_single_component() {
778        // Path: 0-1-2-3-4 → highest degree is 1,2,3 (degree 2)
779        let mut g = Graph::with_vertices(5);
780        g.add_edge(0, 1).unwrap();
781        g.add_edge(1, 2).unwrap();
782        g.add_edge(2, 3).unwrap();
783        g.add_edge(3, 4).unwrap();
784        let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
785        assert_eq!(roots.len(), 1);
786        // Root should have degree 2 (not a leaf)
787        assert!(
788            [1, 2, 3].contains(&roots[0]),
789            "expected interior vertex, got {}",
790            roots[0]
791        );
792    }
793
794    #[test]
795    fn roots_undirected_two_components() {
796        // Component 0: 0-1, Component 1: 2-3-4
797        let mut g = Graph::with_vertices(5);
798        g.add_edge(0, 1).unwrap();
799        g.add_edge(2, 3).unwrap();
800        g.add_edge(3, 4).unwrap();
801        let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
802        assert_eq!(roots.len(), 2);
803        let mut sorted = roots.clone();
804        sorted.sort_unstable();
805        // One root from {0,1}, one from {2,3,4}
806        assert!(sorted[0] <= 1);
807        assert!(sorted[1] >= 2 && sorted[1] <= 4);
808    }
809
810    #[test]
811    fn roots_eccentricity_path() {
812        // Path 0-1-2-3-4: eccentricity centre is vertex 2
813        let mut g = Graph::with_vertices(5);
814        g.add_edge(0, 1).unwrap();
815        g.add_edge(1, 2).unwrap();
816        g.add_edge(2, 3).unwrap();
817        g.add_edge(3, 4).unwrap();
818        let roots = roots_for_tree_layout(&g, RtMode::All, RootChoice::Eccentricity).unwrap();
819        assert_eq!(roots.len(), 1);
820        assert_eq!(roots[0], 2, "eccentricity centre of P5 is vertex 2");
821    }
822
823    #[test]
824    fn roots_directed_dag() {
825        // DAG: 0→1→3, 0→2→3
826        let mut g = Graph::new(4, true).unwrap();
827        g.add_edge(0, 1).unwrap();
828        g.add_edge(0, 2).unwrap();
829        g.add_edge(1, 3).unwrap();
830        g.add_edge(2, 3).unwrap();
831        let roots = roots_for_tree_layout(&g, RtMode::Out, RootChoice::Degree).unwrap();
832        // Vertex 0 is the only source — no incoming edges
833        assert_eq!(roots, vec![0]);
834    }
835
836    #[test]
837    fn roots_directed_two_sources() {
838        // 0→2, 1→2  — two source SCCs (0 and 1), one sink (2)
839        let mut g = Graph::new(3, true).unwrap();
840        g.add_edge(0, 2).unwrap();
841        g.add_edge(1, 2).unwrap();
842        let roots = roots_for_tree_layout(&g, RtMode::Out, RootChoice::Degree).unwrap();
843        assert_eq!(roots.len(), 2);
844        let mut sorted = roots.clone();
845        sorted.sort_unstable();
846        assert_eq!(sorted, vec![0, 1]);
847    }
848
849    #[test]
850    fn roots_directed_in_mode() {
851        // 0→1→2  — with In mode, the "sink" (2) should be root
852        let mut g = Graph::new(3, true).unwrap();
853        g.add_edge(0, 1).unwrap();
854        g.add_edge(1, 2).unwrap();
855        let roots = roots_for_tree_layout(&g, RtMode::In, RootChoice::Degree).unwrap();
856        assert_eq!(roots, vec![2]);
857    }
858
859    #[test]
860    fn roots_directed_cycle_scc() {
861        // Cycle 0→1→2→0 plus 3→0 — the cycle SCC {0,1,2} has incoming
862        // from SCC {3}. SCC {3} has no incoming → it's the root.
863        let mut g = Graph::new(4, true).unwrap();
864        g.add_edge(0, 1).unwrap();
865        g.add_edge(1, 2).unwrap();
866        g.add_edge(2, 0).unwrap();
867        g.add_edge(3, 0).unwrap();
868        let roots = roots_for_tree_layout(&g, RtMode::Out, RootChoice::Degree).unwrap();
869        assert_eq!(roots, vec![3]);
870    }
871
872    #[test]
873    fn roots_undirected_ignores_mode() {
874        // Even if we pass Out mode, undirected should use All (weak comps)
875        let mut g = Graph::with_vertices(3);
876        g.add_edge(0, 1).unwrap();
877        g.add_edge(1, 2).unwrap();
878        let roots_out = roots_for_tree_layout(&g, RtMode::Out, RootChoice::Degree).unwrap();
879        let roots_all = roots_for_tree_layout(&g, RtMode::All, RootChoice::Degree).unwrap();
880        assert_eq!(roots_out, roots_all);
881    }
882}