use crate::error::{CsError, CsResult};
use crate::linalg::norm2;
pub fn coord_descent_lasso(
phi: &[f64],
m: usize,
n: usize,
y: &[f64],
lambda: f64,
x0: Option<&[f64]>,
max_iter: usize,
tol: f64,
) -> CsResult<Vec<f64>> {
if phi.len() != m * n {
return Err(CsError::ShapeMismatch {
expected: vec![m, n],
got: vec![phi.len()],
});
}
if y.len() != m {
return Err(CsError::DimensionMismatch { a: y.len(), b: m });
}
if lambda < 0.0 {
return Err(CsError::InvalidParameter("lambda must be ≥ 0".into()));
}
let mut col_norm_sq = vec![0.0_f64; n];
for i in 0..m {
for j in 0..n {
let v = phi[i * n + j];
col_norm_sq[j] += v * v;
}
}
let mut x = match x0 {
Some(arr) => {
if arr.len() != n {
return Err(CsError::DimensionMismatch { a: arr.len(), b: n });
}
arr.to_vec()
}
None => vec![0.0_f64; n],
};
let mut r = vec![0.0_f64; m];
for i in 0..m {
let mut s = 0.0_f64;
for j in 0..n {
s += phi[i * n + j] * x[j];
}
r[i] = y[i] - s;
}
let m_f = m as f64;
for _ in 0..max_iter {
let mut max_delta = 0.0_f64;
for j in 0..n {
let cn = col_norm_sq[j];
if cn < 1.0e-300 {
continue;
}
let xj_old = x[j];
for i in 0..m {
r[i] += phi[i * n + j] * xj_old;
}
let mut zj = 0.0_f64;
for i in 0..m {
zj += phi[i * n + j] * r[i];
}
zj /= m_f;
let xj_new = if zj > lambda {
(zj - lambda) / (cn / m_f)
} else if zj < -lambda {
(zj + lambda) / (cn / m_f)
} else {
0.0
};
x[j] = xj_new;
for i in 0..m {
r[i] -= phi[i * n + j] * xj_new;
}
let d = (xj_new - xj_old).abs();
if d > max_delta {
max_delta = d;
}
}
if max_delta < tol {
break;
}
}
let _ = norm2(&r);
Ok(x)
}
pub fn lasso_path(
phi: &[f64],
m: usize,
n: usize,
y: &[f64],
lambdas: &[f64],
max_iter: usize,
tol: f64,
) -> CsResult<Vec<Vec<f64>>> {
if lambdas.is_empty() {
return Err(CsError::InvalidParameter("lambdas is empty".into()));
}
for &l in lambdas {
if l < 0.0 {
return Err(CsError::InvalidParameter(format!(
"lambda must be ≥ 0; got {l}"
)));
}
}
let mut out = Vec::with_capacity(lambdas.len());
let mut x_prev: Option<Vec<f64>> = None;
for &lam in lambdas {
let x = coord_descent_lasso(phi, m, n, y, lam, x_prev.as_deref(), max_iter, tol)?;
x_prev = Some(x.clone());
out.push(x);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coord_descent_basic() {
let phi = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
let y = vec![1.0, 2.0, 3.0];
let x = coord_descent_lasso(&phi, 3, 3, &y, 0.5, None, 200, 1.0e-12).expect("ok");
assert!(x.iter().any(|v| v.abs() > 0.5));
}
#[test]
fn coord_descent_zero_with_large_lambda() {
let phi = vec![1.0, 0.0, 0.0, 1.0];
let y = vec![1.0, 1.0];
let x = coord_descent_lasso(&phi, 2, 2, &y, 100.0, None, 100, 1.0e-9).expect("ok");
for &v in &x {
assert!(v.abs() < 1.0e-9);
}
}
#[test]
fn lasso_path_descending() {
let phi = vec![1.0, 0.0, 0.0, 1.0];
let y = vec![1.0, 1.0];
let lams = vec![1.0_f64, 0.5, 0.1, 0.0];
let paths = lasso_path(&phi, 2, 2, &y, &lams, 100, 1.0e-9).expect("ok");
assert_eq!(paths.len(), 4);
let last = &paths[3];
assert!((last[0] - 1.0).abs() < 1.0e-3);
assert!((last[1] - 1.0).abs() < 1.0e-3);
}
}