rust_igraph/algorithms/properties/efficiency.rs
1//! Global + local efficiency (ALGO-PR-029, ALGO-PR-030).
2//!
3//! Counterpart of `igraph_global_efficiency()` from
4//! `references/igraph/src/paths/shortest_paths.c:392` (and the underlying
5//! `igraph_i_average_path_length_unweighted` helper at line 38, called
6//! with `invert = true, unconn = false`); plus `igraph_local_efficiency()`
7//! at line 688 and `igraph_average_local_efficiency()` at line 842.
8//!
9//! Definition (global): `E_g = 1/(N*(N-1)) * sum_{i != j} 1/d(i, j)`.
10//! Pairs that are unreachable contribute 0 (treated as `1/inf`).
11//! Returns `None` when `vcount < 2` (no ordered pairs to average over —
12//! upstream returns NaN; we model this as `Option<f64>` to match the
13//! rest of the Phase-1 averaging APIs).
14//!
15//! Definition (local): for each vertex `v`, let `N(v)` be the unique
16//! neighbours (self-loops dropped). The local efficiency around `v` is
17//! the average inverse shortest-path distance between every ordered
18//! pair `(s, t)` of distinct neighbours, computed in the *induced
19//! subgraph* `G \ {v}` — i.e. paths must not pass through `v`.
20//! `local_efficiency[v] = 0` when `|N(v)| < 2`. The average over all
21//! vertices is `average_local_efficiency`.
22//!
23//! Phase-1 minimal slice: unweighted only. Edge directions are followed
24//! for directed graphs (`distances()` walks OUT edges) — that matches
25//! upstream's `directed = true` default.
26//!
27//! Reference: V. Latora and M. Marchiori, "Efficient Behavior of
28//! Small-World Networks", Phys. Rev. Lett. 87, 198701 (2001); and
29//! I. Vragović, E. Louis, A. Díaz-Guilera, "Efficiency of informational
30//! transfer in regular and complex networks", Phys. Rev. E 71, 036122
31//! (2005).
32
33use std::collections::VecDeque;
34
35use crate::algorithms::paths::distances::distances;
36use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
37
38/// Global efficiency of `graph` — average inverse pairwise shortest
39/// distance over all `N*(N-1)` ordered vertex pairs. Pairs that are
40/// unreachable contribute 0.
41///
42/// Returns `None` when `vcount() < 2` (no pairs).
43///
44/// For undirected graphs each unordered pair contributes twice (once
45/// per direction); the divisor `N*(N-1)` mirrors that, so the formula
46/// is the standard Latora–Marchiori definition.
47///
48/// Counterpart of
49/// `igraph_global_efficiency(_, NULL_weights, _, /*directed=*/true)`.
50///
51/// # Examples
52///
53/// ```
54/// use rust_igraph::{Graph, global_efficiency};
55///
56/// // K3: every ordered pair is at distance 1 → mean inverse distance = 1.
57/// let mut g = Graph::with_vertices(3);
58/// g.add_edge(0, 1).unwrap();
59/// g.add_edge(1, 2).unwrap();
60/// g.add_edge(2, 0).unwrap();
61/// assert_eq!(global_efficiency(&g).unwrap(), Some(1.0));
62///
63/// // Path 0-1-2-3: 12 ordered pairs. d=1 ×6 → 6; d=2 ×4 → 2; d=3 ×2 → 2/3.
64/// // Sum = 26/3; /12 = 13/18.
65/// let mut g = Graph::with_vertices(4);
66/// for i in 0..3u32 { g.add_edge(i, i + 1).unwrap(); }
67/// let e = global_efficiency(&g).unwrap().unwrap();
68/// assert!((e - 13.0 / 18.0).abs() < 1e-12);
69/// ```
70pub fn global_efficiency(graph: &Graph) -> IgraphResult<Option<f64>> {
71 let n = graph.vcount();
72 if n < 2 {
73 return Ok(None);
74 }
75 let mut sum_inv: f64 = 0.0;
76 for v in 0..n {
77 let d = distances(graph, v)?;
78 let v_us = v as usize;
79 for (target, &val) in d.iter().enumerate() {
80 if target == v_us {
81 continue;
82 }
83 if let Some(dist) = val {
84 if dist > 0 {
85 sum_inv += 1.0 / f64::from(dist);
86 }
87 }
88 }
89 }
90 let n_f = f64::from(n);
91 let denom = n_f * (n_f - 1.0);
92 Ok(Some(sum_inv / denom))
93}
94
95/// Per-vertex local efficiency. For each vertex `v`, computes the
96/// average inverse distance between every ordered pair of distinct
97/// vertices in `N(v)` (the unique non-self neighbours of `v`),
98/// **measured in the subgraph obtained by removing `v`** — paths must
99/// not pass through `v`. Pairs unreachable in `G \ {v}` contribute 0.
100///
101/// `local_efficiency[v] = 0` whenever `|N(v)| < 2`. For directed graphs,
102/// `N(v)` is the set of OUT-neighbours and BFS follows OUT edges (mirrors
103/// the simple `directed=true, mode=OUT` slice we expose for [`distances`]
104/// and [`global_efficiency`]).
105///
106/// Counterpart of
107/// `igraph_local_efficiency(_, NULL_weights, _, igraph_vss_all(),
108/// /*directed=*/true, /*mode=*/IGRAPH_OUT)`.
109///
110/// # Examples
111///
112/// ```
113/// use rust_igraph::{Graph, local_efficiency};
114///
115/// // K4: each vertex has 3 neighbours forming a K3, all at distance 1
116/// // in G \ {v} → local efficiency = 1.0 at every vertex.
117/// let mut g = Graph::with_vertices(4);
118/// for i in 0..4u32 {
119/// for j in (i + 1)..4u32 { g.add_edge(i, j).unwrap(); }
120/// }
121/// assert_eq!(local_efficiency(&g).unwrap(), vec![1.0, 1.0, 1.0, 1.0]);
122/// ```
123pub fn local_efficiency(graph: &Graph) -> IgraphResult<Vec<f64>> {
124 let n = graph.vcount();
125 let n_us = n as usize;
126 let mut result = vec![0.0_f64; n_us];
127
128 if n < 3 {
129 return Ok(result);
130 }
131
132 let mut nei_mask = vec![false; n_us];
133
134 for v in 0..n {
135 let raw = graph.neighbors(v)?;
136 let mut neighbors: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
137 neighbors.sort_unstable();
138 neighbors.dedup();
139 let k = neighbors.len();
140 if k < 2 {
141 continue;
142 }
143
144 for &u in &neighbors {
145 nei_mask[u as usize] = true;
146 }
147
148 let mut sum_inv: f64 = 0.0;
149 for &source in &neighbors {
150 let mut dist: Vec<Option<u32>> = vec![None; n_us];
151 dist[source as usize] = Some(0);
152 let mut queue: VecDeque<VertexId> = VecDeque::new();
153 queue.push_back(source);
154 let mut reached = 0_usize;
155
156 'bfs: while let Some(node) = queue.pop_front() {
157 let cur = dist[node as usize].ok_or(IgraphError::Internal(
158 "dequeued unvisited vertex in local_efficiency BFS",
159 ))?;
160 if node != source && nei_mask[node as usize] && cur > 0 {
161 sum_inv += 1.0 / f64::from(cur);
162 reached += 1;
163 if reached + 1 == k {
164 break 'bfs;
165 }
166 }
167 let next = cur.checked_add(1).ok_or(IgraphError::Internal(
168 "distance overflow in local_efficiency",
169 ))?;
170 for w in graph.neighbors(node)? {
171 if w == v {
172 continue;
173 }
174 if dist[w as usize].is_none() {
175 dist[w as usize] = Some(next);
176 queue.push_back(w);
177 }
178 }
179 }
180 }
181
182 for &u in &neighbors {
183 nei_mask[u as usize] = false;
184 }
185
186 let k_u32 = u32::try_from(k)
187 .map_err(|_| IgraphError::Internal("neighbour count exceeds u32::MAX"))?;
188 let k_f = f64::from(k_u32);
189 result[v as usize] = sum_inv / (k_f * (k_f - 1.0));
190 }
191
192 Ok(result)
193}
194
195/// Average of [`local_efficiency`] over all `N` vertices. By upstream
196/// convention, returns `0.0` when `vcount < 3` (no vertex can have two
197/// distinct neighbours, so every per-vertex value is trivially 0).
198///
199/// Counterpart of
200/// `igraph_average_local_efficiency(_, NULL_weights, _,
201/// /*directed=*/true, /*mode=*/IGRAPH_OUT)`.
202///
203/// # Examples
204///
205/// ```
206/// use rust_igraph::{Graph, average_local_efficiency};
207///
208/// // K4: every vertex has local efficiency 1.0, so the average is 1.0.
209/// let mut g = Graph::with_vertices(4);
210/// for i in 0..4u32 {
211/// for j in (i + 1)..4u32 { g.add_edge(i, j).unwrap(); }
212/// }
213/// assert_eq!(average_local_efficiency(&g).unwrap(), 1.0);
214/// ```
215pub fn average_local_efficiency(graph: &Graph) -> IgraphResult<f64> {
216 let n = graph.vcount();
217 if n < 3 {
218 return Ok(0.0);
219 }
220 let local = local_efficiency(graph)?;
221 let n_f = f64::from(n);
222 Ok(local.iter().sum::<f64>() / n_f)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn close(a: f64, b: f64, tol: f64) {
230 assert!((a - b).abs() < tol, "{a} vs {b}");
231 }
232
233 #[test]
234 fn empty_graph_returns_none() {
235 let g = Graph::with_vertices(0);
236 assert_eq!(global_efficiency(&g).unwrap(), None);
237 }
238
239 #[test]
240 fn singleton_returns_none() {
241 let g = Graph::with_vertices(1);
242 assert_eq!(global_efficiency(&g).unwrap(), None);
243 }
244
245 #[test]
246 fn no_edges_two_vertices_zero() {
247 // No reachable pairs → sum 0 → 0/2 = 0.
248 let g = Graph::with_vertices(2);
249 assert_eq!(global_efficiency(&g).unwrap(), Some(0.0));
250 }
251
252 #[test]
253 fn complete_graph_one() {
254 // K_n: every ordered pair at distance 1 → mean = 1.
255 for n in 2..=5u32 {
256 let mut g = Graph::with_vertices(n);
257 for u in 0..n {
258 for v in (u + 1)..n {
259 g.add_edge(u, v).unwrap();
260 }
261 }
262 close(global_efficiency(&g).unwrap().unwrap(), 1.0, 1e-12);
263 }
264 }
265
266 #[test]
267 fn path_3_two_thirds() {
268 // 0-1-2: distances among 6 ordered pairs: (0,1)=1, (0,2)=2,
269 // (1,0)=1, (1,2)=1, (2,0)=2, (2,1)=1. Inverses sum = 4*1 + 2*0.5 = 5.
270 // /6 = 5/6.
271 let mut g = Graph::with_vertices(3);
272 g.add_edge(0, 1).unwrap();
273 g.add_edge(1, 2).unwrap();
274 let e = global_efficiency(&g).unwrap().unwrap();
275 close(e, 5.0 / 6.0, 1e-12);
276 }
277
278 #[test]
279 fn path_4_thirteen_eighteenths() {
280 // 0-1-2-3 ordered pairs:
281 // d=1: 6 pairs → contrib 6.
282 // d=2: 4 pairs → contrib 2.
283 // d=3: 2 pairs → contrib 2/3.
284 // Sum = 26/3. /12 = 13/18.
285 let mut g = Graph::with_vertices(4);
286 for i in 0..3u32 {
287 g.add_edge(i, i + 1).unwrap();
288 }
289 let e = global_efficiency(&g).unwrap().unwrap();
290 close(e, 13.0 / 18.0, 1e-12);
291 }
292
293 #[test]
294 fn isolated_vertices_zero() {
295 // Three isolated vertices: no reachable pairs → 0.
296 let g = Graph::with_vertices(3);
297 assert_eq!(global_efficiency(&g).unwrap(), Some(0.0));
298 }
299
300 #[test]
301 fn disconnected_two_components() {
302 // {0-1}, {2}: ordered pairs (0,1) and (1,0) at d=1 → contrib 2.
303 // Other 4 pairs unreachable → 0. Sum = 2; /6 = 1/3.
304 let mut g = Graph::with_vertices(3);
305 g.add_edge(0, 1).unwrap();
306 let e = global_efficiency(&g).unwrap().unwrap();
307 close(e, 1.0 / 3.0, 1e-12);
308 }
309
310 #[test]
311 fn directed_path_uses_out_edges() {
312 // 0->1->2: reachable pairs (0,1)=1, (0,2)=2, (1,2)=1.
313 // Inverses sum = 1 + 0.5 + 1 = 2.5. /6 = 5/12.
314 let mut g = Graph::new(3, true).unwrap();
315 g.add_edge(0, 1).unwrap();
316 g.add_edge(1, 2).unwrap();
317 let e = global_efficiency(&g).unwrap().unwrap();
318 close(e, 5.0 / 12.0, 1e-12);
319 }
320
321 #[test]
322 fn star_efficiency() {
323 // Star K_{1,3}: centre 0; leaves 1,2,3.
324 // Pairs at d=1: (0,1)(0,2)(0,3) ×2 = 6 → contrib 6.
325 // Pairs at d=2 (between leaves): 3 unordered, ×2 = 6 → contrib 3.
326 // Sum = 9. N=4 → /12 = 0.75.
327 let mut g = Graph::with_vertices(4);
328 for v in 1..4u32 {
329 g.add_edge(0, v).unwrap();
330 }
331 let e = global_efficiency(&g).unwrap().unwrap();
332 close(e, 0.75, 1e-12);
333 }
334
335 #[test]
336 fn matches_harmonic_centrality_average() {
337 // Identity: global_efficiency = sum(harmonic_centrality) / n.
338 // Verify on a small graph.
339 let mut g = Graph::with_vertices(5);
340 g.add_edge(0, 1).unwrap();
341 g.add_edge(0, 2).unwrap();
342 g.add_edge(2, 3).unwrap();
343 g.add_edge(3, 4).unwrap();
344 let e = global_efficiency(&g).unwrap().unwrap();
345 let h = crate::algorithms::properties::harmonic::harmonic_centrality(&g).unwrap();
346 let avg: f64 = h.iter().sum::<f64>() / f64::from(u32::try_from(h.len()).unwrap());
347 close(e, avg, 1e-12);
348 }
349
350 #[test]
351 fn efficiency_in_range() {
352 // For any unweighted graph: 0 ≤ E_g ≤ 1.
353 let mut g = Graph::with_vertices(6);
354 g.add_edge(0, 1).unwrap();
355 g.add_edge(1, 2).unwrap();
356 g.add_edge(2, 3).unwrap();
357 g.add_edge(3, 4).unwrap();
358 g.add_edge(4, 5).unwrap();
359 g.add_edge(0, 5).unwrap(); // 6-cycle
360 let e = global_efficiency(&g).unwrap().unwrap();
361 assert!((0.0..=1.0).contains(&e), "{e}");
362 }
363
364 // ----- local_efficiency / average_local_efficiency tests (PR-030) -----
365
366 #[test]
367 fn local_eff_empty_graph_empty_vec() {
368 let g = Graph::with_vertices(0);
369 assert!(local_efficiency(&g).unwrap().is_empty());
370 }
371
372 #[test]
373 fn local_eff_singleton_zero() {
374 let g = Graph::with_vertices(1);
375 assert_eq!(local_efficiency(&g).unwrap(), vec![0.0]);
376 }
377
378 #[test]
379 fn local_eff_two_vertices_zero() {
380 // n < 3 → all per-vertex values are 0 by convention.
381 let mut g = Graph::with_vertices(2);
382 g.add_edge(0, 1).unwrap();
383 assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0]);
384 }
385
386 #[test]
387 fn local_eff_isolated_three_vertices_zero() {
388 let g = Graph::with_vertices(3);
389 assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0]);
390 }
391
392 #[test]
393 fn local_eff_path_three_zero() {
394 // 0-1-2: vertex 1 has neighbours {0,2}; in G\{1} they are
395 // disconnected → no path → contribution 0. Vertices 0 and 2
396 // have one neighbour each → 0 by convention.
397 let mut g = Graph::with_vertices(3);
398 g.add_edge(0, 1).unwrap();
399 g.add_edge(1, 2).unwrap();
400 assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0]);
401 }
402
403 #[test]
404 fn local_eff_triangle_zero() {
405 // K3: every vertex has 2 neighbours; in G\{v} those two
406 // neighbours are disconnected (only the edge through v is gone,
407 // but the direct edge between them remains).
408 // Wait: in K3, vertices 1 and 2 are adjacent. So in G\{0},
409 // 1-2 is still an edge → distance 1 → local[0] = 2 / (2*1) = 1.0.
410 let mut g = Graph::with_vertices(3);
411 g.add_edge(0, 1).unwrap();
412 g.add_edge(1, 2).unwrap();
413 g.add_edge(2, 0).unwrap();
414 let local = local_efficiency(&g).unwrap();
415 for v in &local {
416 close(*v, 1.0, 1e-12);
417 }
418 }
419
420 #[test]
421 fn local_eff_k4_all_one() {
422 // K4: each vertex's neighbour set is K3, all at distance 1 in
423 // G\{v} → local efficiency 1 at every vertex.
424 let mut g = Graph::with_vertices(4);
425 for i in 0..4u32 {
426 for j in (i + 1)..4u32 {
427 g.add_edge(i, j).unwrap();
428 }
429 }
430 let local = local_efficiency(&g).unwrap();
431 for v in &local {
432 close(*v, 1.0, 1e-12);
433 }
434 }
435
436 #[test]
437 fn local_eff_star_zero() {
438 // K_{1,3}: centre 0 has 3 neighbours but in G\{0} they are
439 // mutually unreachable → local[0] = 0. Each leaf has 1
440 // neighbour → 0 by convention.
441 let mut g = Graph::with_vertices(4);
442 for v in 1..4u32 {
443 g.add_edge(0, v).unwrap();
444 }
445 assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0, 0.0]);
446 }
447
448 #[test]
449 fn local_eff_diamond() {
450 // Diamond: 0-1, 0-2, 0-3, 1-2, 2-3 (so vertex 2 is adjacent to
451 // 0,1,3; vertex 0 to 1,2,3; vertices 1 and 3 are adjacent to
452 // 0 and 2 each).
453 // Vertex 0's neighbours = {1,2,3}. In G\{0}: 1-2 (edge), 2-3
454 // (edge), 1-3 (no edge, but reachable via 2 at distance 2).
455 // Ordered pair contributions: (1,2)=(2,1)=1; (2,3)=(3,2)=1;
456 // (1,3)=(3,1)=1/2. Sum = 4 + 1 = 5. /(3*2)=6 → 5/6.
457 // Vertex 2 is symmetric to vertex 0 → also 5/6.
458 // Vertex 1's neighbours = {0,2}: edge 0-2 in G\{1} → distance
459 // 1. Sum=2; /(2*1)=2 → 1.0.
460 // Vertex 3 symmetric to vertex 1 → 1.0.
461 let mut g = Graph::with_vertices(4);
462 g.add_edge(0, 1).unwrap();
463 g.add_edge(0, 2).unwrap();
464 g.add_edge(0, 3).unwrap();
465 g.add_edge(1, 2).unwrap();
466 g.add_edge(2, 3).unwrap();
467 let local = local_efficiency(&g).unwrap();
468 close(local[0], 5.0 / 6.0, 1e-12);
469 close(local[1], 1.0, 1e-12);
470 close(local[2], 5.0 / 6.0, 1e-12);
471 close(local[3], 1.0, 1e-12);
472 }
473
474 #[test]
475 fn local_eff_ignores_self_loops_and_parallel_edges() {
476 // Self-loops should not count as neighbours; parallel edges
477 // collapse to one neighbour.
478 let mut g = Graph::with_vertices(3);
479 g.add_edge(0, 0).unwrap(); // self-loop on 0
480 g.add_edge(0, 1).unwrap();
481 g.add_edge(0, 1).unwrap(); // parallel
482 g.add_edge(0, 2).unwrap();
483 g.add_edge(1, 2).unwrap();
484 // Vertex 0 has neighbours {1,2}; 1-2 is edge → local[0] = 1.0.
485 // Vertex 1 has neighbours {0,2}; 0-2 is edge → local[1] = 1.0.
486 // Vertex 2 has neighbours {0,1}; 0-1 is edge → local[2] = 1.0.
487 let local = local_efficiency(&g).unwrap();
488 for v in &local {
489 close(*v, 1.0, 1e-12);
490 }
491 }
492
493 #[test]
494 fn average_local_eff_empty_zero() {
495 let g = Graph::with_vertices(0);
496 close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
497 }
498
499 #[test]
500 fn average_local_eff_lt_three_zero() {
501 let mut g = Graph::with_vertices(2);
502 g.add_edge(0, 1).unwrap();
503 close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
504 }
505
506 #[test]
507 fn average_local_eff_k4_one() {
508 let mut g = Graph::with_vertices(4);
509 for i in 0..4u32 {
510 for j in (i + 1)..4u32 {
511 g.add_edge(i, j).unwrap();
512 }
513 }
514 close(average_local_efficiency(&g).unwrap(), 1.0, 1e-12);
515 }
516
517 #[test]
518 fn average_local_eff_diamond() {
519 // Diamond has local = [5/6, 1, 5/6, 1] → avg = (5/6+1+5/6+1)/4 = 11/12.
520 let mut g = Graph::with_vertices(4);
521 g.add_edge(0, 1).unwrap();
522 g.add_edge(0, 2).unwrap();
523 g.add_edge(0, 3).unwrap();
524 g.add_edge(1, 2).unwrap();
525 g.add_edge(2, 3).unwrap();
526 close(average_local_efficiency(&g).unwrap(), 11.0 / 12.0, 1e-12);
527 }
528
529 #[test]
530 fn average_local_eff_path_zero() {
531 // Path 0-1-2-3: vertex 1's neighbours {0,2} disconnected in
532 // G\{1}; vertex 2 symmetric. All others <2 neighbours → all 0.
533 let mut g = Graph::with_vertices(4);
534 for i in 0..3u32 {
535 g.add_edge(i, i + 1).unwrap();
536 }
537 close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
538 }
539}