blazegraph_io_core/graphs/
analytics.rs1use crate::types::*;
2use std::collections::HashMap;
3
4impl DocumentGraph {
5 pub fn compute_structural_profile(&mut self) {
7 let all_nodes: Vec<&DocumentNode> = self.nodes.values().collect();
8 let analytics = GraphAnalytics::compute_analytics(&all_nodes);
9
10 let total_tokens = analytics.token_distribution.overall.total_tokens;
12
13 self.structural_profile.token_distribution = analytics.token_distribution;
15 self.structural_profile.node_type_distribution = analytics.node_type_distribution;
16 self.structural_profile.depth_distribution = analytics.depth_distribution;
17 self.structural_profile.total_tokens = total_tokens;
18 }
19}
20
21pub struct GraphAnalytics;
23
24impl GraphAnalytics {
25 pub fn compute_analytics(nodes: &[&DocumentNode]) -> GraphAnalyticsResult {
27 GraphAnalyticsResult {
28 token_distribution: Self::compute_token_distribution(nodes),
29 node_type_distribution: Self::compute_node_type_distribution(nodes),
30 depth_distribution: Self::compute_depth_distribution(nodes),
31 }
32 }
33
34 fn compute_token_distribution(nodes: &[&DocumentNode]) -> TokenDistribution {
36 let mut overall_tokens = Vec::new();
37 let mut by_type: HashMap<String, Vec<usize>> = HashMap::new();
38
39 for node in nodes {
41 overall_tokens.push(node.token_count);
42 by_type
43 .entry(node.node_type.clone())
44 .or_default()
45 .push(node.token_count);
46 }
47
48 let overall_histogram = Self::create_histogram(&overall_tokens);
49 let mut type_histograms = HashMap::new();
50
51 for (node_type, tokens) in by_type {
52 type_histograms.insert(node_type, Self::create_histogram(&tokens));
53 }
54
55 TokenDistribution {
56 overall: overall_histogram,
57 by_node_type: type_histograms,
58 }
59 }
60
61 fn create_histogram(token_counts: &[usize]) -> TokenHistogram {
63 if token_counts.is_empty() {
64 return TokenHistogram::default();
65 }
66
67 let mut sorted_tokens = token_counts.to_vec();
68 sorted_tokens.sort_unstable();
69
70 let min_tokens = sorted_tokens[0] as u32;
71 let max_tokens = sorted_tokens[sorted_tokens.len() - 1] as u32;
72 let total_tokens: usize = sorted_tokens.iter().sum();
73 let total_count = sorted_tokens.len();
74
75 let bin_ranges = Self::generate_adaptive_bins(min_tokens, max_tokens, 10);
77 let mut bins = Vec::new();
78
79 for (range_start, range_end) in bin_ranges {
80 let count = sorted_tokens
81 .iter()
82 .filter(|&&token| (token as u32) >= range_start && (token as u32) < range_end)
83 .count();
84 let token_sum: usize = sorted_tokens
85 .iter()
86 .filter(|&&token| (token as u32) >= range_start && (token as u32) < range_end)
87 .sum();
88
89 bins.push(HistogramBin {
90 range_start,
91 range_end,
92 count,
93 token_sum,
94 });
95 }
96
97 let mean = if total_count > 0 {
99 total_tokens as f32 / total_count as f32
100 } else {
101 0.0
102 };
103 let median = if sorted_tokens.is_empty() {
104 0.0
105 } else if sorted_tokens.len() % 2 == 0 {
106 let mid = sorted_tokens.len() / 2;
107 (sorted_tokens[mid - 1] + sorted_tokens[mid]) as f32 / 2.0
108 } else {
109 sorted_tokens[sorted_tokens.len() / 2] as f32
110 };
111
112 let mode = bins
113 .iter()
114 .max_by_key(|bin| bin.count)
115 .map(|bin| bin.range_start);
116
117 let variance = if total_count > 1 {
118 let mean_val = mean;
119 sorted_tokens
120 .iter()
121 .map(|&token| (token as f32 - mean_val).powi(2))
122 .sum::<f32>()
123 / (total_count - 1) as f32
124 } else {
125 0.0
126 };
127
128 TokenHistogram {
129 bins,
130 total_count,
131 total_tokens,
132 mean,
133 median,
134 mode,
135 variance,
136 }
137 }
138
139 fn generate_adaptive_bins(min_val: u32, max_val: u32, target_bins: usize) -> Vec<(u32, u32)> {
141 if min_val >= max_val {
142 return vec![(min_val, min_val + 1)];
143 }
144
145 let range = max_val - min_val;
146 let bin_width = ((range as f32 / target_bins as f32).ceil() as u32).max(1);
147
148 let mut bins = Vec::new();
149 let mut current = min_val;
150
151 while current < max_val {
152 let end = (current + bin_width).min(max_val + 1);
153 bins.push((current, end));
154 current = end;
155 }
156
157 bins
158 }
159
160 fn compute_node_type_distribution(nodes: &[&DocumentNode]) -> NodeTypeDistribution {
162 let mut counts = HashMap::new();
163 let total_nodes = nodes.len();
164
165 for node in nodes {
166 *counts.entry(node.node_type.clone()).or_insert(0) += 1;
167 }
168
169 let mut percentages = HashMap::new();
170 for (node_type, count) in &counts {
171 let percentage = if total_nodes > 0 {
172 (*count as f32 / total_nodes as f32) * 100.0
173 } else {
174 0.0
175 };
176 percentages.insert(node_type.clone(), percentage);
177 }
178
179 NodeTypeDistribution {
180 counts,
181 percentages,
182 }
183 }
184
185 fn compute_depth_distribution(nodes: &[&DocumentNode]) -> DepthDistribution {
187 let mut depth_counts = HashMap::new();
188 let mut total_depth = 0u32;
189 let mut max_depth = 0u32;
190
191 for node in nodes {
192 let depth = node.location.semantic.depth;
193 *depth_counts.entry(depth).or_insert(0) += 1;
194 total_depth += depth;
195 max_depth = max_depth.max(depth);
196 }
197
198 let avg_depth = if !nodes.is_empty() {
199 total_depth as f32 / nodes.len() as f32
200 } else {
201 0.0
202 };
203
204 DepthDistribution {
205 max_depth,
206 depth_counts,
207 avg_depth,
208 }
209 }
210}