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 neighbours of `node`, sorted; empty for an unknown node.
118    #[must_use]
119    pub fn neighbours(&self, node: Ordinal) -> &[u32] {
120        let start = self.offsets.get(node.as_usize()).copied();
121        let end = self.offsets.get(node.as_usize().saturating_add(1)).copied();
122        match (start, end) {
123            (Some(start), Some(end)) => self
124                .targets
125                .get(to_usize(start)..to_usize(end))
126                .unwrap_or(&[]),
127            _ => &[],
128        }
129    }
130
131    /// The offsets array.
132    #[must_use]
133    pub fn offsets(&self) -> &[u32] {
134        &self.offsets
135    }
136
137    /// The targets array.
138    #[must_use]
139    pub fn targets(&self) -> &[u32] {
140        &self.targets
141    }
142
143    /// The same edges reversed.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`CsrError`] only if this adjacency is itself inconsistent.
148    pub fn transpose(&self) -> Result<Self, CsrError> {
149        let nodes = self.nodes();
150        let reversed = (0..nodes).flat_map(|from| {
151            self.neighbours(Ordinal::new(from))
152                .iter()
153                .map(move |to| (Ordinal::new(*to), Ordinal::new(from)))
154        });
155        Self::build(nodes, reversed)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{Csr, CsrError};
162    use crate::ordinal::Ordinal;
163
164    fn o(i: u32) -> Ordinal {
165        Ordinal::new(i)
166    }
167
168    #[test]
169    fn rows_are_sorted_and_deduplicated() {
170        let csr = Csr::build(4, [(o(2), o(1)), (o(0), o(3)), (o(0), o(1)), (o(0), o(1))])
171            .expect("builds");
172        assert_eq!(csr.nodes(), 4);
173        assert_eq!(csr.edges(), 3);
174        assert_eq!(csr.neighbours(o(0)), &[1, 3]);
175        assert!(csr.neighbours(o(1)).is_empty());
176        assert_eq!(csr.neighbours(o(2)), &[1]);
177        assert!(csr.neighbours(o(9)).is_empty());
178        assert_eq!(csr.offsets(), &[0, 2, 2, 3, 3]);
179    }
180
181    #[test]
182    fn transpose_reverses_every_edge() {
183        let csr = Csr::build(3, [(o(0), o(1)), (o(0), o(2)), (o(1), o(2))]).expect("builds");
184        let back = csr.transpose().expect("transposes");
185        assert_eq!(back.neighbours(o(2)), &[0, 1]);
186        assert_eq!(back.neighbours(o(1)), &[0]);
187        assert!(back.neighbours(o(0)).is_empty());
188        assert_eq!(back.transpose().expect("transposes"), csr);
189    }
190
191    #[test]
192    fn out_of_range_and_inconsistent_parts_are_refused() {
193        assert_eq!(
194            Csr::build(2, [(o(0), o(2))]),
195            Err(CsrError::OutOfRange {
196                ordinal: o(2),
197                nodes: 2
198            })
199        );
200        assert!(Csr::from_parts(vec![0, 2, 1], vec![1]).is_err());
201        assert!(Csr::from_parts(vec![1, 1], Vec::new()).is_err());
202        assert!(Csr::from_parts(vec![0, 1], vec![0]).is_ok());
203    }
204}