rust_igraph/algorithms/layout/
align.rs1use crate::core::{Graph, IgraphError, IgraphResult};
11
12pub fn layout_align(graph: &Graph, layout: &mut [Vec<f64>]) -> IgraphResult<()> {
49 let vcount = graph.vcount() as usize;
50 let ecount = graph.ecount();
51
52 if layout.len() != vcount {
53 return Err(IgraphError::InvalidArgument(
54 "layout_align: number of points does not match vertex count".to_string(),
55 ));
56 }
57
58 if vcount == 0 {
59 return Ok(());
60 }
61
62 let dim = layout[0].len();
63 for (i, pos) in layout.iter().enumerate() {
64 if pos.len() != dim {
65 return Err(IgraphError::InvalidArgument(format!(
66 "layout_align: vertex {i} has {} coordinates, expected {dim}",
67 pos.len()
68 )));
69 }
70 }
71
72 if dim == 0 {
73 return Err(IgraphError::InvalidArgument(
74 "layout_align: coordinates must be at least one-dimensional".to_string(),
75 ));
76 }
77
78 let mut center = vec![0.0_f64; dim];
80 for pos in layout.iter() {
81 for (j, &c) in pos.iter().enumerate() {
82 center[j] += c;
83 }
84 }
85 let inv_n = 1.0 / vcount as f64;
86 for c in &mut center {
87 *c *= inv_n;
88 }
89 for pos in layout.iter_mut() {
90 for (j, c) in pos.iter_mut().enumerate() {
91 *c -= center[j];
92 }
93 }
94
95 if dim == 1 {
96 return Ok(());
97 }
98
99 let mut m_mat = vec![vec![0.0_f64; dim]; dim];
101 let mut norm2_sum = 0.0_f64;
102
103 let mut correction_saved = false;
105 let mut m_correction = vec![vec![0.0_f64; dim]; dim];
106 let mut norm2_sum_correction = 0.0_f64;
107
108 for eid in 0..ecount {
109 #[allow(clippy::cast_possible_truncation)]
110 let (from, to) = graph.edge(eid as u32)?;
111 if from == to {
112 continue;
113 }
114
115 let f = from as usize;
116 let t = to as usize;
117
118 for i in 0..dim {
119 let vi = layout[f][i] - layout[t][i];
120 for j in 0..dim {
121 let vj = layout[f][j] - layout[t][j];
122 let prod = vi * vj;
123 m_mat[i][j] += prod;
124 if i == j {
125 norm2_sum += prod;
126 }
127 }
128 }
129
130 if !correction_saved && norm2_sum > 0.0 {
131 correction_saved = true;
132 norm2_sum_correction = norm2_sum;
133 for i in 0..dim {
134 for j in 0..dim {
135 m_correction[i][j] = m_mat[i][j];
136 }
137 }
138 }
139 }
140
141 if norm2_sum == 0.0 {
143 for vid in 0..vcount {
144 for i in 0..dim {
145 for j in 0..dim {
146 let prod = layout[vid][i] * layout[vid][j];
147 m_mat[i][j] += prod;
148 if i == j {
149 norm2_sum += prod;
150 }
151 }
152 }
153
154 if !correction_saved && norm2_sum > 0.0 {
155 correction_saved = true;
156 norm2_sum_correction = norm2_sum;
157 for i in 0..dim {
158 for j in 0..dim {
159 m_correction[i][j] = m_mat[i][j];
160 }
161 }
162 }
163 }
164 }
165
166 if norm2_sum == 0.0 {
168 return Ok(());
169 }
170
171 let mut retried = false;
173 let rotation;
174
175 loop {
176 let mut q = vec![vec![0.0_f64; dim]; dim];
178 let inv_norm2 = 1.0 / norm2_sum;
179 let inv_dim = 1.0 / dim as f64;
180 for i in 0..dim {
181 for j in 0..dim {
182 q[i][j] = m_mat[i][j] * inv_norm2;
183 }
184 q[i][i] -= inv_dim;
185 }
186
187 let (eigenvalues, eigenvectors) = symmetric_eigen_jacobi(&q);
189
190 let matrix_norm = eigenvalues.iter().map(|&l| l.abs()).fold(0.0_f64, f64::max);
192
193 if matrix_norm > 1e-3 || retried {
194 rotation = eigenvectors;
195 break;
196 }
197
198 for i in 0..dim {
200 for j in 0..dim {
201 m_mat[i][j] -= m_correction[i][j];
202 }
203 }
204 norm2_sum -= norm2_sum_correction;
205
206 if norm2_sum <= 0.0 {
207 return Ok(());
208 }
209
210 retried = true;
211 }
212
213 let mut temp = vec![vec![0.0_f64; dim]; vcount];
215 for v in 0..vcount {
216 for j in 0..dim {
217 let mut sum = 0.0_f64;
218 for k in 0..dim {
219 sum += layout[v][k] * rotation[k][j];
220 }
221 temp[v][j] = sum;
222 }
223 }
224
225 let mut extents: Vec<(f64, usize)> = (0..dim)
227 .map(|j| {
228 let mut min = f64::INFINITY;
229 let mut max = f64::NEG_INFINITY;
230 for v in 0..vcount {
231 let c = temp[v][j];
232 if c < min {
233 min = c;
234 }
235 if c > max {
236 max = c;
237 }
238 }
239 (max - min, j)
240 })
241 .collect();
242 extents.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
243
244 for v in 0..vcount {
245 for (new_j, &(_, old_j)) in extents.iter().enumerate() {
246 layout[v][new_j] = temp[v][old_j];
247 }
248 }
249
250 Ok(())
251}
252
253fn symmetric_eigen_jacobi(a: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
259 let n = a.len();
260
261 let mut s: Vec<Vec<f64>> = a.to_vec();
263
264 let mut v = vec![vec![0.0_f64; n]; n];
266 for i in 0..n {
267 v[i][i] = 1.0;
268 }
269
270 let max_sweeps = 100;
271
272 for _sweep in 0..max_sweeps {
273 let mut max_off = 0.0_f64;
275 let mut p = 0;
276 let mut q = 1;
277 for i in 0..n {
278 for j in (i + 1)..n {
279 let val = s[i][j].abs();
280 if val > max_off {
281 max_off = val;
282 p = i;
283 q = j;
284 }
285 }
286 }
287
288 if max_off < 1e-15 {
289 break;
290 }
291
292 let (cos, sin) = if (s[p][p] - s[q][q]).abs() < 1e-15 {
294 let angle = std::f64::consts::FRAC_PI_4;
295 (angle.cos(), angle.sin())
296 } else {
297 let tau = (s[q][q] - s[p][p]) / (2.0 * s[p][q]);
298 let t = if tau >= 0.0 {
299 1.0 / (tau + (1.0 + tau * tau).sqrt())
300 } else {
301 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
302 };
303 let c = 1.0 / (1.0 + t * t).sqrt();
304 (c, t * c)
305 };
306
307 let s_pp = s[p][p];
309 let s_qq = s[q][q];
310 let s_pq = s[p][q];
311
312 s[p][p] = cos * cos * s_pp - 2.0 * sin * cos * s_pq + sin * sin * s_qq;
313 s[q][q] = sin * sin * s_pp + 2.0 * sin * cos * s_pq + cos * cos * s_qq;
314 s[p][q] = 0.0;
315 s[q][p] = 0.0;
316
317 for i in 0..n {
318 if i == p || i == q {
319 continue;
320 }
321 let s_ip = s[i][p];
322 let s_iq = s[i][q];
323 s[i][p] = cos * s_ip - sin * s_iq;
324 s[p][i] = s[i][p];
325 s[i][q] = sin * s_ip + cos * s_iq;
326 s[q][i] = s[i][q];
327 }
328
329 for i in 0..n {
331 let v_ip = v[i][p];
332 let v_iq = v[i][q];
333 v[i][p] = cos * v_ip - sin * v_iq;
334 v[i][q] = sin * v_ip + cos * v_iq;
335 }
336 }
337
338 let mut eigenvalues: Vec<f64> = (0..n).map(|i| s[i][i]).collect();
339
340 let mut indices: Vec<usize> = (0..n).collect();
342 indices.sort_unstable_by(|&a, &b| {
343 eigenvalues[a]
344 .partial_cmp(&eigenvalues[b])
345 .unwrap_or(std::cmp::Ordering::Equal)
346 });
347
348 let sorted_eigenvalues: Vec<f64> = indices.iter().map(|&i| eigenvalues[i]).collect();
349 eigenvalues = sorted_eigenvalues;
350
351 let mut sorted_v = vec![vec![0.0_f64; n]; n];
353 for row in 0..n {
354 for (new_col, &old_col) in indices.iter().enumerate() {
355 sorted_v[row][new_col] = v[row][old_col];
356 }
357 }
358
359 (eigenvalues, sorted_v)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 fn approx_eq(a: f64, b: f64) -> bool {
367 (a - b).abs() < 1e-10
368 }
369
370 fn center_of_mass(layout: &[Vec<f64>]) -> Vec<f64> {
371 let n = layout.len();
372 if n == 0 {
373 return vec![];
374 }
375 let dim = layout[0].len();
376 let mut c = vec![0.0; dim];
377 for pos in layout {
378 for (j, &v) in pos.iter().enumerate() {
379 c[j] += v;
380 }
381 }
382 for v in &mut c {
383 *v /= n as f64;
384 }
385 c
386 }
387
388 #[test]
389 fn empty_graph() {
390 let g = Graph::with_vertices(0);
391 let mut layout: Vec<Vec<f64>> = vec![];
392 layout_align(&g, &mut layout).unwrap();
393 assert!(layout.is_empty());
394 }
395
396 #[test]
397 fn single_vertex() {
398 let g = Graph::with_vertices(1);
399 let mut layout = vec![vec![5.0, 3.0]];
400 layout_align(&g, &mut layout).unwrap();
401 assert!(approx_eq(layout[0][0], 0.0));
403 assert!(approx_eq(layout[0][1], 0.0));
404 }
405
406 #[test]
407 fn centering() {
408 let mut g = Graph::with_vertices(3);
409 g.add_edge(0, 1).unwrap();
410 g.add_edge(1, 2).unwrap();
411 let mut layout = vec![vec![10.0, 20.0], vec![12.0, 22.0], vec![14.0, 24.0]];
412 layout_align(&g, &mut layout).unwrap();
413
414 let c = center_of_mass(&layout);
415 assert!(approx_eq(c[0], 0.0));
416 assert!(approx_eq(c[1], 0.0));
417 }
418
419 #[test]
420 fn preserves_distances() {
421 let mut g = Graph::with_vertices(3);
422 g.add_edge(0, 1).unwrap();
423 g.add_edge(1, 2).unwrap();
424 let mut layout = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
425
426 let dist = |a: &[f64], b: &[f64]| -> f64 {
428 a.iter()
429 .zip(b.iter())
430 .map(|(&x, &y)| (x - y) * (x - y))
431 .sum::<f64>()
432 .sqrt()
433 };
434 let d01_before = dist(&layout[0], &layout[1]);
435 let d12_before = dist(&layout[1], &layout[2]);
436
437 layout_align(&g, &mut layout).unwrap();
438
439 let d01_after = dist(&layout[0], &layout[1]);
440 let d12_after = dist(&layout[1], &layout[2]);
441
442 assert!((d01_before - d01_after).abs() < 1e-10);
443 assert!((d12_before - d12_after).abs() < 1e-10);
444 }
445
446 #[test]
447 fn widest_axis_first() {
448 let mut g = Graph::with_vertices(4);
449 g.add_edge(0, 1).unwrap();
450 g.add_edge(2, 3).unwrap();
451 let mut layout = vec![
453 vec![0.0, 0.0],
454 vec![1.0, 0.0],
455 vec![0.0, 10.0],
456 vec![1.0, 10.0],
457 ];
458 layout_align(&g, &mut layout).unwrap();
459
460 let extent_x = layout
462 .iter()
463 .map(|p| p[0])
464 .fold(f64::NEG_INFINITY, f64::max)
465 - layout.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
466 let extent_y = layout
467 .iter()
468 .map(|p| p[1])
469 .fold(f64::NEG_INFINITY, f64::max)
470 - layout.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min);
471
472 assert!(
473 extent_x >= extent_y - 1e-10,
474 "axis 0 extent ({extent_x}) should be >= axis 1 extent ({extent_y})"
475 );
476 }
477
478 #[test]
479 fn one_dimensional() {
480 let mut g = Graph::with_vertices(3);
481 g.add_edge(0, 1).unwrap();
482 g.add_edge(1, 2).unwrap();
483 let mut layout = vec![vec![1.0], vec![3.0], vec![5.0]];
484 layout_align(&g, &mut layout).unwrap();
485 let c = center_of_mass(&layout);
486 assert!(approx_eq(c[0], 0.0));
487 }
488
489 #[test]
490 fn three_dimensional() {
491 let mut g = Graph::with_vertices(4);
492 g.add_edge(0, 1).unwrap();
493 g.add_edge(1, 2).unwrap();
494 g.add_edge(2, 3).unwrap();
495 let mut layout = vec![
496 vec![1.0, 0.0, 0.0],
497 vec![0.0, 1.0, 0.0],
498 vec![0.0, 0.0, 1.0],
499 vec![1.0, 1.0, 1.0],
500 ];
501 layout_align(&g, &mut layout).unwrap();
502
503 let c = center_of_mass(&layout);
504 for &ci in &c {
505 assert!(approx_eq(ci, 0.0));
506 }
507 }
508
509 #[test]
510 fn no_edges_uses_vertices() {
511 let g = Graph::with_vertices(3);
512 let mut layout = vec![vec![0.0, 0.0], vec![1.0, 1.0], vec![2.0, 2.0]];
514 layout_align(&g, &mut layout).unwrap();
515 let c = center_of_mass(&layout);
516 assert!(approx_eq(c[0], 0.0));
517 assert!(approx_eq(c[1], 0.0));
518 }
519
520 #[test]
521 fn all_at_origin() {
522 let mut g = Graph::with_vertices(3);
523 g.add_edge(0, 1).unwrap();
524 let mut layout = vec![vec![0.0, 0.0], vec![0.0, 0.0], vec![0.0, 0.0]];
525 layout_align(&g, &mut layout).unwrap();
526 for pos in &layout {
527 assert!(approx_eq(pos[0], 0.0));
528 assert!(approx_eq(pos[1], 0.0));
529 }
530 }
531
532 #[test]
533 fn self_loops_skipped() {
534 let mut g = Graph::with_vertices(2);
535 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
537 let mut layout = vec![vec![0.0, 0.0], vec![1.0, 1.0]];
538 layout_align(&g, &mut layout).unwrap();
539 let c = center_of_mass(&layout);
540 assert!(approx_eq(c[0], 0.0));
541 assert!(approx_eq(c[1], 0.0));
542 }
543
544 #[test]
545 fn directed_graph() {
546 let mut g = Graph::new(3, true).unwrap();
547 g.add_edge(0, 1).unwrap();
548 g.add_edge(1, 2).unwrap();
549 let mut layout = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
550 layout_align(&g, &mut layout).unwrap();
551 let c = center_of_mass(&layout);
552 assert!(approx_eq(c[0], 0.0));
553 assert!(approx_eq(c[1], 0.0));
554 }
555
556 #[test]
557 fn mismatched_vertex_count() {
558 let g = Graph::with_vertices(3);
559 let mut layout = vec![vec![0.0, 0.0], vec![1.0, 1.0]];
560 assert!(layout_align(&g, &mut layout).is_err());
561 }
562
563 #[test]
564 fn inconsistent_dimensions() {
565 let g = Graph::with_vertices(2);
566 let mut layout = vec![vec![0.0, 0.0], vec![1.0]];
567 assert!(layout_align(&g, &mut layout).is_err());
568 }
569
570 #[test]
571 fn zero_dimensional() {
572 let g = Graph::with_vertices(2);
573 let mut layout = vec![vec![], vec![]];
574 assert!(layout_align(&g, &mut layout).is_err());
575 }
576
577 #[test]
578 fn jacobi_2x2() {
579 let m = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
583 let (vals, vecs) = symmetric_eigen_jacobi(&m);
584
585 let expected_0 = (5.0 - 5.0_f64.sqrt()) / 2.0;
586 let expected_1 = (5.0 + 5.0_f64.sqrt()) / 2.0;
587 assert!((vals[0] - expected_0).abs() < 1e-10);
588 assert!((vals[1] - expected_1).abs() < 1e-10);
589
590 let dot: f64 = vecs[0][0] * vecs[0][1] + vecs[1][0] * vecs[1][1];
592 assert!(dot.abs() < 1e-10);
593 }
594
595 #[test]
596 fn jacobi_identity() {
597 let m = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
598 let (vals, _) = symmetric_eigen_jacobi(&m);
599 assert!((vals[0] - 1.0).abs() < 1e-10);
600 assert!((vals[1] - 1.0).abs() < 1e-10);
601 }
602}