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.
0 commit comments