Skip to main content

rust_igraph/algorithms/layout/
gem.rs

1//! GEM force-directed layout (ALGO-LO-007).
2//!
3//! Graph Embedder algorithm by Arne Frick, Andreas Ludwig, Heiko Mehldau,
4//! "A Fast Adaptive Layout Algorithm for Undirected Graphs",
5//! Proc. Graph Drawing 1994, LNCS 894, pp. 388-403, 1995.
6//!
7//! Key features: per-vertex adaptive temperature, oscillation/rotation
8//! detection via impulse history, gravitational barycenter pull.
9
10use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
11
12/// Parameters for the GEM layout algorithm.
13#[derive(Debug, Clone)]
14pub struct GemParams {
15    /// Maximum number of single-vertex iterations.
16    /// A reasonable default is `40 * n * n`.
17    pub maxiter: u32,
18    /// Maximum allowed local temperature. Default: `n` (vertex count).
19    pub temp_max: f64,
20    /// Global temperature threshold for termination. Default: `0.1`.
21    pub temp_min: f64,
22    /// Initial local temperature per vertex. Default: `sqrt(n)`.
23    pub temp_init: f64,
24}
25
26impl GemParams {
27    /// Create parameters with defaults scaled to the given vertex count.
28    pub fn for_graph(n: u32) -> Self {
29        let nf = f64::from(n);
30        Self {
31            maxiter: 40u32.saturating_mul(n).saturating_mul(n),
32            temp_max: nf,
33            temp_min: 0.1,
34            temp_init: nf.sqrt(),
35        }
36    }
37}
38
39/// Compute the GEM force-directed layout.
40///
41/// Places vertices using adaptive per-vertex temperatures with
42/// gravitational pull toward the barycenter, repulsive forces between
43/// all vertex pairs, and attractive spring forces along edges.
44///
45/// Edge directions are ignored (treated as undirected).
46///
47/// # Arguments
48///
49/// * `graph` — input graph.
50/// * `seed` — optional initial positions. If `None`, random positions
51///   are generated.
52/// * `params` — algorithm parameters (use `GemParams::for_graph(n)`
53///   for sensible defaults).
54///
55/// Returns `[x, y]` positions for each vertex.
56///
57/// # Errors
58///
59/// Returns `InvalidArgument` if temperatures violate
60/// `temp_min <= temp_init <= temp_max`, any temperature is non-positive,
61/// or `seed` length doesn't match vertex count.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, layout_gem, GemParams};
67///
68/// let mut g = Graph::with_vertices(6);
69/// g.add_edge(0, 1).unwrap();
70/// g.add_edge(1, 2).unwrap();
71/// g.add_edge(2, 3).unwrap();
72/// g.add_edge(3, 4).unwrap();
73/// g.add_edge(4, 5).unwrap();
74/// g.add_edge(5, 0).unwrap();
75///
76/// let params = GemParams::for_graph(6);
77/// let pos = layout_gem(&g, None, &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_gem(
82    graph: &Graph,
83    seed: Option<&[[f64; 2]]>,
84    params: &GemParams,
85) -> IgraphResult<Vec<[f64; 2]>> {
86    let n = graph.vcount() as usize;
87    if n == 0 {
88        return Ok(Vec::new());
89    }
90
91    if params.temp_max <= 0.0 {
92        return Err(IgraphError::InvalidArgument(
93            "temp_max must be positive".into(),
94        ));
95    }
96    if params.temp_min <= 0.0 {
97        return Err(IgraphError::InvalidArgument(
98            "temp_min must be positive".into(),
99        ));
100    }
101    if params.temp_init <= 0.0 {
102        return Err(IgraphError::InvalidArgument(
103            "temp_init must be positive".into(),
104        ));
105    }
106    if params.temp_max < params.temp_init || params.temp_init < params.temp_min {
107        return Err(IgraphError::InvalidArgument(
108            "requires temp_min <= temp_init <= temp_max".into(),
109        ));
110    }
111
112    if let Some(s) = seed {
113        if s.len() != n {
114            return Err(IgraphError::InvalidArgument(format!(
115                "seed length ({}) must equal vertex count ({})",
116                s.len(),
117                n
118            )));
119        }
120    }
121
122    // Constants from the paper
123    let elen_des2: f64 = 128.0 * 128.0;
124    let gamma: f64 = 1.0 / 16.0;
125    let alpha_o: f64 = std::f64::consts::PI;
126    let alpha_r: f64 = std::f64::consts::PI / 3.0;
127    let sigma_o: f64 = 1.0 / 3.0;
128    let sigma_r: f64 = 1.0 / (2.0 * n as f64);
129
130    // Compute vertex "mass" (degree * (degree/2 + 1))
131    let mut phi = vec![0.0_f64; n];
132    for v in 0..n {
133        let deg = graph.degree(v as VertexId).unwrap_or(0) as f64;
134        phi[v] = deg * (deg / 2.0 + 1.0);
135    }
136
137    // Initialize positions
138    let mut pos = if let Some(s) = seed {
139        s.to_vec()
140    } else {
141        let width_half = n as f64 * 100.0;
142        let mut rng = SplitMix64::new(42);
143        (0..n)
144            .map(|_| {
145                [
146                    rng.next_uniform() * 2.0 * width_half - width_half,
147                    rng.next_uniform() * 2.0 * width_half - width_half,
148                ]
149            })
150            .collect()
151    };
152
153    // Barycenter
154    let mut barycenter_x: f64 = pos.iter().map(|p| p[0]).sum();
155    let mut barycenter_y: f64 = pos.iter().map(|p| p[1]).sum();
156
157    // Per-vertex state
158    let mut impulse_x = vec![0.0_f64; n];
159    let mut impulse_y = vec![0.0_f64; n];
160    let mut temp = vec![params.temp_init; n];
161    let mut skew_gauge = vec![0.0_f64; n];
162
163    // Permutation for random vertex selection
164    let mut perm: Vec<usize> = (0..n).collect();
165    let mut perm_pointer: usize = 0;
166    let mut rng = SplitMix64::new(123);
167
168    // Build adjacency list (undirected)
169    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
170    let ecount = graph.ecount();
171    for eid in 0..ecount as u32 {
172        if let Ok((src, tgt)) = graph.edge(eid) {
173            if src != tgt {
174                adj[src as usize].push(tgt);
175                adj[tgt as usize].push(src);
176            }
177        }
178    }
179
180    let mut temp_global = params.temp_init * n as f64;
181    let mut maxiter = params.maxiter;
182
183    while temp_global > params.temp_min * n as f64 && maxiter > 0 {
184        // Choose vertex v
185        if perm_pointer == 0 {
186            fisher_yates_shuffle(&mut perm, &mut rng);
187            perm_pointer = n;
188        }
189        perm_pointer -= 1;
190        let v = perm[perm_pointer];
191
192        // Gravitational pull toward barycenter
193        let nf = n as f64;
194        let mut px = (barycenter_x / nf - pos[v][0]) * gamma * phi[v];
195        let mut py = (barycenter_y / nf - pos[v][1]) * gamma * phi[v];
196
197        // Random jitter
198        px += rng.next_uniform() * 64.0 - 32.0;
199        py += rng.next_uniform() * 64.0 - 32.0;
200
201        // Repulsive forces from all other vertices
202        for u in 0..n {
203            if u == v {
204                continue;
205            }
206            let dx = pos[v][0] - pos[u][0];
207            let dy = pos[v][1] - pos[u][1];
208            let dist2 = dx * dx + dy * dy;
209            if dist2 != 0.0 {
210                px += dx * elen_des2 / dist2;
211                py += dy * elen_des2 / dist2;
212            }
213        }
214
215        // Attractive forces from neighbors
216        for &u in &adj[v] {
217            let ui = u as usize;
218            let dx = pos[v][0] - pos[ui][0];
219            let dy = pos[v][1] - pos[ui][1];
220            let dist2 = dx * dx + dy * dy;
221            if phi[v] != 0.0 {
222                px -= dx * dist2 / (elen_des2 * phi[v]);
223                py -= dy * dist2 / (elen_des2 * phi[v]);
224            }
225        }
226
227        // Update position
228        if px != 0.0 || py != 0.0 {
229            let plen = (px * px + py * py).sqrt();
230            px *= temp[v] / plen;
231            py *= temp[v] / plen;
232            pos[v][0] += px;
233            pos[v][1] += py;
234            barycenter_x += px;
235            barycenter_y += py;
236        }
237
238        // Temperature adjustment via oscillation/rotation detection
239        let pvx = impulse_x[v];
240        let pvy = impulse_y[v];
241        if pvx != 0.0 || pvy != 0.0 {
242            let beta = (pvy - py).atan2(pvx - px);
243            let sin_beta = beta.sin();
244            let sign_sin_beta = if sin_beta > 0.0 {
245                1.0
246            } else if sin_beta < 0.0 {
247                -1.0
248            } else {
249                0.0
250            };
251            let cos_beta = beta.cos();
252            let abs_cos_beta = cos_beta.abs();
253            let old_temp = temp[v];
254
255            if sin_beta >= (std::f64::consts::FRAC_PI_2 + alpha_r / 2.0).sin() {
256                skew_gauge[v] += sigma_r * sign_sin_beta;
257            }
258            if abs_cos_beta >= (alpha_o / 2.0).cos() {
259                temp[v] *= sigma_o * cos_beta;
260            }
261            temp[v] *= 1.0 - skew_gauge[v].abs();
262            if temp[v] > params.temp_max {
263                temp[v] = params.temp_max;
264            }
265            if temp[v] < 0.0 {
266                temp[v] = 0.0;
267            }
268            impulse_x[v] = px;
269            impulse_y[v] = py;
270            temp_global += temp[v] - old_temp;
271        }
272
273        maxiter -= 1;
274    }
275
276    Ok(pos)
277}
278
279// ═══════════════════════════════════════════════════════════════════
280// Internal RNG (deterministic, no external deps)
281// ═══════════════════════════════════════════════════════════════════
282
283struct SplitMix64 {
284    state: u64,
285}
286
287impl SplitMix64 {
288    fn new(seed: u64) -> Self {
289        Self { state: seed }
290    }
291
292    fn next_u64(&mut self) -> u64 {
293        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
294        let mut z = self.state;
295        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
296        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
297        z ^ (z >> 31)
298    }
299
300    fn next_uniform(&mut self) -> f64 {
301        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
302    }
303}
304
305fn fisher_yates_shuffle(perm: &mut [usize], rng: &mut SplitMix64) {
306    let n = perm.len();
307    for i in (1..n).rev() {
308        let j = (rng.next_u64() as usize) % (i + 1);
309        perm.swap(i, j);
310    }
311}
312
313// ═══════════════════════════════════════════════════════════════════
314// Tests
315// ═══════════════════════════════════════════════════════════════════
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_gem_empty() {
323        let g = Graph::with_vertices(0);
324        let params = GemParams::for_graph(0);
325        let pos = layout_gem(&g, None, &params).unwrap();
326        assert!(pos.is_empty());
327    }
328
329    #[test]
330    fn test_gem_single_vertex() {
331        let g = Graph::with_vertices(1);
332        let params = GemParams::for_graph(1);
333        let pos = layout_gem(&g, None, &params).unwrap();
334        assert_eq!(pos.len(), 1);
335        assert!(pos[0][0].is_finite());
336        assert!(pos[0][1].is_finite());
337    }
338
339    #[test]
340    fn test_gem_triangle() {
341        let mut g = Graph::with_vertices(3);
342        g.add_edge(0, 1).unwrap();
343        g.add_edge(1, 2).unwrap();
344        g.add_edge(2, 0).unwrap();
345        let params = GemParams::for_graph(3);
346        let pos = layout_gem(&g, None, &params).unwrap();
347        assert_eq!(pos.len(), 3);
348        for p in &pos {
349            assert!(p[0].is_finite());
350            assert!(p[1].is_finite());
351        }
352    }
353
354    #[test]
355    fn test_gem_path() {
356        let mut g = Graph::with_vertices(5);
357        for i in 0..4 {
358            g.add_edge(i, i + 1).unwrap();
359        }
360        let params = GemParams::for_graph(5);
361        let pos = layout_gem(&g, None, &params).unwrap();
362        assert_eq!(pos.len(), 5);
363        for p in &pos {
364            assert!(p[0].is_finite());
365            assert!(p[1].is_finite());
366        }
367    }
368
369    #[test]
370    fn test_gem_no_overlap() {
371        let mut g = Graph::with_vertices(4);
372        g.add_edge(0, 1).unwrap();
373        g.add_edge(1, 2).unwrap();
374        g.add_edge(2, 3).unwrap();
375        g.add_edge(3, 0).unwrap();
376        let params = GemParams::for_graph(4);
377        let pos = layout_gem(&g, None, &params).unwrap();
378        // Vertices shouldn't all collapse to the same point
379        let mut all_same = true;
380        for i in 1..4 {
381            if (pos[i][0] - pos[0][0]).abs() > 1e-6 || (pos[i][1] - pos[0][1]).abs() > 1e-6 {
382                all_same = false;
383                break;
384            }
385        }
386        assert!(!all_same, "all vertices collapsed to the same point");
387    }
388
389    #[test]
390    fn test_gem_with_seed() {
391        let mut g = Graph::with_vertices(3);
392        g.add_edge(0, 1).unwrap();
393        g.add_edge(1, 2).unwrap();
394        let seed = vec![[0.0, 0.0], [100.0, 0.0], [50.0, 86.6]];
395        let params = GemParams::for_graph(3);
396        let pos = layout_gem(&g, Some(&seed), &params).unwrap();
397        assert_eq!(pos.len(), 3);
398        for p in &pos {
399            assert!(p[0].is_finite());
400            assert!(p[1].is_finite());
401        }
402    }
403
404    #[test]
405    fn test_gem_seed_wrong_length() {
406        let g = Graph::with_vertices(3);
407        let seed = vec![[0.0, 0.0], [1.0, 0.0]];
408        let params = GemParams::for_graph(3);
409        let result = layout_gem(&g, Some(&seed), &params);
410        assert!(result.is_err());
411    }
412
413    #[test]
414    fn test_gem_invalid_temp() {
415        let g = Graph::with_vertices(3);
416        let params = GemParams {
417            maxiter: 100,
418            temp_max: -1.0,
419            temp_min: 0.1,
420            temp_init: 1.0,
421        };
422        assert!(layout_gem(&g, None, &params).is_err());
423
424        let params2 = GemParams {
425            maxiter: 100,
426            temp_max: 10.0,
427            temp_min: 5.0,
428            temp_init: 2.0,
429        };
430        assert!(layout_gem(&g, None, &params2).is_err());
431    }
432
433    #[test]
434    fn test_gem_deterministic() {
435        let mut g = Graph::with_vertices(4);
436        g.add_edge(0, 1).unwrap();
437        g.add_edge(1, 2).unwrap();
438        g.add_edge(2, 3).unwrap();
439        let params = GemParams::for_graph(4);
440        let pos1 = layout_gem(&g, None, &params).unwrap();
441        let pos2 = layout_gem(&g, None, &params).unwrap();
442        for i in 0..4 {
443            assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
444            assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
445        }
446    }
447
448    #[test]
449    fn test_gem_disconnected() {
450        let mut g = Graph::with_vertices(4);
451        g.add_edge(0, 1).unwrap();
452        g.add_edge(2, 3).unwrap();
453        let params = GemParams::for_graph(4);
454        let pos = layout_gem(&g, None, &params).unwrap();
455        assert_eq!(pos.len(), 4);
456        for p in &pos {
457            assert!(p[0].is_finite());
458            assert!(p[1].is_finite());
459        }
460    }
461
462    #[test]
463    fn test_gem_star() {
464        let mut g = Graph::with_vertices(6);
465        for i in 1..6 {
466            g.add_edge(0, i).unwrap();
467        }
468        let params = GemParams {
469            maxiter: 1000,
470            temp_max: 6.0,
471            temp_min: 0.1,
472            temp_init: 2.4,
473        };
474        let pos = layout_gem(&g, None, &params).unwrap();
475        assert_eq!(pos.len(), 6);
476        for p in &pos {
477            assert!(p[0].is_finite());
478            assert!(p[1].is_finite());
479        }
480    }
481}