ddsketch/canonical/mapping/
logarithmic.rs1use datadog_protos::sketches::{index_mapping::Interpolation, IndexMapping as ProtoIndexMapping};
2
3use super::IndexMapping;
4use crate::canonical::error::ProtoConversionError;
5use crate::common::float_eq;
6
7#[derive(Clone, Debug, PartialEq)]
12pub struct LogarithmicMapping {
13 gamma: f64,
15
16 multiplier: f64,
18
19 relative_accuracy: f64,
21
22 min_indexable_value: f64,
24
25 max_indexable_value: f64,
27}
28
29impl LogarithmicMapping {
30 pub fn new(relative_accuracy: f64) -> Result<Self, &'static str> {
47 if relative_accuracy <= 0.0 || relative_accuracy >= 1.0 {
48 return Err("relative accuracy must be between 0 and 1 (exclusive)");
49 }
50
51 let gamma = (1.0 + relative_accuracy) / (1.0 - relative_accuracy);
52 if gamma <= 1.0 {
53 return Err("gamma must be greater than 1");
54 }
55
56 let gamma_ln = gamma.ln();
57 let multiplier = 1.0 / gamma_ln;
58
59 let min_indexable_value = f64::MIN_POSITIVE.max(gamma.powf(i32::MIN as f64 + 1.0));
64 let max_indexable_value = gamma.powf(i32::MAX as f64 - 1.0).min(f64::MAX / gamma);
65
66 Ok(Self {
67 gamma,
68 multiplier,
69 relative_accuracy,
70 min_indexable_value,
71 max_indexable_value,
72 })
73 }
74}
75
76impl IndexMapping for LogarithmicMapping {
77 fn index(&self, value: f64) -> i32 {
78 let index = value.ln() * self.multiplier;
79 if index >= 0.0 {
80 index as i32
81 } else {
82 (index as i32) - 1
83 }
84 }
85
86 fn value(&self, index: i32) -> f64 {
87 self.lower_bound(index) * (1.0 + self.relative_accuracy)
88 }
89
90 fn lower_bound(&self, index: i32) -> f64 {
91 (index as f64 / self.multiplier).exp()
92 }
93
94 fn relative_accuracy(&self) -> f64 {
95 self.relative_accuracy
96 }
97
98 fn min_indexable_value(&self) -> f64 {
99 self.min_indexable_value
100 }
101
102 fn max_indexable_value(&self) -> f64 {
103 self.max_indexable_value
104 }
105
106 fn gamma(&self) -> f64 {
107 self.gamma
108 }
109
110 fn index_offset(&self) -> f64 {
111 0.0
112 }
113
114 fn interpolation(&self) -> Interpolation {
115 Interpolation::NONE
116 }
117
118 fn validate_proto_mapping(&self, proto: &ProtoIndexMapping) -> Result<(), ProtoConversionError> {
119 if !float_eq(proto.gamma, self.gamma) {
121 return Err(ProtoConversionError::GammaMismatch {
122 expected: self.gamma,
123 actual: proto.gamma,
124 });
125 }
126
127 if proto.indexOffset != 0.0 {
129 return Err(ProtoConversionError::NonZeroIndexOffset {
130 actual: proto.indexOffset,
131 });
132 }
133
134 let interpolation = proto.interpolation.enum_value_or_default();
136 if interpolation != Interpolation::NONE {
137 return Err(ProtoConversionError::UnsupportedInterpolation {
138 actual: interpolation as i32,
139 });
140 }
141
142 Ok(())
143 }
144
145 fn to_proto(&self) -> ProtoIndexMapping {
146 let mut proto = ProtoIndexMapping::new();
147 proto.gamma = self.gamma;
148 proto.indexOffset = 0.0;
149 proto.interpolation = protobuf::EnumOrUnknown::new(Interpolation::NONE);
150 proto
151 }
152}
153
154impl Default for LogarithmicMapping {
155 fn default() -> Self {
157 Self::new(0.01).expect("0.01 is a valid relative accuracy")
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_new_valid_accuracy() {
167 let mapping = LogarithmicMapping::new(0.01).unwrap();
168 assert!((mapping.relative_accuracy() - 0.01).abs() < 1e-10);
169 }
170
171 #[test]
172 fn test_new_invalid_accuracy_zero() {
173 assert!(LogarithmicMapping::new(0.0).is_err());
174 }
175
176 #[test]
177 fn test_new_invalid_accuracy_one() {
178 assert!(LogarithmicMapping::new(1.0).is_err());
179 }
180
181 #[test]
182 fn test_new_invalid_accuracy_negative() {
183 assert!(LogarithmicMapping::new(-0.1).is_err());
184 }
185
186 #[test]
187 fn test_index_value_roundtrip() {
188 let mapping = LogarithmicMapping::new(0.01).unwrap();
189
190 for i in -100..100 {
192 let value = mapping.value(i);
193 let recovered_index = mapping.index(value);
194 assert!(
196 (recovered_index - i).abs() <= 1,
197 "index {} -> value {} -> index {}",
198 i,
199 value,
200 recovered_index
201 );
202 }
203 }
204
205 #[test]
206 fn test_bounds_ordering() {
207 let mapping = LogarithmicMapping::new(0.01).unwrap();
208
209 for i in -100..100 {
210 let lower = mapping.lower_bound(i);
211 let value = mapping.value(i);
212
213 assert!(
214 lower < value,
215 "lower {} should be < value {} for index {}",
216 lower,
217 value,
218 i
219 );
220 }
221 }
222
223 #[test]
224 fn test_gamma_calculation() {
225 let mapping = LogarithmicMapping::new(0.01).unwrap();
226 let expected_gamma = 1.01 / 0.99;
228 assert!((mapping.gamma() - expected_gamma).abs() < 1e-10);
229 }
230}