use std::fmt;
#[derive(Clone, Copy, Debug)]
pub struct Tolerance {
pub rtol: f32,
pub atol: f32,
}
impl Tolerance {
pub fn new(rtol: f32, atol: f32) -> Tolerance {
Tolerance { rtol, atol }
}
}
impl Default for Tolerance {
fn default() -> Tolerance {
Tolerance {
rtol: 1e-3,
atol: 1e-5,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Mismatch {
pub index: usize,
pub actual: f32,
pub expected: f32,
pub abs_err: f32,
pub rel_err: f32,
}
#[derive(Clone, Debug)]
pub struct Comparison {
pub len: usize,
pub num_mismatched: usize,
pub max_abs_err: f32,
pub max_rel_err: f32,
pub first_mismatch: Option<Mismatch>,
tol: Tolerance,
}
impl Comparison {
pub fn all_close(&self) -> bool {
self.num_mismatched == 0
}
}
impl fmt::Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}/{} elements exceed tolerance (rtol={:e}, atol={:e}); \
max abs err {:e}, max rel err {:e}",
self.num_mismatched,
self.len,
self.tol.rtol,
self.tol.atol,
self.max_abs_err,
self.max_rel_err
)?;
if let Some(m) = self.first_mismatch {
write!(
f,
"; first at [{}]: actual={}, expected={} (abs {:e}, rel {:e})",
m.index, m.actual, m.expected, m.abs_err, m.rel_err
)?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum CompareError {
LengthMismatch { actual: usize, expected: usize },
}
impl fmt::Display for CompareError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompareError::LengthMismatch { actual, expected } => write!(
f,
"length mismatch: actual has {actual} elements, expected has {expected}"
),
}
}
}
impl std::error::Error for CompareError {}
fn is_close(actual: f32, expected: f32, tol: Tolerance) -> bool {
if actual.is_nan() || expected.is_nan() {
return actual.is_nan() && expected.is_nan();
}
if actual.is_infinite() || expected.is_infinite() {
return actual == expected;
}
(actual - expected).abs() <= tol.atol + tol.rtol * expected.abs()
}
pub fn compare_f32(
actual: &[f32],
expected: &[f32],
tol: Tolerance,
) -> Result<Comparison, CompareError> {
if actual.len() != expected.len() {
return Err(CompareError::LengthMismatch {
actual: actual.len(),
expected: expected.len(),
});
}
let mut num_mismatched = 0;
let mut max_abs_err = 0.0f32;
let mut max_rel_err = 0.0f32;
let mut first_mismatch = None;
for (index, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
if a.is_finite() && e.is_finite() {
let abs_err = (a - e).abs();
let rel_err = if e != 0.0 { abs_err / e.abs() } else { abs_err };
max_abs_err = max_abs_err.max(abs_err);
max_rel_err = max_rel_err.max(rel_err);
}
if !is_close(a, e, tol) {
num_mismatched += 1;
if first_mismatch.is_none() {
let abs_err = (a - e).abs();
let rel_err = if e != 0.0 { abs_err / e.abs() } else { abs_err };
first_mismatch = Some(Mismatch {
index,
actual: a,
expected: e,
abs_err,
rel_err,
});
}
}
}
Ok(Comparison {
len: actual.len(),
num_mismatched,
max_abs_err,
max_rel_err,
first_mismatch,
tol,
})
}
#[track_caller]
pub fn assert_close(actual: &[f32], expected: &[f32], tol: Tolerance) {
match compare_f32(actual, expected, tol) {
Err(e) => panic!("{e}"),
Ok(cmp) if !cmp.all_close() => panic!("{cmp}"),
Ok(_) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_slices_are_close() {
let cmp = compare_f32(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0], Tolerance::default()).unwrap();
assert!(cmp.all_close());
assert_eq!(cmp.num_mismatched, 0);
assert_eq!(cmp.max_abs_err, 0.0);
}
#[test]
fn small_differences_within_tolerance() {
let cmp = compare_f32(&[1000.5], &[1000.0], Tolerance::default()).unwrap();
assert!(cmp.all_close());
}
#[test]
fn large_differences_flagged() {
let cmp = compare_f32(&[1.0, 5.0], &[1.0, 2.0], Tolerance::default()).unwrap();
assert!(!cmp.all_close());
assert_eq!(cmp.num_mismatched, 1);
let first = cmp.first_mismatch.unwrap();
assert_eq!(first.index, 1);
assert_eq!(first.expected, 2.0);
}
#[test]
fn length_mismatch_errors() {
assert!(matches!(
compare_f32(&[1.0], &[1.0, 2.0], Tolerance::default()),
Err(CompareError::LengthMismatch { .. })
));
}
#[test]
fn nan_only_matches_nan() {
let tol = Tolerance::default();
assert!(is_close(f32::NAN, f32::NAN, tol));
assert!(!is_close(f32::NAN, 1.0, tol));
assert!(!is_close(1.0, f32::NAN, tol));
}
#[test]
fn infinities_match_by_sign() {
let tol = Tolerance::default();
assert!(is_close(f32::INFINITY, f32::INFINITY, tol));
assert!(!is_close(f32::INFINITY, f32::NEG_INFINITY, tol));
assert!(!is_close(f32::INFINITY, 1e30, tol));
}
#[test]
#[should_panic(expected = "exceed tolerance")]
fn assert_close_panics_on_mismatch() {
assert_close(&[1.0], &[2.0], Tolerance::default());
}
}