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

//! Tolerances and step size.

/// How a gradient check is run and judged.
///
/// The two tolerances play asymmetric roles, and the asymmetry is the point:
///
/// - `rel_tol` is the **certification** criterion. `Pass` requires `|a - n| <= rel_tol * M`.
/// - `abs_tol` is an **excuse only**. It widens the bar for calling something wrong, so
///   float noise near zero does not raise false alarms. It can never grant a pass.
///
/// See [`crate::Verdict`] for why. Allowing `abs_tol` onto the pass side lets a gradient
/// that is 100% wrong be certified as correct whenever both values are small.
#[derive(Debug, Clone)]
pub struct Config {
    /// Central-difference step. Too small and float cancellation dominates; too large and
    /// the truncation error of the approximation does.
    ///
    /// Measured on burn/ndarray with host-side `f64` reduction, the achievable relative
    /// error is `1e-5`–`1e-4`, and the best step is per-operation: `1e-1` for `square`,
    /// `1e-2`–`1e-3` for `tanh`/`exp`/`sin`.
    pub step: f64,
    /// Certification tolerance. A component passes only when
    /// `|analytic - numeric| <= rel_tol * max(|analytic|, |numeric|)`.
    pub rel_tol: f64,
    /// Rejection floor. A component is rejected when
    /// `|analytic - numeric| > abs_tol + rel_tol * max(|analytic|, |numeric|)`.
    ///
    /// Never used to grant a pass.
    pub abs_tol: f64,
    /// The precision the backend computes in. Used to compute the realized step.
    pub precision: crate::Precision,
    /// Certification floor: components whose magnitude is at or below this never pass,
    /// only abstain. Carries no detection semantics — a large enough discrepancy is
    /// rejected at any magnitude, including below this floor.
    pub floor: f64,
}

impl Default for Config {
    fn default() -> Self {
        Self::f32_defaults()
    }
}

impl Config {
    /// Defaults for `f32` frameworks read back through a host-side `f64` reduction.
    ///
    /// These are provisional and gated by the control matrix in `tests/control_matrix.rs`;
    /// they are not claimed to hold universally. Any published result should state which
    /// backends and cases the matrix validated them on.
    pub fn f32_defaults() -> Self {
        Self {
            step: 1e-2,
            rel_tol: 1e-3,
            abs_tol: 1e-4,
            floor: 2e-4,
            precision: crate::Precision::F32,
        }
    }

    /// Tighter settings for frameworks computing in `f64`.
    pub fn f64_defaults() -> Self {
        Self {
            step: 1e-5,
            rel_tol: 1e-6,
            abs_tol: 1e-10,
            floor: 2e-10,
            precision: crate::Precision::F64,
        }
    }

    /// Override the step size.
    pub fn with_step(mut self, step: f64) -> Self {
        self.step = step;
        self
    }

    /// Override the certification tolerance.
    pub fn with_rel_tol(mut self, rel_tol: f64) -> Self {
        self.rel_tol = rel_tol;
        self
    }

    /// Override the rejection floor.
    pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
        self.abs_tol = abs_tol;
        self
    }

    /// Override the backend's working precision.
    pub fn with_precision(mut self, precision: crate::Precision) -> Self {
        self.precision = precision;
        self
    }

    /// Override the certification floor.
    pub fn with_floor(mut self, floor: f64) -> Self {
        self.floor = floor;
        self
    }

    /// True when every tolerance is finite and usable.
    pub(crate) fn is_finite(&self) -> bool {
        self.step.is_finite()
            && self.rel_tol.is_finite()
            && self.abs_tol.is_finite()
            && self.floor.is_finite()
            && self.step > 0.0
    }
}