Skip to main content

concept_graph/
csr.rs

1//! Compressed sparse row adjacency.
2//!
3//! One `Csr` holds the out-edges of one edge kind in one direction: an
4//! offsets array of `node_count + 1` entries and a targets array, so the
5//! neighbours of ordinal `n` are `targets[offsets[n]..offsets[n + 1]]`.
6//! Targets are sorted and deduplicated within each row.
7
8use crate::ordinal::{Ordinal, to_usize};
9
10/// A failure while building adjacency.
11#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum CsrError {
13    /// An edge names an ordinal at or beyond the node count.
14    #[error("ordinal {ordinal} is out of range for {nodes} nodes")]
15    OutOfRange {
16        /// The offending ordinal.
17        ordinal: Ordinal,
18        /// The node count.
19        nodes: u32,
20    },
21    /// The offsets array is not monotone or does not end at the targets length.
22    #[error("offsets are inconsistent with {targets} targets")]
23    Offsets {
24        /// The targets length.
25        targets: usize,
26    },
27}
28
29/// Compressed sparse row adjacency for one edge kind and direction.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Csr {
32    offsets: Vec<u32>,
33    targets: Vec<u32>,
34}
35
36impl Csr {
37    /// Builds adjacency over `nodes` ordinals from `(from, to)` pairs.
38    ///
39    /// Duplicate pairs collapse; each row comes out sorted.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`CsrError::OutOfRange`] when an edge names an ordinal at or
44    /// beyond `nodes`, and [`CsrError::Offsets`] when the edge count exceeds `u32`.
45    pub fn build(
46        nodes: u32,
47        edges: impl IntoIterator<Item = (Ordinal, Ordinal)>,
48    ) -> Result<Self, CsrError> {
49        let mut pairs: Vec<(u32, u32)> = Vec::new();
50        for (from, to) in edges {
51            for ordinal in [from, to] {
52                if ordinal.index() >= nodes {
53                    return Err(CsrError::OutOfRange { ordinal, nodes });
54                }
55            }
56            pairs.push((from.index(), to.index()));
57        }
58        pairs.sort_unstable();
59        pairs.dedup();
60        // A target count past u32::MAX is inconsistent with the offsets; the
61        // conversion error adds nothing to that.
62        let count = |targets: &Vec<u32>| {
63            let Ok(count) = u32::try_from(targets.len()) else {
64                return Err(CsrError::Offsets {
65                    targets: targets.len(),
66                });
67            };
68            Ok(count)
69        };
70        let mut offsets = Vec::with_capacity(to_usize(nodes).saturating_add(1));
71        let mut targets = Vec::with_capacity(pairs.len());
72        let mut cursor = 0_usize;
73        for node in 0..nodes {
74            offsets.push(count(&targets)?);
75            while let Some((from, to)) = pairs.get(cursor).copied() {
76                if from != node {
77                    break;
78                }
79                targets.push(to);
80                cursor = cursor.saturating_add(1);
81            }
82        }
83        offsets.push(count(&targets)?);
84        Ok(Self { offsets, targets })
85    }
86
87    /// Reassembles adjacency from its two arrays, checking their consistency.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`CsrError::Offsets`] when `offsets` is empty, not monotone, or
92    /// does not end at `targets.len()`.
93    pub fn from_parts(offsets: Vec<u32>, targets: Vec<u32>) -> Result<Self, CsrError> {
94        let consistent = offsets.first() == Some(&0)
95            && offsets.windows(2).all(|w| w.first() <= w.get(1))
96            && offsets.last().copied().map(to_usize) == Some(targets.len());
97        if !consistent {
98            return Err(CsrError::Offsets {
99                targets: targets.len(),
100            });
101        }
102        Ok(Self { offsets, targets })
103    }
104
105    /// The number of nodes.
106    #[must_use]
107    pub fn nodes(&self) -> u32 {
108        u32::try_from(self.offsets.len().saturating_sub(1)).unwrap_or(u32::MAX)
109    }
110
111    /// The number of edges.
112    #[must_use]
113    pub fn edges(&self) -> usize {
114        self.targets.len()
115    }
116
117    /// The heap bytes this adjacency holds
118    /// ([`crate::footprint`]).
119    #[must_use]
120    pub fn size_in_bytes(&self) -> usize {
121        crate::footprint::vector(&self.offsets)
122            .saturating_add(crate::footprint::vector(&self.targets))
123    }
124
125    /// The neighbours of `node`, sorted; empty for an unknown node.
126    #[must_use]
127    pub fn neighbours(&self, node: Ordinal) -> &[u32] {
128        let start = self.offsets.get(node.as_usize()).copied();
129        let end = self.offsets.get(node.as_usize().saturating_add(1)).copied();
130        match (start, end) {
131            (Some(start), Some(end)) => self
132                .targets
133                .get(to_usize(start)..to_usize(end))
134                .unwrap_or(&[]),
135            _ => &[],
136        }
137    }
138
139    /// The offsets array.
140    #[must_use]
141    pub fn offsets(&self) -> &[u32] {
142        &self.offsets
143    }
144
145    /// The targets array.
146    #[must_use]
147    pub fn targets(&self) -> &[u32] {
148        &self.targets
149    }
150
151    /// The same edges reversed.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`CsrError`] only if this adjacency is itself inconsistent.
156    pub fn transpose(&self) -> Result<Self, CsrError> {
157        let nodes = self.nodes();
158        let reversed = (0..nodes).flat_map(|from| {
159            self.neighbours(Ordinal::new(from))
160                .iter()
161                .map(move |to| (Ordinal::new(*to), Ordinal::new(from)))
162        });
163        Self::build(nodes, reversed)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::{Csr, CsrError};
170    use crate::ordinal::Ordinal;
171
172    fn o(i: u32) -> Ordinal {
173        Ordinal::new(i)
174    }
175
176    #[test]
177    fn rows_are_sorted_and_deduplicated() {
178        let csr = Csr::build(4, [(o(2), o(1)), (o(0), o(3)), (o(0), o(1)), (o(0), o(1))])
179            .expect("builds");
180        assert_eq!(csr.nodes(), 4);
181        assert_eq!(csr.edges(), 3);
182        assert_eq!(csr.neighbours(o(0)), &[1, 3]);
183        assert!(csr.neighbours(o(1)).is_empty());
184        assert_eq!(csr.neighbours(o(2)), &[1]);
185        assert!(csr.neighbours(o(9)).is_empty());
186        assert_eq!(csr.offsets(), &[0, 2, 2, 3, 3]);
187    }
188
189    #[test]
190    fn transpose_reverses_every_edge() {
191        let csr = Csr::build(3, [(o(0), o(1)), (o(0), o(2)), (o(1), o(2))]).expect("builds");
192        let back = csr.transpose().expect("transposes");
193        assert_eq!(back.neighbours(o(2)), &[0, 1]);
194        assert_eq!(back.neighbours(o(1)), &[0]);
195        assert!(back.neighbours(o(0)).is_empty());
196        assert_eq!(back.transpose().expect("transposes"), csr);
197    }
198
199    #[test]
200    fn out_of_range_and_inconsistent_parts_are_refused() {
201        assert_eq!(
202            Csr::build(2, [(o(0), o(2))]),
203            Err(CsrError::OutOfRange {
204                ordinal: o(2),
205                nodes: 2
206            })
207        );
208        assert!(Csr::from_parts(vec![0, 2, 1], vec![1]).is_err());
209        assert!(Csr::from_parts(vec![1, 1], Vec::new()).is_err());
210        assert!(Csr::from_parts(vec![0, 1], vec![0]).is_ok());
211    }
212}