Skip to main content

rust_igraph/algorithms/properties/
similarity.rs

1//! Vertex similarity measures (ALGO-PR-036).
2//!
3//! Cocitation, bibliographic coupling, Jaccard similarity, and Dice
4//! similarity for vertex pairs.
5
6use crate::core::{Graph, IgraphResult, VertexId};
7
8/// Computes the cocitation scores between all pairs of vertices.
9///
10/// Two vertices are cocited if there is a third vertex citing (having
11/// outgoing edges to) both. The cocitation score of vertices `u` and `v`
12/// is the number of vertices that have edges to both `u` and `v`.
13///
14/// For undirected graphs, this equals the number of common neighbors.
15///
16/// Returns a flat vector of length `n * n` in row-major order, where
17/// `result[u * n + v]` is the cocitation count for the pair `(u, v)`.
18///
19/// # Examples
20///
21/// ```
22/// use rust_igraph::{Graph, cocitation};
23///
24/// // 2 → 0, 2 → 1 means vertices 0 and 1 are cocited by vertex 2
25/// let mut g = Graph::new(3, true).unwrap();
26/// g.add_edge(2, 0).unwrap();
27/// g.add_edge(2, 1).unwrap();
28///
29/// let scores = cocitation(&g).unwrap();
30/// assert_eq!(scores[0 * 3 + 1], 1); // cocitation(0,1) = 1
31/// assert_eq!(scores[1 * 3 + 0], 1); // symmetric
32/// ```
33pub fn cocitation(graph: &Graph) -> IgraphResult<Vec<u32>> {
34    cocitation_impl(graph, true)
35}
36
37/// Computes bibliographic coupling scores between all pairs of vertices.
38///
39/// Two vertices are bibliographically coupled if they both cite (have
40/// outgoing edges to) a common third vertex. The coupling score of `u`
41/// and `v` is the number of vertices that both `u` and `v` have edges to.
42///
43/// For undirected graphs, this equals the number of common neighbors
44/// (same as cocitation).
45///
46/// Returns a flat vector of length `n * n` in row-major order.
47///
48/// # Examples
49///
50/// ```
51/// use rust_igraph::{Graph, bibcoupling};
52///
53/// // 0 → 2, 1 → 2 means vertices 0 and 1 both cite vertex 2
54/// let mut g = Graph::new(3, true).unwrap();
55/// g.add_edge(0, 2).unwrap();
56/// g.add_edge(1, 2).unwrap();
57///
58/// let scores = bibcoupling(&g).unwrap();
59/// assert_eq!(scores[0 * 3 + 1], 1); // coupling(0,1) = 1
60/// assert_eq!(scores[1 * 3 + 0], 1); // symmetric
61/// ```
62pub fn bibcoupling(graph: &Graph) -> IgraphResult<Vec<u32>> {
63    cocitation_impl(graph, false)
64}
65
66/// Computes Jaccard similarity coefficients for given vertex pairs.
67///
68/// The Jaccard similarity of two vertices is:
69/// `|N(u) ∩ N(v)| / |N(u) ∪ N(v)|`
70///
71/// where `N(v)` is the neighbor set of `v`. If both vertices are isolated
72/// (no neighbors), the similarity is 0.
73///
74/// # Arguments
75///
76/// * `graph` — the input graph (treated as undirected for neighbor lookup).
77/// * `pairs` — slice of `(u, v)` vertex pairs to compute similarity for.
78///
79/// # Examples
80///
81/// ```
82/// use rust_igraph::{Graph, similarity_jaccard_pairs};
83///
84/// let mut g = Graph::with_vertices(5);
85/// g.add_edge(0, 2).unwrap();
86/// g.add_edge(0, 3).unwrap();
87/// g.add_edge(1, 2).unwrap();
88/// g.add_edge(1, 3).unwrap();
89/// g.add_edge(1, 4).unwrap();
90///
91/// let sim = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
92/// // N(0) = {2,3}, N(1) = {2,3,4}, intersection = {2,3}, union = {2,3,4}
93/// // Jaccard = 2/3
94/// assert!((sim[0] - 2.0 / 3.0).abs() < 1e-10);
95/// ```
96pub fn similarity_jaccard_pairs(
97    graph: &Graph,
98    pairs: &[(VertexId, VertexId)],
99) -> IgraphResult<Vec<f64>> {
100    let n = graph.vcount();
101    let adj = build_sorted_adjacency(graph)?;
102    let mut result = Vec::with_capacity(pairs.len());
103
104    for &(u, v) in pairs {
105        if u >= n || v >= n {
106            return Err(crate::core::error::IgraphError::InvalidArgument(
107                "vertex ID out of range in similarity_jaccard_pairs".to_string(),
108            ));
109        }
110        if u == v {
111            result.push(1.0);
112            continue;
113        }
114        let (isect, union_size) =
115            sorted_intersection_union_size(&adj[u as usize], &adj[v as usize]);
116        if union_size == 0 {
117            result.push(0.0);
118        } else {
119            #[allow(clippy::cast_precision_loss)]
120            result.push(isect as f64 / union_size as f64);
121        }
122    }
123
124    Ok(result)
125}
126
127/// Computes Dice similarity coefficients for given vertex pairs.
128///
129/// The Dice similarity of two vertices is:
130/// `2 * |N(u) ∩ N(v)| / (|N(u)| + |N(v)|)`
131///
132/// This is related to Jaccard by: `Dice = 2*J / (1+J)`.
133///
134/// # Arguments
135///
136/// * `graph` — the input graph (treated as undirected for neighbor lookup).
137/// * `pairs` — slice of `(u, v)` vertex pairs to compute similarity for.
138///
139/// # Examples
140///
141/// ```
142/// use rust_igraph::{Graph, similarity_dice_pairs};
143///
144/// let mut g = Graph::with_vertices(5);
145/// g.add_edge(0, 2).unwrap();
146/// g.add_edge(0, 3).unwrap();
147/// g.add_edge(1, 2).unwrap();
148/// g.add_edge(1, 3).unwrap();
149/// g.add_edge(1, 4).unwrap();
150///
151/// let sim = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
152/// // N(0)={2,3}, N(1)={2,3,4}, |intersection|=2, |N(0)|+|N(1)|=5
153/// // Dice = 2*2/5 = 0.8
154/// assert!((sim[0] - 0.8).abs() < 1e-10);
155/// ```
156pub fn similarity_dice_pairs(
157    graph: &Graph,
158    pairs: &[(VertexId, VertexId)],
159) -> IgraphResult<Vec<f64>> {
160    let n = graph.vcount();
161    let adj = build_sorted_adjacency(graph)?;
162    let mut result = Vec::with_capacity(pairs.len());
163
164    for &(u, v) in pairs {
165        if u >= n || v >= n {
166            return Err(crate::core::error::IgraphError::InvalidArgument(
167                "vertex ID out of range in similarity_dice_pairs".to_string(),
168            ));
169        }
170        if u == v {
171            result.push(1.0);
172            continue;
173        }
174        let deg_sum = adj[u as usize].len() + adj[v as usize].len();
175        if deg_sum == 0 {
176            result.push(0.0);
177        } else {
178            let (isect, _) = sorted_intersection_union_size(&adj[u as usize], &adj[v as usize]);
179            #[allow(clippy::cast_precision_loss)]
180            result.push(2.0 * isect as f64 / deg_sum as f64);
181        }
182    }
183
184    Ok(result)
185}
186
187/// Computes the inverse log-weighted (Adamic-Adar) similarity for given vertex pairs.
188///
189/// The Adamic-Adar index of two vertices u and v is:
190/// `sum_{w in N(u) ∩ N(v)} 1 / log(deg(w))`
191///
192/// where the sum runs over all common neighbors. High-degree common
193/// neighbors contribute less, reflecting that a shared low-degree
194/// neighbor is more informative. Isolated vertices have zero similarity
195/// to any other vertex.
196///
197/// # Arguments
198///
199/// * `graph` — the input graph (treated as undirected for neighbor lookup).
200/// * `pairs` — slice of `(u, v)` vertex pairs to compute similarity for.
201///
202/// # Examples
203///
204/// ```
205/// use rust_igraph::{Graph, similarity_inverse_log_weighted_pairs};
206///
207/// let mut g = Graph::with_vertices(4);
208/// g.add_edge(0, 2).unwrap();
209/// g.add_edge(0, 3).unwrap();
210/// g.add_edge(1, 2).unwrap();
211/// g.add_edge(1, 3).unwrap();
212///
213/// let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
214/// // Common neighbors: 2 (deg=2), 3 (deg=2)
215/// // AA = 1/ln(2) + 1/ln(2) = 2/ln(2)
216/// assert!((sim[0] - 2.0 / 2.0_f64.ln()).abs() < 1e-10);
217/// ```
218pub fn similarity_inverse_log_weighted_pairs(
219    graph: &Graph,
220    pairs: &[(VertexId, VertexId)],
221) -> IgraphResult<Vec<f64>> {
222    let n = graph.vcount();
223    let adj = build_sorted_adjacency(graph)?;
224
225    // Precompute degrees for weight calculation
226    #[allow(clippy::cast_precision_loss)]
227    let weights: Vec<f64> = adj
228        .iter()
229        .map(|neighbors| {
230            let deg = neighbors.len();
231            if deg > 1 {
232                1.0 / (deg as f64).ln()
233            } else {
234                0.0
235            }
236        })
237        .collect();
238
239    let mut result = Vec::with_capacity(pairs.len());
240
241    for &(u, v) in pairs {
242        if u >= n || v >= n {
243            return Err(crate::core::error::IgraphError::InvalidArgument(
244                "vertex ID out of range in similarity_inverse_log_weighted_pairs".to_string(),
245            ));
246        }
247        if u == v {
248            result.push(0.0);
249            continue;
250        }
251        let common = sorted_intersection_weighted(&adj[u as usize], &adj[v as usize], &weights);
252        result.push(common);
253    }
254
255    Ok(result)
256}
257
258fn sorted_intersection_weighted(a: &[VertexId], b: &[VertexId], weights: &[f64]) -> f64 {
259    let mut i = 0;
260    let mut j = 0;
261    let mut sum = 0.0_f64;
262
263    while i < a.len() && j < b.len() {
264        match a[i].cmp(&b[j]) {
265            std::cmp::Ordering::Less => i += 1,
266            std::cmp::Ordering::Greater => j += 1,
267            std::cmp::Ordering::Equal => {
268                sum += weights[a[i] as usize];
269                i += 1;
270                j += 1;
271            }
272        }
273    }
274
275    sum
276}
277
278/// Internal: cocitation (mode=OUT on neighbors → predecessors share successors)
279/// or bibcoupling (mode=IN → successors share predecessors).
280fn cocitation_impl(graph: &Graph, is_cocitation: bool) -> IgraphResult<Vec<u32>> {
281    let n = graph.vcount() as usize;
282    let mut result = vec![0u32; n * n];
283
284    if n == 0 {
285        return Ok(result);
286    }
287
288    let ecount = graph.ecount();
289    let directed = graph.is_directed();
290
291    // Build adjacency: for cocitation we need in-neighbors (who cites whom)
292    // For bibcoupling we need out-neighbors (whom does each vertex cite)
293    // Cocitation: for each vertex w, get its out-neighbors. Each pair of
294    //   out-neighbors (u,v) is cocited by w.
295    // Bibcoupling: for each vertex w, get its in-neighbors. Each pair of
296    //   in-neighbors (u,v) share w as a common citation target.
297
298    // Build the relevant adjacency list
299    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
300
301    for eid in 0..ecount {
302        #[allow(clippy::cast_possible_truncation)]
303        let (src, tgt) = graph.edge(eid as u32)?;
304
305        if directed {
306            if is_cocitation {
307                // We want: for vertex src, its out-neighbors are those it cites
308                // Cocitation counts common in-neighbors, so we collect out-adj
309                adj[src as usize].push(tgt);
310            } else {
311                // Bibcoupling: collect in-adj (who points to tgt)
312                adj[tgt as usize].push(src);
313            }
314        } else {
315            // Undirected: both directions
316            adj[src as usize].push(tgt);
317            if src != tgt {
318                adj[tgt as usize].push(src);
319            }
320        }
321    }
322
323    // For each vertex w, iterate over pairs of its neighbors
324    for neighbors in &adj {
325        for (i, &nei_u) in neighbors.iter().enumerate() {
326            let u = nei_u as usize;
327            for &nei_v in &neighbors[(i + 1)..] {
328                let v = nei_v as usize;
329                result[u * n + v] += 1;
330                result[v * n + u] += 1;
331            }
332        }
333    }
334
335    Ok(result)
336}
337
338fn build_sorted_adjacency(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
339    let n = graph.vcount() as usize;
340    let ecount = graph.ecount();
341    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
342
343    for eid in 0..ecount {
344        #[allow(clippy::cast_possible_truncation)]
345        let (src, tgt) = graph.edge(eid as u32)?;
346        adj[src as usize].push(tgt);
347        if src != tgt {
348            adj[tgt as usize].push(src);
349        }
350    }
351
352    for neighbors in &mut adj {
353        neighbors.sort_unstable();
354        neighbors.dedup();
355    }
356
357    Ok(adj)
358}
359
360/// Compute the full Jaccard similarity matrix for all vertex pairs.
361///
362/// Returns a flat vector of length `n * n` in row-major order, where
363/// `result[u * n + v]` is the Jaccard similarity between vertices `u`
364/// and `v`. The diagonal is 1.0 (a vertex is perfectly similar to
365/// itself). For pairs of isolated vertices, the similarity is 0.0.
366///
367/// Counterpart of `igraph_similarity_jaccard(_, _, vss_all(), IGRAPH_ALL, loops=false)`
368///
369/// # Examples
370///
371/// ```
372/// use rust_igraph::{Graph, similarity_jaccard};
373///
374/// let mut g = Graph::with_vertices(4);
375/// g.add_edge(0, 2).unwrap();
376/// g.add_edge(0, 3).unwrap();
377/// g.add_edge(1, 2).unwrap();
378/// g.add_edge(1, 3).unwrap();
379/// let sim = similarity_jaccard(&g).unwrap();
380/// // N(0)={2,3}, N(1)={2,3} → Jaccard(0,1)=1.0
381/// assert!((sim[0 * 4 + 1] - 1.0).abs() < 1e-10);
382/// // N(0)={2,3}, N(2)={0,1} → intersection empty, union={0,1,2,3}→0
383/// assert!(sim[0 * 4 + 2].abs() < 1e-10);
384/// ```
385pub fn similarity_jaccard(graph: &Graph) -> IgraphResult<Vec<f64>> {
386    let n = graph.vcount() as usize;
387    let adj = build_sorted_adjacency(graph)?;
388    let mut result = vec![0.0_f64; n * n];
389
390    for u in 0..n {
391        result[u * n + u] = 1.0;
392        for v in (u + 1)..n {
393            let (isect, union_size) = sorted_intersection_union_size(&adj[u], &adj[v]);
394            let sim = if union_size == 0 {
395                0.0
396            } else {
397                #[allow(clippy::cast_precision_loss)]
398                {
399                    isect as f64 / union_size as f64
400                }
401            };
402            result[u * n + v] = sim;
403            result[v * n + u] = sim;
404        }
405    }
406
407    Ok(result)
408}
409
410/// Compute the full Dice similarity matrix for all vertex pairs.
411///
412/// Returns a flat vector of length `n * n` in row-major order.
413/// `Dice(u, v) = 2 * |N(u) ∩ N(v)| / (|N(u)| + |N(v)|)`.
414/// The diagonal is 1.0. For pairs of isolated vertices, similarity is 0.0.
415///
416/// Counterpart of `igraph_similarity_dice(_, _, vss_all(), IGRAPH_ALL, loops=false)`
417///
418/// # Examples
419///
420/// ```
421/// use rust_igraph::{Graph, similarity_dice};
422///
423/// let mut g = Graph::with_vertices(4);
424/// g.add_edge(0, 2).unwrap();
425/// g.add_edge(0, 3).unwrap();
426/// g.add_edge(1, 2).unwrap();
427/// g.add_edge(1, 3).unwrap();
428/// let sim = similarity_dice(&g).unwrap();
429/// // N(0)={2,3}, N(1)={2,3}, |intersect|=2, deg_sum=4 → Dice=4/4=1.0
430/// assert!((sim[0 * 4 + 1] - 1.0).abs() < 1e-10);
431/// ```
432pub fn similarity_dice(graph: &Graph) -> IgraphResult<Vec<f64>> {
433    let n = graph.vcount() as usize;
434    let adj = build_sorted_adjacency(graph)?;
435    let mut result = vec![0.0_f64; n * n];
436
437    for u in 0..n {
438        result[u * n + u] = 1.0;
439        for v in (u + 1)..n {
440            let deg_sum = adj[u].len() + adj[v].len();
441            let sim = if deg_sum == 0 {
442                0.0
443            } else {
444                let (isect, _) = sorted_intersection_union_size(&adj[u], &adj[v]);
445                #[allow(clippy::cast_precision_loss)]
446                {
447                    2.0 * isect as f64 / deg_sum as f64
448                }
449            };
450            result[u * n + v] = sim;
451            result[v * n + u] = sim;
452        }
453    }
454
455    Ok(result)
456}
457
458/// Compute the full inverse-log-weighted (Adamic-Adar) similarity matrix.
459///
460/// Returns a flat vector of length `n * n` in row-major order.
461/// The diagonal is 0.0 (matching igraph convention for self-pairs in
462/// inverse-log-weighted similarity).
463///
464/// Counterpart of `igraph_similarity_inverse_log_weighted(_, _, vss_all(), IGRAPH_ALL)`
465///
466/// # Examples
467///
468/// ```
469/// use rust_igraph::{Graph, similarity_inverse_log_weighted};
470///
471/// let mut g = Graph::with_vertices(4);
472/// g.add_edge(0, 2).unwrap();
473/// g.add_edge(0, 3).unwrap();
474/// g.add_edge(1, 2).unwrap();
475/// g.add_edge(1, 3).unwrap();
476/// let sim = similarity_inverse_log_weighted(&g).unwrap();
477/// // Common neighbors of (0,1): 2 (deg=2), 3 (deg=2)
478/// // AA = 1/ln(2) + 1/ln(2) = 2/ln(2)
479/// assert!((sim[0 * 4 + 1] - 2.0 / 2.0_f64.ln()).abs() < 1e-10);
480/// ```
481pub fn similarity_inverse_log_weighted(graph: &Graph) -> IgraphResult<Vec<f64>> {
482    let n = graph.vcount() as usize;
483    let adj = build_sorted_adjacency(graph)?;
484
485    #[allow(clippy::cast_precision_loss)]
486    let weights: Vec<f64> = adj
487        .iter()
488        .map(|neighbors| {
489            let deg = neighbors.len();
490            if deg > 1 {
491                1.0 / (deg as f64).ln()
492            } else {
493                0.0
494            }
495        })
496        .collect();
497
498    let mut result = vec![0.0_f64; n * n];
499
500    for u in 0..n {
501        for v in (u + 1)..n {
502            let sim = sorted_intersection_weighted(&adj[u], &adj[v], &weights);
503            result[u * n + v] = sim;
504            result[v * n + u] = sim;
505        }
506    }
507
508    Ok(result)
509}
510
511fn sorted_intersection_union_size(a: &[VertexId], b: &[VertexId]) -> (usize, usize) {
512    let mut i = 0;
513    let mut j = 0;
514    let mut isect = 0usize;
515
516    while i < a.len() && j < b.len() {
517        match a[i].cmp(&b[j]) {
518            std::cmp::Ordering::Less => i += 1,
519            std::cmp::Ordering::Greater => j += 1,
520            std::cmp::Ordering::Equal => {
521                isect += 1;
522                i += 1;
523                j += 1;
524            }
525        }
526    }
527
528    let union_size = a.len() + b.len() - isect;
529    (isect, union_size)
530}
531
532/// Computes Jaccard similarity coefficients for pairs of vertices
533/// connected by the given edges.
534///
535/// For each edge ID in `eids`, this retrieves the endpoint pair `(u, v)`
536/// and computes the Jaccard similarity between `u` and `v`.
537///
538/// Counterpart of `igraph_similarity_jaccard_es()`.
539///
540/// # Examples
541///
542/// ```
543/// use rust_igraph::{Graph, similarity_jaccard_es};
544///
545/// let mut g = Graph::with_vertices(5);
546/// g.add_edge(0, 2).unwrap(); // edge 0
547/// g.add_edge(0, 3).unwrap(); // edge 1
548/// g.add_edge(1, 2).unwrap(); // edge 2
549/// g.add_edge(1, 3).unwrap(); // edge 3
550/// g.add_edge(1, 4).unwrap(); // edge 4
551/// g.add_edge(0, 1).unwrap(); // edge 5
552///
553/// // Jaccard of edge 5 endpoints (0,1):
554/// // N(0)={1,2,3}, N(1)={0,2,3,4}, intersection={2,3}, union={0,1,2,3,4}
555/// // Jaccard = 2/5 = 0.4
556/// let sim = similarity_jaccard_es(&g, &[5]).unwrap();
557/// assert!((sim[0] - 0.4).abs() < 1e-10);
558/// ```
559pub fn similarity_jaccard_es(graph: &Graph, eids: &[u32]) -> IgraphResult<Vec<f64>> {
560    let pairs: Vec<(VertexId, VertexId)> = eids
561        .iter()
562        .map(|&e| graph.edge(e))
563        .collect::<IgraphResult<Vec<_>>>()?;
564    similarity_jaccard_pairs(graph, &pairs)
565}
566
567/// Computes Dice similarity coefficients for pairs of vertices
568/// connected by the given edges.
569///
570/// For each edge ID in `eids`, this retrieves the endpoint pair `(u, v)`
571/// and computes the Dice similarity between `u` and `v`.
572///
573/// The Dice similarity is related to Jaccard by: `Dice = 2*J / (1+J)`.
574///
575/// Counterpart of `igraph_similarity_dice_es()`.
576///
577/// # Examples
578///
579/// ```
580/// use rust_igraph::{Graph, similarity_dice_es};
581///
582/// let mut g = Graph::with_vertices(5);
583/// g.add_edge(0, 2).unwrap(); // edge 0
584/// g.add_edge(0, 3).unwrap(); // edge 1
585/// g.add_edge(1, 2).unwrap(); // edge 2
586/// g.add_edge(1, 3).unwrap(); // edge 3
587/// g.add_edge(1, 4).unwrap(); // edge 4
588/// g.add_edge(0, 1).unwrap(); // edge 5
589///
590/// // Dice of edge 5 endpoints (0,1):
591/// // N(0)={1,2,3}, N(1)={0,2,3,4}, |intersection|=2, |N(0)|+|N(1)|=7
592/// // Dice = 2*2/7 ≈ 0.5714
593/// let sim = similarity_dice_es(&g, &[5]).unwrap();
594/// assert!((sim[0] - 4.0 / 7.0).abs() < 1e-10);
595/// ```
596pub fn similarity_dice_es(graph: &Graph, eids: &[u32]) -> IgraphResult<Vec<f64>> {
597    let pairs: Vec<(VertexId, VertexId)> = eids
598        .iter()
599        .map(|&e| graph.edge(e))
600        .collect::<IgraphResult<Vec<_>>>()?;
601    similarity_dice_pairs(graph, &pairs)
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_cocitation_directed() {
610        // 2→0, 2→1 means 0 and 1 are cocited by 2
611        let mut g = Graph::new(3, true).unwrap();
612        g.add_edge(2, 0).unwrap();
613        g.add_edge(2, 1).unwrap();
614
615        let scores = cocitation(&g).unwrap();
616        // scores is 3×3 row-major; check (0,1) and (1,0)
617        assert_eq!(scores[1], 1); // row 0, col 1
618        assert_eq!(scores[3], 1); // row 1, col 0
619        assert_eq!(scores[0], 0); // row 0, col 0
620    }
621
622    #[test]
623    fn test_cocitation_multiple_citers() {
624        // 2→0, 2→1, 3→0, 3→1 → cocitation(0,1) = 2
625        let mut g = Graph::new(4, true).unwrap();
626        g.add_edge(2, 0).unwrap();
627        g.add_edge(2, 1).unwrap();
628        g.add_edge(3, 0).unwrap();
629        g.add_edge(3, 1).unwrap();
630
631        let scores = cocitation(&g).unwrap();
632        // 4×4 matrix: (0,1) = index 1, (1,0) = index 4
633        assert_eq!(scores[1], 2);
634        assert_eq!(scores[4], 2);
635    }
636
637    #[test]
638    fn test_bibcoupling_directed() {
639        // 0→2, 1→2 means 0 and 1 share citation target 2
640        let mut g = Graph::new(3, true).unwrap();
641        g.add_edge(0, 2).unwrap();
642        g.add_edge(1, 2).unwrap();
643
644        let scores = bibcoupling(&g).unwrap();
645        // 3×3 matrix: (0,1) = index 1, (1,0) = index 3
646        assert_eq!(scores[1], 1);
647        assert_eq!(scores[3], 1);
648    }
649
650    #[test]
651    fn test_cocitation_undirected() {
652        // Undirected: common neighbors
653        let mut g = Graph::with_vertices(4);
654        g.add_edge(0, 2).unwrap();
655        g.add_edge(1, 2).unwrap();
656        g.add_edge(0, 3).unwrap();
657        g.add_edge(1, 3).unwrap();
658
659        let scores = cocitation(&g).unwrap();
660        // 4×4 matrix: (0,1) = index 1, (1,0) = index 4
661        // 0 and 1 share neighbors 2 and 3
662        assert_eq!(scores[1], 2);
663        assert_eq!(scores[4], 2);
664    }
665
666    #[test]
667    fn test_cocitation_empty() {
668        let g = Graph::with_vertices(0);
669        let scores = cocitation(&g).unwrap();
670        assert!(scores.is_empty());
671    }
672
673    #[test]
674    fn test_cocitation_isolated() {
675        let g = Graph::with_vertices(3);
676        let scores = cocitation(&g).unwrap();
677        assert!(scores.iter().all(|&x| x == 0));
678    }
679
680    #[test]
681    fn test_jaccard_complete_overlap() {
682        // Triangle: N(0)={1,2}, N(1)={0,2} → intersection={2}, union={0,1,2}→Jaccard=1/3
683        // Wait: N(0)={1,2}, N(1)={0,2}, intersection={2}, union={0,1,2}
684        let mut g = Graph::with_vertices(3);
685        g.add_edge(0, 1).unwrap();
686        g.add_edge(1, 2).unwrap();
687        g.add_edge(0, 2).unwrap();
688
689        let sim = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
690        // N(0)={1,2}, N(1)={0,2}, intersection={2}, union={0,1,2}, J=1/3
691        assert!((sim[0] - 1.0 / 3.0).abs() < 1e-10);
692    }
693
694    #[test]
695    fn test_jaccard_self() {
696        let mut g = Graph::with_vertices(3);
697        g.add_edge(0, 1).unwrap();
698        let sim = similarity_jaccard_pairs(&g, &[(0, 0)]).unwrap();
699        assert!((sim[0] - 1.0).abs() < 1e-10);
700    }
701
702    #[test]
703    fn test_jaccard_isolated() {
704        let g = Graph::with_vertices(3);
705        let sim = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
706        assert!((sim[0]).abs() < 1e-10);
707    }
708
709    #[test]
710    fn test_jaccard_no_overlap() {
711        // 0-1, 2-3: N(0)={1}, N(2)={3}, no overlap
712        let mut g = Graph::with_vertices(4);
713        g.add_edge(0, 1).unwrap();
714        g.add_edge(2, 3).unwrap();
715        let sim = similarity_jaccard_pairs(&g, &[(0, 2)]).unwrap();
716        assert!((sim[0]).abs() < 1e-10);
717    }
718
719    #[test]
720    fn test_jaccard_multiple_pairs() {
721        let mut g = Graph::with_vertices(4);
722        g.add_edge(0, 2).unwrap();
723        g.add_edge(0, 3).unwrap();
724        g.add_edge(1, 2).unwrap();
725        g.add_edge(1, 3).unwrap();
726
727        let sim = similarity_jaccard_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
728        // (0,1): N(0)={2,3}, N(1)={2,3} → J=1.0
729        assert!((sim[0] - 1.0).abs() < 1e-10);
730        // (0,2): N(0)={2,3}, N(2)={0,1} → intersection=empty, union={0,1,2,3}→J=0
731        assert!((sim[1]).abs() < 1e-10);
732        // (2,3): N(2)={0,1}, N(3)={0,1} → J=1.0
733        assert!((sim[2] - 1.0).abs() < 1e-10);
734    }
735
736    #[test]
737    fn test_dice_basic() {
738        let mut g = Graph::with_vertices(5);
739        g.add_edge(0, 2).unwrap();
740        g.add_edge(0, 3).unwrap();
741        g.add_edge(1, 2).unwrap();
742        g.add_edge(1, 3).unwrap();
743        g.add_edge(1, 4).unwrap();
744
745        let sim = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
746        // N(0)={2,3}, N(1)={2,3,4}, |intersect|=2, deg_sum=5
747        // Dice = 2*2/5 = 0.8
748        assert!((sim[0] - 0.8).abs() < 1e-10);
749    }
750
751    #[test]
752    fn test_dice_self() {
753        let mut g = Graph::with_vertices(3);
754        g.add_edge(0, 1).unwrap();
755        let sim = similarity_dice_pairs(&g, &[(0, 0)]).unwrap();
756        assert!((sim[0] - 1.0).abs() < 1e-10);
757    }
758
759    #[test]
760    fn test_dice_isolated() {
761        let g = Graph::with_vertices(3);
762        let sim = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
763        assert!((sim[0]).abs() < 1e-10);
764    }
765
766    #[test]
767    fn test_jaccard_dice_relationship() {
768        let mut g = Graph::with_vertices(5);
769        g.add_edge(0, 2).unwrap();
770        g.add_edge(0, 3).unwrap();
771        g.add_edge(1, 2).unwrap();
772        g.add_edge(1, 3).unwrap();
773        g.add_edge(1, 4).unwrap();
774
775        let jac = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
776        let dice = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
777        // Dice = 2*J / (1+J)
778        let expected_dice = 2.0 * jac[0] / (1.0 + jac[0]);
779        assert!((dice[0] - expected_dice).abs() < 1e-10);
780    }
781
782    #[test]
783    fn test_jaccard_out_of_range() {
784        let g = Graph::with_vertices(3);
785        assert!(similarity_jaccard_pairs(&g, &[(0, 5)]).is_err());
786    }
787
788    #[test]
789    fn test_inverse_log_weighted_basic() {
790        let mut g = Graph::with_vertices(4);
791        g.add_edge(0, 2).unwrap();
792        g.add_edge(0, 3).unwrap();
793        g.add_edge(1, 2).unwrap();
794        g.add_edge(1, 3).unwrap();
795
796        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
797        // Common neighbors: 2 (deg=2), 3 (deg=2)
798        // AA = 1/ln(2) + 1/ln(2) = 2/ln(2)
799        assert!((sim[0] - 2.0 / 2.0_f64.ln()).abs() < 1e-10);
800    }
801
802    #[test]
803    fn test_inverse_log_weighted_self() {
804        let mut g = Graph::with_vertices(3);
805        g.add_edge(0, 1).unwrap();
806        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 0)]).unwrap();
807        assert!((sim[0]).abs() < 1e-10);
808    }
809
810    #[test]
811    fn test_inverse_log_weighted_isolated() {
812        let g = Graph::with_vertices(3);
813        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
814        assert!((sim[0]).abs() < 1e-10);
815    }
816
817    #[test]
818    fn test_inverse_log_weighted_no_common() {
819        let mut g = Graph::with_vertices(4);
820        g.add_edge(0, 1).unwrap();
821        g.add_edge(2, 3).unwrap();
822        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 2)]).unwrap();
823        assert!((sim[0]).abs() < 1e-10);
824    }
825
826    #[test]
827    fn test_inverse_log_weighted_high_degree() {
828        // Hub vertex 4 connected to all others
829        let mut g = Graph::with_vertices(5);
830        g.add_edge(0, 4).unwrap();
831        g.add_edge(1, 4).unwrap();
832        g.add_edge(2, 4).unwrap();
833        g.add_edge(3, 4).unwrap();
834
835        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
836        // Common neighbor: 4 (deg=4)
837        // AA = 1/ln(4)
838        assert!((sim[0] - 1.0 / 4.0_f64.ln()).abs() < 1e-10);
839    }
840
841    #[test]
842    fn test_inverse_log_weighted_degree_one_neighbor() {
843        // Common neighbor with degree 1 contributes 0
844        let mut g = Graph::with_vertices(3);
845        g.add_edge(0, 2).unwrap();
846        // N(0)={2}, N(1)={} → no common neighbors
847        let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
848        assert!((sim[0]).abs() < 1e-10);
849    }
850
851    #[test]
852    fn test_inverse_log_weighted_out_of_range() {
853        let g = Graph::with_vertices(3);
854        assert!(similarity_inverse_log_weighted_pairs(&g, &[(0, 5)]).is_err());
855    }
856
857    // --- Full matrix tests ---
858
859    #[test]
860    fn test_jaccard_matrix_empty() {
861        let g = Graph::with_vertices(0);
862        let sim = similarity_jaccard(&g).unwrap();
863        assert!(sim.is_empty());
864    }
865
866    #[test]
867    fn test_jaccard_matrix_isolated() {
868        let g = Graph::with_vertices(3);
869        let sim = similarity_jaccard(&g).unwrap();
870        // Diagonal is 1.0, off-diagonal is 0.0.
871        for u in 0..3 {
872            assert!((sim[u * 3 + u] - 1.0).abs() < 1e-10);
873            for v in 0..3 {
874                if u != v {
875                    assert!(sim[u * 3 + v].abs() < 1e-10);
876                }
877            }
878        }
879    }
880
881    #[test]
882    fn test_jaccard_matrix_agrees_with_pairs() {
883        let mut g = Graph::with_vertices(5);
884        g.add_edge(0, 2).unwrap();
885        g.add_edge(0, 3).unwrap();
886        g.add_edge(1, 2).unwrap();
887        g.add_edge(1, 3).unwrap();
888        g.add_edge(1, 4).unwrap();
889
890        let n = 5;
891        let matrix = similarity_jaccard(&g).unwrap();
892        let pairs = similarity_jaccard_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
893        assert!((matrix[1] - pairs[0]).abs() < 1e-10);
894        assert!((matrix[2] - pairs[1]).abs() < 1e-10);
895        assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
896    }
897
898    #[test]
899    fn test_jaccard_matrix_symmetric() {
900        let mut g = Graph::with_vertices(4);
901        g.add_edge(0, 1).unwrap();
902        g.add_edge(1, 2).unwrap();
903        g.add_edge(2, 3).unwrap();
904        let sim = similarity_jaccard(&g).unwrap();
905        for u in 0..4 {
906            for v in 0..4 {
907                assert!(
908                    (sim[u * 4 + v] - sim[v * 4 + u]).abs() < 1e-10,
909                    "not symmetric at ({u},{v})"
910                );
911            }
912        }
913    }
914
915    #[test]
916    fn test_dice_matrix_agrees_with_pairs() {
917        let mut g = Graph::with_vertices(5);
918        g.add_edge(0, 2).unwrap();
919        g.add_edge(0, 3).unwrap();
920        g.add_edge(1, 2).unwrap();
921        g.add_edge(1, 3).unwrap();
922        g.add_edge(1, 4).unwrap();
923
924        let n = 5;
925        let matrix = similarity_dice(&g).unwrap();
926        let pairs = similarity_dice_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
927        assert!((matrix[1] - pairs[0]).abs() < 1e-10);
928        assert!((matrix[2] - pairs[1]).abs() < 1e-10);
929        assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
930    }
931
932    #[test]
933    fn test_dice_matrix_diagonal() {
934        let mut g = Graph::with_vertices(3);
935        g.add_edge(0, 1).unwrap();
936        g.add_edge(1, 2).unwrap();
937        let sim = similarity_dice(&g).unwrap();
938        for u in 0..3 {
939            assert!((sim[u * 3 + u] - 1.0).abs() < 1e-10);
940        }
941    }
942
943    #[test]
944    fn test_inverse_log_weighted_matrix_agrees_with_pairs() {
945        let mut g = Graph::with_vertices(4);
946        g.add_edge(0, 2).unwrap();
947        g.add_edge(0, 3).unwrap();
948        g.add_edge(1, 2).unwrap();
949        g.add_edge(1, 3).unwrap();
950
951        let n = 4;
952        let matrix = similarity_inverse_log_weighted(&g).unwrap();
953        let pairs = similarity_inverse_log_weighted_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
954        assert!((matrix[1] - pairs[0]).abs() < 1e-10);
955        assert!((matrix[2] - pairs[1]).abs() < 1e-10);
956        assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
957    }
958
959    #[test]
960    fn test_inverse_log_weighted_matrix_diagonal_zero() {
961        let mut g = Graph::with_vertices(3);
962        g.add_edge(0, 1).unwrap();
963        g.add_edge(1, 2).unwrap();
964        let sim = similarity_inverse_log_weighted(&g).unwrap();
965        for u in 0..3 {
966            assert!(sim[u * 3 + u].abs() < 1e-10);
967        }
968    }
969
970    #[test]
971    fn test_jaccard_dice_matrix_relationship() {
972        let mut g = Graph::with_vertices(5);
973        g.add_edge(0, 2).unwrap();
974        g.add_edge(0, 3).unwrap();
975        g.add_edge(1, 2).unwrap();
976        g.add_edge(1, 3).unwrap();
977        g.add_edge(1, 4).unwrap();
978        g.add_edge(2, 4).unwrap();
979
980        let jac = similarity_jaccard(&g).unwrap();
981        let dice = similarity_dice(&g).unwrap();
982        for u in 0..5 {
983            for v in 0..5 {
984                if u != v {
985                    let j = jac[u * 5 + v];
986                    let d = dice[u * 5 + v];
987                    let expected_d = if j == 0.0 { 0.0 } else { 2.0 * j / (1.0 + j) };
988                    assert!(
989                        (d - expected_d).abs() < 1e-10,
990                        "Dice/Jaccard mismatch at ({u},{v}): d={d}, expected={expected_d}"
991                    );
992                }
993            }
994        }
995    }
996
997    // --- similarity_jaccard_es / similarity_dice_es tests ---
998
999    #[test]
1000    fn test_jaccard_es_matches_pairs() {
1001        let mut g = Graph::with_vertices(5);
1002        g.add_edge(0, 2).unwrap(); // edge 0
1003        g.add_edge(0, 3).unwrap(); // edge 1
1004        g.add_edge(1, 2).unwrap(); // edge 2
1005        g.add_edge(1, 3).unwrap(); // edge 3
1006        g.add_edge(1, 4).unwrap(); // edge 4
1007        g.add_edge(0, 1).unwrap(); // edge 5
1008
1009        let es_result = similarity_jaccard_es(&g, &[0, 5]).unwrap();
1010        let pairs_result = similarity_jaccard_pairs(&g, &[(0, 2), (0, 1)]).unwrap();
1011        assert!((es_result[0] - pairs_result[0]).abs() < 1e-12);
1012        assert!((es_result[1] - pairs_result[1]).abs() < 1e-12);
1013    }
1014
1015    #[test]
1016    fn test_jaccard_es_empty() {
1017        let mut g = Graph::with_vertices(3);
1018        g.add_edge(0, 1).unwrap();
1019        let result = similarity_jaccard_es(&g, &[]).unwrap();
1020        assert!(result.is_empty());
1021    }
1022
1023    #[test]
1024    fn test_jaccard_es_self_loop() {
1025        let mut g = Graph::with_vertices(3);
1026        g.add_edge(0, 0).unwrap(); // self-loop, edge 0
1027        g.add_edge(0, 1).unwrap(); // edge 1
1028        let result = similarity_jaccard_es(&g, &[0]).unwrap();
1029        // self-loop → endpoints are (0,0), Jaccard(0,0) = 1.0
1030        assert!((result[0] - 1.0).abs() < 1e-12);
1031    }
1032
1033    #[test]
1034    fn test_jaccard_es_invalid_edge_id() {
1035        let mut g = Graph::with_vertices(3);
1036        g.add_edge(0, 1).unwrap();
1037        assert!(similarity_jaccard_es(&g, &[99]).is_err());
1038    }
1039
1040    #[test]
1041    fn test_dice_es_matches_pairs() {
1042        let mut g = Graph::with_vertices(5);
1043        g.add_edge(0, 2).unwrap(); // edge 0
1044        g.add_edge(0, 3).unwrap(); // edge 1
1045        g.add_edge(1, 2).unwrap(); // edge 2
1046        g.add_edge(1, 3).unwrap(); // edge 3
1047        g.add_edge(1, 4).unwrap(); // edge 4
1048        g.add_edge(0, 1).unwrap(); // edge 5
1049
1050        let es_result = similarity_dice_es(&g, &[0, 5]).unwrap();
1051        let pairs_result = similarity_dice_pairs(&g, &[(0, 2), (0, 1)]).unwrap();
1052        assert!((es_result[0] - pairs_result[0]).abs() < 1e-12);
1053        assert!((es_result[1] - pairs_result[1]).abs() < 1e-12);
1054    }
1055
1056    #[test]
1057    fn test_dice_es_empty() {
1058        let mut g = Graph::with_vertices(3);
1059        g.add_edge(0, 1).unwrap();
1060        let result = similarity_dice_es(&g, &[]).unwrap();
1061        assert!(result.is_empty());
1062    }
1063
1064    #[test]
1065    fn test_dice_es_jaccard_relationship() {
1066        let mut g = Graph::with_vertices(5);
1067        g.add_edge(0, 2).unwrap(); // edge 0
1068        g.add_edge(0, 3).unwrap(); // edge 1
1069        g.add_edge(1, 2).unwrap(); // edge 2
1070        g.add_edge(1, 3).unwrap(); // edge 3
1071        g.add_edge(1, 4).unwrap(); // edge 4
1072        g.add_edge(0, 1).unwrap(); // edge 5
1073
1074        let jac = similarity_jaccard_es(&g, &[5]).unwrap();
1075        let dice = similarity_dice_es(&g, &[5]).unwrap();
1076        let j = jac[0];
1077        let expected_d = 2.0 * j / (1.0 + j);
1078        assert!((dice[0] - expected_d).abs() < 1e-12);
1079    }
1080
1081    #[test]
1082    fn test_jaccard_es_all_edges() {
1083        let mut g = Graph::with_vertices(4);
1084        g.add_edge(0, 1).unwrap(); // edge 0
1085        g.add_edge(1, 2).unwrap(); // edge 1
1086        g.add_edge(2, 3).unwrap(); // edge 2
1087        g.add_edge(0, 3).unwrap(); // edge 3
1088
1089        let eids: Vec<u32> = (0..4).collect();
1090        let es_result = similarity_jaccard_es(&g, &eids).unwrap();
1091        let pairs = [(0u32, 1u32), (1, 2), (2, 3), (0, 3)];
1092        let pairs_result = similarity_jaccard_pairs(&g, &pairs).unwrap();
1093        for i in 0..4 {
1094            assert!((es_result[i] - pairs_result[i]).abs() < 1e-12);
1095        }
1096    }
1097}