aprender-core 0.29.2

Next-generation machine learning library in pure Rust
//! FALSIFY contract tests for GLM (Generalized Linear Models)
//!
//! Tests fundamental mathematical invariants of GLM predictions:
//! - Poisson predictions are non-negative (exp of linear predictor)
//! - Binomial predictions are in (0, 1) (logistic link)
//! - All predictions are finite (no NaN/Inf)
//! - Predictions are deterministic

// CONTRACT: glm-v1.yaml
// HASH: sha256:c7d8e9f0a1b23456
// Generated by: pv probar --binding
// DO NOT EDIT — regenerate with `pv probar --binding`

use aprender::glm::{Family, GLM};
use aprender::primitives::{Matrix, Vector};
use proptest::prelude::*;

/// Strategy: generate Poisson-suitable training data.
/// y values are non-negative (required by Poisson family validation).
/// Features in [-2, 2] to keep predictions in a reasonable range.
fn poisson_training_strategy() -> impl Strategy<Value = (Matrix<f32>, Vector<f32>)> {
    (20usize..=30, 2usize..=3)
        .prop_flat_map(|(n, d)| {
            let features = proptest::collection::vec(-2.0f32..2.0f32, n * d);
            // Poisson requires non-negative counts: abs(val) + 0.1 avoids zero
            let targets = proptest::collection::vec(0.1f32..10.0f32, n);
            let n_val = Just(n);
            let d_val = Just(d);
            (features, targets, n_val, d_val)
        })
        .prop_map(|(features, targets, n, d)| {
            let x = Matrix::from_vec(n, d, features).expect("valid matrix dimensions");
            let y = Vector::from_vec(targets);
            (x, y)
        })
}

/// Strategy: generate Binomial-suitable training data.
/// y values are in [0, 1] (required by Binomial family validation).
fn binomial_training_strategy() -> impl Strategy<Value = (Matrix<f32>, Vector<f32>)> {
    (20usize..=30, 2usize..=3)
        .prop_flat_map(|(n, d)| {
            let features = proptest::collection::vec(-2.0f32..2.0f32, n * d);
            // Binomial requires values in [0, 1]
            let targets = proptest::collection::vec(0.01f32..0.99f32, n);
            let n_val = Just(n);
            let d_val = Just(d);
            (features, targets, n_val, d_val)
        })
        .prop_map(|(features, targets, n, d)| {
            let x = Matrix::from_vec(n, d, features).expect("valid matrix dimensions");
            let y = Vector::from_vec(targets);
            (x, y)
        })
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(128))]

    // ──────────────────────────────────────────────────────────
    // FALSIFY-GLM-001: Poisson GLM predictions are non-negative
    // Formal: ∀ i, predict(X)[i] >= 0  (since μ = exp(η) > 0)
    // ──────────────────────────────────────────────────────────
    /// Obligation: Poisson GLM with log link produces non-negative predictions.
    /// The inverse link is exp(η), which is strictly positive for all finite η.
    #[test]
    fn prop_poisson_predictions_non_negative(
        (x, y) in poisson_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Poisson);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let predictions = model.predict(&x);
        prop_assume!(predictions.is_ok());
        let preds = predictions.expect("predict succeeded after assume");

        for i in 0..preds.len() {
            prop_assert!(
                preds[i] >= 0.0,
                "FALSIFY-GLM-001: Poisson prediction[{}] = {}, expected >= 0.0 (exp of linear predictor)",
                i, preds[i]
            );
        }
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-GLM-002: Binomial GLM predictions in (0, 1)
    // Formal: ∀ i, 0 < predict(X)[i] < 1  (logistic sigmoid range)
    // ──────────────────────────────────────────────────────────
    /// Obligation: Binomial GLM with logit link produces predictions in (0, 1).
    /// The inverse link is σ(η) = 1/(1+exp(-η)), bounded in (0, 1).
    #[test]
    fn prop_binomial_predictions_in_unit_interval(
        (x, y) in binomial_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Binomial);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let predictions = model.predict(&x);
        prop_assume!(predictions.is_ok());
        let preds = predictions.expect("predict succeeded after assume");

        for i in 0..preds.len() {
            prop_assert!(
                preds[i] > 0.0 && preds[i] < 1.0,
                "FALSIFY-GLM-002: Binomial prediction[{}] = {}, expected in (0, 1) (logistic link)",
                i, preds[i]
            );
        }
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-GLM-003: Predictions are finite (no NaN/Inf)
    // Formal: ∀ family ∈ {Poisson, Binomial}, ∀ i, predict(X)[i].is_finite()
    // ──────────────────────────────────────────────────────────
    /// Obligation: GLM predictions contain no NaN or Inf values.
    /// IRLS with bounded features should always produce finite coefficients.
    #[test]
    fn prop_predictions_finite_poisson(
        (x, y) in poisson_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Poisson);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let predictions = model.predict(&x);
        prop_assume!(predictions.is_ok());
        let preds = predictions.expect("predict succeeded after assume");

        for i in 0..preds.len() {
            prop_assert!(
                preds[i].is_finite(),
                "FALSIFY-GLM-003: Poisson prediction[{}] = {}, expected finite (no NaN/Inf)",
                i, preds[i]
            );
        }
    }

    #[test]
    fn prop_predictions_finite_binomial(
        (x, y) in binomial_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Binomial);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let predictions = model.predict(&x);
        prop_assume!(predictions.is_ok());
        let preds = predictions.expect("predict succeeded after assume");

        for i in 0..preds.len() {
            prop_assert!(
                preds[i].is_finite(),
                "FALSIFY-GLM-003: Binomial prediction[{}] = {}, expected finite (no NaN/Inf)",
                i, preds[i]
            );
        }
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-GLM-004: Predictions are deterministic
    // Formal: predict(X) == predict(X) for same fitted model and input
    // ──────────────────────────────────────────────────────────
    /// Obligation: Calling predict twice on the same input produces identical results.
    #[test]
    fn prop_predictions_deterministic_poisson(
        (x, y) in poisson_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Poisson);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let pred1 = model.predict(&x);
        let pred2 = model.predict(&x);
        prop_assume!(pred1.is_ok() && pred2.is_ok());

        let p1 = pred1.expect("predict succeeded (1st)");
        let p2 = pred2.expect("predict succeeded (2nd)");

        for i in 0..p1.len() {
            prop_assert!(
                p1[i] == p2[i],
                "FALSIFY-GLM-004: Poisson prediction[{}] differs: {} vs {} (must be deterministic)",
                i, p1[i], p2[i]
            );
        }
    }

    #[test]
    fn prop_predictions_deterministic_binomial(
        (x, y) in binomial_training_strategy(),
    ) {
        let mut model = GLM::new(Family::Binomial);
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let pred1 = model.predict(&x);
        let pred2 = model.predict(&x);
        prop_assume!(pred1.is_ok() && pred2.is_ok());

        let p1 = pred1.expect("predict succeeded (1st)");
        let p2 = pred2.expect("predict succeeded (2nd)");

        for i in 0..p1.len() {
            prop_assert!(
                p1[i] == p2[i],
                "FALSIFY-GLM-004: Binomial prediction[{}] differs: {} vs {} (must be deterministic)",
                i, p1[i], p2[i]
            );
        }
    }
}