Skip to main content

rust_igraph/algorithms/layout/
davidson_harel.rs

1//! Davidson-Harel simulated annealing layout (ALGO-LO-008).
2//!
3//! Reference: Ron Davidson, David Harel, "Drawing Graphs Nicely Using
4//! Simulated Annealing", ACM Transactions on Graphics 15(4), 1996.
5//!
6//! Energy function components: node-node distances, border proximity,
7//! edge lengths, edge crossings, node-edge distances.
8
9use crate::core::{Graph, IgraphError, IgraphResult};
10
11/// Parameters for the Davidson-Harel layout.
12#[derive(Debug, Clone)]
13pub struct DhParams {
14    /// Maximum annealing iterations. Default: 10.
15    pub maxiter: u32,
16    /// Fine-tuning iterations. Default: max(10, log2(n)).
17    pub fineiter: u32,
18    /// Cooling factor in (0, 1). Default: 0.75.
19    pub cool_fact: f64,
20    /// Weight for node-node distance repulsion. Default: 1.0.
21    pub weight_node_dist: f64,
22    /// Weight for border proximity. Default: 0.0.
23    pub weight_border: f64,
24    /// Weight for edge length minimization. Default: density/10.
25    pub weight_edge_lengths: f64,
26    /// Weight for edge crossing minimization. Default: 1 - sqrt(density).
27    pub weight_edge_crossings: f64,
28    /// Weight for node-edge distance (fine-tuning only). Default: (1-density)/5.
29    pub weight_node_edge_dist: f64,
30}
31
32impl DhParams {
33    /// Create parameters with defaults scaled to the given graph.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use rust_igraph::{Graph, DhParams};
39    ///
40    /// let g = Graph::with_vertices(10);
41    /// let params = DhParams::for_graph(&g);
42    /// assert_eq!(params.maxiter, 10);
43    /// assert!(params.cool_fact > 0.0 && params.cool_fact < 1.0);
44    /// ```
45    pub fn for_graph(graph: &Graph) -> Self {
46        let n = graph.vcount() as usize;
47        let e = graph.ecount();
48        let max_edges = if n > 1 { n * (n - 1) / 2 } else { 1 };
49        let density = e as f64 / max_edges as f64;
50        let fineiter = if n > 1 {
51            (n as f64).log2().ceil().max(10.0) as u32
52        } else {
53            10
54        };
55        Self {
56            maxiter: 10,
57            fineiter,
58            cool_fact: 0.75,
59            weight_node_dist: 1.0,
60            weight_border: 0.0,
61            weight_edge_lengths: density / 10.0,
62            weight_edge_crossings: (1.0 - density.sqrt()).max(0.0),
63            weight_node_edge_dist: (1.0 - density) / 5.0,
64        }
65    }
66}
67
68/// Compute the Davidson-Harel simulated annealing layout.
69///
70/// Uses a multi-component energy function minimized via simulated
71/// annealing followed by a fine-tuning phase without stochastic
72/// acceptance.
73///
74/// # Arguments
75///
76/// * `graph` — input graph (edge directions ignored).
77/// * `seed` — optional initial positions. If `None`, random positions
78///   are generated.
79/// * `params` — algorithm parameters.
80///
81/// Returns `[x, y]` positions for each vertex.
82///
83/// # Errors
84///
85/// Returns `InvalidArgument` if `cool_fact` is not in (0, 1) or
86/// `seed` length doesn't match vertex count.
87///
88/// # Examples
89///
90/// ```
91/// use rust_igraph::{Graph, layout_davidson_harel, DhParams};
92///
93/// let mut g = Graph::with_vertices(5);
94/// g.add_edge(0, 1).unwrap();
95/// g.add_edge(1, 2).unwrap();
96/// g.add_edge(2, 3).unwrap();
97/// g.add_edge(3, 4).unwrap();
98/// g.add_edge(4, 0).unwrap();
99///
100/// let params = DhParams::for_graph(&g);
101/// let pos = layout_davidson_harel(&g, None, &params).unwrap();
102/// assert_eq!(pos.len(), 5);
103/// assert!(pos.iter().all(|p| p[0].is_finite() && p[1].is_finite()));
104/// ```
105pub fn layout_davidson_harel(
106    graph: &Graph,
107    seed: Option<&[[f64; 2]]>,
108    params: &DhParams,
109) -> IgraphResult<Vec<[f64; 2]>> {
110    let n = graph.vcount() as usize;
111    if n == 0 {
112        return Ok(Vec::new());
113    }
114
115    if params.cool_fact <= 0.0 || params.cool_fact >= 1.0 {
116        return Err(IgraphError::InvalidArgument(
117            "cool_fact must be in (0, 1)".into(),
118        ));
119    }
120    if let Some(s) = seed {
121        if s.len() != n {
122            return Err(IgraphError::InvalidArgument(format!(
123                "seed length ({}) must equal vertex count ({})",
124                s.len(),
125                n
126            )));
127        }
128    }
129
130    let no_edges = graph.ecount();
131    let width = (n as f64).sqrt() * 10.0;
132    let height = width;
133    let no_tries: usize = 30;
134    let fine_tuning_factor = 0.01;
135
136    // Build edge list
137    let mut edges: Vec<(usize, usize)> = Vec::with_capacity(no_edges);
138    for eid in 0..no_edges as u32 {
139        if let Ok((src, tgt)) = graph.edge(eid) {
140            edges.push((src as usize, tgt as usize));
141        }
142    }
143
144    // Build adjacency (undirected, no self-loops)
145    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
146    for &(src, tgt) in &edges {
147        if src != tgt {
148            adj[src].push(tgt);
149            adj[tgt].push(src);
150        }
151    }
152
153    // Initialize positions
154    let mut rng = SplitMix64::new(7);
155    let mut pos = if let Some(s) = seed {
156        s.to_vec()
157    } else {
158        (0..n)
159            .map(|_| {
160                [
161                    rng.next_uniform() * width - width / 2.0,
162                    rng.next_uniform() * height - height / 2.0,
163                ]
164            })
165            .collect()
166    };
167
168    // Compute bounding box
169    let mut min_x = f64::INFINITY;
170    let mut max_x = f64::NEG_INFINITY;
171    let mut min_y = f64::INFINITY;
172    let mut max_y = f64::NEG_INFINITY;
173    for p in &pos {
174        if p[0] < min_x {
175            min_x = p[0];
176        }
177        if p[0] > max_x {
178            max_x = p[0];
179        }
180        if p[1] < min_y {
181            min_y = p[1];
182        }
183        if p[1] > max_y {
184            max_y = p[1];
185        }
186    }
187
188    // Pre-compute try directions (evenly spaced angles)
189    let try_dirs: Vec<[f64; 2]> = (0..no_tries)
190        .map(|i| {
191            let phi = 2.0 * std::f64::consts::PI / no_tries as f64 * i as f64;
192            [phi.cos(), phi.sin()]
193        })
194        .collect();
195
196    let mut perm: Vec<usize> = (0..n).collect();
197    let mut try_idx: Vec<usize> = (0..no_tries).collect();
198    let mut move_radius = width / 2.0;
199
200    let total_rounds = params.maxiter + params.fineiter;
201
202    for round in 0..total_rounds {
203        let fine_tuning = round >= params.maxiter;
204
205        if fine_tuning && round == params.maxiter {
206            let fx = fine_tuning_factor * (max_x - min_x);
207            let fy = fine_tuning_factor * (max_y - min_y);
208            move_radius = fx.min(fy);
209        }
210
211        fisher_yates_shuffle(&mut perm, &mut rng);
212
213        for &v in &perm {
214            fisher_yates_shuffle(&mut try_idx, &mut rng);
215
216            for &ti in &try_idx {
217                let old_x = pos[v][0];
218                let old_y = pos[v][1];
219                let mut new_x = old_x + move_radius * try_dirs[ti][0];
220                let mut new_y = old_y + move_radius * try_dirs[ti][1];
221
222                // Clamp to bounds
223                new_x = new_x.clamp(-width / 2.0 + 1e-6, width / 2.0 - 1e-6);
224                new_y = new_y.clamp(-height / 2.0 + 1e-6, height / 2.0 - 1e-6);
225
226                let mut diff_energy = 0.0_f64;
227
228                // Node-node distance repulsion
229                if params.weight_node_dist != 0.0 {
230                    for u in 0..n {
231                        if u == v {
232                            continue;
233                        }
234                        let odx = old_x - pos[u][0];
235                        let ody = old_y - pos[u][1];
236                        let dx = new_x - pos[u][0];
237                        let dy = new_y - pos[u][1];
238                        let odist2 = odx * odx + ody * ody;
239                        let dist2 = dx * dx + dy * dy;
240                        if dist2 > 0.0 && odist2 > 0.0 {
241                            diff_energy +=
242                                params.weight_node_dist / dist2 - params.weight_node_dist / odist2;
243                        }
244                    }
245                }
246
247                // Border proximity
248                if params.weight_border != 0.0 {
249                    let hw = width / 2.0;
250                    let hh = height / 2.0;
251                    let border_energy = |x: f64, y: f64| -> f64 {
252                        let dx1 = (hw - x).max(2.0);
253                        let dx2 = (x + hw).max(2.0);
254                        let dy1 = (hh - y).max(2.0);
255                        let dy2 = (y + hh).max(2.0);
256                        1.0 / (dx1 * dx1)
257                            + 1.0 / (dx2 * dx2)
258                            + 1.0 / (dy1 * dy1)
259                            + 1.0 / (dy2 * dy2)
260                    };
261                    diff_energy += params.weight_border
262                        * (border_energy(new_x, new_y) - border_energy(old_x, old_y));
263                }
264
265                // Edge lengths
266                if params.weight_edge_lengths != 0.0 {
267                    for &u in &adj[v] {
268                        let odx = old_x - pos[u][0];
269                        let ody = old_y - pos[u][1];
270                        let dx = new_x - pos[u][0];
271                        let dy = new_y - pos[u][1];
272                        let odist2 = odx * odx + ody * ody;
273                        let dist2 = dx * dx + dy * dy;
274                        diff_energy += params.weight_edge_lengths * (dist2 - odist2);
275                    }
276                }
277
278                // Edge crossings
279                if params.weight_edge_crossings != 0.0 {
280                    let mut crossing_diff: i64 = 0;
281                    for &u in &adj[v] {
282                        let ux = pos[u][0];
283                        let uy = pos[u][1];
284                        for &(u1, u2) in &edges {
285                            if u1 == v || u2 == v || u1 == u || u2 == u {
286                                continue;
287                            }
288                            let u1x = pos[u1][0];
289                            let u1y = pos[u1][1];
290                            let u2x = pos[u2][0];
291                            let u2y = pos[u2][1];
292                            if segments_intersect(old_x, old_y, ux, uy, u1x, u1y, u2x, u2y) {
293                                crossing_diff -= 1;
294                            }
295                            if segments_intersect(new_x, new_y, ux, uy, u1x, u1y, u2x, u2y) {
296                                crossing_diff += 1;
297                            }
298                        }
299                    }
300                    diff_energy += params.weight_edge_crossings * crossing_diff as f64;
301                }
302
303                // Node-edge distance (fine-tuning only)
304                if params.weight_node_edge_dist != 0.0 && fine_tuning {
305                    // Non-incident edges from moved vertex
306                    for &(u1, u2) in &edges {
307                        if u1 == v || u2 == v {
308                            continue;
309                        }
310                        let u1x = pos[u1][0];
311                        let u1y = pos[u1][1];
312                        let u2x = pos[u2][0];
313                        let u2y = pos[u2][1];
314                        let d_old = point_segment_dist2(old_x, old_y, u1x, u1y, u2x, u2y);
315                        let d_new = point_segment_dist2(new_x, new_y, u1x, u1y, u2x, u2y);
316                        if d_old > 0.0 {
317                            diff_energy -= params.weight_node_edge_dist / d_old;
318                        }
319                        if d_new > 0.0 {
320                            diff_energy += params.weight_node_edge_dist / d_new;
321                        }
322                    }
323
324                    // All other nodes from v's incident edges
325                    for &u in &adj[v] {
326                        let ux = pos[u][0];
327                        let uy = pos[u][1];
328                        for w in 0..n {
329                            if w == v || w == u {
330                                continue;
331                            }
332                            let wx = pos[w][0];
333                            let wy = pos[w][1];
334                            let d_old = point_segment_dist2(wx, wy, old_x, old_y, ux, uy);
335                            let d_new = point_segment_dist2(wx, wy, new_x, new_y, ux, uy);
336                            if d_old > 0.0 {
337                                diff_energy -= params.weight_node_edge_dist / d_old;
338                            }
339                            if d_new > 0.0 {
340                                diff_energy += params.weight_node_edge_dist / d_new;
341                            }
342                        }
343                    }
344                }
345
346                // Accept or reject
347                let accept = if diff_energy < 0.0 {
348                    true
349                } else if !fine_tuning && move_radius > 0.0 {
350                    rng.next_uniform() < (-diff_energy / move_radius).exp()
351                } else {
352                    false
353                };
354
355                if accept {
356                    pos[v][0] = new_x;
357                    pos[v][1] = new_y;
358                    if new_x < min_x {
359                        min_x = new_x;
360                    }
361                    if new_x > max_x {
362                        max_x = new_x;
363                    }
364                    if new_y < min_y {
365                        min_y = new_y;
366                    }
367                    if new_y > max_y {
368                        max_y = new_y;
369                    }
370                    break;
371                }
372            }
373        }
374
375        move_radius *= params.cool_fact;
376    }
377
378    Ok(pos)
379}
380
381// ═══════════════════════════════════════════════════════════════════
382// Geometry helpers
383// ═══════════════════════════════════════════════════════════════════
384
385fn segments_intersect(
386    p0x: f64,
387    p0y: f64,
388    p1x: f64,
389    p1y: f64,
390    p2x: f64,
391    p2y: f64,
392    p3x: f64,
393    p3y: f64,
394) -> bool {
395    let s1x = p1x - p0x;
396    let s1y = p1y - p0y;
397    let s2x = p3x - p2x;
398    let s2y = p3y - p2y;
399    let denom = -s2x * s1y + s1x * s2y;
400    if denom == 0.0 {
401        return false;
402    }
403    let s = (-s1y * (p0x - p2x) + s1x * (p0y - p2y)) / denom;
404    let t = (s2x * (p0y - p2y) - s2y * (p0x - p2x)) / denom;
405    s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0
406}
407
408fn point_segment_dist2(vx: f64, vy: f64, u1x: f64, u1y: f64, u2x: f64, u2y: f64) -> f64 {
409    let dx = u2x - u1x;
410    let dy = u2y - u1y;
411    let l2 = dx * dx + dy * dy;
412    if l2 == 0.0 {
413        return (vx - u1x) * (vx - u1x) + (vy - u1y) * (vy - u1y);
414    }
415    let t = ((vx - u1x) * dx + (vy - u1y) * dy) / l2;
416    if t < 0.0 {
417        (vx - u1x) * (vx - u1x) + (vy - u1y) * (vy - u1y)
418    } else if t > 1.0 {
419        (vx - u2x) * (vx - u2x) + (vy - u2y) * (vy - u2y)
420    } else {
421        let px = u1x + t * dx;
422        let py = u1y + t * dy;
423        (vx - px) * (vx - px) + (vy - py) * (vy - py)
424    }
425}
426
427// ═══════════════════════════════════════════════════════════════════
428// Internal RNG
429// ═══════════════════════════════════════════════════════════════════
430
431struct SplitMix64 {
432    state: u64,
433}
434
435impl SplitMix64 {
436    fn new(seed: u64) -> Self {
437        Self { state: seed }
438    }
439
440    fn next_u64(&mut self) -> u64 {
441        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
442        let mut z = self.state;
443        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
444        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
445        z ^ (z >> 31)
446    }
447
448    fn next_uniform(&mut self) -> f64 {
449        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
450    }
451}
452
453fn fisher_yates_shuffle(perm: &mut [usize], rng: &mut SplitMix64) {
454    let n = perm.len();
455    for i in (1..n).rev() {
456        let j = (rng.next_u64() as usize) % (i + 1);
457        perm.swap(i, j);
458    }
459}
460
461// ═══════════════════════════════════════════════════════════════════
462// Tests
463// ═══════════════════════════════════════════════════════════════════
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn test_dh_empty() {
471        let g = Graph::with_vertices(0);
472        let params = DhParams::for_graph(&g);
473        let pos = layout_davidson_harel(&g, None, &params).unwrap();
474        assert!(pos.is_empty());
475    }
476
477    #[test]
478    fn test_dh_single() {
479        let g = Graph::with_vertices(1);
480        let params = DhParams::for_graph(&g);
481        let pos = layout_davidson_harel(&g, None, &params).unwrap();
482        assert_eq!(pos.len(), 1);
483        assert!(pos[0][0].is_finite());
484    }
485
486    #[test]
487    fn test_dh_triangle() {
488        let mut g = Graph::with_vertices(3);
489        g.add_edge(0, 1).unwrap();
490        g.add_edge(1, 2).unwrap();
491        g.add_edge(2, 0).unwrap();
492        let params = DhParams::for_graph(&g);
493        let pos = layout_davidson_harel(&g, None, &params).unwrap();
494        assert_eq!(pos.len(), 3);
495        for p in &pos {
496            assert!(p[0].is_finite() && p[1].is_finite());
497        }
498    }
499
500    #[test]
501    fn test_dh_path() {
502        let mut g = Graph::with_vertices(5);
503        for i in 0..4 {
504            g.add_edge(i, i + 1).unwrap();
505        }
506        let params = DhParams::for_graph(&g);
507        let pos = layout_davidson_harel(&g, None, &params).unwrap();
508        assert_eq!(pos.len(), 5);
509    }
510
511    #[test]
512    fn test_dh_with_seed() {
513        let mut g = Graph::with_vertices(3);
514        g.add_edge(0, 1).unwrap();
515        g.add_edge(1, 2).unwrap();
516        let seed = vec![[0.0, 0.0], [5.0, 0.0], [2.5, 4.0]];
517        let params = DhParams::for_graph(&g);
518        let pos = layout_davidson_harel(&g, Some(&seed), &params).unwrap();
519        assert_eq!(pos.len(), 3);
520    }
521
522    #[test]
523    fn test_dh_seed_wrong_length() {
524        let g = Graph::with_vertices(3);
525        let seed = vec![[0.0, 0.0]];
526        let params = DhParams::for_graph(&g);
527        assert!(layout_davidson_harel(&g, Some(&seed), &params).is_err());
528    }
529
530    #[test]
531    fn test_dh_invalid_cool_fact() {
532        let g = Graph::with_vertices(3);
533        let mut params = DhParams::for_graph(&g);
534        params.cool_fact = 1.5;
535        assert!(layout_davidson_harel(&g, None, &params).is_err());
536        params.cool_fact = 0.0;
537        assert!(layout_davidson_harel(&g, None, &params).is_err());
538    }
539
540    #[test]
541    fn test_dh_deterministic() {
542        let mut g = Graph::with_vertices(4);
543        g.add_edge(0, 1).unwrap();
544        g.add_edge(1, 2).unwrap();
545        g.add_edge(2, 3).unwrap();
546        g.add_edge(3, 0).unwrap();
547        let params = DhParams::for_graph(&g);
548        let pos1 = layout_davidson_harel(&g, None, &params).unwrap();
549        let pos2 = layout_davidson_harel(&g, None, &params).unwrap();
550        for i in 0..4 {
551            assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
552            assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
553        }
554    }
555
556    #[test]
557    fn test_dh_no_edges() {
558        let g = Graph::with_vertices(4);
559        let params = DhParams::for_graph(&g);
560        let pos = layout_davidson_harel(&g, None, &params).unwrap();
561        assert_eq!(pos.len(), 4);
562        for p in &pos {
563            assert!(p[0].is_finite() && p[1].is_finite());
564        }
565    }
566
567    #[test]
568    fn test_segments_intersect() {
569        assert!(segments_intersect(0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0));
570        assert!(!segments_intersect(0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0));
571    }
572
573    #[test]
574    fn test_point_segment_dist2_basic() {
575        let d = point_segment_dist2(0.0, 1.0, 0.0, 0.0, 1.0, 0.0);
576        assert!((d - 1.0).abs() < 1e-10);
577        let d2 = point_segment_dist2(2.0, 0.0, 0.0, 0.0, 1.0, 0.0);
578        assert!((d2 - 1.0).abs() < 1e-10);
579    }
580}