fix(tools): h5rs diff compares integers exactly under -d/-p
With a tolerance, integers were converted to f64 before comparing, so int64/uint64 values above 2^53 that differ compared equal: -d 0 on 2^60 and 2^60 + 1 exited 0, where h5diff exits 1. Integer pairs are now compared in i128 (the delta against floor(D), the relative quotient from an exact difference), and the report prints the exact difference. h5diff compares exactly when -p is below the f64 epsilon (2^60 and 2^60 + 1 differ at -p 1e-18, and nextafter(2, 0) and 2 at -p 1.5e-16); h5rs now does the same. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -23,7 +23,8 @@ sets and values, and soft/external link targets.
|
||||
-r, --report list every differing element (position, values, difference)
|
||||
-q, --quiet print nothing; only the exit status
|
||||
-d, --delta D numbers differ only when |a - b| > D
|
||||
-p, --relative R numbers differ only when |a - b| / |a| > R
|
||||
-p, --relative R numbers differ only when |a - b| / |a| > R (an R below
|
||||
the f64 epsilon, 2.2e-16, compares exactly, as h5diff)
|
||||
-c, --count N list at most N differing elements per object with -r
|
||||
--max-bytes N largest dataset read (default 1 GiB); a larger one is an error
|
||||
|
||||
@@ -119,7 +120,15 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
||||
_ => return args.usage_error(out, "--delta needs a number >= 0", USAGE),
|
||||
},
|
||||
"-p" | "--relative" => match args.number::<f64>() {
|
||||
Some(r) if r >= 0.0 => opts.tol = Tol::Relative(r),
|
||||
// As h5diff: a relative tolerance below the f64 epsilon
|
||||
// cannot be told from rounding, so it compares exactly.
|
||||
Some(r) if r >= 0.0 => {
|
||||
opts.tol = if r < f64::EPSILON {
|
||||
Tol::Exact
|
||||
} else {
|
||||
Tol::Relative(r)
|
||||
}
|
||||
}
|
||||
_ => return args.usage_error(out, "--relative needs a number >= 0", USAGE),
|
||||
},
|
||||
"-c" | "--count" => match args.number() {
|
||||
@@ -506,8 +515,9 @@ impl Diff {
|
||||
let pos = format!("[ {} ]", index(i as u64, &dims));
|
||||
let ta = value::text(&va, &|_| None);
|
||||
let tb = value::text(&vb, &|_| None);
|
||||
let dif = match (number(&va), number(&vb)) {
|
||||
(Some(x), Some(y)) => value::fmt_float((x - y).abs(), 64),
|
||||
let dif = match (int_of(&va), int_of(&vb), number(&va), number(&vb)) {
|
||||
(Some(x), Some(y), ..) => x.abs_diff(y).to_string(),
|
||||
(_, _, Some(x), Some(y)) => value::fmt_float((x - y).abs(), 64),
|
||||
_ => String::new(),
|
||||
};
|
||||
rows.push(format!("{pos:<24}{ta:<24}{tb:<24}{dif}"));
|
||||
@@ -595,12 +605,12 @@ impl Diff {
|
||||
}
|
||||
|
||||
fn equal(&self, a: &Side, b: &Side, x: &Value, y: &Value) -> bool {
|
||||
// Integers in integer arithmetic: through f64 they lose precision
|
||||
// above 2^53, and values that differ would compare equal.
|
||||
if let (Some(p), Some(q)) = (int_of(x), int_of(y)) {
|
||||
return int_close(p, q, self.opts.tol);
|
||||
}
|
||||
if let (Some(p), Some(q)) = (number(x), number(y)) {
|
||||
let exact_ints = matches!((x, y), (Value::Int(_), Value::Int(_)))
|
||||
|| matches!((x, y), (Value::Enum(..), Value::Enum(..)));
|
||||
if exact_ints && matches!(self.opts.tol, Tol::Exact) {
|
||||
return int_of(x) == int_of(y);
|
||||
}
|
||||
return close(p, q, self.opts.tol);
|
||||
}
|
||||
match (x, y) {
|
||||
@@ -657,6 +667,22 @@ fn close(a: f64, b: f64, tol: Tol) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `close` for integers, exactly: the difference is taken in i128, so no
|
||||
/// precision is lost at any 64-bit magnitude.
|
||||
fn int_close(a: i128, b: i128, tol: Tol) -> bool {
|
||||
if a == b {
|
||||
return true;
|
||||
}
|
||||
let d = a.abs_diff(b);
|
||||
match tol {
|
||||
Tol::Exact => false,
|
||||
// d is whole, so d <= t exactly when d <= floor(t) (saturating).
|
||||
Tol::Delta(t) => d <= t.floor() as u128,
|
||||
// d >= 1 here, so the quotient is never rounded to 0.
|
||||
Tol::Relative(r) => a != 0 && d as f64 / a.unsigned_abs() as f64 <= r,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_word(e: &Entry) -> &'static str {
|
||||
match e {
|
||||
Entry::Obj(_, k) => kind_word(*k),
|
||||
@@ -766,6 +792,26 @@ mod tests {
|
||||
assert!(!close(f64::NAN, 1.0, Tol::Delta(1e9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_tolerances_are_exact_above_2_pow_53() {
|
||||
let big = 1i128 << 60;
|
||||
assert!(!int_close(big, big + 1, Tol::Delta(0.0)));
|
||||
assert!(!int_close(big, big + 1, Tol::Delta(0.5)));
|
||||
assert!(int_close(big, big + 1, Tol::Delta(1.0)));
|
||||
assert!(!int_close(big, big + 200, Tol::Delta(199.9)));
|
||||
assert!(!int_close(big, big + 1, Tol::Relative(0.0)));
|
||||
assert!(!int_close(big, big + 1, Tol::Relative(1e-19)));
|
||||
assert!(int_close(big, big + 1, Tol::Relative(1e-18)));
|
||||
let umax = i128::from(u64::MAX);
|
||||
assert!(!int_close(umax, umax - 1, Tol::Delta(0.0)));
|
||||
assert!(int_close(
|
||||
i128::from(i64::MIN),
|
||||
umax,
|
||||
Tol::Delta(f64::INFINITY)
|
||||
));
|
||||
assert!(!int_close(0, 1, Tol::Relative(1e9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
assert_eq!(index(5, &[3, 4]), "1 1");
|
||||
|
||||
Reference in New Issue
Block a user