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        let count = |targets: &Vec<u32>| {
61            u32::try_from(targets.len()).map_err(|_| CsrError::Offsets {
62                targets: targets.len(),
63            })
64        };
65        let mut offsets = Vec::with_capacity(to_usize(nodes).saturating_add(1));
66        let mut targets = Vec::with_capacity(pairs.len());
67        let mut cursor = 0_usize;
68        for node in 0..nodes {
69            offsets.push(count(&targets)?);
70            while let Some((from, to)) = pairs.get(cursor).copied() {
71                if from != node {
72                    break;
73                }
74                targets.push(to);
75                cursor = cursor.saturating_add(1);
76            }
77        }
78        offsets.push(count(&targets)?);
79        Ok(Self { offsets, targets })
80    }
81
82    /// Reassembles adjacency from its two arrays, checking their consistency.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`CsrError::Offsets`] when `offsets` is empty, not monotone, or
87    /// does not end at `targets.len()`.
88    pub fn from_parts(offsets: Vec<u32>, targets: Vec<u32>) -> Result<Self, CsrError> {
89        let consistent = offsets.first() == Some(&0)
90            && offsets.windows(2).all(|w| w.first() <= w.get(1))
91            && offsets.last().copied().map(to_usize) == Some(targets.len());
92        if !consistent {
93            return Err(CsrError::Offsets {
94                targets: targets.len(),
95            });
96        }
97        Ok(Self { offsets, targets })
98    }
99
100    /// The number of nodes.
101    #[must_use]
102    pub fn nodes(&self) -> u32 {
103        u32::try_from(self.offsets.len().saturating_sub(1)).unwrap_or(u32::MAX)
104    }
105
106    /// The number of edges.
107    #[must_use]
108    pub fn edges(&self) -> usize {
109        self.targets.len()
110    }
111
112    /// The neighbours of `node`, sorted; empty for an unknown node.
113    #[must_use]
114    pub fn neighbours(&self, node: Ordinal) -> &[u32] {
115        let start = self.offsets.get(node.as_usize()).copied();
116        let end = self.offsets.get(node.as_usize().saturating_add(1)).copied();
117        match (start, end) {
118            (Some(start), Some(end)) => self
119                .targets
120                .get(to_usize(start)..to_usize(end))
121                .unwrap_or(&[]),
122            _ => &[],
123        }
124    }
125
126    /// The offsets array.
127    #[must_use]
128    pub fn offsets(&self) -> &[u32] {
129        &self.offsets
130    }
131
132    /// The targets array.
133    #[must_use]
134    pub fn targets(&self) -> &[u32] {
135        &self.targets
136    }
137
138    /// The same edges reversed.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`CsrError`] only if this adjacency is itself inconsistent.
143    pub fn transpose(&self) -> Result<Self, CsrError> {
144        let nodes = self.nodes();
145        let reversed = (0..nodes).flat_map(|from| {
146            self.neighbours(Ordinal::new(from))
147                .iter()
148                .map(move |to| (Ordinal::new(*to), Ordinal::new(from)))
149        });
150        Self::build(nodes, reversed)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::{Csr, CsrError};
157    use crate::ordinal::Ordinal;
158
159    fn o(i: u32) -> Ordinal {
160        Ordinal::new(i)
161    }
162
163    #[test]
164    fn rows_are_sorted_and_deduplicated() {
165        let csr = Csr::build(4, [(o(2), o(1)), (o(0), o(3)), (o(0), o(1)), (o(0), o(1))])
166            .expect("builds");
167        assert_eq!(csr.nodes(), 4);
168        assert_eq!(csr.edges(), 3);
169        assert_eq!(csr.neighbours(o(0)), &[1, 3]);
170        assert_eq!(csr.neighbours(o(1)), &[] as &[u32]);
171        assert_eq!(csr.neighbours(o(2)), &[1]);
172        assert_eq!(csr.neighbours(o(9)), &[] as &[u32]);
173        assert_eq!(csr.offsets(), &[0, 2, 2, 3, 3]);
174    }
175
176    #[test]
177    fn transpose_reverses_every_edge() {
178        let csr = Csr::build(3, [(o(0), o(1)), (o(0), o(2)), (o(1), o(2))]).expect("builds");
179        let back = csr.transpose().expect("transposes");
180        assert_eq!(back.neighbours(o(2)), &[0, 1]);
181        assert_eq!(back.neighbours(o(1)), &[0]);
182        assert_eq!(back.neighbours(o(0)), &[] as &[u32]);
183        assert_eq!(back.transpose().expect("transposes"), csr);
184    }
185
186    #[test]
187    fn out_of_range_and_inconsistent_parts_are_refused() {
188        assert_eq!(
189            Csr::build(2, [(o(0), o(2))]),
190            Err(CsrError::OutOfRange {
191                ordinal: o(2),
192                nodes: 2
193            })
194        );
195        assert!(Csr::from_parts(vec![0, 2, 1], vec![1]).is_err());
196        assert!(Csr::from_parts(vec![1, 1], Vec::new()).is_err());
197        assert!(Csr::from_parts(vec![0, 1], vec![0]).is_ok());
198    }
199}