1use crate::error::Result;
2use crate::streaming::incremental::IncrementalGraphProcessor;
3use arrow::array::Array;
4use std::collections::HashMap;
5use std::hash::{Hash, Hasher};
6
7#[derive(Debug, Clone)]
13pub struct HyperLogLog {
14 buckets: Vec<u8>,
15 bucket_count: usize,
16 alpha: f64,
17}
18
19impl HyperLogLog {
20 pub fn new(precision: u8) -> Self {
23 let precision = precision.clamp(4, 16);
24 let bucket_count = 1 << precision; let alpha = match bucket_count {
27 16 => 0.673,
28 32 => 0.697,
29 64 => 0.709,
30 _ => 0.7213 / (1.0 + 1.079 / bucket_count as f64),
31 };
32
33 Self {
34 buckets: vec![0; bucket_count],
35 bucket_count,
36 alpha,
37 }
38 }
39
40 pub fn add<T: Hash>(&mut self, element: &T) {
42 let hash = self.hash_element(element);
43 let bucket_index = (hash >> (64 - self.precision_bits())) as usize;
44 let leading_zeros = (hash << self.precision_bits()).leading_zeros() as u8 + 1;
45
46 self.buckets[bucket_index] = self.buckets[bucket_index].max(leading_zeros);
47 }
48
49 pub fn estimate(&self) -> f64 {
51 let raw_estimate = self.alpha * (self.bucket_count as f64).powi(2) /
52 self.buckets.iter().map(|&b| 2.0_f64.powf(-(b as f64))).sum::<f64>();
53
54 if raw_estimate <= 2.5 * self.bucket_count as f64 {
56 let zero_count = self.buckets.iter().filter(|&&b| b == 0).count();
57 if zero_count != 0 {
58 return (self.bucket_count as f64) * (self.bucket_count as f64 / zero_count as f64).ln();
59 }
60 }
61
62 if raw_estimate <= (1.0 / 30.0) * (1u64 << 32) as f64 {
64 raw_estimate
65 } else {
66 -((1u64 << 32) as f64) * (1.0 - raw_estimate / ((1u64 << 32) as f64)).ln()
67 }
68 }
69
70 pub fn merge(&mut self, other: &HyperLogLog) -> Result<()> {
72 if self.bucket_count != other.bucket_count {
73 return Err(crate::error::GraphError::graph_construction("HyperLogLog bucket counts must match for merge"));
74 }
75
76 for i in 0..self.bucket_count {
77 self.buckets[i] = self.buckets[i].max(other.buckets[i]);
78 }
79
80 Ok(())
81 }
82
83 fn hash_element<T: Hash>(&self, element: &T) -> u64 {
84 let mut hasher = std::collections::hash_map::DefaultHasher::new();
85 element.hash(&mut hasher);
86 hasher.finish()
87 }
88
89 fn precision_bits(&self) -> u32 {
90 (self.bucket_count as f64).log2() as u32
91 }
92}
93
94#[derive(Debug, Clone)]
97pub struct CountMinSketch {
98 counters: Vec<Vec<u32>>,
99 hash_functions: Vec<HashFunction>,
100 width: usize,
101 depth: usize,
102}
103
104#[derive(Debug, Clone)]
105struct HashFunction {
106 a: u64,
107 b: u64,
108 p: u64, }
110
111impl CountMinSketch {
112 pub fn new(width: usize, depth: usize) -> Self {
116 let mut hash_functions = Vec::new();
117 let p = 2147483647u64; for i in 0..depth {
120 hash_functions.push(HashFunction {
121 a: (i as u64 * 2 + 1) * 1000000007,
122 b: (i as u64 + 1) * 1000000009,
123 p,
124 });
125 }
126
127 Self {
128 counters: vec![vec![0; width]; depth],
129 hash_functions,
130 width,
131 depth,
132 }
133 }
134
135 pub fn add<T: Hash>(&mut self, element: &T, count: u32) {
137 let hash = self.hash_element(element);
138
139 for (i, hash_func) in self.hash_functions.iter().enumerate() {
140 let index = hash_func.hash(hash) % self.width as u64;
141 self.counters[i][index as usize] += count;
142 }
143 }
144
145 pub fn estimate<T: Hash>(&self, element: &T) -> u32 {
147 let hash = self.hash_element(element);
148 let mut min_count = u32::MAX;
149
150 for (i, hash_func) in self.hash_functions.iter().enumerate() {
151 let index = hash_func.hash(hash) % self.width as u64;
152 min_count = min_count.min(self.counters[i][index as usize]);
153 }
154
155 min_count
156 }
157
158 pub fn merge(&mut self, other: &CountMinSketch) -> Result<()> {
160 if self.width != other.width || self.depth != other.depth {
161 return Err(crate::error::GraphError::graph_construction("Count-Min Sketch dimensions must match for merge"));
162 }
163
164 for i in 0..self.depth {
165 for j in 0..self.width {
166 self.counters[i][j] += other.counters[i][j];
167 }
168 }
169
170 Ok(())
171 }
172
173 fn hash_element<T: Hash>(&self, element: &T) -> u64 {
174 let mut hasher = std::collections::hash_map::DefaultHasher::new();
175 element.hash(&mut hasher);
176 hasher.finish()
177 }
178}
179
180impl HashFunction {
181 fn hash(&self, x: u64) -> u64 {
182 ((self.a.wrapping_mul(x).wrapping_add(self.b)) % self.p) % self.p
183 }
184}
185
186#[derive(Debug, Clone)]
189pub struct BloomFilter {
190 bit_array: Vec<bool>,
191 size: usize,
192 hash_count: u32,
193 element_count: usize,
194}
195
196impl BloomFilter {
197 pub fn new(expected_elements: usize, false_positive_rate: f64) -> Self {
201 let size = Self::optimal_size(expected_elements, false_positive_rate);
202 let hash_count = Self::optimal_hash_count(size, expected_elements);
203
204 Self {
205 bit_array: vec![false; size],
206 size,
207 hash_count,
208 element_count: 0,
209 }
210 }
211
212 pub fn add<T: Hash>(&mut self, element: &T) {
214 let hashes = self.hash_element(element);
215
216 for i in 0..self.hash_count {
217 let index = (hashes.0.wrapping_add((i as u64).wrapping_mul(hashes.1))) % self.size as u64;
218 self.bit_array[index as usize] = true;
219 }
220
221 self.element_count += 1;
222 }
223
224 pub fn contains<T: Hash>(&self, element: &T) -> bool {
228 let hashes = self.hash_element(element);
229
230 for i in 0..self.hash_count {
231 let index = (hashes.0.wrapping_add((i as u64).wrapping_mul(hashes.1))) % self.size as u64;
232 if !self.bit_array[index as usize] {
233 return false;
234 }
235 }
236
237 true
238 }
239
240 pub fn false_positive_probability(&self) -> f64 {
242 let filled_ratio = self.bit_array.iter().filter(|&&b| b).count() as f64 / self.size as f64;
243 filled_ratio.powf(self.hash_count as f64)
244 }
245
246 pub fn union(&mut self, other: &BloomFilter) -> Result<()> {
248 if self.size != other.size || self.hash_count != other.hash_count {
249 return Err(crate::error::GraphError::graph_construction("Bloom Filter parameters must match for union"));
250 }
251
252 for i in 0..self.size {
253 self.bit_array[i] |= other.bit_array[i];
254 }
255
256 self.element_count += other.element_count;
257 Ok(())
258 }
259
260 fn optimal_size(expected_elements: usize, false_positive_rate: f64) -> usize {
261 let m = -(expected_elements as f64 * false_positive_rate.ln()) / (2.0_f64.ln().powi(2));
262 m.ceil() as usize
263 }
264
265 fn optimal_hash_count(size: usize, expected_elements: usize) -> u32 {
266 let k = (size as f64 / expected_elements as f64) * 2.0_f64.ln();
267 k.round().max(1.0) as u32
268 }
269
270 fn hash_element<T: Hash>(&self, element: &T) -> (u64, u64) {
271 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
272 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
273
274 element.hash(&mut hasher1);
275 (element, 1u8).hash(&mut hasher2); (hasher1.finish(), hasher2.finish())
278 }
279}
280
281#[derive(Debug)]
284pub struct ApproximateGraphProcessor {
285 unique_nodes: HyperLogLog,
287 unique_edges: HyperLogLog,
288 unique_triangles: HyperLogLog,
289
290 node_degrees: CountMinSketch,
292 edge_weights: CountMinSketch,
293
294 active_nodes: BloomFilter,
296 recent_edges: BloomFilter,
297
298 config: ApproximateConfig,
300}
301
302#[derive(Debug, Clone)]
304pub struct ApproximateConfig {
305 pub hyperloglog_precision: u8,
306 pub count_min_width: usize,
307 pub count_min_depth: usize,
308 pub bloom_expected_elements: usize,
309 pub bloom_false_positive_rate: f64,
310}
311
312impl Default for ApproximateConfig {
313 fn default() -> Self {
314 Self {
315 hyperloglog_precision: 12, count_min_width: 1000,
317 count_min_depth: 5,
318 bloom_expected_elements: 10000,
319 bloom_false_positive_rate: 0.01, }
321 }
322}
323
324impl ApproximateGraphProcessor {
325 pub fn new(config: ApproximateConfig) -> Self {
327 Self {
328 unique_nodes: HyperLogLog::new(config.hyperloglog_precision),
329 unique_edges: HyperLogLog::new(config.hyperloglog_precision),
330 unique_triangles: HyperLogLog::new(config.hyperloglog_precision),
331 node_degrees: CountMinSketch::new(config.count_min_width, config.count_min_depth),
332 edge_weights: CountMinSketch::new(config.count_min_width, config.count_min_depth),
333 active_nodes: BloomFilter::new(config.bloom_expected_elements, config.bloom_false_positive_rate),
334 recent_edges: BloomFilter::new(config.bloom_expected_elements * 2, config.bloom_false_positive_rate),
335 config,
336 }
337 }
338
339 pub fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
341 let graph = processor.graph();
342 let nodes_batch = &graph.nodes;
343 let edges_batch = &graph.edges;
344
345 if nodes_batch.num_rows() > 0 {
347 let node_ids = nodes_batch.column(0)
348 .as_any()
349 .downcast_ref::<arrow::array::StringArray>()
350 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
351
352 for i in 0..node_ids.len() {
353 let node_id = node_ids.value(i);
354 self.unique_nodes.add(&node_id);
355 self.active_nodes.add(&node_id);
356 }
357 }
358
359 if edges_batch.num_rows() > 0 {
361 let source_ids = edges_batch.column(0)
362 .as_any()
363 .downcast_ref::<arrow::array::StringArray>()
364 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
365 let target_ids = edges_batch.column(1)
366 .as_any()
367 .downcast_ref::<arrow::array::StringArray>()
368 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
369 let weights = edges_batch.column(2)
370 .as_any()
371 .downcast_ref::<arrow::array::Float64Array>()
372 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected float64 array for weights"))?;
373
374 for i in 0..source_ids.len() {
375 let source = source_ids.value(i);
376 let target = target_ids.value(i);
377 let weight = weights.value(i);
378
379 let edge = (source, target);
380 self.unique_edges.add(&edge);
381 self.recent_edges.add(&edge);
382
383 self.node_degrees.add(&source, 1);
385 self.node_degrees.add(&target, 1);
386
387 let weight_key = format!("{}→{}", source, target);
389 self.edge_weights.add(&weight_key, (weight * 1000.0) as u32); }
391 }
392
393 Ok(())
394 }
395
396 pub fn update(&mut self, processor: &IncrementalGraphProcessor) -> Result<ApproximateMetrics> {
398 self.initialize(processor)?;
401
402 Ok(self.get_metrics())
403 }
404
405 pub fn get_metrics(&self) -> ApproximateMetrics {
407 ApproximateMetrics {
408 estimated_unique_nodes: self.unique_nodes.estimate(),
409 estimated_unique_edges: self.unique_edges.estimate(),
410 estimated_unique_triangles: self.unique_triangles.estimate(),
411 active_nodes_bloom_fill_ratio: self.active_nodes.false_positive_probability(),
412 recent_edges_bloom_fill_ratio: self.recent_edges.false_positive_probability(),
413 }
414 }
415
416 pub fn is_node_active(&self, node_id: &str) -> bool {
418 self.active_nodes.contains(&node_id)
419 }
420
421 pub fn is_edge_recent(&self, source: &str, target: &str) -> bool {
423 let edge = (source, target);
424 self.recent_edges.contains(&edge)
425 }
426
427 pub fn estimate_node_degree(&self, node_id: &str) -> u32 {
429 self.node_degrees.estimate(&node_id)
430 }
431
432 pub fn estimate_edge_weight(&self, source: &str, target: &str) -> f64 {
434 let weight_key = format!("{}→{}", source, target);
435 self.edge_weights.estimate(&weight_key) as f64 / 1000.0 }
437
438 pub fn merge(&mut self, other: &ApproximateGraphProcessor) -> Result<()> {
440 self.unique_nodes.merge(&other.unique_nodes)?;
441 self.unique_edges.merge(&other.unique_edges)?;
442 self.unique_triangles.merge(&other.unique_triangles)?;
443 self.node_degrees.merge(&other.node_degrees)?;
444 self.edge_weights.merge(&other.edge_weights)?;
445 self.active_nodes.union(&other.active_nodes)?;
446 self.recent_edges.union(&other.recent_edges)?;
447 Ok(())
448 }
449
450 pub fn add_triangle(&mut self, node_a: &str, node_b: &str, node_c: &str) {
452 let mut nodes = vec![node_a, node_b, node_c];
454 nodes.sort();
455 let triangle = format!("{}→{}→{}", nodes[0], nodes[1], nodes[2]);
456 self.unique_triangles.add(&triangle);
457 }
458
459 pub fn reset(&mut self) {
461 self.unique_nodes = HyperLogLog::new(self.config.hyperloglog_precision);
462 self.unique_edges = HyperLogLog::new(self.config.hyperloglog_precision);
463 self.unique_triangles = HyperLogLog::new(self.config.hyperloglog_precision);
464 self.node_degrees = CountMinSketch::new(self.config.count_min_width, self.config.count_min_depth);
465 self.edge_weights = CountMinSketch::new(self.config.count_min_width, self.config.count_min_depth);
466 self.active_nodes = BloomFilter::new(self.config.bloom_expected_elements, self.config.bloom_false_positive_rate);
467 self.recent_edges = BloomFilter::new(self.config.bloom_expected_elements * 2, self.config.bloom_false_positive_rate);
468 }
469}
470
471#[derive(Debug, Clone)]
473pub struct ApproximateMetrics {
474 pub estimated_unique_nodes: f64,
475 pub estimated_unique_edges: f64,
476 pub estimated_unique_triangles: f64,
477 pub active_nodes_bloom_fill_ratio: f64,
478 pub recent_edges_bloom_fill_ratio: f64,
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::graph::ArrowGraph;
485 use arrow::array::{StringArray, Float64Array};
486 use arrow::record_batch::RecordBatch;
487 use arrow::datatypes::{Schema, Field, DataType};
488 use std::sync::Arc;
489
490 #[test]
491 fn test_hyperloglog_basic() {
492 let mut hll = HyperLogLog::new(12);
493
494 for i in 0..1000 {
496 hll.add(&format!("element_{}", i));
497 }
498
499 let estimate = hll.estimate();
500
501 assert!(estimate > 950.0 && estimate < 1050.0);
503 }
504
505 #[test]
506 fn test_hyperloglog_merge() {
507 let mut hll1 = HyperLogLog::new(8);
508 let mut hll2 = HyperLogLog::new(8);
509
510 for i in 0..500 {
512 hll1.add(&format!("a_{}", i));
513 hll2.add(&format!("b_{}", i));
514 }
515
516 let estimate1 = hll1.estimate();
517 let estimate2 = hll2.estimate();
518
519 hll1.merge(&hll2).unwrap();
520 let merged_estimate = hll1.estimate();
521
522 assert!(merged_estimate > estimate1 + estimate2 - 100.0);
524 }
525
526 #[test]
527 fn test_count_min_sketch() {
528 let mut cms = CountMinSketch::new(100, 5);
529
530 cms.add(&"element1", 10);
532 cms.add(&"element2", 5);
533 cms.add(&"element1", 3); assert!(cms.estimate(&"element1") >= 13);
537 assert!(cms.estimate(&"element2") >= 5);
538 assert_eq!(cms.estimate(&"nonexistent"), 0);
539 }
540
541 #[test]
542 fn test_bloom_filter() {
543 let mut bloom = BloomFilter::new(1000, 0.01);
544
545 bloom.add(&"element1");
547 bloom.add(&"element2");
548 bloom.add(&"element3");
549
550 assert!(bloom.contains(&"element1"));
552 assert!(bloom.contains(&"element2"));
553 assert!(bloom.contains(&"element3"));
554
555 assert!(!bloom.contains(&"nonexistent"));
557 }
558
559 #[test]
560 fn test_bloom_filter_union() {
561 let mut bloom1 = BloomFilter::new(1000, 0.01);
562 let mut bloom2 = BloomFilter::new(1000, 0.01);
563
564 bloom1.add(&"element1");
565 bloom2.add(&"element2");
566
567 bloom1.union(&bloom2).unwrap();
568
569 assert!(bloom1.contains(&"element1"));
571 assert!(bloom1.contains(&"element2"));
572 }
573
574 fn create_test_graph() -> Result<ArrowGraph> {
575 let nodes_schema = Arc::new(Schema::new(vec![
576 Field::new("id", DataType::Utf8, false),
577 ]));
578 let node_ids = StringArray::from(vec!["A", "B", "C", "D"]);
579 let nodes_batch = RecordBatch::try_new(
580 nodes_schema,
581 vec![Arc::new(node_ids)],
582 )?;
583
584 let edges_schema = Arc::new(Schema::new(vec![
585 Field::new("source", DataType::Utf8, false),
586 Field::new("target", DataType::Utf8, false),
587 Field::new("weight", DataType::Float64, false),
588 ]));
589 let sources = StringArray::from(vec!["A", "B", "C"]);
590 let targets = StringArray::from(vec!["B", "C", "D"]);
591 let weights = Float64Array::from(vec![1.0, 2.0, 3.0]);
592 let edges_batch = RecordBatch::try_new(
593 edges_schema,
594 vec![Arc::new(sources), Arc::new(targets), Arc::new(weights)],
595 )?;
596
597 ArrowGraph::new(nodes_batch, edges_batch)
598 }
599
600 #[test]
601 fn test_approximate_graph_processor() {
602 let graph = create_test_graph().unwrap();
603 let processor = IncrementalGraphProcessor::new(graph).unwrap();
604
605 let mut approx = ApproximateGraphProcessor::new(ApproximateConfig::default());
606 approx.initialize(&processor).unwrap();
607
608 let metrics = approx.get_metrics();
609
610 assert!(metrics.estimated_unique_nodes > 3.0 && metrics.estimated_unique_nodes < 5.0);
612 assert!(metrics.estimated_unique_edges > 2.0 && metrics.estimated_unique_edges < 4.0);
613 }
614
615 #[test]
616 fn test_node_and_edge_queries() {
617 let graph = create_test_graph().unwrap();
618 let processor = IncrementalGraphProcessor::new(graph).unwrap();
619
620 let mut approx = ApproximateGraphProcessor::new(ApproximateConfig::default());
621 approx.initialize(&processor).unwrap();
622
623 assert!(approx.is_node_active("A"));
625 assert!(approx.is_node_active("B"));
626 assert!(!approx.is_node_active("nonexistent")); assert!(approx.is_edge_recent("A", "B"));
630 assert!(approx.is_edge_recent("B", "C"));
631
632 assert!(approx.estimate_node_degree("A") > 0);
634 assert!(approx.estimate_node_degree("B") > 0);
635 }
636
637 #[test]
638 fn test_triangle_counting() {
639 let mut approx = ApproximateGraphProcessor::new(ApproximateConfig::default());
640
641 approx.add_triangle("A", "B", "C");
643 approx.add_triangle("B", "C", "D");
644 approx.add_triangle("A", "B", "C"); let metrics = approx.get_metrics();
647
648 assert!(metrics.estimated_unique_triangles > 1.5 && metrics.estimated_unique_triangles < 2.5);
650 }
651
652 #[test]
653 fn test_processor_merge() {
654 let config = ApproximateConfig::default();
655
656 let mut approx1 = ApproximateGraphProcessor::new(config.clone());
657 let mut approx2 = ApproximateGraphProcessor::new(config);
658
659 approx1.unique_nodes.add(&"A");
661 approx1.unique_nodes.add(&"B");
662
663 approx2.unique_nodes.add(&"C");
664 approx2.unique_nodes.add(&"D");
665
666 let estimate1 = approx1.get_metrics().estimated_unique_nodes;
667 let estimate2 = approx2.get_metrics().estimated_unique_nodes;
668
669 approx1.merge(&approx2).unwrap();
670 let merged_estimate = approx1.get_metrics().estimated_unique_nodes;
671
672 assert!(merged_estimate > estimate1);
674 assert!(merged_estimate > estimate2);
675 }
676}