rust_igraph/algorithms/properties/
distance_spectrum.rs1#![allow(
20 clippy::cast_possible_truncation,
21 clippy::cast_precision_loss,
22 clippy::many_single_char_names,
23 clippy::needless_range_loop,
24 clippy::similar_names,
25 clippy::too_many_lines
26)]
27
28use crate::core::{Graph, IgraphError, IgraphResult};
29use std::collections::VecDeque;
30
31fn dense_distance_matrix(graph: &Graph) -> Option<Vec<f64>> {
34 let n = graph.vcount() as usize;
35 if n == 0 {
36 return Some(Vec::new());
37 }
38
39 let mut dist = vec![f64::INFINITY; n * n];
40 for s in 0..n {
41 dist[s * n + s] = 0.0;
42 let mut queue = VecDeque::new();
43 queue.push_back(s as u32);
44 let mut visited = vec![false; n];
45 visited[s] = true;
46
47 while let Some(u) = queue.pop_front() {
48 let ui = u as usize;
49 if let Ok(nbrs) = graph.neighbors(u) {
50 for &v in &nbrs {
51 let vi = v as usize;
52 if !visited[vi] {
53 visited[vi] = true;
54 dist[s * n + vi] = dist[s * n + ui] + 1.0;
55 queue.push_back(v);
56 }
57 }
58 }
59 }
60
61 for j in 0..n {
62 if dist[s * n + j].is_infinite() {
63 return None;
64 }
65 }
66 }
67
68 Some(dist)
69}
70
71fn jacobi_eigen_descending(mat: &mut [f64], n: usize) -> Vec<f64> {
74 if n == 0 {
75 return Vec::new();
76 }
77 if n == 1 {
78 return vec![mat[0]];
79 }
80
81 let max_sweeps = 100;
82 for _ in 0..max_sweeps {
83 let mut max_off = 0.0_f64;
84 let mut p = 0;
85 let mut q = 1;
86 for i in 0..n {
87 for j in (i + 1)..n {
88 let val = mat[i * n + j].abs();
89 if val > max_off {
90 max_off = val;
91 p = i;
92 q = j;
93 }
94 }
95 }
96
97 if max_off < 1e-14 {
98 break;
99 }
100
101 let app = mat[p * n + p];
102 let aqq = mat[q * n + q];
103 let apq = mat[p * n + q];
104
105 let (cos, sin) = if (app - aqq).abs() < 1e-300 {
106 let s = std::f64::consts::FRAC_1_SQRT_2;
107 (s, s)
108 } else {
109 let tau = (aqq - app) / (2.0 * apq);
110 let t = if tau >= 0.0 {
111 1.0 / (tau + (1.0 + tau * tau).sqrt())
112 } else {
113 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
114 };
115 let c = 1.0 / (1.0 + t * t).sqrt();
116 (c, t * c)
117 };
118
119 for i in 0..n {
120 if i == p || i == q {
121 continue;
122 }
123 let aip = mat[i * n + p];
124 let aiq = mat[i * n + q];
125 mat[i * n + p] = cos * aip - sin * aiq;
126 mat[p * n + i] = mat[i * n + p];
127 mat[i * n + q] = sin * aip + cos * aiq;
128 mat[q * n + i] = mat[i * n + q];
129 }
130
131 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
132 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
133 mat[p * n + p] = new_pp;
134 mat[q * n + q] = new_qq;
135 mat[p * n + q] = 0.0;
136 mat[q * n + p] = 0.0;
137 }
138
139 let mut eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
140 eigenvalues.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
141 eigenvalues
142}
143
144pub fn wiener_index(graph: &Graph) -> IgraphResult<f64> {
160 if graph.is_directed() {
161 return Err(IgraphError::InvalidArgument(
162 "wiener_index is defined for connected undirected graphs only".into(),
163 ));
164 }
165 let n = graph.vcount() as usize;
166 if n <= 1 {
167 return Ok(0.0);
168 }
169
170 let dist = dense_distance_matrix(graph).ok_or_else(|| {
171 IgraphError::InvalidArgument("wiener_index requires a connected graph".into())
172 })?;
173
174 let mut w = 0.0_f64;
175 for u in 0..n {
176 for v in (u + 1)..n {
177 w += dist[u * n + v];
178 }
179 }
180 Ok(w)
181}
182
183pub fn distance_spectrum(graph: &Graph) -> IgraphResult<Vec<f64>> {
200 if graph.is_directed() {
201 return Err(IgraphError::InvalidArgument(
202 "distance_spectrum is defined for connected undirected graphs only".into(),
203 ));
204 }
205 let n = graph.vcount() as usize;
206 if n == 0 {
207 return Ok(Vec::new());
208 }
209 if n == 1 {
210 return Ok(vec![0.0]);
211 }
212
213 let mut dist = dense_distance_matrix(graph).ok_or_else(|| {
214 IgraphError::InvalidArgument("distance_spectrum requires a connected graph".into())
215 })?;
216
217 Ok(jacobi_eigen_descending(&mut dist, n))
218}
219
220pub fn distance_spectral_radius(graph: &Graph) -> IgraphResult<f64> {
236 let spec = distance_spectrum(graph)?;
237 if spec.is_empty() {
238 return Ok(0.0);
239 }
240 Ok(spec[0])
241}
242
243pub fn distance_energy(graph: &Graph) -> IgraphResult<f64> {
258 let spec = distance_spectrum(graph)?;
259 Ok(spec.iter().map(|v| v.abs()).sum())
260}
261
262pub fn distance_estrada_index(graph: &Graph) -> IgraphResult<f64> {
276 let spec = distance_spectrum(graph)?;
277 Ok(spec.iter().map(|v| v.exp()).sum())
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn path3() -> Graph {
285 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
286 }
287
288 fn path4() -> Graph {
289 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
290 }
291
292 fn k3() -> Graph {
293 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
294 }
295
296 fn k4() -> Graph {
297 Graph::from_edges(
298 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
299 false,
300 Some(4),
301 )
302 .unwrap()
303 }
304
305 fn cycle4() -> Graph {
306 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
307 }
308
309 fn star4() -> Graph {
310 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
311 }
312
313 #[test]
316 fn wi_path3() {
317 let g = path3();
318 let w = wiener_index(&g).unwrap();
319 assert!((w - 4.0).abs() < 0.01);
321 }
322
323 #[test]
324 fn wi_path4() {
325 let g = path4();
326 let w = wiener_index(&g).unwrap();
327 assert!((w - 10.0).abs() < 0.01);
329 }
330
331 #[test]
332 fn wi_k3() {
333 let g = k3();
334 let w = wiener_index(&g).unwrap();
335 assert!((w - 3.0).abs() < 0.01);
337 }
338
339 #[test]
340 fn wi_k4() {
341 let g = k4();
342 let w = wiener_index(&g).unwrap();
343 assert!((w - 6.0).abs() < 0.01);
345 }
346
347 #[test]
348 fn wi_cycle4() {
349 let g = cycle4();
350 let w = wiener_index(&g).unwrap();
351 assert!((w - 8.0).abs() < 0.01);
353 }
354
355 #[test]
356 fn wi_star4() {
357 let g = star4();
358 let w = wiener_index(&g).unwrap();
359 assert!((w - 9.0).abs() < 0.01);
361 }
362
363 #[test]
364 fn wi_single() {
365 let g = Graph::with_vertices(1);
366 let w = wiener_index(&g).unwrap();
367 assert!(w.abs() < 1e-10);
368 }
369
370 #[test]
371 fn wi_disconnected_error() {
372 let g = Graph::with_vertices(3);
373 assert!(wiener_index(&g).is_err());
374 }
375
376 #[test]
377 fn wi_directed_error() {
378 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
379 assert!(wiener_index(&g).is_err());
380 }
381
382 #[test]
385 fn ds_k3() {
386 let g = k3();
387 let spec = distance_spectrum(&g).unwrap();
388 assert_eq!(spec.len(), 3);
390 assert!((spec[0] - 2.0).abs() < 0.1);
391 assert!((spec[1] - (-1.0)).abs() < 0.1);
392 assert!((spec[2] - (-1.0)).abs() < 0.1);
393 }
394
395 #[test]
396 fn ds_decreasing() {
397 let g = path4();
398 let spec = distance_spectrum(&g).unwrap();
399 for i in 1..spec.len() {
400 assert!(spec[i] <= spec[i - 1] + 1e-10);
401 }
402 }
403
404 #[test]
405 fn ds_empty() {
406 let g = Graph::with_vertices(0);
407 let spec = distance_spectrum(&g).unwrap();
408 assert!(spec.is_empty());
409 }
410
411 #[test]
412 fn ds_single() {
413 let g = Graph::with_vertices(1);
414 let spec = distance_spectrum(&g).unwrap();
415 assert_eq!(spec.len(), 1);
416 assert!(spec[0].abs() < 1e-10);
417 }
418
419 #[test]
420 fn ds_disconnected_error() {
421 let g = Graph::with_vertices(3);
422 assert!(distance_spectrum(&g).is_err());
423 }
424
425 #[test]
428 fn dsr_k3() {
429 let g = k3();
430 let rho = distance_spectral_radius(&g).unwrap();
431 assert!((rho - 2.0).abs() < 0.1);
432 }
433
434 #[test]
435 fn dsr_k4() {
436 let g = k4();
437 let rho = distance_spectral_radius(&g).unwrap();
438 assert!((rho - 3.0).abs() < 0.1);
440 }
441
442 #[test]
443 fn dsr_positive() {
444 let g = star4();
445 let rho = distance_spectral_radius(&g).unwrap();
446 assert!(rho > 0.0);
447 }
448
449 #[test]
452 fn de_k3() {
453 let g = k3();
454 let e = distance_energy(&g).unwrap();
455 assert!((e - 4.0).abs() < 0.1);
457 }
458
459 #[test]
460 fn de_k4() {
461 let g = k4();
462 let e = distance_energy(&g).unwrap();
463 assert!((e - 6.0).abs() < 0.1);
465 }
466
467 #[test]
468 fn de_nonneg() {
469 let g = cycle4();
470 let e = distance_energy(&g).unwrap();
471 assert!(e >= -1e-10);
472 }
473
474 #[test]
477 fn dee_k3() {
478 let g = k3();
479 let dee = distance_estrada_index(&g).unwrap();
480 let expected = 2.0_f64.exp() + 2.0 * (-1.0_f64).exp();
481 assert!((dee - expected).abs() < 0.1);
482 }
483
484 #[test]
485 fn dee_positive() {
486 let g = path4();
487 let dee = distance_estrada_index(&g).unwrap();
488 assert!(dee > 0.0);
489 }
490
491 #[test]
494 fn wiener_equals_half_trace_sum() {
495 let g = star4();
496 let w = wiener_index(&g).unwrap();
497 let dist = dense_distance_matrix(&g).unwrap();
498 let n = 4;
499 let total: f64 = dist.iter().sum();
500 assert!((w - total / 2.0).abs() < 0.01);
501
502 let _ = n;
503 }
504
505 #[test]
506 fn kn_distance_spectral_radius_is_n_minus_1() {
507 for n in 2_u32..=5 {
508 let mut edges = Vec::new();
509 for u in 0..n {
510 for v in (u + 1)..n {
511 edges.push((u, v));
512 }
513 }
514 let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
515 let rho = distance_spectral_radius(&g).unwrap();
516 assert!(
517 (rho - f64::from(n - 1)).abs() < 0.5,
518 "K_{n}: ρ_D = {rho}, expected {}",
519 n - 1
520 );
521 }
522 }
523
524 #[test]
525 fn trace_of_distance_matrix_is_zero() {
526 let g = path4();
527 let spec = distance_spectrum(&g).unwrap();
528 let trace: f64 = spec.iter().sum();
529 assert!(trace.abs() < 0.1);
530 }
531}