#![allow(dead_code)]
use rustc_hash::FxHashMap;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug)]
pub struct Tol {
pub max_relative: f64,
pub epsilon: f64,
}
impl Tol {
pub const fn rel(max_relative: f64) -> Self {
Self {
max_relative,
epsilon: 0.0,
}
}
pub const fn new(max_relative: f64, epsilon: f64) -> Self {
Self {
max_relative,
epsilon,
}
}
}
fn relative_difference(got: f64, want: f64) -> f64 {
if got == want {
return 0.0;
}
if got.is_nan() != want.is_nan() {
return f64::INFINITY;
}
if got.is_nan() && want.is_nan() {
return 0.0;
}
if !got.is_finite() || !want.is_finite() {
return f64::INFINITY;
}
let scale = got.abs().max(want.abs());
if scale == 0.0 {
0.0
} else {
(got - want).abs() / scale
}
}
#[derive(Clone, Copy, Debug)]
struct Worst {
index: usize,
relative: f64,
absolute: f64,
got: f64,
want: f64,
}
pub fn assert_close(got: &[f64], want: &[f64], tol: Tol, label: &str) {
assert_eq!(
got.len(),
want.len(),
"{label}: length mismatch, got {} want {}",
got.len(),
want.len()
);
assert!(
!got.is_empty(),
"{label}: nothing to compare, both sides are empty"
);
let mut worst: Option<Worst> = None;
let mut worst_abs: Option<Worst> = None;
let mut worst_failing: Option<Worst> = None;
let mut n_failed = 0_usize;
let mut worst_beyond_eps = 0.0_f64;
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
let relative = relative_difference(g, w);
let absolute = if g.is_finite() && w.is_finite() {
(g - w).abs()
} else {
f64::INFINITY
};
let here = Worst {
index: i,
relative,
absolute,
got: g,
want: w,
};
if worst.is_none_or(|prev| relative > prev.relative) {
worst = Some(here);
}
if worst_abs.is_none_or(|prev| absolute > prev.absolute) {
worst_abs = Some(here);
}
if absolute > tol.epsilon {
worst_beyond_eps = worst_beyond_eps.max(relative);
if relative > tol.max_relative {
n_failed += 1;
if worst_failing.is_none_or(|prev| relative > prev.relative) {
worst_failing = Some(here);
}
}
}
}
let worst = match worst {
Some(w) => w,
None => return,
};
let worst_abs = worst_abs.expect("non-empty, so a maximum exists");
if report_enabled() {
println!(
"[tol] {label:<34} n={:<6} NEEDS rel {:.3e} (eps {:.0e}) \
raw: rel {:.3e} @{} abs {:.3e} @{}",
got.len(),
worst_beyond_eps,
tol.epsilon,
worst.relative,
worst.index,
worst_abs.absolute,
worst_abs.index
);
}
let Some(bad) = worst_failing else { return };
panic!(
"{label}: {n_failed} of {} values outside tolerance \
(max_relative {:.1e}, epsilon {:.1e}).\n \
worst failure at index {}: got {:.17e}, want {:.17e}, \
relative {:.3e}, absolute {:.3e}",
got.len(),
tol.max_relative,
tol.epsilon,
bad.index,
bad.got,
bad.want,
bad.relative,
bad.absolute
);
}
pub fn assert_close_scalar(got: f64, want: f64, tol: Tol, label: &str) {
assert_close(&[got], &[want], tol, label);
}
pub fn assert_eq_usize(got: &[usize], want: &[usize], label: &str) {
assert_eq!(
got.len(),
want.len(),
"{label}: length mismatch, got {} want {}",
got.len(),
want.len()
);
let diffs: Vec<usize> = (0..got.len()).filter(|&i| got[i] != want[i]).collect();
assert!(
diffs.is_empty(),
"{label}: {} of {} entries differ, first at index {} (got {}, want {})",
diffs.len(),
got.len(),
diffs[0],
got[diffs[0]],
want[diffs[0]]
);
}
fn report_enabled() -> bool {
std::env::var_os("EDGE_RS_TOL_REPORT").is_some()
}
#[derive(Clone, Debug)]
pub struct Table {
names: Vec<String>,
columns: Vec<Vec<f64>>,
n_rows: usize,
source: String,
}
impl Table {
pub fn n_rows(&self) -> usize {
self.n_rows
}
pub fn n_cols(&self) -> usize {
self.columns.len()
}
pub fn column(&self, name: &str) -> &[f64] {
match self.names.iter().position(|n| n == name) {
Some(i) => &self.columns[i],
None => panic!(
"{}: no column {name:?}; available: {}",
self.source,
self.names.join(", ")
),
}
}
pub fn column_usize(&self, name: &str) -> Vec<usize> {
self.column(name)
.iter()
.map(|v| {
assert!(
v.is_finite() && *v >= 0.0 && v.fract() == 0.0,
"{}: column {name:?} holds {v}, which is not a non-negative whole number",
self.source
);
*v as usize
})
.collect()
}
pub fn column_bool(&self, name: &str) -> Vec<bool> {
self.column(name).iter().map(|v| *v != 0.0).collect()
}
pub fn row_major(&self) -> Vec<f64> {
let mut out = Vec::with_capacity(self.n_rows * self.columns.len());
for r in 0..self.n_rows {
for c in &self.columns {
out.push(c[r]);
}
}
out
}
pub fn row_major_counts(&self) -> Vec<f64> {
let v = self.row_major();
for x in &v {
assert!(
x.is_finite() && x.fract() == 0.0,
"{}: expected whole counts, found {x}",
self.source
);
}
v
}
}
fn fixture_path(name: &str) -> PathBuf {
[env!("CARGO_MANIFEST_DIR"), "tests", "data", "e2e", name]
.iter()
.collect()
}
fn parse_field(field: &str, name: &str) -> f64 {
let t = field.trim().trim_matches('"');
match t {
"NA" | "NaN" | "nan" => f64::NAN,
"Inf" | "inf" => f64::INFINITY,
"-Inf" | "-inf" => f64::NEG_INFINITY,
_ => t
.parse::<f64>()
.unwrap_or_else(|e| panic!("bad field {field:?} in {name}: {e}")),
}
}
pub fn table(name: &str) -> Table {
let path = fixture_path(name);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"cannot read {}: {e}. Run `Rscript tests/r/generate_fixtures.R` to create it.",
path.display()
)
});
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let header = lines
.next()
.unwrap_or_else(|| panic!("{name} has no header"));
let names: Vec<String> = header
.split(',')
.map(|h| h.trim().trim_matches('"').to_string())
.collect();
let mut columns: Vec<Vec<f64>> = vec![Vec::new(); names.len()];
let mut n_rows = 0_usize;
for line in lines {
let fields: Vec<&str> = line.split(',').collect();
assert_eq!(
fields.len(),
names.len(),
"ragged fixture {name}: row {n_rows} has {} fields, header has {}",
fields.len(),
names.len()
);
for (c, field) in fields.iter().enumerate() {
columns[c].push(parse_field(field, name));
}
n_rows += 1;
}
Table {
names,
columns,
n_rows,
source: name.to_string(),
}
}
pub fn matrix(name: &str) -> (Vec<f64>, usize, usize) {
let t = table(name);
let (r, c) = (t.n_rows(), t.n_cols());
(t.row_major(), r, c)
}
#[derive(Clone, Debug)]
pub struct Scalars {
values: FxHashMap<(String, String), f64>,
}
impl Scalars {
pub fn get(&self, scenario: &str, name: &str) -> f64 {
*self
.values
.get(&(scenario.to_string(), name.to_string()))
.unwrap_or_else(|| panic!("scalars.csv has no {scenario}/{name}"))
}
pub fn get_usize(&self, scenario: &str, name: &str) -> usize {
let v = self.get(scenario, name);
assert!(
v.is_finite() && v >= 0.0 && v.fract() == 0.0,
"{scenario}/{name} is {v}, not a non-negative whole number"
);
v as usize
}
}
pub fn scalars() -> Scalars {
let name = "scalars.csv";
let path = fixture_path(name);
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let mut values = FxHashMap::default();
for line in text.lines().skip(1).filter(|l| !l.trim().is_empty()) {
let mut parts = line.splitn(3, ',');
let scenario = parts.next().expect("scenario").trim().to_string();
let key = parts.next().expect("name").trim().to_string();
let raw = parts.next().expect("value");
values.insert((scenario, key), parse_field(raw, name));
}
Scalars { values }
}
#[cfg(feature = "gpu-tests")]
pub fn gpu_client() -> cubecl::prelude::ComputeClient<cubecl::wgpu::WgpuRuntime> {
use cubecl::Runtime;
use cubecl::wgpu::{WgpuDevice, WgpuRuntime};
WgpuRuntime::client(&WgpuDevice::default())
}