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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn quantile_at_zero_and_one_returns_min_and_max() {
793 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
797 for i in 1..=100 {
798 sketch.add(i as f64);
799 }
800
801 let min_actual = sketch.quantile(0.0).unwrap();
803 assert_rel_acc_eq!(0.0, 0.01, 1.0, min_actual);
804
805 let max_actual = sketch.quantile(1.0).unwrap();
807 assert_rel_acc_eq!(1.0, 0.01, 100.0, max_actual);
808 }
809
810 #[test]
811 fn add_n() {
812 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
813 sketch.add_n(10.0, 5);
814
815 assert_eq!(sketch.count(), 5);
816 }
817
818 #[test]
819 fn proto_roundtrip() {
820 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
821 sketch.add(1.0);
822 sketch.add(2.0);
823 sketch.add(3.0);
824 sketch.add(100.0);
825
826 let proto = sketch.to_proto();
827 let mapping = LogarithmicMapping::new(0.01).unwrap();
828 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
829
830 assert_eq!(sketch.count(), recovered.count());
832 assert_eq!(sketch.zero_count(), recovered.zero_count());
833
834 for q in [0.25, 0.5, 0.75, 0.99] {
836 let orig = sketch.quantile(q).unwrap();
837 let recov = recovered.quantile(q).unwrap();
838 assert!(
839 (orig - recov).abs() < 0.001,
840 "quantile {} mismatch: {} vs {}",
841 q,
842 orig,
843 recov
844 );
845 }
846 }
847
848 #[test]
849 fn proto_roundtrip_with_negatives() {
850 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
851 sketch.add(-10.0);
852 sketch.add(-5.0);
853 sketch.add(0.0);
854 sketch.add(5.0);
855 sketch.add(10.0);
856
857 let proto = sketch.to_proto();
858 let mapping = LogarithmicMapping::new(0.01).unwrap();
859 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
860
861 assert_eq!(sketch.count(), recovered.count());
862 assert_eq!(sketch.zero_count(), recovered.zero_count());
863 }
864
865 #[test]
866 fn proto_roundtrip_empty() {
867 let sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
868
869 let proto = sketch.to_proto();
870 let mapping = LogarithmicMapping::new(0.01).unwrap();
871 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
872
873 assert!(recovered.is_empty());
874 assert_eq!(recovered.count(), 0);
875 }
876
877 #[test]
878 fn proto_gamma_mismatch() {
879 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
880 sketch.add(1.0);
881
882 let proto = sketch.to_proto();
883
884 let different_mapping = LogarithmicMapping::new(0.05).unwrap();
886 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, different_mapping);
887
888 assert!(result.is_err());
889 match result {
890 Err(crate::canonical::ProtoConversionError::GammaMismatch { .. }) => {}
891 _ => panic!("Expected GammaMismatch error"),
892 }
893 }
894
895 #[test]
896 fn proto_missing_mapping() {
897 use datadog_protos::sketches::DDSketch as ProtoDDSketch;
898
899 let proto = ProtoDDSketch::new(); let mapping = LogarithmicMapping::new(0.01).unwrap();
901 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
902
903 assert!(result.is_err());
904 match result {
905 Err(crate::canonical::ProtoConversionError::MissingMapping) => {}
906 _ => panic!("Expected MissingMapping error"),
907 }
908 }
909
910 #[test]
911 fn proto_non_zero_index_offset() {
912 use datadog_protos::sketches::DDSketch as ProtoDDSketch;
913
914 let mapping = LogarithmicMapping::new(0.01).unwrap();
918 let mut proto_mapping = mapping.to_proto();
919 proto_mapping.indexOffset = 1.0;
920
921 let mut proto = ProtoDDSketch::new();
922 proto.set_mapping(proto_mapping);
923
924 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
925 match result {
926 Err(crate::canonical::ProtoConversionError::NonZeroIndexOffset { actual }) => {
927 assert_eq!(actual, 1.0);
928 }
929 other => panic!("Expected NonZeroIndexOffset error, got {:?}", other),
930 }
931 }
932
933 #[test]
934 fn proto_unsupported_interpolation() {
935 use datadog_protos::sketches::{index_mapping::Interpolation, DDSketch as ProtoDDSketch};
936
937 let mapping = LogarithmicMapping::new(0.01).unwrap();
941 let mut proto_mapping = mapping.to_proto();
942 proto_mapping.interpolation = protobuf::EnumOrUnknown::new(Interpolation::LINEAR);
943
944 let mut proto = ProtoDDSketch::new();
945 proto.set_mapping(proto_mapping);
946
947 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
948 match result {
949 Err(crate::canonical::ProtoConversionError::UnsupportedInterpolation { actual }) => {
950 assert_eq!(actual, Interpolation::LINEAR as i32);
951 }
952 other => panic!("Expected UnsupportedInterpolation error, got {:?}", other),
953 }
954 }
955
956 #[test]
959 fn positive_only_size() {
960 use crate::canonical::mapping::FixedLogarithmicMapping;
961
962 let ddsketch_size = std::mem::size_of::<DDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
964 let positive_only_size =
965 std::mem::size_of::<PositiveOnlyDDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
966
967 assert!(
969 ddsketch_size > positive_only_size,
970 "PositiveOnlyDDSketch ({} bytes) should be smaller than DDSketch ({} bytes)",
971 positive_only_size,
972 ddsketch_size
973 );
974 assert_eq!(
975 ddsketch_size - positive_only_size,
976 48,
977 "Expected 48 bytes savings from removing negative store"
978 );
979 }
980
981 #[test]
982 fn positive_only_empty() {
983 let sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
984
985 assert!(sketch.is_empty());
986 assert_eq!(sketch.count(), 0);
987 assert_eq!(sketch.quantile(0.5), None);
988 }
989
990 #[test]
991 fn positive_only_add_and_quantile() {
992 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
993
994 for i in 1..=100 {
995 sketch.add(i as f64);
996 }
997
998 assert_eq!(sketch.count(), 100);
999
1000 let median = sketch.quantile(0.5).unwrap();
1002 assert!((median - 50.0).abs() < 2.0, "median {} should be close to 50", median);
1003 }
1004
1005 #[test]
1006 fn positive_only_negative_values_become_zero() {
1007 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1008
1009 sketch.add(-10.0);
1010 sketch.add(-5.0);
1011 sketch.add(0.0);
1012 sketch.add(5.0);
1013 sketch.add(10.0);
1014
1015 assert_eq!(sketch.count(), 5);
1016 assert_eq!(sketch.zero_count(), 3);
1018 }
1019
1020 #[test]
1021 fn positive_only_merge() {
1022 let mut sketch1: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1023 sketch1.add(1.0);
1024 sketch1.add(2.0);
1025
1026 let mut sketch2: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1027 sketch2.add(3.0);
1028 sketch2.add(4.0);
1029
1030 sketch1.merge(&sketch2);
1031
1032 assert_eq!(sketch1.count(), 4);
1033 }
1034
1035 #[test]
1036 fn positive_only_clear() {
1037 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1038 sketch.add(1.0);
1039 sketch.add(2.0);
1040
1041 sketch.clear();
1042
1043 assert!(sketch.is_empty());
1044 assert_eq!(sketch.count(), 0);
1045 }
1046
1047 #[test]
1048 fn positive_only_proto_roundtrip() {
1049 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1050 sketch.add(1.0);
1051 sketch.add(2.0);
1052 sketch.add(3.0);
1053 sketch.add(100.0);
1054
1055 let proto = sketch.to_proto();
1056
1057 assert!(proto.negativeValues.is_none());
1059
1060 let mapping = LogarithmicMapping::new(0.01).unwrap();
1061 let recovered: PositiveOnlyDDSketch = PositiveOnlyDDSketch::from_proto(&proto, mapping).unwrap();
1062
1063 assert_eq!(sketch.count(), recovered.count());
1064 assert_eq!(sketch.zero_count(), recovered.zero_count());
1065
1066 for q in [0.25, 0.5, 0.75, 0.99] {
1068 let orig = sketch.quantile(q).unwrap();
1069 let recov = recovered.quantile(q).unwrap();
1070 assert!(
1071 (orig - recov).abs() < 0.01,
1072 "quantile {} mismatch: {} vs {}",
1073 q,
1074 orig,
1075 recov
1076 );
1077 }
1078 }
1079
1080 #[test]
1081 fn positive_only_matches_ddsketch_for_positive_values() {
1082 let mut ddsketch: DDSketch = DDSketch::default();
1084 let mut positive_only: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1085
1086 for i in 1..=1000 {
1087 let value = i as f64;
1088 ddsketch.add(value);
1089 positive_only.add(value);
1090 }
1091
1092 assert_eq!(ddsketch.count(), positive_only.count());
1093
1094 for q in [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1096 let dd_quantile = ddsketch.quantile(q).unwrap();
1097 let po_quantile = positive_only.quantile(q).unwrap();
1098 assert!(
1099 (dd_quantile - po_quantile).abs() < 0.001,
1100 "quantile {} mismatch: DDSketch={}, PositiveOnly={}",
1101 q,
1102 dd_quantile,
1103 po_quantile
1104 );
1105 }
1106 }
1107}