weavatrix_graph/traversal_cache/
core.rs1use super::adaptive::AdaptivePackedU32;
2use super::build::{build_neighbor_pair, neighbor_bits, select_storage};
3use super::elias_fano::EliasFano;
4use super::iter::NeighborIter;
5use super::packed::PackedU32;
6use crate::{NodeIndex, Topology, Vec};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum TraversalStorage {
11 #[default]
13 Auto,
14 Fast,
16 Balanced,
18 Compact,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum TraversalLayout {
25 Fast,
26 Balanced {
27 neighbor_bits: u8,
28 },
29 Compact {
30 neighbor_bits: u8,
31 offset_low_bits: u8,
32 },
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct TraversalCache {
37 node_count: usize,
38 edge_count: usize,
39 pub(super) outgoing: NeighborCsr,
40 pub(super) incoming: NeighborCsr,
41 layout: TraversalLayout,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub(super) struct NeighborCsr {
46 pub(super) offsets: OffsetStorage,
47 pub(super) neighbors: NeighborStorage,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub(super) enum OffsetStorage {
52 Direct(Vec<u32>),
53 EliasFano(EliasFano),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub(super) enum NeighborStorage {
58 Direct(Vec<u32>),
59 Packed(PackedU32),
60 Adaptive(AdaptivePackedU32),
61}
62
63impl TraversalCache {
64 #[must_use]
65 pub fn from_topology(topology: &Topology) -> Self {
66 Self::with_storage(topology, TraversalStorage::Auto)
67 }
68
69 #[must_use]
70 pub fn with_storage(topology: &Topology, requested: TraversalStorage) -> Self {
71 let (endpoints, outgoing, incoming) = topology.traversal_parts();
72 let (out_neighbors, in_neighbors) =
73 build_neighbor_pair(endpoints, outgoing.edges(), incoming.edges());
74 let bits = neighbor_bits(topology.node_count());
75 let selected = select_storage(
76 requested,
77 topology.node_count(),
78 topology.edge_count(),
79 bits,
80 );
81 let (outgoing, layout) =
82 NeighborCsr::build(outgoing.offsets(), out_neighbors, selected, bits);
83 let (incoming, _) = NeighborCsr::build(incoming.offsets(), in_neighbors, selected, bits);
84 Self {
85 node_count: topology.node_count(),
86 edge_count: topology.edge_count(),
87 outgoing,
88 incoming,
89 layout,
90 }
91 }
92
93 #[must_use]
94 pub const fn node_count(&self) -> usize {
95 self.node_count
96 }
97
98 #[must_use]
99 pub const fn edge_count(&self) -> usize {
100 self.edge_count
101 }
102
103 #[must_use]
104 pub const fn layout(&self) -> TraversalLayout {
105 self.layout
106 }
107
108 #[must_use]
109 pub fn storage_bytes(&self) -> usize {
110 self.outgoing.storage_bytes() + self.incoming.storage_bytes()
111 }
112
113 #[must_use]
114 pub fn fast_equivalent_bytes(&self) -> usize {
115 (self.edge_count() * 2 + (self.node_count() + 1) * 2) * size_of::<u32>()
116 }
117
118 #[must_use]
119 pub fn outgoing_neighbors(&self, node: NodeIndex) -> NeighborIter<'_> {
120 self.outgoing.neighbors(node.index(), self.node_count())
121 }
122
123 #[must_use]
124 pub fn incoming_neighbors(&self, node: NodeIndex) -> NeighborIter<'_> {
125 self.incoming.neighbors(node.index(), self.node_count())
126 }
127
128 #[must_use]
129 pub fn out_degree(&self, node: NodeIndex) -> Option<usize> {
130 self.outgoing.degree(node.index(), self.node_count())
131 }
132
133 #[must_use]
134 pub fn in_degree(&self, node: NodeIndex) -> Option<usize> {
135 self.incoming.degree(node.index(), self.node_count())
136 }
137
138 pub(super) fn contains(&self, node: NodeIndex) -> bool {
139 node.index() < self.node_count()
140 }
141
142 pub(super) fn for_each_outgoing(&self, node: NodeIndex, visit: impl FnMut(NodeIndex)) {
143 self.outgoing
144 .for_each(node.index(), self.node_count(), visit);
145 }
146
147 pub(super) fn for_each_incoming(&self, node: NodeIndex, visit: impl FnMut(NodeIndex)) {
148 self.incoming
149 .for_each(node.index(), self.node_count(), visit);
150 }
151}
152
153impl NeighborCsr {
154 fn build(
155 offsets: &[u32],
156 neighbors: Vec<u32>,
157 storage: TraversalStorage,
158 bits: u8,
159 ) -> (Self, TraversalLayout) {
160 match storage {
161 TraversalStorage::Fast | TraversalStorage::Auto => (
162 Self {
163 offsets: OffsetStorage::Direct(offsets.to_vec()),
164 neighbors: NeighborStorage::Direct(neighbors),
165 },
166 TraversalLayout::Fast,
167 ),
168 TraversalStorage::Balanced => (
169 Self {
170 offsets: OffsetStorage::Direct(offsets.to_vec()),
171 neighbors: NeighborStorage::Packed(PackedU32::from_values(&neighbors, bits)),
172 },
173 TraversalLayout::Balanced {
174 neighbor_bits: bits,
175 },
176 ),
177 TraversalStorage::Compact => {
178 let offsets = EliasFano::from_monotone(offsets);
179 let offset_low_bits = offsets.low_bits();
180 let global_bytes =
181 neighbors.len().div_ceil(64) * usize::from(bits) * size_of::<u64>();
182 let neighbor_storage =
183 if AdaptivePackedU32::estimated_storage_bytes(&neighbors) < global_bytes {
184 NeighborStorage::Adaptive(AdaptivePackedU32::from_values(&neighbors))
185 } else {
186 NeighborStorage::Packed(PackedU32::from_values(&neighbors, bits))
187 };
188 (
189 Self {
190 offsets: OffsetStorage::EliasFano(offsets),
191 neighbors: neighbor_storage,
192 },
193 TraversalLayout::Compact {
194 neighbor_bits: bits,
195 offset_low_bits,
196 },
197 )
198 }
199 }
200 }
201
202 fn neighbors(&self, node: usize, node_count: usize) -> NeighborIter<'_> {
203 let Some((start, end)) = self.bounds(node, node_count) else {
204 return NeighborIter::empty(&self.neighbors);
205 };
206 NeighborIter::new(&self.neighbors, start, end)
207 }
208
209 fn degree(&self, node: usize, node_count: usize) -> Option<usize> {
210 self.bounds(node, node_count)
211 .map(|(start, end)| end - start)
212 }
213
214 fn bounds(&self, node: usize, node_count: usize) -> Option<(usize, usize)> {
215 (node < node_count).then(|| {
216 (
217 self.offsets.get(node) as usize,
218 self.offsets.get(node + 1) as usize,
219 )
220 })
221 }
222
223 fn storage_bytes(&self) -> usize {
224 self.offsets.storage_bytes() + self.neighbors.storage_bytes()
225 }
226
227 fn for_each(&self, node: usize, node_count: usize, visit: impl FnMut(NodeIndex)) {
228 let Some((start, end)) = self.bounds(node, node_count) else {
229 return;
230 };
231 self.neighbors.for_each(start, end, visit);
232 }
233}
234
235impl OffsetStorage {
236 #[inline]
237 pub(super) fn get(&self, index: usize) -> u32 {
238 match self {
239 Self::Direct(values) => values[index],
240 Self::EliasFano(values) => values.get(index),
241 }
242 }
243
244 fn storage_bytes(&self) -> usize {
245 match self {
246 Self::Direct(values) => values.len() * size_of::<u32>(),
247 Self::EliasFano(values) => values.storage_bytes(),
248 }
249 }
250}
251
252impl NeighborStorage {
253 #[inline]
254 pub(super) fn get(&self, index: usize) -> u32 {
255 match self {
256 Self::Direct(values) => values[index],
257 Self::Packed(values) => values.get(index),
258 Self::Adaptive(values) => values.get(index),
259 }
260 }
261
262 fn storage_bytes(&self) -> usize {
263 match self {
264 Self::Direct(values) => values.len() * size_of::<u32>(),
265 Self::Packed(values) => values.storage_bytes(),
266 Self::Adaptive(values) => values.storage_bytes(),
267 }
268 }
269
270 fn for_each(&self, start: usize, end: usize, mut visit: impl FnMut(NodeIndex)) {
271 match self {
272 Self::Direct(values) => {
273 for &neighbor in &values[start..end] {
274 visit(NodeIndex::new(neighbor));
275 }
276 }
277 Self::Packed(values) => {
278 values.for_each(start, end, |raw| visit(NodeIndex::new(raw)));
279 }
280 Self::Adaptive(values) => {
281 values.for_each(start, end, |raw| visit(NodeIndex::new(raw)));
282 }
283 }
284 }
285}