mod common;
use common::{assert_close, Tol};
use serde::Deserialize;
use commonstats::density::{kde, Kernel, Bandwidth, histogram_auto, Bins, Rule, Norm};
fn load_str(name: &str) -> String {
let path = format!(
"{}/tests/fixtures/{name}.json",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("missing fixture {path}: {e} — run scripts/gen_oracle.py"))
}
#[derive(Deserialize)]
struct KdeRecord {
label: String,
data: Vec<f64>,
bw_method: Option<String>,
fixed_h: Option<f64>,
grid: Vec<f64>,
bandwidth: f64,
density: Vec<f64>,
}
#[test]
fn kde_oracle_bandwidth_and_evaluate() {
let txt = load_str("kde");
let records: Vec<KdeRecord> = serde_json::from_str(&txt).expect("kde.json parse");
for rec in &records {
let bw = match (rec.bw_method.as_deref(), rec.fixed_h) {
(Some("silverman"), _) => Bandwidth::Silverman,
(Some("scott"), _) => Bandwidth::Scott,
(None, Some(h)) => Bandwidth::Fixed(h),
other => panic!("unexpected bw spec in fixture: {:?}", other),
};
let k = kde(&rec.data, Kernel::Gaussian, bw)
.unwrap_or_else(|e| panic!("kde failed for {}: {e:?}", rec.label));
assert_close(
&format!("{}/bandwidth", rec.label),
k.bandwidth(),
rec.bandwidth,
Tol { rel: 1e-10, abs: 1e-15 },
);
let got_vals = k.evaluate(&rec.grid);
assert_eq!(got_vals.len(), rec.density.len());
for (i, (&got, &want)) in got_vals.iter().zip(rec.density.iter()).enumerate() {
assert_close(
&format!("{}/density[{}]", rec.label, i),
got,
want,
Tol { rel: 1e-12, abs: 1e-15 },
);
}
}
}
#[derive(Deserialize)]
struct HistRecord {
rule: String,
data: Vec<f64>,
edges: Vec<f64>,
counts: Vec<u64>,
density: Vec<f64>,
}
fn rule_from_str(s: &str) -> Rule {
match s {
"sturges" => Rule::Sturges,
"scott" => Rule::Scott,
"fd" => Rule::FreedmanDiaconis,
"rice" => Rule::Rice,
"sqrt" => Rule::Sqrt,
other => panic!("unknown rule in fixture: {other}"),
}
}
#[test]
fn histogram_auto_oracle_edges_counts_density() {
let txt = load_str("histogram_auto");
let records: Vec<HistRecord> = serde_json::from_str(&txt).expect("histogram_auto.json parse");
for rec in &records {
let rule = rule_from_str(&rec.rule);
let (got_edges, got_counts) =
histogram_auto(&rec.data, Bins::Rule(rule), Norm::Count)
.unwrap_or_else(|e| panic!("histogram_auto Count/{}: {e:?}", rec.rule));
assert_eq!(
got_counts.len(), rec.counts.len(),
"rule={}: bin count mismatch (got {}, want {})",
rec.rule, got_counts.len(), rec.counts.len()
);
assert_eq!(got_edges.len(), rec.edges.len(),
"rule={}: edge count mismatch", rec.rule);
for (i, (&g, &w)) in got_edges.iter().zip(rec.edges.iter()).enumerate() {
assert_close(&format!("{}/edges[{i}]", rec.rule), g, w, Tol { rel: 1e-10, abs: 1e-15 });
}
for (i, (&g, &w)) in got_counts.iter().zip(rec.counts.iter()).enumerate() {
assert_eq!(
g as u64, w,
"rule={}/count[{i}]: got {g}, want {w}",
rec.rule
);
}
let (_, got_density) =
histogram_auto(&rec.data, Bins::Rule(rule), Norm::Density)
.unwrap_or_else(|e| panic!("histogram_auto Density/{}: {e:?}", rec.rule));
for (i, (&g, &w)) in got_density.iter().zip(rec.density.iter()).enumerate() {
assert_close(&format!("{}/density[{i}]", rec.rule), g, w, Tol { rel: 1e-12, abs: 1e-15 });
}
}
}
#[test]
fn kde_fixture_exists() {
let _ = load_str("kde");
}
#[test]
fn histogram_auto_fixture_exists() {
let _ = load_str("histogram_auto");
}