Skip to main content

ddsketch/canonical/
sketch.rs

1//! Canonical DDSketch implementation.
2
3use datadog_protos::sketches::DDSketch as ProtoDDSketch;
4
5use super::error::ProtoConversionError;
6use super::mapping::{IndexMapping, LogarithmicMapping};
7use super::store::{CollapsingLowestDenseStore, Store};
8
9/// A fast and fully-mergeable quantile sketch with relative-error guarantees.
10///
11/// This implementation supports most of the capabilities of the various official DDSketch implementations, such as:
12///
13/// - support for tracking negative and positive values
14/// - multiple store types (sparse, dense, collapsing)
15/// - configurable index interpolation schemes (only logarithmic currently supported)
16///
17/// Defaults to using a "low collapsing" dense store with a logarithmic index mapping. This works well for tracking
18/// values like time durations/latencies where the tail latencies (higher percentiles) matter most.
19///
20/// # Example
21///
22/// ```
23/// use ddsketch::canonical::DDSketch;
24///
25/// let mut sketch = DDSketch::with_relative_accuracy(0.01).unwrap();
26/// sketch.add(1.0);
27/// sketch.add(2.0);
28/// sketch.add(3.0);
29///
30/// let median = sketch.quantile(0.5).unwrap();
31/// ```
32#[derive(Clone, Debug)]
33pub struct DDSketch<M: IndexMapping = LogarithmicMapping, S: Store = CollapsingLowestDenseStore> {
34    /// The index mapping for this sketch.
35    mapping: M,
36
37    /// Store for positive values.
38    positive_store: S,
39
40    /// Store for negative values.
41    negative_store: S,
42
43    /// Count of values that map to zero.
44    zero_count: u64,
45}
46
47impl DDSketch<LogarithmicMapping, CollapsingLowestDenseStore> {
48    /// Creates a new `DDSketch` with the given relative accuracy.
49    ///
50    /// Defaults to logarithmic mapping and the "low collapsing" dense store, with a maximum of 2048 bins per store.
51    ///
52    /// # Errors
53    ///
54    /// If the relative accuracy is not between `0` and `1`, an error is returned.
55    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    /// Creates a new `DDSketch` with the given mapping and stores.
67    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    /// Adds a single value to the sketch.
77    pub fn add(&mut self, value: f64) {
78        self.add_n(value, 1);
79    }
80
81    /// Adds a value to the sketch with the given count.
82    ///
83    /// This is useful for weighted values or pre-aggregated data.
84    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    /// Returns the approximate value at the given quantile.
101    ///
102    /// The quantile must be in the range of [0, 1].
103    ///
104    /// Returns `None` if the sketch is empty, or if the quantile is out of bounds. Otherwise, returns the approximate
105    /// value.
106    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            // We need to reverse the rank since negative values are stored with positive indices
122            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    /// Merges another sketch into this one.
139    ///
140    /// The other sketch must use the same mapping type.
141    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    /// Returns `true` if the sketch is empty.
155    pub fn is_empty(&self) -> bool {
156        self.count() == 0
157    }
158
159    /// Returns the total number of values added to the sketch.
160    pub fn count(&self) -> u64 {
161        self.negative_store().total_count() + self.positive_store().total_count() + self.zero_count
162    }
163
164    /// Clears the sketch, removing all values.
165    pub fn clear(&mut self) {
166        self.positive_store.clear();
167        self.negative_store.clear();
168        self.zero_count = 0;
169    }
170
171    /// Returns a reference to the index mapping.
172    pub fn mapping(&self) -> &M {
173        &self.mapping
174    }
175
176    /// Returns a reference to the positive value store.
177    pub fn positive_store(&self) -> &S {
178        &self.positive_store
179    }
180
181    /// Returns a reference to the negative value store.
182    pub fn negative_store(&self) -> &S {
183        &self.negative_store
184    }
185
186    /// Returns the count of values mapped to zero.
187    pub fn zero_count(&self) -> u64 {
188        self.zero_count
189    }
190
191    /// Returns the relative accuracy of this sketch.
192    pub fn relative_accuracy(&self) -> f64 {
193        self.mapping.relative_accuracy()
194    }
195
196    /// Creates a `DDSketch` from a protobuf `DDSketch` message.
197    ///
198    /// This validates that the protobuf's index mapping is compatible with
199    /// the mapping type `M`, then populates the stores with the bin data.
200    ///
201    /// # Arguments
202    ///
203    /// * `proto` - The protobuf `DDSketch` message to convert from
204    /// * `mapping` - The mapping instance to use (must be compatible with proto's mapping)
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if:
209    /// - The protobuf is missing a mapping
210    /// - The mapping parameters don't match the provided mapping
211    /// - Any bin counts are negative or non-integer
212    /// - The zero count is negative or non-integer
213    ///
214    /// # Note
215    ///
216    /// The protobuf `DDSketch` does not include `sum`, `min`, `max`, or `count` fields.
217    /// These are computed or set to defaults:
218    /// - `count`: sum of all bin counts plus zero_count
219    /// - `sum`, `min`, `max`: set to sentinel defaults (cannot be recovered from proto)
220    pub fn from_proto(proto: &ProtoDDSketch, mapping: M) -> Result<Self, ProtoConversionError>
221    where
222        S: Default,
223    {
224        // Validate the mapping
225        let proto_mapping = proto.mapping.as_ref().ok_or(ProtoConversionError::MissingMapping)?;
226        mapping.validate_proto_mapping(proto_mapping)?;
227
228        // Validate and convert zero count
229        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    /// Converts this `DDSketch` to a protobuf `DDSketch` message.
256    ///
257    /// # Note
258    ///
259    /// The protobuf `DDSketch` does not include `sum`, `min`, `max`, or `count` fields.
260    /// This information is lost in the conversion.
261    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/// A DDSketch optimized for positive-only values (e.g., latencies, durations).
298///
299/// This is a memory-optimized variant of [`DDSketch`] that eliminates the negative value store,
300/// saving 48 bytes per sketch instance. Use this when you know all values will be non-negative,
301/// such as when tracking latencies or other timing metrics.
302///
303/// Negative values are treated as zero (mapped to `zero_count`).
304///
305/// # Example
306///
307/// ```
308/// use ddsketch::canonical::PositiveOnlyDDSketch;
309/// use ddsketch::canonical::mapping::FixedLogarithmicMapping;
310/// use ddsketch::canonical::store::CollapsingLowestDenseStore;
311///
312/// let mut sketch: PositiveOnlyDDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore> =
313///     PositiveOnlyDDSketch::default();
314/// sketch.add(1.0);
315/// sketch.add(2.0);
316/// sketch.add(3.0);
317///
318/// let median = sketch.quantile(0.5).unwrap();
319/// ```
320#[derive(Clone, Debug)]
321pub struct PositiveOnlyDDSketch<M: IndexMapping = LogarithmicMapping, S: Store = CollapsingLowestDenseStore> {
322    /// The index mapping for this sketch.
323    mapping: M,
324
325    /// Store for positive values.
326    positive_store: S,
327
328    /// Count of values that map to zero (including negative values).
329    zero_count: u64,
330}
331
332impl<M: IndexMapping, S: Store> PositiveOnlyDDSketch<M, S> {
333    /// Creates a new `PositiveOnlyDDSketch` with the given mapping and store.
334    pub fn new(mapping: M, positive_store: S) -> Self {
335        Self {
336            mapping,
337            positive_store,
338            zero_count: 0,
339        }
340    }
341
342    /// Adds a single value to the sketch.
343    ///
344    /// Negative values are treated as zero.
345    #[inline]
346    pub fn add(&mut self, value: f64) {
347        self.add_n(value, 1);
348    }
349
350    /// Adds a value to the sketch with the given count.
351    ///
352    /// Negative values are treated as zero.
353    #[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            // Values at or below min_indexable_value (including negatives) go to zero_count
364            self.zero_count += n;
365        }
366    }
367
368    /// Returns the approximate value at the given quantile.
369    ///
370    /// The quantile must be in the range of [0, 1].
371    ///
372    /// Returns `None` if the sketch is empty, or if the quantile is out of bounds.
373    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    /// Merges another sketch into this one.
397    ///
398    /// The other sketch must use the same mapping type.
399    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    /// Returns `true` if the sketch is empty.
412    #[inline]
413    pub fn is_empty(&self) -> bool {
414        self.count() == 0
415    }
416
417    /// Returns the total number of values added to the sketch.
418    #[inline]
419    pub fn count(&self) -> u64 {
420        self.positive_store.total_count() + self.zero_count
421    }
422
423    /// Clears the sketch, removing all values.
424    pub fn clear(&mut self) {
425        self.positive_store.clear();
426        self.zero_count = 0;
427    }
428
429    /// Returns a reference to the index mapping.
430    pub fn mapping(&self) -> &M {
431        &self.mapping
432    }
433
434    /// Returns a reference to the positive value store.
435    pub fn positive_store(&self) -> &S {
436        &self.positive_store
437    }
438
439    /// Returns the count of values mapped to zero.
440    pub fn zero_count(&self) -> u64 {
441        self.zero_count
442    }
443
444    /// Returns the relative accuracy of this sketch.
445    pub fn relative_accuracy(&self) -> f64 {
446        self.mapping.relative_accuracy()
447    }
448
449    /// Creates a `PositiveOnlyDDSketch` from a protobuf `DDSketch` message.
450    ///
451    /// This validates that the protobuf's index mapping is compatible with the mapping type `M`,
452    /// then populates the store with the positive bin data. Any negative values in the protobuf
453    /// are ignored.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if:
458    /// - The protobuf is missing a mapping
459    /// - The mapping parameters don't match the provided mapping
460    /// - Any bin counts are negative or non-integer
461    /// - The zero count is negative or non-integer
462    pub fn from_proto(proto: &ProtoDDSketch, mapping: M) -> Result<Self, ProtoConversionError>
463    where
464        S: Default,
465    {
466        // Validate the mapping
467        let proto_mapping = proto.mapping.as_ref().ok_or(ProtoConversionError::MissingMapping)?;
468        mapping.validate_proto_mapping(proto_mapping)?;
469
470        // Validate and convert zero count
471        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        // Note: negativeValues is intentionally ignored for positive-only sketches
485
486        Ok(Self {
487            mapping,
488            positive_store,
489            zero_count,
490        })
491    }
492
493    /// Converts this `PositiveOnlyDDSketch` to a protobuf `DDSketch` message.
494    ///
495    /// The resulting protobuf will have no `negativeValues` field set.
496    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        // No negativeValues - this is a positive-only sketch
506
507        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            /*
558            For debugging purposes:
559
560            println!(
561                "asserting range equality for q={}, expected_lower={} (adj: {}), expected_upper={} (adj: {}), actual={}",
562                $quantile,
563                $expected_lower,
564                expected_lower_adj,
565                $expected_upper,
566                expected_upper_adj,
567                actual
568            );
569            */
570
571            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            // Make sure the total counts match.
614            assert_eq!(raw_data.len() as u64, sketch.count());
615
616            // Sort our raw data before comparing quantiles.
617            raw_data.sort();
618
619            let mut data = Array::from_vec(raw_data);
620            let rel_acc = sketch.relative_accuracy();
621
622            // Compare quantiles.
623            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                        // DDSketch does not do linear interpolation between the two closest values, so for example,
637                        // if we have 10 values (1-5 and 10-15, let's say), with an alpha of 0.01, and we ask for
638                        // q=5, you might expect to get back 7.5 -- (5 + 10) / 2 -- but DDSketch can return anywhere
639                        // from 5*0.99 to 10*1.01, or 4.95 to 10.1.
640                        //
641                        // We capture the quantile from the raw data with different interpolation methods to
642                        // calculate those wider bounds so we can validate against the actual guarantees provided by
643                        // DDSketch.
644                        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        // q=0 should return min
800        let min_actual = sketch.quantile(0.0).unwrap();
801        assert_rel_acc_eq!(0.0, 0.01, 1.0, min_actual);
802
803        // q=1 should return max
804        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        // Check bin data is preserved
829        assert_eq!(sketch.count(), recovered.count());
830        assert_eq!(sketch.zero_count(), recovered.zero_count());
831
832        // Check quantiles are approximately equal
833        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        // Try to decode with a different relative accuracy (different gamma)
883        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(); // No mapping set
898        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    // ==================== PositiveOnlyDDSketch tests ====================
909
910    #[test]
911    fn test_positive_only_size() {
912        use crate::canonical::mapping::FixedLogarithmicMapping;
913
914        // Verify the memory savings - PositiveOnlyDDSketch should be smaller than DDSketch
915        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        // PositiveOnlyDDSketch should be ~48 bytes smaller (one less store)
920        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        // Check median is approximately 50
953        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        // Negative values and zero should all go to zero_count
969        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        // Verify no negative values in proto
1010        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        // Check quantiles are approximately equal
1019        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        // Verify that PositiveOnlyDDSketch produces the same results as DDSketch for positive values
1035        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        // Check that quantiles match
1047        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}