gradcheck 0.1.0

Finite-difference gradient checking for Rust ML frameworks. Verifies an autodiff engine against an independent numerical oracle, with a negative control that must fail.
// gradcheck — finite-difference gradient checking for Rust ML frameworks.
// Copyright (c) 2026 Henos D <henosd19@gmail.com> (GitHub: @4ktLuffy)
// Repository: https://github.com/4ktLuffy/gradcheck
// SPDX-License-Identifier: MIT OR Apache-2.0

//! The control matrix.
//!
//! **Gate rule: a cell passes only on its EXACT expected verdict, stage and reason
//! included. Any other outcome — including a higher-precedence one — fails the cell.**
//!
//! That rule exists because "must not Pass" is satisfiable by any failure, including one
//! produced by the wrong mechanism. A cell that only asserts "not Pass" can be green
//! while the thing it was written to test is broken.
//!
//! Runs with no framework installed: `cargo test --test control_matrix`.

use gradcheck::{compare, Config, InvalidReason, Stage, UncheckedReason, Verdict};

fn cfg() -> Config {
    // atol = 1e-4, rtol = 1e-3, floor = 2e-4
    Config::f32_defaults()
}

/// Assert the exact verdict, and say clearly what was expected when it differs.
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);
}

// ---------------------------------------------------------------------------
// Rejection cells — must produce exactly Mismatch
// ---------------------------------------------------------------------------

#[test]
fn r1_scale_corruption_at_unit_magnitude() {
    // x1.5 on O(1) gradients
    cell(
        "R1",
        vec![1.5, 3.0, -1.5],
        vec![1.0, 2.0, -1.0],
        Verdict::Mismatch,
    );
}

#[test]
fn r2_the_c13_repro() {
    // The measured hole: a 1e-3 gradient doubled used to pass with rel_err 1e-3.
    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() {
    // M = 1e-5 (far below floor = 2e-4) but the discrepancy is enormous.
    // v3 blanket-abstained here; the three-way rule must still reject.
    cell("R8", vec![1e-5], vec![1.001e-3], Verdict::Mismatch);
}

// ---------------------------------------------------------------------------
// Boundary cells
// ---------------------------------------------------------------------------

#[test]
fn b1_the_additive_tolerance_wedge() {
    // THE round-2 wedge. n = 1.001e-4, a = 2n.
    //   diff        = 1.001e-4
    //   reject bound= atol + rtol*M = 1e-4 + 1e-3*2.002e-4 = 1.002002e-4  -> not Mismatch
    //   certify     = rtol*M        = 2.002e-7                            -> not Pass
    // A x2 corruption that is fully finite and fully compared must NEVER be Pass.
    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();
    // Pass requires diff <= rtol*M. At M = 1.0 that is 1e-3.
    // just inside -> Pass
    let inside = compare("B3c", "control", &[1], vec![1.0], vec![1.0 - 9e-4], &c);
    assert_eq!(inside.verdict, Verdict::Pass, "{inside}");
    // just outside the certify bound but inside the reject bound -> abstain
    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}"
    );
    // well outside both -> Mismatch
    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();
    // At or below the floor, a correct component abstains rather than certifying.
    let below = compare("B2a", "control", &[1], vec![1e-4], vec![1e-4], &c);
    assert_eq!(
        below.verdict,
        Verdict::Unchecked(UncheckedReason::AmbiguousAtFloor),
        "{below}"
    );
    // Comfortably above the floor, an exact match certifies.
    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
    );
}

// ---------------------------------------------------------------------------
// Invalid and structural cells — exact stage and reason
// ---------------------------------------------------------------------------

#[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() {
    // The measured hole: an all-NaN gradient returned Pass with worst_rel_error = 0,
    // because `NaN > 0.0` is false and the worst-tracker never updated.
    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() {
    // An empty fold reports "no worst component", which is exactly how C12 passed.
    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,
        },
    );
}

// ---------------------------------------------------------------------------
// A4 — unrepresentable step
// ---------------------------------------------------------------------------

#[test]
fn a4_step_below_ulp_is_unrepresentable() {
    // At x = 1e6 in f32 the ULP is 0.0625, so a step of 1e-3 rounds away entirely.
    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"
    );

    // and a representable one must not report zero
    let ok = gradcheck::realized_step(1.0, 1e-2);
    assert!(
        ok > 0.0,
        "a representable step must realize non-zero, got {ok}"
    );
}

// ---------------------------------------------------------------------------
// Precedence
// ---------------------------------------------------------------------------

#[test]
fn precedence_invalid_beats_mismatch_beats_unchecked_beats_pass() {
    let c = cfg();
    // one Pass component + one Mismatch -> case is Mismatch
    let m = compare("P1", "control", &[2], vec![1.0, 9.0], vec![1.0, 3.0], &c);
    assert_eq!(m.verdict, Verdict::Mismatch, "{m}");

    // one Mismatch + one NaN -> Invalid wins
    let i = compare(
        "P2",
        "control",
        &[2],
        vec![f64::NAN, 9.0],
        vec![1.0, 3.0],
        &c,
    );
    assert!(matches!(i.verdict, Verdict::Invalid { .. }), "{i}");

    // one Pass + one abstention -> Unchecked(Partial), never Pass
    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
    );
}