Skip to main content

oxicuda_cs/greedy/
lista.rs

1//! LISTA — Learned ISTA (Gregor-LeCun 2010).
2//!
3//! "Learning Fast Approximations of Sparse Coding."
4//!
5//! T-layer unrolled ISTA with shared learned weight matrices W and S:
6//!
7//! ```text
8//! x_0 = 0
9//! for t in 0..T:
10//!   x_{t+1} = h_λ( W * y + S * x_t )
11//! where:
12//!   h_λ(v) = sign(v) * max(|v| - λ, 0)   [soft threshold]
13//!   W ≈ (1/L) * D^T                       [learned encoder weight, m × n]
14//!   S ≈ I - (1/L) * D * D^T               [learned recurrent weight, m × m]
15//!   L = ||D||_F^2
16//! ```
17
18use crate::error::{CsError, CsResult};
19
20/// Configuration for LISTA.
21#[derive(Debug, Clone)]
22pub struct ListaConfig {
23    /// Signal / measurement dimension (n).
24    pub n_measurements: usize,
25    /// Dictionary atom / code dimension (m).
26    pub n_atoms: usize,
27    /// Soft-threshold value (λ).
28    pub threshold: f64,
29    /// Number of unrolled ISTA layers (T).
30    pub n_layers: usize,
31}
32
33/// LISTA encoder: T-layer unrolled ISTA with fixed learned matrices derived from a dictionary.
34#[derive(Debug, Clone)]
35pub struct Lista {
36    /// Encoder weight W: [m × n] row-major.
37    w: Vec<f64>,
38    /// Recurrent weight S: [m × m] row-major.
39    s: Vec<f64>,
40    config: ListaConfig,
41}
42
43impl Lista {
44    /// Build a LISTA encoder from dictionary `dict` of shape [m × n] (row-major).
45    ///
46    /// Computes:
47    /// - `L = ||dict||_F^2`
48    /// - `W = (1/L) * dict`  (same shape [m × n], since W encodes y∈ℝⁿ → code∈ℝᵐ)
49    /// - `S = I_m - (1/L) * dict * dict^T`  shape [m × m]
50    ///
51    /// `dict` must have length `m * n`.
52    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        // L = ||dict||_F^2
68        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        // W = (1/L) * dict,  shape [m × n].
72        let w: Vec<f64> = dict.iter().map(|&v| inv_l * v).collect();
73
74        // S = I_m - (1/L) * dict * dict^T,  shape [m × m].
75        // (dict * dict^T)[i, k] = Σ_j dict[i*n+j] * dict[k*n+j]
76        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    /// Encode a single measurement vector `y` of length `n_measurements`.
92    ///
93    /// Runs T unrolled ISTA steps and returns a sparse code of length `n_atoms`.
94    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        // x_0 = 0
104        let mut x = vec![0.0_f64; m];
105
106        // Precompute W * y (constant across all layers).
107        let wy = matvec(&self.w, y, m, n);
108
109        for _ in 0..t {
110            // u = W*y + S*x_t
111            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    /// Encode a batch of `n_batch` measurement vectors stored row-major in `ys`
120    /// (length `n_batch * n_measurements`).
121    ///
122    /// Returns a flat Vec of length `n_batch * n_atoms` (row-major).
123    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    /// Element-wise soft-threshold operator: `sign(v) * max(|v| - λ, 0)`.
142    #[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    /// Reconstruct an approximate signal from code: computes `D * code`.
155    ///
156    /// `dict` is [m × n] row-major; `code` has length `m`; output has length `n`.
157    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        // y_hat[j] = Σ_i dict[i*n + j] * code[i]
171        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    /// Return the number of dictionary atoms (m).
181    #[must_use]
182    pub fn n_atoms(&self) -> usize {
183        self.config.n_atoms
184    }
185
186    /// Return the signal / measurement dimension (n).
187    #[must_use]
188    pub fn n_measurements(&self) -> usize {
189        self.config.n_measurements
190    }
191
192    /// Return the soft-threshold value (λ).
193    #[must_use]
194    pub fn threshold(&self) -> f64 {
195        self.config.threshold
196    }
197}
198
199/// Matrix-vector product `A * x` where `A` is row-major `[out_dim × in_dim]`.
200fn 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 1
236    #[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 2
249    #[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 3
263    #[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 4
270    #[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 5
277    #[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 6
284    #[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 7
300    #[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 8
311    #[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 9
325    #[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 10
349    #[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 11
360    #[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}