1use std::collections::VecDeque;
19
20use crate::core::graph::EdgeId;
21use crate::core::{Graph, IgraphResult, VertexId};
22
23pub fn convergence_degree(graph: &Graph) -> IgraphResult<Vec<f64>> {
48 let (result, _, _) = convergence_degree_full(graph)?;
49 Ok(result)
50}
51
52pub fn convergence_degree_full(graph: &Graph) -> IgraphResult<(Vec<f64>, Vec<f64>, Vec<f64>)> {
70 let n = graph.vcount();
71 let m = graph.ecount();
72 let directed = graph.is_directed();
73
74 let mut ins: Vec<f64> = vec![0.0; m];
75 let mut outs: Vec<f64> = vec![0.0; m];
76
77 if n == 0 || m == 0 {
78 return Ok((vec![f64::NAN; m], ins, outs));
79 }
80
81 let n_us = n as usize;
82 let mut geodist: Vec<i64> = vec![0; n_us];
83 let mut queue: VecDeque<(VertexId, i64)> = VecDeque::new();
84
85 let passes: &[bool] = if directed { &[true, false] } else { &[true] };
89
90 for &is_out_pass in passes {
91 for src in 0..n {
92 geodist.fill(0);
93 queue.clear();
94 geodist[src as usize] = 1;
95 queue.push_back((src, 0));
96
97 while let Some((actnode, actdist)) = queue.pop_front() {
98 let eids = if directed && !is_out_pass {
99 graph.incident_in(actnode)?
100 } else {
101 graph.incident(actnode)?
102 };
103 for &eid in &eids {
104 let (a, b) = graph.edge(eid)?;
105 let neighbor = if a == actnode { b } else { a };
106
107 let nd = geodist[neighbor as usize];
108 if nd != 0 {
109 if nd - 1 == actdist + 1 {
110 bump(
112 directed,
113 is_out_pass,
114 actnode,
115 neighbor,
116 eid,
117 &mut ins,
118 &mut outs,
119 );
120 }
121 } else {
125 geodist[neighbor as usize] = actdist + 2;
126 queue.push_back((neighbor, actdist + 1));
127 bump(
128 directed,
129 is_out_pass,
130 actnode,
131 neighbor,
132 eid,
133 &mut ins,
134 &mut outs,
135 );
136 }
137 }
138 }
139 }
140 }
141
142 let mut result = vec![0.0_f64; m];
143 for i in 0..m {
144 let s = ins[i] + outs[i];
145 result[i] = if s > 0.0 {
146 (ins[i] - outs[i]) / s
147 } else {
148 f64::NAN
149 };
150 }
151 if !directed {
152 for r in &mut result {
153 if *r < 0.0 {
154 *r = -*r;
155 }
156 }
157 }
158
159 Ok((result, ins, outs))
160}
161
162#[inline]
163fn bump(
164 directed: bool,
165 is_out_pass: bool,
166 actnode: VertexId,
167 neighbor: VertexId,
168 eid: EdgeId,
169 ins: &mut [f64],
170 outs: &mut [f64],
171) {
172 let i = eid as usize;
173 if directed {
174 if is_out_pass {
175 ins[i] += 1.0;
176 } else {
177 outs[i] += 1.0;
178 }
179 } else if actnode < neighbor {
180 ins[i] += 1.0;
181 } else {
182 outs[i] += 1.0;
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
191 if a.is_nan() && b.is_nan() {
192 return true;
193 }
194 (a - b).abs() <= tol
195 }
196
197 fn approx_eq_vec(a: &[f64], b: &[f64], tol: f64) -> bool {
198 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| approx_eq(*x, *y, tol))
199 }
200
201 #[test]
202 fn empty_graph_is_empty() {
203 let g = Graph::with_vertices(0);
204 let (r, i, o) = convergence_degree_full(&g).unwrap();
205 assert!(r.is_empty());
206 assert!(i.is_empty());
207 assert!(o.is_empty());
208 }
209
210 #[test]
211 fn no_edges_returns_empty_vectors() {
212 let g = Graph::with_vertices(5);
213 let res = convergence_degree(&g).unwrap();
214 assert!(res.is_empty());
215 }
216
217 #[test]
218 fn upstream_undirected_test_case_matches_out() {
219 let mut g = Graph::with_vertices(7);
224 for (u, v) in [
225 (0, 1),
226 (0, 2),
227 (0, 3),
228 (1, 2),
229 (1, 3),
230 (2, 3),
231 (3, 4),
232 (4, 5),
233 (4, 6),
234 (5, 6),
235 ] {
236 g.add_edge(u, v).unwrap();
237 }
238 let res = convergence_degree(&g).unwrap();
239 let expected = [
240 0.0,
241 0.0,
242 0.6,
243 0.0,
244 0.6,
245 0.6,
246 1.0 / 7.0,
247 2.0 / 3.0,
248 2.0 / 3.0,
249 0.0,
250 ];
251 assert!(
252 approx_eq_vec(&res, &expected, 1e-4),
253 "got {res:?}, want {expected:?}"
254 );
255 }
256
257 #[test]
258 fn upstream_directed_test_case_matches_out() {
259 let mut g = Graph::new(6, true).unwrap();
262 for u in [1u32, 2, 3, 4] {
263 g.add_edge(u, 0).unwrap();
264 }
265 g.add_edge(0, 5).unwrap();
266 let res = convergence_degree(&g).unwrap();
267 let expected = [-1.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0, 2.0 / 3.0];
268 assert!(
269 approx_eq_vec(&res, &expected, 1e-12),
270 "got {res:?}, want {expected:?}"
271 );
272 }
273
274 #[test]
275 fn directed_full_returns_consistent_in_out() {
276 let mut g = Graph::new(6, true).unwrap();
277 for u in [1u32, 2, 3, 4] {
278 g.add_edge(u, 0).unwrap();
279 }
280 g.add_edge(0, 5).unwrap();
281 let (r, ins, outs) = convergence_degree_full(&g).unwrap();
282 for i in 0..4 {
284 assert!(approx_eq(ins[i], 1.0, 1e-12), "ins[{i}] = {}", ins[i]);
285 assert!(approx_eq(outs[i], 2.0, 1e-12), "outs[{i}] = {}", outs[i]);
286 }
287 assert!(approx_eq(ins[4], 5.0, 1e-12), "ins[4] = {}", ins[4]);
289 assert!(approx_eq(outs[4], 1.0, 1e-12), "outs[4] = {}", outs[4]);
290 for i in 0..r.len() {
292 let expected = (ins[i] - outs[i]) / (ins[i] + outs[i]);
293 assert!(approx_eq(r[i], expected, 1e-12));
294 }
295 }
296
297 #[test]
298 fn single_undirected_edge_balanced() {
299 let mut g = Graph::with_vertices(2);
300 g.add_edge(0, 1).unwrap();
301 let (r, ins, outs) = convergence_degree_full(&g).unwrap();
302 assert!(approx_eq(ins[0], 1.0, 1e-12));
305 assert!(approx_eq(outs[0], 1.0, 1e-12));
306 assert!(approx_eq(r[0], 0.0, 1e-12));
308 }
309
310 #[test]
311 fn directed_single_edge() {
312 let mut g = Graph::new(2, true).unwrap();
313 g.add_edge(0, 1).unwrap();
314 let (r, ins, outs) = convergence_degree_full(&g).unwrap();
315 assert!(approx_eq(ins[0], 1.0, 1e-12));
316 assert!(approx_eq(outs[0], 1.0, 1e-12));
317 assert!(approx_eq(r[0], 0.0, 1e-12));
318 }
319
320 #[test]
321 fn undirected_self_loop_yields_nan() {
322 let mut g = Graph::with_vertices(1);
325 g.add_edge(0, 0).unwrap();
326 let res = convergence_degree(&g).unwrap();
327 assert_eq!(res.len(), 1);
328 assert!(res[0].is_nan(), "expected NaN, got {}", res[0]);
329 }
330
331 #[test]
332 fn isolated_vertices_dont_break_things() {
333 let mut g = Graph::with_vertices(5);
335 g.add_edge(0, 1).unwrap();
336 g.add_edge(1, 2).unwrap();
337 let res = convergence_degree(&g).unwrap();
338 assert_eq!(res.len(), 2);
339 let expected = 1.0 / 3.0;
343 for r in &res {
344 assert!(approx_eq(*r, expected, 1e-12), "got {r}, want {expected}");
345 }
346 }
347
348 #[test]
349 fn complete_undirected_is_symmetric() {
350 let mut g = Graph::with_vertices(4);
352 for u in 0..4u32 {
353 for v in (u + 1)..4 {
354 g.add_edge(u, v).unwrap();
355 }
356 }
357 let res = convergence_degree(&g).unwrap();
358 for r in &res {
359 assert!(approx_eq(*r, 0.0, 1e-12), "K_4 edge gave {r}");
360 }
361 }
362
363 #[test]
364 fn star_directed_out_edges_have_negative_convergence() {
365 let mut g = Graph::new(4, true).unwrap();
370 for v in [1, 2, 3] {
371 g.add_edge(0, v).unwrap();
372 }
373 let (r, ins, outs) = convergence_degree_full(&g).unwrap();
374 for &x in &ins {
377 assert!(approx_eq(x, 1.0, 1e-12), "ins = {x}");
378 }
379 for &x in &outs {
382 assert!(approx_eq(x, 1.0, 1e-12), "outs = {x}");
383 }
384 for &x in &r {
385 assert!(approx_eq(x, 0.0, 1e-12), "result = {x}");
386 }
387 }
388
389 #[test]
390 fn parallel_edges_share_credit() {
391 let mut g = Graph::with_vertices(2);
397 g.add_edge(0, 1).unwrap();
398 g.add_edge(0, 1).unwrap();
399 let (_r, ins, outs) = convergence_degree_full(&g).unwrap();
400 for i in 0..2 {
401 assert!(approx_eq(ins[i], 1.0, 1e-12), "ins[{i}] = {}", ins[i]);
402 assert!(approx_eq(outs[i], 1.0, 1e-12), "outs[{i}] = {}", outs[i]);
403 }
404 }
405
406 #[test]
407 fn result_length_matches_ecount() {
408 let mut g = Graph::with_vertices(5);
409 for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)] {
410 g.add_edge(u, v).unwrap();
411 }
412 let res = convergence_degree(&g).unwrap();
413 assert_eq!(res.len(), g.ecount());
414 }
415
416 #[test]
417 fn undirected_results_are_non_negative() {
418 let mut g = Graph::with_vertices(6);
421 for (u, v) in [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)] {
422 g.add_edge(u, v).unwrap();
423 }
424 let res = convergence_degree(&g).unwrap();
425 for r in &res {
426 assert!(r.is_nan() || (*r >= -1e-12 && *r <= 1.0 + 1e-12), "got {r}");
427 }
428 }
429
430 #[test]
431 fn directed_results_within_unit_interval() {
432 let mut g = Graph::new(6, true).unwrap();
433 for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)] {
434 g.add_edge(u, v).unwrap();
435 }
436 let res = convergence_degree(&g).unwrap();
437 for r in &res {
438 assert!(
439 r.is_nan() || (-1.0 - 1e-12 <= *r && *r <= 1.0 + 1e-12),
440 "got {r}"
441 );
442 }
443 }
444
445 #[test]
446 fn ins_plus_outs_strictly_positive_for_simple_path() {
447 let mut g = Graph::with_vertices(4);
450 for (u, v) in [(0, 1), (1, 2), (2, 3)] {
451 g.add_edge(u, v).unwrap();
452 }
453 let (_, ins, outs) = convergence_degree_full(&g).unwrap();
454 for (i, o) in ins.iter().zip(outs.iter()) {
455 assert!(*i + *o > 0.0, "i + o = {} on path edge", i + o);
456 }
457 }
458
459 #[test]
460 fn balanced_directed_cycle_is_zero() {
461 let mut g = Graph::new(5, true).unwrap();
465 for u in 0u32..5 {
466 g.add_edge(u, (u + 1) % 5).unwrap();
467 }
468 let res = convergence_degree(&g).unwrap();
469 let first = res[0];
471 for r in &res {
472 assert!(approx_eq(*r, first, 1e-12), "asymmetric: {res:?}");
473 }
474 }
475}