1use scirs2_core::ndarray::{Array1, Array2};
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6use sklears_core::{error::Result, types::Float};
7
8#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct OMPConfig {
12 pub n_nonzero_coefs: Option<usize>,
14 pub tol: Option<Float>,
16}
17
18impl Default for OMPConfig {
19 fn default() -> Self {
20 Self {
21 n_nonzero_coefs: None,
22 tol: Some(1e-4),
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub struct OMPResult {
31 pub coefficients: Array1<Float>,
33 pub residual_norm: Float,
35 pub n_iter: usize,
37}
38
39#[derive(Debug, Clone)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42pub struct OMPEncoder {
43 config: OMPConfig,
44}
45
46impl OMPEncoder {
47 pub fn new(config: OMPConfig) -> Self {
48 Self { config }
49 }
50
51 pub fn encode(&self, dictionary: &Array2<Float>, signal: &Array1<Float>) -> Result<OMPResult> {
64 use sklears_core::error::SklearsError;
65
66 let (n_features, n_atoms) = dictionary.dim();
67
68 if signal.len() != n_features {
70 return Err(SklearsError::InvalidInput(format!(
71 "Signal length {} doesn't match dictionary features {}",
72 signal.len(),
73 n_features
74 )));
75 }
76
77 let mut dict_normalized = dictionary.clone();
79 for j in 0..n_atoms {
80 let col = dictionary.column(j);
81 let norm = col.mapv(|x| x * x).sum().sqrt();
82 if norm > 1e-10 {
83 for i in 0..n_features {
84 dict_normalized[[i, j]] /= norm;
85 }
86 }
87 }
88
89 let mut coefficients = Array1::zeros(n_atoms);
90 let mut residual = signal.clone();
91 let mut selected_atoms: Vec<usize> = Vec::new();
92 let mut n_iter = 0;
93
94 let max_iter = if let Some(k) = self.config.n_nonzero_coefs {
96 k.min(n_atoms)
97 } else {
98 n_atoms
99 };
100
101 let tol = self.config.tol.unwrap_or(1e-4);
102
103 for iteration in 0..max_iter {
105 let residual_norm = residual.mapv(|x| x * x).sum().sqrt();
107
108 if residual_norm < tol {
110 n_iter = iteration;
111 break;
112 }
113
114 let mut max_corr = 0.0;
116 let mut best_atom = 0;
117
118 for j in 0..n_atoms {
119 if selected_atoms.contains(&j) {
121 continue;
122 }
123
124 let mut corr = 0.0;
126 for i in 0..n_features {
127 corr += dict_normalized[[i, j]] * residual[i];
128 }
129
130 let abs_corr = corr.abs();
131 if abs_corr > max_corr {
132 max_corr = abs_corr;
133 best_atom = j;
134 }
135 }
136
137 selected_atoms.push(best_atom);
139
140 let k = selected_atoms.len();
144
145 let mut d_sel = Array2::zeros((n_features, k));
147 for (col_idx, &atom_idx) in selected_atoms.iter().enumerate() {
148 for row_idx in 0..n_features {
149 d_sel[[row_idx, col_idx]] = dictionary[[row_idx, atom_idx]];
150 }
151 }
152
153 let mut gram = Array2::zeros((k, k));
155 for i in 0..k {
156 for j in 0..k {
157 let mut sum = 0.0;
158 for row in 0..n_features {
159 sum += d_sel[[row, i]] * d_sel[[row, j]];
160 }
161 gram[[i, j]] = sum;
162 }
163 }
164
165 let mut rhs = Array1::zeros(k);
167 for i in 0..k {
168 let mut sum = 0.0;
169 for row in 0..n_features {
170 sum += d_sel[[row, i]] * signal[row];
171 }
172 rhs[i] = sum;
173 }
174
175 let selected_coeffs = solve_linear_system(&gram, &rhs)?;
178
179 coefficients.fill(0.0);
181 for (i, &atom_idx) in selected_atoms.iter().enumerate() {
182 coefficients[atom_idx] = selected_coeffs[i];
183 }
184
185 residual = signal.clone();
187 for (i, &atom_idx) in selected_atoms.iter().enumerate() {
188 for row in 0..n_features {
189 residual[row] -= dictionary[[row, atom_idx]] * selected_coeffs[i];
190 }
191 }
192
193 n_iter = iteration + 1;
194
195 if let Some(k_max) = self.config.n_nonzero_coefs {
197 if selected_atoms.len() >= k_max {
198 break;
199 }
200 }
201 }
202
203 let final_residual_norm = residual.mapv(|x| x * x).sum().sqrt();
204
205 Ok(OMPResult {
206 coefficients,
207 residual_norm: final_residual_norm,
208 n_iter,
209 })
210 }
211}
212
213fn solve_linear_system(a: &Array2<Float>, b: &Array1<Float>) -> Result<Array1<Float>> {
215 use sklears_core::error::SklearsError;
216
217 let n = a.nrows();
218 if a.ncols() != n || b.len() != n {
219 return Err(SklearsError::InvalidInput(
220 "Matrix must be square and match RHS dimension".to_string(),
221 ));
222 }
223
224 let mut aug = Array2::zeros((n, n + 1));
226 for i in 0..n {
227 for j in 0..n {
228 aug[[i, j]] = a[[i, j]];
229 }
230 aug[[i, n]] = b[i];
231 }
232
233 for col in 0..n {
235 let mut max_row = col;
237 let mut max_val = aug[[col, col]].abs();
238 for row in (col + 1)..n {
239 let val = aug[[row, col]].abs();
240 if val > max_val {
241 max_val = val;
242 max_row = row;
243 }
244 }
245
246 if max_val < 1e-10 {
248 return Err(SklearsError::NumericalError(
249 "Singular matrix in OMP least squares".to_string(),
250 ));
251 }
252
253 if max_row != col {
255 for j in 0..=n {
256 let temp = aug[[col, j]];
257 aug[[col, j]] = aug[[max_row, j]];
258 aug[[max_row, j]] = temp;
259 }
260 }
261
262 for row in (col + 1)..n {
264 let factor = aug[[row, col]] / aug[[col, col]];
265 for j in col..=n {
266 aug[[row, j]] -= factor * aug[[col, j]];
267 }
268 }
269 }
270
271 let mut x = Array1::zeros(n);
273 for i in (0..n).rev() {
274 let mut sum = aug[[i, n]];
275 for j in (i + 1)..n {
276 sum -= aug[[i, j]] * x[j];
277 }
278 x[i] = sum / aug[[i, i]];
279 }
280
281 Ok(x)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use scirs2_core::ndarray::Array2;
288
289 #[test]
290 fn test_omp_simple_sparse_signal() {
291 let dictionary = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 1.0])
296 .expect("shape and data length should match");
297 let signal = Array1::from_vec(vec![3.0, 4.0]);
298
299 let config = OMPConfig {
300 n_nonzero_coefs: Some(2),
301 tol: Some(1e-6),
302 };
303
304 let encoder = OMPEncoder::new(config);
305 let result = encoder
306 .encode(&dictionary, &signal)
307 .expect("serialization should succeed");
308
309 assert!((result.coefficients[0] - 3.0).abs() < 1e-6);
311 assert!((result.coefficients[1] - 4.0).abs() < 1e-6);
312 assert!(result.residual_norm < 1e-6);
313 }
314
315 #[test]
316 fn test_omp_sparse_representation() {
317 let dictionary = Array2::from_shape_vec(
320 (3, 5),
321 vec![
322 1.0, 0.5, 0.2, 0.8, 0.3, 0.0, 1.0, 0.3, 0.1, 0.7, 0.0, 0.0, 1.0, 0.2, 0.4, ],
327 )
328 .expect("operation should succeed");
329
330 let signal = Array1::from_vec(vec![
332 2.0 * 1.0 + 3.0 * 0.2, 2.0 * 0.0 + 3.0 * 0.3, 2.0 * 0.0 + 3.0 * 1.0, ]);
336
337 let config = OMPConfig {
338 n_nonzero_coefs: Some(2),
339 tol: Some(1e-4),
340 };
341
342 let encoder = OMPEncoder::new(config);
343 let result = encoder
344 .encode(&dictionary, &signal)
345 .expect("serialization should succeed");
346
347 assert!(result.coefficients[0].abs() > 1.0); assert!(result.coefficients[2].abs() > 1.0); assert!(result.residual_norm < 0.1); }
352
353 #[test]
354 fn test_omp_tolerance_stopping() {
355 let dictionary =
357 Array2::from_shape_vec((3, 3), vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
358 .expect("operation should succeed");
359 let signal = Array1::from_vec(vec![1.0, 2.0, 3.0]);
360
361 let config = OMPConfig {
362 n_nonzero_coefs: None, tol: Some(1e-6), };
365
366 let encoder = OMPEncoder::new(config);
367 let result = encoder
368 .encode(&dictionary, &signal)
369 .expect("serialization should succeed");
370
371 assert!(result.residual_norm < 1e-5);
373 assert!(result.n_iter <= 3); }
375
376 #[test]
377 fn test_omp_max_nonzero_coefs() {
378 let dictionary = Array2::from_shape_vec(
380 (3, 5),
381 vec![
382 1.0, 0.5, 0.2, 0.8, 0.3, 0.0, 1.0, 0.3, 0.1, 0.7, 0.0, 0.0, 1.0, 0.2, 0.4,
383 ],
384 )
385 .expect("operation should succeed");
386 let signal = Array1::from_vec(vec![2.5, 1.5, 2.0]);
387
388 let config = OMPConfig {
389 n_nonzero_coefs: Some(2), tol: None,
391 };
392
393 let encoder = OMPEncoder::new(config);
394 let result = encoder
395 .encode(&dictionary, &signal)
396 .expect("serialization should succeed");
397
398 let nnz = result
400 .coefficients
401 .iter()
402 .filter(|&&x| x.abs() > 1e-10)
403 .count();
404 assert_eq!(nnz, 2);
405 }
406
407 #[test]
408 fn test_omp_exact_representation() {
409 let dictionary = Array2::from_shape_vec(
411 (4, 3),
412 vec![1.0, 0.0, 0.5, 0.0, 1.0, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
413 )
414 .expect("operation should succeed");
415
416 let signal = Array1::from_vec(vec![2.0, 3.0, 0.0, 0.0]);
418
419 let config = OMPConfig {
420 n_nonzero_coefs: Some(3),
421 tol: Some(1e-6),
422 };
423
424 let encoder = OMPEncoder::new(config);
425 let result = encoder
426 .encode(&dictionary, &signal)
427 .expect("serialization should succeed");
428
429 let mut reconstructed = Array1::zeros(4);
431 for (atom_idx, &coef) in result.coefficients.iter().enumerate() {
432 for row in 0..4 {
433 reconstructed[row] += coef * dictionary[[row, atom_idx]];
434 }
435 }
436
437 let recon_error: Float = signal
439 .iter()
440 .zip(reconstructed.iter())
441 .map(|(a, b): (&Float, &Float)| (a - b).powi(2))
442 .sum::<Float>()
443 .sqrt();
444 assert!(recon_error < 1e-3);
445 assert!(result.residual_norm < 1e-3);
446 }
447
448 #[test]
449 fn test_omp_dimension_validation() {
450 let dictionary = Array2::from_shape_vec((3, 2), vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
452 .expect("shape and data length should match");
453 let signal = Array1::from_vec(vec![1.0, 2.0]); let config = OMPConfig::default();
456 let encoder = OMPEncoder::new(config);
457 let result = encoder.encode(&dictionary, &signal);
458
459 assert!(result.is_err());
460 }
461
462 #[test]
463 fn test_solve_linear_system() {
464 let a = Array2::from_shape_vec((2, 2), vec![2.0, 1.0, 1.0, 3.0])
469 .expect("shape and data length should match");
470 let b = Array1::from_vec(vec![5.0, 6.0]);
471
472 let x = solve_linear_system(&a, &b).expect("operation should succeed");
473
474 assert!((x[0] - 9.0 / 5.0).abs() < 1e-6);
475 assert!((x[1] - 7.0 / 5.0).abs() < 1e-6);
476 }
477
478 #[test]
479 fn test_solve_linear_system_identity() {
480 let a = Array2::from_shape_vec((3, 3), vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
482 .expect("operation should succeed");
483 let b = Array1::from_vec(vec![4.0, 5.0, 6.0]);
484
485 let x = solve_linear_system(&a, &b).expect("operation should succeed");
486
487 assert!((x[0] - 4.0).abs() < 1e-10);
488 assert!((x[1] - 5.0).abs() < 1e-10);
489 assert!((x[2] - 6.0).abs() < 1e-10);
490 }
491
492 #[test]
493 fn test_solve_linear_system_with_pivoting() {
494 let a = Array2::from_shape_vec((2, 2), vec![0.0, 1.0, 1.0, 1.0])
499 .expect("shape and data length should match");
500 let b = Array1::from_vec(vec![2.0, 3.0]);
501
502 let x = solve_linear_system(&a, &b).expect("operation should succeed");
503
504 assert!((x[0] - 1.0).abs() < 1e-6);
505 assert!((x[1] - 2.0).abs() < 1e-6);
506 }
507}