1use crate::{q_hash, NQuin};
14
15pub const MAX_CAUSAL_NODES: usize = 256;
17
18const NO_REMOVAL: u64 = u64::MAX;
20
21#[inline]
23pub fn cause_predicate() -> u64 {
24 q_hash("q42:causeOf")
25}
26
27fn caused_internal(edges: &[NQuin], roots: &[u64], target: u64, removed: u64) -> bool {
30 if target == removed {
31 return false;
32 }
33 let p = cause_predicate();
34 let mut frontier = [0u64; MAX_CAUSAL_NODES];
35 let mut visited = [0u64; MAX_CAUSAL_NODES];
36 let mut fl = 0usize;
37 let mut vl = 0usize;
38 for &r in roots {
39 if r == removed {
40 continue;
41 }
42 if r == target {
43 return true;
44 }
45 if fl < MAX_CAUSAL_NODES {
46 frontier[fl] = r;
47 fl += 1;
48 }
49 }
50 while fl > 0 {
51 fl -= 1;
52 let cur = frontier[fl];
53 if visited[..vl].contains(&cur) {
54 continue;
55 }
56 if vl < MAX_CAUSAL_NODES {
57 visited[vl] = cur;
58 vl += 1;
59 } else {
60 break; }
62 for e in edges {
63 if e.predicate == p && e.subject == cur && e.subject != removed && e.object != removed {
64 let nxt = e.object;
65 if nxt == target {
66 return true;
67 }
68 if fl < MAX_CAUSAL_NODES && !visited[..vl].contains(&nxt) {
69 frontier[fl] = nxt;
70 fl += 1;
71 }
72 }
73 }
74 }
75 false
76}
77
78#[inline]
80pub fn caused(edges: &[NQuin], roots: &[u64], effect: u64) -> bool {
81 caused_internal(edges, roots, effect, NO_REMOVAL)
82}
83
84pub fn but_for_cause(edges: &[NQuin], roots: &[u64], cause: u64, effect: u64) -> bool {
88 caused(edges, roots, effect) && !caused_internal(edges, roots, effect, cause)
89}
90
91pub fn is_voided_by(edges: &[NQuin], roots: &[u64], removed: u64, node: u64) -> bool {
95 caused(edges, roots, node) && !caused_internal(edges, roots, node, removed)
96}
97
98pub fn dependents_voided(
101 edges: &[NQuin],
102 roots: &[u64],
103 removed: u64,
104 candidates: &[u64],
105 out: &mut [u64],
106) -> usize {
107 let mut n = 0usize;
108 for &c in candidates {
109 if is_voided_by(edges, roots, removed, c) {
110 if n >= out.len() {
111 break;
112 }
113 out[n] = c;
114 n += 1;
115 }
116 }
117 n
118}
119
120pub fn is_overdetermined(edges: &[NQuin], roots: &[u64], causes: &[u64], effect: u64) -> bool {
124 if causes.len() < 2 || !caused(edges, roots, effect) {
125 return false;
126 }
127 causes
129 .iter()
130 .all(|&c| caused_internal(edges, roots, effect, c))
131}
132
133pub fn do_intervene(
140 edges: &[NQuin],
141 roots: &[u64],
142 set_present: &[u64],
143 set_absent: &[u64],
144 effect: u64,
145) -> bool {
146 if set_absent.contains(&effect) {
147 return false;
148 }
149 let p = cause_predicate();
150 let mut frontier = [0u64; MAX_CAUSAL_NODES];
151 let mut visited = [0u64; MAX_CAUSAL_NODES];
152 let mut fl = 0usize;
153 let mut vl = 0usize;
154 let push = |n: u64, frontier: &mut [u64; MAX_CAUSAL_NODES], fl: &mut usize| {
155 if !set_absent.contains(&n) && *fl < MAX_CAUSAL_NODES {
156 frontier[*fl] = n;
157 *fl += 1;
158 }
159 };
160 for &r in roots {
161 if r == effect {
162 return true;
163 }
164 push(r, &mut frontier, &mut fl);
165 }
166 for &x in set_present {
167 if x == effect {
168 return true;
169 }
170 push(x, &mut frontier, &mut fl);
171 }
172 while fl > 0 {
173 fl -= 1;
174 let cur = frontier[fl];
175 if visited[..vl].contains(&cur) {
176 continue;
177 }
178 if vl < MAX_CAUSAL_NODES {
179 visited[vl] = cur;
180 vl += 1;
181 } else {
182 break;
183 }
184 for e in edges {
185 if e.predicate == p
186 && e.subject == cur
187 && !set_absent.contains(&e.subject)
188 && !set_absent.contains(&e.object)
189 {
190 let nxt = e.object;
191 if nxt == effect {
192 return true;
193 }
194 if fl < MAX_CAUSAL_NODES && !visited[..vl].contains(&nxt) {
195 frontier[fl] = nxt;
196 fl += 1;
197 }
198 }
199 }
200 }
201 false
202}
203
204pub fn is_exogenous(edges: &[NQuin], node: u64) -> bool {
209 let p = cause_predicate();
210 !edges.iter().any(|e| e.predicate == p && e.object == node)
211}
212
213#[inline]
215pub fn is_endogenous(edges: &[NQuin], node: u64) -> bool {
216 !is_exogenous(edges, node)
217}
218
219pub fn counterfactual_absent(
226 edges: &[NQuin],
227 roots: &[u64],
228 intervene: u64,
229 effect: u64,
230) -> (bool, bool) {
231 let factual = caused(edges, roots, effect);
232 let counterfactual = do_intervene(edges, roots, &[], &[intervene], effect);
233 (factual, counterfactual)
234}
235
236pub fn backdoor_satisfied(edges: &[NQuin], x: u64, y: u64, z: &[u64], nodes: &[u64]) -> bool {
243 for &zn in z {
245 if zn != x && caused(edges, &[x], zn) {
246 return false;
247 }
248 }
249 for &c in nodes {
251 if c != x && c != y && caused(edges, &[c], x) && caused(edges, &[c], y) && !z.contains(&c) {
252 return false;
253 }
254 }
255 true
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn edge(cause: u64, effect: u64) -> NQuin {
263 let mut q = NQuin {
264 subject: cause,
265 predicate: cause_predicate(),
266 object: effect,
267 context: 0,
268 metadata: 0,
269 parity: 0,
270 };
271 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
272 q
273 }
274
275 #[test]
276 fn but_for_along_a_chain() {
277 let fund = q_hash("cause:missingFunding");
279 let staff = q_hash("cause:noStaff");
280 let harm = q_hash("harm:serviceFailure");
281 let edges = [edge(fund, staff), edge(staff, harm)];
282 let roots = [fund];
283 assert!(caused(&edges, &roots, harm));
284 assert!(but_for_cause(&edges, &roots, fund, harm));
286 assert!(but_for_cause(&edges, &roots, staff, harm));
287 assert!(!but_for_cause(
289 &edges,
290 &roots,
291 q_hash("cause:weather"),
292 harm
293 ));
294 }
295
296 #[test]
297 fn overdetermination_is_joint_not_but_for() {
298 let c1 = q_hash("cause:fireA");
300 let c2 = q_hash("cause:fireB");
301 let harm = q_hash("harm:houseDestroyed");
302 let edges = [edge(c1, harm), edge(c2, harm)];
303 let roots = [c1, c2];
304 assert!(caused(&edges, &roots, harm));
305 assert!(!but_for_cause(&edges, &roots, c1, harm));
307 assert!(!but_for_cause(&edges, &roots, c2, harm));
308 assert!(is_overdetermined(&edges, &roots, &[c1, c2], harm));
310 }
311
312 #[test]
313 fn root_removal_voids_dependents() {
314 let food = q_hash("support:food");
316 let shelter = q_hash("support:shelter");
317 let health = q_hash("capacity:health");
318 let work = q_hash("capacity:work");
319 let edges = [
320 edge(food, health),
321 edge(shelter, health),
322 edge(health, work),
323 ];
324 let roots = [food, shelter];
325 assert!(!is_voided_by(&edges, &roots, food, work));
327 let edu = q_hash("support:education");
329 let lit = q_hash("capacity:literacy");
330 let edges2 = [edge(edu, lit)];
331 let roots2 = [edu];
332 let mut out = [0u64; 4];
333 let n = dependents_voided(&edges2, &roots2, edu, &[lit], &mut out);
334 assert_eq!(n, 1);
335 assert_eq!(out[0], lit);
336 }
337
338 #[test]
339 fn do_operator_and_scm_classification() {
340 let (smoke, tar, cancer) = (q_hash("v:smoke"), q_hash("v:tar"), q_hash("v:cancer"));
342 let edges = [edge(smoke, tar), edge(tar, cancer)];
343 assert!(do_intervene(&edges, &[], &[smoke], &[], cancer));
345 assert!(!do_intervene(&edges, &[], &[smoke], &[tar], cancer));
347 assert!(is_exogenous(&edges, smoke));
349 assert!(is_endogenous(&edges, tar) && is_endogenous(&edges, cancer));
350 }
351
352 #[test]
353 fn counterfactual_and_backdoor() {
354 let (smoke, tar, cancer) = (q_hash("v:smoke"), q_hash("v:tar"), q_hash("v:cancer"));
355 let edges = [edge(smoke, tar), edge(tar, cancer)];
356 let (factual, cf) = counterfactual_absent(&edges, &[smoke], tar, cancer);
358 assert!(
359 factual && !cf,
360 "tar is counterfactually necessary for cancer"
361 );
362
363 let genes = q_hash("v:genes");
365 let confounded = [edge(genes, smoke), edge(smoke, cancer), edge(genes, cancer)];
366 let nodes = [genes, smoke, cancer];
367 assert!(!backdoor_satisfied(&confounded, smoke, cancer, &[], &nodes));
369 assert!(backdoor_satisfied(
370 &confounded,
371 smoke,
372 cancer,
373 &[genes],
374 &nodes
375 ));
376 assert!(!backdoor_satisfied(
378 &confounded,
379 smoke,
380 cancer,
381 &[genes, cancer],
382 &nodes
383 ));
384 }
385}