use super::csr::CsrIndex;
pub struct Traversal {
stamp: Vec<u32>,
cur_gen: u32,
}
impl Traversal {
pub fn new(n_nodes: usize) -> Self {
Self {
stamp: vec![0u32; n_nodes],
cur_gen: 0,
}
}
pub fn bounded_bfs(
&mut self,
csr: &CsrIndex,
seeds: &[usize],
hops: usize,
max_frontier: usize,
) -> Vec<usize> {
self.cur_gen = self.cur_gen.wrapping_add(1);
if self.cur_gen == 0 {
for s in self.stamp.iter_mut() {
*s = 0;
}
self.cur_gen = 1;
}
let cur_gen = self.cur_gen;
let n = csr.n_nodes();
let mut out: Vec<usize> = Vec::new();
let mut frontier: Vec<usize> = Vec::new();
for &s in seeds {
if out.len() >= max_frontier {
break;
}
if s >= n || s >= self.stamp.len() || self.stamp[s] == cur_gen {
continue;
}
self.stamp[s] = cur_gen;
out.push(s);
frontier.push(s);
}
for _ in 0..hops {
if out.len() >= max_frontier || frontier.is_empty() {
break;
}
let mut next: Vec<usize> = Vec::new();
'level: for &node in &frontier {
for &nbr in csr.neighbors(node) {
if out.len() >= max_frontier {
break 'level;
}
let nbr = nbr as usize;
if nbr >= n || nbr >= self.stamp.len() || self.stamp[nbr] == cur_gen {
continue;
}
self.stamp[nbr] = cur_gen;
out.push(nbr);
next.push(nbr);
}
}
frontier = next;
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use urna_format::sections::graph::{EDGE_TYPE_NEXT_CHUNK, Edge, encode_graph_adjacency};
fn line_csr(n: usize) -> CsrIndex {
let mut edges = Vec::new();
for i in 0..n.saturating_sub(1) {
edges.push(Edge {
src: i as u32,
dst: (i + 1) as u32,
edge_type: EDGE_TYPE_NEXT_CHUNK,
});
edges.push(Edge {
src: (i + 1) as u32,
dst: i as u32,
edge_type: EDGE_TYPE_NEXT_CHUNK,
});
}
let payload = encode_graph_adjacency(n, &edges).unwrap();
CsrIndex::from_bytes(&payload, n).unwrap()
}
#[test]
fn one_hop_collects_immediate_neighbors() {
let csr = line_csr(10);
let mut t = Traversal::new(10);
let mut got = t.bounded_bfs(&csr, &[5], 1, 100);
got.sort_unstable();
assert_eq!(got, vec![4, 5, 6]);
}
#[test]
fn hops_bound_the_radius() {
let csr = line_csr(20);
let mut t = Traversal::new(20);
let mut got = t.bounded_bfs(&csr, &[10], 2, 100);
got.sort_unstable();
assert_eq!(got, vec![8, 9, 10, 11, 12]);
}
#[test]
fn max_frontier_caps_the_candidate_count() {
let csr = line_csr(50);
let mut t = Traversal::new(50);
let got = t.bounded_bfs(&csr, &[25], 10, 5);
assert_eq!(got.len(), 5, "max_frontier must cap the result");
}
#[test]
fn dedup_and_seeds_come_first() {
let csr = line_csr(10);
let mut t = Traversal::new(10);
let got = t.bounded_bfs(&csr, &[3, 3, 4], 1, 100);
let mut sorted = got.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), got.len(), "result must be deduped");
assert_eq!(&got[..2], &[3, 4], "seeds come first, in order");
}
#[test]
fn generational_buffer_reuse_is_correct() {
let csr = line_csr(10);
let mut t = Traversal::new(10);
let q1 = t.bounded_bfs(&csr, &[1], 1, 100);
let q2 = t.bounded_bfs(&csr, &[8], 1, 100);
let mut s1 = q1.clone();
s1.sort_unstable();
let mut s2 = q2.clone();
s2.sort_unstable();
assert_eq!(s1, vec![0, 1, 2]);
assert_eq!(s2, vec![7, 8, 9]);
}
#[test]
fn zero_hops_returns_only_seeds() {
let csr = line_csr(10);
let mut t = Traversal::new(10);
let got = t.bounded_bfs(&csr, &[2, 5], 0, 100);
assert_eq!(got, vec![2, 5]);
}
#[test]
fn out_of_range_seed_is_ignored() {
let csr = line_csr(5);
let mut t = Traversal::new(5);
let got = t.bounded_bfs(&csr, &[99, 2], 1, 100);
let mut sorted = got.clone();
sorted.sort_unstable();
assert_eq!(sorted, vec![1, 2, 3]);
}
}