Skip to main content

rust_igraph/algorithms/properties/
local_scan_k.rs

1//! Generalized local scan-k edge count (ALGO-PR-053).
2//!
3//! Counterpart of `igraph_local_scan_k_ecount()` from
4//! `references/igraph/src/misc/scan.c:450`.
5//!
6//! For each vertex, counts edges (or sums edge weights) within its
7//! closed k-neighborhood (all vertices reachable within k steps).
8
9use std::collections::VecDeque;
10
11use crate::algorithms::properties::degree::DegreeMode;
12use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
13
14/// For each vertex, count edges within its closed k-neighborhood.
15///
16/// The closed k-neighborhood of vertex `v` is the set of all vertices
17/// reachable from `v` in at most `k` hops. This function counts all
18/// edges that have both endpoints in that set.
19///
20/// For undirected graphs, each edge is counted once.
21/// For directed graphs with `IGRAPH_ALL` mode equivalent, edges are
22/// counted once per direction observed.
23///
24/// `k`: radius of the neighborhood (must be ≥ 0). k=0 counts only
25/// self-loops at each vertex. k=1 is equivalent to `local_scan_1`.
26///
27/// `weights`: optional edge weights (length must equal `ecount()`).
28/// When provided, sums edge weights instead of counting edges.
29///
30/// Returns a vector of length `vcount()`.
31///
32/// # Examples
33///
34/// ```
35/// use rust_igraph::{Graph, local_scan_k};
36///
37/// // Triangle: k=1 neighborhood of each vertex is the whole graph.
38/// let mut g = Graph::with_vertices(3);
39/// g.add_edge(0, 1).unwrap();
40/// g.add_edge(1, 2).unwrap();
41/// g.add_edge(2, 0).unwrap();
42/// let s = local_scan_k(&g, 1, None).unwrap();
43/// assert!((s[0] - 3.0).abs() < 1e-10);
44///
45/// // Path 0-1-2-3-4 with k=2:
46/// // N_2[0] = {0,1,2}, edges {0-1,1-2} → 2
47/// // N_2[2] = {0,1,2,3,4}, all 4 edges → 4
48/// let mut g = Graph::with_vertices(5);
49/// for i in 0..4u32 { g.add_edge(i, i+1).unwrap(); }
50/// let s = local_scan_k(&g, 2, None).unwrap();
51/// assert!((s[0] - 2.0).abs() < 1e-10);
52/// assert!((s[2] - 4.0).abs() < 1e-10);
53/// ```
54pub fn local_scan_k(graph: &Graph, k: u32, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
55    let n = graph.vcount();
56    let ecount = graph.ecount();
57
58    if let Some(w) = weights {
59        if w.len() != ecount {
60            return Err(IgraphError::InvalidArgument(format!(
61                "local_scan_k: weights length ({}) does not match edge count ({ecount})",
62                w.len()
63            )));
64        }
65    }
66
67    let n_usize = n as usize;
68    let mut result = vec![0.0_f64; n_usize];
69
70    if n == 0 || ecount == 0 {
71        return Ok(result);
72    }
73
74    let m_u32 =
75        u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
76
77    // Build adjacency lists.
78    let mut adj: Vec<Vec<u32>> = vec![Vec::new(); n_usize];
79    for eid in 0..m_u32 {
80        let (from, to) = graph.edge(eid)?;
81        adj[from as usize].push(to);
82        if !graph.is_directed() && from != to {
83            adj[to as usize].push(from);
84        }
85    }
86
87    // For each vertex, BFS to distance k, mark all reachable vertices,
88    // then count edges with both endpoints marked.
89    let mut marked = vec![0u32; n_usize];
90    let mut queue: VecDeque<(u32, u32)> = VecDeque::new();
91
92    for v in 0..n {
93        let tag = v + 1;
94        marked[v as usize] = tag;
95        queue.clear();
96        queue.push_back((v, 0));
97
98        while let Some((cur, dist)) = queue.pop_front() {
99            if dist < k {
100                for &nei in &adj[cur as usize] {
101                    if marked[nei as usize] != tag {
102                        marked[nei as usize] = tag;
103                        queue.push_back((nei, dist + 1));
104                    }
105                }
106            }
107        }
108
109        // Count edges with both endpoints marked.
110        let mut count = 0.0_f64;
111        for eid in 0..m_u32 {
112            let (from, to) = graph.edge(eid)?;
113            if marked[from as usize] == tag && marked[to as usize] == tag {
114                let w = weights.map_or(1.0, |ws| ws[eid as usize]);
115                count += w;
116            }
117        }
118
119        result[v as usize] = count;
120    }
121
122    Ok(result)
123}
124
125/// Collect incident edge IDs for a vertex respecting mode.
126fn incident_with_mode(graph: &Graph, v: VertexId, mode: DegreeMode) -> IgraphResult<Vec<u32>> {
127    if !graph.is_directed() {
128        return graph.incident(v);
129    }
130    match mode {
131        DegreeMode::Out => graph.incident(v),
132        DegreeMode::In => graph.incident_in(v),
133        DegreeMode::All => {
134            let mut out = graph.incident(v)?;
135            let in_edges = graph.incident_in(v)?;
136            out.extend(in_edges);
137            Ok(out)
138        }
139    }
140}
141
142/// Mode-aware local scan-k edge count / weight sum.
143///
144/// For each vertex, counts edges (or sums edge weights) whose both
145/// endpoints lie within the k-neighbourhood. Supports directed modes.
146///
147/// # Examples
148///
149/// ```
150/// use rust_igraph::{Graph, local_scan_k_ecount, DegreeMode};
151///
152/// let mut g = Graph::with_vertices(4);
153/// g.add_edge(0, 1).unwrap();
154/// g.add_edge(1, 2).unwrap();
155/// g.add_edge(2, 3).unwrap();
156/// let res = local_scan_k_ecount(&g, 2, None, DegreeMode::All).unwrap();
157/// assert!((res[0] - 2.0).abs() < 1e-10);
158/// assert!((res[1] - 3.0).abs() < 1e-10);
159/// ```
160pub fn local_scan_k_ecount(
161    graph: &Graph,
162    k: u32,
163    weights: Option<&[f64]>,
164    mode: DegreeMode,
165) -> IgraphResult<Vec<f64>> {
166    if let Some(w) = weights {
167        if w.len() != graph.ecount() {
168            return Err(IgraphError::InvalidArgument(format!(
169                "local_scan_k_ecount: weights length ({}) != edge count ({})",
170                w.len(),
171                graph.ecount()
172            )));
173        }
174    }
175
176    let n = graph.vcount();
177    let n_usize = n as usize;
178    let ecount = graph.ecount();
179    let mut result = vec![0.0_f64; n_usize];
180
181    if n == 0 || ecount == 0 {
182        return Ok(result);
183    }
184
185    let undirected_or_all = mode == DegreeMode::All || !graph.is_directed();
186    let mut marker: Vec<i64> = vec![-1; n_usize];
187    let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
188
189    for node in 0..n {
190        let node_i = i64::from(node);
191        queue.clear();
192        queue.push_back((node, 0));
193        marker[node as usize] = node_i;
194
195        while let Some((act, dist)) = queue.pop_front() {
196            let next_dist = dist
197                .checked_add(1)
198                .ok_or(IgraphError::Internal("k-scan distance overflow"))?;
199            let edges = incident_with_mode(graph, act, mode)?;
200
201            for &e in &edges {
202                let nei = graph.edge_other(e, act)?;
203                let w = weights.map_or(1.0, |ws| ws[e as usize]);
204
205                if next_dist <= k || marker[nei as usize] == node_i {
206                    result[node as usize] += w;
207                }
208
209                if next_dist <= k && marker[nei as usize] != node_i {
210                    queue.push_back((nei, next_dist));
211                    marker[nei as usize] = node_i;
212                }
213            }
214        }
215
216        if undirected_or_all {
217            result[node as usize] /= 2.0;
218        }
219    }
220
221    Ok(result)
222}
223
224/// Mode-aware local scan-k on two graphs.
225///
226/// Counts edges from `them` whose both endpoints lie within the
227/// k-neighbourhood defined by BFS in `us`.
228///
229/// # Examples
230///
231/// ```
232/// use rust_igraph::{Graph, local_scan_k_ecount_them, DegreeMode};
233///
234/// let mut us = Graph::with_vertices(3);
235/// us.add_edge(0, 1).unwrap();
236/// us.add_edge(1, 2).unwrap();
237/// let mut them = Graph::with_vertices(3);
238/// them.add_edge(0, 1).unwrap();
239/// them.add_edge(1, 2).unwrap();
240/// them.add_edge(0, 2).unwrap();
241/// let res = local_scan_k_ecount_them(&us, &them, 2, None, DegreeMode::All).unwrap();
242/// assert!((res[0] - 3.0).abs() < 1e-10);
243/// ```
244pub fn local_scan_k_ecount_them(
245    us: &Graph,
246    them: &Graph,
247    k: u32,
248    weights_them: Option<&[f64]>,
249    mode: DegreeMode,
250) -> IgraphResult<Vec<f64>> {
251    if us.vcount() != them.vcount() {
252        return Err(IgraphError::InvalidArgument(
253            "local_scan_k_ecount_them: vertex count mismatch".to_string(),
254        ));
255    }
256    if us.is_directed() != them.is_directed() {
257        return Err(IgraphError::InvalidArgument(
258            "local_scan_k_ecount_them: directedness mismatch".to_string(),
259        ));
260    }
261    if let Some(w) = weights_them {
262        if w.len() != them.ecount() {
263            return Err(IgraphError::InvalidArgument(format!(
264                "local_scan_k_ecount_them: weight vector length {} != them edge count {}",
265                w.len(),
266                them.ecount()
267            )));
268        }
269    }
270
271    let n = us.vcount();
272    let n_usize = n as usize;
273    let mut result = vec![0.0_f64; n_usize];
274    let mut marker: Vec<i64> = vec![-1; n_usize];
275    let undirected_or_all = mode == DegreeMode::All || !us.is_directed();
276
277    let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
278    let mut marked_vertices: Vec<VertexId> = Vec::new();
279
280    for node in 0..n {
281        let node_i = i64::from(node);
282        queue.clear();
283        marked_vertices.clear();
284
285        queue.push_back((node, 0));
286        marker[node as usize] = node_i;
287        marked_vertices.push(node);
288
289        while let Some((act, dist)) = queue.pop_front() {
290            let next_dist = dist
291                .checked_add(1)
292                .ok_or(IgraphError::Internal("k-scan distance overflow"))?;
293            let edges = incident_with_mode(us, act, mode)?;
294
295            for &e in &edges {
296                let nei = us.edge_other(e, act)?;
297                if next_dist <= k && marker[nei as usize] != node_i {
298                    queue.push_back((nei, next_dist));
299                    marker[nei as usize] = node_i;
300                    marked_vertices.push(nei);
301                }
302            }
303        }
304
305        for &mv in &marked_vertices {
306            let them_edges = incident_with_mode(them, mv, mode)?;
307            for &e in &them_edges {
308                let nei = them.edge_other(e, mv)?;
309                if marker[nei as usize] == node_i {
310                    let w = weights_them.map_or(1.0, |ws| ws[e as usize]);
311                    result[node as usize] += w;
312                }
313            }
314        }
315
316        if undirected_or_all {
317            result[node as usize] /= 2.0;
318        }
319    }
320
321    Ok(result)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    fn close(a: f64, b: f64) -> bool {
329        (a - b).abs() < 1e-10
330    }
331
332    #[test]
333    fn empty_graph() {
334        let g = Graph::with_vertices(0);
335        let s = local_scan_k(&g, 1, None).unwrap();
336        assert!(s.is_empty());
337    }
338
339    #[test]
340    fn no_edges() {
341        let g = Graph::with_vertices(5);
342        let s = local_scan_k(&g, 2, None).unwrap();
343        assert!(s.iter().all(|&v| close(v, 0.0)));
344    }
345
346    #[test]
347    fn k_zero_no_self_loops() {
348        // k=0: only the vertex itself is in the neighborhood.
349        // No self-loops → no edges in neighborhood.
350        let mut g = Graph::with_vertices(3);
351        g.add_edge(0, 1).unwrap();
352        g.add_edge(1, 2).unwrap();
353        let s = local_scan_k(&g, 0, None).unwrap();
354        assert!(s.iter().all(|&v| close(v, 0.0)));
355    }
356
357    #[test]
358    fn k_zero_with_self_loop() {
359        // k=0: only the vertex itself. Self-loop on 0.
360        let mut g = Graph::with_vertices(2);
361        g.add_edge(0, 0).unwrap();
362        g.add_edge(0, 1).unwrap();
363        let s = local_scan_k(&g, 0, None).unwrap();
364        assert!(close(s[0], 1.0)); // self-loop
365        assert!(close(s[1], 0.0)); // no self-loop on 1
366    }
367
368    #[test]
369    fn k_one_triangle() {
370        // Same as local_scan_1: each vertex sees all 3 edges.
371        let mut g = Graph::with_vertices(3);
372        g.add_edge(0, 1).unwrap();
373        g.add_edge(1, 2).unwrap();
374        g.add_edge(2, 0).unwrap();
375        let s = local_scan_k(&g, 1, None).unwrap();
376        assert!(close(s[0], 3.0));
377        assert!(close(s[1], 3.0));
378        assert!(close(s[2], 3.0));
379    }
380
381    #[test]
382    fn k_one_path_5() {
383        // Same as local_scan_1.
384        let mut g = Graph::with_vertices(5);
385        for i in 0..4u32 {
386            g.add_edge(i, i + 1).unwrap();
387        }
388        let s = local_scan_k(&g, 1, None).unwrap();
389        assert!(close(s[0], 1.0));
390        assert!(close(s[1], 2.0));
391        assert!(close(s[2], 2.0));
392        assert!(close(s[3], 2.0));
393        assert!(close(s[4], 1.0));
394    }
395
396    #[test]
397    fn k_two_path_5() {
398        // N_2[0] = {0,1,2}: edges {0-1, 1-2} → 2
399        // N_2[1] = {0,1,2,3}: edges {0-1, 1-2, 2-3} → 3
400        // N_2[2] = {0,1,2,3,4}: all 4 edges → 4
401        // N_2[3] = {1,2,3,4}: edges {1-2, 2-3, 3-4} → 3
402        // N_2[4] = {2,3,4}: edges {2-3, 3-4} → 2
403        let mut g = Graph::with_vertices(5);
404        for i in 0..4u32 {
405            g.add_edge(i, i + 1).unwrap();
406        }
407        let s = local_scan_k(&g, 2, None).unwrap();
408        assert!(close(s[0], 2.0));
409        assert!(close(s[1], 3.0));
410        assert!(close(s[2], 4.0));
411        assert!(close(s[3], 3.0));
412        assert!(close(s[4], 2.0));
413    }
414
415    #[test]
416    fn k_large_returns_total_edges() {
417        // k large enough to cover entire graph → every vertex sees all edges.
418        let mut g = Graph::with_vertices(5);
419        for i in 0..4u32 {
420            g.add_edge(i, i + 1).unwrap();
421        }
422        let s = local_scan_k(&g, 100, None).unwrap();
423        for &val in &s {
424            assert!(close(val, 4.0));
425        }
426    }
427
428    #[test]
429    fn two_components() {
430        // {0,1,2} triangle + {3,4} edge.
431        // N_1[0] = {0,1,2}: 3 edges.
432        // N_1[3] = {3,4}: 1 edge.
433        let mut g = Graph::with_vertices(5);
434        g.add_edge(0, 1).unwrap();
435        g.add_edge(1, 2).unwrap();
436        g.add_edge(2, 0).unwrap();
437        g.add_edge(3, 4).unwrap();
438        let s = local_scan_k(&g, 1, None).unwrap();
439        assert!(close(s[0], 3.0));
440        assert!(close(s[3], 1.0));
441        assert!(close(s[4], 1.0));
442    }
443
444    #[test]
445    fn weighted() {
446        // Triangle with weights [2, 3, 5], k=1.
447        let mut g = Graph::with_vertices(3);
448        g.add_edge(0, 1).unwrap();
449        g.add_edge(1, 2).unwrap();
450        g.add_edge(2, 0).unwrap();
451        let w = vec![2.0, 3.0, 5.0];
452        let s = local_scan_k(&g, 1, Some(&w)).unwrap();
453        // Each vertex sees all 3 edges → sum = 10.
454        assert!(close(s[0], 10.0));
455        assert!(close(s[1], 10.0));
456        assert!(close(s[2], 10.0));
457    }
458
459    #[test]
460    fn star_k2() {
461        // Star: 0 connected to 1,2,3. k=2: N_2[1] = {0,1,2,3} (all).
462        // All 3 edges in that set → 3.
463        let mut g = Graph::with_vertices(4);
464        g.add_edge(0, 1).unwrap();
465        g.add_edge(0, 2).unwrap();
466        g.add_edge(0, 3).unwrap();
467        let s = local_scan_k(&g, 2, None).unwrap();
468        // N_2[0] = all → 3, N_2[1] = all → 3
469        assert!(close(s[0], 3.0));
470        assert!(close(s[1], 3.0));
471        assert!(close(s[2], 3.0));
472        assert!(close(s[3], 3.0));
473    }
474
475    #[test]
476    fn weights_mismatch_errors() {
477        let mut g = Graph::with_vertices(2);
478        g.add_edge(0, 1).unwrap();
479        assert!(local_scan_k(&g, 1, Some(&[1.0, 2.0])).is_err());
480    }
481}