Skip to main content

antecedent_graph/
ancestry.rs

1//! Directed ancestry, descendants, and intervention mutilation.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use crate::dag::Dag;
6use crate::error::GraphError;
7use crate::overlay::GraphOverlay;
8use crate::types::DenseNodeId;
9use crate::workspace::{BitSet, GraphWorkspace};
10
11impl Dag {
12    /// Collect all ancestors of `nodes` (including `nodes` themselves) into `out`.
13    pub fn ancestors_of(&self, nodes: &[DenseNodeId], out: &mut BitSet, ws: &mut GraphWorkspace) {
14        self.ancestors_of_with(nodes, out, ws, None);
15    }
16
17    /// Ancestors under an optional edge-visibility overlay.
18    pub(crate) fn ancestors_of_with(
19        &self,
20        nodes: &[DenseNodeId],
21        out: &mut BitSet,
22        ws: &mut GraphWorkspace,
23        overlay: Option<&GraphOverlay>,
24    ) {
25        let n = self.node_count();
26        out.resize(n);
27        out.clear();
28        ws.prepare(n);
29        for &v in nodes {
30            if v.as_usize() >= n {
31                continue;
32            }
33            if !out.contains(v) {
34                out.insert(v);
35                ws.frontier.push(v);
36            }
37        }
38        while let Some(u) = ws.frontier.pop() {
39            for &p in self.parents(u) {
40                if let Some(ov) = overlay {
41                    if !ov.edge_visible(p, u) {
42                        continue;
43                    }
44                }
45                if !out.contains(p) {
46                    out.insert(p);
47                    ws.frontier.push(p);
48                }
49            }
50        }
51    }
52
53    /// Collect all descendants of `nodes` (including `nodes`) into `out`.
54    pub fn descendants_of(&self, nodes: &[DenseNodeId], out: &mut BitSet, ws: &mut GraphWorkspace) {
55        self.descendants_of_with(nodes, out, ws, None);
56    }
57
58    /// Descendants under an optional edge-visibility overlay.
59    pub(crate) fn descendants_of_with(
60        &self,
61        nodes: &[DenseNodeId],
62        out: &mut BitSet,
63        ws: &mut GraphWorkspace,
64        overlay: Option<&GraphOverlay>,
65    ) {
66        let n = self.node_count();
67        out.resize(n);
68        out.clear();
69        ws.prepare(n);
70        for &v in nodes {
71            if v.as_usize() >= n {
72                continue;
73            }
74            if !out.contains(v) {
75                out.insert(v);
76                ws.frontier.push(v);
77            }
78        }
79        while let Some(u) = ws.frontier.pop() {
80            for &c in self.children(u) {
81                if let Some(ov) = overlay {
82                    if !ov.edge_visible(u, c) {
83                        continue;
84                    }
85                }
86                if !out.contains(c) {
87                    out.insert(c);
88                    ws.frontier.push(c);
89                }
90            }
91        }
92    }
93
94    /// Whether `anc` is an ancestor of `desc` (or equal).
95    #[must_use]
96    pub fn is_ancestor(&self, anc: DenseNodeId, desc: DenseNodeId) -> bool {
97        self.reaches(anc, desc)
98    }
99
100    /// Markov blanket of `node`: parents ∪ children ∪ spouses (co-parents of
101    /// children). Does not include `node` itself.
102    ///
103    /// # Errors
104    ///
105    /// Unknown node id.
106    pub fn markov_blanket(&self, node: DenseNodeId, out: &mut BitSet) -> Result<(), GraphError> {
107        self.validate_node_pub(node)?;
108        let n = self.node_count();
109        out.resize(n);
110        out.clear();
111        for &p in self.parents(node) {
112            out.insert(p);
113        }
114        for &c in self.children(node) {
115            out.insert(c);
116            for &spouse in self.parents(c) {
117                if spouse != node {
118                    out.insert(spouse);
119                }
120            }
121        }
122        Ok(())
123    }
124
125    /// Sorted Markov blanket of `node` (excluding `node`).
126    ///
127    /// # Errors
128    ///
129    /// Unknown node id.
130    pub fn markov_blanket_nodes(&self, node: DenseNodeId) -> Result<Vec<DenseNodeId>, GraphError> {
131        let mut bits = BitSet::with_len(self.node_count());
132        self.markov_blanket(node, &mut bits)?;
133        Ok((0..self.node_count())
134            .map(|i| DenseNodeId::from_raw(u32::try_from(i).expect("node fit")))
135            .filter(|&id| bits.contains(id))
136            .collect())
137    }
138
139    /// Mutilate the graph under intervention: remove all edges into each
140    /// intervened node. Returns a new DAG (nodes preserved).
141    ///
142    /// Prefer [`Dag::view`] with [`GraphOverlay::do_intervention`] on hot paths
143    /// to avoid cloning adjacency.
144    ///
145    /// # Errors
146    ///
147    /// Unknown node ids.
148    pub fn mutilate(&self, intervened: &[DenseNodeId]) -> Result<Dag, GraphError> {
149        for &v in intervened {
150            self.validate_node_pub(v)?;
151        }
152        let overlay = GraphOverlay::do_intervention(self.node_count(), intervened);
153        self.view(&overlay).materialize()
154    }
155
156    pub(crate) fn validate_node_pub(&self, id: DenseNodeId) -> Result<(), GraphError> {
157        if id.as_usize() >= self.node_count() {
158            Err(GraphError::UnknownNode { id: id.raw() })
159        } else {
160            Ok(())
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn markov_blanket_includes_parents_children_spouses() {
171        // A → T ← B, T → Y ← C  ⇒  MB(T) = {A, B, Y, C}
172        let mut graph = Dag::with_variables(5);
173        let parent_a = DenseNodeId::from_raw(0);
174        let parent_b = DenseNodeId::from_raw(1);
175        let treatment = DenseNodeId::from_raw(2);
176        let outcome = DenseNodeId::from_raw(3);
177        let spouse_c = DenseNodeId::from_raw(4);
178        graph.insert_directed(parent_a, treatment).unwrap();
179        graph.insert_directed(parent_b, treatment).unwrap();
180        graph.insert_directed(treatment, outcome).unwrap();
181        graph.insert_directed(spouse_c, outcome).unwrap();
182
183        let mb = graph.markov_blanket_nodes(treatment).unwrap();
184        assert_eq!(mb, vec![parent_a, parent_b, outcome, spouse_c]);
185        assert!(!mb.contains(&treatment));
186    }
187
188    #[test]
189    fn markov_blanket_of_root_includes_child_and_spouse() {
190        let mut graph = Dag::with_variables(3);
191        let parent_a = DenseNodeId::from_raw(0);
192        let parent_b = DenseNodeId::from_raw(1);
193        let outcome = DenseNodeId::from_raw(2);
194        graph.insert_directed(parent_a, outcome).unwrap();
195        graph.insert_directed(parent_b, outcome).unwrap();
196        assert_eq!(graph.markov_blanket_nodes(outcome).unwrap(), vec![parent_a, parent_b]);
197        assert_eq!(graph.markov_blanket_nodes(parent_a).unwrap(), vec![parent_b, outcome]);
198    }
199}