#![cfg(feature = "quantile")]
use bitrep::{Mergeable, RelSketch};
type Dist = (&'static str, fn(&mut Rng) -> f64);
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed | 1)
}
fn next_u64(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn normal(&mut self) -> f64 {
let (u, v) = (self.unit().max(1e-15), self.unit());
(-2.0 * u.ln()).sqrt() * (std::f64::consts::TAU * v).cos()
}
}
fn lognormal(rng: &mut Rng) -> f64 {
(1.5 + 0.75 * rng.normal()).exp()
}
fn pareto(rng: &mut Rng) -> f64 {
5.0 / rng.unit().max(1e-15).powf(1.0 / 1.5)
}
fn bimodal(rng: &mut Rng) -> f64 {
if rng.unit() < 0.7 {
(2.0 + 0.3 * rng.normal()).exp()
} else {
(5.0 + 0.3 * rng.normal()).exp()
}
}
fn shuffle<T>(v: &mut [T], rng: &mut Rng) {
for i in (1..v.len()).rev() {
let j = (rng.next_u64() % (i as u64 + 1)) as usize;
v.swap(i, j);
}
}
fn build(data: &[f64], alpha: f64) -> RelSketch {
let mut s = RelSketch::new(alpha).unwrap();
for &x in data {
s.add(x);
}
s
}
fn exact_quantile(data: &[f64], q: f64) -> f64 {
let mut v: Vec<f64> = data.iter().copied().filter(|x| !x.is_nan()).collect();
v.sort_by(f64::total_cmp);
let idx = ((q * v.len() as f64).ceil() as usize).clamp(1, v.len()) - 1;
v[idx]
}
#[test]
fn byte_identity_under_order_shard_and_merge() {
let mut rng = Rng::new(0xB17E_1DEA);
let mut data: Vec<f64> = Vec::new();
for _ in 0..20_000 {
data.push(lognormal(&mut rng));
}
for _ in 0..2_000 {
data.push(-pareto(&mut rng)); }
for &x in &[
f64::MIN_POSITIVE, f64::MIN_POSITIVE / 2.0, 5e-324, f64::MAX,
1e300,
1e-300,
0.0,
-0.0,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
-f64::MAX,
] {
for _ in 0..37 {
data.push(x);
}
}
let reference = build(&data, 0.01);
let ref_bytes = reference.to_bytes();
assert_eq!(RelSketch::from_bytes(&ref_bytes).unwrap(), reference);
for seed in 0..25 {
let mut d = data.clone();
shuffle(&mut d, &mut Rng::new(seed + 1));
assert_eq!(
build(&d, 0.01).to_bytes(),
ref_bytes,
"insertion order changed bytes"
);
}
for k in 1..=13usize {
let mut d = data.clone();
shuffle(&mut d, &mut Rng::new(1000 + k as u64));
let mut shards: Vec<RelSketch> = (0..k).map(|_| RelSketch::new(0.01).unwrap()).collect();
for (i, &x) in d.iter().enumerate() {
shards[i % k].add(x);
}
let mut order: Vec<usize> = (0..k).collect();
shuffle(&mut order, &mut Rng::new(9000 + k as u64));
let mut merged = RelSketch::new(0.01).unwrap();
for &s in &order {
merged.merge(&shards[s]);
}
assert_eq!(merged.to_bytes(), ref_bytes, "sharding K={k} changed bytes");
}
let mut shards: Vec<RelSketch> = data.chunks(777).map(|c| build(c, 0.01)).collect();
while shards.len() > 1 {
let b = shards.pop().unwrap();
let last = shards.len() - 1;
shards[last].merge(&b); }
assert_eq!(
shards[0].to_bytes(),
ref_bytes,
"merge tree shape changed bytes"
);
println!(
"[byte-identity] {} samples, {} buckets, {} bytes: identical across \
25 orders, K=1..13 shardings, and an unbalanced merge tree.",
reference.count(),
reference.bucket_count(),
ref_bytes.len()
);
}
#[test]
fn accuracy_within_guarantee() {
let dists: [Dist; 3] = [
("lognormal", lognormal),
("pareto(1.5)", pareto),
("bimodal", bimodal),
];
let qs = [0.5, 0.9, 0.95, 0.99, 0.999];
let alpha = 0.01;
let n = 1_000_000usize;
println!("\n[accuracy] {n} samples/dist, requested alpha = {alpha}");
for (name, f) in dists {
let mut rng = Rng::new(0xACC0_0000 ^ name.len() as u64);
let data: Vec<f64> = (0..n).map(|_| f(&mut rng)).collect();
let sketch = build(&data, alpha);
let guar = sketch.guaranteed_alpha();
print!(" {name:<12} guarantee {guar:.5} | ");
let mut worst = 0.0f64;
for &q in &qs {
let exact = exact_quantile(&data, q);
let est = sketch.quantile(q).unwrap();
let rel = (est - exact).abs() / exact.abs();
worst = worst.max(rel);
print!("p{}={:.5} ", (q * 1000.0) as u32, rel);
assert!(
rel <= guar * 1.001,
"{name} p{q}: rel err {rel} exceeds guarantee {guar}"
);
}
println!("| worst {worst:.5}");
assert!(worst <= guar * 1.001);
}
}
#[test]
fn merge_equals_concatenation() {
let mut rng = Rng::new(0x3E4D_1234);
let data: Vec<f64> = (0..50_000).map(|_| lognormal(&mut rng)).collect();
let whole = build(&data, 0.005);
let whole_bytes = whole.to_bytes();
for trial in 0..40 {
let k: u64 = 2 + (trial % 11);
let mut splits: Vec<RelSketch> = (0..k).map(|_| RelSketch::new(0.005).unwrap()).collect();
let mut r = Rng::new(0xFEED_0000 + trial);
for &x in &data {
let b = (r.next_u64() % k) as usize;
splits[b].add(x);
}
shuffle(&mut splits, &mut Rng::new(0xBEEF_0000 + trial));
let mut merged = RelSketch::new(0.005).unwrap();
for s in &splits {
merged.merge(s);
}
assert_eq!(
merged.to_bytes(),
whole_bytes,
"trial {trial}: merge != concat"
);
}
println!("\n[merge] 40 random splits (K=2..12): merged state byte-identical to whole.");
}
#[test]
fn size_report() {
let dists: [Dist; 3] = [
("lognormal", lognormal),
("pareto(1.5)", pareto),
("bimodal", bimodal),
];
println!("\n[size] 1e6 samples, alpha = 0.01 (sub_bits = 6)");
println!(
" {:<12} {:>8} {:>10} {:>12} {:>14}",
"dist", "buckets", "bytes", "bytes/bkt", "vs raw f64"
);
for (name, f) in dists {
let mut rng = Rng::new(0x512E_0000 ^ name.len() as u64);
let data: Vec<f64> = (0..1_000_000).map(|_| f(&mut rng)).collect();
let s = build(&data, 0.01);
let bytes = s.to_bytes().len();
let raw = data.len() * 8;
println!(
" {:<12} {:>8} {:>10} {:>12.1} {:>13}x",
name,
s.bucket_count(),
bytes,
bytes as f64 / s.bucket_count().max(1) as f64,
raw / bytes
);
}
}
#[test]
fn mapping_is_pure_integer_shift() {
let s = RelSketch::with_sub_bits(6).unwrap();
let _ = s; let sub_bits = 6u32;
let shift = 52 - sub_bits;
let base = 1.0f64.to_bits(); let a = f64::from_bits(base); let b = f64::from_bits(base + (1u64 << (shift - 1))); let c = f64::from_bits(base + (1u64 << shift)); let ka = a.to_bits() >> shift;
let kb = b.to_bits() >> shift;
let kc = c.to_bits() >> shift;
assert_eq!(ka, kb, "sub-prefix-equal values must share a bucket");
assert_eq!(
kc,
ka + 1,
"crossing the prefix must step exactly one bucket"
);
}