Skip to content

Commit ea31ded

Browse files
committed
feat(deep_causality): Implemented Adaptive Reasoning #282
This commit introduces adaptive reasoning capabilities via the `PropagatingEffect::RelayTo` variant and refines the core effect propagation mechanisms in `DeepCausality`. Signed-off-by: Marvin Hansen <[email protected]>
1 parent 182fca3 commit ea31ded

File tree

2 files changed

+61
-10
lines changed
  • deep_causality/src
    • traits/causable_graph/graph_reasoning
    • types/reasoning_types/propagating_effect

2 files changed

+61
-10
lines changed

deep_causality/src/traits/causable_graph/graph_reasoning/mod.rs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ where
4747
/// the output effect of a parent node becomes the input effect for its child node.
4848
/// The traversal continues as long as no `CausalityError` is returned.
4949
///
50+
/// ## Adaptive Reasoning
51+
///
52+
/// If a `Causaloid` returns a `PropagatingEffect::RelayTo(target_index, inner_effect)`,
53+
/// the BFS traversal dynamically jumps to `target_index`, and `inner_effect` becomes
54+
/// the new input for the relayed path. This enables *adaptive reasoning* conditional to the deciding
55+
/// causaloid. To illustrate adaptive reasoning, an example clinical patent risk model may operate
56+
/// very differently for patients with normal blood pressure compared to high blood pressure patients.
57+
/// Therefore, two highly specialized models are defined and a dedicated dispatch causaloid.
58+
/// The dispatch causaloid analyses blood pressure and then, conditional on its finding, dispatches
59+
/// further reasoning to the matching model i.e. a dedicated sub-graph. Ensure that all possible
60+
/// values of target_index exists in the graph before implementing adaptive reasoning.
61+
/// For more details, see section 5.10.3 Adaptive Reasoning in The EPP reference paper:
62+
/// https://github.com/deepcausality-rs/papers/blob/main/effect_propagation_process/epp.pdf
63+
///
5064
/// # Arguments
5165
///
5266
/// * `start_index` - The index of the node to start the traversal from.
@@ -100,13 +114,33 @@ where
100114
// This ensures the function returns the effect of the last node on the path.
101115
last_propagated_effect = result_effect.clone();
102116

103-
// Only a CausalityError returned from cause.evaluate() will abort the traversal.
104-
let children = self.get_graph().outbound_edges(current_index)?;
105-
for child_index in children {
106-
if !visited[child_index] {
107-
visited[child_index] = true;
108-
// Pass the result_effect of the current node to its children.
109-
queue.push_back((child_index, result_effect.clone()));
117+
match result_effect {
118+
// Adaptive reasoning:
119+
// The Causaloid itself determines the next step in the reasoning process
120+
// conditional on its reasoning outcome. Based on its own internal logic,
121+
// a Causaloid then dynamically dispatches the flow of causality
122+
// to another Causaloid in the graph, enabling adaptive reasoning.
123+
PropagatingEffect::RelayTo(target_index, inner_effect) => {
124+
// If a RelayTo effect is returned, clear the queue and add the target_index
125+
// with the inner_effect as the new starting point for traversal.
126+
queue.clear();
127+
if !visited[target_index] {
128+
visited[target_index] = true;
129+
queue.push_back((target_index, *inner_effect));
130+
}
131+
// Update last_propagated_effect to reflect the effect of the relayed node.
132+
// This is already handled by the line above: last_propagated_effect = result_effect.clone();
133+
}
134+
_ => {
135+
// Only a CausalityError returned from cause.evaluate() will abort the traversal.
136+
let children = self.get_graph().outbound_edges(current_index)?;
137+
for child_index in children {
138+
if !visited[child_index] {
139+
visited[child_index] = true;
140+
// Pass the result_effect of the current node to its children.
141+
queue.push_back((child_index, result_effect.clone()));
142+
}
143+
}
110144
}
111145
}
112146
}
@@ -121,6 +155,15 @@ where
121155
/// one causaloid becomes the input for the next causaloid in the path. If any node
122156
/// fails evaluation or returns a non-propagating effect that prunes the path, the reasoning stops.
123157
///
158+
/// If a `Causaloid` returns a `PropagatingEffect::RelayTo(target_index, inner_effect)`,
159+
/// the shortest path traversal is immediately interrupted, and the `RelayTo` effect
160+
/// is returned to the caller, signaling a dynamic redirection. The runtime behavior differs
161+
/// from `evaluate_subgraph_from_cause` because a shortest path is assumed to be a fixed path
162+
/// and thus RelayTo is not supposed to happen in the middle of the path. Therefore, the
163+
/// call-site must handle the occurrence i.e. when its a known final effect.
164+
/// For more details, see section 5.10.3 Adaptive Reasoning in The EPP reference paper:
165+
/// https://github.com/deepcausality-rs/papers/blob/main/effect_propagation_process/epp.pdf
166+
///
124167
/// # Arguments
125168
///
126169
/// * `start_index` - The index of the start cause.
@@ -130,6 +173,7 @@ where
130173
/// # Returns
131174
///
132175
/// * `Ok(PropagatingEffect)`: The final `PropagatingEffect` from the last evaluated node on the path.
176+
/// If a `RelayTo` effect is encountered, that effect is returned immediately.
133177
/// * `Err(CausalityError)` if the path cannot be found or an evaluation fails.
134178
fn evaluate_shortest_path_between_causes(
135179
&self,
@@ -164,8 +208,14 @@ where
164208
// Evaluate the current cause with the effect propagated from the previous node.
165209
// Then, overwrite the current_effect with the result of the evaluation, which then
166210
// serves as the input for the next node.
167-
// Only a CausalityError returned from cause.evaluate() will abort the traversal.
211+
// For normal traversal, a CausalityError returned from cause.evaluate() will abort the traversal.
168212
current_effect = cause.evaluate(&current_effect)?;
213+
214+
// If a RelayTo effect is returned, stop the shortest path traversal and return it
215+
// because it braks the assumption of a fixed shortest path.
216+
if let PropagatingEffect::RelayTo(_, _) = current_effect {
217+
return Ok(current_effect);
218+
}
169219
}
170220

171221
// If the loop completes, all nodes on the path were successfully evaluated.

deep_causality/src/types/reasoning_types/propagating_effect/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ pub enum PropagatingEffect {
4141
Map(HashMap<IdentificationValue, Box<PropagatingEffect>>),
4242
/// A graph of effects, for passing complex relational data.
4343
Graph(Arc<EffectGraph>),
44-
/// A dispatch command that directs a reasoning engine to jump to a specific
45-
/// next causaloid with the specified id (`usize`) and provide it with the encapsulated effect as its new input.
44+
/// A dispatch command that directs the reasoning engine to dynamically jump to a specific
45+
/// causaloid within the graph. The `usize` is the target causaloid's index, and the `Box<PropagatingEffect>`
46+
/// is the effect to be passed as input to that target causaloid. This enables adaptive reasoning.
4647
RelayTo(usize, Box<PropagatingEffect>),
4748
}
4849

0 commit comments

Comments
 (0)