Skip to main content

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}