use crate::Mergeable;
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct RelSketch {
sub_bits: u8,
collapse_shift: u8,
pos: BTreeMap<u64, u64>,
neg: BTreeMap<u64, u64>,
zero: u64,
nan: u64,
pos_inf: u64,
neg_inf: u64,
min: f64,
max: f64,
count: u64,
mismatched: bool,
}
impl PartialEq for RelSketch {
fn eq(&self, other: &Self) -> bool {
self.sub_bits == other.sub_bits
&& self.collapse_shift == other.collapse_shift
&& self.mismatched == other.mismatched
&& self.pos == other.pos
&& self.neg == other.neg
&& self.zero == other.zero
&& self.nan == other.nan
&& self.pos_inf == other.pos_inf
&& self.neg_inf == other.neg_inf
&& self.min.to_bits() == other.min.to_bits()
&& self.max.to_bits() == other.max.to_bits()
&& self.count == other.count
}
}
impl Eq for RelSketch {}
impl RelSketch {
pub const MAX_BUCKETS: usize = 1 << 16;
pub fn new(alpha: f64) -> Option<Self> {
if !(alpha > 0.0 && alpha < 1.0) {
return None;
}
let mut s: u8 = 0;
while s < 52 {
let bound = (0.5f64).powi(s as i32 + 1);
if bound <= alpha {
break;
}
s += 1;
}
Self::with_sub_bits(s)
}
pub fn with_sub_bits(sub_bits: u8) -> Option<Self> {
if sub_bits == 0 || sub_bits > 52 {
return None;
}
Some(Self {
sub_bits,
collapse_shift: 0,
pos: BTreeMap::new(),
neg: BTreeMap::new(),
zero: 0,
nan: 0,
pos_inf: 0,
neg_inf: 0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
count: 0,
mismatched: false,
})
}
pub fn guaranteed_alpha(&self) -> f64 {
let eff = self.sub_bits as i32 - self.collapse_shift as i32;
if eff <= 0 {
1.0
} else {
(0.5f64).powi(eff + 1)
}
}
pub const fn sub_bits(&self) -> u8 {
self.sub_bits
}
pub const fn collapse_shift(&self) -> u8 {
self.collapse_shift
}
#[inline]
pub const fn effective_shift(&self) -> u32 {
52 - self.sub_bits as u32 + self.collapse_shift as u32
}
#[inline]
fn key_of_positive(&self, x: f64) -> u64 {
debug_assert!(x > 0.0 && x.is_finite());
x.to_bits() >> self.effective_shift()
}
pub fn add(&mut self, x: f64) {
self.count = self.count.saturating_add(1);
if x.is_nan() {
self.nan = self.nan.saturating_add(1);
return;
}
if x.total_cmp(&self.min).is_lt() {
self.min = x;
}
if x.total_cmp(&self.max).is_gt() {
self.max = x;
}
if x == f64::INFINITY {
self.pos_inf = self.pos_inf.saturating_add(1);
} else if x == f64::NEG_INFINITY {
self.neg_inf = self.neg_inf.saturating_add(1);
} else if x == 0.0 {
self.zero = self.zero.saturating_add(1);
} else if x > 0.0 {
let k = self.key_of_positive(x);
let e = self.pos.entry(k).or_insert(0);
*e = e.saturating_add(1);
self.enforce_bucket_cap();
} else {
let k = self.key_of_positive(-x);
let e = self.neg.entry(k).or_insert(0);
*e = e.saturating_add(1);
self.enforce_bucket_cap();
}
}
fn collapse_map(m: &BTreeMap<u64, u64>) -> BTreeMap<u64, u64> {
let mut out = BTreeMap::new();
for (&k, &c) in m.iter() {
let e = out.entry(k >> 1).or_insert(0u64);
*e = e.saturating_add(c);
}
out
}
fn enforce_bucket_cap(&mut self) {
while self.pos.len() + self.neg.len() > Self::MAX_BUCKETS {
if self.effective_shift() + 1 >= 64 {
break;
}
self.collapse_shift += 1;
self.pos = Self::collapse_map(&self.pos);
self.neg = Self::collapse_map(&self.neg);
}
}
fn rankable(&self) -> u64 {
let mut n = self
.neg_inf
.saturating_add(self.zero)
.saturating_add(self.pos_inf);
for &c in self.pos.values() {
n = n.saturating_add(c);
}
for &c in self.neg.values() {
n = n.saturating_add(c);
}
n
}
fn range_of_key(&self, key: u64) -> (f64, f64) {
let shift = self.effective_shift();
let lo = key
.checked_shl(shift)
.map(f64::from_bits)
.unwrap_or(f64::INFINITY);
let hi = (key + 1)
.checked_shl(shift)
.map(f64::from_bits)
.unwrap_or(f64::INFINITY);
(lo, hi)
}
fn representative(&self, key: u64) -> f64 {
let (lo, hi) = self.range_of_key(key);
if hi.is_finite() {
lo + (hi - lo) * 0.5
} else {
lo
}
}
pub fn quantile(&self, q: f64) -> Option<f64> {
if self.mismatched || !(0.0..=1.0).contains(&q) {
return None;
}
let n = self.rankable();
if n == 0 {
return None;
}
let rank = ((q * n as f64).ceil() as u64).clamp(1, n);
let mut seen = 0u64;
seen = seen.saturating_add(self.neg_inf);
if self.neg_inf > 0 && seen >= rank {
return Some(f64::NEG_INFINITY);
}
for (&key, &c) in self.neg.iter().rev() {
seen = seen.saturating_add(c);
if seen >= rank {
return Some(-self.representative(key));
}
}
seen = seen.saturating_add(self.zero);
if self.zero > 0 && seen >= rank {
return Some(0.0);
}
for (&key, &c) in self.pos.iter() {
seen = seen.saturating_add(c);
if seen >= rank {
return Some(self.representative(key));
}
}
Some(f64::INFINITY)
}
pub const fn count(&self) -> u64 {
self.count
}
pub fn min(&self) -> Option<f64> {
if self.mismatched || self.rankable() == 0 {
None
} else {
Some(self.min)
}
}
pub fn max(&self) -> Option<f64> {
if self.mismatched || self.rankable() == 0 {
None
} else {
Some(self.max)
}
}
pub const fn nan_count(&self) -> u64 {
self.nan
}
pub fn bucket_count(&self) -> usize {
self.pos.len() + self.neg.len()
}
pub fn otel_scale(&self) -> i32 {
self.sub_bits as i32 - self.collapse_shift as i32
}
pub fn otel_positive_indices(&self) -> Vec<(i64, u64)> {
let one_key = 1.0f64.to_bits() >> self.effective_shift();
self.pos
.iter()
.map(|(&k, &c)| (k as i64 - one_key as i64, c))
.collect()
}
pub fn otel_negative_indices(&self) -> Vec<(i64, u64)> {
let one_key = 1.0f64.to_bits() >> self.effective_shift();
self.neg
.iter()
.map(|(&k, &c)| (k as i64 - one_key as i64, c))
.collect()
}
pub fn from_otel(
sub_bits: u8,
positive: &[(i64, u64)],
negative: &[(i64, u64)],
) -> Option<Self> {
let mut s = Self::with_sub_bits(sub_bits)?;
let one_key = (1.0f64.to_bits() >> s.effective_shift()) as i64;
let max_key = f64::MAX.to_bits() >> s.effective_shift();
fn build(arr: &[(i64, u64)], one_key: i64, max_key: u64) -> Option<BTreeMap<u64, u64>> {
let mut m = BTreeMap::new();
let mut prev: Option<i64> = None;
for &(idx, c) in arr {
if c == 0 {
return None;
}
if prev.is_some_and(|p| idx <= p) {
return None; }
prev = Some(idx);
let key = one_key.checked_add(idx)?;
if key < 0 || key as u64 > max_key {
return None; }
m.insert(key as u64, c);
}
Some(m)
}
s.pos = build(positive, one_key, max_key)?;
s.neg = build(negative, one_key, max_key)?;
s.count = s
.pos
.values()
.chain(s.neg.values())
.fold(0u64, |a, &c| a.saturating_add(c));
Some(s)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(self.sub_bits);
out.push(self.collapse_shift);
out.push(self.mismatched as u8);
out.extend_from_slice(&self.nan.to_le_bytes());
out.extend_from_slice(&self.pos_inf.to_le_bytes());
out.extend_from_slice(&self.neg_inf.to_le_bytes());
out.extend_from_slice(&self.zero.to_le_bytes());
out.extend_from_slice(&self.min.to_bits().to_le_bytes());
out.extend_from_slice(&self.max.to_bits().to_le_bytes());
out.extend_from_slice(&self.count.to_le_bytes());
Self::write_map(&mut out, &self.pos);
Self::write_map(&mut out, &self.neg);
out
}
fn write_map(out: &mut Vec<u8>, m: &BTreeMap<u64, u64>) {
write_uvarint(out, m.len() as u64);
let mut prev: Option<u64> = None;
for (&k, &c) in m.iter() {
let delta = match prev {
None => k,
Some(p) => k - p, };
write_uvarint(out, delta);
write_uvarint(out, c);
prev = Some(k);
}
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
const HEAD: usize = 1 + 1 + 1 + 8 * 4 + 8 * 2 + 8;
if bytes.len() < HEAD {
return None;
}
let sub_bits = bytes[0];
if sub_bits == 0 || sub_bits > 52 {
return None;
}
let collapse_shift = bytes[1];
if 52 - sub_bits as u32 + collapse_shift as u32 >= 64 {
return None;
}
let mismatched = match bytes[2] {
0 => false,
1 => true,
_ => return None,
};
let mut at = 3;
let rd8 = |bytes: &[u8], at: &mut usize| -> u64 {
let mut w = [0u8; 8];
w.copy_from_slice(&bytes[*at..*at + 8]);
*at += 8;
u64::from_le_bytes(w)
};
let nan = rd8(bytes, &mut at);
let pos_inf = rd8(bytes, &mut at);
let neg_inf = rd8(bytes, &mut at);
let zero = rd8(bytes, &mut at);
let min = f64::from_bits(rd8(bytes, &mut at));
let max = f64::from_bits(rd8(bytes, &mut at));
let count = rd8(bytes, &mut at);
let pos = read_map(bytes, &mut at)?;
let neg = read_map(bytes, &mut at)?;
if at != bytes.len() {
return None; }
let max_key = f64::MAX.to_bits() >> (52 - sub_bits as u32 + collapse_shift as u32);
if pos.keys().next_back().is_some_and(|&k| k > max_key)
|| neg.keys().next_back().is_some_and(|&k| k > max_key)
{
return None;
}
Some(Self {
sub_bits,
collapse_shift,
pos,
neg,
zero,
nan,
pos_inf,
neg_inf,
min,
max,
count,
mismatched,
})
}
}
fn write_uvarint(out: &mut Vec<u8>, mut v: u64) {
loop {
let byte = (v & 0x7f) as u8;
v >>= 7;
if v == 0 {
out.push(byte);
return;
}
out.push(byte | 0x80);
}
}
fn read_uvarint(bytes: &[u8], at: &mut usize) -> Option<u64> {
let mut result: u64 = 0;
let mut shift: u32 = 0;
loop {
let b = *bytes.get(*at)?;
*at += 1;
let low = (b & 0x7f) as u64;
if shift >= 64 {
return None; }
if shift == 63 && low > 1 {
return None;
}
result |= low << shift;
if b & 0x80 == 0 {
if b == 0 && shift != 0 {
return None;
}
return Some(result);
}
shift += 7;
}
}
fn read_map(bytes: &[u8], at: &mut usize) -> Option<BTreeMap<u64, u64>> {
let n = read_uvarint(bytes, at)?;
let mut map = BTreeMap::new();
let mut prev: Option<u64> = None;
for _ in 0..n {
let delta = read_uvarint(bytes, at)?;
let key = match prev {
None => delta,
Some(p) => {
if delta == 0 {
return None; }
p.checked_add(delta)? }
};
let cnt = read_uvarint(bytes, at)?;
if cnt == 0 {
return None; }
prev = Some(key);
map.insert(key, cnt);
}
Some(map)
}
impl Mergeable for RelSketch {
fn merge(&mut self, other: &Self) {
if self.sub_bits != other.sub_bits {
self.mismatched = true;
return;
}
self.mismatched |= other.mismatched;
let target = self.collapse_shift.max(other.collapse_shift);
while self.collapse_shift < target {
self.collapse_shift += 1;
self.pos = Self::collapse_map(&self.pos);
self.neg = Self::collapse_map(&self.neg);
}
let (mut o_pos, mut o_neg) = (other.pos.clone(), other.neg.clone());
let mut oc = other.collapse_shift;
while oc < target {
o_pos = Self::collapse_map(&o_pos);
o_neg = Self::collapse_map(&o_neg);
oc += 1;
}
for (&k, &c) in o_pos.iter() {
let e = self.pos.entry(k).or_insert(0);
*e = e.saturating_add(c);
}
for (&k, &c) in o_neg.iter() {
let e = self.neg.entry(k).or_insert(0);
*e = e.saturating_add(c);
}
self.zero = self.zero.saturating_add(other.zero);
self.nan = self.nan.saturating_add(other.nan);
self.pos_inf = self.pos_inf.saturating_add(other.pos_inf);
self.neg_inf = self.neg_inf.saturating_add(other.neg_inf);
if other.min.total_cmp(&self.min).is_lt() {
self.min = other.min;
}
if other.max.total_cmp(&self.max).is_gt() {
self.max = other.max;
}
self.count = self.count.saturating_add(other.count);
self.enforce_bucket_cap();
}
fn count(&self) -> u64 {
self.count
}
#[cfg(feature = "std")]
fn encode(&self) -> Vec<u8> {
self.to_bytes()
}
#[cfg(feature = "std")]
fn decode(bytes: &[u8]) -> Option<Self> {
Self::from_bytes(bytes)
}
}
impl FromIterator<f64> for RelSketch {
fn from_iter<I: IntoIterator<Item = f64>>(iter: I) -> Self {
let mut s = Self::new(0.01).expect("alpha 0.01 is valid");
for x in iter {
s.add(x);
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_reads_are_none() {
let s = RelSketch::new(0.01).unwrap();
assert_eq!(s.quantile(0.5), None);
assert_eq!(s.min(), None);
assert_eq!(s.max(), None);
assert_eq!(s.count(), 0);
}
#[test]
fn alpha_to_sub_bits() {
assert_eq!(RelSketch::new(0.01).unwrap().sub_bits(), 6);
assert!(RelSketch::new(0.01).unwrap().guaranteed_alpha() <= 0.01);
assert!(RelSketch::new(0.001).unwrap().guaranteed_alpha() <= 0.001);
assert_eq!(RelSketch::new(0.0), None);
assert_eq!(RelSketch::new(1.0), None);
}
#[test]
fn monotone_quantiles() {
let mut s = RelSketch::new(0.01).unwrap();
for i in 1..=1000 {
s.add(i as f64);
}
let (mut prev, qs) = (f64::NEG_INFINITY, [0.1, 0.5, 0.9, 0.99]);
for &q in &qs {
let v = s.quantile(q).unwrap();
assert!(v >= prev, "quantiles must be nondecreasing");
prev = v;
}
assert_eq!(s.min(), Some(1.0));
assert_eq!(s.max(), Some(1000.0));
}
#[test]
fn specials_are_out_of_band() {
let mut s = RelSketch::new(0.01).unwrap();
for x in [
1.0,
2.0,
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
0.0,
-3.0,
] {
s.add(x);
}
assert_eq!(s.nan_count(), 1);
assert_eq!(s.count(), 7);
assert_eq!(s.quantile(0.0), Some(f64::NEG_INFINITY));
assert_eq!(s.quantile(1.0), Some(f64::INFINITY));
}
#[test]
fn roundtrip_bytes() {
let mut s = RelSketch::new(0.005).unwrap();
for i in 0..500 {
s.add((i as f64) * 0.5 - 100.0);
}
let bytes = s.to_bytes();
let back = RelSketch::from_bytes(&bytes).unwrap();
assert_eq!(s, back);
assert_eq!(bytes, back.to_bytes());
assert_eq!(RelSketch::from_bytes(&bytes[..bytes.len() - 1]), None);
}
#[test]
fn merge_mismatch_poisons() {
let mut a = RelSketch::with_sub_bits(6).unwrap();
let b = RelSketch::with_sub_bits(7).unwrap();
a.add(1.0);
a.merge(&b);
assert_eq!(a.quantile(0.5), None);
}
#[test]
fn varint_roundtrip_minimal() {
for v in [0u64, 1, 127, 128, 300, u64::MAX, u64::MAX - 1, 1 << 63] {
let mut buf = Vec::new();
write_uvarint(&mut buf, v);
let mut at = 0;
assert_eq!(read_uvarint(&buf, &mut at), Some(v));
assert_eq!(at, buf.len());
}
let mut at = 0;
assert_eq!(read_uvarint(&[0x80, 0x00], &mut at), None);
let mut at = 0;
assert_eq!(read_uvarint(&[0x81, 0x00], &mut at), None);
}
}