Skip to main content

rust_igraph/algorithms/layout/
lgl.rs

1//! Large Graph Layout (ALGO-LO-011).
2//!
3//! BFS-layered initial placement followed by Fruchterman-Reingold style
4//! force-directed refinement. Designed for large graphs where global
5//! force-directed algorithms are too slow.
6//!
7//! Reference: igraph C `igraph_layout_lgl()` in `src/layout/large_graph.c`.
8
9use std::collections::VecDeque;
10
11use crate::core::{Graph, IgraphError, IgraphResult};
12
13/// Parameters for the LGL layout algorithm.
14#[derive(Debug, Clone)]
15pub struct LglParams {
16    /// Maximum number of force-directed iterations. Default: 150.
17    pub maxit: u32,
18    /// Maximum displacement per iteration (per vertex). Default: vertex count.
19    pub maxdelta: Option<f64>,
20    /// Area parameter for force computation. Default: n².
21    pub area: Option<f64>,
22    /// Cooling exponent. Default: 1.5.
23    pub coolexp: f64,
24    /// Repulsion cancellation radius. Default: area × n.
25    pub repulserad: Option<f64>,
26    /// Cell size for grid acceleration. Default: area^0.25.
27    pub cellsize: Option<f64>,
28    /// Root vertex for BFS layering. Default: vertex with highest degree.
29    pub root: Option<u32>,
30}
31
32impl Default for LglParams {
33    fn default() -> Self {
34        Self {
35            maxit: 150,
36            maxdelta: None,
37            area: None,
38            coolexp: 1.5,
39            repulserad: None,
40            cellsize: None,
41            root: None,
42        }
43    }
44}
45
46/// Compute the Large Graph Layout (LGL).
47///
48/// Places vertices using BFS layering from a root vertex followed by
49/// Fruchterman-Reingold style force-directed refinement with grid-based
50/// repulsion acceleration.
51///
52/// # Arguments
53///
54/// * `graph` — input graph (treated as undirected).
55/// * `params` — algorithm parameters (all have sensible defaults).
56///
57/// Returns `[x, y]` positions for each vertex.
58///
59/// # Errors
60///
61/// Returns `InvalidArgument` if root is out of range or if the graph
62/// is empty.
63///
64/// # Examples
65///
66/// ```
67/// use rust_igraph::{Graph, layout_lgl, LglParams};
68///
69/// let mut g = Graph::with_vertices(6);
70/// g.add_edge(0, 1).unwrap();
71/// g.add_edge(1, 2).unwrap();
72/// g.add_edge(2, 3).unwrap();
73/// g.add_edge(3, 4).unwrap();
74/// g.add_edge(4, 5).unwrap();
75///
76/// let params = LglParams::default();
77/// let pos = layout_lgl(&g, &params).unwrap();
78/// assert_eq!(pos.len(), 6);
79/// assert!(pos.iter().all(|p| p[0].is_finite() && p[1].is_finite()));
80/// ```
81pub fn layout_lgl(graph: &Graph, params: &LglParams) -> IgraphResult<Vec<[f64; 2]>> {
82    let n = graph.vcount() as usize;
83    if n == 0 {
84        return Ok(Vec::new());
85    }
86    if n == 1 {
87        return Ok(vec![[0.0, 0.0]]);
88    }
89
90    let root = if let Some(r) = params.root {
91        if r >= graph.vcount() {
92            return Err(IgraphError::InvalidArgument(format!(
93                "root vertex {} out of range (vcount={})",
94                r,
95                graph.vcount()
96            )));
97        }
98        r as usize
99    } else {
100        highest_degree_vertex(graph)
101    };
102
103    let maxdelta = params.maxdelta.unwrap_or(n as f64);
104    let area = params.area.unwrap_or((n * n) as f64);
105    let coolexp = params.coolexp;
106    let repulserad = params.repulserad.unwrap_or(area * n as f64);
107    let cellsize = params.cellsize.unwrap_or(area.sqrt().sqrt());
108
109    let frk = (area / n as f64).sqrt();
110
111    // Build adjacency list
112    let adj = build_adjacency(graph, n);
113
114    // BFS layering from root
115    let (layers, parent) = bfs_layers(n, root, &adj);
116
117    // Initial placement: layer by layer on circles around parents
118    let mut pos = vec![[0.0_f64; 2]; n];
119    let mut placed = vec![false; n];
120    let mut rng = SplitMix64::new(42);
121
122    // Place root at origin
123    pos[root] = [0.0, 0.0];
124    placed[root] = true;
125
126    // Place each subsequent layer
127    for layer in layers.iter().skip(1) {
128        for &v in layer {
129            let p = parent[v];
130            if p == usize::MAX {
131                pos[v] = [rng.next_uniform() - 0.5, rng.next_uniform() - 0.5];
132                placed[v] = true;
133                continue;
134            }
135            // Place on a circle around parent
136            let angle = rng.next_uniform() * std::f64::consts::TAU;
137            let radius = frk * 0.5;
138            pos[v] = [
139                pos[p][0] + radius * angle.cos(),
140                pos[p][1] + radius * angle.sin(),
141            ];
142            placed[v] = true;
143        }
144    }
145
146    // Place any unreached vertices (disconnected components)
147    for v in 0..n {
148        if !placed[v] {
149            pos[v] = [
150                (rng.next_uniform() - 0.5) * frk * 2.0,
151                (rng.next_uniform() - 0.5) * frk * 2.0,
152            ];
153        }
154    }
155
156    // Force-directed refinement with grid acceleration
157    let mut disp = vec![[0.0_f64; 2]; n];
158
159    for iter in 0..params.maxit {
160        // Cooling schedule
161        let temp = maxdelta * (-coolexp * (iter as f64) / (params.maxit as f64)).exp();
162
163        // Clear displacements
164        disp.fill([0.0, 0.0]);
165
166        // Repulsion using grid
167        apply_repulsion_grid(n, &pos, &mut disp, frk, repulserad, cellsize);
168
169        // Attraction along edges
170        for v in 0..n {
171            for &u in &adj[v] {
172                if u <= v {
173                    continue;
174                }
175                let dx = pos[v][0] - pos[u][0];
176                let dy = pos[v][1] - pos[u][1];
177                let dist = (dx * dx + dy * dy).sqrt();
178                if dist == 0.0 {
179                    continue;
180                }
181                // Attraction: dist²/frk
182                let force = dist * dist / frk;
183                let fx = force * dx / dist;
184                let fy = force * dy / dist;
185                disp[v][0] -= fx;
186                disp[v][1] -= fy;
187                disp[u][0] += fx;
188                disp[u][1] += fy;
189            }
190        }
191
192        // Apply displacement with temperature limit
193        for v in 0..n {
194            let dx = disp[v][0];
195            let dy = disp[v][1];
196            let len = (dx * dx + dy * dy).sqrt();
197            if len > 0.0 {
198                let scale = temp.min(len) / len;
199                pos[v][0] += dx * scale;
200                pos[v][1] += dy * scale;
201            }
202        }
203    }
204
205    Ok(pos)
206}
207
208fn highest_degree_vertex(graph: &Graph) -> usize {
209    let n = graph.vcount();
210    let mut best = 0_usize;
211    let mut best_deg = 0_usize;
212    for v in 0..n {
213        if let Ok(d) = graph.degree(v) {
214            if d > best_deg {
215                best_deg = d;
216                best = v as usize;
217            }
218        }
219    }
220    best
221}
222
223fn build_adjacency(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
224    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
225    for eid in 0..graph.ecount() as u32 {
226        if let Ok((src, tgt)) = graph.edge(eid) {
227            let s = src as usize;
228            let t = tgt as usize;
229            adj[s].push(t);
230            if s != t {
231                adj[t].push(s);
232            }
233        }
234    }
235    adj
236}
237
238fn bfs_layers(n: usize, root: usize, adj: &[Vec<usize>]) -> (Vec<Vec<usize>>, Vec<usize>) {
239    let mut visited = vec![false; n];
240    let mut parent = vec![usize::MAX; n];
241    let mut layers: Vec<Vec<usize>> = Vec::new();
242
243    let mut queue = VecDeque::new();
244    queue.push_back(root);
245    visited[root] = true;
246
247    let mut current_layer = vec![root];
248    let mut next_layer = Vec::new();
249
250    layers.push(current_layer.clone());
251
252    loop {
253        next_layer.clear();
254        for &v in &current_layer {
255            for &u in &adj[v] {
256                if !visited[u] {
257                    visited[u] = true;
258                    parent[u] = v;
259                    next_layer.push(u);
260                }
261            }
262        }
263        if next_layer.is_empty() {
264            break;
265        }
266        layers.push(next_layer.clone());
267        current_layer.clone_from(&next_layer);
268    }
269
270    (layers, parent)
271}
272
273fn apply_repulsion_grid(
274    n: usize,
275    pos: &[[f64; 2]],
276    disp: &mut [[f64; 2]],
277    frk: f64,
278    repulserad: f64,
279    cellsize: f64,
280) {
281    if n <= 100 {
282        // Brute-force for small graphs
283        for i in 0..n {
284            for j in (i + 1)..n {
285                let dx = pos[i][0] - pos[j][0];
286                let dy = pos[i][1] - pos[j][1];
287                let dist_sq = dx * dx + dy * dy;
288                let dist = dist_sq.sqrt();
289                if dist == 0.0 || dist_sq >= repulserad {
290                    continue;
291                }
292                // Repulsion: frk² * (1/dist - dist²/repulserad)
293                let force = frk * frk * (1.0 / dist - dist_sq / repulserad);
294                let fx = force * dx / dist;
295                let fy = force * dy / dist;
296                disp[i][0] += fx;
297                disp[i][1] += fy;
298                disp[j][0] -= fx;
299                disp[j][1] -= fy;
300            }
301        }
302    } else {
303        // Grid-based acceleration
304        grid_repulsion(n, pos, disp, frk, repulserad, cellsize);
305    }
306}
307
308fn grid_repulsion(
309    n: usize,
310    pos: &[[f64; 2]],
311    disp: &mut [[f64; 2]],
312    frk: f64,
313    repulserad: f64,
314    cellsize: f64,
315) {
316    if cellsize <= 0.0 {
317        return;
318    }
319
320    // Find bounding box
321    let mut min_x = f64::INFINITY;
322    let mut max_x = f64::NEG_INFINITY;
323    let mut min_y = f64::INFINITY;
324    let mut max_y = f64::NEG_INFINITY;
325    for p in pos.iter().take(n) {
326        if p[0] < min_x {
327            min_x = p[0];
328        }
329        if p[0] > max_x {
330            max_x = p[0];
331        }
332        if p[1] < min_y {
333            min_y = p[1];
334        }
335        if p[1] > max_y {
336            max_y = p[1];
337        }
338    }
339
340    let width = max_x - min_x;
341    let height = max_y - min_y;
342    if width <= 0.0 && height <= 0.0 {
343        return;
344    }
345
346    let cols = ((width / cellsize).ceil() as usize).max(1);
347    let rows = ((height / cellsize).ceil() as usize).max(1);
348
349    // Cap grid size to prevent memory explosion
350    let max_cells = 10_000;
351    if cols.saturating_mul(rows) > max_cells {
352        // Fall back to brute-force subset: only check nearby pairs
353        brute_force_repulsion(n, pos, disp, frk, repulserad);
354        return;
355    }
356
357    // Assign vertices to cells
358    let mut grid: Vec<Vec<usize>> = vec![Vec::new(); cols * rows];
359    let mut cell_of = vec![0_usize; n];
360
361    for v in 0..n {
362        let cx = ((pos[v][0] - min_x) / cellsize).floor() as usize;
363        let cy = ((pos[v][1] - min_y) / cellsize).floor() as usize;
364        let cx = cx.min(cols - 1);
365        let cy = cy.min(rows - 1);
366        let cell = cy * cols + cx;
367        grid[cell].push(v);
368        cell_of[v] = cell;
369    }
370
371    // For each vertex, check its cell and 8 neighbors
372    for v in 0..n {
373        let cell = cell_of[v];
374        let cy = cell / cols;
375        let cx = cell % cols;
376
377        let row_start = if cy > 0 { cy - 1 } else { 0 };
378        let row_end = (cy + 1).min(rows - 1);
379        let col_start = if cx > 0 { cx - 1 } else { 0 };
380        let col_end = (cx + 1).min(cols - 1);
381
382        for ry in row_start..=row_end {
383            for rx in col_start..=col_end {
384                let neighbor_cell = ry * cols + rx;
385                for &u in &grid[neighbor_cell] {
386                    if u <= v {
387                        continue;
388                    }
389                    let dx = pos[v][0] - pos[u][0];
390                    let dy = pos[v][1] - pos[u][1];
391                    let dist_sq = dx * dx + dy * dy;
392                    let dist = dist_sq.sqrt();
393                    if dist == 0.0 || dist_sq >= repulserad {
394                        continue;
395                    }
396                    let force = frk * frk * (1.0 / dist - dist_sq / repulserad);
397                    let fx = force * dx / dist;
398                    let fy = force * dy / dist;
399                    disp[v][0] += fx;
400                    disp[v][1] += fy;
401                    disp[u][0] -= fx;
402                    disp[u][1] -= fy;
403                }
404            }
405        }
406    }
407}
408
409fn brute_force_repulsion(
410    n: usize,
411    pos: &[[f64; 2]],
412    disp: &mut [[f64; 2]],
413    frk: f64,
414    repulserad: f64,
415) {
416    for i in 0..n {
417        for j in (i + 1)..n {
418            let dx = pos[i][0] - pos[j][0];
419            let dy = pos[i][1] - pos[j][1];
420            let dist_sq = dx * dx + dy * dy;
421            let dist = dist_sq.sqrt();
422            if dist == 0.0 || dist_sq >= repulserad {
423                continue;
424            }
425            let force = frk * frk * (1.0 / dist - dist_sq / repulserad);
426            let fx = force * dx / dist;
427            let fy = force * dy / dist;
428            disp[i][0] += fx;
429            disp[i][1] += fy;
430            disp[j][0] -= fx;
431            disp[j][1] -= fy;
432        }
433    }
434}
435
436// ═══════════════════════════════════════════════════════════════════
437// Internal RNG
438// ═══════════════════════════════════════════════════════════════════
439
440struct SplitMix64 {
441    state: u64,
442}
443
444impl SplitMix64 {
445    fn new(seed: u64) -> Self {
446        Self { state: seed }
447    }
448
449    fn next_u64(&mut self) -> u64 {
450        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
451        let mut z = self.state;
452        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
453        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
454        z ^ (z >> 31)
455    }
456
457    fn next_uniform(&mut self) -> f64 {
458        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
459    }
460}
461
462// ═══════════════════════════════════════════════════════════════════
463// Tests
464// ═══════════════════════════════════════════════════════════════════
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_lgl_empty() {
472        let g = Graph::with_vertices(0);
473        let params = LglParams::default();
474        let pos = layout_lgl(&g, &params).unwrap();
475        assert!(pos.is_empty());
476    }
477
478    #[test]
479    fn test_lgl_single() {
480        let g = Graph::with_vertices(1);
481        let params = LglParams::default();
482        let pos = layout_lgl(&g, &params).unwrap();
483        assert_eq!(pos.len(), 1);
484        assert!(pos[0][0].abs() < 1e-10 && pos[0][1].abs() < 1e-10);
485    }
486
487    #[test]
488    fn test_lgl_two_vertices() {
489        let mut g = Graph::with_vertices(2);
490        g.add_edge(0, 1).unwrap();
491        let params = LglParams::default();
492        let pos = layout_lgl(&g, &params).unwrap();
493        assert_eq!(pos.len(), 2);
494        assert!(pos[0][0].is_finite() && pos[1][0].is_finite());
495        let dx = pos[0][0] - pos[1][0];
496        let dy = pos[0][1] - pos[1][1];
497        assert!((dx * dx + dy * dy).sqrt() > 0.01);
498    }
499
500    #[test]
501    fn test_lgl_path() {
502        let mut g = Graph::with_vertices(6);
503        for i in 0..5 {
504            g.add_edge(i, i + 1).unwrap();
505        }
506        let params = LglParams::default();
507        let pos = layout_lgl(&g, &params).unwrap();
508        assert_eq!(pos.len(), 6);
509        for p in &pos {
510            assert!(p[0].is_finite() && p[1].is_finite());
511        }
512    }
513
514    #[test]
515    fn test_lgl_cycle() {
516        let mut g = Graph::with_vertices(8);
517        for i in 0..8 {
518            g.add_edge(i, (i + 1) % 8).unwrap();
519        }
520        let params = LglParams::default();
521        let pos = layout_lgl(&g, &params).unwrap();
522        assert_eq!(pos.len(), 8);
523        for p in &pos {
524            assert!(p[0].is_finite() && p[1].is_finite());
525        }
526    }
527
528    #[test]
529    fn test_lgl_complete() {
530        let mut g = Graph::with_vertices(5);
531        for i in 0..5u32 {
532            for j in (i + 1)..5 {
533                g.add_edge(i, j).unwrap();
534            }
535        }
536        let params = LglParams::default();
537        let pos = layout_lgl(&g, &params).unwrap();
538        assert_eq!(pos.len(), 5);
539        for p in &pos {
540            assert!(p[0].is_finite() && p[1].is_finite());
541        }
542    }
543
544    #[test]
545    fn test_lgl_with_root() {
546        let mut g = Graph::with_vertices(5);
547        g.add_edge(0, 1).unwrap();
548        g.add_edge(1, 2).unwrap();
549        g.add_edge(2, 3).unwrap();
550        g.add_edge(3, 4).unwrap();
551        let params = LglParams {
552            root: Some(2),
553            ..LglParams::default()
554        };
555        let pos = layout_lgl(&g, &params).unwrap();
556        assert_eq!(pos.len(), 5);
557        for p in &pos {
558            assert!(p[0].is_finite() && p[1].is_finite());
559        }
560    }
561
562    #[test]
563    fn test_lgl_root_out_of_range() {
564        let g = Graph::with_vertices(3);
565        let params = LglParams {
566            root: Some(5),
567            ..LglParams::default()
568        };
569        assert!(layout_lgl(&g, &params).is_err());
570    }
571
572    #[test]
573    fn test_lgl_disconnected() {
574        let mut g = Graph::with_vertices(6);
575        g.add_edge(0, 1).unwrap();
576        g.add_edge(1, 2).unwrap();
577        g.add_edge(3, 4).unwrap();
578        g.add_edge(4, 5).unwrap();
579        let params = LglParams::default();
580        let pos = layout_lgl(&g, &params).unwrap();
581        assert_eq!(pos.len(), 6);
582        for p in &pos {
583            assert!(p[0].is_finite() && p[1].is_finite());
584        }
585    }
586
587    #[test]
588    fn test_lgl_deterministic() {
589        let mut g = Graph::with_vertices(5);
590        g.add_edge(0, 1).unwrap();
591        g.add_edge(1, 2).unwrap();
592        g.add_edge(2, 3).unwrap();
593        g.add_edge(3, 4).unwrap();
594        g.add_edge(4, 0).unwrap();
595        let params = LglParams::default();
596        let pos1 = layout_lgl(&g, &params).unwrap();
597        let pos2 = layout_lgl(&g, &params).unwrap();
598        for i in 0..5 {
599            assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
600            assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
601        }
602    }
603
604    #[test]
605    fn test_lgl_custom_params() {
606        let mut g = Graph::with_vertices(5);
607        g.add_edge(0, 1).unwrap();
608        g.add_edge(1, 2).unwrap();
609        g.add_edge(2, 3).unwrap();
610        g.add_edge(3, 4).unwrap();
611        let params = LglParams {
612            maxit: 50,
613            coolexp: 2.0,
614            ..LglParams::default()
615        };
616        let pos = layout_lgl(&g, &params).unwrap();
617        assert_eq!(pos.len(), 5);
618        for p in &pos {
619            assert!(p[0].is_finite() && p[1].is_finite());
620        }
621    }
622
623    #[test]
624    fn test_lgl_star_topology() {
625        let mut g = Graph::with_vertices(7);
626        for i in 1..7 {
627            g.add_edge(0, i).unwrap();
628        }
629        let params = LglParams {
630            root: Some(0),
631            ..LglParams::default()
632        };
633        let pos = layout_lgl(&g, &params).unwrap();
634        assert_eq!(pos.len(), 7);
635        for p in &pos {
636            assert!(p[0].is_finite() && p[1].is_finite());
637        }
638    }
639}