1use crate::NQuin;
2
3pub use crate::frame_layout::{COUNTERFACTUAL_BIT, DO_INTERVENTION_BIT, SYNTHESIZED_BIT};
5
6pub fn do_intervention(
17 graph: &[NQuin],
18 intervention_var: u64,
19 intervention_value: u64,
20 target_var: u64,
21) -> Option<f64> {
22 let mut causal_paths = Vec::new();
23 let mut intervened_graph = graph.to_vec();
24
25 intervened_graph.retain(|q| q.object != intervention_var); for quin in &mut intervened_graph {
31 if quin.subject == intervention_var {
32 quin.metadata = DO_INTERVENTION_BIT | (intervention_value << 32);
33 }
34 }
35
36 find_causal_paths(
38 &intervened_graph,
39 intervention_var,
40 target_var,
41 &mut causal_paths,
42 );
43
44 if causal_paths.is_empty() {
45 return None;
46 }
47
48 let total_count = causal_paths.len() as f64;
52 Some(total_count / total_count) }
54
55pub fn counterfactual_query(
57 actual_graph: &[NQuin],
58 factual_outcome: u64,
59 counterfactual_intervention: u64,
60 intervention_value: u64,
61 target_var: u64,
62) -> Option<NQuin> {
63 let mut updated_graph = actual_graph.to_vec();
65 for quin in &mut updated_graph {
66 if quin.subject == target_var {
67 quin.object = factual_outcome;
68 quin.metadata |= COUNTERFACTUAL_BIT;
69 }
70 }
71
72 for quin in &mut updated_graph {
77 if quin.subject == counterfactual_intervention {
78 quin.metadata |= DO_INTERVENTION_BIT | (intervention_value << 32);
79 }
80 }
81
82 if let Some(counterfactual_prob) = do_intervention(
84 &updated_graph,
85 counterfactual_intervention,
86 intervention_value,
87 target_var,
88 ) {
89 let mut result = NQuin::default();
90 result.subject = target_var;
91 result.predicate = crate::q_hash("has_counterfactual_probability");
92 result.object = (counterfactual_prob * 1000.0) as u64; result.metadata = COUNTERFACTUAL_BIT;
94 result.parity = result.subject ^ result.predicate ^ result.object ^ result.context;
95
96 Some(result)
97 } else {
98 None
99 }
100}
101
102fn find_causal_paths(graph: &[NQuin], source: u64, target: u64, paths: &mut Vec<Vec<NQuin>>) {
104 let mut visited = std::collections::HashSet::new();
106 let mut current_path = Vec::new();
107
108 dfs_find_paths(
109 graph,
110 source,
111 target,
112 &mut visited,
113 &mut current_path,
114 paths,
115 );
116}
117
118fn dfs_find_paths(
120 graph: &[NQuin],
121 current: u64,
122 target: u64,
123 visited: &mut std::collections::HashSet<u64>,
124 current_path: &mut Vec<NQuin>,
125 all_paths: &mut Vec<Vec<NQuin>>,
126) {
127 if visited.contains(¤t) {
128 return;
129 }
130
131 visited.insert(current);
132
133 for quin in graph {
135 if quin.subject == current {
136 current_path.push(*quin);
137
138 if quin.object == target {
139 all_paths.push(current_path.clone());
141 } else {
142 dfs_find_paths(graph, quin.object, target, visited, current_path, all_paths);
144 }
145
146 current_path.pop();
147 }
148 }
149
150 visited.remove(¤t);
151}
152
153pub fn are_confounded(graph: &[NQuin], var1: u64, var2: u64) -> bool {
155 let mut parents1 = std::collections::HashSet::new();
157 let mut parents2 = std::collections::HashSet::new();
158
159 for quin in graph {
160 if quin.object == var1 {
161 parents1.insert(quin.subject);
162 }
163 if quin.object == var2 {
164 parents2.insert(quin.subject);
165 }
166 }
167
168 !parents1.is_disjoint(&parents2)
170}
171
172pub fn adjust_for_confounding(
174 graph: &[NQuin],
175 treatment: u64,
176 outcome: u64,
177 confounder: u64,
178) -> Option<f64> {
179 let mut adjusted_prob = 0.0;
183 let mut confounder_values = std::collections::HashSet::new();
184
185 for quin in graph {
187 if quin.subject == confounder {
188 confounder_values.insert(quin.object);
189 }
190 }
191
192 for &confounder_val in &confounder_values {
194 let mut filtered_graph = graph.to_vec();
196 for quin in &mut filtered_graph {
197 if quin.subject == treatment {
198 quin.metadata |= DO_INTERVENTION_BIT;
199 }
200 if quin.subject == confounder {
201 quin.object = confounder_val;
202 }
203 }
204
205 if let Some(p_y_given_x_z) =
206 compute_conditional_probability(&filtered_graph, outcome, treatment)
207 {
208 let p_z = 1.0 / confounder_values.len() as f64;
210 adjusted_prob += p_y_given_x_z * p_z;
211 }
212 }
213
214 if adjusted_prob > 0.0 {
215 Some(adjusted_prob)
216 } else {
217 None
218 }
219}
220
221fn compute_conditional_probability(graph: &[NQuin], y_var: u64, x_var: u64) -> Option<f64> {
228 let x_has_edges = graph.iter().any(|q| q.subject == x_var);
230 if !x_has_edges {
231 return None;
232 }
233
234 let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
236 let mut frontier: Vec<u64> = vec![x_var];
237 while let Some(current) = frontier.pop() {
238 if current == y_var {
239 return Some(1.0);
240 }
241 if visited.insert(current) {
242 for quin in graph {
243 if quin.subject == current && !visited.contains(&quin.object) {
244 frontier.push(quin.object);
245 }
246 }
247 }
248 }
249
250 Some(0.0)
252}
253
254pub fn synthesize_dialectical(thesis: &NQuin, antithesis: &NQuin) -> Option<NQuin> {
255 if thesis.subject == antithesis.subject
257 && thesis.predicate == antithesis.predicate
258 && thesis.object != antithesis.object
259 {
260 let mut synthesized = *thesis;
261 synthesized.context = thesis.context ^ antithesis.context;
262 synthesized.metadata |= SYNTHESIZED_BIT;
263 synthesized.object = thesis.object ^ antithesis.object;
265
266 synthesized.parity =
268 synthesized.subject ^ synthesized.predicate ^ synthesized.object ^ synthesized.context;
269
270 return Some(synthesized);
271 }
272 None
273}
274
275pub const MAX_CAUSAL_NODES: usize = 256;
279
280pub fn reachable_avoiding(graph: &[NQuin], source: u64, target: u64, avoid: u64) -> bool {
286 if source == avoid {
287 return false;
288 }
289 if source == target {
290 return true;
291 }
292 let mut stack = [0u64; MAX_CAUSAL_NODES];
293 let mut slen = 1usize;
294 stack[0] = source;
295 let mut visited = [0u64; MAX_CAUSAL_NODES];
296 let mut vlen = 1usize;
297 visited[0] = source;
298
299 while slen > 0 {
300 slen -= 1;
301 let node = stack[slen];
302 for q in graph {
303 if q.subject != node || q.object == avoid {
304 continue;
305 }
306 if q.object == target {
307 return true;
308 }
309 let mut seen = false;
310 for &v in visited.iter().take(vlen) {
311 if v == q.object {
312 seen = true;
313 break;
314 }
315 }
316 if !seen && vlen < MAX_CAUSAL_NODES && slen < MAX_CAUSAL_NODES {
317 visited[vlen] = q.object;
318 vlen += 1;
319 stack[slen] = q.object;
320 slen += 1;
321 }
322 }
323 }
324 false
325}
326
327pub fn is_necessary_cause(graph: &[NQuin], root: u64, candidate: u64, effect: u64) -> bool {
332 reachable_avoiding(graph, root, effect, u64::MAX)
333 && !reachable_avoiding(graph, root, effect, candidate)
334}
335
336pub fn is_dialectical_contradiction(thesis: &NQuin, antithesis: &NQuin) -> bool {
343 thesis.subject == antithesis.subject
344 && thesis.predicate == antithesis.predicate
345 && thesis.object != antithesis.object
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum IbisNode {
354 Issue,
355 Position,
356 Argument(bool),
358}
359
360#[inline]
363pub fn ibis_position_favoured(supporting: u32, objecting: u32) -> bool {
364 supporting > objecting
365}
366
367pub fn synthesis_coherence(thesis: &NQuin, antithesis: &NQuin, synthesis: &NQuin) -> f32 {
374 let mut score = 0.0f32;
375 if synthesis.subject == thesis.subject && synthesis.subject == antithesis.subject {
376 score += 0.4;
377 }
378 if synthesis.predicate == thesis.predicate && synthesis.predicate == antithesis.predicate {
379 score += 0.3;
380 }
381 if synthesis.object != thesis.object && synthesis.object != antithesis.object {
382 score += 0.3;
383 }
384 score
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 fn edge(cause: u64, effect: u64) -> NQuin {
392 let mut q = NQuin {
393 subject: cause,
394 predicate: crate::q_hash("causal:causes"),
395 object: effect,
396 context: 0,
397 metadata: 0,
398 parity: 0,
399 };
400 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
401 q
402 }
403
404 #[test]
405 fn but_for_causal_necessity() {
406 let chain = [edge(1, 2), edge(2, 3)];
408 assert!(
409 is_necessary_cause(&chain, 1, 2, 3),
410 "C is a necessary cause in a chain"
411 );
412 let diamond = [edge(1, 2), edge(2, 4), edge(1, 3), edge(3, 4)];
414 assert!(
415 !is_necessary_cause(&diamond, 1, 2, 4),
416 "C is not necessary when an alternative path exists"
417 );
418 assert!(
419 reachable_avoiding(&diamond, 1, 4, u64::MAX),
420 "effect is reachable normally"
421 );
422 }
423
424 #[test]
425 fn dialectical_contradiction_ibis_and_coherence() {
426 let mk = |s: u64, p: u64, o: u64| {
427 let mut q = NQuin {
428 subject: s,
429 predicate: p,
430 object: o,
431 context: 0,
432 metadata: 0,
433 parity: 0,
434 };
435 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
436 q
437 };
438 let (subj, pred) = (crate::q_hash("policy:borders"), crate::q_hash("stance"));
439 let thesis = mk(subj, pred, crate::q_hash("open"));
440 let antithesis = mk(subj, pred, crate::q_hash("closed"));
441 assert!(is_dialectical_contradiction(&thesis, &antithesis));
443 let agree = mk(subj, pred, crate::q_hash("open"));
444 assert!(!is_dialectical_contradiction(&thesis, &agree));
445
446 let synthesis = synthesize_dialectical(&thesis, &antithesis).unwrap();
448 assert!((synthesis_coherence(&thesis, &antithesis, &synthesis) - 1.0).abs() < 1e-6);
449 assert!(synthesis_coherence(&thesis, &antithesis, &thesis) < 1.0);
451
452 assert!(ibis_position_favoured(3, 1));
454 assert!(!ibis_position_favoured(1, 1));
455 assert_eq!(IbisNode::Argument(true), IbisNode::Argument(true));
456 }
457
458 #[test]
459 fn test_synthesize_dialectical() {
460 let thesis = NQuin {
461 subject: 1,
462 predicate: 2,
463 object: 3,
464 context: 10,
465 metadata: 0,
466 parity: 0,
467 };
468 let antithesis = NQuin {
469 subject: 1,
470 predicate: 2,
471 object: 4,
472 context: 20,
473 metadata: 0,
474 parity: 0,
475 };
476
477 let syn = synthesize_dialectical(&thesis, &antithesis).unwrap();
478 assert_eq!(syn.context, 10 ^ 20);
479 assert!(syn.metadata & SYNTHESIZED_BIT != 0);
480 }
481
482 #[test]
483 fn test_do_intervention() {
484 let mut graph = Vec::new();
486
487 let mut x_to_y = NQuin::default();
489 x_to_y.subject = 1; x_to_y.predicate = crate::q_hash("causes");
491 x_to_y.object = 2; x_to_y.context = 100;
493 x_to_y.parity = x_to_y.subject ^ x_to_y.predicate ^ x_to_y.object ^ x_to_y.context;
494 graph.push(x_to_y);
495
496 let result = do_intervention(&graph, 1, 1, 2);
498 assert!(result.is_some());
499 assert!(result.unwrap() > 0.0);
500 }
501
502 #[test]
503 fn test_counterfactual_query() {
504 let mut graph = Vec::new();
506
507 let mut treatment_to_outcome = NQuin::default();
508 treatment_to_outcome.subject = 10; treatment_to_outcome.predicate = crate::q_hash("causes");
510 treatment_to_outcome.object = 20; treatment_to_outcome.context = 200;
512 treatment_to_outcome.parity = treatment_to_outcome.subject
513 ^ treatment_to_outcome.predicate
514 ^ treatment_to_outcome.object
515 ^ treatment_to_outcome.context;
516 graph.push(treatment_to_outcome);
517
518 let result = counterfactual_query(&graph, 1, 10, 0, 20);
520 assert!(result.is_some());
521
522 let counterfactual = result.unwrap();
523 assert_eq!(counterfactual.subject, 20); assert!(counterfactual.metadata & COUNTERFACTUAL_BIT != 0);
525 }
526
527 #[test]
528 fn test_confounding_detection() {
529 let mut graph = Vec::new();
531
532 let mut conf_to_treat = NQuin::default();
534 conf_to_treat.subject = 100; conf_to_treat.predicate = crate::q_hash("causes");
536 conf_to_treat.object = 10; conf_to_treat.context = 300;
538 conf_to_treat.parity = conf_to_treat.subject
539 ^ conf_to_treat.predicate
540 ^ conf_to_treat.object
541 ^ conf_to_treat.context;
542 graph.push(conf_to_treat);
543
544 let mut conf_to_outcome = NQuin::default();
546 conf_to_outcome.subject = 100; conf_to_outcome.predicate = crate::q_hash("causes");
548 conf_to_outcome.object = 20; conf_to_outcome.context = 301;
550 conf_to_outcome.parity = conf_to_outcome.subject
551 ^ conf_to_outcome.predicate
552 ^ conf_to_outcome.object
553 ^ conf_to_outcome.context;
554 graph.push(conf_to_outcome);
555
556 let confounded = are_confounded(&graph, 10, 20);
558 assert!(confounded);
559 }
560
561 #[test]
562 fn test_adjust_for_confounding() {
563 let mut graph = Vec::new();
565
566 let mut conf_to_treat = NQuin::default();
568 conf_to_treat.subject = 100; conf_to_treat.predicate = crate::q_hash("causes");
570 conf_to_treat.object = 10; conf_to_treat.context = 400;
572 conf_to_treat.parity = conf_to_treat.subject
573 ^ conf_to_treat.predicate
574 ^ conf_to_treat.object
575 ^ conf_to_treat.context;
576 graph.push(conf_to_treat);
577
578 let mut treat_to_outcome = NQuin::default();
580 treat_to_outcome.subject = 10; treat_to_outcome.predicate = crate::q_hash("causes");
582 treat_to_outcome.object = 20; treat_to_outcome.context = 401;
584 treat_to_outcome.parity = treat_to_outcome.subject
585 ^ treat_to_outcome.predicate
586 ^ treat_to_outcome.object
587 ^ treat_to_outcome.context;
588 graph.push(treat_to_outcome);
589
590 let adjusted = adjust_for_confounding(&graph, 10, 20, 100);
592 assert!(adjusted.is_some());
593 assert!(adjusted.unwrap() >= 0.0);
594 }
595
596 #[test]
597 fn test_no_contradiction() {
598 let thesis = NQuin {
599 subject: 1,
600 predicate: 2,
601 object: 3,
602 context: 10,
603 metadata: 0,
604 parity: 0,
605 };
606
607 let no_contradiction = NQuin {
608 subject: 1,
609 predicate: 3, object: 4,
611 context: 20,
612 metadata: 0,
613 parity: 0,
614 };
615
616 assert!(synthesize_dialectical(&thesis, &no_contradiction).is_none());
617 }
618}