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                if value < self.min {
448                    self.min = value;
449                }
450                if value > self.max {
451                    self.max = value;
452                }
453
454                // Out-of-range magnitudes collapse into the edge bins (the
455                // α guarantee holds inside `accurate_range()`);
456                // until 2026-09-17 they were counted but stored nowhere, so
457                // every quantile above them shifted by one rank per lost value.
458                if value > 0.0 {
459                    let idx = self.bucket_index(value).min($bins - 1);
460                    self.positive_bins[idx] += 1;
461                } else if value < 0.0 {
462                    let idx = self.bucket_index(-value).min($bins - 1);
463                    self.negative_bins[idx] += 1;
464                } else {
465                    self.zero_count += 1;
466                }
467            }
468
469            /// Bucket index calculation
470            /// Uses standard `ln()` for quantile accuracy (`DDSketch` requires precise buckets)
471            ///
472            /// `saturating_add` rather than `+`: an extreme **finite**
473            /// magnitude reaches this cast with a product large enough to
474            /// saturate `as i32` at `i32::MAX`, and adding the offset to that
475            /// overflows. A small `alpha` is enough on its own, because
476            /// `inv_ln_gamma` grows without bound as `alpha` approaches 0
477            /// (`alpha = 0` makes it infinite); non-finite samples no longer
478            /// reach here at all, they are counted in `non_finite`.
479            /// Before 2026-10-07 the overflow panicked in a
480            /// debug build and wrapped to a negative index in a release one,
481            /// where `max(0)` then filed the value in bin 0 — the same
482            /// rank shift that the 2026-09-17 edge-bin fix removed, in the
483            /// opposite direction. Saturating keeps it in the top edge bin,
484            /// which is what `accurate_range()` already documents for
485            /// magnitudes outside the guaranteed range.
486            #[inline]
487            fn bucket_index(&self, value: f64) -> usize {
488                let idx =
489                    f64_i32((ln64(value) * self.inv_ln_gamma).ceil()).saturating_add(self.offset);
490                usize::try_from(idx.max(0)).unwrap_or(0)
491            }
492
493            /// Fast bucket index using IEEE 754 bit extraction (for non-critical paths)
494            /// ~10x faster than `ln()` but has ~1-2% error
495            #[inline(always)]
496            #[allow(dead_code)]
497            fn bucket_index_fast(&self, value: f64) -> usize {
498                let log2_gamma = self.ln_gamma / core::f64::consts::LN_2;
499                let inv_log2_gamma = 1.0 / log2_gamma;
500                let log2_value = fast_log2_approx(value);
501                let idx = f64_i32((log2_value * inv_log2_gamma).ceil()).saturating_add(self.offset);
502                usize::try_from(idx.max(0)).unwrap_or(0)
503            }
504
505            /// Representative value of bin `idx` (which covers
506            /// `(γ^(i−1), γ^i]`, `i = idx − offset`): `2γ^i / (γ + 1)`, the point
507            /// whose relative distance to both bin edges is exactly α — the
508            /// `DDSketch` guarantee (Masson, Rim & Lee 2019).
509            ///
510            /// History (2026-09-17, oracle `tests/analytic_oracle.rs`): the
511            /// **lower bound** γ^(i−1) was returned, so a value near the top of
512            /// its bin came back up to γ − 1 = 2α/(1−α) too small (3.1 % at
513            /// α = 0.02 on a 6-decade stream) — outside the published bound.
514            #[inline]
515            fn bucket_representative(&self, idx: usize) -> f64 {
516                let exp = i32::try_from(idx).unwrap_or(i32::MAX) - self.offset;
517                2.0 * ipow64(self.gamma, exp) / (self.gamma + 1.0)
518            }
519
520            pub fn quantile(&self, q: f64) -> f64 {
521                if self.count == 0 {
522                    return 0.0;
523                }
524                let rank = f64_u64((q * u64_f64(self.count)).ceil()).max(1);
525                // a bin representative may lie outside the observed range;
526                // the true order statistic never does
527                let clamp = |v: f64| v.clamp(self.min, self.max);
528                let mut cumulative = 0u64;
529                // negative values ascend from the largest magnitude (most
530                // negative) — until 2026-09-17 the bins were walked from the
531                // smallest magnitude, so the order of every negative quantile
532                // was reversed
533                for (idx, &count) in self.negative_bins.iter().enumerate().rev() {
534                    cumulative += count;
535                    if cumulative >= rank {
536                        return clamp(-self.bucket_representative(idx));
537                    }
538                }
539                cumulative += self.zero_count;
540                if cumulative >= rank {
541                    return 0.0;
542                }
543                for (idx, &count) in self.positive_bins.iter().enumerate() {
544                    cumulative += count;
545                    if cumulative >= rank {
546                        return clamp(self.bucket_representative(idx));
547                    }
548                }
549                self.max
550            }
551            /// Samples summarised: finite values only. A non-finite sample
552            /// is in [`non_finite`](Self::non_finite) instead, so the ranks
553            /// `quantile` computes from this count are the ranks of the
554            /// values that actually reached a bin.
555            #[inline]
556            pub const fn count(&self) -> u64 {
557                self.count
558            }
559
560            /// Samples rejected as non-finite (`NaN`, `+inf`, `-inf`)
561            ///
562            /// Named after `law::ResidualSummary::non_finite` (feature `law`),
563            /// which counts the same class the same way. A non-zero value
564            /// means the stream carried something that is not a magnitude —
565            /// an upstream division by zero, a missing sample encoded as
566            /// `NaN` — and that those samples are in no other figure here.
567            #[inline]
568            pub const fn non_finite(&self) -> u64 {
569                self.non_finite
570            }
571
572            /// Exact sum of the summarised (finite) samples
573            ///
574            /// Finite in, finite out: a `NaN` sample can no longer poison it.
575            #[inline]
576            pub const fn sum(&self) -> f64 {
577                self.sum
578            }
579
580            /// Mean of the summarised (finite) samples, `0.0` when there are
581            /// none
582            #[inline(always)]
583            pub fn mean(&self) -> f64 {
584                if self.count == 0 {
585                    0.0
586                } else {
587                    self.sum * (1.0 / u64_f64(self.count))
588                }
589            }
590
591            /// Smallest summarised (finite) sample, `f64::INFINITY` when there
592            /// are none
593            #[inline]
594            pub const fn min(&self) -> f64 {
595                self.min
596            }
597
598            /// Largest summarised (finite) sample, `f64::NEG_INFINITY` when
599            /// there are none
600            #[inline]
601            pub const fn max(&self) -> f64 {
602                self.max
603            }
604
605            #[inline]
606            pub const fn alpha(&self) -> f64 {
607                self.alpha
608            }
609
610            /// Magnitudes `[γ^(−offset), γ^(BINS − 1 − offset)]` for which the
611            /// relative-error guarantee holds (`offset = BINS / 4`)
612            ///
613            /// Bin `idx` covers `(γ^(i−1), γ^i]` with `i = idx − offset`; a
614            /// magnitude below the first bin or above the last one is counted
615            /// in that edge bin, so its quantile keeps the rank but not the
616            /// `α` bound. Zero is counted exactly and needs no bin.
617            #[must_use]
618            pub fn accurate_range(&self) -> (f64, f64) {
619                let lowest = -self.offset;
620                let highest = i32::try_from($bins - 1).unwrap_or(i32::MAX) - self.offset;
621                (ipow64(self.gamma, lowest), ipow64(self.gamma, highest))
622            }
623
624            pub const fn clear(&mut self) {
625                self.positive_bins = [0u64; $bins];
626                self.negative_bins = [0u64; $bins];
627                self.zero_count = 0;
628                self.count = 0;
629                self.non_finite = 0;
630                self.min = f64::INFINITY;
631                self.max = f64::NEG_INFINITY;
632                self.sum = 0.0;
633            }
634        }
635
636        impl Mergeable for $name {
637            fn merge(&mut self, other: &Self) {
638                for (dst, &src) in self
639                    .positive_bins
640                    .iter_mut()
641                    .zip(other.positive_bins.iter())
642                {
643                    *dst += src;
644                }
645                for (dst, &src) in self
646                    .negative_bins
647                    .iter_mut()
648                    .zip(other.negative_bins.iter())
649                {
650                    *dst += src;
651                }
652                self.zero_count += other.zero_count;
653                self.count += other.count;
654                self.non_finite += other.non_finite;
655                self.sum += other.sum;
656
657                if other.min < self.min {
658                    self.min = other.min;
659                }
660                if other.max > self.max {
661                    self.max = other.max;
662                }
663            }
664        }
665    };
666}
667
668// Generate common DDSketch sizes
669impl_ddsketch!(DDSketch128, 128); // Small, use alpha >= 0.1
670impl_ddsketch!(DDSketch256, 256); // Medium, use alpha >= 0.05
671impl_ddsketch!(DDSketch512, 512); // Good balance
672impl_ddsketch!(DDSketch1024, 1024); // High accuracy, alpha >= 0.02
673impl_ddsketch!(DDSketch2048, 2048); // Very high accuracy, alpha >= 0.01
674
675/// Type alias for the most common `DDSketch` size (good for alpha=0.01)
676pub type DDSketch = DDSketch2048;
677
678// ============================================================================
679// Count-Min Sketch - Frequency Estimation
680// ============================================================================
681
682/// Macro to generate `CountMinSketch` implementations
683macro_rules! impl_countmin {
684    ($name:ident, $w:expr, $d:expr) => {
685        /// Count-Min Sketch for frequency estimation
686        #[derive(Clone, Debug)]
687        pub struct $name {
688            counters: [[u64; $w]; $d],
689            total: u64,
690        }
691
692        impl $name {
693            pub const WIDTH: usize = $w;
694            pub const DEPTH: usize = $d;
695
696            #[inline]
697            pub const fn new() -> Self {
698                #[allow(clippy::large_stack_arrays)]
699                Self {
700                    counters: [[0u64; $w]; $d],
701                    total: 0,
702                }
703            }
704
705            #[inline]
706            const fn hash_for_row(hash: u64, row: usize) -> usize {
707                let h = hash.wrapping_add((row as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15));
708                let mixed = h ^ (h >> 33);
709                let mixed = mixed.wrapping_mul(0xff51_afd7_ed55_8ccd);
710                let mixed = mixed ^ (mixed >> 33);
711                hash_index(mixed) & ($w - 1)
712            }
713
714            #[inline]
715            pub fn insert_hash(&mut self, hash: u64, count: u64) {
716                self.total += count;
717                for row in 0..$d {
718                    let col = Self::hash_for_row(hash, row);
719                    self.counters[row][col] = self.counters[row][col].saturating_add(count);
720                }
721            }
722
723            #[inline]
724            pub fn insert<T: Hash>(&mut self, item: &T) {
725                let mut hasher = FnvHasher::new();
726                item.hash(&mut hasher);
727                self.insert_hash(hasher.finish(), 1);
728            }
729
730            #[inline]
731            pub fn insert_bytes(&mut self, bytes: &[u8]) {
732                self.insert_hash(FnvHasher::hash_bytes(bytes), 1);
733            }
734
735            #[inline]
736            pub fn estimate_hash(&self, hash: u64) -> u64 {
737                let mut min_count = u64::MAX;
738                for row in 0..$d {
739                    let col = Self::hash_for_row(hash, row);
740                    min_count = min_count.min(self.counters[row][col]);
741                }
742                min_count
743            }
744
745            #[inline]
746            pub fn estimate<T: Hash>(&self, item: &T) -> u64 {
747                let mut hasher = FnvHasher::new();
748                item.hash(&mut hasher);
749                self.estimate_hash(hasher.finish())
750            }
751
752            #[inline]
753            pub fn estimate_bytes(&self, bytes: &[u8]) -> u64 {
754                self.estimate_hash(FnvHasher::hash_bytes(bytes))
755            }
756
757            #[inline]
758            pub const fn total(&self) -> u64 {
759                self.total
760            }
761
762            #[inline]
763            pub const fn clear(&mut self) {
764                #[allow(clippy::large_stack_arrays)]
765                {
766                    self.counters = [[0u64; $w]; $d];
767                }
768                self.total = 0;
769            }
770
771            #[inline(always)]
772            pub fn error_bound(&self) -> f64 {
773                core::f64::consts::E * (1.0 / f64::from($w))
774            }
775
776            #[inline]
777            pub fn confidence(&self) -> f64 {
778                1.0 - exp64(-f64::from($d))
779            }
780        }
781
782        impl Default for $name {
783            fn default() -> Self {
784                Self::new()
785            }
786        }
787
788        impl Mergeable for $name {
789            fn merge(&mut self, other: &Self) {
790                self.total += other.total;
791                for row in 0..$d {
792                    for col in 0..$w {
793                        self.counters[row][col] =
794                            self.counters[row][col].saturating_add(other.counters[row][col]);
795                    }
796                }
797            }
798        }
799    };
800}
801
802// Generate common CountMinSketch sizes
803impl_countmin!(CountMinSketch1024x5, 1024, 5);
804impl_countmin!(CountMinSketch2048x7, 2048, 7);
805impl_countmin!(CountMinSketch4096x5, 4096, 5);
806
807/// Type alias for the default Count-Min Sketch
808pub type CountMinSketch = CountMinSketch1024x5;
809
810// ============================================================================
811// Heavy Hitters (Top-K using Count-Min Sketch + Heap)
812// ============================================================================
813
814/// Entry for heavy hitters tracking
815#[derive(Clone, Copy, Debug)]
816pub struct HeavyHitterEntry {
817    /// Hash of the item
818    pub hash: u64,
819    /// Estimated frequency
820    pub count: u64,
821}
822
823/// Macro to generate `HeavyHitters` implementations
824macro_rules! impl_heavy_hitters {
825    ($name:ident, $cms_name:ident, $k:expr) => {
826        /// Heavy Hitters tracker using Count-Min Sketch
827        #[derive(Clone, Debug)]
828        pub struct $name {
829            cms: $cms_name,
830            top_k: [HeavyHitterEntry; $k],
831            count: usize,
832        }
833
834        impl $name {
835            pub const K: usize = $k;
836
837            #[inline]
838            pub const fn new() -> Self {
839                Self {
840                    cms: $cms_name::new(),
841                    top_k: [HeavyHitterEntry { hash: 0, count: 0 }; $k],
842                    count: 0,
843                }
844            }
845
846            pub fn insert_hash(&mut self, hash: u64) {
847                self.cms.insert_hash(hash, 1);
848                let estimated = self.cms.estimate_hash(hash);
849
850                let mut found_idx = None;
851                for i in 0..self.count {
852                    if self.top_k[i].hash == hash {
853                        found_idx = Some(i);
854                        break;
855                    }
856                }
857
858                if let Some(idx) = found_idx {
859                    self.top_k[idx].count = estimated;
860                    self.sort_top_k();
861                } else if self.count < $k {
862                    self.top_k[self.count] = HeavyHitterEntry {
863                        hash,
864                        count: estimated,
865                    };
866                    self.count += 1;
867                    self.sort_top_k();
868                } else if estimated > self.top_k[0].count {
869                    self.top_k[0] = HeavyHitterEntry {
870                        hash,
871                        count: estimated,
872                    };
873                    self.sort_top_k();
874                }
875            }
876
877            fn sort_top_k(&mut self) {
878                for i in 1..self.count {
879                    let entry = self.top_k[i];
880                    let mut j = i;
881                    while j > 0 && self.top_k[j - 1].count > entry.count {
882                        self.top_k[j] = self.top_k[j - 1];
883                        j -= 1;
884                    }
885                    self.top_k[j] = entry;
886                }
887            }
888
889            pub fn top(&self) -> impl Iterator<Item = &HeavyHitterEntry> {
890                self.top_k[..self.count].iter().rev()
891            }
892
893            #[inline]
894            pub const fn cms(&self) -> &$cms_name {
895                &self.cms
896            }
897
898            pub const fn clear(&mut self) {
899                self.cms.clear();
900                self.top_k = [HeavyHitterEntry { hash: 0, count: 0 }; $k];
901                self.count = 0;
902            }
903        }
904
905        impl Default for $name {
906            fn default() -> Self {
907                Self::new()
908            }
909        }
910    };
911}
912
913// Generate HeavyHitters variants
914impl_heavy_hitters!(HeavyHitters10, CountMinSketch1024x5, 10);
915impl_heavy_hitters!(HeavyHitters20, CountMinSketch2048x7, 20);
916impl_heavy_hitters!(HeavyHitters5, CountMinSketch1024x5, 5);
917
918/// Type alias for the default Heavy Hitters tracker
919pub type HeavyHitters = HeavyHitters10;
920
921// ============================================================================
922// Tests
923// ============================================================================
924
925#[cfg(test)]
926#[allow(clippy::float_cmp)]
927mod tests {
928    use super::*;
929
930    #[test]
931    fn test_fnv_hash() {
932        let h1 = FnvHasher::hash_u64(12345);
933        let h2 = FnvHasher::hash_u64(12345);
934        let h3 = FnvHasher::hash_u64(12346);
935
936        assert_eq!(h1, h2);
937        assert_ne!(h1, h3);
938    }
939
940    #[test]
941    fn test_hyperloglog_basic() {
942        // Use HyperLogLog16 (64K registers) for best accuracy
943        let mut hll = HyperLogLog16::new();
944
945        // Insert 1000 unique values
946        for i in 0..1000u64 {
947            hll.insert_hash(FnvHasher::hash_u64(i));
948        }
949
950        let estimate = hll.cardinality();
951        // Relaxed tolerance due to statistical nature
952        assert!(
953            estimate > 800.0 && estimate < 1200.0,
954            "estimate = {estimate}"
955        );
956    }
957
958    #[test]
959    fn test_hyperloglog_merge() {
960        let mut hll1 = HyperLogLog16::new();
961        let mut hll2 = HyperLogLog16::new();
962
963        for i in 0..500u64 {
964            hll1.insert_hash(FnvHasher::hash_u64(i));
965        }
966        for i in 500..1000u64 {
967            hll2.insert_hash(FnvHasher::hash_u64(i));
968        }
969
970        hll1.merge(&hll2);
971        let estimate = hll1.cardinality();
972        assert!(
973            estimate > 800.0 && estimate < 1200.0,
974            "estimate = {estimate}"
975        );
976    }
977
978    #[test]
979    fn test_hyperloglog_16_large() {
980        let mut hll = HyperLogLog16::new();
981
982        // Use explicit hash for consistency with other tests
983        for i in 0..100_000_u64 {
984            hll.insert_hash(FnvHasher::hash_u64(i));
985        }
986
987        let estimate = hll.cardinality();
988        // With 100K values in 64K registers, expect reasonable accuracy (±25%)
989        assert!(
990            estimate > 75000.0 && estimate < 125_000.0,
991            "estimate = {estimate}"
992        );
993    }
994
995    #[test]
996    fn test_ddsketch_basic() {
997        // Use DDSketch2048 for alpha=0.01 to ensure enough bins
998        let mut sketch = DDSketch2048::new(0.01);
999
1000        let values = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0];
1001        for v in values {
1002            sketch.insert(v);
1003        }
1004
1005        assert_eq!(sketch.count(), 10);
1006        assert!((sketch.mean() - 55.0).abs() < 0.001);
1007
1008        let p50 = sketch.quantile(0.5);
1009        assert!(p50 > 40.0 && p50 < 70.0, "p50 = {p50}");
1010
1011        let p99 = sketch.quantile(0.99);
1012        assert!(p99 > 80.0 && p99 <= 100.0, "p99 = {p99}");
1013    }
1014
1015    #[test]
1016    fn test_countmin_basic() {
1017        let mut cms = CountMinSketch1024x5::new();
1018
1019        for _ in 0..100 {
1020            cms.insert_hash(1, 1);
1021        }
1022        for _ in 0..50 {
1023            cms.insert_hash(2, 1);
1024        }
1025        for _ in 0..10 {
1026            cms.insert_hash(3, 1);
1027        }
1028
1029        assert!(cms.estimate_hash(1) >= 100);
1030        assert!(cms.estimate_hash(2) >= 50);
1031        assert!(cms.estimate_hash(3) >= 10);
1032    }
1033
1034    #[test]
1035    fn test_countmin_merge() {
1036        let mut cms1 = CountMinSketch1024x5::new();
1037        let mut cms2 = CountMinSketch1024x5::new();
1038
1039        for _ in 0..50 {
1040            cms1.insert_hash(1, 1);
1041        }
1042        for _ in 0..50 {
1043            cms2.insert_hash(1, 1);
1044        }
1045
1046        cms1.merge(&cms2);
1047        assert!(cms1.estimate_hash(1) >= 100);
1048    }
1049
1050    // `format!` / `Vec` / the entropy-seeded constructors are `std`-only,
1051    // so this case cannot run on the `no_std` build
1052    #[cfg(feature = "std")]
1053    #[test]
1054    fn test_heavy_hitters() {
1055        let mut hh = HeavyHitters5::new();
1056
1057        for _ in 0..100 {
1058            hh.insert_hash(1);
1059        }
1060        for _ in 0..50 {
1061            hh.insert_hash(2);
1062        }
1063        for _ in 0..30 {
1064            hh.insert_hash(3);
1065        }
1066        for _ in 0..10 {
1067            hh.insert_hash(4);
1068        }
1069        for _ in 0..5 {
1070            hh.insert_hash(5);
1071        }
1072
1073        let top: Vec<_> = hh.top().collect();
1074        assert_eq!(top.len(), 5);
1075
1076        assert_eq!(top[0].hash, 1);
1077        assert_eq!(top[1].hash, 2);
1078        assert_eq!(top[2].hash, 3);
1079    }
1080
1081    #[test]
1082    fn test_fnv_hash_bytes() {
1083        let h1 = FnvHasher::hash_bytes(b"hello");
1084        let h2 = FnvHasher::hash_bytes(b"hello");
1085        let h3 = FnvHasher::hash_bytes(b"world");
1086        assert_eq!(h1, h2);
1087        assert_ne!(h1, h3);
1088    }
1089
1090    #[test]
1091    fn test_fnv_hash_u128() {
1092        let h1 = FnvHasher::hash_u128(12345u128);
1093        let h2 = FnvHasher::hash_u128(12345u128);
1094        let h3 = FnvHasher::hash_u128(12346u128);
1095        assert_eq!(h1, h2);
1096        assert_ne!(h1, h3);
1097    }
1098
1099    #[test]
1100    fn test_fnv_hasher_default() {
1101        let h = FnvHasher::default();
1102        assert_eq!(h.state, FnvHasher::FNV_OFFSET);
1103    }
1104
1105    #[test]
1106    fn test_fnv_hasher_finish() {
1107        let mut h = FnvHasher::new();
1108        h.write(b"test");
1109        let result = h.finish();
1110        assert_ne!(result, 0);
1111    }
1112
1113    #[test]
1114    fn test_hyperloglog_empty() {
1115        let hll = HyperLogLog16::new();
1116        let estimate = hll.cardinality();
1117        assert_eq!(estimate, 0.0);
1118    }
1119
1120    #[test]
1121    fn test_hyperloglog_single() {
1122        let mut hll = HyperLogLog16::new();
1123        hll.insert_hash(FnvHasher::hash_u64(42));
1124        let estimate = hll.cardinality();
1125        assert!(estimate > 0.0 && estimate < 5.0, "estimate = {estimate}");
1126    }
1127
1128    #[test]
1129    fn test_hyperloglog_clear() {
1130        let mut hll = HyperLogLog16::new();
1131        for i in 0..100u64 {
1132            hll.insert_hash(FnvHasher::hash_u64(i));
1133        }
1134        assert!(hll.cardinality() > 0.0);
1135        hll.clear();
1136        assert_eq!(hll.cardinality(), 0.0);
1137    }
1138
1139    #[test]
1140    fn test_hyperloglog_registers() {
1141        let hll = HyperLogLog16::new();
1142        assert_eq!(hll.registers().len(), HyperLogLog16::M);
1143    }
1144
1145    #[test]
1146    fn test_hyperloglog_default() {
1147        let hll = HyperLogLog16::default();
1148        assert_eq!(hll.cardinality(), 0.0);
1149    }
1150
1151    #[test]
1152    fn test_hyperloglog_insert_bytes() {
1153        let mut hll = HyperLogLog16::new();
1154        hll.insert_bytes(b"test_item");
1155        assert!(hll.cardinality() > 0.0);
1156    }
1157
1158    #[test]
1159    fn test_hyperloglog_insert_generic() {
1160        let mut hll = HyperLogLog16::new();
1161        hll.insert(&42u64);
1162        hll.insert(&"hello");
1163        assert!(hll.cardinality() > 0.0);
1164    }
1165
1166    #[test]
1167    fn test_hyperloglog10_accuracy() {
1168        let mut hll = HyperLogLog10::new();
1169        for i in 0..1000u64 {
1170            hll.insert_hash(FnvHasher::hash_u64(i));
1171        }
1172        let estimate = hll.cardinality();
1173        // HLL10 has ~3.2% error, allow ±50%
1174        assert!(
1175            estimate > 500.0 && estimate < 1500.0,
1176            "estimate = {estimate}"
1177        );
1178    }
1179
1180    #[test]
1181    fn test_hyperloglog12_accuracy() {
1182        let mut hll = HyperLogLog12::new();
1183        for i in 0..1000u64 {
1184            hll.insert_hash(FnvHasher::hash_u64(i));
1185        }
1186        let estimate = hll.cardinality();
1187        assert!(
1188            estimate > 700.0 && estimate < 1300.0,
1189            "estimate = {estimate}"
1190        );
1191    }
1192
1193    #[test]
1194    fn test_ddsketch_empty() {
1195        let sketch = DDSketch2048::new(0.01);
1196        assert_eq!(sketch.count(), 0);
1197        assert_eq!(sketch.quantile(0.5), 0.0);
1198        assert_eq!(sketch.mean(), 0.0);
1199    }
1200
1201    #[test]
1202    fn test_ddsketch_single() {
1203        let mut sketch = DDSketch2048::new(0.01);
1204        sketch.insert(42.0);
1205        assert_eq!(sketch.count(), 1);
1206        assert_eq!(sketch.sum(), 42.0);
1207        assert_eq!(sketch.min(), 42.0);
1208        assert_eq!(sketch.max(), 42.0);
1209    }
1210
1211    #[test]
1212    fn test_ddsketch_clear() {
1213        let mut sketch = DDSketch2048::new(0.01);
1214        for v in [10.0, 20.0, 30.0] {
1215            sketch.insert(v);
1216        }
1217        assert_eq!(sketch.count(), 3);
1218        sketch.clear();
1219        assert_eq!(sketch.count(), 0);
1220        assert_eq!(sketch.sum(), 0.0);
1221    }
1222
1223    #[test]
1224    fn test_ddsketch_alpha() {
1225        let sketch = DDSketch2048::new(0.05);
1226        assert!((sketch.alpha() - 0.05).abs() < f64::EPSILON);
1227    }
1228
1229    #[test]
1230    fn test_ddsketch_merge() {
1231        let mut s1 = DDSketch2048::new(0.01);
1232        let mut s2 = DDSketch2048::new(0.01);
1233        for v in [10.0, 20.0, 30.0] {
1234            s1.insert(v);
1235        }
1236        for v in [40.0, 50.0] {
1237            s2.insert(v);
1238        }
1239        s1.merge(&s2);
1240        assert_eq!(s1.count(), 5);
1241        assert!((s1.sum() - 150.0).abs() < 0.001);
1242        assert_eq!(s1.min(), 10.0);
1243        assert_eq!(s1.max(), 50.0);
1244    }
1245
1246    #[test]
1247    fn test_ddsketch128() {
1248        let mut sketch = DDSketch128::new(0.1);
1249        sketch.insert(100.0);
1250        assert_eq!(sketch.count(), 1);
1251    }
1252
1253    #[test]
1254    fn test_ddsketch_negative() {
1255        let mut sketch = DDSketch2048::new(0.01);
1256        sketch.insert(-5.0);
1257        sketch.insert(-10.0);
1258        sketch.insert(0.0);
1259        assert_eq!(sketch.count(), 3);
1260    }
1261
1262    #[test]
1263    fn test_countmin_empty() {
1264        let cms = CountMinSketch1024x5::new();
1265        assert_eq!(cms.estimate_hash(42), 0);
1266        assert_eq!(cms.total(), 0);
1267    }
1268
1269    #[test]
1270    fn test_countmin_total() {
1271        let mut cms = CountMinSketch1024x5::new();
1272        cms.insert_hash(1, 10);
1273        cms.insert_hash(2, 20);
1274        assert_eq!(cms.total(), 30);
1275    }
1276
1277    #[test]
1278    fn test_countmin_clear() {
1279        let mut cms = CountMinSketch1024x5::new();
1280        cms.insert_hash(1, 100);
1281        assert!(cms.estimate_hash(1) >= 100);
1282        cms.clear();
1283        assert_eq!(cms.estimate_hash(1), 0);
1284        assert_eq!(cms.total(), 0);
1285    }
1286
1287    #[test]
1288    fn test_countmin_default() {
1289        let cms = CountMinSketch1024x5::default();
1290        assert_eq!(cms.total(), 0);
1291    }
1292
1293    #[test]
1294    fn test_countmin_insert_generic() {
1295        let mut cms = CountMinSketch1024x5::new();
1296        cms.insert(&"hello");
1297        assert!(cms.estimate(&"hello") >= 1);
1298    }
1299
1300    #[test]
1301    fn test_countmin_insert_bytes() {
1302        let mut cms = CountMinSketch1024x5::new();
1303        cms.insert_bytes(b"test_key");
1304        assert!(cms.estimate_bytes(b"test_key") >= 1);
1305    }
1306
1307    #[test]
1308    fn test_countmin_error_bound() {
1309        let cms = CountMinSketch1024x5::new();
1310        let error = cms.error_bound();
1311        assert!(error > 0.0);
1312    }
1313
1314    #[test]
1315    fn test_countmin_confidence() {
1316        let cms = CountMinSketch1024x5::new();
1317        let conf = cms.confidence();
1318        assert!(conf > 0.0 && conf < 1.0);
1319    }
1320
1321    #[test]
1322    fn test_countmin_2048x7() {
1323        let mut cms = CountMinSketch2048x7::new();
1324        cms.insert_hash(42, 5);
1325        assert!(cms.estimate_hash(42) >= 5);
1326    }
1327
1328    #[test]
1329    fn test_countmin_4096x5() {
1330        let mut cms = CountMinSketch4096x5::new();
1331        cms.insert_hash(42, 5);
1332        assert!(cms.estimate_hash(42) >= 5);
1333    }
1334
1335    #[test]
1336    fn test_heavy_hitters_default() {
1337        let hh = HeavyHitters5::default();
1338        assert_eq!(hh.top().count(), 0);
1339    }
1340
1341    #[test]
1342    fn test_heavy_hitters_clear() {
1343        let mut hh = HeavyHitters5::new();
1344        for _ in 0..10 {
1345            hh.insert_hash(1);
1346        }
1347        assert!(hh.top().count() > 0);
1348        hh.clear();
1349        assert_eq!(hh.top().count(), 0);
1350    }
1351
1352    #[test]
1353    fn test_heavy_hitters_cms_access() {
1354        let mut hh = HeavyHitters5::new();
1355        hh.insert_hash(42);
1356        assert!(hh.cms().estimate_hash(42) >= 1);
1357    }
1358
1359    #[test]
1360    fn test_heavy_hitters_10() {
1361        let mut hh = HeavyHitters10::new();
1362        for i in 0..10u64 {
1363            for _ in 0..(10 - i) {
1364                hh.insert_hash(i);
1365            }
1366        }
1367        assert_eq!(hh.top().count(), 10);
1368    }
1369
1370    #[test]
1371    fn test_heavy_hitters_20() {
1372        let mut hh = HeavyHitters20::new();
1373        hh.insert_hash(1);
1374        assert_eq!(hh.top().count(), 1);
1375    }
1376
1377    #[test]
1378    fn test_fast_log2_approx() {
1379        let approx = fast_log2_approx(1024.0);
1380        assert!((approx - 10.0).abs() < 0.1, "approx = {approx}");
1381    }
1382
1383    #[test]
1384    fn test_fast_log2_approx_one() {
1385        let approx = fast_log2_approx(1.0);
1386        assert!(approx.abs() < 0.01, "approx = {approx}");
1387    }
1388
1389    #[test]
1390    fn test_hyperloglog14_type_alias() {
1391        let mut hll = HyperLogLog::new();
1392        hll.insert_hash(FnvHasher::hash_u64(1));
1393        assert!(hll.cardinality() > 0.0);
1394    }
1395
1396    #[test]
1397    fn test_ddsketch_type_alias() {
1398        let mut sketch = DDSketch::new(0.01);
1399        sketch.insert(42.0);
1400        assert_eq!(sketch.count(), 1);
1401    }
1402
1403    #[test]
1404    fn test_countmin_type_alias() {
1405        let mut cms = CountMinSketch::new();
1406        cms.insert_hash(1, 1);
1407        assert!(cms.estimate_hash(1) >= 1);
1408    }
1409
1410    #[test]
1411    fn test_heavy_hitters_type_alias() {
1412        let mut hh = HeavyHitters::new();
1413        hh.insert_hash(1);
1414        assert_eq!(hh.top().count(), 1);
1415    }
1416}