use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::error::AnalysisError;
use crate::input::{
split_edge_weight_key, validate_node_id, BoundarySpecInput, DependencyGraphInput,
LeidenConfigInput, PriorPartition, QualityFunctionInput,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoundaryDetectionResult {
pub cluster_assignments: BTreeMap<String, u32>,
pub community_count: u32,
pub modularity: f64,
pub internal_edge_density: BTreeMap<u32, f64>,
pub historical_stability: f64,
pub disconnected_components: u32,
}
pub fn detect_boundaries(
graph: &DependencyGraphInput,
cfg: &LeidenConfigInput,
prior: &[PriorPartition],
) -> Result<BoundaryDetectionResult, AnalysisError> {
for node in &graph.nodes {
validate_node_id(&node.id)?;
}
let node_paths: Vec<String> = graph.nodes.iter().map(|n| n.id.clone()).collect();
let id_to_idx: BTreeMap<&str, usize> = graph
.nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect();
let raw_edges: Vec<(usize, usize)> = graph
.edges
.iter()
.filter_map(|e| {
let from = *id_to_idx.get(e.source.as_str())?;
let to = *id_to_idx.get(e.target.as_str())?;
if from != to {
Some((from, to))
} else {
None
}
})
.collect();
let dg =
sdivi_graph::dependency_graph::build_dependency_graph_from_edges(&node_paths, &raw_edges);
let quality = match cfg.quality {
QualityFunctionInput::Modularity => sdivi_detection::QualityFunction::Modularity,
QualityFunctionInput::Cpm => sdivi_detection::QualityFunction::Cpm { gamma: cfg.gamma },
};
let leiden_cfg = sdivi_detection::LeidenConfig {
seed: cfg.seed,
max_iterations: cfg.iterations,
quality,
gamma: cfg.gamma,
};
let partition = if let Some(ref ew) = cfg.edge_weights {
let weight_map: std::collections::BTreeMap<(usize, usize), f64> = ew
.iter()
.filter_map(|(key, &w)| {
let (s, t) = split_edge_weight_key(key)?;
let si = *id_to_idx.get(s)?;
let ti = *id_to_idx.get(t)?;
let ordered = if si < ti { (si, ti) } else { (ti, si) };
Some((ordered, w))
})
.collect();
sdivi_detection::run_leiden_with_weights(&dg, &leiden_cfg, None, &weight_map)
} else {
sdivi_detection::run_leiden(&dg, &leiden_cfg, None)
};
let cluster_assignments: BTreeMap<String, u32> = partition
.assignments
.iter()
.filter_map(|(&node_idx, &comm)| {
node_paths.get(node_idx).map(|id| (id.clone(), comm as u32))
})
.collect();
let community_count = partition.community_count() as u32;
let internal_edge_density: BTreeMap<u32, f64> = partition
.stability
.iter()
.map(|(&comm, &density)| (comm as u32, density))
.collect();
let historical_stability = super::stability::compute_historical_stability(prior);
let component_count = sdivi_graph::metrics::compute_metrics(&dg).component_count as u32;
Ok(BoundaryDetectionResult {
cluster_assignments,
community_count,
modularity: partition.modularity,
internal_edge_density,
historical_stability,
disconnected_components: component_count,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoundaryViolationResult {
pub violation_count: u32,
pub violations: Vec<(String, String)>,
}
pub fn compute_boundary_violations(
graph: &DependencyGraphInput,
spec: &BoundarySpecInput,
) -> Result<BoundaryViolationResult, AnalysisError> {
for node in &graph.nodes {
validate_node_id(&node.id)?;
}
if spec.boundaries.is_empty() {
return Ok(BoundaryViolationResult {
violation_count: 0,
violations: vec![],
});
}
let compiled = super::violation::compile_boundaries(spec)?;
let node_boundaries: BTreeMap<&str, &str> = graph
.nodes
.iter()
.filter_map(|n| {
super::violation::match_boundary(&n.id, &compiled).map(|b| (n.id.as_str(), b))
})
.collect();
let allow_map: BTreeMap<&str, &[String]> = compiled
.iter()
.map(|cb| (cb.name.as_str(), cb.allow_imports_from.as_slice()))
.collect();
let mut violations: Vec<(String, String)> = Vec::new();
for edge in &graph.edges {
let from_b = match node_boundaries.get(edge.source.as_str()) {
Some(&b) => b,
None => continue,
};
let to_b = match node_boundaries.get(edge.target.as_str()) {
Some(&b) => b,
None => continue,
};
if from_b == to_b {
continue;
}
let allowed = allow_map
.get(from_b)
.is_some_and(|list| list.iter().any(|a| a == to_b));
if !allowed {
violations.push((edge.source.clone(), edge.target.clone()));
}
}
violations.sort();
let violation_count = violations.len() as u32;
Ok(BoundaryViolationResult {
violation_count,
violations,
})
}