Skip to main content

rust_igraph/algorithms/properties/
local_scan.rs

1//! Local scan statistics (ALGO-PR-051 + ALGO-PR-042).
2//!
3//! Counterparts of `igraph_local_scan_*` from
4//! `references/igraph/src/misc/scan.c`.
5//!
6//! Scan statistics summarise local neighbourhood structure. For each
7//! vertex the k-neighbourhood is identified and the edges (or total
8//! weight) within that neighbourhood are counted.
9//!
10//! ## Functions
11//!
12//! - [`local_scan_1`] — undirected-only 1-neighbourhood edge count
13//!   (original ALGO-PR-051, kept for backward compatibility).
14//! - [`local_scan_0`] — k=0 (degree / strength).
15//! - [`local_scan_1_ecount`] — k=1 with directed-mode support.
16//! - [`local_scan_0_them`] — two-graph k=0 scan.
17//! - [`local_scan_1_ecount_them`] — two-graph k=1 scan.
18//! - [`local_scan_subset_ecount`] — edge count in given vertex subsets.
19
20use crate::algorithms::properties::degree::DegreeMode;
21use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
22
23/// Collect incident edge IDs for a vertex respecting mode.
24fn incident_with_mode(graph: &Graph, v: VertexId, mode: DegreeMode) -> IgraphResult<Vec<u32>> {
25    if !graph.is_directed() {
26        return graph.incident(v);
27    }
28    match mode {
29        DegreeMode::Out => graph.incident(v),
30        DegreeMode::In => graph.incident_in(v),
31        DegreeMode::All => {
32            let mut out = graph.incident(v)?;
33            let in_edges = graph.incident_in(v)?;
34            out.extend(in_edges);
35            Ok(out)
36        }
37    }
38}
39
40/// Collect vertex neighbours respecting mode.
41fn neighbors_with_mode(graph: &Graph, v: VertexId, mode: DegreeMode) -> IgraphResult<Vec<u32>> {
42    if !graph.is_directed() {
43        return graph.neighbors(v);
44    }
45    match mode {
46        DegreeMode::Out => {
47            let edges = graph.incident(v)?;
48            edges.iter().map(|&e| graph.edge_other(e, v)).collect()
49        }
50        DegreeMode::In => {
51            let edges = graph.incident_in(v)?;
52            edges.iter().map(|&e| graph.edge_other(e, v)).collect()
53        }
54        DegreeMode::All => {
55            let out_edges = graph.incident(v)?;
56            let in_edges = graph.incident_in(v)?;
57            let mut neis = Vec::with_capacity(out_edges.len() + in_edges.len());
58            for &e in &out_edges {
59                neis.push(graph.edge_other(e, v)?);
60            }
61            for &e in &in_edges {
62                neis.push(graph.edge_other(e, v)?);
63            }
64            Ok(neis)
65        }
66    }
67}
68
69// ────────────────── local_scan_1 (legacy, backward-compat) ──────────────────
70
71/// For each vertex, count edges within its closed 1-neighborhood.
72///
73/// The closed 1-neighborhood of vertex `v` is `{v} ∪ neighbors(v)`.
74/// This function counts all edges that have both endpoints in that set.
75///
76/// For undirected graphs, each such edge is counted once per vertex whose
77/// neighborhood contains it.
78///
79/// `weights`: optional edge weights (length must equal `ecount()`).
80/// When provided, sums edge weights instead of counting edges.
81///
82/// Returns a vector of length `vcount()`.
83///
84/// # Examples
85///
86/// ```
87/// use rust_igraph::{Graph, local_scan_1};
88///
89/// // Triangle: each vertex's 1-neighborhood is the whole graph.
90/// // 3 edges in the neighborhood of each vertex.
91/// let mut g = Graph::with_vertices(3);
92/// g.add_edge(0, 1).unwrap();
93/// g.add_edge(1, 2).unwrap();
94/// g.add_edge(2, 0).unwrap();
95/// let s = local_scan_1(&g, None).unwrap();
96/// assert!((s[0] - 3.0).abs() < 1e-10);
97/// assert!((s[1] - 3.0).abs() < 1e-10);
98/// assert!((s[2] - 3.0).abs() < 1e-10);
99/// ```
100pub fn local_scan_1(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
101    local_scan_1_ecount(graph, weights, DegreeMode::All)
102}
103
104// ──────────────────────── local_scan_0 ──────────────────────────────────────
105
106/// Local scan-0: vertex degree (unweighted) or strength (weighted).
107///
108/// For `k = 0` the scan statistic is defined as the degree (or
109/// weighted strength) of each vertex.
110///
111/// # Examples
112///
113/// ```
114/// use rust_igraph::{Graph, local_scan_0, DegreeMode};
115///
116/// let mut g = Graph::with_vertices(4);
117/// g.add_edge(0, 1).unwrap();
118/// g.add_edge(0, 2).unwrap();
119/// g.add_edge(0, 3).unwrap();
120/// let res = local_scan_0(&g, None, DegreeMode::All).unwrap();
121/// assert!((res[0] - 3.0).abs() < 1e-12);
122/// assert!((res[1] - 1.0).abs() < 1e-12);
123/// ```
124pub fn local_scan_0(
125    graph: &Graph,
126    weights: Option<&[f64]>,
127    mode: DegreeMode,
128) -> IgraphResult<Vec<f64>> {
129    if let Some(w) = weights {
130        if w.len() != graph.ecount() {
131            return Err(IgraphError::InvalidArgument(format!(
132                "weight vector length {} != edge count {}",
133                w.len(),
134                graph.ecount()
135            )));
136        }
137    }
138
139    let n = graph.vcount() as usize;
140    let mut res = vec![0.0_f64; n];
141    let directed = graph.is_directed();
142    let ecount = graph.ecount();
143
144    for eid in 0..ecount {
145        let eid_u32 =
146            u32::try_from(eid).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
147        let from = graph.edge_source(eid_u32)? as usize;
148        let to = graph.edge_target(eid_u32)? as usize;
149        let w = weights.map_or(1.0, |ws| ws[eid]);
150
151        if directed {
152            match mode {
153                DegreeMode::Out => res[from] += w,
154                DegreeMode::In => res[to] += w,
155                DegreeMode::All => {
156                    res[from] += w;
157                    res[to] += w;
158                }
159            }
160        } else {
161            res[from] += w;
162            res[to] += w;
163        }
164    }
165
166    Ok(res)
167}
168
169// ──────────────────────── local_scan_1_ecount ───────────────────────────────
170
171/// Local scan-1 for directed graphs (mode != All): mark+crawl approach.
172fn local_scan_1_directed(
173    graph: &Graph,
174    weights: Option<&[f64]>,
175    mode: DegreeMode,
176) -> IgraphResult<Vec<f64>> {
177    let n = graph.vcount();
178    let n_usize = n as usize;
179    let mut res = vec![0.0_f64; n_usize];
180    let mut marker: Vec<i64> = vec![-1; n_usize];
181
182    for node in 0..n {
183        let node_i = i64::from(node);
184        let edges1 = incident_with_mode(graph, node, mode)?;
185
186        marker[node as usize] = node_i;
187        for &e in &edges1 {
188            let nei = graph.edge_other(e, node)?;
189            let w = weights.map_or(1.0, |ws| ws[e as usize]);
190            marker[nei as usize] = node_i;
191            res[node as usize] += w;
192        }
193
194        for &e in &edges1 {
195            let nei = graph.edge_other(e, node)?;
196            if nei == node {
197                break;
198            }
199            let edges2 = incident_with_mode(graph, nei, mode)?;
200            for &e2 in &edges2 {
201                let nei2 = graph.edge_other(e2, nei)?;
202                if marker[nei2 as usize] == node_i {
203                    let w2 = weights.map_or(1.0, |ws| ws[e2 as usize]);
204                    res[node as usize] += w2;
205                }
206            }
207        }
208    }
209
210    Ok(res)
211}
212
213/// Local scan-1 for directed graphs with mode = All: unmark-after-visit.
214fn local_scan_1_directed_all(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
215    let n = graph.vcount();
216    let n_usize = n as usize;
217    let mut res = vec![0.0_f64; n_usize];
218    let mut marker: Vec<i64> = vec![-1; n_usize];
219
220    for node in 0..n {
221        let node_i = i64::from(node);
222        let edges1 = incident_with_mode(graph, node, DegreeMode::All)?;
223
224        for &e in &edges1 {
225            let nei = graph.edge_other(e, node)?;
226            let w = weights.map_or(1.0, |ws| ws[e as usize]);
227            marker[nei as usize] = node_i;
228            res[node as usize] += w;
229        }
230
231        marker[node as usize] = -1;
232
233        for &e in &edges1 {
234            let nei = graph.edge_other(e, node)?;
235            if marker[nei as usize] != node_i {
236                continue;
237            }
238            let edges2 = incident_with_mode(graph, nei, DegreeMode::All)?;
239            for &e2 in &edges2 {
240                let nei2 = graph.edge_other(e2, nei)?;
241                if marker[nei2 as usize] == node_i {
242                    let w2 = weights.map_or(1.0, |ws| ws[e2 as usize]);
243                    res[node as usize] += w2;
244                }
245            }
246            marker[nei as usize] = -1;
247        }
248    }
249
250    Ok(res)
251}
252
253/// Local scan-1 edge count / weight sum in the 1-neighbourhood of
254/// every vertex, with directed-mode support.
255///
256/// # Examples
257///
258/// ```
259/// use rust_igraph::{Graph, local_scan_1_ecount, DegreeMode};
260///
261/// // Triangle 0-1-2 plus pendant 3-0
262/// let mut g = Graph::with_vertices(4);
263/// g.add_edge(0, 1).unwrap();
264/// g.add_edge(1, 2).unwrap();
265/// g.add_edge(2, 0).unwrap();
266/// g.add_edge(3, 0).unwrap();
267/// let res = local_scan_1_ecount(&g, None, DegreeMode::All).unwrap();
268/// assert!((res[0] - 4.0).abs() < 1e-12);
269/// assert!((res[3] - 1.0).abs() < 1e-12);
270/// ```
271pub fn local_scan_1_ecount(
272    graph: &Graph,
273    weights: Option<&[f64]>,
274    mode: DegreeMode,
275) -> IgraphResult<Vec<f64>> {
276    if let Some(w) = weights {
277        if w.len() != graph.ecount() {
278            return Err(IgraphError::InvalidArgument(format!(
279                "weight vector length {} != edge count {}",
280                w.len(),
281                graph.ecount()
282            )));
283        }
284    }
285
286    if graph.is_directed() {
287        if mode == DegreeMode::All {
288            local_scan_1_directed_all(graph, weights)
289        } else {
290            local_scan_1_directed(graph, weights, mode)
291        }
292    } else {
293        // For undirected graphs, use k-general BFS approach
294        crate::algorithms::properties::local_scan_k::local_scan_k_ecount(graph, 1, weights, mode)
295    }
296}
297
298// ────────────────────── local_scan_0_them ────────────────────────────────────
299
300/// Local scan-0 on two graphs: degree/strength from `them` restricted
301/// to edges that also exist in `us`.
302///
303/// Both graphs must have the same vertex count and directedness.
304///
305/// # Examples
306///
307/// ```
308/// use rust_igraph::{Graph, local_scan_0_them, DegreeMode};
309///
310/// let mut us = Graph::with_vertices(3);
311/// us.add_edge(0, 1).unwrap();
312/// us.add_edge(0, 2).unwrap();
313/// let mut them = Graph::with_vertices(3);
314/// them.add_edge(0, 1).unwrap();
315/// them.add_edge(1, 2).unwrap();
316/// let res = local_scan_0_them(&us, &them, None, DegreeMode::All).unwrap();
317/// assert!((res[0] - 1.0).abs() < 1e-12);
318/// assert!((res[1] - 1.0).abs() < 1e-12);
319/// assert!((res[2] - 0.0).abs() < 1e-12);
320/// ```
321pub fn local_scan_0_them(
322    us: &Graph,
323    them: &Graph,
324    weights_them: Option<&[f64]>,
325    mode: DegreeMode,
326) -> IgraphResult<Vec<f64>> {
327    check_them_compat(us, them, weights_them, "local_scan_0_them")?;
328
329    let is_graph = crate::algorithms::operators::intersection::intersection(us, them)?;
330
331    // For unweighted, just compute scan_0 on the intersection
332    if weights_them.is_none() {
333        return local_scan_0(&is_graph, None, mode);
334    }
335
336    // For weighted, we need to map weights from `them` to the intersection.
337    // The intersection result has edge_map2 which maps intersection edges
338    // back to `them` edges. But our intersection returns IntersectionResult
339    // which only has .graph. We'll do the unweighted path for now and
340    // handle weighted via a different approach.
341    //
342    // Weighted scan-0-them: for each edge in `us`, check if the same edge
343    // exists in `them` and if so add its weight.
344    let ws = weights_them.expect("checked above");
345    let n = us.vcount() as usize;
346    let mut res = vec![0.0_f64; n];
347    let directed = us.is_directed();
348    let us_ecount = us.ecount();
349
350    for eid in 0..us_ecount {
351        let eid_u32 =
352            u32::try_from(eid).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
353        let from = us.edge_source(eid_u32)?;
354        let to = us.edge_target(eid_u32)?;
355
356        // Check if this edge exists in `them`
357        if let Some(them_eid) = them.find_eid(from, to)? {
358            let w = ws[them_eid as usize];
359            if directed {
360                match mode {
361                    DegreeMode::Out => res[from as usize] += w,
362                    DegreeMode::In => res[to as usize] += w,
363                    DegreeMode::All => {
364                        res[from as usize] += w;
365                        res[to as usize] += w;
366                    }
367                }
368            } else {
369                res[from as usize] += w;
370                res[to as usize] += w;
371            }
372        }
373    }
374
375    Ok(res)
376}
377
378// ────────────────── local_scan_1_ecount_them ─────────────────────────────────
379
380/// Local scan-1 edge count on two graphs: count edges from `them` in
381/// the 1-neighbourhood defined by `us`.
382///
383/// Both graphs must have the same vertex count and directedness.
384///
385/// # Examples
386///
387/// ```
388/// use rust_igraph::{Graph, local_scan_1_ecount_them, DegreeMode};
389///
390/// let mut us = Graph::with_vertices(3);
391/// us.add_edge(0, 1).unwrap();
392/// us.add_edge(1, 2).unwrap();
393/// let mut them = Graph::with_vertices(3);
394/// them.add_edge(0, 1).unwrap();
395/// them.add_edge(0, 2).unwrap();
396/// them.add_edge(1, 2).unwrap();
397/// let res = local_scan_1_ecount_them(&us, &them, None, DegreeMode::All).unwrap();
398/// assert!((res[1] - 3.0).abs() < 1e-12);
399/// ```
400pub fn local_scan_1_ecount_them(
401    us: &Graph,
402    them: &Graph,
403    weights_them: Option<&[f64]>,
404    mode: DegreeMode,
405) -> IgraphResult<Vec<f64>> {
406    check_them_compat(us, them, weights_them, "local_scan_1_ecount_them")?;
407
408    let n = us.vcount();
409    let n_usize = n as usize;
410    let mut res = vec![0.0_f64; n_usize];
411    let mut marker: Vec<i64> = vec![-1; n_usize];
412    let undirected_or_all = mode == DegreeMode::All || !us.is_directed();
413
414    for node in 0..n {
415        let node_i = i64::from(node);
416
417        marker[node as usize] = node_i;
418        let us_neis = neighbors_with_mode(us, node, mode)?;
419        for &nei in &us_neis {
420            marker[nei as usize] = node_i;
421        }
422
423        let them_edges_ego = incident_with_mode(them, node, mode)?;
424        for &e in &them_edges_ego {
425            let nei = them.edge_other(e, node)?;
426            if marker[nei as usize] == node_i {
427                let w = weights_them.map_or(1.0, |ws| ws[e as usize]);
428                res[node as usize] += w;
429            }
430        }
431
432        for &us_nei in &us_neis {
433            let them_edges = incident_with_mode(them, us_nei, mode)?;
434            for &e2 in &them_edges {
435                let nei2 = them.edge_other(e2, us_nei)?;
436                if marker[nei2 as usize] == node_i {
437                    let w = weights_them.map_or(1.0, |ws| ws[e2 as usize]);
438                    res[node as usize] += w;
439                }
440            }
441        }
442
443        if undirected_or_all {
444            res[node as usize] /= 2.0;
445        }
446    }
447
448    Ok(res)
449}
450
451// ────────────────── local_scan_subset_ecount ─────────────────────────────────
452
453/// Local scan on given vertex subsets: count edges (or sum weights)
454/// in the induced subgraph of each subset.
455///
456/// # Examples
457///
458/// ```
459/// use rust_igraph::{Graph, local_scan_subset_ecount};
460///
461/// let mut g = Graph::with_vertices(4);
462/// g.add_edge(0, 1).unwrap();
463/// g.add_edge(1, 2).unwrap();
464/// g.add_edge(2, 0).unwrap();
465/// g.add_edge(2, 3).unwrap();
466/// let subsets = vec![vec![0, 1, 2], vec![2, 3]];
467/// let res = local_scan_subset_ecount(&g, &subsets, None).unwrap();
468/// assert!((res[0] - 3.0).abs() < 1e-12);
469/// assert!((res[1] - 1.0).abs() < 1e-12);
470/// ```
471pub fn local_scan_subset_ecount(
472    graph: &Graph,
473    subsets: &[Vec<VertexId>],
474    weights: Option<&[f64]>,
475) -> IgraphResult<Vec<f64>> {
476    if let Some(w) = weights {
477        if w.len() != graph.ecount() {
478            return Err(IgraphError::InvalidArgument(format!(
479                "weight vector length {} != edge count {}",
480                w.len(),
481                graph.ecount()
482            )));
483        }
484    }
485
486    let n = graph.vcount();
487    let n_usize = n as usize;
488    let directed = graph.is_directed();
489    let num_subsets = subsets.len();
490    let mut res = vec![0.0_f64; num_subsets];
491    let mut marker: Vec<i64> = vec![-1; n_usize];
492
493    for (subset_idx, subset) in subsets.iter().enumerate() {
494        let subset_i = i64::try_from(subset_idx)
495            .map_err(|_| IgraphError::Internal("subset index overflow"))?;
496
497        for &v in subset {
498            if v >= n {
499                return Err(IgraphError::InvalidArgument(format!(
500                    "vertex {v} out of range for graph with {n} vertices"
501                )));
502            }
503            marker[v as usize] = subset_i;
504        }
505
506        // Use out-edges (IGRAPH_OUT mode with IGRAPH_LOOPS_TWICE) to
507        // count each undirected edge once from each endpoint, then halve.
508        for &v in subset {
509            let edges = graph.incident(v)?;
510            for &e in &edges {
511                let nei = graph.edge_other(e, v)?;
512                if marker[nei as usize] == subset_i {
513                    let w = weights.map_or(1.0, |ws| ws[e as usize]);
514                    res[subset_idx] += w;
515                }
516            }
517
518            if directed {
519                let in_edges = graph.incident_in(v)?;
520                for &e in &in_edges {
521                    let nei = graph.edge_other(e, v)?;
522                    if marker[nei as usize] == subset_i {
523                        let w = weights.map_or(1.0, |ws| ws[e as usize]);
524                        res[subset_idx] += w;
525                    }
526                }
527            }
528        }
529
530        res[subset_idx] /= 2.0;
531    }
532
533    Ok(res)
534}
535
536/// Validate compatibility for two-graph ("them") scan functions.
537fn check_them_compat(
538    us: &Graph,
539    them: &Graph,
540    weights_them: Option<&[f64]>,
541    name: &str,
542) -> IgraphResult<()> {
543    if us.vcount() != them.vcount() {
544        return Err(IgraphError::InvalidArgument(format!(
545            "{name}: vertex count mismatch ({} vs {})",
546            us.vcount(),
547            them.vcount()
548        )));
549    }
550    if us.is_directed() != them.is_directed() {
551        return Err(IgraphError::InvalidArgument(format!(
552            "{name}: directedness mismatch"
553        )));
554    }
555    if let Some(w) = weights_them {
556        if w.len() != them.ecount() {
557            return Err(IgraphError::InvalidArgument(format!(
558                "{name}: weight vector length {} != them edge count {}",
559                w.len(),
560                them.ecount()
561            )));
562        }
563    }
564    Ok(())
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    fn close(a: f64, b: f64) -> bool {
572        (a - b).abs() < 1e-10
573    }
574
575    // --- local_scan_1 (legacy) tests ---
576
577    #[test]
578    fn empty_graph() {
579        let g = Graph::with_vertices(0);
580        let s = local_scan_1(&g, None).unwrap();
581        assert!(s.is_empty());
582    }
583
584    #[test]
585    fn no_edges() {
586        let g = Graph::with_vertices(5);
587        let s = local_scan_1(&g, None).unwrap();
588        assert!(s.iter().all(|&v| close(v, 0.0)));
589    }
590
591    #[test]
592    fn triangle() {
593        let mut g = Graph::with_vertices(3);
594        g.add_edge(0, 1).unwrap();
595        g.add_edge(1, 2).unwrap();
596        g.add_edge(2, 0).unwrap();
597        let s = local_scan_1(&g, None).unwrap();
598        assert!(close(s[0], 3.0));
599        assert!(close(s[1], 3.0));
600        assert!(close(s[2], 3.0));
601    }
602
603    #[test]
604    fn path_5() {
605        let mut g = Graph::with_vertices(5);
606        for i in 0..4u32 {
607            g.add_edge(i, i + 1).unwrap();
608        }
609        let s = local_scan_1(&g, None).unwrap();
610        assert!(close(s[0], 1.0));
611        assert!(close(s[1], 2.0));
612        assert!(close(s[2], 2.0));
613        assert!(close(s[3], 2.0));
614        assert!(close(s[4], 1.0));
615    }
616
617    #[test]
618    fn star() {
619        let mut g = Graph::with_vertices(4);
620        g.add_edge(0, 1).unwrap();
621        g.add_edge(0, 2).unwrap();
622        g.add_edge(0, 3).unwrap();
623        let s = local_scan_1(&g, None).unwrap();
624        assert!(close(s[0], 3.0));
625        assert!(close(s[1], 1.0));
626        assert!(close(s[2], 1.0));
627        assert!(close(s[3], 1.0));
628    }
629
630    #[test]
631    fn k4() {
632        let mut g = Graph::with_vertices(4);
633        for u in 0..4u32 {
634            for v in (u + 1)..4 {
635                g.add_edge(u, v).unwrap();
636            }
637        }
638        let s = local_scan_1(&g, None).unwrap();
639        for &val in &s {
640            assert!(close(val, 6.0));
641        }
642    }
643
644    #[test]
645    fn two_triangles_with_bridge() {
646        let mut g = Graph::with_vertices(6);
647        g.add_edge(0, 1).unwrap();
648        g.add_edge(0, 2).unwrap();
649        g.add_edge(1, 2).unwrap();
650        g.add_edge(3, 4).unwrap();
651        g.add_edge(3, 5).unwrap();
652        g.add_edge(4, 5).unwrap();
653        g.add_edge(2, 3).unwrap();
654        let s = local_scan_1(&g, None).unwrap();
655        assert!(close(s[0], 3.0));
656        assert!(close(s[1], 3.0));
657        assert!(close(s[2], 4.0));
658        assert!(close(s[3], 4.0));
659        assert!(close(s[4], 3.0));
660        assert!(close(s[5], 3.0));
661    }
662
663    #[test]
664    fn weighted() {
665        let mut g = Graph::with_vertices(3);
666        g.add_edge(0, 1).unwrap();
667        g.add_edge(1, 2).unwrap();
668        g.add_edge(2, 0).unwrap();
669        let w = vec![2.0, 3.0, 5.0];
670        let s = local_scan_1(&g, Some(&w)).unwrap();
671        assert!(close(s[0], 10.0));
672        assert!(close(s[1], 10.0));
673        assert!(close(s[2], 10.0));
674    }
675
676    #[test]
677    fn self_loop() {
678        let mut g = Graph::with_vertices(2);
679        g.add_edge(0, 0).unwrap();
680        g.add_edge(0, 1).unwrap();
681        let s = local_scan_1(&g, None).unwrap();
682        assert!(close(s[0], 2.0));
683        assert!(close(s[1], 2.0));
684    }
685
686    #[test]
687    fn weights_mismatch_errors() {
688        let mut g = Graph::with_vertices(2);
689        g.add_edge(0, 1).unwrap();
690        assert!(local_scan_1(&g, Some(&[1.0, 2.0])).is_err());
691    }
692
693    // --- local_scan_0 tests ---
694
695    #[test]
696    fn scan0_empty() {
697        let g = Graph::with_vertices(0);
698        let res = local_scan_0(&g, None, DegreeMode::All).unwrap();
699        assert!(res.is_empty());
700    }
701
702    #[test]
703    fn scan0_no_edges() {
704        let g = Graph::with_vertices(5);
705        let res = local_scan_0(&g, None, DegreeMode::All).unwrap();
706        assert_eq!(res, vec![0.0; 5]);
707    }
708
709    #[test]
710    fn scan0_triangle() {
711        let mut g = Graph::with_vertices(3);
712        g.add_edge(0, 1).unwrap();
713        g.add_edge(1, 2).unwrap();
714        g.add_edge(2, 0).unwrap();
715        let res = local_scan_0(&g, None, DegreeMode::All).unwrap();
716        for &d in &res {
717            assert!(close(d, 2.0));
718        }
719    }
720
721    #[test]
722    fn scan0_weighted() {
723        let mut g = Graph::with_vertices(3);
724        g.add_edge(0, 1).unwrap();
725        g.add_edge(1, 2).unwrap();
726        let w = vec![3.0, 5.0];
727        let res = local_scan_0(&g, Some(&w), DegreeMode::All).unwrap();
728        assert!(close(res[0], 3.0));
729        assert!(close(res[1], 8.0));
730        assert!(close(res[2], 5.0));
731    }
732
733    #[test]
734    fn scan0_directed_out() {
735        let mut g = Graph::new(3, true).unwrap();
736        g.add_edge(0, 1).unwrap();
737        g.add_edge(0, 2).unwrap();
738        g.add_edge(1, 2).unwrap();
739        let res = local_scan_0(&g, None, DegreeMode::Out).unwrap();
740        assert!(close(res[0], 2.0));
741        assert!(close(res[1], 1.0));
742        assert!(close(res[2], 0.0));
743    }
744
745    #[test]
746    fn scan0_directed_in() {
747        let mut g = Graph::new(3, true).unwrap();
748        g.add_edge(0, 1).unwrap();
749        g.add_edge(0, 2).unwrap();
750        g.add_edge(1, 2).unwrap();
751        let res = local_scan_0(&g, None, DegreeMode::In).unwrap();
752        assert!(close(res[0], 0.0));
753        assert!(close(res[1], 1.0));
754        assert!(close(res[2], 2.0));
755    }
756
757    // --- local_scan_1_ecount tests ---
758
759    #[test]
760    fn scan1e_triangle() {
761        let mut g = Graph::with_vertices(3);
762        g.add_edge(0, 1).unwrap();
763        g.add_edge(1, 2).unwrap();
764        g.add_edge(2, 0).unwrap();
765        let res = local_scan_1_ecount(&g, None, DegreeMode::All).unwrap();
766        for &v in &res {
767            assert!(close(v, 3.0));
768        }
769    }
770
771    #[test]
772    fn scan1e_path4() {
773        let mut g = Graph::with_vertices(4);
774        g.add_edge(0, 1).unwrap();
775        g.add_edge(1, 2).unwrap();
776        g.add_edge(2, 3).unwrap();
777        let res = local_scan_1_ecount(&g, None, DegreeMode::All).unwrap();
778        assert!(close(res[0], 1.0));
779        assert!(close(res[1], 2.0));
780        assert!(close(res[2], 2.0));
781        assert!(close(res[3], 1.0));
782    }
783
784    #[test]
785    fn scan1e_directed_out() {
786        let mut g = Graph::new(3, true).unwrap();
787        g.add_edge(0, 1).unwrap();
788        g.add_edge(1, 2).unwrap();
789        let res = local_scan_1_ecount(&g, None, DegreeMode::Out).unwrap();
790        assert!(close(res[0], 1.0));
791        assert!(close(res[1], 1.0));
792        assert!(close(res[2], 0.0));
793    }
794
795    #[test]
796    fn scan1e_directed_all_cycle() {
797        let mut g = Graph::new(3, true).unwrap();
798        g.add_edge(0, 1).unwrap();
799        g.add_edge(1, 2).unwrap();
800        g.add_edge(2, 0).unwrap();
801        let res = local_scan_1_ecount(&g, None, DegreeMode::All).unwrap();
802        for &v in &res {
803            assert!(close(v, 3.0));
804        }
805    }
806
807    // --- local_scan_0_them tests ---
808
809    #[test]
810    fn scan0t_basic() {
811        let mut us = Graph::with_vertices(3);
812        us.add_edge(0, 1).unwrap();
813        us.add_edge(0, 2).unwrap();
814        let mut them = Graph::with_vertices(3);
815        them.add_edge(0, 1).unwrap();
816        them.add_edge(1, 2).unwrap();
817        let res = local_scan_0_them(&us, &them, None, DegreeMode::All).unwrap();
818        assert!(close(res[0], 1.0));
819        assert!(close(res[1], 1.0));
820        assert!(close(res[2], 0.0));
821    }
822
823    #[test]
824    fn scan0t_identical() {
825        let mut g = Graph::with_vertices(3);
826        g.add_edge(0, 1).unwrap();
827        g.add_edge(1, 2).unwrap();
828        g.add_edge(2, 0).unwrap();
829        let res = local_scan_0_them(&g, &g, None, DegreeMode::All).unwrap();
830        let self_scan = local_scan_0(&g, None, DegreeMode::All).unwrap();
831        for (a, b) in res.iter().zip(self_scan.iter()) {
832            assert!(close(*a, *b));
833        }
834    }
835
836    #[test]
837    fn scan0t_vcount_mismatch() {
838        let us = Graph::with_vertices(3);
839        let them = Graph::with_vertices(4);
840        assert!(local_scan_0_them(&us, &them, None, DegreeMode::All).is_err());
841    }
842
843    // --- local_scan_1_ecount_them tests ---
844
845    #[test]
846    fn scan1t_basic() {
847        let mut us = Graph::with_vertices(3);
848        us.add_edge(0, 1).unwrap();
849        us.add_edge(1, 2).unwrap();
850        let mut them = Graph::with_vertices(3);
851        them.add_edge(0, 1).unwrap();
852        them.add_edge(0, 2).unwrap();
853        them.add_edge(1, 2).unwrap();
854        let res = local_scan_1_ecount_them(&us, &them, None, DegreeMode::All).unwrap();
855        // v1: nbd in us = {0,1,2}, edges in them within: 0-1,0-2,1-2 = 3
856        assert!(close(res[1], 3.0));
857    }
858
859    #[test]
860    fn scan1t_vcount_mismatch() {
861        let us = Graph::with_vertices(3);
862        let them = Graph::with_vertices(4);
863        assert!(local_scan_1_ecount_them(&us, &them, None, DegreeMode::All).is_err());
864    }
865
866    // --- local_scan_subset_ecount tests ---
867
868    #[test]
869    fn subset_triangle() {
870        let mut g = Graph::with_vertices(4);
871        g.add_edge(0, 1).unwrap();
872        g.add_edge(1, 2).unwrap();
873        g.add_edge(2, 0).unwrap();
874        g.add_edge(2, 3).unwrap();
875        let subsets = vec![vec![0, 1, 2], vec![2, 3], vec![0, 3]];
876        let res = local_scan_subset_ecount(&g, &subsets, None).unwrap();
877        assert!(close(res[0], 3.0));
878        assert!(close(res[1], 1.0));
879        assert!(close(res[2], 0.0));
880    }
881
882    #[test]
883    fn subset_weighted() {
884        let mut g = Graph::with_vertices(3);
885        g.add_edge(0, 1).unwrap();
886        g.add_edge(1, 2).unwrap();
887        let w = vec![3.0, 7.0];
888        let subsets = vec![vec![0, 1, 2]];
889        let res = local_scan_subset_ecount(&g, &subsets, Some(&w)).unwrap();
890        assert!(close(res[0], 10.0));
891    }
892
893    #[test]
894    fn subset_empty_subset() {
895        let mut g = Graph::with_vertices(3);
896        g.add_edge(0, 1).unwrap();
897        let subsets: Vec<Vec<VertexId>> = vec![vec![]];
898        let res = local_scan_subset_ecount(&g, &subsets, None).unwrap();
899        assert!(close(res[0], 0.0));
900    }
901
902    #[test]
903    fn subset_invalid_vertex() {
904        let g = Graph::with_vertices(3);
905        let subsets = vec![vec![0, 5]];
906        assert!(local_scan_subset_ecount(&g, &subsets, None).is_err());
907    }
908
909    #[test]
910    fn subset_directed() {
911        let mut g = Graph::new(3, true).unwrap();
912        g.add_edge(0, 1).unwrap();
913        g.add_edge(1, 2).unwrap();
914        g.add_edge(2, 0).unwrap();
915        let subsets = vec![vec![0, 1, 2]];
916        let res = local_scan_subset_ecount(&g, &subsets, None).unwrap();
917        assert!(close(res[0], 3.0));
918    }
919}