urna_runtime/graph/
traverse.rs1use super::csr::CsrIndex;
13
14pub struct Traversal {
18 stamp: Vec<u32>,
22 cur_gen: u32,
23}
24
25impl Traversal {
26 pub fn new(n_nodes: usize) -> Self {
27 Self {
28 stamp: vec![0u32; n_nodes],
29 cur_gen: 0,
30 }
31 }
32
33 pub fn bounded_bfs(
38 &mut self,
39 csr: &CsrIndex,
40 seeds: &[usize],
41 hops: usize,
42 max_frontier: usize,
43 ) -> Vec<usize> {
44 self.cur_gen = self.cur_gen.wrapping_add(1);
47 if self.cur_gen == 0 {
48 for s in self.stamp.iter_mut() {
49 *s = 0;
50 }
51 self.cur_gen = 1;
52 }
53 let cur_gen = self.cur_gen;
54 let n = csr.n_nodes();
55
56 let mut out: Vec<usize> = Vec::new();
57 let mut frontier: Vec<usize> = Vec::new();
58
59 for &s in seeds {
60 if out.len() >= max_frontier {
61 break;
62 }
63 if s >= n || s >= self.stamp.len() || self.stamp[s] == cur_gen {
64 continue;
65 }
66 self.stamp[s] = cur_gen;
67 out.push(s);
68 frontier.push(s);
69 }
70
71 for _ in 0..hops {
74 if out.len() >= max_frontier || frontier.is_empty() {
75 break;
76 }
77 let mut next: Vec<usize> = Vec::new();
78 'level: for &node in &frontier {
79 for &nbr in csr.neighbors(node) {
80 if out.len() >= max_frontier {
81 break 'level;
82 }
83 let nbr = nbr as usize;
84 if nbr >= n || nbr >= self.stamp.len() || self.stamp[nbr] == cur_gen {
85 continue;
86 }
87 self.stamp[nbr] = cur_gen;
88 out.push(nbr);
89 next.push(nbr);
90 }
91 }
92 frontier = next;
93 }
94 out
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use urna_format::sections::graph::{EDGE_TYPE_NEXT_CHUNK, Edge, encode_graph_adjacency};
102
103 fn line_csr(n: usize) -> CsrIndex {
106 let mut edges = Vec::new();
107 for i in 0..n.saturating_sub(1) {
108 edges.push(Edge {
109 src: i as u32,
110 dst: (i + 1) as u32,
111 edge_type: EDGE_TYPE_NEXT_CHUNK,
112 });
113 edges.push(Edge {
114 src: (i + 1) as u32,
115 dst: i as u32,
116 edge_type: EDGE_TYPE_NEXT_CHUNK,
117 });
118 }
119 let payload = encode_graph_adjacency(n, &edges).unwrap();
120 CsrIndex::from_bytes(&payload, n).unwrap()
121 }
122
123 #[test]
124 fn one_hop_collects_immediate_neighbors() {
125 let csr = line_csr(10);
126 let mut t = Traversal::new(10);
127 let mut got = t.bounded_bfs(&csr, &[5], 1, 100);
129 got.sort_unstable();
130 assert_eq!(got, vec![4, 5, 6]);
131 }
132
133 #[test]
134 fn hops_bound_the_radius() {
135 let csr = line_csr(20);
136 let mut t = Traversal::new(20);
137 let mut got = t.bounded_bfs(&csr, &[10], 2, 100);
139 got.sort_unstable();
140 assert_eq!(got, vec![8, 9, 10, 11, 12]);
141 }
142
143 #[test]
144 fn max_frontier_caps_the_candidate_count() {
145 let csr = line_csr(50);
146 let mut t = Traversal::new(50);
147 let got = t.bounded_bfs(&csr, &[25], 10, 5);
148 assert_eq!(got.len(), 5, "max_frontier must cap the result");
149 }
150
151 #[test]
152 fn dedup_and_seeds_come_first() {
153 let csr = line_csr(10);
154 let mut t = Traversal::new(10);
155 let got = t.bounded_bfs(&csr, &[3, 3, 4], 1, 100);
157 let mut sorted = got.clone();
158 sorted.sort_unstable();
159 sorted.dedup();
160 assert_eq!(sorted.len(), got.len(), "result must be deduped");
161 assert_eq!(&got[..2], &[3, 4], "seeds come first, in order");
162 }
163
164 #[test]
165 fn generational_buffer_reuse_is_correct() {
166 let csr = line_csr(10);
167 let mut t = Traversal::new(10);
168 let q1 = t.bounded_bfs(&csr, &[1], 1, 100);
171 let q2 = t.bounded_bfs(&csr, &[8], 1, 100);
172 let mut s1 = q1.clone();
173 s1.sort_unstable();
174 let mut s2 = q2.clone();
175 s2.sort_unstable();
176 assert_eq!(s1, vec![0, 1, 2]);
177 assert_eq!(s2, vec![7, 8, 9]);
178 }
179
180 #[test]
181 fn zero_hops_returns_only_seeds() {
182 let csr = line_csr(10);
183 let mut t = Traversal::new(10);
184 let got = t.bounded_bfs(&csr, &[2, 5], 0, 100);
185 assert_eq!(got, vec![2, 5]);
186 }
187
188 #[test]
189 fn out_of_range_seed_is_ignored() {
190 let csr = line_csr(5);
191 let mut t = Traversal::new(5);
192 let got = t.bounded_bfs(&csr, &[99, 2], 1, 100);
193 let mut sorted = got.clone();
194 sorted.sort_unstable();
195 assert_eq!(sorted, vec![1, 2, 3]);
196 }
197}