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