Skip to main content

causal_hub/inference/
graphical_separation.rs

1use std::collections::VecDeque;
2
3use crate::{
4    models::{DiGraph, Graph},
5    set,
6    types::{Error, Result, Set},
7};
8
9/// A trait for graphical separation.
10pub trait GraphicalSeparation {
11    /// Checks if the `Z` is a separator set for `X` and `Y`.
12    ///
13    /// # Arguments
14    ///
15    /// * `x` - A set of vertices representing set `X`.
16    /// * `y` - A set of vertices representing set `Y`.
17    /// * `z` - A set of vertices representing set `Z`.
18    ///
19    /// # Errors
20    ///
21    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, or `Z` are out of bounds.
22    /// * `SetsNotDisjoint` if `X`, `Y` or `Z` are not disjoint sets.
23    /// * `EmptySet` if `X` or `Y` are empty sets.
24    ///
25    /// # Returns
26    ///
27    /// `true` if `X` and `Y` are separated by `Z`, `false` otherwise.
28    ///
29    fn is_separator_set(&self, x: &Set<usize>, y: &Set<usize>, z: &Set<usize>) -> Result<bool>;
30
31    /// Checks if the `Z` is a minimal separator set for `X` and `Y`.
32    ///
33    /// # Arguments
34    ///
35    /// * `x` - A set of vertices representing set `X`.
36    /// * `y` - A set of vertices representing set `Y`.
37    /// * `z` - A set of vertices representing set `Z`.
38    /// * `w` - An optional iterable collection of vertices representing set `W`.
39    /// * `v` - An optional iterable collection of vertices representing set `V`.
40    ///
41    /// # Errors
42    ///
43    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, `Z`, `W`, or `V` are out of bounds.
44    /// * `SetsNotDisjoint` if `X`, `Y` or `Z` are not disjoint sets.
45    /// * `EmptySet` if `X` or `Y` are empty sets.
46    /// * `SubsetMismatch` if not `W` <= `Z` <= `V`.
47    ///
48    /// # Returns
49    ///
50    /// `true` if `Z` is a minimal separator set for `X` and `Y`, `false` otherwise.
51    ///
52    fn is_minimal_separator_set(
53        &self,
54        x: &Set<usize>,
55        y: &Set<usize>,
56        z: &Set<usize>,
57        w: Option<&Set<usize>>,
58        v: Option<&Set<usize>>,
59    ) -> Result<bool>;
60
61    /// Finds a minimal separator set for the vertex sets `X` and `Y`, if any.
62    ///
63    /// # Arguments
64    ///
65    /// * `x` - A set of vertices representing set `X`.
66    /// * `y` - A set of vertices representing set `Y`.
67    /// * `w` - An optional iterable collection of vertices representing set `W`.
68    /// * `v` - An optional iterable collection of vertices representing set `V`.
69    ///
70    /// # Errors
71    ///
72    /// * `IndexOutOfBounds` if any of the vertex in `X`, `Y`, `W`, or `V` are out of bounds.
73    /// * `SetsNotDisjoint` if `X` and `Y` are not disjoint sets.
74    /// * `EmptySet` if `X` or `Y` are empty sets.
75    /// * `SubsetMismatch` if not `W` <= `V`.
76    ///
77    /// # Returns
78    ///
79    /// `Some(Set)` containing the minimal separator set, or `None` if no separator set exists.
80    ///
81    fn find_minimal_separator_set(
82        &self,
83        x: &Set<usize>,
84        y: &Set<usize>,
85        w: Option<&Set<usize>>,
86        v: Option<&Set<usize>>,
87    ) -> Result<Option<Set<usize>>>;
88}
89
90// Implementation of the `GraphicalSeparation` trait for directed graphs.
91pub(crate) mod digraph {
92    use super::*;
93    use crate::inference::TopologicalOrder;
94
95    /// Checks the validity of the sets and returns them as `Set<usize>`.
96    pub(crate) fn _assert(
97        graph: &DiGraph,
98        x: &Set<usize>,
99        y: &Set<usize>,
100        z: Option<&Set<usize>>,
101        w: Option<&Set<usize>>,
102        v: Option<&Set<usize>>,
103    ) -> Result<()> {
104        // Check the included set is a subset of the restricted set.
105        if let (Some(w), Some(v)) = (w.as_ref(), v.as_ref())
106            && !w.is_subset(v)
107        {
108            return Err(Error::SubsetMismatch("W", "V"));
109        }
110
111        // Convert X to set, while checking for out of bounds.
112        x.iter().try_for_each(|&x| {
113            if !graph.has_vertex(x) {
114                return Err(Error::IndexOutOfBounds(x));
115            }
116            Ok(())
117        })?;
118        // Convert Y to set, while checking for out of bounds.
119        y.iter().try_for_each(|&y| {
120            if !graph.has_vertex(y) {
121                return Err(Error::IndexOutOfBounds(y));
122            }
123            Ok(())
124        })?;
125        // Convert Z to set, while checking for out of bounds.
126        if let Some(z) = z {
127            z.iter().try_for_each(|&z| {
128                if !graph.has_vertex(z) {
129                    return Err(Error::IndexOutOfBounds(z));
130                }
131                Ok(())
132            })?;
133        }
134
135        // Check X is non-empty.
136        if x.is_empty() {
137            return Err(Error::EmptySet("X"));
138        }
139        // Check Y is non-empty.
140        if y.is_empty() {
141            return Err(Error::EmptySet("Y"));
142        }
143
144        // Check X and Y are disjoint.
145        if !x.is_disjoint(y) {
146            return Err(Error::SetsNotDisjoint("X", "Y"));
147        }
148
149        // If Z is provided, convert it to a set.
150        if let Some(z) = &z {
151            // Check X and Z are disjoint.
152            if !x.is_disjoint(z) {
153                return Err(Error::SetsNotDisjoint("X", "Z"));
154            }
155            // Check Y and Z are disjoint.
156            if !y.is_disjoint(z) {
157                return Err(Error::SetsNotDisjoint("Y", "Z"));
158            }
159            // Check Z includes.
160            if let Some(w) = w
161                && !z.is_superset(w)
162            {
163                return Err(Error::SubsetMismatch("W", "Z"));
164            }
165            // Check Z is restricted.
166            if let Some(v) = v
167                && !z.is_subset(v)
168            {
169                return Err(Error::SubsetMismatch("Z", "V"));
170            }
171        }
172        Ok(())
173    }
174
175    fn _reachable(
176        graph: &DiGraph,
177        x: &Set<usize>,
178        an_x: &Set<usize>,
179        z: &Set<usize>,
180    ) -> Result<Set<usize>> {
181        // Check the graph is a DAG.
182        if graph.topological_order().is_none() {
183            return Err(Error::NotADag());
184        }
185
186        // Check if the ball passes or not.
187        let _pass = |evidence: bool, v: usize, f: bool, n: usize| {
188            let is_element_of_a = an_x.contains(&n);
189            let almost_definite_status = true; // NOTE: Always true for DAGs, not so for RCGs.
190            let collider_if_in_z = !z.contains(&v) || (evidence && !f);
191            // If the edge is forward, the vertex must be an ancestor or in Z.
192            is_element_of_a && collider_if_in_z && almost_definite_status
193        };
194
195        // Initialize the queue.
196        let mut queue: VecDeque<(bool, usize)> = Default::default();
197        // For each vertex in X, add backward/forward edges to the queue.
198        for &w in x.iter() {
199            // If the vertex has predecessors, add it to the queue as a backward edge.
200            if !graph.parents(&set![w])?.is_empty() {
201                queue.push_back((false, w));
202            }
203            // If the vertex has successors, add it to the queue as a forward edge.
204            if !graph.children(&set![w])?.is_empty() {
205                queue.push_back((true, w));
206            }
207        }
208
209        // Initialize the processed set with the queue.
210        let mut visited = queue.clone();
211
212        // For each element in the queue ...
213        while let Some((evidence, v)) = queue.pop_front() {
214            // Get the predecessors and successors of the vertex.
215            let pa_v = graph.parents(&set![v])?.into_iter().map(|n| (false, n));
216            let ch_v = graph.children(&set![v])?.into_iter().map(|n| (true, n));
217
218            // Create pairs of (forward, vertex) for predecessors and successors.
219            // Filter and add unvisited pairs that pass the condition.
220            for (f, n) in pa_v.chain(ch_v) {
221                if !visited.contains(&(f, n)) && _pass(evidence, v, f, n) {
222                    // Add it to the queue and mark it as processed.
223                    queue.push_back((f, n));
224                    visited.push_back((f, n));
225                }
226            }
227        }
228
229        // Return the set of visited vertices.
230        Ok(visited.into_iter().map(|(_, w)| w).collect())
231    }
232
233    impl GraphicalSeparation for DiGraph {
234        fn is_separator_set(&self, x: &Set<usize>, y: &Set<usize>, z: &Set<usize>) -> Result<bool> {
235            // Perform sanity checks and convert sets.
236            _assert(self, x, y, Some(z), None::<&Set<_>>, None::<&Set<_>>)?;
237
238            // Initialize the forward and backward deques and visited sets.
239
240            // Contains -> and <-> edges from starting vertex.
241            let mut forward_deque: VecDeque<usize> = Default::default();
242            let mut forward_visited: Set<usize> = set![];
243            // Contains <- and - edges from starting vertex.
244            let mut backward_deque: VecDeque<usize> = Default::default();
245            let mut backward_visited: Set<usize> = set![];
246
247            // Initialize the backward deque with the vertices in X.
248            backward_deque.extend(x.iter().cloned());
249
250            // Compute the ancestors of X and Z.
251            let ancestors_or_z = &self.ancestors(z)? | &(z | x);
252
253            // While there are vertices to visit in the forward or backward deques ...
254            while !forward_deque.is_empty() || !backward_deque.is_empty() {
255                // If there are vertices in the backward deque ...
256                if let Some(w) = backward_deque.pop_front() {
257                    // Mark the W as visited.
258                    backward_visited.insert(w);
259                    // If the W is in Y, return false (not separated).
260                    if y.contains(&w) {
261                        return Ok(false);
262                    }
263                    // If the W is in Z, continue to the next iteration.
264                    if z.contains(&w) {
265                        continue;
266                    }
267                    // Add all predecessors of the W to the backward deque.
268                    self.parents(&set![w])?
269                        .into_iter()
270                        .filter(|pred| !backward_visited.contains(pred))
271                        .for_each(|pred| backward_deque.push_back(pred));
272                    // Add all successors of the W to the forward deque.
273                    self.children(&set![w])?
274                        .into_iter()
275                        .filter(|succ| !forward_visited.contains(succ))
276                        .for_each(|succ| forward_deque.push_back(succ));
277                }
278
279                // If there are vertices in the forward deque ...
280                if let Some(w) = forward_deque.pop_front() {
281                    // Mark the W as visited.
282                    forward_visited.insert(w);
283                    // If the W is in Y, return false (not separated).
284                    if y.contains(&w) {
285                        return Ok(false);
286                    }
287                    // If the W is an ancestor or in Z, add its predecessors to the backward deque.
288                    if ancestors_or_z.contains(&w) {
289                        self.parents(&set![w])?
290                            .into_iter()
291                            .filter(|pred| !backward_visited.contains(pred))
292                            .for_each(|pred| backward_deque.push_back(pred));
293                    }
294                    // If the W is not in Z, add its successors to the forward deque.
295                    if !z.contains(&w) {
296                        self.children(&set![w])?
297                            .into_iter()
298                            .filter(|succ| !forward_visited.contains(succ))
299                            .for_each(|succ| forward_deque.push_back(succ));
300                    }
301                }
302            }
303
304            // Otherwise, return true.
305            Ok(true)
306        }
307
308        fn is_minimal_separator_set(
309            &self,
310            x: &Set<usize>,
311            y: &Set<usize>,
312            z: &Set<usize>,
313            w: Option<&Set<usize>>,
314            v: Option<&Set<usize>>,
315        ) -> Result<bool> {
316            // Perform sanity checks and convert sets.
317            _assert(self, x, y, Some(z), w, v)?;
318
319            // Set default values for W if not provided.
320            let w = match w {
321                Some(w) => w,
322                None => &set![],
323            };
324
325            // Compute the ancestors of X and Y.
326            let x_y_w = &(x | y) | w;
327            let an_x_y_w = &self.ancestors(&x_y_w)? | &x_y_w;
328
329            // a) Check that Z is a separator.
330            let x_closure = _reachable(self, x, &an_x_y_w, z)?;
331            if !x_closure.is_disjoint(y) {
332                return Ok(false);
333            }
334
335            // b) Check that Z is constrained to An(X, Y).
336            if !z.is_subset(&an_x_y_w) {
337                return Ok(false);
338            }
339
340            // c) Check that Z is minimal.
341            let y_closure = _reachable(self, y, &an_x_y_w, z)?;
342            if !((z - w).is_subset(&(&x_closure & &y_closure))) {
343                return Ok(false);
344            }
345
346            // Otherwise, return true.
347            Ok(true)
348        }
349
350        fn find_minimal_separator_set(
351            &self,
352            x: &Set<usize>,
353            y: &Set<usize>,
354            w: Option<&Set<usize>>,
355            v: Option<&Set<usize>>,
356        ) -> Result<Option<Set<usize>>> {
357            // Perform sanity checks and convert sets.
358            _assert(self, x, y, None::<&Set<_>>, w, v)?;
359
360            // Set default values for W and V if not provided.
361            let w = match w {
362                Some(w) => w,
363                None => &set![],
364            };
365            let v = match v {
366                Some(v) => v,
367                None => &self.vertices(),
368            };
369
370            // Compute the ancestors of X and Y.
371            let x_y_w = &(x | y) | w;
372            let an_x_y_w = &self.ancestors(&x_y_w)? | &x_y_w;
373
374            // Initialize the restricted set with the intersection of X, Y, and included.
375            let z = v & &(&an_x_y_w - &(x | y));
376
377            // Check if Z is a separator.
378            let x_closure = _reachable(self, x, &an_x_y_w, &z)?;
379            if !x_closure.is_disjoint(y) {
380                return Ok(None); // No minimal separator exists.
381            }
382
383            // Update Z.
384            let z = &z & &(&x_closure | w);
385
386            // Check if Z is a separator.
387            let y_closure = _reachable(self, y, &an_x_y_w, &z)?;
388
389            // Return the minimal separator.
390            Ok(Some(&z & &(&y_closure | w)))
391        }
392    }
393}