ddsketch/canonical/mapping/
fixed.rs

1//! Fixed logarithmic index mapping with hardcoded 1% relative accuracy.
2
3use datadog_protos::sketches::{index_mapping::Interpolation, IndexMapping as ProtoIndexMapping};
4
5use super::IndexMapping;
6use crate::canonical::error::ProtoConversionError;
7use crate::common::float_eq;
8
9/// A zero-sized logarithmic index mapping with hardcoded 1% relative accuracy.
10///
11/// This mapping is functionally identical to `LogarithmicMapping::new(0.01)` but uses compile-time constants instead of
12/// storing gamma and multiplier as fields. This makes the type zero-sized, saving 16 bytes per DDSketch instance.
13///
14/// Use this mapping when you know all your sketches will use 1% relative accuracy (the common default).
15///
16/// # Example
17///
18/// ```
19/// use ddsketch::canonical::mapping::FixedLogarithmicMapping;
20/// use ddsketch::canonical::DDSketch;
21/// use ddsketch::canonical::store::CollapsingLowestDenseStore;
22///
23/// // Create a sketch with the fixed mapping (saves 16 bytes vs LogarithmicMapping)
24/// let sketch: DDSketch<FixedLogarithmicMapping, CollapsingLowestDenseStore> = DDSketch::default();
25/// ```
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub struct FixedLogarithmicMapping;
28
29impl FixedLogarithmicMapping {
30    // For alpha = 0.01 (1% relative accuracy):
31    // gamma = (1 + alpha) / (1 - alpha) = 1.01 / 0.99
32    const GAMMA: f64 = 1.02020202020202;
33
34    // multiplier = 1 / ln(gamma)
35    const MULTIPLIER: f64 = 49.99833328888678;
36
37    // The relative accuracy this mapping provides
38    const RELATIVE_ACCURACY: f64 = 0.01;
39
40    /// Creates a new `FixedLogarithmicMapping`.
41    ///
42    /// This is a no-op since the type is zero-sized, but provided for API consistency.
43    #[inline]
44    pub const fn new() -> Self {
45        Self
46    }
47}
48
49impl IndexMapping for FixedLogarithmicMapping {
50    #[inline]
51    fn index(&self, value: f64) -> i32 {
52        let index = value.ln() * Self::MULTIPLIER;
53        if index >= 0.0 {
54            index as i32
55        } else {
56            (index as i32) - 1
57        }
58    }
59
60    #[inline]
61    fn value(&self, index: i32) -> f64 {
62        self.lower_bound(index) * (1.0 + self.relative_accuracy())
63    }
64
65    #[inline]
66    fn lower_bound(&self, index: i32) -> f64 {
67        (index as f64 / Self::MULTIPLIER).exp()
68    }
69
70    #[inline]
71    fn relative_accuracy(&self) -> f64 {
72        Self::RELATIVE_ACCURACY
73    }
74
75    #[inline]
76    fn min_indexable_value(&self) -> f64 {
77        f64::MIN_POSITIVE.max(Self::GAMMA.powf(i32::MIN as f64 + 1.0))
78    }
79
80    #[inline]
81    fn max_indexable_value(&self) -> f64 {
82        Self::GAMMA.powf(i32::MAX as f64 - 1.0).min(f64::MAX / Self::GAMMA)
83    }
84
85    #[inline]
86    fn gamma(&self) -> f64 {
87        Self::GAMMA
88    }
89
90    #[inline]
91    fn index_offset(&self) -> f64 {
92        0.0
93    }
94
95    #[inline]
96    fn interpolation(&self) -> Interpolation {
97        Interpolation::NONE
98    }
99
100    fn validate_proto_mapping(&self, proto: &ProtoIndexMapping) -> Result<(), ProtoConversionError> {
101        // Check gamma matches (with floating-point tolerance)
102        if !float_eq(proto.gamma, Self::GAMMA) {
103            return Err(ProtoConversionError::GammaMismatch {
104                expected: Self::GAMMA,
105                actual: proto.gamma,
106            });
107        }
108
109        // Check indexOffset is 0.0 (LogarithmicMapping doesn't use offset)
110        if proto.indexOffset != 0.0 {
111            return Err(ProtoConversionError::NonZeroIndexOffset {
112                actual: proto.indexOffset,
113            });
114        }
115
116        // Check interpolation is NONE (LogarithmicMapping uses exact log)
117        let interpolation = proto.interpolation.enum_value_or_default();
118        if interpolation != Interpolation::NONE {
119            return Err(ProtoConversionError::UnsupportedInterpolation {
120                actual: interpolation as i32,
121            });
122        }
123
124        Ok(())
125    }
126
127    fn to_proto(&self) -> ProtoIndexMapping {
128        let mut proto = ProtoIndexMapping::new();
129        proto.gamma = Self::GAMMA;
130        proto.indexOffset = 0.0;
131        proto.interpolation = protobuf::EnumOrUnknown::new(Interpolation::NONE);
132        proto
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::canonical::mapping::LogarithmicMapping;
140
141    // Shared `IndexMapping` conformance suite (round-trip, bound ordering, gamma/accuracy consistency, proto
142    // self-validation). This subsumes the former per-impl index/value round-trip and proto round-trip tests.
143    #[test]
144    fn conforms_to_index_mapping_contract() {
145        let mapping = FixedLogarithmicMapping::new();
146        crate::canonical::mapping::conformance::assert_index_mapping_conformance(&mapping, 0.01);
147    }
148
149    // Being zero-sized is the entire reason this mapping exists, so it's asserted directly rather than via the
150    // shared conformance suite.
151    #[test]
152    fn is_zero_sized() {
153        assert_eq!(std::mem::size_of::<FixedLogarithmicMapping>(), 0);
154    }
155
156    #[test]
157    fn matches_logarithmic_mapping() {
158        let fixed = FixedLogarithmicMapping::new();
159        let dynamic = LogarithmicMapping::new(0.01).unwrap();
160
161        // Verify constants match
162        assert!(
163            (fixed.gamma() - dynamic.gamma()).abs() < 1e-10,
164            "gamma mismatch: {} vs {}",
165            fixed.gamma(),
166            dynamic.gamma()
167        );
168        assert!(
169            (fixed.relative_accuracy() - dynamic.relative_accuracy()).abs() < 1e-10,
170            "relative_accuracy mismatch: {} vs {}",
171            fixed.relative_accuracy(),
172            dynamic.relative_accuracy()
173        );
174
175        // Verify index calculations match for various values
176        for &value in &[0.001, 0.1, 1.0, 10.0, 100.0, 1000.0, 1_000_000.0] {
177            let fixed_idx = fixed.index(value);
178            let dynamic_idx = dynamic.index(value);
179            assert_eq!(
180                fixed_idx, dynamic_idx,
181                "index mismatch for value {}: {} vs {}",
182                value, fixed_idx, dynamic_idx
183            );
184        }
185
186        // Verify value calculations match for various indices
187        for idx in -100..100 {
188            let fixed_val = fixed.value(idx);
189            let dynamic_val = dynamic.value(idx);
190            assert!(
191                (fixed_val - dynamic_val).abs() < 1e-10 || (fixed_val / dynamic_val - 1.0).abs() < 1e-10,
192                "value mismatch for index {}: {} vs {}",
193                idx,
194                fixed_val,
195                dynamic_val
196            );
197        }
198    }
199}