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
20impl LogarithmicMapping {
21 pub fn new(relative_accuracy: f64) -> Result<Self, &'static str> {
38 if relative_accuracy <= 0.0 || relative_accuracy >= 1.0 {
39 return Err("relative accuracy must be between 0 and 1 (exclusive)");
40 }
41
42 let gamma = (1.0 + relative_accuracy) / (1.0 - relative_accuracy);
43 if gamma <= 1.0 {
44 return Err("gamma must be greater than 1");
45 }
46
47 let multiplier = 1.0 / gamma.ln();
48
49 Ok(Self { gamma, multiplier })
50 }
51}
52
53impl IndexMapping for LogarithmicMapping {
54 fn index(&self, value: f64) -> i32 {
55 let index = value.ln() * self.multiplier;
56 if index >= 0.0 {
57 index as i32
58 } else {
59 (index as i32) - 1
60 }
61 }
62
63 fn value(&self, index: i32) -> f64 {
64 self.lower_bound(index) * (1.0 + self.relative_accuracy())
65 }
66
67 fn lower_bound(&self, index: i32) -> f64 {
68 (index as f64 / self.multiplier).exp()
69 }
70
71 fn relative_accuracy(&self) -> f64 {
72 (self.gamma - 1.0) / (self.gamma + 1.0)
73 }
74
75 fn min_indexable_value(&self) -> f64 {
76 f64::MIN_POSITIVE.max(self.gamma.powf(i32::MIN as f64 + 1.0))
77 }
78
79 fn max_indexable_value(&self) -> f64 {
80 self.gamma.powf(i32::MAX as f64 - 1.0).min(f64::MAX / self.gamma)
81 }
82
83 fn gamma(&self) -> f64 {
84 self.gamma
85 }
86
87 fn index_offset(&self) -> f64 {
88 0.0
89 }
90
91 fn interpolation(&self) -> Interpolation {
92 Interpolation::NONE
93 }
94
95 fn validate_proto_mapping(&self, proto: &ProtoIndexMapping) -> Result<(), ProtoConversionError> {
96 if !float_eq(proto.gamma, self.gamma) {
98 return Err(ProtoConversionError::GammaMismatch {
99 expected: self.gamma,
100 actual: proto.gamma,
101 });
102 }
103
104 if proto.indexOffset != 0.0 {
106 return Err(ProtoConversionError::NonZeroIndexOffset {
107 actual: proto.indexOffset,
108 });
109 }
110
111 let interpolation = proto.interpolation.enum_value_or_default();
113 if interpolation != Interpolation::NONE {
114 return Err(ProtoConversionError::UnsupportedInterpolation {
115 actual: interpolation as i32,
116 });
117 }
118
119 Ok(())
120 }
121
122 fn to_proto(&self) -> ProtoIndexMapping {
123 let mut proto = ProtoIndexMapping::new();
124 proto.gamma = self.gamma;
125 proto.indexOffset = 0.0;
126 proto.interpolation = protobuf::EnumOrUnknown::new(Interpolation::NONE);
127 proto
128 }
129}
130
131impl Default for LogarithmicMapping {
132 fn default() -> Self {
134 Self::new(0.01).expect("0.01 is a valid relative accuracy")
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_new_valid_accuracy() {
144 let mapping = LogarithmicMapping::new(0.01).unwrap();
145 assert!((mapping.relative_accuracy() - 0.01).abs() < 1e-10);
146 }
147
148 #[test]
149 fn test_new_invalid_accuracy_zero() {
150 assert!(LogarithmicMapping::new(0.0).is_err());
151 }
152
153 #[test]
154 fn test_new_invalid_accuracy_one() {
155 assert!(LogarithmicMapping::new(1.0).is_err());
156 }
157
158 #[test]
159 fn test_new_invalid_accuracy_negative() {
160 assert!(LogarithmicMapping::new(-0.1).is_err());
161 }
162
163 #[test]
164 fn test_index_value_roundtrip() {
165 let mapping = LogarithmicMapping::new(0.01).unwrap();
166
167 for i in -100..100 {
169 let value = mapping.value(i);
170 let recovered_index = mapping.index(value);
171 assert!(
173 (recovered_index - i).abs() <= 1,
174 "index {} -> value {} -> index {}",
175 i,
176 value,
177 recovered_index
178 );
179 }
180 }
181
182 #[test]
183 fn test_bounds_ordering() {
184 let mapping = LogarithmicMapping::new(0.01).unwrap();
185
186 for i in -100..100 {
187 let lower = mapping.lower_bound(i);
188 let value = mapping.value(i);
189
190 assert!(
191 lower < value,
192 "lower {} should be < value {} for index {}",
193 lower,
194 value,
195 i
196 );
197 }
198 }
199
200 #[test]
201 fn test_gamma_calculation() {
202 let mapping = LogarithmicMapping::new(0.01).unwrap();
203 let expected_gamma = 1.01 / 0.99;
205 assert!((mapping.gamma() - expected_gamma).abs() < 1e-10);
206 }
207}