use log::info;
use std::sync::Arc;
use crate::db_trait::{AnalysisDatabase, AnalysisError};
use crate::models::partitions::ComponentAnalysisResult;
pub struct CommunityAnalyzer {
db_client: Arc<dyn AnalysisDatabase>,
}
impl CommunityAnalyzer {
pub fn new(db_client: Arc<dyn AnalysisDatabase>) -> Self {
Self { db_client }
}
pub async fn analyze_louvain_communities(
&self,
projection_name: &str,
params: &crate::config::LouvainConfig,
) -> Result<ComponentAnalysisResult, AnalysisError> {
info!("=== Starting Louvain Community Detection Analysis ===");
let result = self
.db_client
.calculate_louvain_communities(projection_name, params)
.await?;
info!("=== Louvain Community Detection Analysis Complete ===");
info!(
"Network has {} communities detected by Louvain algorithm",
result.components.len()
);
Ok(result)
}
pub async fn analyze_label_propagation_communities(
&self,
projection_name: &str,
params: &crate::config::LabelPropagationConfig,
) -> Result<ComponentAnalysisResult, AnalysisError> {
info!(
"=== Starting Label Propagation Community Detection Analysis ==="
);
let result = self
.db_client
.calculate_label_propagation_communities(projection_name, params)
.await?;
info!(
"=== Label Propagation Community Detection Analysis Complete ==="
);
info!(
"Network has {} communities detected by Label Propagation \
algorithm",
result.total_components.unwrap_or(0)
);
Ok(result)
}
pub fn display_louvain_community_analysis(
&self,
result: &ComponentAnalysisResult,
config: &crate::config::AnalysisSettings,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Louvain Community Detection Analysis:");
info!(
"Total Communities: {}",
result.total_components.unwrap_or(0)
);
info!(
"Largest Community Size: {}",
result.largest_component_size.unwrap_or(0)
);
info!(
"Smallest Community Size: {}",
result.smallest_component_size.unwrap_or(0)
);
let isolation_ratio = result.isolation_ratio.unwrap_or(0.0);
info!("Isolation Ratio: {:.2}%", isolation_ratio);
if let Some(modularity) = result.modularity {
info!("Modularity Score: {:.4}", modularity);
if modularity >= 0.3 {
info!(
"✅ Strong community structure detected (modularity >= 0.3)"
);
} else if modularity >= 0.1 {
info!(
"⚠️ Moderate community structure detected \
(modularity >= 0.1)"
);
} else {
info!(
"❌ Weak community structure detected (modularity < 0.1)"
);
}
} else {
info!("Modularity Score: Not calculated");
}
if isolation_ratio < config.isolation_ratio_threshold {
info!(
"⚠️ Network fragmentation detected: Isolation ratio \
{:.2}% is below threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
} else {
info!(
"✅ Network connectivity is healthy: Isolation ratio \
{:.2}% is above threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
}
if config.calculate_distribution {
if let Some(distribution) = &result.component_size_distribution {
info!("Community Size Distribution:");
let mut sizes: Vec<_> = distribution.iter().collect();
sizes.sort_by(|a, b| b.0.cmp(a.0)); for (size, count) in
sizes.iter().take(config.max_display_components.min(5))
{
info!(
"{} community/communities with {} relays each",
count, size
);
}
}
}
if result.components.len() > 1 {
info!(
"Top {} Largest Communities:",
config.max_display_components.min(result.components.len())
);
for (i, community) in result
.components
.iter()
.take(config.max_display_components)
.enumerate()
{
info!(
"{}. Community {}: {} relays",
i + 1,
community.component_id,
community.size
);
}
}
Ok(())
}
pub fn display_label_propagation_community_analysis(
&self,
result: &ComponentAnalysisResult,
config: &crate::config::AnalysisSettings,
) -> Result<(), Box<dyn std::error::Error>> {
info!("Label Propagation Community Detection Analysis:");
info!(
"Total Communities: {}",
result.total_components.unwrap_or(0)
);
info!(
"Largest Community Size: {}",
result.largest_component_size.unwrap_or(0)
);
info!(
"Smallest Community Size: {}",
result.smallest_component_size.unwrap_or(0)
);
let isolation_ratio = result.isolation_ratio.unwrap_or(0.0);
info!("Isolation Ratio: {:.2}%", isolation_ratio);
if let Some(modularity) = result.modularity {
info!("Modularity Score: {:.4}", modularity);
if modularity >= 0.3 {
info!(
"✅ Strong community structure detected (modularity >= 0.3)"
);
} else if modularity >= 0.1 {
info!(
"⚠️ Moderate community structure detected \
(modularity >= 0.1)"
);
} else {
info!(
"❌ Weak community structure detected (modularity < 0.1)"
);
}
} else {
info!("Modularity Score: Not calculated");
}
if isolation_ratio < config.isolation_ratio_threshold {
info!(
"⚠️ Network fragmentation detected: Isolation ratio \
{:.2}% is below threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
} else {
info!(
"✅ Network connectivity is healthy: Isolation ratio \
{:.2}% is above threshold {:.1}%",
isolation_ratio, config.isolation_ratio_threshold
);
}
if config.calculate_distribution {
if let Some(distribution) = &result.component_size_distribution {
info!("Community Size Distribution:");
let mut sizes: Vec<_> = distribution.iter().collect();
sizes.sort_by(|a, b| b.0.cmp(a.0)); for (size, count) in
sizes.iter().take(config.max_display_components.min(5))
{
info!(
"{} community/communities with {} relays each",
count, size
);
}
}
}
if result.components.len() > 1 {
info!(
"Top {} Largest Communities:",
config.max_display_components.min(result.components.len())
);
for (i, community) in result
.components
.iter()
.take(config.max_display_components)
.enumerate()
{
info!(
"{}. Community {}: {} relays",
i + 1,
community.component_id,
community.size
);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db_trait::mock::MockDatabase;
use crate::models::metrics::NodeMetrics;
#[tokio::test]
async fn test_louvain_algorithm_correctness() {
let nodes = vec![
NodeMetrics {
fingerprint: "RELAY_A".to_string(),
in_degree: 0,
out_degree: 2,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_B".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_C".to_string(),
in_degree: 1,
out_degree: 0,
total_degree: 1,
},
NodeMetrics {
fingerprint: "RELAY_D".to_string(),
in_degree: 0,
out_degree: 2,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_E".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_F".to_string(),
in_degree: 1,
out_degree: 0,
total_degree: 1,
},
];
let db = Arc::new(
MockDatabase::new().with_projection("test_louvain", nodes),
);
let analyzer = CommunityAnalyzer::new(db);
let result = analyzer
.analyze_louvain_communities(
"test_louvain",
&crate::config::LouvainConfig::default(),
)
.await
.expect("Louvain analysis should succeed");
assert_eq!(result.total_components, Some(2));
assert_eq!(result.components.len(), 2); assert!(result.largest_component_size.is_some());
assert!(result.smallest_component_size.is_some());
assert!(result.isolation_ratio.is_some());
assert!(result.modularity.is_some());
let modularity = result.modularity.unwrap();
assert!((0.0..=1.0).contains(&modularity));
let total_fingerprints: usize =
result.components.iter().map(|c| c.size).sum();
assert_eq!(total_fingerprints, 6);
}
#[tokio::test]
async fn test_label_propagation_algorithm_with_modularity() {
let nodes = vec![
NodeMetrics {
fingerprint: "RELAY_1".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_2".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_3".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_4".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_5".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_6".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_7".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_8".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
NodeMetrics {
fingerprint: "RELAY_9".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
},
];
let db =
Arc::new(MockDatabase::new().with_projection("test_lpa", nodes));
let analyzer = CommunityAnalyzer::new(db);
let result = analyzer
.analyze_label_propagation_communities(
"test_lpa",
&crate::config::LabelPropagationConfig::default(),
)
.await
.expect("LPA analysis should succeed");
assert_eq!(result.total_components, Some(3));
assert_eq!(result.components.len(), 3); assert!(result.modularity.is_some());
let modularity = result.modularity.unwrap();
assert!((0.0..=1.0).contains(&modularity));
assert_eq!(modularity, 0.42); let total_fingerprints: usize =
result.components.iter().map(|c| c.size).sum();
assert_eq!(total_fingerprints, 9);
}
#[tokio::test]
async fn test_modularity_integration() {
let db = MockDatabase::new().with_projection(
"test_graph",
vec![
NodeMetrics {
fingerprint: "RELAY_A".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
NodeMetrics {
fingerprint: "RELAY_B".to_string(),
in_degree: 2,
out_degree: 2,
total_degree: 4,
},
],
);
let analyzer = CommunityAnalyzer::new(Arc::new(db));
let result = analyzer
.analyze_louvain_communities(
"test_graph",
&crate::config::LouvainConfig::default(),
)
.await
.unwrap();
assert!(result.modularity.is_some());
assert_eq!(result.modularity.unwrap(), 0.42);
}
#[tokio::test]
async fn test_modularity_error_handling() {
let db = MockDatabase::new()
.with_projection(
"test_fail",
vec![NodeMetrics {
fingerprint: "RELAY_X".to_string(),
in_degree: 1,
out_degree: 1,
total_degree: 2,
}],
)
.fail_on("calculate_modularity");
let result = db.calculate_modularity("test_fail", "community").await;
assert!(result.is_err());
assert_eq!(db.get_call_count("calculate_modularity"), 1);
}
}