coreason_runtime_rust/execution_plane/
blast_radius.rs1use petgraph::graph::{DiGraph, NodeIndex};
16use petgraph::visit::{Bfs, Reversed};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CascadeResult {
23 pub defeated_node: String,
24 pub affected_nodes: Vec<String>,
25 pub cascade_depth: u32,
26 pub defeasibility_scores: HashMap<String, f64>,
27 pub total_affected: usize,
28}
29
30struct EpistemicGraph<'a> {
35 graph: DiGraph<&'a str, ()>,
36 node_map: HashMap<&'a str, NodeIndex>,
37}
38
39impl<'a> EpistemicGraph<'a> {
40 fn from_adjacency(adjacency: &'a HashMap<String, Vec<String>>) -> Self {
42 let mut graph = DiGraph::<&'a str, ()>::new();
43 let mut node_map: HashMap<&'a str, NodeIndex> = HashMap::new();
44
45 for (parent, children) in adjacency {
47 node_map
48 .entry(parent.as_str())
49 .or_insert_with(|| graph.add_node(parent.as_str()));
50 for child in children {
51 node_map
52 .entry(child.as_str())
53 .or_insert_with(|| graph.add_node(child.as_str()));
54 }
55 }
56
57 for (parent, children) in adjacency {
59 let &parent_idx = node_map.get(parent.as_str()).unwrap();
60 for child in children {
61 let &child_idx = node_map.get(child.as_str()).unwrap();
62 graph.add_edge(parent_idx, child_idx, ());
63 }
64 }
65
66 Self { graph, node_map }
67 }
68
69 fn reversed_view(&self) -> Reversed<&DiGraph<&'a str, ()>> {
72 Reversed(&self.graph)
73 }
74}
75
76pub struct BlastRadiusCalculator;
82
83impl BlastRadiusCalculator {
84 pub fn calculate_blast_radius(
89 defeated_node_cid: &str,
90 adjacency: &HashMap<String, Vec<String>>,
91 ) -> CascadeResult {
92 let eg = EpistemicGraph::from_adjacency(adjacency);
93
94 let start_idx = match eg.node_map.get(defeated_node_cid) {
95 Some(&idx) => idx,
96 None => {
97 return CascadeResult {
98 defeated_node: defeated_node_cid.to_string(),
99 affected_nodes: Vec::new(),
100 cascade_depth: 0,
101 defeasibility_scores: HashMap::new(),
102 total_affected: 0,
103 };
104 }
105 };
106
107 let mut bfs = Bfs::new(&eg.graph, start_idx);
109 let mut depth_map: HashMap<NodeIndex, u32> = HashMap::new();
110 depth_map.insert(start_idx, 0);
111
112 let mut affected: Vec<String> = Vec::new();
113 let mut defeasibility: HashMap<String, f64> = HashMap::new();
114 let mut max_depth: u32 = 0;
115
116 while let Some(node_idx) = bfs.next(&eg.graph) {
117 let node_label = eg.graph[node_idx];
118
119 let depth = if node_idx == start_idx {
121 0
122 } else {
123 eg.graph
125 .neighbors_directed(node_idx, petgraph::Direction::Incoming)
126 .filter_map(|pred| depth_map.get(&pred))
127 .min()
128 .map(|d| d + 1)
129 .unwrap_or(0)
130 };
131 depth_map.insert(node_idx, depth);
132 max_depth = max_depth.max(depth);
133
134 if node_label != defeated_node_cid {
135 affected.push(node_label.to_string());
136 }
137
138 defeasibility.insert(node_label.to_string(), 1.0 / (1.0 + depth as f64));
140 }
141
142 CascadeResult {
143 defeated_node: defeated_node_cid.to_string(),
144 affected_nodes: affected.clone(),
145 cascade_depth: max_depth,
146 defeasibility_scores: defeasibility,
147 total_affected: affected.len(),
148 }
149 }
150
151 pub fn compute_defeasibility_score(
157 node_cid: &str,
158 adjacency: &HashMap<String, Vec<String>>,
159 ) -> f64 {
160 let eg = EpistemicGraph::from_adjacency(adjacency);
161
162 let start_idx = match eg.node_map.get(node_cid) {
163 Some(&idx) => idx,
164 None => return 0.0,
165 };
166
167 let rev = eg.reversed_view();
169 let mut bfs = Bfs::new(&rev, start_idx);
170 let mut ancestor_count: usize = 0;
171
172 while let Some(node_idx) = bfs.next(&rev) {
173 if node_idx != start_idx {
174 ancestor_count += 1;
175 }
176 }
177
178 let total_nodes = adjacency.len().max(1);
179 (ancestor_count as f64 / total_nodes as f64).min(1.0)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn sample_graph() -> HashMap<String, Vec<String>> {
188 let mut g = HashMap::new();
189 g.insert("root".to_string(), vec!["a".to_string(), "b".to_string()]);
190 g.insert("a".to_string(), vec!["c".to_string(), "d".to_string()]);
191 g.insert("b".to_string(), vec!["d".to_string(), "e".to_string()]);
192 g.insert("c".to_string(), vec![]);
193 g.insert("d".to_string(), vec!["f".to_string()]);
194 g.insert("e".to_string(), vec![]);
195 g.insert("f".to_string(), vec![]);
196 g
197 }
198
199 #[test]
200 fn test_blast_radius_from_root() {
201 let graph = sample_graph();
202 let result = BlastRadiusCalculator::calculate_blast_radius("root", &graph);
203
204 assert_eq!(result.defeated_node, "root");
205 assert!(result.total_affected >= 5);
207 assert!(result.cascade_depth >= 2);
208 assert_eq!(result.defeasibility_scores["root"], 1.0);
209 }
210
211 #[test]
212 fn test_blast_radius_leaf_node() {
213 let graph = sample_graph();
214 let result = BlastRadiusCalculator::calculate_blast_radius("f", &graph);
215
216 assert_eq!(result.total_affected, 0);
217 assert_eq!(result.cascade_depth, 0);
218 }
219
220 #[test]
221 fn test_blast_radius_missing_node() {
222 let graph = sample_graph();
223 let result = BlastRadiusCalculator::calculate_blast_radius("nonexistent", &graph);
224
225 assert_eq!(result.total_affected, 0);
226 assert_eq!(result.cascade_depth, 0);
227 }
228
229 #[test]
230 fn test_defeasibility_score_root_has_no_ancestors() {
231 let graph = sample_graph();
232 let score = BlastRadiusCalculator::compute_defeasibility_score("root", &graph);
233 assert_eq!(score, 0.0);
234 }
235
236 #[test]
237 fn test_defeasibility_score_deep_node_has_ancestors() {
238 let graph = sample_graph();
239 let score = BlastRadiusCalculator::compute_defeasibility_score("f", &graph);
240 assert!(score > 0.0);
241 }
242}