Skip to main content

concept_graph/
closure.rs

1//! The transitive closure of the is-a hierarchy.
2//!
3//! Computed once, offline, by a bitset sweep in topological order: a node's
4//! ancestor set is the union of its parents' ancestor sets plus the parents
5//! themselves; descendants are the same sweep over the transposed adjacency.
6//! A cycle in the is-a edges is a defect in the input and is refused.
7//! Roaring bitmaps keep the sets small and iterate in sorted order, which
8//! keeps every consumer deterministic.
9
10use roaring::RoaringBitmap;
11
12use crate::csr::{Csr, CsrError};
13use crate::ordinal::{Ordinal, to_usize};
14
15/// A failure while computing the closure.
16#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17pub enum ClosureError {
18    /// The is-a edges contain a cycle through these ordinals.
19    #[error("the is-a hierarchy has a cycle through {} node(s), for example {first}", .members.len())]
20    Cycle {
21        /// The first node found on a cycle.
22        first: Ordinal,
23        /// Every node not reachable from the roots by a topological sweep.
24        members: Vec<Ordinal>,
25    },
26    /// The adjacency is inconsistent.
27    #[error(transparent)]
28    Csr(#[from] CsrError),
29}
30
31/// The ancestor and descendant sets of every node.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Closure {
34    ancestors: Vec<RoaringBitmap>,
35    descendants: Vec<RoaringBitmap>,
36}
37
38static EMPTY: std::sync::LazyLock<RoaringBitmap> = std::sync::LazyLock::new(RoaringBitmap::new);
39
40impl Closure {
41    /// Computes the closure of `is_a`, whose edges point from child to parent.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`ClosureError::Cycle`] when the edges are not acyclic.
46    pub fn compute(is_a: &Csr) -> Result<Self, ClosureError> {
47        let parents = is_a;
48        let children = is_a.transpose()?;
49        let order = topological_order(parents, &children)?;
50        let nodes = to_usize(parents.nodes());
51        let mut ancestors: Vec<RoaringBitmap> = vec![RoaringBitmap::new(); nodes];
52        for node in &order {
53            let mut set = RoaringBitmap::new();
54            for parent in parents.neighbours(*node) {
55                set.insert(*parent);
56                if let Some(above) = ancestors.get(to_usize(*parent)) {
57                    set |= above;
58                }
59            }
60            if let Some(slot) = ancestors.get_mut(node.as_usize()) {
61                *slot = set;
62            }
63        }
64        let mut descendants: Vec<RoaringBitmap> = vec![RoaringBitmap::new(); nodes];
65        for node in order.iter().rev() {
66            let mut set = RoaringBitmap::new();
67            for child in children.neighbours(*node) {
68                set.insert(*child);
69                if let Some(below) = descendants.get(to_usize(*child)) {
70                    set |= below;
71                }
72            }
73            if let Some(slot) = descendants.get_mut(node.as_usize()) {
74                *slot = set;
75            }
76        }
77        Ok(Self {
78            ancestors,
79            descendants,
80        })
81    }
82
83    /// Reassembles a closure from its two bitmap lists.
84    #[must_use]
85    pub fn from_parts(ancestors: Vec<RoaringBitmap>, descendants: Vec<RoaringBitmap>) -> Self {
86        Self {
87            ancestors,
88            descendants,
89        }
90    }
91
92    /// The number of nodes.
93    #[must_use]
94    pub fn nodes(&self) -> u32 {
95        u32::try_from(self.ancestors.len()).unwrap_or(u32::MAX)
96    }
97
98    /// The proper ancestors of `node` (ECL `>`); empty for an unknown node.
99    #[must_use]
100    pub fn ancestors(&self, node: Ordinal) -> &RoaringBitmap {
101        self.ancestors
102            .get(node.as_usize())
103            .unwrap_or_else(|| &*EMPTY)
104    }
105
106    /// The proper descendants of `node` (ECL `<`); empty for an unknown node.
107    #[must_use]
108    pub fn descendants(&self, node: Ordinal) -> &RoaringBitmap {
109        self.descendants
110            .get(node.as_usize())
111            .unwrap_or_else(|| &*EMPTY)
112    }
113
114    /// `node` and its descendants (ECL `<<`).
115    #[must_use]
116    pub fn descendants_or_self(&self, node: Ordinal) -> RoaringBitmap {
117        let mut set = self.descendants(node).clone();
118        set.insert(node.index());
119        set
120    }
121
122    /// `node` and its ancestors (ECL `>>`).
123    #[must_use]
124    pub fn ancestors_or_self(&self, node: Ordinal) -> RoaringBitmap {
125        let mut set = self.ancestors(node).clone();
126        set.insert(node.index());
127        set
128    }
129
130    /// Whether `ancestor` is a proper ancestor of `node`.
131    #[must_use]
132    pub fn is_ancestor(&self, ancestor: Ordinal, node: Ordinal) -> bool {
133        self.ancestors(node).contains(ancestor.index())
134    }
135
136    /// The ancestor bitmaps, in ordinal order.
137    #[must_use]
138    pub fn ancestor_sets(&self) -> &[RoaringBitmap] {
139        &self.ancestors
140    }
141
142    /// The descendant bitmaps, in ordinal order.
143    #[must_use]
144    pub fn descendant_sets(&self) -> &[RoaringBitmap] {
145        &self.descendants
146    }
147}
148
149/// Kahn's algorithm over child-to-parent edges: parents before children.
150fn topological_order(parents: &Csr, children: &Csr) -> Result<Vec<Ordinal>, ClosureError> {
151    let nodes = parents.nodes();
152    let mut remaining: Vec<u32> = (0..nodes)
153        .map(|n| u32::try_from(parents.neighbours(Ordinal::new(n)).len()).unwrap_or(u32::MAX))
154        .collect();
155    let mut ready: Vec<Ordinal> = (0..nodes)
156        .filter(|n| remaining.get(to_usize(*n)) == Some(&0))
157        .map(Ordinal::new)
158        .collect();
159    let mut order = Vec::with_capacity(to_usize(nodes));
160    while let Some(node) = ready.pop() {
161        order.push(node);
162        for child in children.neighbours(node) {
163            if let Some(count) = remaining.get_mut(to_usize(*child)) {
164                *count = count.saturating_sub(1);
165                if *count == 0 {
166                    ready.push(Ordinal::new(*child));
167                }
168            }
169        }
170    }
171    if order.len() != to_usize(nodes) {
172        let members: Vec<Ordinal> = (0..nodes)
173            .filter(|n| remaining.get(to_usize(*n)).is_some_and(|c| *c > 0))
174            .map(Ordinal::new)
175            .collect();
176        let first = members.first().copied().unwrap_or(Ordinal::new(0));
177        return Err(ClosureError::Cycle { first, members });
178    }
179    Ok(order)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::{Closure, ClosureError};
185    use crate::csr::Csr;
186    use crate::ordinal::Ordinal;
187
188    fn o(i: u32) -> Ordinal {
189        Ordinal::new(i)
190    }
191
192    /// 0 is the root; 1 and 2 are its children; 3 is a child of both 1 and 2.
193    fn diamond() -> Closure {
194        let is_a = Csr::build(4, [(o(1), o(0)), (o(2), o(0)), (o(3), o(1)), (o(3), o(2))])
195            .expect("builds");
196        Closure::compute(&is_a).expect("acyclic")
197    }
198
199    #[test]
200    fn ancestors_and_descendants_are_transitive_and_inverse() {
201        let closure = diamond();
202        assert_eq!(
203            closure.ancestors(o(3)).iter().collect::<Vec<_>>(),
204            vec![0, 1, 2]
205        );
206        assert_eq!(
207            closure.descendants(o(0)).iter().collect::<Vec<_>>(),
208            vec![1, 2, 3]
209        );
210        assert_eq!(
211            closure.descendants(o(1)).iter().collect::<Vec<_>>(),
212            vec![3]
213        );
214        assert!(closure.ancestors(o(0)).is_empty());
215        assert!(closure.descendants(o(3)).is_empty());
216        assert!(closure.is_ancestor(o(0), o(3)));
217        assert!(!closure.is_ancestor(o(3), o(0)));
218        assert!(closure.descendants_or_self(o(3)).contains(3));
219        assert_eq!(closure.ancestors_or_self(o(3)).len(), 4);
220    }
221
222    #[test]
223    fn a_cycle_is_refused() {
224        let is_a = Csr::build(3, [(o(0), o(1)), (o(1), o(2)), (o(2), o(0))]).expect("builds");
225        match Closure::compute(&is_a) {
226            Err(ClosureError::Cycle { members, .. }) => assert_eq!(members.len(), 3),
227            other => panic!("expected a cycle, got {other:?}"),
228        }
229    }
230
231    #[test]
232    fn unknown_nodes_have_empty_sets() {
233        let closure = diamond();
234        assert!(closure.ancestors(o(99)).is_empty());
235        assert!(closure.descendants(o(99)).is_empty());
236    }
237}