1use crate::dag::Dag;
6use crate::dsep::{DSeparationWorkspace, SeparationResult};
7use crate::error::GraphError;
8use crate::types::DenseNodeId;
9use crate::workspace::{BitSet, GraphWorkspace};
10
11#[derive(Clone, Debug)]
16pub struct GraphOverlay {
17 hide_incoming: BitSet,
19 hide_outgoing: BitSet,
21}
22
23impl GraphOverlay {
24 #[must_use]
26 pub fn observational(n: usize) -> Self {
27 Self { hide_incoming: BitSet::with_len(n), hide_outgoing: BitSet::with_len(n) }
28 }
29
30 #[must_use]
32 pub fn do_intervention(n: usize, intervened: &[DenseNodeId]) -> Self {
33 let mut overlay = Self::observational(n);
34 for &v in intervened {
35 if v.as_usize() < n {
36 overlay.hide_incoming.insert(v);
37 }
38 }
39 overlay
40 }
41
42 #[must_use]
44 pub fn remove_outgoing(n: usize, nodes: &[DenseNodeId]) -> Self {
45 let mut overlay = Self::observational(n);
46 for &v in nodes {
47 if v.as_usize() < n {
48 overlay.hide_outgoing.insert(v);
49 }
50 }
51 overlay
52 }
53
54 #[must_use]
56 pub fn is_observational(&self) -> bool {
57 !self.hide_incoming.any() && !self.hide_outgoing.any()
58 }
59
60 #[must_use]
62 pub fn edge_visible(&self, from: DenseNodeId, to: DenseNodeId) -> bool {
63 !self.hide_outgoing.contains(from) && !self.hide_incoming.contains(to)
64 }
65}
66
67#[derive(Clone, Copy, Debug)]
69pub struct DagView<'a> {
70 dag: &'a Dag,
71 overlay: &'a GraphOverlay,
72}
73
74impl<'a> DagView<'a> {
75 #[must_use]
77 pub fn node_count(&self) -> usize {
78 self.dag.node_count()
79 }
80
81 #[must_use]
83 pub fn dag(&self) -> &'a Dag {
84 self.dag
85 }
86
87 #[must_use]
89 pub fn overlay(&self) -> &'a GraphOverlay {
90 self.overlay
91 }
92
93 pub fn parents_into(&self, id: DenseNodeId, out: &mut Vec<DenseNodeId>) {
95 out.clear();
96 if id.as_usize() >= self.dag.node_count() {
97 return;
98 }
99 for &p in self.dag.parents(id) {
100 if self.overlay.edge_visible(p, id) {
101 out.push(p);
102 }
103 }
104 }
105
106 pub fn children_into(&self, id: DenseNodeId, out: &mut Vec<DenseNodeId>) {
108 out.clear();
109 if id.as_usize() >= self.dag.node_count() {
110 return;
111 }
112 for &c in self.dag.children(id) {
113 if self.overlay.edge_visible(id, c) {
114 out.push(c);
115 }
116 }
117 }
118
119 pub fn ancestors_of(&self, nodes: &[DenseNodeId], out: &mut BitSet, ws: &mut GraphWorkspace) {
121 self.dag.ancestors_of_with(nodes, out, ws, Some(self.overlay));
122 }
123
124 pub fn descendants_of(&self, nodes: &[DenseNodeId], out: &mut BitSet, ws: &mut GraphWorkspace) {
126 self.dag.descendants_of_with(nodes, out, ws, Some(self.overlay));
127 }
128
129 pub fn is_d_separated(
135 &self,
136 x: DenseNodeId,
137 y: DenseNodeId,
138 z: &[DenseNodeId],
139 ws: &mut DSeparationWorkspace,
140 ) -> Result<bool, GraphError> {
141 self.dag.is_d_separated_with(x, y, z, ws, Some(self.overlay))
142 }
143
144 pub fn d_separation(
150 &self,
151 x: DenseNodeId,
152 y: DenseNodeId,
153 z: &[DenseNodeId],
154 ws: &mut DSeparationWorkspace,
155 ) -> Result<SeparationResult, GraphError> {
156 self.dag.d_separation_with(x, y, z, ws, Some(self.overlay))
157 }
158
159 pub fn materialize(&self) -> Result<Dag, GraphError> {
165 let n = u32::try_from(self.dag.node_count()).map_err(|_| GraphError::TooManyNodes)?;
166 let mut out = Dag::with_variables(n);
167 for i in 0..self.dag.node_count() {
169 let from = DenseNodeId::from_raw(u32::try_from(i).expect("node fit"));
170 for &to in self.dag.children(from) {
171 if self.overlay.edge_visible(from, to) {
172 out.insert_directed_unchecked(from, to);
173 }
174 }
175 }
176 Ok(out)
177 }
178}
179
180impl Dag {
181 #[must_use]
183 pub fn view<'a>(&'a self, overlay: &'a GraphOverlay) -> DagView<'a> {
184 DagView { dag: self, overlay }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::dsep::DSeparationWorkspace;
192
193 fn chain3() -> Dag {
194 let mut g = Dag::with_variables(3);
195 g.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
196 g.insert_directed(DenseNodeId::from_raw(1), DenseNodeId::from_raw(2)).unwrap();
197 g
198 }
199
200 #[test]
201 fn do_intervention_view_matches_materialize() {
202 let g = chain3();
203 let t = DenseNodeId::from_raw(1);
204 let overlay = GraphOverlay::do_intervention(g.node_count(), &[t]);
205 let view = g.view(&overlay);
206 let mut parents = Vec::new();
207 view.parents_into(t, &mut parents);
208 assert!(parents.is_empty());
209 let mut children = Vec::new();
210 view.children_into(DenseNodeId::from_raw(0), &mut children);
211 assert!(children.is_empty());
212 view.children_into(t, &mut children);
213 assert_eq!(children, vec![DenseNodeId::from_raw(2)]);
214
215 let m = view.materialize().unwrap();
216 assert!(m.parents(t).is_empty());
217 assert!(m.children(DenseNodeId::from_raw(0)).is_empty());
218 assert_eq!(m.children(t).len(), 1);
219 }
220
221 #[test]
222 fn remove_outgoing_hides_treatment_children() {
223 let g = chain3();
224 let t = DenseNodeId::from_raw(1);
225 let overlay = GraphOverlay::remove_outgoing(g.node_count(), &[t]);
226 let view = g.view(&overlay);
227 let mut children = Vec::new();
228 view.children_into(t, &mut children);
229 assert!(children.is_empty());
230 let mut parents = Vec::new();
231 view.parents_into(t, &mut parents);
232 assert_eq!(parents, vec![DenseNodeId::from_raw(0)]);
233 }
234
235 #[test]
236 fn view_dsep_matches_materialized_mutilate() {
237 let mut g = Dag::with_variables(3);
239 let a = DenseNodeId::from_raw(0);
240 let t = DenseNodeId::from_raw(1);
241 let y = DenseNodeId::from_raw(2);
242 g.insert_directed(a, t).unwrap();
243 g.insert_directed(t, y).unwrap();
244 g.insert_directed(a, y).unwrap();
245
246 let overlay = GraphOverlay::do_intervention(g.node_count(), &[t]);
247 let view = g.view(&overlay);
248 let materialized = view.materialize().unwrap();
249 let mut ws = DSeparationWorkspace::default();
250 let view_sep = view.is_d_separated(t, y, &[a], &mut ws).unwrap();
251 let mat_sep = materialized.is_d_separated(t, y, &[a], &mut ws).unwrap();
252 assert_eq!(view_sep, mat_sep);
253 assert!(!view_sep);
255 }
256
257 #[test]
259 fn property_overlay_dsep_matches_mutilate_on_random_dags() {
260 use antecedent_core::CausalRng;
261
262 let mut rng = CausalRng::from_seed(17);
263 let mut ws = DSeparationWorkspace::default();
264 for _ in 0..40 {
265 let node_count = 4 + u32::try_from(rng.next_u64() % 3).unwrap_or(0); let mut graph = Dag::with_variables(node_count);
267 let mut order: Vec<u32> = (0..node_count).collect();
268 let n_usize = usize::try_from(node_count).unwrap_or(0);
269 for i in (1..n_usize).rev() {
270 let bound = u64::try_from(i + 1).unwrap_or(1);
271 let j = usize::try_from(rng.next_u64() % bound).unwrap_or(0);
272 order.swap(i, j);
273 }
274 for i in 0..n_usize {
275 for j in (i + 1)..n_usize {
276 if rng.next_u64() % 3 == 0 {
277 let _ = graph.insert_directed(
278 DenseNodeId::from_raw(order[i]),
279 DenseNodeId::from_raw(order[j]),
280 );
281 }
282 }
283 }
284 let treat_cap = n_usize.clamp(1, 3);
285 let treat_bound = u64::try_from(treat_cap).unwrap_or(1);
286 let n_treated = 1 + usize::try_from(rng.next_u64() % treat_bound).unwrap_or(0);
287 let mut treated = Vec::new();
288 while treated.len() < n_treated {
289 let raw = u32::try_from(rng.next_u64() % u64::from(node_count)).unwrap_or(0);
290 let treatment = DenseNodeId::from_raw(raw);
291 if !treated.contains(&treatment) {
292 treated.push(treatment);
293 }
294 }
295 let overlay = GraphOverlay::do_intervention(graph.node_count(), &treated);
296 let view = graph.view(&overlay);
297 let mutilated = graph.mutilate(&treated).unwrap();
298 let mat = view.materialize().unwrap();
300 for i in 0..node_count {
301 let u = DenseNodeId::from_raw(i);
302 assert_eq!(mat.children(u), mutilated.children(u));
303 }
304 for _ in 0..12 {
306 let x_raw = u32::try_from(rng.next_u64() % u64::from(node_count)).unwrap_or(0);
307 let source = DenseNodeId::from_raw(x_raw);
308 let mut y_raw = u32::try_from(rng.next_u64() % u64::from(node_count)).unwrap_or(0);
309 let mut target = DenseNodeId::from_raw(y_raw);
310 while target == source {
311 y_raw = u32::try_from(rng.next_u64() % u64::from(node_count)).unwrap_or(0);
312 target = DenseNodeId::from_raw(y_raw);
313 }
314 let mut conditioning = Vec::new();
315 for i in 0..node_count {
316 let node = DenseNodeId::from_raw(i);
317 if node == source || node == target {
318 continue;
319 }
320 if rng.next_u64() % 2 == 0 {
321 conditioning.push(node);
322 }
323 }
324 let view_sep = view.is_d_separated(source, target, &conditioning, &mut ws).unwrap();
325 let mut_sep =
326 mutilated.is_d_separated(source, target, &conditioning, &mut ws).unwrap();
327 assert_eq!(
328 view_sep,
329 mut_sep,
330 "overlay≠mutilate d-sep x={} y={} z={:?} T={:?}",
331 source.raw(),
332 target.raw(),
333 conditioning.iter().map(|v| v.raw()).collect::<Vec<_>>(),
334 treated.iter().map(|v| v.raw()).collect::<Vec<_>>()
335 );
336 }
337 }
338 }
339}