1use std::collections::HashMap;
11use std::mem::size_of;
12
13use serde::{Deserialize, Serialize};
14
15use super::CsrIndex;
16use crate::GraphError;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LabelStats {
21 pub edge_count: usize,
23 pub distinct_sources: usize,
25 pub distinct_targets: usize,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DegreeHistogram {
32 pub min: usize,
33 pub max: usize,
34 pub avg: f64,
35 pub p50: usize,
36 pub p95: usize,
37 pub p99: usize,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct GraphStatistics {
43 pub node_count: usize,
45 pub edge_count: usize,
47 pub label_count: usize,
49 pub label_stats: HashMap<String, LabelStats>,
51 pub out_degree_histogram: DegreeHistogram,
53 pub in_degree_histogram: DegreeHistogram,
55}
56
57impl CsrIndex {
58 pub fn compute_statistics(&self) -> Result<GraphStatistics, GraphError> {
68 let n = self.node_count();
69 if n == 0 {
70 return Ok(GraphStatistics {
71 node_count: 0,
72 edge_count: 0,
73 label_count: 0,
74 label_stats: HashMap::new(),
75 out_degree_histogram: DegreeHistogram {
76 min: 0,
77 max: 0,
78 avg: 0.0,
79 p50: 0,
80 p95: 0,
81 p99: 0,
82 },
83 in_degree_histogram: DegreeHistogram {
84 min: 0,
85 max: 0,
86 avg: 0.0,
87 p50: 0,
88 p95: 0,
89 p99: 0,
90 },
91 });
92 }
93
94 let degree_bytes = 2 * n * size_of::<usize>();
96 let _degree_guard = self.memory.reserve(degree_bytes)?;
97
98 let mut label_edge_count: HashMap<u32, usize> = HashMap::new();
100 let mut label_sources: HashMap<u32, std::collections::HashSet<u32>> = HashMap::new();
101 let mut label_targets: HashMap<u32, std::collections::HashSet<u32>> = HashMap::new();
102
103 let mut out_degrees: Vec<usize> = Vec::with_capacity(n);
105 let mut in_degrees: Vec<usize> = Vec::with_capacity(n);
106
107 let mut total_edges = 0usize;
108
109 for node in 0..n {
110 let node_id = node as u32;
111 let mut out_deg = 0usize;
112 let mut in_deg = 0usize;
113
114 for (lid, dst) in self.dense_iter_out(node_id) {
115 out_deg += 1;
116 total_edges += 1;
117 *label_edge_count.entry(lid).or_insert(0) += 1;
118 label_sources.entry(lid).or_default().insert(node_id);
119 label_targets.entry(lid).or_default().insert(dst);
120 }
121
122 for (_lid, _src) in self.dense_iter_in(node_id) {
123 in_deg += 1;
124 }
125
126 out_degrees.push(out_deg);
127 in_degrees.push(in_deg);
128 }
129
130 let mut label_stats = HashMap::new();
132 for (&lid, &count) in &label_edge_count {
133 let label_name = self.label_name(lid).to_string();
134 label_stats.insert(
135 label_name,
136 LabelStats {
137 edge_count: count,
138 distinct_sources: label_sources.get(&lid).map_or(0, |s| s.len()),
139 distinct_targets: label_targets.get(&lid).map_or(0, |s| s.len()),
140 },
141 );
142 }
143
144 Ok(GraphStatistics {
145 node_count: n,
146 edge_count: total_edges,
147 label_count: label_edge_count.len(),
148 label_stats,
149 out_degree_histogram: compute_histogram(&out_degrees),
150 in_degree_histogram: compute_histogram(&in_degrees),
151 })
152 }
153
154 pub fn label_edge_count(&self, label: &str) -> usize {
158 let Some(lid) = self.label_id(label) else {
159 return 0;
160 };
161
162 let n = self.node_count();
163 let mut count = 0usize;
164 for node in 0..n {
165 for (l, _dst) in self.dense_iter_out(node as u32) {
166 if l == lid {
167 count += 1;
168 }
169 }
170 }
171 count
172 }
173
174 pub fn label_selectivity(&self, label: &str) -> f64 {
179 let total = self.edge_count();
180 if total == 0 {
181 return 0.0;
182 }
183 let count = self.label_edge_count(label);
184 if count == 0 {
185 return 1.0; }
187 count as f64 / total as f64
188 }
189}
190
191fn compute_histogram(degrees: &[usize]) -> DegreeHistogram {
193 if degrees.is_empty() {
194 return DegreeHistogram {
195 min: 0,
196 max: 0,
197 avg: 0.0,
198 p50: 0,
199 p95: 0,
200 p99: 0,
201 };
202 }
203
204 let mut sorted = degrees.to_vec();
205 sorted.sort_unstable();
206
207 let n = sorted.len();
208 let sum: usize = sorted.iter().sum();
209
210 DegreeHistogram {
211 min: sorted[0],
212 max: sorted[n - 1],
213 avg: sum as f64 / n as f64,
214 p50: sorted[n / 2],
215 p95: sorted[(n as f64 * 0.95) as usize],
216 p99: sorted[((n as f64 * 0.99) as usize).min(n - 1)],
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::test_support::test_memory;
224
225 #[test]
226 fn statistics_empty_graph() {
227 let csr = CsrIndex::new(test_memory());
228 let stats = csr.compute_statistics().unwrap();
229 assert_eq!(stats.node_count, 0);
230 assert_eq!(stats.edge_count, 0);
231 assert_eq!(stats.label_count, 0);
232 }
233
234 #[test]
235 fn statistics_basic() {
236 let mut csr = CsrIndex::new(test_memory());
237 csr.add_edge("a", "KNOWS", "b").unwrap();
238 csr.add_edge("b", "KNOWS", "c").unwrap();
239 csr.add_edge("a", "LIKES", "c").unwrap();
240 csr.compact()
241 .expect("test governor ceiling covers this reservation");
242
243 let stats = csr.compute_statistics().unwrap();
244 assert_eq!(stats.node_count, 3);
245 assert_eq!(stats.edge_count, 3);
246 assert_eq!(stats.label_count, 2);
247
248 let knows = &stats.label_stats["KNOWS"];
249 assert_eq!(knows.edge_count, 2);
250 assert_eq!(knows.distinct_sources, 2);
251 assert_eq!(knows.distinct_targets, 2);
252
253 let likes = &stats.label_stats["LIKES"];
254 assert_eq!(likes.edge_count, 1);
255 }
256
257 #[test]
258 fn degree_histogram_values() {
259 let mut csr = CsrIndex::new(test_memory());
260 csr.add_edge("a", "L", "b").unwrap();
261 csr.add_edge("a", "L", "c").unwrap();
262 csr.add_edge("a", "L", "d").unwrap();
263 csr.add_edge("b", "L", "c").unwrap();
264 csr.compact()
265 .expect("test governor ceiling covers this reservation");
266
267 let stats = csr.compute_statistics().unwrap();
268 assert_eq!(stats.out_degree_histogram.min, 0);
269 assert_eq!(stats.out_degree_histogram.max, 3);
270 assert!(stats.out_degree_histogram.avg > 0.0);
271 }
272
273 #[test]
274 fn label_edge_count_direct() {
275 let mut csr = CsrIndex::new(test_memory());
276 csr.add_edge("a", "KNOWS", "b").unwrap();
277 csr.add_edge("b", "KNOWS", "c").unwrap();
278 csr.add_edge("a", "LIKES", "c").unwrap();
279 csr.compact()
280 .expect("test governor ceiling covers this reservation");
281
282 assert_eq!(csr.label_edge_count("KNOWS"), 2);
283 assert_eq!(csr.label_edge_count("LIKES"), 1);
284 assert_eq!(csr.label_edge_count("NONEXISTENT"), 0);
285 }
286
287 #[test]
288 fn label_selectivity_values() {
289 let mut csr = CsrIndex::new(test_memory());
290 csr.add_edge("a", "KNOWS", "b").unwrap();
291 csr.add_edge("b", "KNOWS", "c").unwrap();
292 csr.add_edge("a", "LIKES", "c").unwrap();
293 csr.compact()
294 .expect("test governor ceiling covers this reservation");
295
296 let sel_knows = csr.label_selectivity("KNOWS");
297 let sel_likes = csr.label_selectivity("LIKES");
298
299 assert!((sel_knows - 2.0 / 3.0).abs() < 1e-9);
300 assert!((sel_likes - 1.0 / 3.0).abs() < 1e-9);
301 assert_eq!(csr.label_selectivity("NONEXISTENT"), 1.0);
302 }
303
304 #[test]
305 fn statistics_serde_roundtrip() {
306 let mut csr = CsrIndex::new(test_memory());
307 csr.add_edge("a", "KNOWS", "b").unwrap();
308 csr.compact()
309 .expect("test governor ceiling covers this reservation");
310
311 let stats = csr.compute_statistics().unwrap();
312 let json = sonic_rs::to_string(&stats).unwrap();
313 let parsed: GraphStatistics = sonic_rs::from_str(&json).unwrap();
314 assert_eq!(parsed.node_count, stats.node_count);
315 assert_eq!(parsed.edge_count, stats.edge_count);
316 }
317
318 #[test]
319 fn statistics_with_buffer_edges() {
320 let mut csr = CsrIndex::new(test_memory());
321 csr.add_edge("a", "KNOWS", "b").unwrap();
322 let stats = csr.compute_statistics().unwrap();
324 assert_eq!(stats.edge_count, 1);
325 assert_eq!(stats.label_stats["KNOWS"].edge_count, 1);
326 }
327}