rust_igraph/algorithms/layout/
davidson_harel.rs1use crate::core::{Graph, IgraphError, IgraphResult};
10
11#[derive(Debug, Clone)]
13pub struct DhParams {
14 pub maxiter: u32,
16 pub fineiter: u32,
18 pub cool_fact: f64,
20 pub weight_node_dist: f64,
22 pub weight_border: f64,
24 pub weight_edge_lengths: f64,
26 pub weight_edge_crossings: f64,
28 pub weight_node_edge_dist: f64,
30}
31
32impl DhParams {
33 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
68pub 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 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 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 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 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 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 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 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 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 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 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 if params.weight_node_edge_dist != 0.0 && fine_tuning {
305 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 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 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
381fn 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
427struct 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#[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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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), ¶ms).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), ¶ms).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, ¶ms).is_err());
536 params.cool_fact = 0.0;
537 assert!(layout_davidson_harel(&g, None, ¶ms).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, ¶ms).unwrap();
549 let pos2 = layout_davidson_harel(&g, None, ¶ms).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, ¶ms).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}