use std::collections::BTreeSet;
use super::Adjacency;
fn neighbours(g: &Adjacency, i: usize) -> BTreeSet<usize> {
g.und.get(i).map(|l| l.iter().map(|&(j, _)| j).collect()).unwrap_or_default()
}
#[must_use]
pub fn common_neighbours(g: &Adjacency, a: usize, b: usize) -> usize {
if a >= g.n || b >= g.n {
return 0;
}
let (na, nb) = (neighbours(g, a), neighbours(g, b));
na.intersection(&nb).count()
}
#[must_use]
pub fn jaccard(g: &Adjacency, a: usize, b: usize) -> f32 {
if a >= g.n || b >= g.n {
return 0.0;
}
let (na, nb) = (neighbours(g, a), neighbours(g, b));
let inter = na.intersection(&nb).count();
let union = na.union(&nb).count();
if union == 0 { 0.0 } else { inter as f32 / union as f32 }
}
#[must_use]
pub fn adamic_adar(g: &Adjacency, a: usize, b: usize) -> f32 {
if a >= g.n || b >= g.n {
return 0.0;
}
let (na, nb) = (neighbours(g, a), neighbours(g, b));
na.intersection(&nb)
.filter_map(|&z| {
let d = g.degree(z) as f32;
if d > 1.0 { Some(1.0 / d.ln()) } else { None }
})
.sum()
}
#[must_use]
pub fn cosine(g: &Adjacency, a: usize, b: usize) -> f32 {
if a >= g.n || b >= g.n {
return 0.0;
}
let (na, nb) = (neighbours(g, a), neighbours(g, b));
let inter = na.intersection(&nb).count() as f32;
let denom = ((na.len() * nb.len()) as f32).sqrt();
if denom <= 0.0 { 0.0 } else { inter / denom }
}
#[must_use]
pub fn most_similar(g: &Adjacency, a: usize, top_k: usize) -> Vec<(usize, f32)> {
if a >= g.n {
return Vec::new();
}
let mut scored: Vec<(usize, f32)> =
(0..g.n).filter(|&b| b != a).map(|b| (b, jaccard(g, a, b))).filter(|&(_, s)| s > 0.0).collect();
scored.sort_by(|x, y| y.1.partial_cmp(&x.1).unwrap_or(std::cmp::Ordering::Equal).then(x.0.cmp(&y.0)));
scored.truncate(top_k);
scored
}
#[cfg(test)]
mod tests {
use super::*;
fn shared() -> Adjacency {
Adjacency::from_edges(5, &[(0, 2), (0, 3), (1, 2), (1, 3), (4, 2)])
}
#[test]
fn common_neighbours_and_jaccard() {
let g = shared();
assert_eq!(common_neighbours(&g, 0, 1), 2, "0 and 1 share {{2,3}}");
assert!((jaccard(&g, 0, 1) - 1.0).abs() < 1e-6);
assert!((jaccard(&g, 0, 4) - 0.5).abs() < 1e-6);
}
#[test]
fn cosine_matches_hand_calc() {
let g = shared();
assert!((cosine(&g, 0, 1) - 1.0).abs() < 1e-6);
assert!((cosine(&g, 0, 4) - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-4);
}
#[test]
fn adamic_adar_weights_rare_shared_neighbours() {
let g = shared();
let expect = 1.0 / 3f32.ln() + 1.0 / 2f32.ln();
assert!((adamic_adar(&g, 0, 1) - expect).abs() < 1e-4, "AA = {}", adamic_adar(&g, 0, 1));
}
#[test]
fn most_similar_ranks_the_twin_first() {
let g = shared();
let top = most_similar(&g, 0, 3);
assert_eq!(top[0].0, 1, "node 1 is 0's exact neighbourhood twin");
assert!((top[0].1 - 1.0).abs() < 1e-6);
assert!(top.iter().any(|&(n, _)| n == 4));
}
#[test]
fn out_of_range_and_empty_are_safe() {
let g = shared();
assert_eq!(common_neighbours(&g, 0, 99), 0);
assert_eq!(jaccard(&g, 99, 0), 0.0);
assert!(most_similar(&g, 99, 5).is_empty());
}
}