rust_igraph/algorithms/properties/
resistance_ratios.rs1#![allow(
13 clippy::cast_lossless,
14 clippy::cast_possible_truncation,
15 clippy::cast_precision_loss,
16 clippy::many_single_char_names,
17 clippy::needless_range_loop,
18 clippy::similar_names,
19 clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24pub fn kirchhoff_index_ratio(graph: &Graph) -> IgraphResult<f64> {
49 let n = graph.vcount() as usize;
50 if n < 2 {
51 return Ok(0.0);
52 }
53
54 let (dist_sum, diameter, connected) = bfs_all_pairs_stats(graph, n)?;
55 if !connected || diameter == 0 {
56 return Ok(0.0);
57 }
58
59 let pairs = n * (n - 1) / 2;
60 Ok(dist_sum as f64 / (pairs as f64 * diameter as f64))
61}
62
63pub fn resistance_regularity(graph: &Graph) -> IgraphResult<f64> {
82 let n = graph.vcount() as usize;
83 if n < 2 {
84 return Ok(0.0);
85 }
86
87 let m = graph.ecount();
88 if m == 0 {
89 return Ok(0.0);
90 }
91
92 let mut degrees = Vec::with_capacity(n);
93 for v in 0..n {
94 degrees.push(graph.degree(v as u32)?);
95 }
96
97 let mut min_r = f64::MAX;
98 let mut max_r = 0.0_f64;
99
100 for v in 0..n {
101 let nbrs = graph.neighbors(v as u32)?;
102 for &u in &nbrs {
103 let ui = u as usize;
104 if ui > v {
105 let min_deg = degrees[v].min(degrees[ui]);
106 if min_deg == 0 {
107 continue;
108 }
109 let r = 1.0 / min_deg as f64;
110 if r < min_r {
111 min_r = r;
112 }
113 if r > max_r {
114 max_r = r;
115 }
116 }
117 }
118 }
119
120 if max_r < 1e-30 {
121 return Ok(0.0);
122 }
123
124 Ok(min_r / max_r)
125}
126
127pub fn spanning_tree_ratio(graph: &Graph) -> IgraphResult<f64> {
147 let n = graph.vcount() as usize;
148 if n < 2 {
149 return Ok(0.0);
150 }
151
152 let m = graph.ecount();
153 if m == 0 {
154 return Ok(0.0);
155 }
156
157 let laplacian = build_laplacian(graph, n)?;
159
160 let eigenvalues = symmetric_eigenvalues(&laplacian, n);
162
163 let eps = 1e-10;
165 let mut log_product = 0.0_f64;
166 let mut nonzero_count = 0_usize;
167 for &ev in &eigenvalues {
168 if ev > eps {
169 log_product += ev.ln();
170 nonzero_count += 1;
171 }
172 }
173
174 if nonzero_count < n - 1 {
175 return Ok(0.0);
177 }
178
179 let log_tau = log_product - (n as f64).ln();
181 let normalizer = (n - 1) as f64 * (n as f64).ln();
182 if normalizer < 1e-30 {
183 return Ok(0.0);
184 }
185
186 Ok((log_tau / normalizer).clamp(0.0, 1.0))
187}
188
189fn bfs_all_pairs_stats(graph: &Graph, n: usize) -> IgraphResult<(u64, u32, bool)> {
191 let mut total_sum = 0_u64;
192 let mut diameter = 0_u32;
193
194 for s in 0..n {
195 let mut dist = vec![u32::MAX; n];
196 dist[s] = 0;
197 let mut queue = std::collections::VecDeque::new();
198 queue.push_back(s);
199 let mut visit_count = 1_usize;
200
201 while let Some(v) = queue.pop_front() {
202 let nbrs = graph.neighbors(v as u32)?;
203 for &u in &nbrs {
204 let ui = u as usize;
205 if dist[ui] == u32::MAX {
206 dist[ui] = dist[v] + 1;
207 visit_count += 1;
208 queue.push_back(ui);
209 }
210 }
211 }
212
213 if visit_count < n {
214 return Ok((0, 0, false));
215 }
216
217 for t in (s + 1)..n {
218 total_sum += dist[t] as u64;
219 if dist[t] > diameter {
220 diameter = dist[t];
221 }
222 }
223 }
224
225 Ok((total_sum, diameter, true))
226}
227
228fn build_laplacian(graph: &Graph, n: usize) -> IgraphResult<Vec<Vec<f64>>> {
230 let mut lap = vec![vec![0.0_f64; n]; n];
231
232 for v in 0..n {
233 let nbrs = graph.neighbors(v as u32)?;
234 lap[v][v] = nbrs.len() as f64;
235 for &u in &nbrs {
236 let ui = u as usize;
237 lap[v][ui] -= 1.0;
238 }
239 }
240
241 Ok(lap)
242}
243
244fn symmetric_eigenvalues(mat: &[Vec<f64>], n: usize) -> Vec<f64> {
246 if n == 0 {
247 return Vec::new();
248 }
249 if n == 1 {
250 return vec![mat[0][0]];
251 }
252
253 let mut a = mat.to_vec();
254 let max_iter = 100 * n * n;
255
256 for _ in 0..max_iter {
257 let mut max_val = 0.0_f64;
259 let mut p = 0_usize;
260 let mut q = 1_usize;
261 for i in 0..n {
262 for j in (i + 1)..n {
263 if a[i][j].abs() > max_val {
264 max_val = a[i][j].abs();
265 p = i;
266 q = j;
267 }
268 }
269 }
270
271 if max_val < 1e-12 {
272 break;
273 }
274
275 let app = a[p][p];
277 let aqq = a[q][q];
278 let apq = a[p][q];
279
280 let theta = if (app - aqq).abs() < 1e-30 {
281 std::f64::consts::FRAC_PI_4
282 } else {
283 0.5 * (2.0 * apq / (app - aqq)).atan()
284 };
285
286 let cos_t = theta.cos();
287 let sin_t = theta.sin();
288
289 let mut new_a = a.clone();
291 for i in 0..n {
292 if i != p && i != q {
293 new_a[i][p] = cos_t * a[i][p] + sin_t * a[i][q];
294 new_a[p][i] = new_a[i][p];
295 new_a[i][q] = -sin_t * a[i][p] + cos_t * a[i][q];
296 new_a[q][i] = new_a[i][q];
297 }
298 }
299 new_a[p][p] = cos_t * cos_t * app + 2.0 * sin_t * cos_t * apq + sin_t * sin_t * aqq;
300 new_a[q][q] = sin_t * sin_t * app - 2.0 * sin_t * cos_t * apq + cos_t * cos_t * aqq;
301 new_a[p][q] = 0.0;
302 new_a[q][p] = 0.0;
303
304 a = new_a;
305 }
306
307 (0..n).map(|i| a[i][i]).collect()
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn empty() -> Graph {
315 Graph::with_vertices(0)
316 }
317
318 fn single() -> Graph {
319 Graph::with_vertices(1)
320 }
321
322 fn single_edge() -> Graph {
323 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
324 }
325
326 fn path3() -> Graph {
327 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
328 }
329
330 fn path4() -> Graph {
331 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
332 }
333
334 fn k3() -> Graph {
335 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
336 }
337
338 fn k4() -> Graph {
339 Graph::from_edges(
340 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
341 false,
342 Some(4),
343 )
344 .unwrap()
345 }
346
347 fn cycle4() -> Graph {
348 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
349 }
350
351 fn star5() -> Graph {
352 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
353 }
354
355 fn disconnected() -> Graph {
356 Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap()
357 }
358
359 #[test]
362 fn kir_empty() {
363 assert!(kirchhoff_index_ratio(&empty()).unwrap().abs() < 1e-10);
364 }
365
366 #[test]
367 fn kir_single() {
368 assert!(kirchhoff_index_ratio(&single()).unwrap().abs() < 1e-10);
369 }
370
371 #[test]
372 fn kir_disconnected() {
373 assert!(kirchhoff_index_ratio(&disconnected()).unwrap().abs() < 1e-10);
374 }
375
376 #[test]
377 fn kir_single_edge() {
378 assert!((kirchhoff_index_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
380 }
381
382 #[test]
383 fn kir_k3() {
384 assert!((kirchhoff_index_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
386 }
387
388 #[test]
389 fn kir_path4() {
390 let r = kirchhoff_index_ratio(&path4()).unwrap();
392 assert!((r - 10.0 / 18.0).abs() < 1e-10);
393 }
394
395 #[test]
396 fn kir_in_01() {
397 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
398 let r = kirchhoff_index_ratio(g).unwrap();
399 assert!(r >= -1e-10);
400 assert!(r <= 1.0 + 1e-10);
401 }
402 }
403
404 #[test]
407 fn rr_empty() {
408 assert!(resistance_regularity(&empty()).unwrap().abs() < 1e-10);
409 }
410
411 #[test]
412 fn rr_single() {
413 assert!(resistance_regularity(&single()).unwrap().abs() < 1e-10);
414 }
415
416 #[test]
417 fn rr_k3() {
418 assert!((resistance_regularity(&k3()).unwrap() - 1.0).abs() < 1e-10);
420 }
421
422 #[test]
423 fn rr_k4() {
424 assert!((resistance_regularity(&k4()).unwrap() - 1.0).abs() < 1e-10);
426 }
427
428 #[test]
429 fn rr_cycle4() {
430 assert!((resistance_regularity(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
432 }
433
434 #[test]
435 fn rr_star5() {
436 assert!((resistance_regularity(&star5()).unwrap() - 1.0).abs() < 1e-10);
440 }
441
442 #[test]
443 fn rr_path3() {
444 assert!((resistance_regularity(&path3()).unwrap() - 1.0).abs() < 1e-10);
449 }
450
451 #[test]
452 fn rr_in_01() {
453 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
454 let r = resistance_regularity(g).unwrap();
455 assert!(r >= -1e-10);
456 assert!(r <= 1.0 + 1e-10);
457 }
458 }
459
460 #[test]
461 fn rr_paw() {
462 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap();
469 assert!((resistance_regularity(&g).unwrap() - 0.5).abs() < 1e-10);
470 }
471
472 #[test]
475 fn str_empty() {
476 assert!(spanning_tree_ratio(&empty()).unwrap().abs() < 1e-10);
477 }
478
479 #[test]
480 fn str_single() {
481 assert!(spanning_tree_ratio(&single()).unwrap().abs() < 1e-10);
482 }
483
484 #[test]
485 fn str_disconnected() {
486 assert!(spanning_tree_ratio(&disconnected()).unwrap().abs() < 1e-10);
487 }
488
489 #[test]
490 fn str_single_edge() {
491 let r = spanning_tree_ratio(&single_edge()).unwrap();
494 assert!(r.abs() < 1e-10);
495 }
496
497 #[test]
498 fn str_k3() {
499 let r = spanning_tree_ratio(&k3()).unwrap();
501 assert!((r - 0.5).abs() < 0.05);
502 }
503
504 #[test]
505 fn str_k4() {
506 let r = spanning_tree_ratio(&k4()).unwrap();
508 assert!((r - 2.0 / 3.0).abs() < 0.05);
509 }
510
511 #[test]
512 fn str_path_tree() {
513 let r = spanning_tree_ratio(&path4()).unwrap();
515 assert!(r.abs() < 1e-10);
516 }
517
518 #[test]
519 fn str_in_01() {
520 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
521 let r = spanning_tree_ratio(g).unwrap();
522 assert!(r >= -1e-10);
523 assert!(r <= 1.0 + 1e-10);
524 }
525 }
526
527 #[test]
528 fn str_cycle4() {
529 let r = spanning_tree_ratio(&cycle4()).unwrap();
531 assert!((r - 1.0 / 3.0).abs() < 0.05);
532 }
533
534 #[test]
537 fn regular_graphs_unit_resistance() {
538 assert!((resistance_regularity(&k3()).unwrap() - 1.0).abs() < 1e-10);
540 assert!((resistance_regularity(&k4()).unwrap() - 1.0).abs() < 1e-10);
541 assert!((resistance_regularity(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
542 }
543
544 #[test]
545 fn trees_zero_spanning_ratio() {
546 assert!(spanning_tree_ratio(&path3()).unwrap().abs() < 1e-10);
548 assert!(spanning_tree_ratio(&path4()).unwrap().abs() < 1e-10);
549 assert!(spanning_tree_ratio(&star5()).unwrap().abs() < 1e-10);
550 }
551
552 #[test]
553 fn complete_diameter_one_kirchhoff() {
554 assert!((kirchhoff_index_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
556 assert!((kirchhoff_index_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
557 }
558}