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 {
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
57pub 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 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 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 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 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 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 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 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 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 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 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 if params.weight_node_edge_dist != 0.0 && fine_tuning {
294 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 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 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
370fn 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
416struct 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#[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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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), ¶ms).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), ¶ms).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, ¶ms).is_err());
525 params.cool_fact = 0.0;
526 assert!(layout_davidson_harel(&g, None, ¶ms).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, ¶ms).unwrap();
538 let pos2 = layout_davidson_harel(&g, None, ¶ms).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, ¶ms).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}