use gradcheck::{compare, Config, InvalidReason, Stage, UncheckedReason, Verdict};
fn cfg() -> Config {
Config::f32_defaults()
}
fn cell(id: &str, analytic: Vec<f64>, numeric: Vec<f64>, expected: Verdict) {
let n = analytic.len().max(numeric.len()).max(1);
let r = compare(id, "control", &[n], analytic, numeric, &cfg());
assert_eq!(
r.verdict, expected,
"\ncell {id}: expected {expected:?}, got {:?}\n {r}\n",
r.verdict
);
println!("cell {id:<4} {:?}", r.verdict);
}
#[test]
fn r1_scale_corruption_at_unit_magnitude() {
cell(
"R1",
vec![1.5, 3.0, -1.5],
vec![1.0, 2.0, -1.0],
Verdict::Mismatch,
);
}
#[test]
fn r2_the_c13_repro() {
cell("R2", vec![2e-3, 2e-3], vec![1e-3, 1e-3], Verdict::Mismatch);
}
#[test]
fn r3_sign_flip() {
cell("R3", vec![-1.0, -2.0], vec![1.0, 2.0], Verdict::Mismatch);
}
#[test]
fn r4_single_element_corruption() {
cell(
"R4",
vec![1.0, 2.0, 9.0],
vec![1.0, 2.0, 3.0],
Verdict::Mismatch,
);
}
#[test]
fn r8_gross_corruption_below_the_floor() {
cell("R8", vec![1e-5], vec![1.001e-3], Verdict::Mismatch);
}
#[test]
fn b1_the_additive_tolerance_wedge() {
let r = compare(
"B1",
"control",
&[1],
vec![2.002e-4],
vec![1.001e-4],
&cfg(),
);
assert_eq!(
r.verdict,
Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor),
"\ncell B1: the wedge must abstain, never certify. got {:?}\n {r}\n",
r.verdict
);
assert!(!r.passed(), "B1 must not be a pass");
println!("cell B1 {:?}", r.verdict);
}
#[test]
fn b3_the_pass_boundary() {
let c = cfg();
let inside = compare("B3c", "control", &[1], vec![1.0], vec![1.0 - 9e-4], &c);
assert_eq!(inside.verdict, Verdict::Pass, "{inside}");
let edge = compare("B3b", "control", &[1], vec![1.0], vec![1.0 - 1.05e-3], &c);
assert_eq!(
edge.verdict,
Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor),
"{edge}"
);
let outside = compare("B3a", "control", &[1], vec![1.0], vec![0.5], &c);
assert_eq!(outside.verdict, Verdict::Mismatch, "{outside}");
println!(
"cell B3 inside={:?} edge={:?} outside={:?}",
inside.verdict, edge.verdict, outside.verdict
);
}
#[test]
fn b2_magnitude_at_the_certification_floor() {
let c = cfg();
let below = compare("B2a", "control", &[1], vec![1e-4], vec![1e-4], &c);
assert_eq!(
below.verdict,
Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor),
"{below}"
);
let above = compare("B2c", "control", &[1], vec![1.0], vec![1.0], &c);
assert_eq!(above.verdict, Verdict::Pass, "{above}");
println!(
"cell B2 below={:?} above={:?}",
below.verdict, above.verdict
);
}
#[test]
fn a1_nan_in_analytic_only() {
cell(
"A1",
vec![f64::NAN, 1.0],
vec![1.0, 1.0],
Verdict::Invalid {
stage: Stage::Analytic,
reason: InvalidReason::NonFinite,
},
);
}
#[test]
fn a2_all_nan_the_true_c12_repro() {
cell(
"A2",
vec![f64::NAN; 4],
vec![f64::NAN; 4],
Verdict::Invalid {
stage: Stage::Analytic,
reason: InvalidReason::NonFinite,
},
);
}
#[test]
fn a3_nan_in_numeric_only() {
cell(
"A3",
vec![1.0, 1.0],
vec![f64::NAN, 1.0],
Verdict::Invalid {
stage: Stage::Probe,
reason: InvalidReason::NonFinite,
},
);
}
#[test]
fn a5_length_mismatch() {
cell(
"A5",
vec![1.0, 2.0, 3.0],
vec![1.0, 2.0],
Verdict::StructuralMismatch,
);
}
#[test]
fn a6_empty_analytic_gradient() {
cell("A6", vec![], vec![], Verdict::StructuralMismatch);
}
#[test]
fn a7_nan_in_the_tolerances() {
let bad = Config::f32_defaults().with_rel_tol(f64::NAN);
let r = compare("A7", "control", &[2], vec![1.0, 2.0], vec![1.0, 2.0], &bad);
assert_eq!(
r.verdict,
Verdict::Invalid {
stage: Stage::Comparator,
reason: InvalidReason::NonFinite
},
"{r}"
);
println!("cell A7 {:?}", r.verdict);
}
#[test]
fn a8_infinity_is_not_finite() {
cell(
"A8",
vec![f64::INFINITY, 1.0],
vec![1.0, 1.0],
Verdict::Invalid {
stage: Stage::Analytic,
reason: InvalidReason::NonFinite,
},
);
}
#[test]
fn a4_step_below_ulp_is_unrepresentable() {
let realized = gradcheck::realized_step(1e6, 1e-3);
println!("cell A4 realized_step(1e6, 1e-3) = {realized}");
assert_eq!(
realized, 0.0,
"a step below ULP must realize as exactly zero"
);
let ok = gradcheck::realized_step(1.0, 1e-2);
assert!(
ok > 0.0,
"a representable step must realize non-zero, got {ok}"
);
}
#[test]
fn precedence_invalid_beats_mismatch_beats_unchecked_beats_pass() {
let c = cfg();
let m = compare("P1", "control", &[2], vec![1.0, 9.0], vec![1.0, 3.0], &c);
assert_eq!(m.verdict, Verdict::Mismatch, "{m}");
let i = compare(
"P2",
"control",
&[2],
vec![f64::NAN, 9.0],
vec![1.0, 3.0],
&c,
);
assert!(matches!(i.verdict, Verdict::Invalid { .. }), "{i}");
let u = compare("P3", "control", &[2], vec![1.0, 1e-5], vec![1.0, 1e-5], &c);
assert_eq!(
u.verdict,
Verdict::Unchecked(UncheckedReason::Partial {
checked: 1,
total: 2
}),
"{u}"
);
assert!(
!u.passed(),
"a case with an abstaining component must not pass"
);
println!(
"precedence: {:?} / {:?} / {:?}",
m.verdict, i.verdict, u.verdict
);
}