Skip to content

Commit 512a6c0

Browse files
committed
Enhanced graph reasoning: introduced proper effect propagation logic and improved documentation clarity in causable graph methods.
Signed-off-by: Marvin Hansen <[email protected]>
1 parent 68d86e4 commit 512a6c0

File tree

3 files changed

+55
-44
lines changed

3 files changed

+55
-44
lines changed

deep_causality/src/traits/causable_collection/collection_explaining/collection_explaining_impl.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55
use crate::{Causable, CausalityError};
66

7-
pub(in crate::traits::causable_collection) fn _explain<T: Causable>(causes: Vec<&T>) -> Result<String, CausalityError> {
7+
pub(in crate::traits::causable_collection) fn _explain<T: Causable>(
8+
causes: Vec<&T>,
9+
) -> Result<String, CausalityError> {
810
if causes.is_empty() {
911
return Err(CausalityError::new(
1012
"Causal Collection is empty".to_string(),

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

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,25 @@ where
4343
/// Reasons over a subgraph by traversing all nodes reachable from a given start index.
4444
///
4545
/// This method performs a Breadth-First Search (BFS) traversal of all descendants
46-
/// of the `start_index`. It calls `evaluate` on each node and uses the resulting
47-
/// `PropagatingEffect` to decide whether to continue the traversal down that path.
46+
/// of the `start_index`. The `PropagatingEffect` is passed sequentially:
47+
/// the output effect of a parent node becomes the input effect for its child node.
48+
/// The traversal continues as long as no `CausalityError` is returned.
4849
///
4950
/// # Arguments
5051
///
5152
/// * `start_index` - The index of the node to start the traversal from.
52-
/// * `effect` - The runtime effect to be passed to each node's evaluation function.
53+
/// * `initial_effect` - The initial runtime effect to be passed to the starting node's evaluation function.
5354
///
5455
/// # Returns
5556
///
56-
/// * `Ok(PropagatingEffect::Halting)` if any node in the traversal returns `Halting`.
57-
/// * `Ok(PropagatingEffect::Deterministic(true))` if the traversal completes.
57+
/// * `Ok(PropagatingEffect)`: The final `PropagatingEffect` from the last successfully evaluated node
58+
/// in the main traversal path. `Deterministic(false)` now propagates and does not implicitly halt propagation.
59+
/// Only a `Causaloid` returning a `CausalityError` will abort the traversal.
5860
/// * `Err(CausalityError)` if the graph is not frozen, a node is missing, or an evaluation fails.
5961
fn evaluate_subgraph_from_cause(
6062
&self,
6163
start_index: usize,
62-
effect: &PropagatingEffect,
64+
initial_effect: &PropagatingEffect,
6365
) -> Result<PropagatingEffect, CausalityError> {
6466
if !self.is_frozen() {
6567
return Err(CausalityError(
@@ -73,68 +75,67 @@ where
7375
)));
7476
}
7577

76-
let mut queue = VecDeque::with_capacity(self.number_nodes());
78+
// Queue stores (node_index, incoming_effect_for_this_node)
79+
let mut queue = VecDeque::<(usize, PropagatingEffect)>::with_capacity(self.number_nodes());
7780
let mut visited = vec![false; self.number_nodes()];
7881

79-
queue.push_back(start_index);
82+
// Initialize the queue with the starting node and the initial effect
83+
queue.push_back((start_index, initial_effect.clone()));
8084
visited[start_index] = true;
8185

82-
while let Some(current_index) = queue.pop_front() {
86+
// This will hold the effect of the last successfully processed node.
87+
// It's initialized with the initial_effect, in case the start_index node
88+
// itself prunes the path or is the only node.
89+
let mut last_propagated_effect = initial_effect.clone();
90+
91+
while let Some((current_index, incoming_effect)) = queue.pop_front() {
8392
let cause = self.get_causaloid(current_index).ok_or_else(|| {
8493
CausalityError(format!("Failed to get causaloid at index {current_index}"))
8594
})?;
8695

87-
// Evaluate the current cause using the new unified method.
88-
// The same `effect` is passed to each node, and the node's CausalFn
89-
// is responsible for extracting the data it needs from the effect map.
90-
let effect = cause.evaluate(effect)?;
91-
92-
match effect {
93-
// If the cause is deterministically false, we stop traversing this path,
94-
// but the overall subgraph reasoning does not fail or halt.
95-
PropagatingEffect::Deterministic(false) => continue,
96-
97-
// For any other propagating effect (true, probabilistic, etc.),
98-
// we continue the traversal to its children.
99-
_ => {
100-
// The cause is valid, so add its children to the queue.
101-
let children = self.get_graph().outbound_edges(current_index)?;
102-
for child_index in children {
103-
if !visited[child_index] {
104-
visited[child_index] = true;
105-
queue.push_back(child_index);
106-
}
107-
}
96+
// Evaluate the current cause using the incoming_effect.
97+
let result_effect = cause.evaluate(&incoming_effect)?;
98+
99+
// Update the last_propagated_effect with the result of the current node's evaluation.
100+
// This ensures the function returns the effect of the last node on the path.
101+
last_propagated_effect = result_effect.clone();
102+
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()));
108110
}
109111
}
110112
}
111113

112-
// If the loop completes without halting, the reasoning is considered successful.
113-
Ok(PropagatingEffect::Deterministic(true))
114+
// If the loop completes, return the effect of the last node processed.
115+
Ok(last_propagated_effect)
114116
}
115117

116118
/// Reasons over the shortest path between a start and stop cause.
117119
///
118-
/// It evaluates each node along the path. If any node fails evaluation or returns
119-
/// a non-propagating effect, the reasoning stops.
120+
/// It evaluates each node sequentially along the path. The `PropagatingEffect` returned by
121+
/// one causaloid becomes the input for the next causaloid in the path. If any node
122+
/// fails evaluation or returns a non-propagating effect that prunes the path, the reasoning stops.
120123
///
121124
/// # Arguments
122125
///
123126
/// * `start_index` - The index of the start cause.
124127
/// * `stop_index` - The index of the stop cause.
125-
/// * `effect` - The runtime effect to be passed to each node's evaluation function.
128+
/// * `initial_effect` - The runtime effect to be passed as input to the first node's evaluation function.
126129
///
127130
/// # Returns
128131
///
129-
/// * `Ok(PropagatingEffect::Halting)` if any node on the path returns `Halting`.
130-
/// * `Ok(PropagatingEffect::Deterministic(false))` if any node returns `Deterministic(false)`.
131-
/// * `Ok(PropagatingEffect::Deterministic(true))` if all nodes on the path propagate successfully.
132+
/// * `Ok(PropagatingEffect)`: The final `PropagatingEffect` from the last evaluated node on the path.
132133
/// * `Err(CausalityError)` if the path cannot be found or an evaluation fails.
133134
fn evaluate_shortest_path_between_causes(
134135
&self,
135136
start_index: usize,
136137
stop_index: usize,
137-
effect: &PropagatingEffect,
138+
initial_effect: &PropagatingEffect,
138139
) -> Result<PropagatingEffect, CausalityError> {
139140
if !self.is_frozen() {
140141
return Err(CausalityError(
@@ -147,21 +148,27 @@ where
147148
let cause = self.get_causaloid(start_index).ok_or_else(|| {
148149
CausalityError(format!("Failed to get causaloid at index {start_index}"))
149150
})?;
150-
return cause.evaluate(effect);
151+
return cause.evaluate(initial_effect);
151152
}
152153

153154
// get_shortest_path will handle checks for missing nodes.
154155
let path = self.get_shortest_path(start_index, stop_index)?;
155156

157+
let mut current_effect = initial_effect.clone();
158+
156159
for index in path {
157160
let cause = self.get_causaloid(index).ok_or_else(|| {
158161
CausalityError(format!("Failed to get causaloid at index {index}"))
159162
})?;
160163

161-
let _ = cause.evaluate(effect)?;
164+
// Evaluate the current cause with the effect propagated from the previous node.
165+
// Then, overwrite the current_effect with the result of the evaluation, which then
166+
// serves as the input for the next node.
167+
// Only a CausalityError returned from cause.evaluate() will abort the traversal.
168+
current_effect = cause.evaluate(&current_effect)?;
162169
}
163170

164171
// If the loop completes, all nodes on the path were successfully evaluated.
165-
Ok(PropagatingEffect::Deterministic(true))
172+
Ok(current_effect)
166173
}
167174
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ pub enum PropagatingEffect {
2626
/// Represents the absence of a signal or evidence. Serves as the default.
2727
#[default]
2828
None,
29-
/// Represents a simple boolean value. As an output, this is often a terminal effect.
29+
/// Represents a simple boolean value. This effect propagates like any other,
30+
/// and its interpretation (e.g., whether it prunes a traversal) is left to the
31+
/// consuming logic or explicit error handling within Causaloids.
3032
Deterministic(bool),
3133
/// Represents a standard numerical value.
3234
Numerical(NumericalValue),

0 commit comments

Comments
 (0)