1use std::{error::Error, fmt, mem};
4
5#[derive(Clone, Debug, PartialEq)]
7pub struct QuantilePolicy {
8 pub rank_error: f64,
10 pub exact_threshold: usize,
12 pub max_summary_entries: usize,
16}
17
18impl QuantilePolicy {
19 pub fn new(
21 rank_error: f64,
22 exact_threshold: usize,
23 max_summary_entries: usize,
24 ) -> Result<Self, QuantileError> {
25 let policy = Self {
26 rank_error,
27 exact_threshold,
28 max_summary_entries,
29 };
30 policy.validate()?;
31 Ok(policy)
32 }
33
34 fn validate(&self) -> Result<(), QuantileError> {
35 if !self.rank_error.is_finite() || !(0.0..0.5).contains(&self.rank_error) {
36 return Err(QuantileError::InvalidPolicy {
37 field: "rank_error",
38 reason: "must be finite and in the half-open interval [0, 0.5)",
39 });
40 }
41 if self.exact_threshold == 0 {
42 return Err(QuantileError::InvalidPolicy {
43 field: "exact_threshold",
44 reason: "must be greater than zero",
45 });
46 }
47 if self.max_summary_entries < self.exact_threshold {
48 return Err(QuantileError::InvalidPolicy {
49 field: "max_summary_entries",
50 reason: "must be at least exact_threshold",
51 });
52 }
53 Ok(())
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct QuantileEstimate {
60 pub quantile: f64,
62 pub value: f64,
64 pub rank_lower: f64,
66 pub rank_upper: f64,
68 pub observations: u64,
70 pub retained_entries: usize,
72 pub exact: bool,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
77struct SummaryEntry {
78 value: f64,
79 gap: u64,
80 slack: u64,
81}
82
83#[derive(Clone, Debug, PartialEq)]
91pub struct QuantileSketch {
92 policy: QuantilePolicy,
93 observations: u64,
94 exact_values: Option<Vec<f64>>,
95 summary: Vec<SummaryEntry>,
96}
97
98impl QuantileSketch {
99 pub fn new(policy: QuantilePolicy) -> Result<Self, QuantileError> {
101 policy.validate()?;
102 Ok(Self {
103 policy,
104 observations: 0,
105 exact_values: Some(Vec::new()),
106 summary: Vec::new(),
107 })
108 }
109
110 pub fn policy(&self) -> &QuantilePolicy {
112 &self.policy
113 }
114
115 pub fn len(&self) -> u64 {
117 self.observations
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.observations == 0
123 }
124
125 pub fn retained_entries(&self) -> usize {
127 self.exact_values
128 .as_ref()
129 .map_or(self.summary.len(), Vec::len)
130 }
131
132 pub fn retained_entry_bytes(&self) -> usize {
135 if let Some(values) = &self.exact_values {
136 values.len() * mem::size_of::<f64>()
137 } else {
138 self.summary.len() * mem::size_of::<SummaryEntry>()
139 }
140 }
141
142 pub fn insert(&mut self, value: f64) -> Result<(), QuantileError> {
146 if !value.is_finite() {
147 return Err(QuantileError::NonFinite { index: None, value });
148 }
149 let mut candidate = self.clone();
150 candidate.insert_unchecked(value)?;
151 *self = candidate;
152 Ok(())
153 }
154
155 pub fn merge(&mut self, other: &Self) -> Result<(), QuantileError> {
157 if self.policy != other.policy {
158 return Err(QuantileError::IncompatiblePolicy);
159 }
160 if other.is_empty() {
161 return Ok(());
162 }
163 if self.is_empty() {
164 *self = other.clone();
165 return Ok(());
166 }
167 let mut candidate = self.clone();
168 candidate.merge_unchecked(other)?;
169 *self = candidate;
170 Ok(())
171 }
172
173 pub fn estimate(&self, quantile: f64) -> Result<QuantileEstimate, QuantileError> {
175 validate_quantile(quantile)?;
176 if self.is_empty() {
177 return Err(QuantileError::EmptyInput);
178 }
179 if let Some(values) = &self.exact_values {
180 let value = exact_quantile(values, quantile)?;
181 let normalized_rank = if values.len() == 1 { 0.0 } else { quantile };
182 return Ok(QuantileEstimate {
183 quantile,
184 value,
185 rank_lower: normalized_rank,
186 rank_upper: normalized_rank,
187 observations: self.observations,
188 retained_entries: values.len(),
189 exact: true,
190 });
191 }
192
193 let last = self.summary.len() - 1;
194 let selected = if quantile == 0.0 {
195 0
196 } else if quantile == 1.0 {
197 last
198 } else {
199 self.summary_index(quantile)
200 };
201 let (rank_lower, rank_upper) = self.rank_interval(selected);
202 Ok(QuantileEstimate {
203 quantile,
204 value: self.summary[selected].value,
205 rank_lower,
206 rank_upper,
207 observations: self.observations,
208 retained_entries: self.summary.len(),
209 exact: false,
210 })
211 }
212
213 fn insert_unchecked(&mut self, value: f64) -> Result<(), QuantileError> {
214 if let Some(values) = &mut self.exact_values {
215 values.push(value);
216 self.observations = self
217 .observations
218 .checked_add(1)
219 .ok_or(QuantileError::CountOverflow)?;
220 if values.len() <= self.policy.exact_threshold {
221 return Ok(());
222 }
223 let exact = self.exact_values.take().unwrap_or_default();
224 self.observations = 0;
225 for exact_value in exact {
226 self.insert_summary_value(exact_value)?;
227 }
228 return Ok(());
229 }
230 self.insert_summary_value(value)
231 }
232
233 fn insert_summary_value(&mut self, value: f64) -> Result<(), QuantileError> {
234 self.observations = self
235 .observations
236 .checked_add(1)
237 .ok_or(QuantileError::CountOverflow)?;
238 let position = self.summary.partition_point(|entry| entry.value <= value);
239 let slack = if position == 0 || position == self.summary.len() {
240 0
241 } else {
242 self.allowable_error().saturating_sub(1)
243 };
244 self.summary.insert(
245 position,
246 SummaryEntry {
247 value,
248 gap: 1,
249 slack,
250 },
251 );
252 self.compress()?;
253 self.check_memory()
254 }
255
256 fn merge_unchecked(&mut self, other: &Self) -> Result<(), QuantileError> {
257 let combined = self
258 .observations
259 .checked_add(other.observations)
260 .ok_or(QuantileError::CountOverflow)?;
261 if combined <= self.policy.exact_threshold as u64 {
262 let mut values = self.exact_values.take().unwrap_or_default();
263 values.extend(other.exact_values.as_deref().unwrap_or_default());
264 self.observations = combined;
265 self.exact_values = Some(values);
266 self.summary.clear();
267 return Ok(());
268 }
269
270 let own_entries = self.entries_for_merge();
271 let other_entries = other.entries_for_merge();
272 let mut merged = Vec::with_capacity(own_entries.len() + other_entries.len());
273 for (entries, other_entries) in [
274 (own_entries.as_slice(), other_entries.as_slice()),
275 (other_entries.as_slice(), own_entries.as_slice()),
276 ] {
277 for entry in entries.iter().copied() {
278 merged.push(SummaryEntry {
279 slack: entry
280 .slack
281 .saturating_add(cross_rank_uncertainty(entry.value, other_entries)),
282 ..entry
283 });
284 }
285 }
286 merged.sort_by(|left, right| left.value.total_cmp(&right.value));
287 if let Some(first) = merged.first_mut() {
288 first.slack = 0;
289 }
290 if let Some(last) = merged.last_mut() {
291 last.slack = 0;
292 }
293 self.observations = combined;
294 self.exact_values = None;
295 self.summary = merged;
296 self.compress()?;
297 self.check_memory()
298 }
299
300 fn entries_for_merge(&self) -> Vec<SummaryEntry> {
301 self.exact_values.as_ref().map_or_else(
302 || self.summary.clone(),
303 |values| {
304 let mut values = values.clone();
305 values.sort_by(f64::total_cmp);
306 values
307 .into_iter()
308 .map(|value| SummaryEntry {
309 value,
310 gap: 1,
311 slack: 0,
312 })
313 .collect()
314 },
315 )
316 }
317
318 fn compress(&mut self) -> Result<(), QuantileError> {
319 if self.summary.len() < 3 {
320 return Ok(());
321 }
322 let allowable = self.allowable_error();
323 let mut index = self.summary.len() - 2;
324 while index > 0 {
325 let combined = self.summary[index]
326 .gap
327 .checked_add(self.summary[index + 1].gap)
328 .and_then(|value| value.checked_add(self.summary[index + 1].slack))
329 .ok_or(QuantileError::CountOverflow)?;
330 if combined <= allowable {
331 let removed = self.summary.remove(index);
332 self.summary[index].gap = self.summary[index]
333 .gap
334 .checked_add(removed.gap)
335 .ok_or(QuantileError::CountOverflow)?;
336 }
337 index -= 1;
338 }
339 Ok(())
340 }
341
342 fn summary_index(&self, quantile: f64) -> usize {
343 let target = quantile.mul_add(self.observations.saturating_sub(1) as f64, 1.0);
344 let permitted = self.policy.rank_error * self.observations as f64;
345 let mut minimum_rank = 0_u64;
346 let mut previous = 0;
347 for (index, entry) in self.summary.iter().enumerate() {
348 minimum_rank = minimum_rank.saturating_add(entry.gap);
349 let maximum_rank = minimum_rank.saturating_add(entry.slack);
350 if maximum_rank as f64 > target + permitted {
351 return previous;
352 }
353 previous = index;
354 }
355 self.summary.len() - 1
356 }
357
358 fn rank_interval(&self, selected: usize) -> (f64, f64) {
359 if self.observations <= 1 {
360 return (0.0, 0.0);
361 }
362 let minimum = self.summary[..=selected]
363 .iter()
364 .map(|entry| entry.gap)
365 .sum::<u64>();
366 let maximum = minimum.saturating_add(self.summary[selected].slack);
367 let denominator = (self.observations - 1) as f64;
368 (
369 minimum.saturating_sub(1) as f64 / denominator,
370 maximum.saturating_sub(1) as f64 / denominator,
371 )
372 }
373
374 fn allowable_error(&self) -> u64 {
375 allowance(self.policy.rank_error, self.observations)
376 }
377
378 fn check_memory(&self) -> Result<(), QuantileError> {
379 if self.summary.len() > self.policy.max_summary_entries {
380 return Err(QuantileError::MemoryLimit {
381 required_entries: self.summary.len(),
382 max_entries: self.policy.max_summary_entries,
383 });
384 }
385 Ok(())
386 }
387}
388
389pub fn exact_quantile(values: &[f64], quantile: f64) -> Result<f64, QuantileError> {
391 validate_quantile(quantile)?;
392 if values.is_empty() {
393 return Err(QuantileError::EmptyInput);
394 }
395 let mut ordered = values.to_vec();
396 for (index, value) in ordered.iter().copied().enumerate() {
397 if !value.is_finite() {
398 return Err(QuantileError::NonFinite {
399 index: Some(index),
400 value,
401 });
402 }
403 }
404 ordered.sort_by(f64::total_cmp);
405 let position = quantile * (ordered.len() - 1) as f64;
406 let lower = position.floor() as usize;
407 let upper = position.ceil() as usize;
408 let fraction = position - lower as f64;
409 Ok((ordered[upper] - ordered[lower]).mul_add(fraction, ordered[lower]))
410}
411
412#[derive(Clone, Debug, PartialEq)]
414pub enum QuantileError {
415 EmptyInput,
417 InvalidPolicy {
419 field: &'static str,
421 reason: &'static str,
423 },
424 QuantileOutOfRange {
426 quantile: f64,
428 },
429 NonFinite {
431 index: Option<usize>,
433 value: f64,
435 },
436 IncompatiblePolicy,
438 MemoryLimit {
440 required_entries: usize,
442 max_entries: usize,
444 },
445 CountOverflow,
447}
448
449impl fmt::Display for QuantileError {
450 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
451 match self {
452 Self::EmptyInput => write!(formatter, "quantile input must not be empty"),
453 Self::InvalidPolicy { field, reason } => {
454 write!(formatter, "invalid quantile policy {field}: {reason}")
455 }
456 Self::QuantileOutOfRange { quantile } => {
457 write!(formatter, "quantile outside finite 0..=1 range: {quantile}")
458 }
459 Self::NonFinite { index, value } => match index {
460 Some(index) => write!(formatter, "quantile value {index} is not finite: {value}"),
461 None => write!(formatter, "quantile observation is not finite: {value}"),
462 },
463 Self::IncompatiblePolicy => write!(formatter, "quantile policies do not match"),
464 Self::MemoryLimit {
465 required_entries,
466 max_entries,
467 } => write!(
468 formatter,
469 "quantile summary requires {required_entries} entries, limit is {max_entries}"
470 ),
471 Self::CountOverflow => write!(formatter, "quantile observation count overflow"),
472 }
473 }
474}
475
476impl Error for QuantileError {}
477
478fn validate_quantile(quantile: f64) -> Result<(), QuantileError> {
479 if quantile.is_finite() && (0.0..=1.0).contains(&quantile) {
480 Ok(())
481 } else {
482 Err(QuantileError::QuantileOutOfRange { quantile })
483 }
484}
485
486fn allowance(rank_error: f64, observations: u64) -> u64 {
487 (2.0 * rank_error * observations as f64).floor() as u64
488}
489
490fn cross_rank_uncertainty(value: f64, entries: &[SummaryEntry]) -> u64 {
491 let upper = entries.partition_point(|entry| entry.value <= value);
492 if upper == 0 || upper == entries.len() {
493 0
494 } else {
495 entries[upper]
496 .gap
497 .saturating_add(entries[upper].slack)
498 .saturating_sub(1)
499 }
500}