1#![allow(
15 clippy::cast_possible_truncation,
16 clippy::cast_precision_loss,
17 clippy::many_single_char_names,
18 clippy::needless_range_loop,
19 clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23use std::collections::VecDeque;
24
25pub fn balaban_j_index(graph: &Graph) -> IgraphResult<f64> {
44 let n = graph.vcount() as usize;
45 let m = graph.ecount();
46 if m == 0 || n == 0 {
47 return Ok(0.0);
48 }
49
50 let (dist, sigma) = compute_distances_and_sums(graph, n);
51 let components = count_components(&dist, n);
52
53 let mu = m + components - n;
54 let prefix = m as f64 / (mu as f64 + 1.0);
55
56 let mut sum = 0.0;
57 for (u, v) in graph.edges() {
58 let ui = u as usize;
59 let vi = v as usize;
60 if ui == vi {
61 continue;
62 }
63 let su = sigma[ui];
64 let sv = sigma[vi];
65 if su > 0.0 && sv > 0.0 {
66 sum += 1.0 / (su * sv).sqrt();
67 }
68 }
69
70 Ok(prefix * sum)
71}
72
73fn compute_distances_and_sums(graph: &Graph, n: usize) -> (Vec<u32>, Vec<f64>) {
74 let adj = build_adj_list(graph, n);
75 let mut dist = vec![u32::MAX; n * n];
76
77 for src in 0..n {
78 bfs_distances(&adj, n, src, &mut dist);
79 }
80
81 let mut sigma = vec![0.0_f64; n];
82 for v in 0..n {
83 let mut s = 0.0_f64;
84 for w in 0..n {
85 let d = dist[v * n + w];
86 if d != u32::MAX {
87 s += f64::from(d);
88 }
89 }
90 sigma[v] = s;
91 }
92
93 (dist, sigma)
94}
95
96fn count_components(dist: &[u32], n: usize) -> usize {
97 let mut visited = vec![false; n];
98 let mut components = 0_usize;
99 for v in 0..n {
100 if !visited[v] {
101 components += 1;
102 for w in 0..n {
103 if dist[v * n + w] != u32::MAX {
104 visited[w] = true;
105 }
106 }
107 }
108 }
109 components
110}
111
112fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
113 let mut adj = vec![Vec::new(); n];
114 for (u, v) in graph.edges() {
115 let ui = u as usize;
116 let vi = v as usize;
117 adj[ui].push(vi);
118 if !graph.is_directed() && ui != vi {
119 adj[vi].push(ui);
120 }
121 }
122 adj
123}
124
125fn bfs_distances(adj: &[Vec<usize>], n: usize, src: usize, dist: &mut [u32]) {
126 dist[src * n + src] = 0;
127 let mut queue = VecDeque::new();
128 queue.push_back(src);
129 while let Some(v) = queue.pop_front() {
130 let d = dist[src * n + v];
131 for &w in &adj[v] {
132 if dist[src * n + w] == u32::MAX {
133 dist[src * n + w] = d.saturating_add(1);
134 queue.push_back(w);
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn single_edge() -> Graph {
145 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
146 }
147
148 fn path3() -> Graph {
149 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
150 }
151
152 fn path4() -> Graph {
153 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
154 }
155
156 fn k3() -> Graph {
157 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
158 }
159
160 fn k4() -> Graph {
161 Graph::from_edges(
162 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
163 false,
164 Some(4),
165 )
166 .unwrap()
167 }
168
169 fn cycle4() -> Graph {
170 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
171 }
172
173 fn star5() -> Graph {
174 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
175 }
176
177 #[test]
178 fn bj_empty() {
179 let g = Graph::with_vertices(0);
180 assert!((balaban_j_index(&g).unwrap() - 0.0).abs() < 1e-10);
181 }
182
183 #[test]
184 fn bj_no_edges() {
185 let g = Graph::with_vertices(3);
186 assert!((balaban_j_index(&g).unwrap() - 0.0).abs() < 1e-10);
187 }
188
189 #[test]
190 fn bj_single_edge() {
191 assert!((balaban_j_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
195 }
196
197 #[test]
198 fn bj_path3() {
199 let j = balaban_j_index(&path3()).unwrap();
203 assert!((j - 4.0 / 6.0_f64.sqrt()).abs() < 1e-10);
204 }
205
206 #[test]
207 fn bj_k3() {
208 let j = balaban_j_index(&k3()).unwrap();
212 assert!((j - 2.25).abs() < 1e-10);
213 }
214
215 #[test]
216 fn bj_cycle4() {
217 let j = balaban_j_index(&cycle4()).unwrap();
221 assert!((j - 2.0).abs() < 1e-10);
222 }
223
224 #[test]
225 fn bj_star5() {
226 let j = balaban_j_index(&star5()).unwrap();
230 let expected = 16.0 / (2.0 * 7.0_f64.sqrt());
231 assert!((j - expected).abs() < 1e-10);
232 }
233
234 #[test]
235 fn bj_k4() {
236 let j = balaban_j_index(&k4()).unwrap();
240 assert!((j - 3.0).abs() < 1e-10);
241 }
242
243 #[test]
244 fn bj_path4() {
245 let j = balaban_j_index(&path4()).unwrap();
251 let expected = 3.0 * (2.0 / 24.0_f64.sqrt() + 0.25);
252 assert!((j - expected).abs() < 1e-10);
253 }
254
255 #[test]
256 fn bj_positive_for_connected() {
257 for g in &[
258 single_edge(),
259 path3(),
260 path4(),
261 k3(),
262 k4(),
263 cycle4(),
264 star5(),
265 ] {
266 assert!(balaban_j_index(g).unwrap() > 0.0);
267 }
268 }
269
270 #[test]
271 fn bj_with_isolated() {
272 let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
274 let j = balaban_j_index(&g).unwrap();
275 assert!((j - 1.0).abs() < 1e-10);
279 }
280
281 #[test]
282 fn bj_regular_graphs() {
283 let j = balaban_j_index(&k3()).unwrap();
286 assert!((j - 2.25).abs() < 1e-10);
287 }
288
289 #[test]
290 fn bj_single_vertex() {
291 let g = Graph::with_vertices(1);
292 assert!((balaban_j_index(&g).unwrap() - 0.0).abs() < 1e-10);
293 }
294
295 #[test]
296 fn bj_cycle5() {
297 let g =
301 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap();
302 let j = balaban_j_index(&g).unwrap();
303 assert!((j - 25.0 / 12.0).abs() < 1e-10);
304 }
305
306 #[test]
307 fn bj_cycle6() {
308 let g = Graph::from_edges(
312 &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
313 false,
314 Some(6),
315 )
316 .unwrap();
317 let j = balaban_j_index(&g).unwrap();
318 assert!((j - 2.0).abs() < 1e-10);
319 }
320
321 #[test]
322 fn bj_two_components() {
323 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
328 let j = balaban_j_index(&g).unwrap();
329 assert!((j - 4.0).abs() < 1e-10);
330 }
331
332 #[test]
333 fn bj_two_triangles() {
334 let g = Graph::from_edges(
339 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)],
340 false,
341 Some(6),
342 )
343 .unwrap();
344 let j = balaban_j_index(&g).unwrap();
345 assert!((j - 6.0).abs() < 1e-10);
346 }
347
348 #[test]
349 fn bj_path5() {
350 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
356 let j = balaban_j_index(&g).unwrap();
357 let expected = 4.0 * (2.0 / 70.0_f64.sqrt() + 2.0 / 42.0_f64.sqrt());
358 assert!((j - expected).abs() < 1e-10);
359 }
360
361 #[test]
362 fn bj_k5() {
363 let edges: Vec<(u32, u32)> = (0..5_u32)
367 .flat_map(|i| ((i + 1)..5).map(move |j| (i, j)))
368 .collect();
369 let g = Graph::from_edges(&edges, false, Some(5)).unwrap();
370 let j = balaban_j_index(&g).unwrap();
371 assert!((j - 25.0 / 7.0).abs() < 1e-10);
372 }
373
374 #[test]
375 fn bj_complete_bipartite_k23() {
376 let g = Graph::from_edges(
387 &[(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)],
388 false,
389 Some(5),
390 )
391 .unwrap();
392 let j = balaban_j_index(&g).unwrap();
393 let expected = 12.0 / 30.0_f64.sqrt();
394 assert!((j - expected).abs() < 1e-10);
395 }
396
397 #[test]
398 fn bj_monotone_with_edges() {
399 let p = path4();
401 let jp = balaban_j_index(&p).unwrap();
402 assert!(jp > 0.0);
403
404 let c = cycle4();
406 let jc = balaban_j_index(&c).unwrap();
407 assert!(jc > 0.0);
408 assert!((jp - jc).abs() > 1e-12);
409 }
410
411 #[test]
412 fn bj_isomorphic_same_value() {
413 let g1 = Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap();
415 let g2 = Graph::from_edges(&[(0, 2), (2, 1), (0, 1)], false, Some(3)).unwrap();
416 let j1 = balaban_j_index(&g1).unwrap();
417 let j2 = balaban_j_index(&g2).unwrap();
418 assert!((j1 - j2).abs() < 1e-10);
419 }
420
421 #[test]
422 fn bj_regular_formula() {
423 for g in &[k3(), k4(), cycle4()] {
426 let n = g.vcount() as usize;
427 let m = g.ecount();
428 let j = balaban_j_index(g).unwrap();
429
430 let deg0 = g.degree(0).unwrap();
431 let sigma0: f64 = {
432 let (_, sigma) = compute_distances_and_sums(g, n);
433 sigma[0]
434 };
435 let components = {
436 let (dist, _) = compute_distances_and_sums(g, n);
437 count_components(&dist, n)
438 };
439 let mu = m + components - n;
440 let prefix = m as f64 / (mu as f64 + 1.0);
441 let expected = prefix * m as f64 / sigma0;
442
443 assert!(
444 (j - expected).abs() < 1e-10,
445 "Regular formula failed for {deg0}-regular graph: J={j}, expected={expected}"
446 );
447 }
448 }
449
450 #[test]
451 fn bj_diamond() {
452 let g =
463 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap();
464 let j = balaban_j_index(&g).unwrap();
465 let expected = 5.0 / 3.0 * (1.0 / 3.0 + 2.0 / 3.0_f64.sqrt());
466 assert!((j - expected).abs() < 1e-10);
467 }
468}