Skip to main content

alice_analytics/
sketch.rs

1//! Probabilistic Sketch Data Structures
2//!
3//! - HyperLogLog++: Cardinality estimation with ~1.04/√m standard error
4//! - `DDSketch`: Relative-error quantile estimation
5//! - Count-Min Sketch: Frequency estimation for heavy hitters
6
7// a no_std unit-test build links std (libtest), whose inherent methods win;
8// FloatExt only covers correctly rounded operations, so the bits are the same
9#[cfg(not(any(feature = "std", test)))]
10use crate::math::FloatExt;
11use crate::math::{f64_i32, f64_u64, hash_index, i64_f64, ipow64, u64_f64, usize_f64};
12use alice_det_math::{exp64, ln64};
13use core::hash::{Hash, Hasher};
14
15// ============================================================================
16// Lookup Tables for Fast Computation
17// ============================================================================
18
19/// Precomputed 2^{-k} values for k = 0..64 (`HyperLogLog` optimization)
20/// Eliminates expensive `powi()` calls in cardinality estimation
21const POW2_NEG_LUT: [f64; 65] = [
22    1.0,                         // 2^-0
23    0.5,                         // 2^-1
24    0.25,                        // 2^-2
25    0.125,                       // 2^-3
26    0.0625,                      // 2^-4
27    0.031_25,                    // 2^-5
28    0.015_625,                   // 2^-6
29    0.007_812_5,                 // 2^-7
30    0.003_906_25,                // 2^-8
31    0.001_953_125,               // 2^-9
32    0.000_976_562_5,             // 2^-10
33    0.000_488_281_25,            // 2^-11
34    0.000_244_140_625,           // 2^-12
35    0.000_122_070_312_5,         // 2^-13
36    6.103_515_625e-5,            // 2^-14
37    3.051_757_812_5e-5,          // 2^-15
38    1.525_878_906_25e-5,         // 2^-16
39    7.629_394_531_25e-6,         // 2^-17
40    3.814_697_265_625e-6,        // 2^-18
41    1.907_348_632_812_5e-6,      // 2^-19
42    9.536_743_164_062_5e-7,      // 2^-20
43    4.768_371_582_031_25e-7,     // 2^-21
44    2.384_185_791_015_625e-7,    // 2^-22
45    1.192_092_895_507_812_5e-7,  // 2^-23
46    5.960_464_477_539_063e-8,    // 2^-24
47    2.980_232_238_769_531_2e-8,  // 2^-25
48    1.490_116_119_384_765_6e-8,  // 2^-26
49    7.450_580_596_923_828e-9,    // 2^-27
50    3.725_290_298_461_914e-9,    // 2^-28
51    1.862_645_149_230_957e-9,    // 2^-29
52    9.313_225_746_154_785e-10,   // 2^-30
53    4.656_612_873_077_393e-10,   // 2^-31
54    2.328_306_436_538_696_3e-10, // 2^-32
55    1.164_153_218_269_348_1e-10, // 2^-33
56    5.820_766_091_346_741e-11,   // 2^-34
57    2.910_383_045_673_370_4e-11, // 2^-35
58    1.455_191_522_836_685_2e-11, // 2^-36
59    7.275_957_614_183_426e-12,   // 2^-37
60    3.637_978_807_091_713e-12,   // 2^-38
61    1.818_989_403_545_856_5e-12, // 2^-39
62    9.094_947_017_729_282e-13,   // 2^-40
63    4.547_473_508_864_641e-13,   // 2^-41
64    2.273_736_754_432_320_6e-13, // 2^-42
65    1.136_868_377_216_160_3e-13, // 2^-43
66    5.684_341_886_080_802e-14,   // 2^-44
67    2.842_170_943_040_401e-14,   // 2^-45
68    1.421_085_471_520_200_4e-14, // 2^-46
69    7.105_427_357_601_002e-15,   // 2^-47
70    3.552_713_678_800_501e-15,   // 2^-48
71    1.776_356_839_400_250_5e-15, // 2^-49
72    8.881_784_197_001_252e-16,   // 2^-50
73    4.440_892_098_500_626e-16,   // 2^-51
74    2.220_446_049_250_313e-16,   // 2^-52
75    1.110_223_024_625_156_5e-16, // 2^-53
76    5.551_115_123_125_783e-17,   // 2^-54
77    2.775_557_561_562_891_4e-17, // 2^-55
78    1.387_778_780_781_445_7e-17, // 2^-56
79    6.938_893_903_907_228e-18,   // 2^-57
80    3.469_446_951_953_614e-18,   // 2^-58
81    1.734_723_475_976_807e-18,   // 2^-59
82    8.673_617_379_884_035e-19,   // 2^-60
83    4.336_808_689_942_018e-19,   // 2^-61
84    2.168_404_344_971_009e-19,   // 2^-62
85    1.084_202_172_485_504_4e-19, // 2^-63
86    5.421_010_862_427_522e-20,   // 2^-64
87];
88
89/// Reciprocal of 2^52 for fast log2 approximation (avoids division in hot path)
90const RCP_POW2_52: f64 = 1.0 / 4_503_599_627_370_496.0; // 1/2^52, precomputed
91
92/// Fast approximate log2 using IEEE 754 bit extraction
93/// Returns floor(log2(x)) for positive x, useful for bucket indexing
94#[inline]
95fn fast_log2_approx(x: f64) -> f64 {
96    // IEEE 754 double: sign(1) | exponent(11) | mantissa(52)
97    // For positive x: log2(x) ≈ exponent - 1023 + mantissa_fraction
98    let bits = x.to_bits();
99    // 11 bit の指数なので i64 に収まる (u64 → i64 の wrap は起きない)
100    #[allow(clippy::cast_possible_wrap)]
101    let exponent = ((bits >> 52) & 0x7FF) as i64;
102    let mantissa = bits & 0x000F_FFFF_FFFF_FFFF;
103
104    // exponent - 1023 gives the integer part of log2
105    // mantissa * (1/2^52) gives a value in [0, 1) for linear interpolation
106    let int_part = exponent - 1023;
107    let frac_part = u64_f64(mantissa) * RCP_POW2_52;
108
109    i64_f64(int_part) + frac_part
110}
111
112// ============================================================================
113// Mergeable Trait - All sketches can be merged across distributed nodes
114// ============================================================================
115
116/// Trait for mergeable probabilistic data structures
117pub trait Mergeable {
118    /// Merge another sketch into this one
119    fn merge(&mut self, other: &Self);
120}
121
122// ============================================================================
123// Simple Hash Function (FNV-1a variant for determinism)
124// ============================================================================
125
126/// FNV-1a hash for deterministic, fast hashing
127#[derive(Clone, Copy, Debug)]
128pub struct FnvHasher {
129    state: u64,
130}
131
132impl FnvHasher {
133    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
134    const FNV_PRIME: u64 = 0x0100_0000_01b3;
135
136    #[inline]
137    #[must_use]
138    pub const fn new() -> Self {
139        Self {
140            state: Self::FNV_OFFSET,
141        }
142    }
143
144    /// Avalanche bit mixer (from `MurmurHash3` finalizer)
145    /// Ensures all bits are well-distributed for `HyperLogLog`
146    #[inline]
147    const fn mix(mut h: u64) -> u64 {
148        h ^= h >> 33;
149        h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
150        h ^= h >> 33;
151        h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
152        h ^= h >> 33;
153        h
154    }
155
156    #[inline]
157    #[must_use]
158    pub fn hash_bytes(data: &[u8]) -> u64 {
159        let mut hasher = Self::new();
160        hasher.write(data);
161        Self::mix(hasher.state)
162    }
163
164    #[inline]
165    #[must_use]
166    pub fn hash_u64(value: u64) -> u64 {
167        Self::hash_bytes(&value.to_le_bytes())
168    }
169
170    #[inline]
171    #[must_use]
172    pub fn hash_u128(value: u128) -> u64 {
173        Self::hash_bytes(&value.to_le_bytes())
174    }
175}
176
177impl Default for FnvHasher {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl Hasher for FnvHasher {
184    #[inline]
185    fn write(&mut self, bytes: &[u8]) {
186        for &byte in bytes {
187            self.state ^= u64::from(byte);
188            self.state = self.state.wrapping_mul(Self::FNV_PRIME);
189        }
190    }
191
192    #[inline]
193    fn finish(&self) -> u64 {
194        // Apply avalanche mixer for better bit distribution
195        Self::mix(self.state)
196    }
197}
198
199// ============================================================================
200// HyperLogLog++ - Cardinality Estimation
201// ============================================================================
202
203/// Macro to generate `HyperLogLog` implementations for specific sizes
204macro_rules! impl_hyperloglog {
205    ($name:ident, $p:expr, $m:expr) => {
206        /// `HyperLogLog`++ for cardinality (unique count) estimation
207        ///
208        /// Memory: $m bytes
209        /// Error: ~1.04 / sqrt($m)
210        #[derive(Clone, Debug)]
211        pub struct $name {
212            /// Registers storing maximum leading zeros + 1
213            registers: [u8; $m],
214        }
215
216        impl $name {
217            /// Number of registers (m = 2^P)
218            pub const M: usize = $m;
219            /// Bits used for register index
220            pub const P: usize = $p;
221
222            /// Alpha constant for bias correction
223            const ALPHA: f64 = 0.7213 / (1.0 + 1.079 / ($m as f64));
224
225            /// Create a new empty `HyperLogLog`
226            #[inline]
227            pub const fn new() -> Self {
228                #[allow(clippy::large_stack_arrays)]
229                Self {
230                    registers: [0u8; $m],
231                }
232            }
233
234            /// Insert an already-hashed value
235            #[inline]
236            pub const fn insert_hash(&mut self, hash: u64) {
237                let idx = hash_index(hash) & (Self::M - 1);
238                let w = hash >> $p;
239                // rho = position of first 1 bit in the (64-P) remaining bits
240                // leading_zeros(w) includes the P bits we shifted away, so subtract them
241                let rho = if w == 0 {
242                    // P ≤ 16 なので 64 - P + 1 ≤ 65 = u8 に収まる (rho は 1..=65)
243                    #[allow(clippy::cast_possible_truncation)]
244                    let rho = (64 - $p + 1) as u8;
245                    rho
246                } else {
247                    #[allow(clippy::cast_possible_truncation)]
248                    let rho = (w.leading_zeros() as usize - $p + 1) as u8;
249                    rho
250                };
251                if rho > self.registers[idx] {
252                    self.registers[idx] = rho;
253                }
254            }
255
256            /// Insert a hashable value
257            #[inline]
258            pub fn insert<T: Hash>(&mut self, value: &T) {
259                let mut hasher = FnvHasher::new();
260                value.hash(&mut hasher);
261                self.insert_hash(hasher.finish());
262            }
263
264            /// Insert raw bytes
265            #[inline]
266            pub fn insert_bytes(&mut self, bytes: &[u8]) {
267                self.insert_hash(FnvHasher::hash_bytes(bytes));
268            }
269
270            /// Estimate cardinality using `HyperLogLog`++ algorithm
271            /// Optimized with LUT for 2^{-k} values
272            pub fn cardinality(&self) -> f64 {
273                let mut sum = 0.0f64;
274                let mut zeros = 0usize;
275
276                // Use LUT instead of expensive powi() calls
277                for &reg in &self.registers {
278                    // LUT has 65 entries (0..=64), clamp to be safe
279                    let idx = (reg as usize).min(64);
280                    sum += POW2_NEG_LUT[idx];
281                    if reg == 0 {
282                        zeros += 1;
283                    }
284                }
285
286                let m = usize_f64(Self::M);
287                let inv_sum = 1.0 / sum;
288                let raw_estimate = Self::ALPHA * m * m * inv_sum;
289
290                if raw_estimate <= 2.5 * m && zeros > 0 {
291                    let inv_zeros = 1.0 / usize_f64(zeros);
292                    m * ln64(m * inv_zeros)
293                } else {
294                    raw_estimate
295                }
296            }
297
298            /// Get raw registers
299            #[inline]
300            pub const fn registers(&self) -> &[u8] {
301                &self.registers
302            }
303
304            /// Reset all registers to zero
305            #[inline]
306            pub const fn clear(&mut self) {
307                #[allow(clippy::large_stack_arrays)]
308                {
309                    self.registers = [0u8; $m];
310                }
311            }
312        }
313
314        impl Default for $name {
315            fn default() -> Self {
316                Self::new()
317            }
318        }
319
320        impl Mergeable for $name {
321            fn merge(&mut self, other: &Self) {
322                for (dst, &src) in self.registers.iter_mut().zip(other.registers.iter()) {
323                    if src > *dst {
324                        *dst = src;
325                    }
326                }
327            }
328        }
329    };
330}
331
332// Generate common HyperLogLog sizes
333impl_hyperloglog!(HyperLogLog10, 10, 1024); // 1KB, ~3.2% error
334impl_hyperloglog!(HyperLogLog12, 12, 4096); // 4KB, ~1.6% error
335impl_hyperloglog!(HyperLogLog14, 14, 16384); // 16KB, ~0.8% error
336impl_hyperloglog!(HyperLogLog16, 16, 65536); // 64KB, ~0.4% error
337
338/// Type alias for the most common `HyperLogLog` size (16KB, ~0.8% error)
339pub type HyperLogLog = HyperLogLog14;
340
341// ============================================================================
342// DDSketch - Relative Error Quantile Estimation
343// ============================================================================
344
345/// `DDSketch` for quantile estimation with relative error guarantee
346///
347/// Guarantees that for any quantile q, the returned value v satisfies:
348/// |v - `true_value`| <= α * `true_value`
349///
350/// where α is the relative accuracy (e.g., 0.01 for 1% error)
351///
352/// # Example
353/// ```
354/// use alice_analytics::sketch::DDSketch256;
355///
356/// let mut sketch = DDSketch256::new(0.01); // 1% relative error
357///
358/// for latency in [10.0, 20.0, 30.0, 100.0, 500.0] {
359///     sketch.insert(latency);
360/// }
361///
362/// let p99 = sketch.quantile(0.99);
363/// // p99 ≈ 500.0 (within 1% relative error)
364/// ```
365/// Macro to generate `DDSketch` implementations for specific bin counts
366macro_rules! impl_ddsketch {
367    ($name:ident, $bins:expr) => {
368        #[derive(Clone, Debug)]
369        pub struct $name {
370            positive_bins: [u64; $bins],
371            negative_bins: [u64; $bins],
372            zero_count: u64,
373            count: u64,
374            non_finite: u64,
375            min: f64,
376            max: f64,
377            sum: f64,
378            gamma: f64,
379            ln_gamma: f64,
380            inv_ln_gamma: f64,
381            alpha: f64,
382            offset: i32,
383        }
384
385        impl $name {
386            pub const BINS: usize = $bins;
387
388            pub fn new(alpha: f64) -> Self {
389                let gamma = (1.0 + alpha) / (1.0 - alpha);
390                let ln_gamma = ln64(gamma);
391                // Offset to center around 1.0 (ln(1.0) = 0)
392                // For typical latencies (1ms - 10s), we want indices to fit in BINS
393                // With offset at BINS/4, we can handle values from gamma^(-BINS/4) to gamma^(3*BINS/4)
394                let offset = ($bins / 4) as i32;
395
396                Self {
397                    positive_bins: [0u64; $bins],
398                    negative_bins: [0u64; $bins],
399                    zero_count: 0,
400                    count: 0,
401                    non_finite: 0,
402                    min: f64::INFINITY,
403                    max: f64::NEG_INFINITY,
404                    sum: 0.0,
405                    gamma,
406                    ln_gamma,
407                    inv_ln_gamma: 1.0 / ln_gamma,
408                    alpha,
409                    offset,
410                }
411            }
412
413            /// Add one observation.
414            ///
415            /// Contract for inputs outside the usual range:
416            ///
417            /// * **a non-finite sample (`NaN`, `+inf`, `-inf`) is not a
418            ///   magnitude**: it is counted in
419            ///   [`non_finite`](Self::non_finite) and nothing else is touched
420            ///   — not [`count`](Self::count), not [`sum`](Self::sum), not
421            ///   [`min`](Self::min) / [`max`](Self::max), and no bin. The
422            ///   classification follows `law::PointClass::NonFinite`
423            ///   (feature `law`), which groups "NaN or infinite" and keeps
424            ///   such points out of the summary;
425            /// * a **finite** magnitude outside
426            ///   [`accurate_range`](Self::accurate_range) is counted and filed
427            ///   in the nearest edge bin, so later ranks do not shift; the `α`
428            ///   bound does not hold for it;
429            /// * `0.0` is counted exactly and needs no bin.
430            ///
431            /// Until 2026-10-07 a `NaN` was counted in `count`, poisoned `sum`
432            /// (and therefore `mean`), and was filed **with the exact zeros**,
433            /// because it fails both `value > 0.0` and `value < 0.0`: a stream
434            /// of nothing but `NaN` reported `quantile(q) == 0.0`. Counting a
435            /// `NaN` sample as a zero sample is a misclassification, not a
436            /// rounding question.
437            #[inline]
438            pub fn insert(&mut self, value: f64) {
439                if !value.is_finite() {
440                    self.non_finite += 1;
441                    return;
442                }
443
444                self.count += 1;
445                self.sum += value;
446
447                self.min = alice_det_math::min64(self.min, value);
448                self.max = alice_det_math::max64(self.max, value);
449
450                // Out-of-range magnitudes collapse into the edge bins (the
451                // α guarantee holds inside `accurate_range()`);
452                // until 2026-09-17 they were counted but stored nowhere, so
453                // every quantile above them shifted by one rank per lost value.
454                if value > 0.0 {
455                    let idx = self.bucket_index(value).min($bins - 1);
456                    self.positive_bins[idx] += 1;
457                } else if value < 0.0 {
458                    let idx = self.bucket_index(-value).min($bins - 1);
459                    self.negative_bins[idx] += 1;
460                } else {
461                    self.zero_count += 1;
462                }
463            }
464
465            /// Bucket index calculation
466            /// Uses standard `ln()` for quantile accuracy (`DDSketch` requires precise buckets)
467            ///
468            /// `saturating_add` rather than `+`: an extreme **finite**
469            /// magnitude reaches this cast with a product large enough to
470            /// saturate `as i32` at `i32::MAX`, and adding the offset to that
471            /// overflows. A small `alpha` is enough on its own, because
472            /// `inv_ln_gamma` grows without bound as `alpha` approaches 0
473            /// (`alpha = 0` makes it infinite); non-finite samples no longer
474            /// reach here at all, they are counted in `non_finite`.
475            /// Before 2026-10-07 the overflow panicked in a
476            /// debug build and wrapped to a negative index in a release one,
477            /// where `max(0)` then filed the value in bin 0 — the same
478            /// rank shift that the 2026-09-17 edge-bin fix removed, in the
479            /// opposite direction. Saturating keeps it in the top edge bin,
480            /// which is what `accurate_range()` already documents for
481            /// magnitudes outside the guaranteed range.
482            #[inline]
483            fn bucket_index(&self, value: f64) -> usize {
484                let idx =
485                    f64_i32((ln64(value) * self.inv_ln_gamma).ceil()).saturating_add(self.offset);
486                usize::try_from(idx.max(0)).unwrap_or(0)
487            }
488
489            /// Fast bucket index using IEEE 754 bit extraction (for non-critical paths)
490            /// ~10x faster than `ln()` but has ~1-2% error
491            #[inline(always)]
492            #[allow(dead_code)]
493            fn bucket_index_fast(&self, value: f64) -> usize {
494                let log2_gamma = self.ln_gamma / core::f64::consts::LN_2;
495                let inv_log2_gamma = 1.0 / log2_gamma;
496                let log2_value = fast_log2_approx(value);
497                let idx = f64_i32((log2_value * inv_log2_gamma).ceil()).saturating_add(self.offset);
498                usize::try_from(idx.max(0)).unwrap_or(0)
499            }
500
501            /// Representative value of bin `idx` (which covers
502            /// `(γ^(i−1), γ^i]`, `i = idx − offset`): `2γ^i / (γ + 1)`, the point
503            /// whose relative distance to both bin edges is exactly α — the
504            /// `DDSketch` guarantee (Masson, Rim & Lee 2019).
505            ///
506            /// History (2026-09-17, oracle `tests/analytic_oracle.rs`): the
507            /// **lower bound** γ^(i−1) was returned, so a value near the top of
508            /// its bin came back up to γ − 1 = 2α/(1−α) too small (3.1 % at
509            /// α = 0.02 on a 6-decade stream) — outside the published bound.
510            #[inline]
511            fn bucket_representative(&self, idx: usize) -> f64 {
512                let exp = i32::try_from(idx).unwrap_or(i32::MAX) - self.offset;
513                2.0 * ipow64(self.gamma, exp) / (self.gamma + 1.0)
514            }
515
516            pub fn quantile(&self, q: f64) -> f64 {
517                if self.count == 0 {
518                    return 0.0;
519                }
520                let rank = f64_u64((q * u64_f64(self.count)).ceil()).max(1);
521                // a bin representative may lie outside the observed range;
522                // the true order statistic never does
523                let clamp = |v: f64| v.clamp(self.min, self.max);
524                let mut cumulative = 0u64;
525                // negative values ascend from the largest magnitude (most
526                // negative) — until 2026-09-17 the bins were walked from the
527                // smallest magnitude, so the order of every negative quantile
528                // was reversed
529                for (idx, &count) in self.negative_bins.iter().enumerate().rev() {
530                    cumulative += count;
531                    if cumulative >= rank {
532                        return clamp(-self.bucket_representative(idx));
533                    }
534                }
535                cumulative += self.zero_count;
536                if cumulative >= rank {
537                    return 0.0;
538                }
539                for (idx, &count) in self.positive_bins.iter().enumerate() {
540                    cumulative += count;
541                    if cumulative >= rank {
542                        return clamp(self.bucket_representative(idx));
543                    }
544                }
545                self.max
546            }
547            /// Samples summarised: finite values only. A non-finite sample
548            /// is in [`non_finite`](Self::non_finite) instead, so the ranks
549            /// `quantile` computes from this count are the ranks of the
550            /// values that actually reached a bin.
551            #[inline]
552            pub const fn count(&self) -> u64 {
553                self.count
554            }
555
556            /// Samples rejected as non-finite (`NaN`, `+inf`, `-inf`)
557            ///
558            /// Named after `law::ResidualSummary::non_finite` (feature `law`),
559            /// which counts the same class the same way. A non-zero value
560            /// means the stream carried something that is not a magnitude —
561            /// an upstream division by zero, a missing sample encoded as
562            /// `NaN` — and that those samples are in no other figure here.
563            #[inline]
564            pub const fn non_finite(&self) -> u64 {
565                self.non_finite
566            }
567
568            /// Exact sum of the summarised (finite) samples
569            ///
570            /// Finite in, finite out: a `NaN` sample can no longer poison it.
571            #[inline]
572            pub const fn sum(&self) -> f64 {
573                self.sum
574            }
575
576            /// Mean of the summarised (finite) samples, `0.0` when there are
577            /// none
578            #[inline(always)]
579            pub fn mean(&self) -> f64 {
580                if self.count == 0 {
581                    0.0
582                } else {
583                    self.sum * (1.0 / u64_f64(self.count))
584                }
585            }
586
587            /// Smallest summarised (finite) sample, `f64::INFINITY` when there
588            /// are none
589            #[inline]
590            pub const fn min(&self) -> f64 {
591                self.min
592            }
593
594            /// Largest summarised (finite) sample, `f64::NEG_INFINITY` when
595            /// there are none
596            #[inline]
597            pub const fn max(&self) -> f64 {
598                self.max
599            }
600
601            #[inline]
602            pub const fn alpha(&self) -> f64 {
603                self.alpha
604            }
605
606            /// Magnitudes `[γ^(−offset), γ^(BINS − 1 − offset)]` for which the
607            /// relative-error guarantee holds (`offset = BINS / 4`)
608            ///
609            /// Bin `idx` covers `(γ^(i−1), γ^i]` with `i = idx − offset`; a
610            /// magnitude below the first bin or above the last one is counted
611            /// in that edge bin, so its quantile keeps the rank but not the
612            /// `α` bound. Zero is counted exactly and needs no bin.
613            #[must_use]
614            pub fn accurate_range(&self) -> (f64, f64) {
615                let lowest = -self.offset;
616                let highest = i32::try_from($bins - 1).unwrap_or(i32::MAX) - self.offset;
617                (ipow64(self.gamma, lowest), ipow64(self.gamma, highest))
618            }
619
620            pub const fn clear(&mut self) {
621                self.positive_bins = [0u64; $bins];
622                self.negative_bins = [0u64; $bins];
623                self.zero_count = 0;
624                self.count = 0;
625                self.non_finite = 0;
626                self.min = f64::INFINITY;
627                self.max = f64::NEG_INFINITY;
628                self.sum = 0.0;
629            }
630        }
631
632        impl Mergeable for $name {
633            fn merge(&mut self, other: &Self) {
634                for (dst, &src) in self
635                    .positive_bins
636                    .iter_mut()
637                    .zip(other.positive_bins.iter())
638                {
639                    *dst += src;
640                }
641                for (dst, &src) in self
642                    .negative_bins
643                    .iter_mut()
644                    .zip(other.negative_bins.iter())
645                {
646                    *dst += src;
647                }
648                self.zero_count += other.zero_count;
649                self.count += other.count;
650                self.non_finite += other.non_finite;
651                self.sum += other.sum;
652
653                self.min = alice_det_math::min64(self.min, other.min);
654                self.max = alice_det_math::max64(self.max, other.max);
655            }
656        }
657    };
658}
659
660// Generate common DDSketch sizes
661impl_ddsketch!(DDSketch128, 128); // Small, use alpha >= 0.1
662impl_ddsketch!(DDSketch256, 256); // Medium, use alpha >= 0.05
663impl_ddsketch!(DDSketch512, 512); // Good balance
664impl_ddsketch!(DDSketch1024, 1024); // High accuracy, alpha >= 0.02
665impl_ddsketch!(DDSketch2048, 2048); // Very high accuracy, alpha >= 0.01
666
667/// Type alias for the most common `DDSketch` size (good for alpha=0.01)
668pub type DDSketch = DDSketch2048;
669
670// ============================================================================
671// Count-Min Sketch - Frequency Estimation
672// ============================================================================
673
674/// Macro to generate `CountMinSketch` implementations
675macro_rules! impl_countmin {
676    ($name:ident, $w:expr, $d:expr) => {
677        /// Count-Min Sketch for frequency estimation
678        #[derive(Clone, Debug)]
679        pub struct $name {
680            counters: [[u64; $w]; $d],
681            total: u64,
682        }
683
684        impl $name {
685            pub const WIDTH: usize = $w;
686            pub const DEPTH: usize = $d;
687
688            #[inline]
689            pub const fn new() -> Self {
690                #[allow(clippy::large_stack_arrays)]
691                Self {
692                    counters: [[0u64; $w]; $d],
693                    total: 0,
694                }
695            }
696
697            #[inline]
698            const fn hash_for_row(hash: u64, row: usize) -> usize {
699                let h = hash.wrapping_add((row as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15));
700                let mixed = h ^ (h >> 33);
701                let mixed = mixed.wrapping_mul(0xff51_afd7_ed55_8ccd);
702                let mixed = mixed ^ (mixed >> 33);
703                hash_index(mixed) & ($w - 1)
704            }
705
706            #[inline]
707            pub fn insert_hash(&mut self, hash: u64, count: u64) {
708                self.total += count;
709                for row in 0..$d {
710                    let col = Self::hash_for_row(hash, row);
711                    self.counters[row][col] = self.counters[row][col].saturating_add(count);
712                }
713            }
714
715            #[inline]
716            pub fn insert<T: Hash>(&mut self, item: &T) {
717                let mut hasher = FnvHasher::new();
718                item.hash(&mut hasher);
719                self.insert_hash(hasher.finish(), 1);
720            }
721
722            #[inline]
723            pub fn insert_bytes(&mut self, bytes: &[u8]) {
724                self.insert_hash(FnvHasher::hash_bytes(bytes), 1);
725            }
726
727            #[inline]
728            pub fn estimate_hash(&self, hash: u64) -> u64 {
729                let mut min_count = u64::MAX;
730                for row in 0..$d {
731                    let col = Self::hash_for_row(hash, row);
732                    min_count = min_count.min(self.counters[row][col]);
733                }
734                min_count
735            }
736
737            #[inline]
738            pub fn estimate<T: Hash>(&self, item: &T) -> u64 {
739                let mut hasher = FnvHasher::new();
740                item.hash(&mut hasher);
741                self.estimate_hash(hasher.finish())
742            }
743
744            #[inline]
745            pub fn estimate_bytes(&self, bytes: &[u8]) -> u64 {
746                self.estimate_hash(FnvHasher::hash_bytes(bytes))
747            }
748
749            #[inline]
750            pub const fn total(&self) -> u64 {
751                self.total
752            }
753
754            #[inline]
755            pub const fn clear(&mut self) {
756                #[allow(clippy::large_stack_arrays)]
757                {
758                    self.counters = [[0u64; $w]; $d];
759                }
760                self.total = 0;
761            }
762
763            #[inline(always)]
764            pub fn error_bound(&self) -> f64 {
765                core::f64::consts::E * (1.0 / f64::from($w))
766            }
767
768            #[inline]
769            pub fn confidence(&self) -> f64 {
770                1.0 - exp64(-f64::from($d))
771            }
772        }
773
774        impl Default for $name {
775            fn default() -> Self {
776                Self::new()
777            }
778        }
779
780        impl Mergeable for $name {
781            fn merge(&mut self, other: &Self) {
782                self.total += other.total;
783                for row in 0..$d {
784                    for col in 0..$w {
785                        self.counters[row][col] =
786                            self.counters[row][col].saturating_add(other.counters[row][col]);
787                    }
788                }
789            }
790        }
791    };
792}
793
794// Generate common CountMinSketch sizes
795impl_countmin!(CountMinSketch1024x5, 1024, 5);
796impl_countmin!(CountMinSketch2048x7, 2048, 7);
797impl_countmin!(CountMinSketch4096x5, 4096, 5);
798
799/// Type alias for the default Count-Min Sketch
800pub type CountMinSketch = CountMinSketch1024x5;
801
802// ============================================================================
803// Heavy Hitters (Top-K using Count-Min Sketch + Heap)
804// ============================================================================
805
806/// Entry for heavy hitters tracking
807#[derive(Clone, Copy, Debug)]
808pub struct HeavyHitterEntry {
809    /// Hash of the item
810    pub hash: u64,
811    /// Estimated frequency
812    pub count: u64,
813}
814
815/// Macro to generate `HeavyHitters` implementations
816macro_rules! impl_heavy_hitters {
817    ($name:ident, $cms_name:ident, $k:expr) => {
818        /// Heavy Hitters tracker using Count-Min Sketch
819        #[derive(Clone, Debug)]
820        pub struct $name {
821            cms: $cms_name,
822            top_k: [HeavyHitterEntry; $k],
823            count: usize,
824        }
825
826        impl $name {
827            pub const K: usize = $k;
828
829            #[inline]
830            pub const fn new() -> Self {
831                Self {
832                    cms: $cms_name::new(),
833                    top_k: [HeavyHitterEntry { hash: 0, count: 0 }; $k],
834                    count: 0,
835                }
836            }
837
838            pub fn insert_hash(&mut self, hash: u64) {
839                self.cms.insert_hash(hash, 1);
840                let estimated = self.cms.estimate_hash(hash);
841
842                let mut found_idx = None;
843                for i in 0..self.count {
844                    if self.top_k[i].hash == hash {
845                        found_idx = Some(i);
846                        break;
847                    }
848                }
849
850                if let Some(idx) = found_idx {
851                    self.top_k[idx].count = estimated;
852                    self.sort_top_k();
853                } else if self.count < $k {
854                    self.top_k[self.count] = HeavyHitterEntry {
855                        hash,
856                        count: estimated,
857                    };
858                    self.count += 1;
859                    self.sort_top_k();
860                } else if estimated > self.top_k[0].count {
861                    self.top_k[0] = HeavyHitterEntry {
862                        hash,
863                        count: estimated,
864                    };
865                    self.sort_top_k();
866                }
867            }
868
869            fn sort_top_k(&mut self) {
870                for i in 1..self.count {
871                    let entry = self.top_k[i];
872                    let mut j = i;
873                    while j > 0 && self.top_k[j - 1].count > entry.count {
874                        self.top_k[j] = self.top_k[j - 1];
875                        j -= 1;
876                    }
877                    self.top_k[j] = entry;
878                }
879            }
880
881            pub fn top(&self) -> impl Iterator<Item = &HeavyHitterEntry> {
882                self.top_k[..self.count].iter().rev()
883            }
884
885            #[inline]
886            pub const fn cms(&self) -> &$cms_name {
887                &self.cms
888            }
889
890            pub const fn clear(&mut self) {
891                self.cms.clear();
892                self.top_k = [HeavyHitterEntry { hash: 0, count: 0 }; $k];
893                self.count = 0;
894            }
895        }
896
897        impl Default for $name {
898            fn default() -> Self {
899                Self::new()
900            }
901        }
902    };
903}
904
905// Generate HeavyHitters variants
906impl_heavy_hitters!(HeavyHitters10, CountMinSketch1024x5, 10);
907impl_heavy_hitters!(HeavyHitters20, CountMinSketch2048x7, 20);
908impl_heavy_hitters!(HeavyHitters5, CountMinSketch1024x5, 5);
909
910/// Type alias for the default Heavy Hitters tracker
911pub type HeavyHitters = HeavyHitters10;
912
913// ============================================================================
914// Tests
915// ============================================================================
916
917#[cfg(test)]
918#[allow(clippy::float_cmp)]
919mod tests {
920    use super::*;
921
922    #[test]
923    fn test_fnv_hash() {
924        let h1 = FnvHasher::hash_u64(12345);
925        let h2 = FnvHasher::hash_u64(12345);
926        let h3 = FnvHasher::hash_u64(12346);
927
928        assert_eq!(h1, h2);
929        assert_ne!(h1, h3);
930    }
931
932    #[test]
933    fn test_hyperloglog_basic() {
934        // Use HyperLogLog16 (64K registers) for best accuracy
935        let mut hll = HyperLogLog16::new();
936
937        // Insert 1000 unique values
938        for i in 0..1000u64 {
939            hll.insert_hash(FnvHasher::hash_u64(i));
940        }
941
942        let estimate = hll.cardinality();
943        // Relaxed tolerance due to statistical nature
944        assert!(
945            estimate > 800.0 && estimate < 1200.0,
946            "estimate = {estimate}"
947        );
948    }
949
950    #[test]
951    fn test_hyperloglog_merge() {
952        let mut hll1 = HyperLogLog16::new();
953        let mut hll2 = HyperLogLog16::new();
954
955        for i in 0..500u64 {
956            hll1.insert_hash(FnvHasher::hash_u64(i));
957        }
958        for i in 500..1000u64 {
959            hll2.insert_hash(FnvHasher::hash_u64(i));
960        }
961
962        hll1.merge(&hll2);
963        let estimate = hll1.cardinality();
964        assert!(
965            estimate > 800.0 && estimate < 1200.0,
966            "estimate = {estimate}"
967        );
968    }
969
970    #[test]
971    fn test_hyperloglog_16_large() {
972        let mut hll = HyperLogLog16::new();
973
974        // Use explicit hash for consistency with other tests
975        for i in 0..100_000_u64 {
976            hll.insert_hash(FnvHasher::hash_u64(i));
977        }
978
979        let estimate = hll.cardinality();
980        // With 100K values in 64K registers, expect reasonable accuracy (±25%)
981        assert!(
982            estimate > 75000.0 && estimate < 125_000.0,
983            "estimate = {estimate}"
984        );
985    }
986
987    #[test]
988    fn test_ddsketch_basic() {
989        // Use DDSketch2048 for alpha=0.01 to ensure enough bins
990        let mut sketch = DDSketch2048::new(0.01);
991
992        let values = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
993        for v in values {
994            sketch.insert(v);
995        }
996
997        assert_eq!(sketch.count(), 10);
998        assert!((sketch.mean() - 55.0).abs() < 0.001);
999
1000        let p50 = sketch.quantile(0.5);
1001        assert!(p50 > 40.0 && p50 < 70.0, "p50 = {p50}");
1002
1003        let p99 = sketch.quantile(0.99);
1004        assert!(p99 > 80.0 && p99 <= 100.0, "p99 = {p99}");
1005    }
1006
1007    #[test]
1008    fn test_countmin_basic() {
1009        let mut cms = CountMinSketch1024x5::new();
1010
1011        for _ in 0..100 {
1012            cms.insert_hash(1, 1);
1013        }
1014        for _ in 0..50 {
1015            cms.insert_hash(2, 1);
1016        }
1017        for _ in 0..10 {
1018            cms.insert_hash(3, 1);
1019        }
1020
1021        assert!(cms.estimate_hash(1) >= 100);
1022        assert!(cms.estimate_hash(2) >= 50);
1023        assert!(cms.estimate_hash(3) >= 10);
1024    }
1025
1026    #[test]
1027    fn test_countmin_merge() {
1028        let mut cms1 = CountMinSketch1024x5::new();
1029        let mut cms2 = CountMinSketch1024x5::new();
1030
1031        for _ in 0..50 {
1032            cms1.insert_hash(1, 1);
1033        }
1034        for _ in 0..50 {
1035            cms2.insert_hash(1, 1);
1036        }
1037
1038        cms1.merge(&cms2);
1039        assert!(cms1.estimate_hash(1) >= 100);
1040    }
1041
1042    // `format!` / `Vec` / the entropy-seeded constructors are `std`-only,
1043    // so this case cannot run on the `no_std` build
1044    #[cfg(feature = "std")]
1045    #[test]
1046    fn test_heavy_hitters() {
1047        let mut hh = HeavyHitters5::new();
1048
1049        for _ in 0..100 {
1050            hh.insert_hash(1);
1051        }
1052        for _ in 0..50 {
1053            hh.insert_hash(2);
1054        }
1055        for _ in 0..30 {
1056            hh.insert_hash(3);
1057        }
1058        for _ in 0..10 {
1059            hh.insert_hash(4);
1060        }
1061        for _ in 0..5 {
1062            hh.insert_hash(5);
1063        }
1064
1065        let top: Vec<_> = hh.top().collect();
1066        assert_eq!(top.len(), 5);
1067
1068        assert_eq!(top[0].hash, 1);
1069        assert_eq!(top[1].hash, 2);
1070        assert_eq!(top[2].hash, 3);
1071    }
1072
1073    #[test]
1074    fn test_fnv_hash_bytes() {
1075        let h1 = FnvHasher::hash_bytes(b"hello");
1076        let h2 = FnvHasher::hash_bytes(b"hello");
1077        let h3 = FnvHasher::hash_bytes(b"world");
1078        assert_eq!(h1, h2);
1079        assert_ne!(h1, h3);
1080    }
1081
1082    #[test]
1083    fn test_fnv_hash_u128() {
1084        let h1 = FnvHasher::hash_u128(12345u128);
1085        let h2 = FnvHasher::hash_u128(12345u128);
1086        let h3 = FnvHasher::hash_u128(12346u128);
1087        assert_eq!(h1, h2);
1088        assert_ne!(h1, h3);
1089    }
1090
1091    #[test]
1092    fn test_fnv_hasher_default() {
1093        let h = FnvHasher::default();
1094        assert_eq!(h.state, FnvHasher::FNV_OFFSET);
1095    }
1096
1097    #[test]
1098    fn test_fnv_hasher_finish() {
1099        let mut h = FnvHasher::new();
1100        h.write(b"test");
1101        let result = h.finish();
1102        assert_ne!(result, 0);
1103    }
1104
1105    #[test]
1106    fn test_hyperloglog_empty() {
1107        let hll = HyperLogLog16::new();
1108        let estimate = hll.cardinality();
1109        assert_eq!(estimate, 0.0);
1110    }
1111
1112    #[test]
1113    fn test_hyperloglog_single() {
1114        let mut hll = HyperLogLog16::new();
1115        hll.insert_hash(FnvHasher::hash_u64(42));
1116        let estimate = hll.cardinality();
1117        assert!(estimate > 0.0 && estimate < 5.0, "estimate = {estimate}");
1118    }
1119
1120    #[test]
1121    fn test_hyperloglog_clear() {
1122        let mut hll = HyperLogLog16::new();
1123        for i in 0..100u64 {
1124            hll.insert_hash(FnvHasher::hash_u64(i));
1125        }
1126        assert!(hll.cardinality() > 0.0);
1127        hll.clear();
1128        assert_eq!(hll.cardinality(), 0.0);
1129    }
1130
1131    #[test]
1132    fn test_hyperloglog_registers() {
1133        let hll = HyperLogLog16::new();
1134        assert_eq!(hll.registers().len(), HyperLogLog16::M);
1135    }
1136
1137    #[test]
1138    fn test_hyperloglog_default() {
1139        let hll = HyperLogLog16::default();
1140        assert_eq!(hll.cardinality(), 0.0);
1141    }
1142
1143    #[test]
1144    fn test_hyperloglog_insert_bytes() {
1145        let mut hll = HyperLogLog16::new();
1146        hll.insert_bytes(b"test_item");
1147        assert!(hll.cardinality() > 0.0);
1148    }
1149
1150    #[test]
1151    fn test_hyperloglog_insert_generic() {
1152        let mut hll = HyperLogLog16::new();
1153        hll.insert(&42u64);
1154        hll.insert(&"hello");
1155        assert!(hll.cardinality() > 0.0);
1156    }
1157
1158    #[test]
1159    fn test_hyperloglog10_accuracy() {
1160        let mut hll = HyperLogLog10::new();
1161        for i in 0..1000u64 {
1162            hll.insert_hash(FnvHasher::hash_u64(i));
1163        }
1164        let estimate = hll.cardinality();
1165        // HLL10 has ~3.2% error, allow ±50%
1166        assert!(
1167            estimate > 500.0 && estimate < 1500.0,
1168            "estimate = {estimate}"
1169        );
1170    }
1171
1172    #[test]
1173    fn test_hyperloglog12_accuracy() {
1174        let mut hll = HyperLogLog12::new();
1175        for i in 0..1000u64 {
1176            hll.insert_hash(FnvHasher::hash_u64(i));
1177        }
1178        let estimate = hll.cardinality();
1179        assert!(
1180            estimate > 700.0 && estimate < 1300.0,
1181            "estimate = {estimate}"
1182        );
1183    }
1184
1185    #[test]
1186    fn test_ddsketch_empty() {
1187        let sketch = DDSketch2048::new(0.01);
1188        assert_eq!(sketch.count(), 0);
1189        assert_eq!(sketch.quantile(0.5), 0.0);
1190        assert_eq!(sketch.mean(), 0.0);
1191    }
1192
1193    #[test]
1194    fn test_ddsketch_single() {
1195        let mut sketch = DDSketch2048::new(0.01);
1196        sketch.insert(42.0);
1197        assert_eq!(sketch.count(), 1);
1198        assert_eq!(sketch.sum(), 42.0);
1199        assert_eq!(sketch.min(), 42.0);
1200        assert_eq!(sketch.max(), 42.0);
1201    }
1202
1203    #[test]
1204    fn test_ddsketch_clear() {
1205        let mut sketch = DDSketch2048::new(0.01);
1206        for v in [10.0, 20.0, 30.0] {
1207            sketch.insert(v);
1208        }
1209        assert_eq!(sketch.count(), 3);
1210        sketch.clear();
1211        assert_eq!(sketch.count(), 0);
1212        assert_eq!(sketch.sum(), 0.0);
1213    }
1214
1215    #[test]
1216    fn test_ddsketch_alpha() {
1217        let sketch = DDSketch2048::new(0.05);
1218        assert!((sketch.alpha() - 0.05).abs() < f64::EPSILON);
1219    }
1220
1221    #[test]
1222    fn test_ddsketch_merge() {
1223        let mut s1 = DDSketch2048::new(0.01);
1224        let mut s2 = DDSketch2048::new(0.01);
1225        for v in [10.0, 20.0, 30.0] {
1226            s1.insert(v);
1227        }
1228        for v in [40.0, 50.0] {
1229            s2.insert(v);
1230        }
1231        s1.merge(&s2);
1232        assert_eq!(s1.count(), 5);
1233        assert!((s1.sum() - 150.0).abs() < 0.001);
1234        assert_eq!(s1.min(), 10.0);
1235        assert_eq!(s1.max(), 50.0);
1236    }
1237
1238    #[test]
1239    fn test_ddsketch128() {
1240        let mut sketch = DDSketch128::new(0.1);
1241        sketch.insert(100.0);
1242        assert_eq!(sketch.count(), 1);
1243    }
1244
1245    #[test]
1246    fn test_ddsketch_negative() {
1247        let mut sketch = DDSketch2048::new(0.01);
1248        sketch.insert(-5.0);
1249        sketch.insert(-10.0);
1250        sketch.insert(0.0);
1251        assert_eq!(sketch.count(), 3);
1252    }
1253
1254    #[test]
1255    fn test_countmin_empty() {
1256        let cms = CountMinSketch1024x5::new();
1257        assert_eq!(cms.estimate_hash(42), 0);
1258        assert_eq!(cms.total(), 0);
1259    }
1260
1261    #[test]
1262    fn test_countmin_total() {
1263        let mut cms = CountMinSketch1024x5::new();
1264        cms.insert_hash(1, 10);
1265        cms.insert_hash(2, 20);
1266        assert_eq!(cms.total(), 30);
1267    }
1268
1269    #[test]
1270    fn test_countmin_clear() {
1271        let mut cms = CountMinSketch1024x5::new();
1272        cms.insert_hash(1, 100);
1273        assert!(cms.estimate_hash(1) >= 100);
1274        cms.clear();
1275        assert_eq!(cms.estimate_hash(1), 0);
1276        assert_eq!(cms.total(), 0);
1277    }
1278
1279    #[test]
1280    fn test_countmin_default() {
1281        let cms = CountMinSketch1024x5::default();
1282        assert_eq!(cms.total(), 0);
1283    }
1284
1285    #[test]
1286    fn test_countmin_insert_generic() {
1287        let mut cms = CountMinSketch1024x5::new();
1288        cms.insert(&"hello");
1289        assert!(cms.estimate(&"hello") >= 1);
1290    }
1291
1292    #[test]
1293    fn test_countmin_insert_bytes() {
1294        let mut cms = CountMinSketch1024x5::new();
1295        cms.insert_bytes(b"test_key");
1296        assert!(cms.estimate_bytes(b"test_key") >= 1);
1297    }
1298
1299    #[test]
1300    fn test_countmin_error_bound() {
1301        let cms = CountMinSketch1024x5::new();
1302        let error = cms.error_bound();
1303        assert!(error > 0.0);
1304    }
1305
1306    #[test]
1307    fn test_countmin_confidence() {
1308        let cms = CountMinSketch1024x5::new();
1309        let conf = cms.confidence();
1310        assert!(conf > 0.0 && conf < 1.0);
1311    }
1312
1313    #[test]
1314    fn test_countmin_2048x7() {
1315        let mut cms = CountMinSketch2048x7::new();
1316        cms.insert_hash(42, 5);
1317        assert!(cms.estimate_hash(42) >= 5);
1318    }
1319
1320    #[test]
1321    fn test_countmin_4096x5() {
1322        let mut cms = CountMinSketch4096x5::new();
1323        cms.insert_hash(42, 5);
1324        assert!(cms.estimate_hash(42) >= 5);
1325    }
1326
1327    #[test]
1328    fn test_heavy_hitters_default() {
1329        let hh = HeavyHitters5::default();
1330        assert_eq!(hh.top().count(), 0);
1331    }
1332
1333    #[test]
1334    fn test_heavy_hitters_clear() {
1335        let mut hh = HeavyHitters5::new();
1336        for _ in 0..10 {
1337            hh.insert_hash(1);
1338        }
1339        assert!(hh.top().count() > 0);
1340        hh.clear();
1341        assert_eq!(hh.top().count(), 0);
1342    }
1343
1344    #[test]
1345    fn test_heavy_hitters_cms_access() {
1346        let mut hh = HeavyHitters5::new();
1347        hh.insert_hash(42);
1348        assert!(hh.cms().estimate_hash(42) >= 1);
1349    }
1350
1351    #[test]
1352    fn test_heavy_hitters_10() {
1353        let mut hh = HeavyHitters10::new();
1354        for i in 0..10u64 {
1355            for _ in 0..(10 - i) {
1356                hh.insert_hash(i);
1357            }
1358        }
1359        assert_eq!(hh.top().count(), 10);
1360    }
1361
1362    #[test]
1363    fn test_heavy_hitters_20() {
1364        let mut hh = HeavyHitters20::new();
1365        hh.insert_hash(1);
1366        assert_eq!(hh.top().count(), 1);
1367    }
1368
1369    #[test]
1370    fn test_fast_log2_approx() {
1371        let approx = fast_log2_approx(1024.0);
1372        assert!((approx - 10.0).abs() < 0.1, "approx = {approx}");
1373    }
1374
1375    #[test]
1376    fn test_fast_log2_approx_one() {
1377        let approx = fast_log2_approx(1.0);
1378        assert!(approx.abs() < 0.01, "approx = {approx}");
1379    }
1380
1381    #[test]
1382    fn test_hyperloglog14_type_alias() {
1383        let mut hll = HyperLogLog::new();
1384        hll.insert_hash(FnvHasher::hash_u64(1));
1385        assert!(hll.cardinality() > 0.0);
1386    }
1387
1388    #[test]
1389    fn test_ddsketch_type_alias() {
1390        let mut sketch = DDSketch::new(0.01);
1391        sketch.insert(42.0);
1392        assert_eq!(sketch.count(), 1);
1393    }
1394
1395    #[test]
1396    fn test_countmin_type_alias() {
1397        let mut cms = CountMinSketch::new();
1398        cms.insert_hash(1, 1);
1399        assert!(cms.estimate_hash(1) >= 1);
1400    }
1401
1402    #[test]
1403    fn test_heavy_hitters_type_alias() {
1404        let mut hh = HeavyHitters::new();
1405        hh.insert_hash(1);
1406        assert_eq!(hh.top().count(), 1);
1407    }
1408}