1use crate::error::{CsError, CsResult};
19
20#[derive(Debug, Clone)]
22pub struct ListaConfig {
23 pub n_measurements: usize,
25 pub n_atoms: usize,
27 pub threshold: f64,
29 pub n_layers: usize,
31}
32
33#[derive(Debug, Clone)]
35pub struct Lista {
36 w: Vec<f64>,
38 s: Vec<f64>,
40 config: ListaConfig,
41}
42
43impl Lista {
44 pub fn from_dict(dict: &[f64], config: ListaConfig) -> CsResult<Self> {
53 let m = config.n_atoms;
54 let n = config.n_measurements;
55 if dict.len() != m * n {
56 return Err(CsError::ShapeMismatch {
57 expected: vec![m, n],
58 got: vec![dict.len()],
59 });
60 }
61 if m == 0 || n == 0 {
62 return Err(CsError::InvalidParameter(
63 "n_atoms and n_measurements must be > 0".into(),
64 ));
65 }
66
67 let l: f64 = dict.iter().map(|&v| v * v).sum();
69 let inv_l = if l > 0.0 { 1.0 / l } else { 1.0 };
70
71 let w: Vec<f64> = dict.iter().map(|&v| inv_l * v).collect();
73
74 let mut s = vec![0.0_f64; m * m];
77 for i in 0..m {
78 for k in 0..m {
79 let mut dot = 0.0_f64;
80 for j in 0..n {
81 dot += dict[i * n + j] * dict[k * n + j];
82 }
83 let identity_val = if i == k { 1.0 } else { 0.0 };
84 s[i * m + k] = identity_val - inv_l * dot;
85 }
86 }
87
88 Ok(Self { w, s, config })
89 }
90
91 pub fn encode(&self, y: &[f64]) -> CsResult<Vec<f64>> {
95 let m = self.config.n_atoms;
96 let n = self.config.n_measurements;
97 if y.len() != n {
98 return Err(CsError::DimensionMismatch { a: y.len(), b: n });
99 }
100 let lam = self.config.threshold;
101 let t = self.config.n_layers;
102
103 let mut x = vec![0.0_f64; m];
105
106 let wy = matvec(&self.w, y, m, n);
108
109 for _ in 0..t {
110 let sx = matvec(&self.s, &x, m, m);
112 for i in 0..m {
113 x[i] = Self::soft_threshold(wy[i] + sx[i], lam);
114 }
115 }
116 Ok(x)
117 }
118
119 pub fn encode_batch(&self, ys: &[f64], n_batch: usize) -> CsResult<Vec<f64>> {
124 let n = self.config.n_measurements;
125 let m = self.config.n_atoms;
126 if ys.len() != n_batch * n {
127 return Err(CsError::ShapeMismatch {
128 expected: vec![n_batch, n],
129 got: vec![ys.len()],
130 });
131 }
132 let mut out = Vec::with_capacity(n_batch * m);
133 for b in 0..n_batch {
134 let y_b = &ys[b * n..(b + 1) * n];
135 let code = self.encode(y_b)?;
136 out.extend_from_slice(&code);
137 }
138 Ok(out)
139 }
140
141 #[inline]
143 #[must_use]
144 pub fn soft_threshold(v: f64, lambda: f64) -> f64 {
145 if v > lambda {
146 v - lambda
147 } else if v < -lambda {
148 v + lambda
149 } else {
150 0.0
151 }
152 }
153
154 pub fn reconstruct(dict: &[f64], code: &[f64], n: usize, m: usize) -> CsResult<Vec<f64>> {
158 if dict.len() != m * n {
159 return Err(CsError::ShapeMismatch {
160 expected: vec![m, n],
161 got: vec![dict.len()],
162 });
163 }
164 if code.len() != m {
165 return Err(CsError::DimensionMismatch {
166 a: code.len(),
167 b: m,
168 });
169 }
170 let mut y_hat = vec![0.0_f64; n];
172 for i in 0..m {
173 for j in 0..n {
174 y_hat[j] += dict[i * n + j] * code[i];
175 }
176 }
177 Ok(y_hat)
178 }
179
180 #[must_use]
182 pub fn n_atoms(&self) -> usize {
183 self.config.n_atoms
184 }
185
186 #[must_use]
188 pub fn n_measurements(&self) -> usize {
189 self.config.n_measurements
190 }
191
192 #[must_use]
194 pub fn threshold(&self) -> f64 {
195 self.config.threshold
196 }
197}
198
199fn matvec(a: &[f64], x: &[f64], out_dim: usize, in_dim: usize) -> Vec<f64> {
201 let mut y = vec![0.0_f64; out_dim];
202 for i in 0..out_dim {
203 let mut s = 0.0_f64;
204 for j in 0..in_dim {
205 s += a[i * in_dim + j] * x[j];
206 }
207 y[i] = s;
208 }
209 y
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn make_dict(m: usize, n: usize) -> Vec<f64> {
217 (0..m * n)
218 .map(|k| {
219 let i = k / n;
220 let j = k % n;
221 if (i + j) % 3 == 0 { 0.5 } else { -0.1 }
222 })
223 .collect()
224 }
225
226 fn make_config(n: usize, m: usize, lambda: f64, layers: usize) -> ListaConfig {
227 ListaConfig {
228 n_measurements: n,
229 n_atoms: m,
230 threshold: lambda,
231 n_layers: layers,
232 }
233 }
234
235 #[test]
237 fn encode_output_shape() {
238 let n = 8;
239 let m = 12;
240 let dict = make_dict(m, n);
241 let cfg = make_config(n, m, 0.1, 10);
242 let lista = Lista::from_dict(&dict, cfg).expect("ok");
243 let y = vec![0.5_f64; n];
244 let code = lista.encode(&y).expect("ok");
245 assert_eq!(code.len(), m);
246 }
247
248 #[test]
250 fn encode_sparse() {
251 let n = 8;
252 let m = 12;
253 let dict = make_dict(m, n);
254 let cfg = make_config(n, m, 0.5, 20);
255 let lista = Lista::from_dict(&dict, cfg).expect("ok");
256 let y = vec![0.3_f64; n];
257 let code = lista.encode(&y).expect("ok");
258 let nnz = code.iter().filter(|&&v| v.abs() > 1e-10).count();
259 assert!(nnz < m, "expected sparse code, got {nnz}/{m} nonzeros");
260 }
261
262 #[test]
264 fn soft_threshold_positive() {
265 let v = Lista::soft_threshold(1.5, 0.5);
266 assert!((v - 1.0).abs() < 1.0e-12);
267 }
268
269 #[test]
271 fn soft_threshold_negative() {
272 let v = Lista::soft_threshold(-1.5, 0.5);
273 assert!((v + 1.0).abs() < 1.0e-12);
274 }
275
276 #[test]
278 fn soft_threshold_within_band() {
279 let v = Lista::soft_threshold(0.3, 0.5);
280 assert!(v.abs() < 1.0e-12);
281 }
282
283 #[test]
285 fn encode_all_finite() {
286 let n = 10;
287 let m = 15;
288 let dict = make_dict(m, n);
289 let cfg = make_config(n, m, 0.05, 15);
290 let lista = Lista::from_dict(&dict, cfg).expect("ok");
291 let y: Vec<f64> = (0..n).map(|i| (i as f64) * 0.1 - 0.5).collect();
292 let code = lista.encode(&y).expect("ok");
293 assert!(
294 code.iter().all(|v| v.is_finite()),
295 "code has non-finite value"
296 );
297 }
298
299 #[test]
301 fn reconstruct_shape() {
302 let n = 8;
303 let m = 12;
304 let dict = make_dict(m, n);
305 let code = vec![0.1_f64; m];
306 let y_hat = Lista::reconstruct(&dict, &code, n, m).expect("ok");
307 assert_eq!(y_hat.len(), n);
308 }
309
310 #[test]
312 fn encode_batch_shape() {
313 let n = 8;
314 let m = 12;
315 let n_batch = 5;
316 let dict = make_dict(m, n);
317 let cfg = make_config(n, m, 0.1, 10);
318 let lista = Lista::from_dict(&dict, cfg).expect("ok");
319 let ys = vec![0.4_f64; n_batch * n];
320 let codes = lista.encode_batch(&ys, n_batch).expect("ok");
321 assert_eq!(codes.len(), n_batch * m);
322 }
323
324 #[test]
326 fn larger_lambda_sparser() {
327 let n = 8;
328 let m = 12;
329 let dict = make_dict(m, n);
330 let y = vec![0.5_f64; n];
331
332 let cfg_small = make_config(n, m, 0.05, 10);
333 let lista_small = Lista::from_dict(&dict, cfg_small).expect("ok");
334 let code_small = lista_small.encode(&y).expect("ok");
335
336 let cfg_large = make_config(n, m, 0.5, 10);
337 let lista_large = Lista::from_dict(&dict, cfg_large).expect("ok");
338 let code_large = lista_large.encode(&y).expect("ok");
339
340 let nnz_small = code_small.iter().filter(|&&v| v.abs() > 1e-10).count();
341 let nnz_large = code_large.iter().filter(|&&v| v.abs() > 1e-10).count();
342 assert!(
343 nnz_large <= nnz_small,
344 "larger lambda should produce sparser code: nnz_small={nnz_small}, nnz_large={nnz_large}"
345 );
346 }
347
348 #[test]
350 fn from_dict_dim_mismatch() {
351 let n = 8;
352 let m = 12;
353 let bad_dict = vec![0.0_f64; m * n + 1];
354 let cfg = make_config(n, m, 0.1, 10);
355 let result = Lista::from_dict(&bad_dict, cfg);
356 assert!(result.is_err(), "expected Err for dim mismatch");
357 }
358
359 #[test]
361 fn zero_layers_returns_zeros() {
362 let n = 8;
363 let m = 12;
364 let dict = make_dict(m, n);
365 let cfg = make_config(n, m, 0.1, 0);
366 let lista = Lista::from_dict(&dict, cfg).expect("ok");
367 let y = vec![1.0_f64; n];
368 let code = lista.encode(&y).expect("ok");
369 assert!(
370 code.iter().all(|&v| v == 0.0),
371 "zero layers must return all zeros"
372 );
373 }
374}