use linreg_core::linalg::Matrix;
use linreg_core::regularized::{
make_lambda_path, LambdaPathOptions,
elastic_net_path, ElasticNetOptions,
elastic_net_fit,
};
use linreg_core::regularized::path::compute_lambda_max;
use linreg_core::regularized::preprocess::{standardize_xy, StandardizeOptions};
#[test]
fn test_compute_lambda_max_lasso_finite_positive() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x2 = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let n = 5;
let p = 2;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
x_data[i * (p + 1) + 2] = x2[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (x_standardized, y_standardized, _) = standardize_xy(&x, &y, &standardization_options);
let lambda_max = compute_lambda_max(&x_standardized, &y_standardized, 1.0, None, Some(0));
assert!(lambda_max > 0.0, "lambda_max should be positive for lasso");
assert!(lambda_max.is_finite(), "lambda_max should be finite for lasso");
}
#[test]
fn test_compute_lambda_max_ridge_returns_infinity() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (x_standardized, y_standardized, _) = standardize_xy(&x, &y, &standardization_options);
let lambda_max = compute_lambda_max(&x_standardized, &y_standardized, 0.0, None, Some(0));
assert!(lambda_max.is_finite(), "lambda_max should be finite for ridge (alpha=0) using max(alpha, 1e-3)");
assert!(lambda_max > 100.0, "lambda_max should be large for ridge");
}
#[test]
fn test_standardize_y_unit_norm() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (_, y_standardized, info) = standardize_xy(&x, &y, &standardization_options);
let y_norm: f64 = y_standardized.iter().map(|&v| v * v).sum::<f64>().sqrt();
assert!((y_norm - 1.0).abs() < 1e-10, "y should be at unit norm after standardization");
let y_centered: Vec<f64> = y.iter().map(|&yi| yi - 3.0).collect();
let expected_scale: f64 = y_centered.iter().map(|&v| v * v).sum::<f64>().sqrt() / (n as f64).sqrt();
if let Some(scale) = info.y_scale {
assert!((scale - expected_scale).abs() < 1e-10, "y_scale should match L2 norm");
}
}
#[test]
fn test_standardize_preserves_intercept_column() {
let y = vec![10.0, 20.0, 30.0, 40.0, 50.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (x_standardized, _, _) = standardize_xy(&x, &y, &standardization_options);
for i in 0..n {
assert!((x_standardized.get(i, 0) - 1.0).abs() < 1e-10,
"Intercept column should remain as ones when intercept=true");
}
}
#[test]
fn test_lambda_path_geometric_decay() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (x_standardized, y_standardized, _) = standardize_xy(&x, &y, &standardization_options);
let nlambda = 10;
let lambda_min_ratio_val = 0.01;
let path_options = LambdaPathOptions {
nlambda,
lambda_min_ratio: Some(lambda_min_ratio_val),
alpha: 1.0,
eps_for_ridge: 1e-3,
};
let lambdas = make_lambda_path(&x_standardized, &y_standardized, &path_options, None, Some(0));
assert_eq!(lambdas.len(), nlambda, "Should generate exactly nlambda lambdas");
assert_eq!(lambdas[0], f64::INFINITY, "First lambda should be LAMBDA_EFFECTIVE_INFINITY (infinity)");
const EPS: f64 = 1.0e-6;
let lambda_min_ratio_clamped = lambda_min_ratio_val.max(EPS);
let lambda_decay_factor = lambda_min_ratio_clamped.powf(1.0 / (nlambda - 1) as f64);
for i in 2..lambdas.len() {
let expected_ratio = lambdas[i] / lambdas[i - 1];
assert!((expected_ratio - lambda_decay_factor).abs() < 1e-10,
"Lambda path should follow geometric progression at index {}: expected {}, got {}",
i, lambda_decay_factor, expected_ratio);
}
for i in 1..lambdas.len() {
assert!(lambdas[i] < lambdas[i - 1],
"Lambda path should be strictly decreasing at index {}", i);
}
}
#[test]
fn test_lambda_path_first_lambda_is_lambda_max() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x2 = vec![2.0, 1.0, 0.5, 0.2, 0.1];
let n = 5;
let p = 2;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
x_data[i * (p + 1) + 2] = x2[i];
}
let x = Matrix::new(n, p + 1, x_data);
let standardization_options = StandardizeOptions {
intercept: true,
standardize_x: true,
standardize_y: true,
weights: None,
};
let (x_standardized, y_standardized, _) = standardize_xy(&x, &y, &standardization_options);
let lambda_max = compute_lambda_max(&x_standardized, &y_standardized, 1.0, None, Some(0));
let path_options = LambdaPathOptions {
nlambda: 5,
lambda_min_ratio: Some(0.1),
alpha: 1.0,
eps_for_ridge: 1e-3,
};
let lambdas = make_lambda_path(&x_standardized, &y_standardized, &path_options, None, Some(0));
assert_eq!(lambdas[0], f64::INFINITY, "First lambda should be LAMBDA_EFFECTIVE_INFINITY (infinity)");
let expected_second = 0.1_f64.powf(1.0 / 4.0) * lambda_max;
assert!((lambdas[1] - expected_second).abs() < 1e-10,
"Second lambda should equal lambda_decay_factor * lambda_max");
}
#[test]
fn test_soft_threshold_formula() {
let test_cases: [(f64, f64, f64); 6] = [
(1.5, 0.5, 1.0), (0.3, 0.5, 0.0), (-1.2, 1.0, -0.2), (-0.8, 0.3, -0.5), (0.0, 0.5, 0.0), (5.0, 0.0, 5.0), ];
for (z, threshold, expected) in test_cases {
let result = if z.abs() > threshold {
z.signum() * (z.abs() - threshold)
} else {
0.0_f64
};
assert!((result - expected).abs() < 1e-10,
"Soft threshold failed for z={}, threshold={}: got {}, expected {}",
z, threshold, result, expected);
}
}
#[test]
fn test_elastic_net_denominator() {
let lambda: f64 = 0.5;
let alpha: f64 = 0.7; let penalty_factor_value: f64 = 1.0;
let expected = 1.0_f64 + penalty_factor_value * lambda * (1.0 - alpha);
assert!((expected - 1.15_f64).abs() < 1e-10, "Elastic net denominator calculation");
let lasso_denom = 1.0_f64 + lambda * (1.0 - 1.0);
assert!((lasso_denom - 1.0_f64).abs() < 1e-10, "Lasso denominator should be 1.0");
let ridge_denom = 1.0_f64 + lambda * (1.0 - 0.0);
assert!((ridge_denom - 1.5_f64).abs() < 1e-10, "Ridge denominator should be 1.5");
}
#[test]
fn test_elastic_net_path_convergence() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x2 = vec![0.5, 1.0, 1.5, 2.0, 2.5];
let n = 5;
let p = 2;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
x_data[i * (p + 1) + 2] = x2[i];
}
let x = Matrix::new(n, p + 1, x_data);
let path_options = LambdaPathOptions {
nlambda: 5,
lambda_min_ratio: Some(0.1),
alpha: 1.0,
eps_for_ridge: 1e-3,
};
let fit_options = ElasticNetOptions {
lambda: 0.0, alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 1000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: None,
};
let result = elastic_net_path(&x, &y, &path_options, &fit_options);
assert!(result.is_ok(), "Elastic net path should converge");
let fits = result.unwrap();
assert_eq!(fits.len(), 5, "Should generate 5 fits");
for (i, fit) in fits.iter().enumerate() {
assert!(fit.converged, "Fit {} should converge", i);
}
for i in 1..fits.len() {
assert!(fits[i].lambda < fits[i - 1].lambda,
"Lambda values should be decreasing: {} < {}",
fits[i].lambda, fits[i - 1].lambda);
}
let first_fit = &fits[0];
let nonzero_count = first_fit.coefficients.iter().filter(|&&c| c.abs() > 0.0).count();
assert!(nonzero_count <= 1, "At high lambda, most coefficients should be zero");
}
#[test]
fn test_coefficient_bounds_non_negative() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x2 = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
let n = 5;
let p = 2;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
x_data[i * (p + 1) + 2] = x2[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![(0.0, f64::INFINITY); p];
let options = ElasticNetOptions {
lambda: 0.1,
alpha: 1.0, intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_ok(), "Fit with non-negative bounds should succeed");
let fit = result.unwrap();
assert!(fit.converged, "Should converge with bounds");
for (i, &coef) in fit.coefficients.iter().enumerate() {
assert!(coef >= 0.0, "Coefficient {} should be non-negative, got {}", i, coef);
}
}
#[test]
fn test_coefficient_bounds_upper_limit() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![(-f64::INFINITY, 1.0)];
let options = ElasticNetOptions {
lambda: 0.01, alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_ok(), "Fit with upper bound should succeed");
let fit = result.unwrap();
assert!(fit.converged, "Should converge with upper bound");
assert!(fit.coefficients[0] <= 1.0,
"Coefficient should be <= 1.0, got {}", fit.coefficients[0]);
}
#[test]
fn test_coefficient_bounds_both_limits() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x2 = vec![0.5, 1.0, 1.5, 2.0, 2.5];
let n = 5;
let p = 2;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
x_data[i * (p + 1) + 2] = x2[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![
(-1.0, 1.0), (0.0, 2.0), ];
let options = ElasticNetOptions {
lambda: 0.01,
alpha: 0.5, intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_ok(), "Fit with both bounds should succeed");
let fit = result.unwrap();
assert!(fit.converged, "Should converge with both bounds");
assert!(fit.coefficients[0] >= -1.0 && fit.coefficients[0] <= 1.0,
"Coefficient 0 should be in [-1, 1], got {}", fit.coefficients[0]);
assert!(fit.coefficients[1] >= 0.0 && fit.coefficients[1] <= 2.0,
"Coefficient 1 should be in [0, 2], got {}", fit.coefficients[1]);
}
#[test]
fn test_coefficient_bounds_validation_wrong_length() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)];
let options = ElasticNetOptions {
lambda: 0.1,
alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_err(), "Should reject bounds with wrong length");
}
#[test]
fn test_coefficient_bounds_validation_inverted_bounds() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![(1.0, 0.0)];
let options = ElasticNetOptions {
lambda: 0.1,
alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_err(), "Should reject inverted bounds");
}
#[test]
fn test_coefficient_bounds_no_bounds_same_result() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let options_no_bounds = ElasticNetOptions {
lambda: 0.1,
alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: None,
};
let options_unbounded = ElasticNetOptions {
coefficient_bounds: Some(vec![(-f64::INFINITY, f64::INFINITY)]),
..options_no_bounds.clone()
};
let result_no_bounds = elastic_net_fit(&x, &y, &options_no_bounds);
let result_unbounded = elastic_net_fit(&x, &y, &options_unbounded);
assert!(result_no_bounds.is_ok());
assert!(result_unbounded.is_ok());
let fit_no_bounds = result_no_bounds.unwrap();
let fit_unbounded = result_unbounded.unwrap();
assert!((fit_no_bounds.coefficients[0] - fit_unbounded.coefficients[0]).abs() < 1e-10,
"None bounds and explicit unbounded should produce same result");
}
#[test]
fn test_coefficient_bounds_at_convergence() {
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0]; let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let n = 5;
let p = 1;
let mut x_data = vec![1.0; n * (p + 1)];
for i in 0..n {
x_data[i * (p + 1) + 1] = x1[i];
}
let x = Matrix::new(n, p + 1, x_data);
let bounds = vec![(0.0, 1.5)];
let options = ElasticNetOptions {
lambda: 0.0, alpha: 1.0,
intercept: true,
standardize: true,
max_iter: 10000,
tol: 1e-7,
penalty_factor: None,
weights: None,
warm_start: None,
coefficient_bounds: Some(bounds),
};
let result = elastic_net_fit(&x, &y, &options);
assert!(result.is_ok(), "Fit should succeed");
let fit = result.unwrap();
assert!(fit.converged, "Should converge even when at bound");
assert!(fit.coefficients[0] <= 1.5 + 1e-6,
"Coefficient should be clamped to upper bound, got {}", fit.coefficients[0]);
}