ddsketch/canonical/mapping/
fixed.rs1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub struct FixedLogarithmicMapping;
28
29impl FixedLogarithmicMapping {
30 const GAMMA: f64 = 1.02020202020202;
33
34 const MULTIPLIER: f64 = 49.99833328888678;
36
37 const RELATIVE_ACCURACY: f64 = 0.01;
39
40 #[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 if !float_eq(proto.gamma, Self::GAMMA) {
103 return Err(ProtoConversionError::GammaMismatch {
104 expected: Self::GAMMA,
105 actual: proto.gamma,
106 });
107 }
108
109 if proto.indexOffset != 0.0 {
111 return Err(ProtoConversionError::NonZeroIndexOffset {
112 actual: proto.indexOffset,
113 });
114 }
115
116 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 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 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 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}