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 =
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}