use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::{Bfs, Reversed};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CascadeResult {
pub defeated_node: String,
pub affected_nodes: Vec<String>,
pub cascade_depth: u32,
pub defeasibility_scores: HashMap<String, f64>,
pub total_affected: usize,
}
struct EpistemicGraph<'a> {
graph: DiGraph<&'a str, ()>,
node_map: HashMap<&'a str, NodeIndex>,
}
impl<'a> EpistemicGraph<'a> {
fn from_adjacency(adjacency: &'a HashMap<String, Vec<String>>) -> Self {
let mut graph = DiGraph::<&'a str, ()>::new();
let mut node_map: HashMap<&'a str, NodeIndex> = HashMap::new();
for (parent, children) in adjacency {
node_map
.entry(parent.as_str())
.or_insert_with(|| graph.add_node(parent.as_str()));
for child in children {
node_map
.entry(child.as_str())
.or_insert_with(|| graph.add_node(child.as_str()));
}
}
for (parent, children) in adjacency {
let &parent_idx = node_map.get(parent.as_str()).unwrap();
for child in children {
let &child_idx = node_map.get(child.as_str()).unwrap();
graph.add_edge(parent_idx, child_idx, ());
}
}
Self { graph, node_map }
}
fn reversed_view(&self) -> Reversed<&DiGraph<&'a str, ()>> {
Reversed(&self.graph)
}
}
pub struct BlastRadiusCalculator;
impl BlastRadiusCalculator {
pub fn calculate_blast_radius(
defeated_node_cid: &str,
adjacency: &HashMap<String, Vec<String>>,
) -> CascadeResult {
let eg = EpistemicGraph::from_adjacency(adjacency);
let start_idx = match eg.node_map.get(defeated_node_cid) {
Some(&idx) => idx,
None => {
return CascadeResult {
defeated_node: defeated_node_cid.to_string(),
affected_nodes: Vec::new(),
cascade_depth: 0,
defeasibility_scores: HashMap::new(),
total_affected: 0,
};
}
};
let mut bfs = Bfs::new(&eg.graph, start_idx);
let mut depth_map: HashMap<NodeIndex, u32> = HashMap::new();
depth_map.insert(start_idx, 0);
let mut affected: Vec<String> = Vec::new();
let mut defeasibility: HashMap<String, f64> = HashMap::new();
let mut max_depth: u32 = 0;
while let Some(node_idx) = bfs.next(&eg.graph) {
let node_label = eg.graph[node_idx];
let depth = if node_idx == start_idx {
0
} else {
eg.graph
.neighbors_directed(node_idx, petgraph::Direction::Incoming)
.filter_map(|pred| depth_map.get(&pred))
.min()
.map(|d| d + 1)
.unwrap_or(0)
};
depth_map.insert(node_idx, depth);
max_depth = max_depth.max(depth);
if node_label != defeated_node_cid {
affected.push(node_label.to_string());
}
defeasibility.insert(node_label.to_string(), 1.0 / (1.0 + depth as f64));
}
CascadeResult {
defeated_node: defeated_node_cid.to_string(),
affected_nodes: affected.clone(),
cascade_depth: max_depth,
defeasibility_scores: defeasibility,
total_affected: affected.len(),
}
}
pub fn compute_defeasibility_score(
node_cid: &str,
adjacency: &HashMap<String, Vec<String>>,
) -> f64 {
let eg = EpistemicGraph::from_adjacency(adjacency);
let start_idx = match eg.node_map.get(node_cid) {
Some(&idx) => idx,
None => return 0.0,
};
let rev = eg.reversed_view();
let mut bfs = Bfs::new(&rev, start_idx);
let mut ancestor_count: usize = 0;
while let Some(node_idx) = bfs.next(&rev) {
if node_idx != start_idx {
ancestor_count += 1;
}
}
let total_nodes = adjacency.len().max(1);
(ancestor_count as f64 / total_nodes as f64).min(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_graph() -> HashMap<String, Vec<String>> {
let mut g = HashMap::new();
g.insert("root".to_string(), vec!["a".to_string(), "b".to_string()]);
g.insert("a".to_string(), vec!["c".to_string(), "d".to_string()]);
g.insert("b".to_string(), vec!["d".to_string(), "e".to_string()]);
g.insert("c".to_string(), vec![]);
g.insert("d".to_string(), vec!["f".to_string()]);
g.insert("e".to_string(), vec![]);
g.insert("f".to_string(), vec![]);
g
}
#[test]
fn test_blast_radius_from_root() {
let graph = sample_graph();
let result = BlastRadiusCalculator::calculate_blast_radius("root", &graph);
assert_eq!(result.defeated_node, "root");
assert!(result.total_affected >= 5);
assert!(result.cascade_depth >= 2);
assert_eq!(result.defeasibility_scores["root"], 1.0);
}
#[test]
fn test_blast_radius_leaf_node() {
let graph = sample_graph();
let result = BlastRadiusCalculator::calculate_blast_radius("f", &graph);
assert_eq!(result.total_affected, 0);
assert_eq!(result.cascade_depth, 0);
}
#[test]
fn test_blast_radius_missing_node() {
let graph = sample_graph();
let result = BlastRadiusCalculator::calculate_blast_radius("nonexistent", &graph);
assert_eq!(result.total_affected, 0);
assert_eq!(result.cascade_depth, 0);
}
#[test]
fn test_defeasibility_score_root_has_no_ancestors() {
let graph = sample_graph();
let score = BlastRadiusCalculator::compute_defeasibility_score("root", &graph);
assert_eq!(score, 0.0);
}
#[test]
fn test_defeasibility_score_deep_node_has_ancestors() {
let graph = sample_graph();
let score = BlastRadiusCalculator::compute_defeasibility_score("f", &graph);
assert!(score > 0.0);
}
}