use crate::types::Number;
pub fn is_significant(value: Number, scale: Number, tol: Number) -> bool {
if !value.is_finite() {
return false;
}
let s = scale.abs();
let threshold = if s.is_finite() && s > 0.0 {
tol * s
} else {
tol
};
value.abs() > threshold
}
pub fn is_negligible(value: Number, scale: Number, tol: Number) -> bool {
if !value.is_finite() {
return false;
}
let s = scale.abs();
let widened = if s.is_finite() { s.max(1.0) } else { 1.0 };
value.abs() <= tol * widened
}
pub fn row_scale_from_factor(factor: Number) -> Number {
if factor.is_finite() && factor > 0.0 {
1.0 / factor
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_is_invariant_under_row_scaling() {
let tol = 1e-8;
for k in -12..=12 {
let s = 10f64.powi(k);
assert!(
is_significant(1.0 * s, s, tol),
"a unit violation at scale 10^{k} must stay significant"
);
assert!(
!is_significant(1e-12 * s, s, tol),
"noise at scale 10^{k} must stay insignificant"
);
}
}
#[test]
fn clamped_form_would_lose_the_down_scaled_direction() {
let tol = 1e-8;
let (value, scale) = (1e-12, 1e-12); assert!(is_significant(value, scale, tol));
assert!(
!(value.abs() > tol * scale.abs().max(1.0)),
"the clamped form misses this — that is why it is not used"
);
}
#[test]
fn degenerate_scale_falls_back_to_absolute() {
let tol = 1e-8;
assert!(is_significant(1e-6, 0.0, tol));
assert!(!is_significant(1e-10, 0.0, tol));
assert!(is_significant(1e-6, f64::INFINITY, tol));
assert!(is_significant(1e-6, f64::NAN, tol));
}
#[test]
fn non_finite_value_is_not_evidence() {
let tol = 1e-8;
assert!(!is_significant(f64::NAN, 1.0, tol));
assert!(!is_significant(f64::INFINITY, 1.0, tol));
assert!(!is_significant(f64::NEG_INFINITY, 1.0, tol));
}
#[test]
fn exactly_at_threshold_is_not_significant() {
assert!(!is_significant(1e-8, 1.0, 1e-8));
assert!(is_significant(1.0000001e-8, 1.0, 1e-8));
}
#[test]
fn accepting_direction_never_stricter_than_absolute() {
let tol = 1e-6;
assert!(is_negligible(1e-8, 1e-3, tol));
assert!(
1e-8f64 > tol * 1e-3,
"pure relative flags it — that is the bug"
);
}
#[test]
fn accepting_direction_scales_up_for_large_rows() {
let tol = 1e-6;
assert!(is_negligible(3.15e3, 5e13, tol));
assert!(!is_negligible(1e10, 5e13, tol));
}
#[test]
fn the_two_directions_are_not_negations() {
let tol = 1e-6;
let (value, scale) = (1e-8, 1e-3);
assert!(is_negligible(value, scale, tol));
assert!(is_significant(value, scale, tol));
}
#[test]
fn non_finite_is_never_negligible() {
let tol = 1e-6;
assert!(!is_negligible(f64::NAN, 1.0, tol));
assert!(!is_negligible(f64::INFINITY, 1.0, tol));
}
#[test]
fn row_scale_inverts_the_factor() {
assert_eq!(row_scale_from_factor(1.0), 1.0);
assert_eq!(row_scale_from_factor(1e-6), 1e6);
assert_eq!(row_scale_from_factor(1e6), 1e-6);
assert_eq!(row_scale_from_factor(0.0), 1.0);
assert_eq!(row_scale_from_factor(-1.0), 1.0);
assert_eq!(row_scale_from_factor(f64::NAN), 1.0);
assert_eq!(row_scale_from_factor(f64::INFINITY), 1.0);
}
#[test]
fn factor_and_significance_compose() {
let tol = 1e-8;
let scale = row_scale_from_factor(1e-6);
assert_eq!(scale, 1e6);
assert!(
!is_significant(1e-3, scale, tol),
"1e-3 is below the 1e-2 threshold"
);
assert!(is_significant(1e-1, scale, tol), "1e-1 is above it");
}
}