Skip to main content

blazegraph_io_core/graphs/
analytics.rs

1use crate::types::*;
2use std::collections::HashMap;
3
4impl DocumentGraph {
5    /// Compute structural profile analytics for the entire graph
6    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        // Extract total_tokens before moving analytics fields
11        let total_tokens = analytics.token_distribution.overall.total_tokens;
12
13        // Update structural profile with analytics results
14        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
21/// Analytics computer that can analyze any subset of nodes in the graph
22pub struct GraphAnalytics;
23
24impl GraphAnalytics {
25    /// Compute analytics for any collection of nodes (enables subtree analysis)
26    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    /// Compute histogram-based token distribution with adaptive binning
35    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        // Collect token counts by type
40        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    /// Create histogram with adaptive binning based on data distribution
62    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        // Generate adaptive bins (use equal-width for simplicity, can be enhanced)
76        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        // Calculate statistics
98        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    /// Generate adaptive bin boundaries from data range
140    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    /// Compute node type distribution with counts and percentages
161    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    /// Compute depth distribution and statistics
186    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}