Skip to main content

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    #[test]
142    fn test_zero_sized() {
143        assert_eq!(std::mem::size_of::<FixedLogarithmicMapping>(), 0);
144    }
145
146    #[test]
147    fn test_matches_logarithmic_mapping() {
148        let fixed = FixedLogarithmicMapping::new();
149        let dynamic = LogarithmicMapping::new(0.01).unwrap();
150
151        // Verify constants match
152        assert!(
153            (fixed.gamma() - dynamic.gamma()).abs() < 1e-10,
154            "gamma mismatch: {} vs {}",
155            fixed.gamma(),
156            dynamic.gamma()
157        );
158        assert!(
159            (fixed.relative_accuracy() - dynamic.relative_accuracy()).abs() < 1e-10,
160            "relative_accuracy mismatch: {} vs {}",
161            fixed.relative_accuracy(),
162            dynamic.relative_accuracy()
163        );
164
165        // Verify index calculations match for various values
166        for &value in &[0.001, 0.1, 1.0, 10.0, 100.0, 1000.0, 1_000_000.0] {
167            let fixed_idx = fixed.index(value);
168            let dynamic_idx = dynamic.index(value);
169            assert_eq!(
170                fixed_idx, dynamic_idx,
171                "index mismatch for value {}: {} vs {}",
172                value, fixed_idx, dynamic_idx
173            );
174        }
175
176        // Verify value calculations match for various indices
177        for idx in -100..100 {
178            let fixed_val = fixed.value(idx);
179            let dynamic_val = dynamic.value(idx);
180            assert!(
181                (fixed_val - dynamic_val).abs() < 1e-10 || (fixed_val / dynamic_val - 1.0).abs() < 1e-10,
182                "value mismatch for index {}: {} vs {}",
183                idx,
184                fixed_val,
185                dynamic_val
186            );
187        }
188    }
189
190    #[test]
191    fn test_index_value_roundtrip() {
192        let mapping = FixedLogarithmicMapping::new();
193
194        for i in -100..100 {
195            let value = mapping.value(i);
196            let recovered_index = mapping.index(value);
197            assert!(
198                (recovered_index - i).abs() <= 1,
199                "index {} -> value {} -> index {}",
200                i,
201                value,
202                recovered_index
203            );
204        }
205    }
206
207    #[test]
208    fn test_proto_roundtrip() {
209        let mapping = FixedLogarithmicMapping::new();
210        let proto = mapping.to_proto();
211
212        assert!(mapping.validate_proto_mapping(&proto).is_ok());
213    }
214}