Skip to main content

weavatrix_graph/traversal_cache/
core.rs

1use 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/// Chooses the speed/space trade-off of a derived traversal cache.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum TraversalStorage {
11    /// Selects `Balanced` when packing saves at least 12.5%, otherwise `Fast`.
12    #[default]
13    Auto,
14    /// Direct `u32` neighbors and offsets for minimum traversal overhead.
15    Fast,
16    /// Bit-packed neighbors with direct `u32` offsets.
17    Balanced,
18    /// Bit-packed neighbors and Elias-Fano monotone offsets.
19    Compact,
20}
21
22/// Actual physical layout selected for a traversal cache.
23#[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 = if AdaptivePackedU32::estimated_storage_bytes(&neighbors)
183                    < global_bytes
184                {
185                    match AdaptivePackedU32::try_from_values(&neighbors) {
186                        Some(values) => NeighborStorage::Adaptive(values),
187                        None => NeighborStorage::Packed(PackedU32::from_values(&neighbors, bits)),
188                    }
189                } else {
190                    NeighborStorage::Packed(PackedU32::from_values(&neighbors, bits))
191                };
192                (
193                    Self {
194                        offsets: OffsetStorage::EliasFano(offsets),
195                        neighbors: neighbor_storage,
196                    },
197                    TraversalLayout::Compact {
198                        neighbor_bits: bits,
199                        offset_low_bits,
200                    },
201                )
202            }
203        }
204    }
205
206    fn neighbors(&self, node: usize, node_count: usize) -> NeighborIter<'_> {
207        let Some((start, end)) = self.bounds(node, node_count) else {
208            return NeighborIter::empty(&self.neighbors);
209        };
210        NeighborIter::new(&self.neighbors, start, end)
211    }
212
213    fn degree(&self, node: usize, node_count: usize) -> Option<usize> {
214        self.bounds(node, node_count)
215            .map(|(start, end)| end - start)
216    }
217
218    fn bounds(&self, node: usize, node_count: usize) -> Option<(usize, usize)> {
219        (node < node_count).then(|| {
220            (
221                self.offsets.get(node) as usize,
222                self.offsets.get(node + 1) as usize,
223            )
224        })
225    }
226
227    fn storage_bytes(&self) -> usize {
228        self.offsets.storage_bytes() + self.neighbors.storage_bytes()
229    }
230
231    fn for_each(&self, node: usize, node_count: usize, visit: impl FnMut(NodeIndex)) {
232        let Some((start, end)) = self.bounds(node, node_count) else {
233            return;
234        };
235        self.neighbors.for_each(start, end, visit);
236    }
237}
238
239impl OffsetStorage {
240    #[inline]
241    pub(super) fn get(&self, index: usize) -> u32 {
242        match self {
243            Self::Direct(values) => values[index],
244            Self::EliasFano(values) => values.get(index),
245        }
246    }
247
248    fn storage_bytes(&self) -> usize {
249        match self {
250            Self::Direct(values) => values.len() * size_of::<u32>(),
251            Self::EliasFano(values) => values.storage_bytes(),
252        }
253    }
254}
255
256impl NeighborStorage {
257    #[inline]
258    pub(super) fn get(&self, index: usize) -> u32 {
259        match self {
260            Self::Direct(values) => values[index],
261            Self::Packed(values) => values.get(index),
262            Self::Adaptive(values) => values.get(index),
263        }
264    }
265
266    fn storage_bytes(&self) -> usize {
267        match self {
268            Self::Direct(values) => values.len() * size_of::<u32>(),
269            Self::Packed(values) => values.storage_bytes(),
270            Self::Adaptive(values) => values.storage_bytes(),
271        }
272    }
273
274    fn for_each(&self, start: usize, end: usize, mut visit: impl FnMut(NodeIndex)) {
275        match self {
276            Self::Direct(values) => {
277                for &neighbor in &values[start..end] {
278                    visit(NodeIndex::new(neighbor));
279                }
280            }
281            Self::Packed(values) => {
282                values.for_each(start, end, |raw| visit(NodeIndex::new(raw)));
283            }
284            Self::Adaptive(values) => {
285                values.for_each(start, end, |raw| visit(NodeIndex::new(raw)));
286            }
287        }
288    }
289}