1use datadog_protos::sketches::DDSketch as ProtoDDSketch;
4
5use super::error::ProtoConversionError;
6use super::mapping::{IndexMapping, LogarithmicMapping};
7use super::store::{CollapsingLowestDenseStore, Store};
8
9#[derive(Clone, Debug)]
33pub struct DDSketch<M: IndexMapping = LogarithmicMapping, S: Store = CollapsingLowestDenseStore> {
34 mapping: M,
36
37 positive_store: S,
39
40 negative_store: S,
42
43 zero_count: u64,
45}
46
47impl DDSketch<LogarithmicMapping, CollapsingLowestDenseStore> {
48 pub fn with_relative_accuracy(relative_accuracy: f64) -> Result<Self, &'static str> {
56 let mapping = LogarithmicMapping::new(relative_accuracy)?;
57 Ok(Self::new(
58 mapping,
59 CollapsingLowestDenseStore::default(),
60 CollapsingLowestDenseStore::default(),
61 ))
62 }
63}
64
65impl<M: IndexMapping, S: Store> DDSketch<M, S> {
66 pub fn new(mapping: M, positive_store: S, negative_store: S) -> Self {
68 Self {
69 mapping,
70 positive_store,
71 negative_store,
72 zero_count: 0,
73 }
74 }
75
76 pub fn add(&mut self, value: f64) {
78 self.add_n(value, 1);
79 }
80
81 pub fn add_n(&mut self, value: f64, n: u64) {
85 if n == 0 {
86 return;
87 }
88
89 if value > self.mapping.min_indexable_value() {
90 let index = self.mapping.index(value);
91 self.positive_store.add(index, n);
92 } else if value < -self.mapping.min_indexable_value() {
93 let index = self.mapping.index(-value);
94 self.negative_store.add(index, n);
95 } else {
96 self.zero_count += n;
97 }
98 }
99
100 pub fn quantile(&self, q: f64) -> Option<f64> {
107 if self.is_empty() {
108 return None;
109 }
110
111 if !(0.0..=1.0).contains(&q) {
112 return None;
113 }
114
115 let rank = (q * (self.count() - 1) as f64).round_ties_even() as u64;
116
117 let negative_count = self.negative_store.total_count();
118 let total_negative_and_zero = negative_count + self.zero_count;
119
120 if rank < negative_count {
121 let reverse_rank = negative_count - rank - 1;
123 if let Some(index) = self.negative_store.key_at_rank(reverse_rank) {
124 return Some(-self.mapping.value(index));
125 }
126 } else if rank < total_negative_and_zero {
127 return Some(0.0);
128 } else {
129 let positive_rank = rank - total_negative_and_zero;
130 if let Some(index) = self.positive_store.key_at_rank(positive_rank) {
131 return Some(self.mapping.value(index));
132 }
133 }
134
135 unreachable!("rank out of bounds on non-empty sketch")
136 }
137
138 pub fn merge(&mut self, other: &Self)
142 where
143 M: PartialEq,
144 {
145 if other.is_empty() {
146 return;
147 }
148
149 self.positive_store.merge(&other.positive_store);
150 self.negative_store.merge(&other.negative_store);
151 self.zero_count += other.zero_count;
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.count() == 0
157 }
158
159 pub fn count(&self) -> u64 {
161 self.negative_store().total_count() + self.positive_store().total_count() + self.zero_count
162 }
163
164 pub fn clear(&mut self) {
166 self.positive_store.clear();
167 self.negative_store.clear();
168 self.zero_count = 0;
169 }
170
171 pub fn mapping(&self) -> &M {
173 &self.mapping
174 }
175
176 pub fn positive_store(&self) -> &S {
178 &self.positive_store
179 }
180
181 pub fn negative_store(&self) -> &S {
183 &self.negative_store
184 }
185
186 pub fn zero_count(&self) -> u64 {
188 self.zero_count
189 }
190
191 pub fn relative_accuracy(&self) -> f64 {
193 self.mapping.relative_accuracy()
194 }
195
196 pub fn from_proto(proto: &ProtoDDSketch, mapping: M) -> Result<Self, ProtoConversionError>
221 where
222 S: Default,
223 {
224 let proto_mapping = proto.mapping.as_ref().ok_or(ProtoConversionError::MissingMapping)?;
226 mapping.validate_proto_mapping(proto_mapping)?;
227
228 let zero_count = if proto.zeroCount < 0.0 {
230 return Err(ProtoConversionError::NegativeZeroCount { count: proto.zeroCount });
231 } else if proto.zeroCount.fract() != 0.0 {
232 return Err(ProtoConversionError::NonIntegerZeroCount { count: proto.zeroCount });
233 } else {
234 proto.zeroCount as u64
235 };
236
237 let mut positive_store = S::default();
238 if let Some(proto_positive) = proto.positiveValues.as_ref() {
239 positive_store.merge_from_proto(proto_positive)?;
240 }
241
242 let mut negative_store = S::default();
243 if let Some(proto_negative) = proto.negativeValues.as_ref() {
244 negative_store.merge_from_proto(proto_negative)?;
245 }
246
247 Ok(Self {
248 mapping,
249 positive_store,
250 negative_store,
251 zero_count,
252 })
253 }
254
255 pub fn to_proto(&self) -> ProtoDDSketch {
262 let mut proto = ProtoDDSketch::new();
263
264 proto.set_mapping(self.mapping.to_proto());
265
266 if !self.positive_store().is_empty() {
267 proto.set_positiveValues(self.positive_store.to_proto());
268 }
269
270 if !self.negative_store().is_empty() {
271 proto.set_negativeValues(self.negative_store.to_proto());
272 }
273
274 proto.set_zeroCount(self.zero_count as f64);
275
276 proto
277 }
278}
279
280impl<M: IndexMapping + PartialEq, S: Store + PartialEq> PartialEq for DDSketch<M, S> {
281 fn eq(&self, other: &Self) -> bool {
282 self.mapping == other.mapping
283 && self.positive_store == other.positive_store
284 && self.negative_store == other.negative_store
285 && self.zero_count == other.zero_count
286 }
287}
288
289impl<M: IndexMapping + PartialEq, S: Store + PartialEq> Eq for DDSketch<M, S> {}
290
291impl<M: IndexMapping + Default, S: Store + Default> Default for DDSketch<M, S> {
292 fn default() -> Self {
293 Self::new(M::default(), S::default(), S::default())
294 }
295}
296
297#[derive(Clone, Debug)]
321pub struct PositiveOnlyDDSketch<M: IndexMapping = LogarithmicMapping, S: Store = CollapsingLowestDenseStore> {
322 mapping: M,
324
325 positive_store: S,
327
328 zero_count: u64,
330}
331
332impl<M: IndexMapping, S: Store> PositiveOnlyDDSketch<M, S> {
333 pub fn new(mapping: M, positive_store: S) -> Self {
335 Self {
336 mapping,
337 positive_store,
338 zero_count: 0,
339 }
340 }
341
342 #[inline]
346 pub fn add(&mut self, value: f64) {
347 self.add_n(value, 1);
348 }
349
350 #[inline]
354 pub fn add_n(&mut self, value: f64, n: u64) {
355 if n == 0 {
356 return;
357 }
358
359 if value > self.mapping.min_indexable_value() {
360 let index = self.mapping.index(value);
361 self.positive_store.add(index, n);
362 } else {
363 self.zero_count += n;
365 }
366 }
367
368 pub fn quantile(&self, q: f64) -> Option<f64> {
374 if self.is_empty() {
375 return None;
376 }
377
378 if !(0.0..=1.0).contains(&q) {
379 return None;
380 }
381
382 let rank = (q * (self.count() - 1) as f64).round_ties_even() as u64;
383
384 if rank < self.zero_count {
385 return Some(0.0);
386 }
387
388 let positive_rank = rank - self.zero_count;
389 if let Some(index) = self.positive_store.key_at_rank(positive_rank) {
390 return Some(self.mapping.value(index));
391 }
392
393 unreachable!("rank out of bounds on non-empty sketch")
394 }
395
396 pub fn merge(&mut self, other: &Self)
400 where
401 M: PartialEq,
402 {
403 if other.is_empty() {
404 return;
405 }
406
407 self.positive_store.merge(&other.positive_store);
408 self.zero_count += other.zero_count;
409 }
410
411 #[inline]
413 pub fn is_empty(&self) -> bool {
414 self.count() == 0
415 }
416
417 #[inline]
419 pub fn count(&self) -> u64 {
420 self.positive_store.total_count() + self.zero_count
421 }
422
423 pub fn clear(&mut self) {
425 self.positive_store.clear();
426 self.zero_count = 0;
427 }
428
429 pub fn mapping(&self) -> &M {
431 &self.mapping
432 }
433
434 pub fn positive_store(&self) -> &S {
436 &self.positive_store
437 }
438
439 pub fn zero_count(&self) -> u64 {
441 self.zero_count
442 }
443
444 pub fn relative_accuracy(&self) -> f64 {
446 self.mapping.relative_accuracy()
447 }
448
449 pub fn from_proto(proto: &ProtoDDSketch, mapping: M) -> Result<Self, ProtoConversionError>
463 where
464 S: Default,
465 {
466 let proto_mapping = proto.mapping.as_ref().ok_or(ProtoConversionError::MissingMapping)?;
468 mapping.validate_proto_mapping(proto_mapping)?;
469
470 let zero_count = if proto.zeroCount < 0.0 {
472 return Err(ProtoConversionError::NegativeZeroCount { count: proto.zeroCount });
473 } else if proto.zeroCount.fract() != 0.0 {
474 return Err(ProtoConversionError::NonIntegerZeroCount { count: proto.zeroCount });
475 } else {
476 proto.zeroCount as u64
477 };
478
479 let mut positive_store = S::default();
480 if let Some(proto_positive) = proto.positiveValues.as_ref() {
481 positive_store.merge_from_proto(proto_positive)?;
482 }
483
484 Ok(Self {
487 mapping,
488 positive_store,
489 zero_count,
490 })
491 }
492
493 pub fn to_proto(&self) -> ProtoDDSketch {
497 let mut proto = ProtoDDSketch::new();
498
499 proto.set_mapping(self.mapping.to_proto());
500
501 if !self.positive_store.is_empty() {
502 proto.set_positiveValues(self.positive_store.to_proto());
503 }
504
505 proto.set_zeroCount(self.zero_count as f64);
508
509 proto
510 }
511}
512
513impl<M: IndexMapping + PartialEq, S: Store + PartialEq> PartialEq for PositiveOnlyDDSketch<M, S> {
514 fn eq(&self, other: &Self) -> bool {
515 self.mapping == other.mapping
516 && self.positive_store == other.positive_store
517 && self.zero_count == other.zero_count
518 }
519}
520
521impl<M: IndexMapping + PartialEq, S: Store + PartialEq> Eq for PositiveOnlyDDSketch<M, S> {}
522
523impl<M: IndexMapping + Default, S: Store + Default> Default for PositiveOnlyDDSketch<M, S> {
524 fn default() -> Self {
525 Self::new(M::default(), S::default())
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use ndarray::{Array, Axis};
532 use ndarray_stats::{
533 interpolate::{Higher, Lower},
534 QuantileExt,
535 };
536 use noisy_float::types::N64;
537 use num_traits::ToPrimitive as _;
538
539 use super::*;
540
541 macro_rules! assert_rel_acc_range_eq {
542 ($quantile:expr, $rel_acc:expr, $expected_lower:expr, $expected_upper:expr, $actual:expr) => {{
543 let expected_lower_f64 = $expected_lower.to_f64().unwrap();
544 let expected_lower_adj = if expected_lower_f64 > 0.0 {
545 expected_lower_f64 * (1.0 - $rel_acc)
546 } else {
547 expected_lower_f64 * (1.0 + $rel_acc)
548 };
549 let expected_upper_f64 = $expected_upper.to_f64().unwrap();
550 let expected_upper_adj = if expected_upper_f64 > 0.0 {
551 expected_upper_f64 * (1.0 + $rel_acc)
552 } else {
553 expected_upper_f64 * (1.0 - $rel_acc)
554 };
555 let actual = $actual.to_f64().unwrap();
556
557 assert!(
572 actual >= expected_lower_adj && actual <= expected_upper_adj,
573 "mismatch at q={}: expected {} - {} ({}% relative accuracy), got {}",
574 $quantile,
575 expected_lower_adj,
576 expected_upper_adj,
577 $rel_acc * 100.0,
578 actual
579 );
580 }};
581 }
582
583 macro_rules! assert_rel_acc_eq {
584 ($quantile:expr, $rel_acc:expr, $expected:expr, $actual:expr) => {{
585 assert_rel_acc_range_eq!($quantile, $rel_acc, $expected, $expected, $actual);
586 }};
587 }
588
589 struct Dataset<M: IndexMapping, S: Store> {
590 raw_data: Vec<N64>,
591 sketch: DDSketch<M, S>,
592 }
593
594 impl<M: IndexMapping, S: Store + Default> Dataset<M, S> {
595 fn new<V>(index_mapping: M, values: V) -> Self
596 where
597 V: Iterator<Item = f64>,
598 {
599 let mut raw_data = Vec::new();
600 let mut sketch = DDSketch::new(index_mapping, S::default(), S::default());
601 for value in values {
602 raw_data.push(N64::new(value));
603 sketch.add(value);
604 }
605
606 Self { raw_data, sketch }
607 }
608
609 #[track_caller]
610 fn validate(self, quantiles: &[f64]) {
611 let Self { mut raw_data, sketch } = self;
612
613 assert_eq!(raw_data.len() as u64, sketch.count());
615
616 raw_data.sort();
618
619 let mut data = Array::from_vec(raw_data);
620 let rel_acc = sketch.relative_accuracy();
621
622 for q in quantiles {
624 let expected_lower = data
625 .quantile_axis_mut(Axis(0), N64::new(*q), &Lower)
626 .map(|v| v.into_scalar())
627 .ok();
628 let expected_upper = data
629 .quantile_axis_mut(Axis(0), N64::new(*q), &Higher)
630 .map(|v| v.into_scalar())
631 .ok();
632 let actual = sketch.quantile(*q).map(N64::new);
633
634 match (expected_lower, expected_upper, actual) {
635 (Some(expected_lower), Some(expected_upper), Some(actual)) => {
636 assert_rel_acc_range_eq!(q, rel_acc, expected_lower, expected_upper, actual);
645 }
646 (None, None, None) => (),
647 _ => panic!(
648 "mismatched quantiles: expected_lower={:?}, expected_upper={:?}, actual {:?}",
649 expected_lower, expected_upper, actual
650 ),
651 }
652 }
653 }
654 }
655
656 fn integers(start: i64, end: i64) -> impl Iterator<Item = f64> {
657 (start..=end).map(|x| x as f64)
658 }
659
660 #[test]
661 fn test_empty_sketch() {
662 let sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
663
664 assert!(sketch.is_empty());
665 assert_eq!(sketch.count(), 0);
666 assert_eq!(sketch.quantile(0.5), None);
667 }
668
669 #[test]
670 fn test_accuracy_integers_positive_only_even_small() {
671 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
672 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 10));
673 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
674 }
675
676 #[test]
677 fn test_accuracy_integers_positive_only_even_medium() {
678 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
679 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 250));
680 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
681 }
682
683 #[test]
684 fn test_accuracy_integers_positive_only_even_large() {
685 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
686 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 1000));
687 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
688 }
689
690 #[test]
691 fn test_accuracy_integers_positive_only_odd_small() {
692 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
693 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 11));
694 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
695 }
696
697 #[test]
698 fn test_accuracy_integers_positive_only_odd_medium() {
699 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
700 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 293));
701 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
702 }
703
704 #[test]
705 fn test_accuracy_integers_positive_only_odd_large() {
706 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
707 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 1023));
708 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
709 }
710
711 #[test]
712 fn test_accuracy_integers_negative_only_even_small() {
713 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
714 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-10, -1));
715 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
716 }
717
718 #[test]
719 fn test_accuracy_integers_negative_only_even_medium() {
720 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
721 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-250, -1));
722 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
723 }
724
725 #[test]
726 fn test_accuracy_integers_negative_only_even_large() {
727 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
728 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-1000, -1));
729 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
730 }
731
732 #[test]
733 fn test_accuracy_integers_negative_only_odd_small() {
734 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
735 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-11, -1));
736 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
737 }
738
739 #[test]
740 fn test_accuracy_integers_negative_only_odd_medium() {
741 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
742 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-293, -1));
743 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
744 }
745
746 #[test]
747 fn test_accuracy_integers_negative_only_odd_large() {
748 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
749 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-1023, -1));
750 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
751 }
752
753 #[test]
754 fn test_zero_values() {
755 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
756 sketch.add(0.0);
757 sketch.add(0.0);
758 sketch.add(1.0);
759
760 assert_eq!(sketch.count(), 3);
761 assert_eq!(sketch.zero_count(), 2);
762 }
763
764 #[test]
765 fn test_merge() {
766 let mut sketch1 = DDSketch::with_relative_accuracy(0.01).unwrap();
767 sketch1.add(1.0);
768 sketch1.add(2.0);
769
770 let mut sketch2 = DDSketch::with_relative_accuracy(0.01).unwrap();
771 sketch2.add(3.0);
772 sketch2.add(4.0);
773
774 sketch1.merge(&sketch2);
775
776 assert_eq!(sketch1.count(), 4);
777 }
778
779 #[test]
780 fn test_clear() {
781 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
782 sketch.add(1.0);
783 sketch.add(2.0);
784
785 sketch.clear();
786
787 assert!(sketch.is_empty());
788 assert_eq!(sketch.count(), 0);
789 }
790
791 #[test]
792 #[ignore]
793 fn test_quantile_bounds() {
794 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
795 for i in 1..=100 {
796 sketch.add(i as f64);
797 }
798
799 let min_actual = sketch.quantile(0.0).unwrap();
801 assert_rel_acc_eq!(0.0, 0.01, 1.0, min_actual);
802
803 let max_actual = sketch.quantile(1.0).unwrap();
805 assert_rel_acc_eq!(1.0, 0.01, 100.0, max_actual);
806 }
807
808 #[test]
809 fn test_add_n() {
810 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
811 sketch.add_n(10.0, 5);
812
813 assert_eq!(sketch.count(), 5);
814 }
815
816 #[test]
817 fn test_proto_roundtrip() {
818 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
819 sketch.add(1.0);
820 sketch.add(2.0);
821 sketch.add(3.0);
822 sketch.add(100.0);
823
824 let proto = sketch.to_proto();
825 let mapping = LogarithmicMapping::new(0.01).unwrap();
826 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
827
828 assert_eq!(sketch.count(), recovered.count());
830 assert_eq!(sketch.zero_count(), recovered.zero_count());
831
832 for q in [0.25, 0.5, 0.75, 0.99] {
834 let orig = sketch.quantile(q).unwrap();
835 let recov = recovered.quantile(q).unwrap();
836 assert!(
837 (orig - recov).abs() < 0.001,
838 "quantile {} mismatch: {} vs {}",
839 q,
840 orig,
841 recov
842 );
843 }
844 }
845
846 #[test]
847 fn test_proto_roundtrip_with_negatives() {
848 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
849 sketch.add(-10.0);
850 sketch.add(-5.0);
851 sketch.add(0.0);
852 sketch.add(5.0);
853 sketch.add(10.0);
854
855 let proto = sketch.to_proto();
856 let mapping = LogarithmicMapping::new(0.01).unwrap();
857 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
858
859 assert_eq!(sketch.count(), recovered.count());
860 assert_eq!(sketch.zero_count(), recovered.zero_count());
861 }
862
863 #[test]
864 fn test_proto_roundtrip_empty() {
865 let sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
866
867 let proto = sketch.to_proto();
868 let mapping = LogarithmicMapping::new(0.01).unwrap();
869 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
870
871 assert!(recovered.is_empty());
872 assert_eq!(recovered.count(), 0);
873 }
874
875 #[test]
876 fn test_proto_gamma_mismatch() {
877 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
878 sketch.add(1.0);
879
880 let proto = sketch.to_proto();
881
882 let different_mapping = LogarithmicMapping::new(0.05).unwrap();
884 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, different_mapping);
885
886 assert!(result.is_err());
887 match result {
888 Err(crate::canonical::ProtoConversionError::GammaMismatch { .. }) => {}
889 _ => panic!("Expected GammaMismatch error"),
890 }
891 }
892
893 #[test]
894 fn test_proto_missing_mapping() {
895 use datadog_protos::sketches::DDSketch as ProtoDDSketch;
896
897 let proto = ProtoDDSketch::new(); let mapping = LogarithmicMapping::new(0.01).unwrap();
899 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
900
901 assert!(result.is_err());
902 match result {
903 Err(crate::canonical::ProtoConversionError::MissingMapping) => {}
904 _ => panic!("Expected MissingMapping error"),
905 }
906 }
907
908 #[test]
911 fn test_positive_only_size() {
912 use crate::canonical::mapping::FixedLogarithmicMapping;
913
914 let ddsketch_size = std::mem::size_of::<DDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
916 let positive_only_size =
917 std::mem::size_of::<PositiveOnlyDDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
918
919 assert!(
921 ddsketch_size > positive_only_size,
922 "PositiveOnlyDDSketch ({} bytes) should be smaller than DDSketch ({} bytes)",
923 positive_only_size,
924 ddsketch_size
925 );
926 assert_eq!(
927 ddsketch_size - positive_only_size,
928 48,
929 "Expected 48 bytes savings from removing negative store"
930 );
931 }
932
933 #[test]
934 fn test_positive_only_empty() {
935 let sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
936
937 assert!(sketch.is_empty());
938 assert_eq!(sketch.count(), 0);
939 assert_eq!(sketch.quantile(0.5), None);
940 }
941
942 #[test]
943 fn test_positive_only_add_and_quantile() {
944 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
945
946 for i in 1..=100 {
947 sketch.add(i as f64);
948 }
949
950 assert_eq!(sketch.count(), 100);
951
952 let median = sketch.quantile(0.5).unwrap();
954 assert!((median - 50.0).abs() < 2.0, "median {} should be close to 50", median);
955 }
956
957 #[test]
958 fn test_positive_only_negative_values_become_zero() {
959 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
960
961 sketch.add(-10.0);
962 sketch.add(-5.0);
963 sketch.add(0.0);
964 sketch.add(5.0);
965 sketch.add(10.0);
966
967 assert_eq!(sketch.count(), 5);
968 assert_eq!(sketch.zero_count(), 3);
970 }
971
972 #[test]
973 fn test_positive_only_merge() {
974 let mut sketch1: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
975 sketch1.add(1.0);
976 sketch1.add(2.0);
977
978 let mut sketch2: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
979 sketch2.add(3.0);
980 sketch2.add(4.0);
981
982 sketch1.merge(&sketch2);
983
984 assert_eq!(sketch1.count(), 4);
985 }
986
987 #[test]
988 fn test_positive_only_clear() {
989 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
990 sketch.add(1.0);
991 sketch.add(2.0);
992
993 sketch.clear();
994
995 assert!(sketch.is_empty());
996 assert_eq!(sketch.count(), 0);
997 }
998
999 #[test]
1000 fn test_positive_only_proto_roundtrip() {
1001 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1002 sketch.add(1.0);
1003 sketch.add(2.0);
1004 sketch.add(3.0);
1005 sketch.add(100.0);
1006
1007 let proto = sketch.to_proto();
1008
1009 assert!(proto.negativeValues.is_none());
1011
1012 let mapping = LogarithmicMapping::new(0.01).unwrap();
1013 let recovered: PositiveOnlyDDSketch = PositiveOnlyDDSketch::from_proto(&proto, mapping).unwrap();
1014
1015 assert_eq!(sketch.count(), recovered.count());
1016 assert_eq!(sketch.zero_count(), recovered.zero_count());
1017
1018 for q in [0.25, 0.5, 0.75, 0.99] {
1020 let orig = sketch.quantile(q).unwrap();
1021 let recov = recovered.quantile(q).unwrap();
1022 assert!(
1023 (orig - recov).abs() < 0.01,
1024 "quantile {} mismatch: {} vs {}",
1025 q,
1026 orig,
1027 recov
1028 );
1029 }
1030 }
1031
1032 #[test]
1033 fn test_positive_only_matches_ddsketch_for_positive_values() {
1034 let mut ddsketch: DDSketch = DDSketch::default();
1036 let mut positive_only: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1037
1038 for i in 1..=1000 {
1039 let value = i as f64;
1040 ddsketch.add(value);
1041 positive_only.add(value);
1042 }
1043
1044 assert_eq!(ddsketch.count(), positive_only.count());
1045
1046 for q in [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1048 let dd_quantile = ddsketch.quantile(q).unwrap();
1049 let po_quantile = positive_only.quantile(q).unwrap();
1050 assert!(
1051 (dd_quantile - po_quantile).abs() < 0.001,
1052 "quantile {} mismatch: DDSketch={}, PositiveOnly={}",
1053 q,
1054 dd_quantile,
1055 po_quantile
1056 );
1057 }
1058 }
1059}