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}