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