Skip to content

Commit 75656c0

Browse files
committed
Added SCM example
Signed-off-by: Marvin Hansen <[email protected]>
1 parent 5938b5e commit 75656c0

File tree

8 files changed

+331
-6
lines changed

8 files changed

+331
-6
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/epp_rcm/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
This example demonstrates how to implement a simple Rubin Causal Model (RCM) scenario using the `DeepCausality` library. It showcases the Effect Propagation Process (EPP)'s capability for **Contextual Alternation** to directly compute potential outcomes and determine an individual treatment effect.
44

5+
## How to Run
6+
7+
To run this example, navigate to the root of the `deep_causality` project and execute:
8+
9+
```bash
10+
cargo run -p example-rcm
11+
```
12+
513
## Background: The Rubin Causal Model and EPP
614

715
The RCM defines a causal effect by comparing two "potential outcomes" for a single unit: the outcome if the unit receives a treatment (Y(1)) versus the outcome if it does not (Y(0)). The fundamental challenge is that only one of these outcomes can be observed in reality.
@@ -42,13 +50,7 @@ By evaluating the *same causal model* against these different contexts, `DeepCau
4250
* The Individual Treatment Effect (ITE) is calculated as `Y(1) - Y(0)`.
4351
* The result is printed, demonstrating the drug's predicted effect on the patient's blood pressure.
4452

45-
## How to Run
46-
47-
To run this example, navigate to the root of the `deep_causality` project and execute:
4853

49-
```bash
50-
cargo run -p example-rcm
51-
```
5254

5355
## Reference
5456

examples/epp_scm/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
3+
4+
[package]
5+
name = "example-scm"
6+
version = "0.1.0"
7+
edition = "2021"
8+
rust-version = "1.80"
9+
publish = false
10+
11+
[dependencies]
12+
deep_causality = { path = "../../deep_causality" }

examples/epp_scm/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# EPP Example: Pearl's Ladder of Causation
2+
3+
This crate demonstrates how the `DeepCausality` library, which implements the Effect Propagation Process (EPP), models the three rungs of Judea Pearl's Ladder of Causation. Each rung represents a different level of causal reasoning, and this example shows how the EPP's unique architecture addresses each one.
4+
5+
The examples are separated into three files, each corresponding to a rung on the ladder:
6+
7+
- `rung1_association.rs`
8+
- `rung2_intervention.rs`
9+
- `rung3_counterfactual.rs`
10+
11+
## How to Run
12+
13+
From within the `examples/epp_scm` directory, run:
14+
15+
```bash
16+
cargo run
17+
```
18+
19+
---
20+
21+
### Rung 1: Association (Seeing)
22+
23+
- **File:** `rung1_association.rs`
24+
- **Goal:** Demonstrates simple observational inference. It answers the question: "Given that we observe X, what is the likelihood of Y?" (i.e., `P(Y|X)`).
25+
26+
#### EPP Implementation
27+
28+
Association is modeled as a straightforward evaluation of a `CausaloidGraph`. The graph represents the assumed causal chain (`Smoking -> Tar -> Cancer`). We provide an initial `PropagatingEffect` representing the observation (e.g., high nicotine levels), and the graph's evaluation propagates this effect to determine the associated outcome (cancer risk).
29+
30+
This aligns with **Rung 1** by showing how the system processes passive observations to find statistical associations within the model.
31+
32+
### Rung 2: Intervention (Doing)
33+
34+
- **File:** `rung2_intervention.rs`
35+
- **Goal:** Demonstrates taking an action based on an observation. It answers the question: "What would Y be if we *do* X?" (i.e., `P(Y|do(X))`).
36+
37+
#### EPP Implementation
38+
39+
Intervention is modeled using the **Causal State Machine (CSM)**.
40+
41+
1. A `CausalState` is defined, with its condition for activation being the result of the causal graph (e.g., "High Cancer Risk" is true).
42+
2. A `CausalAction` is defined, which represents the real-world intervention (e.g., prescribing therapy).
43+
3. The CSM links the state to the action. When the CSM is evaluated with an effect that makes the state true, it automatically fires the action.
44+
45+
This aligns with **Rung 2** by providing a formal mechanism to move from inference to a deterministic, real-world action.
46+
47+
### Rung 3: Counterfactuals (Imagining)
48+
49+
- **File:** `rung3_counterfactual.rs`
50+
- **Goal:** Demonstrates reasoning about alternate possibilities. It answers the retrospective question: "What would Y have been, had X been different?"
51+
52+
#### EPP Implementation
53+
54+
The EPP models counterfactuals not by surgically altering the causal model itself, but through **Contextual Alternation**.
55+
56+
1. A **Factual Context** is created to represent the observed reality (e.g., a person who smokes and has high tar).
57+
2. A **Counterfactual Context** is created by cloning the factual one and then modifying a specific past condition (e.g., setting the smoking level to low, but leaving the tar level high).
58+
3. The *exact same* `Causaloid` (representing the causal laws) is evaluated against both contexts.
59+
60+
This example shows that even if we imagine the person had not smoked, their cancer risk remains high because the direct consequence (tar) is still present in the counterfactual world. This demonstrates the EPP's powerful ability to reason about alternative realities by separating causal logic from the context it operates on.
61+
62+
## Reference
63+
64+
For more information on the EPP, please see chapter 5 in the EPP document:
65+
https://github.com/deepcausality-rs/papers/blob/main/effect_propagation_process/epp.pdf

examples/epp_scm/src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
mod rung1_association;
7+
mod rung2_intervention;
8+
mod rung3_counterfactual;
9+
10+
fn main() {
11+
rung1_association::run_rung1_association();
12+
rung2_intervention::run_rung2_intervention();
13+
rung3_counterfactual::run_rung3_counterfactual();
14+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
use deep_causality::*;
7+
8+
// Define Causaloids
9+
fn get_smoking_causaloid() -> BaseCausaloid {
10+
Causaloid::new(1, |effect| {
11+
let nicotine_level = effect.as_numerical().unwrap_or(0.0);
12+
Ok(PropagatingEffect::Deterministic(nicotine_level > 0.6))
13+
}, "Smoking Status")
14+
}
15+
16+
fn get_tar_causaloid() -> BaseCausaloid {
17+
Causaloid::new(2, |effect| {
18+
// Tar is present if the preceding cause (smoking) is active.
19+
let is_smoking = effect.as_bool().unwrap_or(false);
20+
Ok(PropagatingEffect::Deterministic(is_smoking))
21+
}, "Tar in Lungs")
22+
}
23+
24+
fn get_cancer_risk_causaloid() -> BaseCausaloid {
25+
Causaloid::new(3, |effect| {
26+
// Cancer risk is high if tar is present.
27+
let has_tar = effect.as_bool().unwrap_or(false);
28+
Ok(PropagatingEffect::Deterministic(has_tar))
29+
}, "Cancer Risk")
30+
}
31+
32+
pub fn run_rung1_association() {
33+
println!("--- Rung 1: Association ---");
34+
println!("Demonstrating observational inference: Given smoking, what is the cancer risk?");
35+
36+
// 1. Build CausaloidGraph
37+
let mut graph = CausaloidGraph::new(1);
38+
let smoke_idx = graph.add_causaloid(get_smoking_causaloid()).unwrap();
39+
let tar_idx = graph.add_causaloid(get_tar_causaloid()).unwrap();
40+
let cancer_idx = graph.add_causaloid(get_cancer_risk_causaloid()).unwrap();
41+
42+
graph.add_edge(smoke_idx, tar_idx).unwrap();
43+
graph.add_edge(tar_idx, cancer_idx).unwrap();
44+
graph.freeze();
45+
46+
// 2. Execute and Observe
47+
// Represents observing a high nicotine level.
48+
let initial_effect = PropagatingEffect::Numerical(0.8);
49+
50+
// Evaluate the full chain of events starting from the first cause.
51+
let final_effect = graph.evaluate_shortest_path_between_causes(smoke_idx, cancer_idx, &initial_effect).unwrap();
52+
53+
// 3. Assert and Explain
54+
assert_eq!(final_effect, PropagatingEffect::Deterministic(true));
55+
println!("Result: Observation of high nicotine level is associated with high cancer risk.");
56+
println!("\n");
57+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
use deep_causality::*;
7+
8+
fn get_smoking_causaloid() -> BaseCausaloid {
9+
Causaloid::new(1, |effect| {
10+
let nicotine_level = effect.as_numerical().unwrap_or(0.0);
11+
Ok(PropagatingEffect::Deterministic(nicotine_level > 0.6))
12+
}, "Smoking Status")
13+
}
14+
15+
fn get_tar_causaloid() -> BaseCausaloid {
16+
Causaloid::new(2, |effect| {
17+
let is_smoking = effect.as_bool().unwrap_or(false);
18+
Ok(PropagatingEffect::Deterministic(is_smoking))
19+
}, "Tar in Lungs")
20+
}
21+
22+
fn get_cancer_risk_causaloid() -> BaseCausaloid {
23+
Causaloid::new(3, |effect| {
24+
let has_tar = effect.as_bool().unwrap_or(false);
25+
Ok(PropagatingEffect::Deterministic(has_tar))
26+
}, "Cancer Risk")
27+
}
28+
29+
pub fn run_rung2_intervention() {
30+
println!("--- Rung 2: Intervention ---");
31+
println!("Demonstrating an intervention: If high cancer risk is detected, prescribe therapy.");
32+
33+
// 1. Setup Causal Model (same as Rung 1)
34+
let mut graph = CausaloidGraph::new(1);
35+
let smoke_idx = graph.add_causaloid(get_smoking_causaloid()).unwrap();
36+
let tar_idx = graph.add_causaloid(get_tar_causaloid()).unwrap();
37+
let cancer_idx = graph.add_causaloid(get_cancer_risk_causaloid()).unwrap();
38+
graph.add_edge(smoke_idx, tar_idx).unwrap();
39+
graph.add_edge(tar_idx, cancer_idx).unwrap();
40+
graph.freeze();
41+
42+
// The causaloid representing the final effect we want to act upon.
43+
let final_risk_causaloid = graph.get_causaloid(cancer_idx).unwrap().clone();
44+
45+
// 2. Define State and Action
46+
let high_cancer_risk_state = CausalState::new(
47+
10, // state ID
48+
1, // version
49+
PropagatingEffect::Deterministic(true), // The data to evaluate against the causaloid
50+
final_risk_causaloid,
51+
);
52+
53+
let prescribe_therapy_action = CausalAction::new(
54+
|| {
55+
println!("Intervention: Cessation therapy prescribed.");
56+
Ok(())
57+
},
58+
"Prescribe Therapy",
59+
1,
60+
);
61+
62+
// 3. Build Causal State Machine (CSM)
63+
let state_action_pair = &[(&high_cancer_risk_state, &prescribe_therapy_action)];
64+
let csm = CSM::new(state_action_pair);
65+
66+
// 4. Execute and Intervene
67+
// We need to evaluate the full causal chain first to get the final effect.
68+
let initial_effect = PropagatingEffect::Numerical(0.8);
69+
let final_effect = graph.evaluate_shortest_path_between_causes(smoke_idx, cancer_idx, &initial_effect).unwrap();
70+
71+
// Now, use the final effect as input to the CSM.
72+
// The CSM will check if the `high_cancer_risk_state` is met by this effect.
73+
let result = csm.eval_single_state(10, &final_effect);
74+
75+
assert!(result.is_ok());
76+
println!("Result: High cancer risk was detected, and the intervention was successfully fired.");
77+
println!("\n");
78+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SPDX-License-Identifier: MIT
3+
* Copyright (c) "2025" . The DeepCausality Authors and Contributors. All Rights Reserved.
4+
*/
5+
6+
use deep_causality::*;
7+
use std::sync::Arc;
8+
9+
// Contextoid IDs
10+
const NICOTINE_ID: IdentificationValue = 1;
11+
const TAR_ID: IdentificationValue = 2;
12+
13+
/// A contextual causal function that determines cancer risk.
14+
/// It prioritizes checking for tar, then for smoking.
15+
fn contextual_cancer_risk_logic(
16+
_effect: &PropagatingEffect,
17+
context: &Arc<BaseContext>,
18+
) -> Result<PropagatingEffect, CausalityError> {
19+
let mut tar_level = 0.0;
20+
let mut nicotine_level = 0.0;
21+
22+
// Scan the context for relevant data.
23+
for i in 0..context.number_of_nodes() {
24+
if let Some(node) = context.get_node(i) {
25+
if let ContextoidType::Datoid(data_node) = node.vertex_type() {
26+
match data_node.id() {
27+
TAR_ID => tar_level = data_node.get_data(),
28+
NICOTINE_ID => nicotine_level = data_node.get_data(),
29+
_ => (),
30+
}
31+
}
32+
}
33+
}
34+
35+
// Causal Logic: High tar is a direct cause of cancer risk, regardless of smoking.
36+
if tar_level > 0.6 {
37+
return Ok(PropagatingEffect::Deterministic(true));
38+
}
39+
// If tar is low, then smoking becomes the relevant factor.
40+
if nicotine_level > 0.6 {
41+
return Ok(PropagatingEffect::Deterministic(true));
42+
}
43+
44+
Ok(PropagatingEffect::Deterministic(false))
45+
}
46+
47+
pub fn run_rung3_counterfactual() {
48+
println!("--- Rung 3: Counterfactual ---");
49+
println!("Query: Given a smoker with high tar, what would their cancer risk be if they hadn't smoked?");
50+
51+
// 1. Define the Causaloid with our contextual logic
52+
let cancer_risk_causaloid = Causaloid::new_with_context(
53+
1,
54+
contextual_cancer_risk_logic,
55+
Arc::new(BaseContext::with_capacity(0, "temp", 1)), // Temporary context, will be replaced
56+
"Contextual Cancer Risk",
57+
);
58+
59+
// 2. Create Factual Context: A person who smokes and has high tar.
60+
let mut factual_context = BaseContext::with_capacity(1, "Factual", 5);
61+
factual_context.add_node(Contextoid::new(1, ContextoidType::Datoid(Data::new(NICOTINE_ID, 0.8)))).unwrap();
62+
factual_context.add_node(Contextoid::new(2, ContextoidType::Datoid(Data::new(TAR_ID, 0.8)))).unwrap();
63+
64+
// 3. Create Counterfactual Context: Same person, but we hypothetically set smoking to zero.
65+
let mut counterfactual_context = factual_context.clone();
66+
// To update, we need to know the index. In this simple case, it's 0.
67+
// A real implementation might use a HashMap<ID, Index> for lookup.
68+
let new_nicotine_datoid = Contextoid::new(1, ContextoidType::Datoid(Data::new(NICOTINE_ID, 0.1)));
69+
counterfactual_context.update_node(1, new_nicotine_datoid).unwrap();
70+
71+
// 4. Evaluate Both Scenarios
72+
let mut factual_causaloid = cancer_risk_causaloid.clone();
73+
factual_causaloid.set_context(Some(Arc::new(factual_context)));
74+
75+
let mut counterfactual_causaloid = cancer_risk_causaloid.clone();
76+
counterfactual_causaloid.set_context(Some(Arc::new(counterfactual_context)));
77+
78+
let factual_risk = factual_causaloid.evaluate(&PropagatingEffect::None).unwrap();
79+
let counterfactual_risk = counterfactual_causaloid.evaluate(&PropagatingEffect::None).unwrap();
80+
81+
// 5. Assert and Explain
82+
println!("Factual Result (smoker with high tar): Cancer risk is high -> {}", factual_risk.as_bool().unwrap());
83+
println!("Counterfactual Result (non-smoker with high tar): Cancer risk is high -> {}", counterfactual_risk.as_bool().unwrap());
84+
85+
assert_eq!(factual_risk, PropagatingEffect::Deterministic(true));
86+
assert_eq!(counterfactual_risk, PropagatingEffect::Deterministic(true));
87+
88+
println!("Conclusion: The cancer risk remains high in the counterfactual world because the direct cause (tar) was not undone.");
89+
println!("\n");
90+
}

0 commit comments

Comments
 (0)