use crate::adapter::{serialize, Codec};
use crate::harness::{self, CorpusSummary};
use crate::metrics;
use std::time::Instant;
pub type CorpusFile = (Vec<Vec<i64>>, f64);
fn flatten_f64(signal: &[Vec<i64>]) -> Vec<f64> {
let total: usize = signal.iter().map(|c| c.len()).sum();
let mut out = Vec::with_capacity(total);
for chan in signal {
out.extend(chan.iter().map(|&s| s as f64));
}
out
}
fn raw_bytes(signal: &[Vec<i64>]) -> u64 {
serialize(signal).len() as u64
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RdPoint {
pub cr: f64,
pub prd: f64,
pub r: f64,
}
pub fn rate_distortion(codec: &dyn Codec, signals: &[CorpusFile]) -> Vec<RdPoint> {
let mut points = Vec::with_capacity(signals.len());
for (signal, fs) in signals {
let raw = raw_bytes(signal);
let blob = codec.encode(signal, *fs);
let comp = blob.len() as u64;
let recon = codec.decode(&blob);
let cr = metrics::compression_ratio(raw, comp);
let orig_f = flatten_f64(signal);
let recon_f = flatten_f64(&recon);
let prd = metrics::prd(&orig_f, &recon_f);
let r = metrics::pearson_r(&orig_f, &recon_f);
points.push(RdPoint { cr, prd, r });
}
points
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CrDistribution {
pub n_files: usize,
pub min: f64,
pub p5: f64,
pub p25: f64,
pub median: f64,
pub p75: f64,
pub p95: f64,
pub max: f64,
pub mean: f64,
}
pub fn per_file_cr_distribution(codec: &dyn Codec, signals: &[CorpusFile]) -> CrDistribution {
let n = signals.len();
if n == 0 {
return CrDistribution {
n_files: 0,
min: 0.0,
p5: 0.0,
p25: 0.0,
median: 0.0,
p75: 0.0,
p95: 0.0,
max: 0.0,
mean: 0.0,
};
}
let mut crs: Vec<f64> = Vec::with_capacity(n);
for (signal, fs) in signals {
let raw = raw_bytes(signal);
let comp = codec.encode(signal, *fs).len() as u64;
crs.push(metrics::compression_ratio(raw, comp));
}
crs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mean = crs.iter().sum::<f64>() / n as f64;
let pct = |p: f64| -> f64 {
let idx = (p * (n as f64 - 1.0) / 100.0).round() as i64;
let idx = idx.clamp(0, n as i64 - 1) as usize;
crs[idx]
};
CrDistribution {
n_files: n,
min: crs[0],
p5: pct(5.0),
p25: pct(25.0),
median: pct(50.0),
p75: pct(75.0),
p95: pct(95.0),
max: crs[n - 1],
mean,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Throughput {
pub raw_bytes: u64,
pub encode_mibs: f64,
pub decode_mibs: f64,
}
pub fn throughput(codec: &dyn Codec, signals: &[CorpusFile]) -> Throughput {
let raw_total: u64 = signals.iter().map(|(s, _)| raw_bytes(s)).sum();
let t_enc = Instant::now();
let mut blobs: Vec<Vec<u8>> = Vec::with_capacity(signals.len());
for (signal, fs) in signals {
blobs.push(codec.encode(signal, *fs));
}
let enc_secs = t_enc.elapsed().as_secs_f64();
let t_dec = Instant::now();
for blob in &blobs {
let _ = codec.decode(blob);
}
let dec_secs = t_dec.elapsed().as_secs_f64();
let mib = raw_total as f64 / (1024.0 * 1024.0);
let encode_mibs = if enc_secs > 0.0 { mib / enc_secs } else { 0.0 };
let decode_mibs = if dec_secs > 0.0 { mib / dec_secs } else { 0.0 };
Throughput {
raw_bytes: raw_total,
encode_mibs,
decode_mibs,
}
}
pub fn corpus_summary(codec: &dyn Codec, signals: &[CorpusFile]) -> CorpusSummary {
let (_reports, summary) = harness::run_corpus(codec, signals);
summary
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::{Gzip, Store};
fn synthetic(scale: f64) -> Vec<i64> {
let fs = 256.0;
let n = 256;
(0..n)
.map(|i| {
let t = i as f64 / fs;
let v = scale
* (100.0 * (2.0 * std::f64::consts::PI * 2.0 * t).sin()
+ 60.0 * (2.0 * std::f64::consts::PI * 10.0 * t).sin()
+ 30.0 * (2.0 * std::f64::consts::PI * 40.0 * t).sin());
v.round() as i64
})
.collect()
}
fn corpus() -> Vec<CorpusFile> {
vec![
(vec![synthetic(1.0), synthetic(1.5)], 256.0),
(vec![synthetic(0.7), synthetic(2.0), synthetic(1.2)], 256.0),
(vec![synthetic(0.5)], 256.0),
]
}
#[test]
fn rd_lossless_is_zero_distortion() {
let files = corpus();
for pts in [rate_distortion(&Store, &files), rate_distortion(&Gzip, &files)] {
assert_eq!(pts.len(), files.len());
for p in &pts {
assert_eq!(p.prd, 0.0, "lossless codec must have prd 0");
assert_eq!(p.r, 1.0, "lossless codec must have r 1");
assert!(p.cr > 0.0 && p.cr.is_finite());
}
}
}
#[test]
fn rd_gzip_compresses_more_than_store() {
let files = corpus();
let store = rate_distortion(&Store, &files);
let gzip = rate_distortion(&Gzip, &files);
for (s, g) in store.iter().zip(&gzip) {
assert!(
g.cr >= s.cr,
"gzip cr {} should be >= store cr {}",
g.cr,
s.cr
);
}
}
#[test]
fn rd_deterministic() {
let files = corpus();
assert_eq!(rate_distortion(&Store, &files), rate_distortion(&Store, &files));
}
#[test]
fn rd_empty_corpus() {
assert!(rate_distortion(&Store, &[]).is_empty());
}
#[test]
fn cr_distribution_ordered_and_consistent() {
let files = corpus();
let d = per_file_cr_distribution(&Gzip, &files);
assert_eq!(d.n_files, files.len());
assert!(d.min <= d.p5);
assert!(d.p5 <= d.p25);
assert!(d.p25 <= d.median);
assert!(d.median <= d.p75);
assert!(d.p75 <= d.p95);
assert!(d.p95 <= d.max);
assert!(d.mean >= d.min && d.mean <= d.max);
}
#[test]
fn cr_distribution_percentile_indices() {
let one = vec![(vec![synthetic(1.0)], 256.0)];
let d = per_file_cr_distribution(&Gzip, &one);
assert_eq!(d.n_files, 1);
assert_eq!(d.min, d.max);
assert_eq!(d.median, d.min);
assert_eq!(d.mean, d.min);
assert_eq!(d.p5, d.min);
assert_eq!(d.p95, d.min);
}
#[test]
fn cr_distribution_store_is_near_one() {
let files = corpus();
let d = per_file_cr_distribution(&Store, &files);
assert!(d.min >= 0.8 && d.max <= 1.2, "store cr spread {:?}", d);
}
#[test]
fn cr_distribution_deterministic() {
let files = corpus();
assert_eq!(
per_file_cr_distribution(&Gzip, &files),
per_file_cr_distribution(&Gzip, &files)
);
}
#[test]
fn cr_distribution_empty_corpus() {
let d = per_file_cr_distribution(&Store, &[]);
assert_eq!(d.n_files, 0);
assert_eq!(d.mean, 0.0);
assert_eq!(d.median, 0.0);
}
#[test]
fn throughput_reports_finite_rates() {
let files = corpus();
let t = throughput(&Gzip, &files);
let expected_raw: u64 = files
.iter()
.map(|(s, _)| serialize(s).len() as u64)
.sum();
assert_eq!(t.raw_bytes, expected_raw);
assert!(t.encode_mibs.is_finite());
assert!(t.decode_mibs.is_finite());
assert!(t.encode_mibs >= 0.0);
assert!(t.decode_mibs >= 0.0);
}
#[test]
fn throughput_empty_corpus_is_zero() {
let t = throughput(&Store, &[]);
assert_eq!(t.raw_bytes, 0);
assert_eq!(t.encode_mibs, 0.0);
assert_eq!(t.decode_mibs, 0.0);
}
#[test]
fn corpus_summary_matches_run_corpus() {
let files = corpus();
let summary = corpus_summary(&Store, &files);
let (_, expected) = harness::run_corpus(&Store, &files);
assert_eq!(summary.codec, expected.codec);
assert_eq!(summary.n_files, expected.n_files);
assert_eq!(summary.mean_cr, expected.mean_cr);
assert_eq!(summary.mean_prd, expected.mean_prd);
assert_eq!(summary.mean_r, expected.mean_r);
assert_eq!(summary.worst_grade, expected.worst_grade);
assert_eq!(summary.all_bit_exact, expected.all_bit_exact);
}
#[test]
fn corpus_summary_lossless_is_grade_l() {
let files = corpus();
let summary = corpus_summary(&Gzip, &files);
assert_eq!(summary.n_files, files.len());
assert!(summary.all_bit_exact, "gzip is lossless on synthetic corpus");
assert_eq!(summary.worst_grade, 'L');
assert_eq!(summary.mean_prd, 0.0);
assert_eq!(summary.mean_r, 1.0);
assert!(summary.mean_cr >= 1.0);
}
#[test]
fn corpus_summary_empty_is_below_floor() {
let summary = corpus_summary(&Store, &[]);
assert_eq!(summary.n_files, 0);
assert_eq!(summary.worst_grade, '\0');
}
}