1use crate::error::{CsError, CsResult};
11use crate::linalg::cholesky::{cholesky_factor, cholesky_solve};
12use crate::linalg::{mat_t_vec, mat_vec, submat_columns};
13
14#[derive(Debug, Clone)]
16pub struct LarsStep {
17 pub lambda: f64,
18 pub beta: Vec<f64>,
19 pub active: Vec<usize>,
20}
21
22#[derive(Debug, Clone)]
24pub struct LarsPath {
25 pub steps: Vec<LarsStep>,
26}
27
28pub fn lars(phi: &[f64], m: usize, n: usize, y: &[f64], max_iter: usize) -> CsResult<LarsPath> {
33 if phi.len() != m * n {
34 return Err(CsError::ShapeMismatch {
35 expected: vec![m, n],
36 got: vec![phi.len()],
37 });
38 }
39 if y.len() != m {
40 return Err(CsError::DimensionMismatch { a: y.len(), b: m });
41 }
42 let mut active: Vec<usize> = Vec::new();
43 let mut signs: Vec<f64> = Vec::new();
44 let mut beta = vec![0.0_f64; n];
45 let mut residual = y.to_vec();
46 let mut steps: Vec<LarsStep> = Vec::new();
47 let max_steps = max_iter.min(2 * n.min(m));
48 for _step in 0..max_steps {
49 let corr = mat_t_vec(phi, m, n, &residual)?;
50 let mut c_max = 0.0_f64;
52 for (j, &c) in corr.iter().enumerate() {
53 let a = c.abs();
54 if !active.contains(&j) && a > c_max {
55 c_max = a;
56 }
57 }
58 steps.push(LarsStep {
60 lambda: c_max,
61 beta: beta.clone(),
62 active: active.clone(),
63 });
64 if c_max < 1.0e-12 {
65 break;
66 }
67 let mut new_idx = usize::MAX;
69 let mut best_val = 0.0_f64;
70 for (j, &c) in corr.iter().enumerate() {
71 if active.contains(&j) {
72 continue;
73 }
74 let a = c.abs();
75 if a > best_val {
76 best_val = a;
77 new_idx = j;
78 }
79 }
80 if new_idx == usize::MAX {
81 break;
82 }
83 let sign_new = corr[new_idx].signum();
84 active.push(new_idx);
85 signs.push(sign_new);
86 let phi_a = submat_columns(phi, m, n, &active)?;
88 let a_sz = active.len();
89 let mut g = vec![0.0_f64; a_sz * a_sz];
91 for i in 0..m {
92 for a in 0..a_sz {
93 let pai = phi_a[i * a_sz + a];
94 for b in 0..a_sz {
95 g[a * a_sz + b] += pai * phi_a[i * a_sz + b];
96 }
97 }
98 }
99 for d in 0..a_sz {
101 g[d * a_sz + d] += 1.0e-12;
102 }
103 let l = cholesky_factor(&g, a_sz)?;
104 let w = cholesky_solve(&l, a_sz, &signs)?;
105 let dot_wsigns: f64 = w.iter().zip(signs.iter()).map(|(a, b)| a * b).sum();
107 let aa = if dot_wsigns > 0.0 {
108 1.0 / dot_wsigns.sqrt()
109 } else {
110 return Err(CsError::NumericalInstability(
111 "LARS: negative or zero dot(w, signs)".into(),
112 ));
113 };
114 let u_a: Vec<f64> = w.iter().map(|wi| wi * aa).collect();
115 let mut d_beta = vec![0.0_f64; n];
117 for (k, &j) in active.iter().enumerate() {
118 d_beta[j] = u_a[k];
119 }
120 let eq = mat_vec(phi, m, n, &d_beta)?;
122 let a_j = mat_t_vec(phi, m, n, &eq)?;
124 let mut gamma = c_max / aa;
126 if active.len() < n {
127 for (j, &cj) in corr.iter().enumerate() {
128 if active.contains(&j) {
129 continue;
130 }
131 let aj = a_j[j];
132 let g1 = (c_max - cj) / (aa - aj);
133 let g2 = (c_max + cj) / (aa + aj);
134 if g1 > 1.0e-12 && g1 < gamma {
135 gamma = g1;
136 }
137 if g2 > 1.0e-12 && g2 < gamma {
138 gamma = g2;
139 }
140 }
141 }
142 let mut hit_zero = usize::MAX;
144 let mut gamma_zero = gamma;
145 for (k, &j) in active.iter().enumerate() {
146 let dj = d_beta[j];
147 if dj.abs() > 1.0e-12 {
148 let t = -beta[j] / dj;
149 if t > 1.0e-12 && t < gamma_zero {
150 gamma_zero = t;
151 hit_zero = k;
152 }
153 }
154 }
155 let actual_gamma = if hit_zero != usize::MAX {
156 gamma_zero
157 } else {
158 gamma
159 };
160 for j in 0..n {
162 beta[j] += actual_gamma * d_beta[j];
163 }
164 for i in 0..m {
166 residual[i] -= actual_gamma * eq[i];
167 }
168 if hit_zero != usize::MAX {
170 let idx_drop = active[hit_zero];
171 beta[idx_drop] = 0.0;
172 active.remove(hit_zero);
173 signs.remove(hit_zero);
174 }
175 }
176 steps.push(LarsStep {
177 lambda: 0.0,
178 beta: beta.clone(),
179 active: active.clone(),
180 });
181 Ok(LarsPath { steps })
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn lars_runs() {
190 let phi = vec![1.0, 0.1, 0.0, 1.0];
192 let y = vec![1.0, 1.0];
193 let path = lars(&phi, 2, 2, &y, 10).expect("ok");
194 assert!(!path.steps.is_empty());
195 let near_zero = path
197 .steps
198 .iter()
199 .min_by(|a, b| {
200 a.lambda
201 .abs()
202 .partial_cmp(&b.lambda.abs())
203 .unwrap_or(std::cmp::Ordering::Equal)
204 })
205 .expect("ok");
206 assert!(
208 (near_zero.beta[0] - 0.9).abs() < 0.2,
209 "beta[0]={}",
210 near_zero.beta[0]
211 );
212 assert!(
213 (near_zero.beta[1] - 1.0).abs() < 0.2,
214 "beta[1]={}",
215 near_zero.beta[1]
216 );
217 }
218}