aprender-core 0.29.1

Next-generation machine learning library in pure Rust
// CONTRACT: linear-models-v1.yaml
// HASH: sha256:b4c5d6e7f8a91023
// Generated by: pv probar --binding
// DO NOT EDIT — regenerate with `pv probar --binding`

use aprender::linear_model::LinearRegression;
use aprender::primitives::{Matrix, Vector};
use aprender::traits::Estimator;
use proptest::prelude::*;

/// Strategy: generate random training data with n samples and d features.
/// Ensures n_samples > n_features + 1 so OLS is well-determined with intercept.
fn ols_training_strategy() -> impl Strategy<Value = (Matrix<f32>, Vector<f32>, usize, usize)> {
    (10usize..=30, 2usize..=4)
        .prop_flat_map(|(n, d)| {
            let features = proptest::collection::vec(-10.0f32..10.0f32, n * d);
            let targets = proptest::collection::vec(-50.0f32..50.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_slice(&targets);
            (x, y, n, d)
        })
}

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

    // ──────────────────────────────────────────────────────────
    // FALSIFY-LM-001: OLS training R² non-negative
    // Formal: score(X_train, y_train) >= -epsilon for fitted OLS
    // ──────────────────────────────────────────────────────────
    /// Obligation: R² on training data is non-negative for overdetermined OLS
    #[test]
    fn prop_ols_r2_non_negative(
        (x, y, n, d) in ols_training_strategy(),
    ) {
        // OLS requires n > d (with intercept: n > d + 1)
        prop_assume!(n > d + 1);

        let mut model = LinearRegression::new();
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let r2 = model.score(&x, &y);
        let eps = 1e-6;
        prop_assert!(
            r2 >= -eps,
            "FALSIFY-LM-001: R²={}, expected >= -epsilon (n={}, d={})", r2, n, d
        );
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-LM-002: Prediction deterministic
    // Formal: predict(X) == predict(X) for same fitted model and input
    // ──────────────────────────────────────────────────────────
    /// Obligation: Predictions are deterministic for the same input
    #[test]
    fn prop_prediction_deterministic(
        (x, y, n, d) in ols_training_strategy(),
    ) {
        prop_assume!(n > d + 1);

        let mut model = LinearRegression::new();
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

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

        prop_assert!(
            pred1 == pred2,
            "FALSIFY-LM-002: predictions differ between two calls on same data"
        );
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-LM-003: Perfect fit on exact linear data
    // Formal: y = 2*x0 + 3*x1 + 1 => R² > 0.99
    // ──────────────────────────────────────────────────────────
    /// Obligation: OLS achieves near-perfect R² on exactly linear data
    #[test]
    fn prop_perfect_fit_linear_data(
        n in 10usize..30,
        seed in 0u64..10000,
    ) {
        let d = 2;

        // Deterministic pseudo-random feature generation via xorshift64
        let mut rng_state = seed.wrapping_add(1); // avoid zero seed
        let mut next_val = || -> f32 {
            rng_state ^= rng_state << 13;
            rng_state ^= rng_state >> 7;
            rng_state ^= rng_state << 17;
            // Map to [-5, 5]
            (rng_state as f32 / u64::MAX as f32) * 10.0 - 5.0
        };

        let mut features = Vec::with_capacity(n * d);
        let mut targets = Vec::with_capacity(n);

        for _ in 0..n {
            let x0 = next_val();
            let x1 = next_val();
            features.push(x0);
            features.push(x1);
            // Exact linear relationship: y = 2*x0 + 3*x1 + 1
            targets.push(2.0 * x0 + 3.0 * x1 + 1.0);
        }

        let x = Matrix::from_vec(n, d, features).expect("valid matrix dimensions");
        let y = Vector::from_slice(&targets);

        let mut model = LinearRegression::new();
        let fit_result = model.fit(&x, &y);
        prop_assume!(fit_result.is_ok());

        let r2 = model.score(&x, &y);
        prop_assert!(
            r2 > 0.99,
            "FALSIFY-LM-003: R²={}, expected > 0.99 on exact linear data (n={})", r2, n
        );
    }

    // ──────────────────────────────────────────────────────────
    // FALSIFY-LM-004: Prediction finite
    // Formal: ∀ i, predict(X_test)[i].is_finite()
    // ──────────────────────────────────────────────────────────
    /// Obligation: All predictions are finite (no NaN or Inf)
    #[test]
    fn prop_prediction_finite(
        (x_train, y_train, n, d) in ols_training_strategy(),
        test_features in proptest::collection::vec(-10.0f32..10.0f32, 5 * 4usize),
    ) {
        prop_assume!(n > d + 1);

        let mut model = LinearRegression::new();
        let fit_result = model.fit(&x_train, &y_train);
        prop_assume!(fit_result.is_ok());

        // Build test matrix with same number of features
        let n_test = 5.min(test_features.len() / d);
        prop_assume!(n_test > 0);
        let test_data: Vec<f32> = test_features[..n_test * d].to_vec();
        let x_test = Matrix::from_vec(n_test, d, test_data).expect("valid test matrix");

        let predictions = model.predict(&x_test);

        for i in 0..n_test {
            prop_assert!(
                predictions[i].is_finite(),
                "FALSIFY-LM-004: prediction[{}]={}, expected finite value", i, predictions[i]
            );
        }
    }
}