Skip to main content

rust_igraph/algorithms/properties/
edge_neighborhood_overlap.rs

1//! Edge neighborhood overlap aggregates (ALGO-TR-092).
2//!
3//! Graph-level indices that aggregate the neighborhood overlap of each
4//! edge's endpoints:
5//!
6//! - **Common neighbor sum** `Σ |N(u) ∩ N(v)|` — total shared neighbors
7//! - **Jaccard sum** `Σ |N(u) ∩ N(v)| / |N(u) ∪ N(v)|` — average overlap
8//! - **Overlap coefficient sum** `Σ |N(u) ∩ N(v)| / min(d(u),d(v))` — relative overlap
9//! - **Adamic–Adar sum** `Σ_{(u,v)∈E} Σ_{w∈N(u)∩N(v)} 1/ln(d(w))` — rare-neighbor weight
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::similar_names,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22fn common_neighbors(graph: &Graph, u: u32, v: u32) -> IgraphResult<Vec<u32>> {
23    let nu = graph.neighbors(u)?;
24    let nv = graph.neighbors(v)?;
25    let mut nu_sorted = nu;
26    let mut nv_sorted = nv;
27    nu_sorted.sort_unstable();
28    nv_sorted.sort_unstable();
29
30    let mut common = Vec::new();
31    let (mut i, mut j) = (0, 0);
32    while i < nu_sorted.len() && j < nv_sorted.len() {
33        match nu_sorted[i].cmp(&nv_sorted[j]) {
34            std::cmp::Ordering::Equal => {
35                let w = nu_sorted[i];
36                if w != u && w != v {
37                    common.push(w);
38                }
39                i += 1;
40                j += 1;
41            }
42            std::cmp::Ordering::Less => i += 1,
43            std::cmp::Ordering::Greater => j += 1,
44        }
45    }
46    Ok(common)
47}
48
49/// Compute the sum of common neighbor counts across all edges.
50///
51/// `Σ_{(u,v)∈E} |N(u) ∩ N(v) \ {u,v}|`
52///
53/// Counts the total number of triangles-closing common neighbors.
54/// Returns 0 for edgeless or tree-like graphs. Self-loops are skipped.
55///
56/// # Examples
57///
58/// ```
59/// use rust_igraph::{Graph, edge_common_neighbor_sum};
60///
61/// // K_3: each edge shares 1 common neighbor → 3·1 = 3
62/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
63/// assert_eq!(edge_common_neighbor_sum(&g).unwrap(), 3);
64/// ```
65pub fn edge_common_neighbor_sum(graph: &Graph) -> IgraphResult<u64> {
66    let mut result = 0_u64;
67
68    for (u, v) in graph.edges() {
69        if u == v {
70            continue;
71        }
72        let cn = common_neighbors(graph, u, v)?;
73        result = result.saturating_add(cn.len() as u64);
74    }
75
76    Ok(result)
77}
78
79/// Compute the sum of Jaccard overlap coefficients across all edges.
80///
81/// `Σ_{(u,v)∈E} |N(u) ∩ N(v) \ {u,v}| / |N(u) ∪ N(v) \ {u,v}|`
82///
83/// Each edge contributes a value in [0, 1]. Returns 0.0 for the empty
84/// graph. Self-loops are skipped. Edges where the union is empty
85/// (isolated edge components) contribute 0.
86///
87/// # Examples
88///
89/// ```
90/// use rust_igraph::{Graph, edge_jaccard_sum};
91///
92/// // K_3: each edge: |{w}|/|{w}| = 1 → 3·1 = 3.0
93/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
94/// assert!((edge_jaccard_sum(&g).unwrap() - 3.0).abs() < 1e-10);
95/// ```
96pub fn edge_jaccard_sum(graph: &Graph) -> IgraphResult<f64> {
97    let mut result = 0.0_f64;
98
99    for (u, v) in graph.edges() {
100        if u == v {
101            continue;
102        }
103        let nu = graph.neighbors(u)?;
104        let nv = graph.neighbors(v)?;
105
106        let mut nu_set: Vec<u32> = nu.into_iter().filter(|&w| w != u && w != v).collect();
107        let mut nv_set: Vec<u32> = nv.into_iter().filter(|&w| w != u && w != v).collect();
108        nu_set.sort_unstable();
109        nv_set.sort_unstable();
110
111        let mut inter = 0_usize;
112        let (mut i, mut j) = (0, 0);
113        while i < nu_set.len() && j < nv_set.len() {
114            match nu_set[i].cmp(&nv_set[j]) {
115                std::cmp::Ordering::Equal => {
116                    inter += 1;
117                    i += 1;
118                    j += 1;
119                }
120                std::cmp::Ordering::Less => i += 1,
121                std::cmp::Ordering::Greater => j += 1,
122            }
123        }
124
125        let union_size = nu_set.len() + nv_set.len() - inter;
126        if union_size > 0 {
127            result += inter as f64 / union_size as f64;
128        }
129    }
130
131    Ok(result)
132}
133
134/// Compute the sum of overlap coefficients across all edges.
135///
136/// `Σ_{(u,v)∈E} |N(u) ∩ N(v) \ {u,v}| / min(|N(u)\{v}|, |N(v)\{u}|)`
137///
138/// Each edge contributes a value in [0, 1]. Edges where min neighbor
139/// count (excluding the other endpoint) is zero contribute 0.
140/// Self-loops are skipped. Returns 0.0 for the empty graph.
141///
142/// # Examples
143///
144/// ```
145/// use rust_igraph::{Graph, edge_overlap_sum};
146///
147/// // K_3: each edge: |{w}| / min(1,1) = 1 → 3·1 = 3.0
148/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
149/// assert!((edge_overlap_sum(&g).unwrap() - 3.0).abs() < 1e-10);
150/// ```
151pub fn edge_overlap_sum(graph: &Graph) -> IgraphResult<f64> {
152    let mut result = 0.0_f64;
153
154    for (u, v) in graph.edges() {
155        if u == v {
156            continue;
157        }
158        let cn = common_neighbors(graph, u, v)?;
159        let du = graph.degree(u)?;
160        let dv = graph.degree(v)?;
161        let du_excl = du.saturating_sub(1);
162        let dv_excl = dv.saturating_sub(1);
163        let min_d = du_excl.min(dv_excl);
164        if min_d == 0 {
165            continue;
166        }
167        result += cn.len() as f64 / min_d as f64;
168    }
169
170    Ok(result)
171}
172
173/// Compute the sum of Adamic–Adar indices across all edges.
174///
175/// `Σ_{(u,v)∈E} Σ_{w ∈ N(u) ∩ N(v)} 1/ln(d(w))`
176///
177/// Common neighbors with higher degree contribute less (rare shared
178/// neighbors are more informative). Returns 0.0 for the empty graph.
179/// Vertices with degree ≤ 1 contribute infinity in theory; we skip
180/// them (they can't be common neighbors of an edge's endpoints in
181/// a simple graph with ≥ 3 vertices). Self-loops are skipped.
182///
183/// # Examples
184///
185/// ```
186/// use rust_igraph::{Graph, edge_adamic_adar_sum};
187///
188/// // K_3: each edge has 1 common neighbor of degree 2
189/// // → 3 · (1/ln(2)) ≈ 3/0.6931 ≈ 4.328
190/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
191/// let expected = 3.0 / 2.0_f64.ln();
192/// assert!((edge_adamic_adar_sum(&g).unwrap() - expected).abs() < 1e-10);
193/// ```
194pub fn edge_adamic_adar_sum(graph: &Graph) -> IgraphResult<f64> {
195    let mut result = 0.0_f64;
196
197    for (u, v) in graph.edges() {
198        if u == v {
199            continue;
200        }
201        let cn = common_neighbors(graph, u, v)?;
202        for &w in &cn {
203            let dw = graph.degree(w)?;
204            if dw >= 2 {
205                result += 1.0 / (dw as f64).ln();
206            }
207        }
208    }
209
210    Ok(result)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn single_edge() -> Graph {
218        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
219    }
220
221    fn path3() -> Graph {
222        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
223    }
224
225    fn k3() -> Graph {
226        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
227    }
228
229    fn k4() -> Graph {
230        Graph::from_edges(
231            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
232            false,
233            Some(4),
234        )
235        .unwrap()
236    }
237
238    fn cycle4() -> Graph {
239        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
240    }
241
242    fn star5() -> Graph {
243        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
244    }
245
246    fn paw() -> Graph {
247        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
248    }
249
250    // --- edge_common_neighbor_sum ---
251
252    #[test]
253    fn cn_empty() {
254        let g = Graph::with_vertices(0);
255        assert_eq!(edge_common_neighbor_sum(&g).unwrap(), 0);
256    }
257
258    #[test]
259    fn cn_isolated() {
260        let g = Graph::with_vertices(5);
261        assert_eq!(edge_common_neighbor_sum(&g).unwrap(), 0);
262    }
263
264    #[test]
265    fn cn_single_edge() {
266        // No common neighbors
267        assert_eq!(edge_common_neighbor_sum(&single_edge()).unwrap(), 0);
268    }
269
270    #[test]
271    fn cn_path3() {
272        // No edge shares a common neighbor
273        assert_eq!(edge_common_neighbor_sum(&path3()).unwrap(), 0);
274    }
275
276    #[test]
277    fn cn_k3() {
278        // Each of 3 edges has 1 common neighbor → 3
279        assert_eq!(edge_common_neighbor_sum(&k3()).unwrap(), 3);
280    }
281
282    #[test]
283    fn cn_k4() {
284        // Each of 6 edges has 2 common neighbors → 12
285        assert_eq!(edge_common_neighbor_sum(&k4()).unwrap(), 12);
286    }
287
288    #[test]
289    fn cn_star5() {
290        // No edge shares a common neighbor (leaves not connected)
291        assert_eq!(edge_common_neighbor_sum(&star5()).unwrap(), 0);
292    }
293
294    #[test]
295    fn cn_cycle4() {
296        // Opposite edges share 0 cn; adjacent shares 0 cn
297        // (0,1) cn with {3,0}∩{0,2}\{0,1} → check: N(0)\{1}={3}, N(1)\{0}={2} → ∩=∅
298        // Actually all edges of C4: each pair has no common neighbor
299        assert_eq!(edge_common_neighbor_sum(&cycle4()).unwrap(), 0);
300    }
301
302    #[test]
303    fn cn_paw() {
304        // (0,1): N(0)\{1}={2}, N(1)\{0}={2} → {2} → 1
305        // (0,2): N(0)\{2}={1}, N(2)\{0}={1,3} → {1} → 1
306        // (1,2): N(1)\{2}={0}, N(2)\{1}={0,3} → {0} → 1
307        // (2,3): N(2)\{3}={0,1}, N(3)\{2}={} → {} → 0
308        assert_eq!(edge_common_neighbor_sum(&paw()).unwrap(), 3);
309    }
310
311    // --- edge_jaccard_sum ---
312
313    #[test]
314    fn jac_empty() {
315        let g = Graph::with_vertices(0);
316        assert!(edge_jaccard_sum(&g).unwrap().abs() < 1e-10);
317    }
318
319    #[test]
320    fn jac_single_edge() {
321        // N(0)\{1} = ∅, N(1)\{0} = ∅, union=0 → contribute 0
322        assert!(edge_jaccard_sum(&single_edge()).unwrap().abs() < 1e-10);
323    }
324
325    #[test]
326    fn jac_path3() {
327        // (0,1): N(0)\{1}=∅, N(1)\{0}={2} → inter=0, union=1 → 0
328        // (1,2): N(1)\{2}={0}, N(2)\{1}=∅ → inter=0, union=1 → 0
329        assert!(edge_jaccard_sum(&path3()).unwrap().abs() < 1e-10);
330    }
331
332    #[test]
333    fn jac_k3() {
334        // Each edge: N(u)\{v}={w}, N(v)\{u}={w} → inter=1, union=1 → 1.0
335        // 3 edges → 3.0
336        assert!((edge_jaccard_sum(&k3()).unwrap() - 3.0).abs() < 1e-10);
337    }
338
339    #[test]
340    fn jac_k4() {
341        // Each edge: |CN|=2, union=|N(u)\{v}|+|N(v)\{u}|-|CN|=2+2-2=2
342        // Jaccard = 2/2 = 1.0, 6 edges → 6.0
343        assert!((edge_jaccard_sum(&k4()).unwrap() - 6.0).abs() < 1e-10);
344    }
345
346    #[test]
347    fn jac_star5() {
348        // (0,k): N(0)\{k}={others 3 leaves}, N(k)\{0}=∅ → inter=0, union=3 → 0
349        assert!(edge_jaccard_sum(&star5()).unwrap().abs() < 1e-10);
350    }
351
352    #[test]
353    fn jac_paw() {
354        // (0,1): N(0)\{1}={2}, N(1)\{0}={2} → inter=1, union=1 → 1.0
355        // (0,2): N(0)\{2}={1}, N(2)\{0}={1,3} → inter=1, union=2 → 0.5
356        // (1,2): N(1)\{2}={0}, N(2)\{1}={0,3} → inter=1, union=2 → 0.5
357        // (2,3): N(2)\{3}={0,1}, N(3)\{2}=∅ → inter=0, union=2 → 0
358        let expected = 1.0 + 0.5 + 0.5 + 0.0;
359        assert!((edge_jaccard_sum(&paw()).unwrap() - expected).abs() < 1e-10);
360    }
361
362    // --- edge_overlap_sum ---
363
364    #[test]
365    fn ovl_empty() {
366        let g = Graph::with_vertices(0);
367        assert!(edge_overlap_sum(&g).unwrap().abs() < 1e-10);
368    }
369
370    #[test]
371    fn ovl_single_edge() {
372        // min(d-1, d-1) = min(0,0) = 0 → skip → 0
373        assert!(edge_overlap_sum(&single_edge()).unwrap().abs() < 1e-10);
374    }
375
376    #[test]
377    fn ovl_path3() {
378        // (0,1): min(0,1)=0 → skip
379        // (1,2): min(1,0)=0 → skip
380        assert!(edge_overlap_sum(&path3()).unwrap().abs() < 1e-10);
381    }
382
383    #[test]
384    fn ovl_k3() {
385        // Each edge: |CN|=1, min(d-1,d-1)=min(1,1)=1 → 1/1=1
386        // 3 edges → 3.0
387        assert!((edge_overlap_sum(&k3()).unwrap() - 3.0).abs() < 1e-10);
388    }
389
390    #[test]
391    fn ovl_k4() {
392        // Each edge: |CN|=2, min(d-1,d-1)=min(2,2)=2 → 2/2=1
393        // 6 edges → 6.0
394        assert!((edge_overlap_sum(&k4()).unwrap() - 6.0).abs() < 1e-10);
395    }
396
397    #[test]
398    fn ovl_star5() {
399        // (0,k): |CN|=0, min(3,0)=0 → skip
400        assert!(edge_overlap_sum(&star5()).unwrap().abs() < 1e-10);
401    }
402
403    #[test]
404    fn ovl_paw() {
405        // (0,1): |CN|=1, min(1,1)=1 → 1.0
406        // (0,2): |CN|=1, min(1,2)=1 → 1.0
407        // (1,2): |CN|=1, min(1,2)=1 → 1.0
408        // (2,3): |CN|=0, min(2,0)=0 → skip
409        assert!((edge_overlap_sum(&paw()).unwrap() - 3.0).abs() < 1e-10);
410    }
411
412    // --- edge_adamic_adar_sum ---
413
414    #[test]
415    fn aa_empty() {
416        let g = Graph::with_vertices(0);
417        assert!(edge_adamic_adar_sum(&g).unwrap().abs() < 1e-10);
418    }
419
420    #[test]
421    fn aa_single_edge() {
422        assert!(edge_adamic_adar_sum(&single_edge()).unwrap().abs() < 1e-10);
423    }
424
425    #[test]
426    fn aa_path3() {
427        assert!(edge_adamic_adar_sum(&path3()).unwrap().abs() < 1e-10);
428    }
429
430    #[test]
431    fn aa_k3() {
432        // Each edge: 1 common neighbor of degree 2
433        // 3 · 1/ln(2)
434        let expected = 3.0 / 2.0_f64.ln();
435        assert!((edge_adamic_adar_sum(&k3()).unwrap() - expected).abs() < 1e-10);
436    }
437
438    #[test]
439    fn aa_k4() {
440        // Each edge: 2 common neighbors each of degree 3
441        // 6 · 2/ln(3) = 12/ln(3)
442        let expected = 12.0 / 3.0_f64.ln();
443        assert!((edge_adamic_adar_sum(&k4()).unwrap() - expected).abs() < 1e-10);
444    }
445
446    #[test]
447    fn aa_star5() {
448        assert!(edge_adamic_adar_sum(&star5()).unwrap().abs() < 1e-10);
449    }
450
451    #[test]
452    fn aa_paw() {
453        // (0,1): CN={2}, d(2)=3 → 1/ln(3)
454        // (0,2): CN={1}, d(1)=2 → 1/ln(2)
455        // (1,2): CN={0}, d(0)=2 → 1/ln(2)
456        // (2,3): CN=∅ → 0
457        let expected = 1.0 / 3.0_f64.ln() + 2.0 / 2.0_f64.ln();
458        assert!((edge_adamic_adar_sum(&paw()).unwrap() - expected).abs() < 1e-10);
459    }
460
461    // --- cross-consistency ---
462
463    #[test]
464    fn cn_eq_3_times_triangles_for_complete() {
465        // In K_n, each edge has (n-2) common neighbors
466        // total = C(n,2)·(n-2) = n(n-1)(n-2)/2
467        // K_3: 3·1=3, K_4: 6·2=12
468        assert_eq!(edge_common_neighbor_sum(&k3()).unwrap(), 3);
469        assert_eq!(edge_common_neighbor_sum(&k4()).unwrap(), 12);
470    }
471
472    #[test]
473    fn jaccard_perfect_for_complete() {
474        // In K_n, Jaccard = 1.0 for every edge → sum = m
475        assert!((edge_jaccard_sum(&k3()).unwrap() - 3.0).abs() < 1e-10);
476        assert!((edge_jaccard_sum(&k4()).unwrap() - 6.0).abs() < 1e-10);
477    }
478
479    #[test]
480    fn overlap_perfect_for_complete() {
481        // In K_n, overlap coeff = 1.0 for every edge → sum = m
482        assert!((edge_overlap_sum(&k3()).unwrap() - 3.0).abs() < 1e-10);
483        assert!((edge_overlap_sum(&k4()).unwrap() - 6.0).abs() < 1e-10);
484    }
485
486    #[test]
487    fn cn_bounded_by_m_times_n() {
488        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
489            let cn = edge_common_neighbor_sum(g).unwrap();
490            let bound = g.ecount() as u64 * u64::from(g.vcount());
491            assert!(cn <= bound);
492        }
493    }
494
495    #[test]
496    fn jaccard_in_0_m() {
497        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
498            let j = edge_jaccard_sum(g).unwrap();
499            let m = g.ecount() as f64;
500            assert!(j >= -1e-10);
501            assert!(j <= m + 1e-10);
502        }
503    }
504
505    #[test]
506    fn aa_nonnegative() {
507        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
508            assert!(edge_adamic_adar_sum(g).unwrap() >= -1e-10);
509        }
510    }
511}