use scirs2_core::ndarray::{Array1, Array2};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use sklears_core::{error::Result, types::Float};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OMPConfig {
pub n_nonzero_coefs: Option<usize>,
pub tol: Option<Float>,
}
impl Default for OMPConfig {
fn default() -> Self {
Self {
n_nonzero_coefs: None,
tol: Some(1e-4),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OMPResult {
pub coefficients: Array1<Float>,
pub residual_norm: Float,
pub n_iter: usize,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OMPEncoder {
config: OMPConfig,
}
impl OMPEncoder {
pub fn new(config: OMPConfig) -> Self {
Self { config }
}
pub fn encode(&self, dictionary: &Array2<Float>, signal: &Array1<Float>) -> Result<OMPResult> {
use sklears_core::error::SklearsError;
let (n_features, n_atoms) = dictionary.dim();
if signal.len() != n_features {
return Err(SklearsError::InvalidInput(format!(
"Signal length {} doesn't match dictionary features {}",
signal.len(),
n_features
)));
}
let mut dict_normalized = dictionary.clone();
for j in 0..n_atoms {
let col = dictionary.column(j);
let norm = col.mapv(|x| x * x).sum().sqrt();
if norm > 1e-10 {
for i in 0..n_features {
dict_normalized[[i, j]] /= norm;
}
}
}
let mut coefficients = Array1::zeros(n_atoms);
let mut residual = signal.clone();
let mut selected_atoms: Vec<usize> = Vec::new();
let mut n_iter = 0;
let max_iter = if let Some(k) = self.config.n_nonzero_coefs {
k.min(n_atoms)
} else {
n_atoms
};
let tol = self.config.tol.unwrap_or(1e-4);
for iteration in 0..max_iter {
let residual_norm = residual.mapv(|x| x * x).sum().sqrt();
if residual_norm < tol {
n_iter = iteration;
break;
}
let mut max_corr = 0.0;
let mut best_atom = 0;
for j in 0..n_atoms {
if selected_atoms.contains(&j) {
continue;
}
let mut corr = 0.0;
for i in 0..n_features {
corr += dict_normalized[[i, j]] * residual[i];
}
let abs_corr = corr.abs();
if abs_corr > max_corr {
max_corr = abs_corr;
best_atom = j;
}
}
selected_atoms.push(best_atom);
let k = selected_atoms.len();
let mut d_sel = Array2::zeros((n_features, k));
for (col_idx, &atom_idx) in selected_atoms.iter().enumerate() {
for row_idx in 0..n_features {
d_sel[[row_idx, col_idx]] = dictionary[[row_idx, atom_idx]];
}
}
let mut gram = Array2::zeros((k, k));
for i in 0..k {
for j in 0..k {
let mut sum = 0.0;
for row in 0..n_features {
sum += d_sel[[row, i]] * d_sel[[row, j]];
}
gram[[i, j]] = sum;
}
}
let mut rhs = Array1::zeros(k);
for i in 0..k {
let mut sum = 0.0;
for row in 0..n_features {
sum += d_sel[[row, i]] * signal[row];
}
rhs[i] = sum;
}
let selected_coeffs = solve_linear_system(&gram, &rhs)?;
coefficients.fill(0.0);
for (i, &atom_idx) in selected_atoms.iter().enumerate() {
coefficients[atom_idx] = selected_coeffs[i];
}
residual = signal.clone();
for (i, &atom_idx) in selected_atoms.iter().enumerate() {
for row in 0..n_features {
residual[row] -= dictionary[[row, atom_idx]] * selected_coeffs[i];
}
}
n_iter = iteration + 1;
if let Some(k_max) = self.config.n_nonzero_coefs {
if selected_atoms.len() >= k_max {
break;
}
}
}
let final_residual_norm = residual.mapv(|x| x * x).sum().sqrt();
Ok(OMPResult {
coefficients,
residual_norm: final_residual_norm,
n_iter,
})
}
}
fn solve_linear_system(a: &Array2<Float>, b: &Array1<Float>) -> Result<Array1<Float>> {
use sklears_core::error::SklearsError;
let n = a.nrows();
if a.ncols() != n || b.len() != n {
return Err(SklearsError::InvalidInput(
"Matrix must be square and match RHS dimension".to_string(),
));
}
let mut aug = Array2::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n]] = b[i];
}
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[[col, col]].abs();
for row in (col + 1)..n {
let val = aug[[row, col]].abs();
if val > max_val {
max_val = val;
max_row = row;
}
}
if max_val < 1e-10 {
return Err(SklearsError::NumericalError(
"Singular matrix in OMP least squares".to_string(),
));
}
if max_row != col {
for j in 0..=n {
let temp = aug[[col, j]];
aug[[col, j]] = aug[[max_row, j]];
aug[[max_row, j]] = temp;
}
}
for row in (col + 1)..n {
let factor = aug[[row, col]] / aug[[col, col]];
for j in col..=n {
aug[[row, j]] -= factor * aug[[col, j]];
}
}
}
let mut x = Array1::zeros(n);
for i in (0..n).rev() {
let mut sum = aug[[i, n]];
for j in (i + 1)..n {
sum -= aug[[i, j]] * x[j];
}
x[i] = sum / aug[[i, i]];
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_omp_simple_sparse_signal() {
let dictionary = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 1.0])
.expect("shape and data length should match");
let signal = Array1::from_vec(vec![3.0, 4.0]);
let config = OMPConfig {
n_nonzero_coefs: Some(2),
tol: Some(1e-6),
};
let encoder = OMPEncoder::new(config);
let result = encoder
.encode(&dictionary, &signal)
.expect("serialization should succeed");
assert!((result.coefficients[0] - 3.0).abs() < 1e-6);
assert!((result.coefficients[1] - 4.0).abs() < 1e-6);
assert!(result.residual_norm < 1e-6);
}
#[test]
fn test_omp_sparse_representation() {
let dictionary = Array2::from_shape_vec(
(3, 5),
vec![
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, ],
)
.expect("operation should succeed");
let signal = Array1::from_vec(vec![
2.0 * 1.0 + 3.0 * 0.2, 2.0 * 0.0 + 3.0 * 0.3, 2.0 * 0.0 + 3.0 * 1.0, ]);
let config = OMPConfig {
n_nonzero_coefs: Some(2),
tol: Some(1e-4),
};
let encoder = OMPEncoder::new(config);
let result = encoder
.encode(&dictionary, &signal)
.expect("serialization should succeed");
assert!(result.coefficients[0].abs() > 1.0); assert!(result.coefficients[2].abs() > 1.0); assert!(result.residual_norm < 0.1); }
#[test]
fn test_omp_tolerance_stopping() {
let dictionary =
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])
.expect("operation should succeed");
let signal = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let config = OMPConfig {
n_nonzero_coefs: None, tol: Some(1e-6), };
let encoder = OMPEncoder::new(config);
let result = encoder
.encode(&dictionary, &signal)
.expect("serialization should succeed");
assert!(result.residual_norm < 1e-5);
assert!(result.n_iter <= 3); }
#[test]
fn test_omp_max_nonzero_coefs() {
let dictionary = Array2::from_shape_vec(
(3, 5),
vec![
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,
],
)
.expect("operation should succeed");
let signal = Array1::from_vec(vec![2.5, 1.5, 2.0]);
let config = OMPConfig {
n_nonzero_coefs: Some(2), tol: None,
};
let encoder = OMPEncoder::new(config);
let result = encoder
.encode(&dictionary, &signal)
.expect("serialization should succeed");
let nnz = result
.coefficients
.iter()
.filter(|&&x| x.abs() > 1e-10)
.count();
assert_eq!(nnz, 2);
}
#[test]
fn test_omp_exact_representation() {
let dictionary = Array2::from_shape_vec(
(4, 3),
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],
)
.expect("operation should succeed");
let signal = Array1::from_vec(vec![2.0, 3.0, 0.0, 0.0]);
let config = OMPConfig {
n_nonzero_coefs: Some(3),
tol: Some(1e-6),
};
let encoder = OMPEncoder::new(config);
let result = encoder
.encode(&dictionary, &signal)
.expect("serialization should succeed");
let mut reconstructed = Array1::zeros(4);
for (atom_idx, &coef) in result.coefficients.iter().enumerate() {
for row in 0..4 {
reconstructed[row] += coef * dictionary[[row, atom_idx]];
}
}
let recon_error: Float = signal
.iter()
.zip(reconstructed.iter())
.map(|(a, b): (&Float, &Float)| (a - b).powi(2))
.sum::<Float>()
.sqrt();
assert!(recon_error < 1e-3);
assert!(result.residual_norm < 1e-3);
}
#[test]
fn test_omp_dimension_validation() {
let dictionary = Array2::from_shape_vec((3, 2), vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
.expect("shape and data length should match");
let signal = Array1::from_vec(vec![1.0, 2.0]);
let config = OMPConfig::default();
let encoder = OMPEncoder::new(config);
let result = encoder.encode(&dictionary, &signal);
assert!(result.is_err());
}
#[test]
fn test_solve_linear_system() {
let a = Array2::from_shape_vec((2, 2), vec![2.0, 1.0, 1.0, 3.0])
.expect("shape and data length should match");
let b = Array1::from_vec(vec![5.0, 6.0]);
let x = solve_linear_system(&a, &b).expect("operation should succeed");
assert!((x[0] - 9.0 / 5.0).abs() < 1e-6);
assert!((x[1] - 7.0 / 5.0).abs() < 1e-6);
}
#[test]
fn test_solve_linear_system_identity() {
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])
.expect("operation should succeed");
let b = Array1::from_vec(vec![4.0, 5.0, 6.0]);
let x = solve_linear_system(&a, &b).expect("operation should succeed");
assert!((x[0] - 4.0).abs() < 1e-10);
assert!((x[1] - 5.0).abs() < 1e-10);
assert!((x[2] - 6.0).abs() < 1e-10);
}
#[test]
fn test_solve_linear_system_with_pivoting() {
let a = Array2::from_shape_vec((2, 2), vec![0.0, 1.0, 1.0, 1.0])
.expect("shape and data length should match");
let b = Array1::from_vec(vec![2.0, 3.0]);
let x = solve_linear_system(&a, &b).expect("operation should succeed");
assert!((x[0] - 1.0).abs() < 1e-6);
assert!((x[1] - 2.0).abs() < 1e-6);
}
}