#![allow(clippy::unwrap_used, clippy::expect_used)]
use chronix_encoding::{
AlpEncoder, Chimp128Encoder, ChimpEncoder, GorillaEncoder, PatasEncoder, PcoEncoder,
};
struct Workload {
name: &'static str,
values: Vec<f64>,
min_alp_ratio: f64,
alp_should_win: bool,
}
fn workloads() -> Vec<Workload> {
vec![
Workload {
name: "power meter, W, 2dp",
values: (0..8192)
.map(|i| 230.0 + f64::from((i * 37) % 4000) / 100.0)
.collect(),
min_alp_ratio: 3.5,
alp_should_win: true,
},
Workload {
name: "temperature, C, 1dp",
values: (0..8192)
.map(|i| 18.0 + f64::from((i * 7) % 120) / 10.0)
.collect(),
min_alp_ratio: 8.0,
alp_should_win: true,
},
Workload {
name: "energy, kWh, 3dp cumulative",
values: (0..8192).map(|i| 1000.0 + f64::from(i) / 1000.0).collect(),
min_alp_ratio: 4.0,
alp_should_win: true,
},
Workload {
name: "scientific doubles",
values: (0..8192)
.map(|i| f64::from(i).sin() * std::f64::consts::PI)
.collect(),
min_alp_ratio: 0.0,
alp_should_win: false,
},
]
}
#[test]
fn float_codec_ratios_hold() {
for w in workloads() {
let raw = w.values.len() * 8;
let alp = AlpEncoder::encode(&w.values).unwrap().len();
let chimp = ChimpEncoder::encode(&w.values).unwrap().len();
let chimp128 = Chimp128Encoder::encode(&w.values).unwrap().len();
let gorilla = GorillaEncoder::encode(&w.values).unwrap().len();
let patas = PatasEncoder::encode(&w.values).unwrap().len();
let pco = PcoEncoder::encode_f64(&w.values).unwrap().len();
let ratio = |n: usize| raw as f64 / n as f64;
println!(
"{:<30} pco {:5.1}x alp {:5.1}x chimp {:4.1}x chimp128 {:4.1}x gorilla {:4.1}x patas {:4.1}x",
w.name,
ratio(pco),
ratio(alp),
ratio(chimp),
ratio(chimp128),
ratio(gorilla),
ratio(patas),
);
if w.alp_should_win {
assert!(
ratio(pco) >= w.min_alp_ratio * 2.5,
"{}: pco {:.1}x is no longer well ahead of ALP's floor {:.1}x",
w.name,
ratio(pco),
w.min_alp_ratio
);
}
assert!(
pco as f64 <= alp as f64 * 1.05,
"{}: pco ({pco} B) lost to ALP ({alp} B)",
w.name
);
assert!(
ratio(alp) >= w.min_alp_ratio,
"{}: ALP ratio {:.2}x fell below the documented {:.2}x",
w.name,
ratio(alp),
w.min_alp_ratio
);
if w.alp_should_win {
let best_xor = chimp.min(chimp128).min(gorilla).min(patas);
assert!(
alp < best_xor,
"{}: ALP ({alp} bytes) no longer beats the best XOR codec ({best_xor} bytes)",
w.name
);
}
}
}
fn lcg(seed: &mut u64) -> u64 {
*seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
*seed >> 33
}
fn unit(seed: &mut u64) -> f64 {
(lcg(seed) % 2_000_001) as f64 / 1_000_000.0 - 1.0
}
fn noise(seed: &mut u64, sigma: f64) -> f64 {
(unit(seed) + unit(seed) + unit(seed)) / 3.0 * sigma * 3.0
}
fn quantize(v: f64, dp: i32) -> f64 {
let scale = 10f64.powi(dp);
(v * scale).round() / scale
}
const REALISTIC_N: usize = 8192;
fn realistic_workloads() -> Vec<Realistic> {
let mut seed = 0xC0FF_EE00_u64;
let pv: Vec<f64> = (0..REALISTIC_N)
.map(|i| {
let day = i as f64 / REALISTIC_N as f64 * 2.0;
let sun = (day * std::f64::consts::PI).sin().max(0.0);
let mut w = 9000.0 * sun.powf(1.4);
if w > 6500.0 {
w = 6500.0; }
if lcg(&mut seed) % 97 < 6 {
w *= 0.2 + (lcg(&mut seed) % 500) as f64 / 1000.0;
}
quantize((w + noise(&mut seed, 4.0)).max(0.0), 1)
})
.collect();
let mut load_base = 240.0;
let mut appliance = 0.0;
let mut appliance_left = 0usize;
let load: Vec<f64> = (0..REALISTIC_N)
.map(|_| {
if appliance_left == 0 {
if lcg(&mut seed).is_multiple_of(40) {
appliance = [2000.0, 3600.0, 7400.0, 800.0][(lcg(&mut seed) % 4) as usize];
appliance_left = 5 + (lcg(&mut seed) % 60) as usize;
} else {
appliance = 0.0;
}
} else {
appliance_left -= 1;
}
load_base += noise(&mut seed, 3.0);
load_base = load_base.clamp(150.0, 400.0);
quantize(load_base + appliance + noise(&mut seed, 2.0), 2)
})
.collect();
let temp: Vec<f64> = (0..REALISTIC_N)
.map(|i| {
let drift = 45.0 + 12.0 * (i as f64 / 900.0).sin();
quantize(drift + noise(&mut seed, 0.12), 1)
})
.collect();
let mut kwh = 0.317_f64;
let energy: Vec<f64> = (0..REALISTIC_N)
.map(|i| {
kwh += (pv[i] / 1000.0 / 3600.0).max(0.0) + 0.000_2;
quantize(kwh, 3)
})
.collect();
let soc: Vec<f64> = (0..REALISTIC_N)
.map(|i| {
let phase = (i as f64 / 700.0).sin();
let raw = 50.0 + 60.0 * phase;
quantize(raw.clamp(10.0, 100.0) + noise(&mut seed, 0.03), 1)
})
.collect();
let adc: Vec<f64> = (0..REALISTIC_N)
.map(|i| {
let counts = 2048.0 + 1800.0 * (i as f64 / 311.0).sin() + noise(&mut seed, 12.0);
counts * (3.3 / 4095.0) * 1.000_137_2
})
.collect();
vec![
Realistic::new("PV inverter, W, 1dp", pv.clone(), 6.0, 3.5),
Realistic::new("house load, W, 2dp", load, 4.5, 2.8),
Realistic::new("flow temp, C, 1dp", temp, 14.0, 7.0),
Realistic::new("energy counter, kWh, 3dp", energy, 25.0, 4.0),
Realistic::new("battery SoC, %, 1dp", soc, 17.0, 5.5),
Realistic::new("raw ADC volts, non-decimal", adc, 1.2, 0.0),
]
}
struct Realistic {
name: &'static str,
values: Vec<f64>,
min_pco_ratio: f64,
min_alp_ratio: f64,
}
impl Realistic {
fn new(name: &'static str, values: Vec<f64>, min_pco_ratio: f64, min_alp_ratio: f64) -> Self {
Self {
name,
values,
min_pco_ratio,
min_alp_ratio,
}
}
}
#[test]
fn realistic_float_codec_ratios() {
println!(
"\n{:<28} {:>9} {:>8} {:>8} {:>9} {:>8} {:>8}",
"realistic workload", "pco", "alp", "chimp", "chimp128", "gorilla", "patas"
);
for w in realistic_workloads() {
let raw = w.values.len() * 8;
let alp = AlpEncoder::encode(&w.values).unwrap().len();
let chimp = ChimpEncoder::encode(&w.values).unwrap().len();
let chimp128 = Chimp128Encoder::encode(&w.values).unwrap().len();
let gorilla = GorillaEncoder::encode(&w.values).unwrap().len();
let patas = PatasEncoder::encode(&w.values).unwrap().len();
let pco = PcoEncoder::encode_f64(&w.values).unwrap().len();
let r = |n: usize| raw as f64 / n as f64;
println!(
"{:<28} {:8.2}x {:7.2}x {:7.2}x {:8.2}x {:7.2}x {:7.2}x",
w.name,
r(pco),
r(alp),
r(chimp),
r(chimp128),
r(gorilla),
r(patas)
);
assert!(
r(pco) >= w.min_pco_ratio,
"{}: pco {:.2}x fell below the documented {:.2}x",
w.name,
r(pco),
w.min_pco_ratio
);
assert!(
r(alp) >= w.min_alp_ratio,
"{}: ALP {:.2}x fell below the documented {:.2}x",
w.name,
r(alp),
w.min_alp_ratio
);
assert!(
pco < alp,
"{}: pco ({pco} B) no longer beats ALP ({alp} B) on realistic data",
w.name
);
}
}
#[test]
fn synthetic_ratios_are_an_order_of_magnitude_above_realistic_ones() {
let best_realistic = realistic_workloads()
.iter()
.map(|w| {
w.values.len() as f64 * 8.0 / PcoEncoder::encode_f64(&w.values).unwrap().len() as f64
})
.fold(0.0_f64, f64::max);
let best_synthetic = workloads()
.iter()
.map(|w| {
w.values.len() as f64 * 8.0 / PcoEncoder::encode_f64(&w.values).unwrap().len() as f64
})
.fold(0.0_f64, f64::max);
println!("best pco ratio: synthetic {best_synthetic:.1}x, realistic {best_realistic:.1}x");
assert!(
best_realistic < 30.0,
"realistic pco reached {best_realistic:.1}x - re-derive the documented range"
);
assert!(
best_synthetic > 80.0,
"synthetic pco fell to {best_synthetic:.1}x - the older documented range no longer holds either"
);
}
#[test]
fn missing_values_cost_a_validity_bitmap() {
use chronix_core::config::FloatEncoding;
use chronix_encoding::{ColumnDecoder, ColumnEncoder, DecodedColumn};
let dense = realistic_workloads().swap_remove(0).values;
let mut seed = 0x51DE_u64;
let sparse: Vec<Option<f64>> = dense
.iter()
.map(|&v| {
if lcg(&mut seed).is_multiple_of(50) {
None
} else {
Some(v)
}
})
.collect();
let nulls = sparse.iter().filter(|v| v.is_none()).count();
assert!(nulls > 100, "generator produced only {nulls} nulls");
let block = ColumnEncoder::encode_f64_nullable(&sparse, FloatEncoding::Chimp).unwrap();
match ColumnDecoder::decode(&block).unwrap() {
DecodedColumn::NullableF64(back) => assert_eq!(back, sparse),
other => panic!("nullable f64 decoded to {other:?}"),
}
let raw = sparse.len() * 8;
let bitmap_bytes = sparse.len().div_ceil(8);
let ratio = raw as f64 / (block.payload.len() + 1) as f64;
println!(
"PV with {:.1}% dropouts: {ratio:.2}x ({} B, of which {bitmap_bytes} B is the validity bitmap)",
nulls as f64 / sparse.len() as f64 * 100.0,
block.payload.len()
);
assert!(ratio >= 5.0, "nullable PV fell to {ratio:.2}x");
assert!(
block.payload.len() > bitmap_bytes,
"block is smaller than its own validity bitmap"
);
}