use crate::types::*;
use std::collections::HashMap;
impl DocumentGraph {
pub fn compute_structural_profile(&mut self) {
let all_nodes: Vec<&DocumentNode> = self.nodes.values().collect();
let analytics = GraphAnalytics::compute_analytics(&all_nodes);
let total_tokens = analytics.token_distribution.overall.total_tokens;
self.structural_profile.token_distribution = analytics.token_distribution;
self.structural_profile.node_type_distribution = analytics.node_type_distribution;
self.structural_profile.depth_distribution = analytics.depth_distribution;
self.structural_profile.total_tokens = total_tokens;
}
}
pub struct GraphAnalytics;
impl GraphAnalytics {
pub fn compute_analytics(nodes: &[&DocumentNode]) -> GraphAnalyticsResult {
GraphAnalyticsResult {
token_distribution: Self::compute_token_distribution(nodes),
node_type_distribution: Self::compute_node_type_distribution(nodes),
depth_distribution: Self::compute_depth_distribution(nodes),
}
}
fn compute_token_distribution(nodes: &[&DocumentNode]) -> TokenDistribution {
let mut overall_tokens = Vec::new();
let mut by_type: HashMap<String, Vec<usize>> = HashMap::new();
for node in nodes {
overall_tokens.push(node.token_count);
by_type.entry(node.node_type.clone())
.or_default()
.push(node.token_count);
}
let overall_histogram = Self::create_histogram(&overall_tokens);
let mut type_histograms = HashMap::new();
for (node_type, tokens) in by_type {
type_histograms.insert(node_type, Self::create_histogram(&tokens));
}
TokenDistribution {
overall: overall_histogram,
by_node_type: type_histograms,
}
}
fn create_histogram(token_counts: &[usize]) -> TokenHistogram {
if token_counts.is_empty() {
return TokenHistogram::default();
}
let mut sorted_tokens = token_counts.to_vec();
sorted_tokens.sort_unstable();
let min_tokens = sorted_tokens[0] as u32;
let max_tokens = sorted_tokens[sorted_tokens.len() - 1] as u32;
let total_tokens: usize = sorted_tokens.iter().sum();
let total_count = sorted_tokens.len();
let bin_ranges = Self::generate_adaptive_bins(min_tokens, max_tokens, 10);
let mut bins = Vec::new();
for (range_start, range_end) in bin_ranges {
let count = sorted_tokens
.iter()
.filter(|&&token| (token as u32) >= range_start && (token as u32) < range_end)
.count();
let token_sum: usize = sorted_tokens
.iter()
.filter(|&&token| (token as u32) >= range_start && (token as u32) < range_end)
.sum();
bins.push(HistogramBin {
range_start,
range_end,
count,
token_sum,
});
}
let mean = if total_count > 0 { total_tokens as f32 / total_count as f32 } else { 0.0 };
let median = if sorted_tokens.is_empty() {
0.0
} else if sorted_tokens.len() % 2 == 0 {
let mid = sorted_tokens.len() / 2;
(sorted_tokens[mid - 1] + sorted_tokens[mid]) as f32 / 2.0
} else {
sorted_tokens[sorted_tokens.len() / 2] as f32
};
let mode = bins.iter()
.max_by_key(|bin| bin.count)
.map(|bin| bin.range_start);
let variance = if total_count > 1 {
let mean_val = mean;
sorted_tokens.iter()
.map(|&token| (token as f32 - mean_val).powi(2))
.sum::<f32>() / (total_count - 1) as f32
} else {
0.0
};
TokenHistogram {
bins,
total_count,
total_tokens,
mean,
median,
mode,
variance,
}
}
fn generate_adaptive_bins(min_val: u32, max_val: u32, target_bins: usize) -> Vec<(u32, u32)> {
if min_val >= max_val {
return vec![(min_val, min_val + 1)];
}
let range = max_val - min_val;
let bin_width = ((range as f32 / target_bins as f32).ceil() as u32).max(1);
let mut bins = Vec::new();
let mut current = min_val;
while current < max_val {
let end = (current + bin_width).min(max_val + 1);
bins.push((current, end));
current = end;
}
bins
}
fn compute_node_type_distribution(nodes: &[&DocumentNode]) -> NodeTypeDistribution {
let mut counts = HashMap::new();
let total_nodes = nodes.len();
for node in nodes {
*counts.entry(node.node_type.clone()).or_insert(0) += 1;
}
let mut percentages = HashMap::new();
for (node_type, count) in &counts {
let percentage = if total_nodes > 0 {
(*count as f32 / total_nodes as f32) * 100.0
} else {
0.0
};
percentages.insert(node_type.clone(), percentage);
}
NodeTypeDistribution {
counts,
percentages,
}
}
fn compute_depth_distribution(nodes: &[&DocumentNode]) -> DepthDistribution {
let mut depth_counts = HashMap::new();
let mut total_depth = 0u32;
let mut max_depth = 0u32;
for node in nodes {
let depth = node.location.semantic.depth;
*depth_counts.entry(depth).or_insert(0) += 1;
total_depth += depth;
max_depth = max_depth.max(depth);
}
let avg_depth = if !nodes.is_empty() {
total_depth as f32 / nodes.len() as f32
} else {
0.0
};
DepthDistribution {
max_depth,
depth_counts,
avg_depth,
}
}
}