Skip to main content

concept_graph/
csr.rs

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