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]
681 fn large_upward_jump_preserves_counts_and_maximum_quantile() {
682 let mut sketch = DDSketch::with_relative_accuracy(1.0 / 1024.0).unwrap();
683 sketch.add_n(1.0, 100);
684 sketch.add(((1u64 << 30) - 1) as f64);
685
686 assert_eq!(sketch.count(), 101);
687
688 let stored: f64 = sketch.positive_store().to_proto().contiguousBinCounts.iter().sum();
689 assert_eq!(
690 stored, 101.0,
691 "every observation must be present in the bins, not just in the count"
692 );
693
694 let maximum = sketch.quantile(1.0).expect("the maximum quantile must resolve");
695 assert_rel_acc_eq!(1.0, 1.0 / 1024.0, ((1u64 << 30) - 1) as f64, maximum);
696 }
697
698 #[test]
705 fn collapsing_lowest_quantiles_do_not_depend_on_insertion_order() {
706 let large = ((1u64 << 30) - 1) as f64;
707
708 let mut ascending = DDSketch::with_relative_accuracy(1.0 / 1024.0).unwrap();
709 ascending.add_n(1.0, 100);
710 ascending.add(large);
711
712 let mut descending = DDSketch::with_relative_accuracy(1.0 / 1024.0).unwrap();
713 descending.add(large);
714 descending.add_n(1.0, 100);
715
716 assert_eq!(descending.count(), ascending.count());
717 for quantile in [0.0, 0.5, 0.95, 1.0] {
718 assert_eq!(
719 descending.quantile(quantile),
720 ascending.quantile(quantile),
721 "quantile({quantile}) must not depend on insertion order"
722 );
723 }
724 }
725
726 #[test]
727 fn accuracy_integers_positive_only_even_small() {
728 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
729 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 10));
730 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
731 }
732
733 #[test]
734 fn accuracy_integers_positive_only_even_medium() {
735 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
736 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 250));
737 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
738 }
739
740 #[test]
741 fn accuracy_integers_positive_only_even_large() {
742 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
743 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 1000));
744 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
745 }
746
747 #[test]
748 fn accuracy_integers_positive_only_odd_small() {
749 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
750 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 11));
751 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
752 }
753
754 #[test]
755 fn accuracy_integers_positive_only_odd_medium() {
756 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
757 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 293));
758 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
759 }
760
761 #[test]
762 fn accuracy_integers_positive_only_odd_large() {
763 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
764 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(1, 1023));
765 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
766 }
767
768 #[test]
769 fn accuracy_integers_negative_only_even_small() {
770 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
771 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-10, -1));
772 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
773 }
774
775 #[test]
776 fn accuracy_integers_negative_only_even_medium() {
777 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
778 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-250, -1));
779 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
780 }
781
782 #[test]
783 fn accuracy_integers_negative_only_even_large() {
784 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
785 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-1000, -1));
786 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
787 }
788
789 #[test]
790 fn accuracy_integers_negative_only_odd_small() {
791 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
792 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-11, -1));
793 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
794 }
795
796 #[test]
797 fn accuracy_integers_negative_only_odd_medium() {
798 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
799 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-293, -1));
800 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
801 }
802
803 #[test]
804 fn accuracy_integers_negative_only_odd_large() {
805 let index_mapping = LogarithmicMapping::new(0.01).unwrap();
806 let dataset = Dataset::<_, CollapsingLowestDenseStore>::new(index_mapping, integers(-1023, -1));
807 dataset.validate(&[0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]);
808 }
809
810 #[test]
811 fn zero_values() {
812 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
813 sketch.add(0.0);
814 sketch.add(0.0);
815 sketch.add(1.0);
816
817 assert_eq!(sketch.count(), 3);
818 assert_eq!(sketch.zero_count(), 2);
819 }
820
821 #[test]
822 fn merge() {
823 let mut sketch1 = DDSketch::with_relative_accuracy(0.01).unwrap();
824 sketch1.add(1.0);
825 sketch1.add(2.0);
826
827 let mut sketch2 = DDSketch::with_relative_accuracy(0.01).unwrap();
828 sketch2.add(3.0);
829 sketch2.add(4.0);
830
831 sketch1.merge(&sketch2);
832
833 assert_eq!(sketch1.count(), 4);
834 }
835
836 #[test]
837 fn clear() {
838 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
839 sketch.add(1.0);
840 sketch.add(2.0);
841
842 sketch.clear();
843
844 assert!(sketch.is_empty());
845 assert_eq!(sketch.count(), 0);
846 }
847
848 #[test]
849 fn quantile_at_zero_and_one_returns_min_and_max() {
850 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
854 for i in 1..=100 {
855 sketch.add(i as f64);
856 }
857
858 let min_actual = sketch.quantile(0.0).unwrap();
860 assert_rel_acc_eq!(0.0, 0.01, 1.0, min_actual);
861
862 let max_actual = sketch.quantile(1.0).unwrap();
864 assert_rel_acc_eq!(1.0, 0.01, 100.0, max_actual);
865 }
866
867 #[test]
868 fn add_n() {
869 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
870 sketch.add_n(10.0, 5);
871
872 assert_eq!(sketch.count(), 5);
873 }
874
875 #[test]
876 fn proto_roundtrip() {
877 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
878 sketch.add(1.0);
879 sketch.add(2.0);
880 sketch.add(3.0);
881 sketch.add(100.0);
882
883 let proto = sketch.to_proto();
884 let mapping = LogarithmicMapping::new(0.01).unwrap();
885 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
886
887 assert_eq!(sketch.count(), recovered.count());
889 assert_eq!(sketch.zero_count(), recovered.zero_count());
890
891 for q in [0.25, 0.5, 0.75, 0.99] {
893 let orig = sketch.quantile(q).unwrap();
894 let recov = recovered.quantile(q).unwrap();
895 assert!(
896 (orig - recov).abs() < 0.001,
897 "quantile {} mismatch: {} vs {}",
898 q,
899 orig,
900 recov
901 );
902 }
903 }
904
905 #[test]
906 fn proto_roundtrip_with_negatives() {
907 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
908 sketch.add(-10.0);
909 sketch.add(-5.0);
910 sketch.add(0.0);
911 sketch.add(5.0);
912 sketch.add(10.0);
913
914 let proto = sketch.to_proto();
915 let mapping = LogarithmicMapping::new(0.01).unwrap();
916 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
917
918 assert_eq!(sketch.count(), recovered.count());
919 assert_eq!(sketch.zero_count(), recovered.zero_count());
920 }
921
922 #[test]
923 fn proto_roundtrip_empty() {
924 let sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
925
926 let proto = sketch.to_proto();
927 let mapping = LogarithmicMapping::new(0.01).unwrap();
928 let recovered: DDSketch = DDSketch::from_proto(&proto, mapping).unwrap();
929
930 assert!(recovered.is_empty());
931 assert_eq!(recovered.count(), 0);
932 }
933
934 #[test]
935 fn proto_gamma_mismatch() {
936 let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
937 sketch.add(1.0);
938
939 let proto = sketch.to_proto();
940
941 let different_mapping = LogarithmicMapping::new(0.05).unwrap();
943 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, different_mapping);
944
945 assert!(result.is_err());
946 match result {
947 Err(crate::canonical::ProtoConversionError::GammaMismatch { .. }) => {}
948 _ => panic!("Expected GammaMismatch error"),
949 }
950 }
951
952 #[test]
953 fn proto_missing_mapping() {
954 use datadog_protos::sketches::DDSketch as ProtoDDSketch;
955
956 let proto = ProtoDDSketch::new(); let mapping = LogarithmicMapping::new(0.01).unwrap();
958 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
959
960 assert!(result.is_err());
961 match result {
962 Err(crate::canonical::ProtoConversionError::MissingMapping) => {}
963 _ => panic!("Expected MissingMapping error"),
964 }
965 }
966
967 #[test]
968 fn proto_non_zero_index_offset() {
969 use datadog_protos::sketches::DDSketch as ProtoDDSketch;
970
971 let mapping = LogarithmicMapping::new(0.01).unwrap();
975 let mut proto_mapping = mapping.to_proto();
976 proto_mapping.indexOffset = 1.0;
977
978 let mut proto = ProtoDDSketch::new();
979 proto.set_mapping(proto_mapping);
980
981 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
982 match result {
983 Err(crate::canonical::ProtoConversionError::NonZeroIndexOffset { actual }) => {
984 assert_eq!(actual, 1.0);
985 }
986 other => panic!("Expected NonZeroIndexOffset error, got {:?}", other),
987 }
988 }
989
990 #[test]
991 fn proto_unsupported_interpolation() {
992 use datadog_protos::sketches::{index_mapping::Interpolation, DDSketch as ProtoDDSketch};
993
994 let mapping = LogarithmicMapping::new(0.01).unwrap();
998 let mut proto_mapping = mapping.to_proto();
999 proto_mapping.interpolation = protobuf::EnumOrUnknown::new(Interpolation::LINEAR);
1000
1001 let mut proto = ProtoDDSketch::new();
1002 proto.set_mapping(proto_mapping);
1003
1004 let result = DDSketch::<_, CollapsingLowestDenseStore>::from_proto(&proto, mapping);
1005 match result {
1006 Err(crate::canonical::ProtoConversionError::UnsupportedInterpolation { actual }) => {
1007 assert_eq!(actual, Interpolation::LINEAR as i32);
1008 }
1009 other => panic!("Expected UnsupportedInterpolation error, got {:?}", other),
1010 }
1011 }
1012
1013 #[test]
1016 fn positive_only_size() {
1017 use crate::canonical::mapping::FixedLogarithmicMapping;
1018
1019 let ddsketch_size = std::mem::size_of::<DDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
1021 let positive_only_size =
1022 std::mem::size_of::<PositiveOnlyDDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore>>();
1023
1024 assert!(
1026 ddsketch_size > positive_only_size,
1027 "PositiveOnlyDDSketch ({} bytes) should be smaller than DDSketch ({} bytes)",
1028 positive_only_size,
1029 ddsketch_size
1030 );
1031 assert_eq!(
1032 ddsketch_size - positive_only_size,
1033 48,
1034 "Expected 48 bytes savings from removing negative store"
1035 );
1036 }
1037
1038 #[test]
1039 fn positive_only_empty() {
1040 let sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1041
1042 assert!(sketch.is_empty());
1043 assert_eq!(sketch.count(), 0);
1044 assert_eq!(sketch.quantile(0.5), None);
1045 }
1046
1047 #[test]
1048 fn positive_only_add_and_quantile() {
1049 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1050
1051 for i in 1..=100 {
1052 sketch.add(i as f64);
1053 }
1054
1055 assert_eq!(sketch.count(), 100);
1056
1057 let median = sketch.quantile(0.5).unwrap();
1059 assert!((median - 50.0).abs() < 2.0, "median {} should be close to 50", median);
1060 }
1061
1062 #[test]
1063 fn positive_only_negative_values_become_zero() {
1064 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1065
1066 sketch.add(-10.0);
1067 sketch.add(-5.0);
1068 sketch.add(0.0);
1069 sketch.add(5.0);
1070 sketch.add(10.0);
1071
1072 assert_eq!(sketch.count(), 5);
1073 assert_eq!(sketch.zero_count(), 3);
1075 }
1076
1077 #[test]
1078 fn positive_only_merge() {
1079 let mut sketch1: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1080 sketch1.add(1.0);
1081 sketch1.add(2.0);
1082
1083 let mut sketch2: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1084 sketch2.add(3.0);
1085 sketch2.add(4.0);
1086
1087 sketch1.merge(&sketch2);
1088
1089 assert_eq!(sketch1.count(), 4);
1090 }
1091
1092 #[test]
1093 fn positive_only_clear() {
1094 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1095 sketch.add(1.0);
1096 sketch.add(2.0);
1097
1098 sketch.clear();
1099
1100 assert!(sketch.is_empty());
1101 assert_eq!(sketch.count(), 0);
1102 }
1103
1104 #[test]
1105 fn positive_only_proto_roundtrip() {
1106 let mut sketch: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1107 sketch.add(1.0);
1108 sketch.add(2.0);
1109 sketch.add(3.0);
1110 sketch.add(100.0);
1111
1112 let proto = sketch.to_proto();
1113
1114 assert!(proto.negativeValues.is_none());
1116
1117 let mapping = LogarithmicMapping::new(0.01).unwrap();
1118 let recovered: PositiveOnlyDDSketch = PositiveOnlyDDSketch::from_proto(&proto, mapping).unwrap();
1119
1120 assert_eq!(sketch.count(), recovered.count());
1121 assert_eq!(sketch.zero_count(), recovered.zero_count());
1122
1123 for q in [0.25, 0.5, 0.75, 0.99] {
1125 let orig = sketch.quantile(q).unwrap();
1126 let recov = recovered.quantile(q).unwrap();
1127 assert!(
1128 (orig - recov).abs() < 0.01,
1129 "quantile {} mismatch: {} vs {}",
1130 q,
1131 orig,
1132 recov
1133 );
1134 }
1135 }
1136
1137 #[test]
1138 fn positive_only_matches_ddsketch_for_positive_values() {
1139 let mut ddsketch: DDSketch = DDSketch::default();
1141 let mut positive_only: PositiveOnlyDDSketch = PositiveOnlyDDSketch::default();
1142
1143 for i in 1..=1000 {
1144 let value = i as f64;
1145 ddsketch.add(value);
1146 positive_only.add(value);
1147 }
1148
1149 assert_eq!(ddsketch.count(), positive_only.count());
1150
1151 for q in [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1153 let dd_quantile = ddsketch.quantile(q).unwrap();
1154 let po_quantile = positive_only.quantile(q).unwrap();
1155 assert!(
1156 (dd_quantile - po_quantile).abs() < 0.001,
1157 "quantile {} mismatch: DDSketch={}, PositiveOnly={}",
1158 q,
1159 dd_quantile,
1160 po_quantile
1161 );
1162 }
1163 }
1164}