use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, Axis};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::thread_rng;
use scirs2_core::random::RngExt;
use scirs2_core::random::SeedableRng;
use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Fit, Transform},
types::Float,
};
#[derive(Debug, Clone)]
pub struct ManifoldCompressedSensing {
n_measurements: usize,
n_components: usize,
sparsity_level: usize,
manifold_reg: Float,
sparsity_reg: Float,
max_iter: usize,
tol: Float,
random_state: Option<u64>,
}
impl ManifoldCompressedSensing {
pub fn new(n_measurements: usize, n_components: usize) -> Self {
Self {
n_measurements,
n_components,
sparsity_level: 10,
manifold_reg: 1e-1,
sparsity_reg: 1e-3,
max_iter: 100,
tol: 1e-6,
random_state: None,
}
}
pub fn sparsity_level(mut self, level: usize) -> Self {
self.sparsity_level = level;
self
}
pub fn manifold_reg(mut self, reg: Float) -> Self {
self.manifold_reg = reg;
self
}
pub fn sparsity_reg(mut self, reg: Float) -> Self {
self.sparsity_reg = reg;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: Float) -> Self {
self.tol = tol;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
#[derive(Debug, Clone)]
pub struct FittedManifoldCompressedSensing {
n_measurements: usize,
#[allow(dead_code)] n_components: usize,
measurement_matrix: Array2<Float>,
manifold_basis: Array2<Float>,
sparse_dictionary: Array2<Float>,
#[allow(dead_code)]
reconstruction_weights: Array2<Float>,
#[allow(dead_code)] sparsity_level: usize,
manifold_reg: Float,
sparsity_reg: Float,
tol: Float,
}
impl Fit<Array2<Float>, ()> for ManifoldCompressedSensing {
type Fitted = FittedManifoldCompressedSensing;
fn fit(self, data: &Array2<Float>, _y: &()) -> SklResult<Self::Fitted> {
let x = data;
let n_samples = x.nrows();
let n_features = x.ncols();
if self.n_measurements >= n_features {
return Err(SklearsError::InvalidInput(format!(
"n_measurements ({}) must be less than n_features ({})",
self.n_measurements, n_features
)));
}
if self.n_components >= n_samples {
return Err(SklearsError::InvalidInput(format!(
"n_components ({}) must be less than n_samples ({})",
self.n_components, n_samples
)));
}
let mut rng = if let Some(seed) = self.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::seed_from_u64(thread_rng().random())
};
let (manifold_basis, _) = self.learn_manifold_structure(x)?;
let measurement_matrix = self.generate_measurement_matrix(&mut rng, n_features)?;
let sparse_dictionary = self.learn_sparse_dictionary(x, &manifold_basis, &mut rng)?;
let reconstruction_weights =
self.compute_reconstruction_weights(x, &manifold_basis, &sparse_dictionary)?;
Ok(FittedManifoldCompressedSensing {
n_measurements: self.n_measurements,
n_components: self.n_components,
measurement_matrix,
manifold_basis,
sparse_dictionary,
reconstruction_weights,
sparsity_level: self.sparsity_level,
manifold_reg: self.manifold_reg,
sparsity_reg: self.sparsity_reg,
tol: self.tol,
})
}
}
impl Transform<Array2<Float>, Array2<Float>> for FittedManifoldCompressedSensing {
fn transform(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let x = data;
if x.ncols() != self.measurement_matrix.ncols() {
return Err(SklearsError::InvalidInput(format!(
"Input has {} features, expected {}",
x.ncols(),
self.measurement_matrix.ncols()
)));
}
let measurements = x.dot(&self.measurement_matrix.t());
Ok(measurements)
}
}
impl FittedManifoldCompressedSensing {
pub fn reconstruct(&self, measurements: &Array2<Float>) -> SklResult<Array2<Float>> {
if measurements.ncols() != self.n_measurements {
return Err(SklearsError::InvalidInput(format!(
"Measurements have {} dimensions, expected {}",
measurements.ncols(),
self.n_measurements
)));
}
let n_samples = measurements.nrows();
let n_features = self.measurement_matrix.ncols();
let mut reconstructed = Array2::zeros((n_samples, n_features));
for i in 0..n_samples {
let measurement = measurements.row(i);
let reconstruction = self.reconstruct_single_sample(&measurement)?;
reconstructed.row_mut(i).assign(&reconstruction);
}
Ok(reconstructed)
}
fn reconstruct_single_sample(
&self,
measurement: &ArrayView1<Float>,
) -> SklResult<Array1<Float>> {
let _n_features = self.measurement_matrix.ncols();
let mut reconstruction = self.pseudoinverse_reconstruction(measurement)?;
let initial_norm = reconstruction.mapv(|x| x * x).sum().sqrt();
if initial_norm > 1000.0 {
reconstruction = Array1::zeros(reconstruction.len());
}
for _iter in 0..100 {
let old_reconstruction = reconstruction.clone();
let manifold_projection = self.project_onto_manifold(&reconstruction)?;
let sparse_representation = self.enforce_sparsity(&manifold_projection)?;
reconstruction = self.sparse_dictionary.dot(&sparse_representation);
let recon_norm = reconstruction.mapv(|x| x * x).sum().sqrt();
if recon_norm > 1000.0 {
break;
}
let predicted_measurement = self.measurement_matrix.dot(&reconstruction);
let measurement_error = measurement - &predicted_measurement;
let correction = self.measurement_matrix.t().dot(&measurement_error);
let correction_norm = correction.mapv(|x| x * x).sum().sqrt();
let damping_factor = if correction_norm > 1.0 { 0.01 } else { 0.1 };
reconstruction += &(damping_factor * correction);
let final_norm = reconstruction.mapv(|x| x * x).sum().sqrt();
if final_norm > 1000.0 {
break;
}
let change = (&reconstruction - &old_reconstruction)
.mapv(|x| x.abs())
.sum();
if change < self.tol {
break;
}
}
Ok(reconstruction)
}
fn project_onto_manifold(&self, signal: &Array1<Float>) -> SklResult<Array1<Float>> {
let manifold_coords = self.manifold_basis.t().dot(signal);
let projection = self.manifold_basis.dot(&manifold_coords);
Ok(projection)
}
fn enforce_sparsity(&self, signal: &Array1<Float>) -> SklResult<Array1<Float>> {
let coefficients = self.sparse_dictionary.t().dot(signal);
let threshold = self.sparsity_reg;
let sparse_coefficients = coefficients.mapv(|x| {
if x.abs() > threshold {
x - threshold * x.signum()
} else {
0.0
}
});
Ok(sparse_coefficients)
}
fn pseudoinverse_reconstruction(
&self,
measurement: &ArrayView1<Float>,
) -> SklResult<Array1<Float>> {
let (u, s, vt) = self
.measurement_matrix
.svd(true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
let s_reg_inv = s.mapv(|x| {
if x > 1e-10 {
x / (x * x + self.manifold_reg)
} else {
0.0
}
});
let rank = s.len();
let u_truncated = u.slice(s![.., ..rank]).to_owned();
let vt_truncated = vt.slice(s![..rank, ..]).to_owned();
let pseudoinverse = vt_truncated
.t()
.dot(&Array2::from_diag(&s_reg_inv))
.dot(&u_truncated.t());
let reconstruction = pseudoinverse.dot(measurement);
Ok(reconstruction)
}
pub fn measurement_matrix(&self) -> &Array2<Float> {
&self.measurement_matrix
}
pub fn manifold_basis(&self) -> &Array2<Float> {
&self.manifold_basis
}
pub fn sparse_dictionary(&self) -> &Array2<Float> {
&self.sparse_dictionary
}
}
impl ManifoldCompressedSensing {
fn learn_manifold_structure(
&self,
data: &Array2<Float>,
) -> SklResult<(Array2<Float>, Array1<Float>)> {
let n_samples = data.nrows();
let mean = data.mean_axis(Axis(0)).expect("operation should succeed");
let centered_data = data - &mean.insert_axis(Axis(0));
let covariance = centered_data.t().dot(¢ered_data) / (n_samples as Float - 1.0);
let symmetric_cov = (&covariance + &covariance.t()) / 2.0;
let (eigenvalues, eigenvectors) = symmetric_cov.eigh(UPLO::Upper).map_err(|e| {
SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
})?;
let mut eigen_pairs: Vec<(Float, Array1<Float>)> = eigenvalues
.iter()
.zip(eigenvectors.axis_iter(Axis(1)))
.map(|(&val, vec)| (val, vec.to_owned()))
.collect();
eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).expect("operation should succeed"));
let selected_eigenvalues: Array1<Float> = eigen_pairs
.iter()
.take(self.n_components)
.map(|(val, _)| *val)
.collect();
let manifold_basis = Array2::from_shape_fn((data.ncols(), self.n_components), |(i, j)| {
eigen_pairs[j].1[i]
});
Ok((manifold_basis, selected_eigenvalues))
}
fn generate_measurement_matrix(
&self,
rng: &mut StdRng,
n_features: usize,
) -> SklResult<Array2<Float>> {
let mut measurement_matrix = Array2::<Float>::zeros((self.n_measurements, n_features));
for elem in measurement_matrix.iter_mut() {
*elem = rng.sample::<Float, _>(scirs2_core::StandardNormal);
}
for i in 0..self.n_measurements {
let row_norm = measurement_matrix.row(i).mapv(|x| x * x).sum().sqrt();
if row_norm > 1e-10 {
measurement_matrix.row_mut(i).mapv_inplace(|x| x / row_norm);
}
}
Ok(measurement_matrix)
}
fn learn_sparse_dictionary(
&self,
data: &Array2<Float>,
manifold_basis: &Array2<Float>,
rng: &mut StdRng,
) -> SklResult<Array2<Float>> {
let n_features = data.ncols();
let n_atoms = (n_features * 2).min(self.sparsity_level * 4);
let mut dictionary = Array2::<Float>::zeros((n_features, n_atoms));
for elem in dictionary.iter_mut() {
*elem = rng.sample::<Float, _>(scirs2_core::StandardNormal) * 0.1;
}
for j in 0..n_atoms {
let atom_norm = dictionary.column(j).mapv(|x| x * x).sum().sqrt();
if atom_norm > 1e-10 {
dictionary.column_mut(j).mapv_inplace(|x| x / atom_norm);
}
}
let manifold_data = data.dot(manifold_basis).dot(&manifold_basis.t());
for _ in 0..50 {
let sparse_codes = self.sparse_coding(&manifold_data, &dictionary)?;
dictionary = self.update_dictionary(&manifold_data, &sparse_codes, &dictionary)?;
}
Ok(dictionary)
}
fn sparse_coding(
&self,
data: &Array2<Float>,
dictionary: &Array2<Float>,
) -> SklResult<Array2<Float>> {
let n_samples = data.nrows();
let n_atoms = dictionary.ncols();
let mut sparse_codes = Array2::zeros((n_samples, n_atoms));
for i in 0..n_samples {
let sample = data.row(i);
let mut code = Array1::zeros(n_atoms);
for _ in 0..20 {
for j in 0..n_atoms {
let residual = &sample - &dictionary.dot(&code)
+ dictionary.column(j).to_owned() * code[j];
let correlation = dictionary.column(j).dot(&residual);
code[j] = soft_threshold(correlation, self.sparsity_reg);
}
}
sparse_codes.row_mut(i).assign(&code);
}
Ok(sparse_codes)
}
fn update_dictionary(
&self,
data: &Array2<Float>,
sparse_codes: &Array2<Float>,
old_dictionary: &Array2<Float>,
) -> SklResult<Array2<Float>> {
let n_features = data.ncols();
let n_atoms = old_dictionary.ncols();
let mut new_dictionary = old_dictionary.clone();
for j in 0..n_atoms {
let mut active_samples = Vec::new();
for i in 0..sparse_codes.nrows() {
if sparse_codes[[i, j]].abs() > 1e-10 {
active_samples.push(i);
}
}
if active_samples.is_empty() {
continue;
}
let mut residual_matrix = Array2::zeros((active_samples.len(), n_features));
let mut active_codes = Array1::zeros(active_samples.len());
for (idx, &sample_idx) in active_samples.iter().enumerate() {
let sample = data.row(sample_idx);
let code = sparse_codes.row(sample_idx);
let reconstruction =
new_dictionary.dot(&code) - new_dictionary.column(j).to_owned() * code[j];
let residual = &sample - &reconstruction;
residual_matrix.row_mut(idx).assign(&residual);
active_codes[idx] = code[j];
}
if let Ok((u, s, _vt)) = residual_matrix.t().svd(true) {
if !s.is_empty() {
let new_atom = u.column(0).to_owned();
new_dictionary.column_mut(j).assign(&new_atom);
}
}
}
Ok(new_dictionary)
}
fn compute_reconstruction_weights(
&self,
data: &Array2<Float>,
manifold_basis: &Array2<Float>,
_sparse_dictionary: &Array2<Float>,
) -> SklResult<Array2<Float>> {
let n_samples = data.nrows();
let n_components = manifold_basis.ncols();
let mut weights = Array2::zeros((n_samples, n_components));
let manifold_coords = data.dot(manifold_basis);
for i in 0..n_samples {
let coords = manifold_coords.row(i);
weights.row_mut(i).assign(&coords);
}
Ok(weights)
}
}
#[derive(Debug, Clone)]
pub struct OrthogonalMatchingPursuit {
sparsity_level: usize,
tol: Float,
max_iter: usize,
}
impl OrthogonalMatchingPursuit {
pub fn new(sparsity_level: usize) -> Self {
Self {
sparsity_level,
tol: 1e-6,
max_iter: 100,
}
}
pub fn tol(mut self, tol: Float) -> Self {
self.tol = tol;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn solve(
&self,
dictionary: &Array2<Float>,
signal: &Array1<Float>,
) -> SklResult<Array1<Float>> {
let n_atoms = dictionary.ncols();
let mut coefficients = Array1::zeros(n_atoms);
let mut residual = signal.clone();
let mut selected_atoms = Vec::new();
for _iter in 0..self.max_iter.min(self.sparsity_level) {
let mut max_correlation = 0.0;
let mut best_atom = 0;
for j in 0..n_atoms {
if !selected_atoms.contains(&j) {
let correlation = dictionary.column(j).dot(&residual).abs();
if correlation > max_correlation {
max_correlation = correlation;
best_atom = j;
}
}
}
if max_correlation < self.tol {
break;
}
selected_atoms.push(best_atom);
let selected_dictionary = self.extract_selected_atoms(dictionary, &selected_atoms);
let selected_coefficients = self.solve_least_squares(&selected_dictionary, signal)?;
for (i, &atom_idx) in selected_atoms.iter().enumerate() {
coefficients[atom_idx] = selected_coefficients[i];
}
residual = signal - &dictionary.dot(&coefficients);
if residual.mapv(|x| x * x).sum().sqrt() < self.tol {
break;
}
}
Ok(coefficients)
}
fn extract_selected_atoms(
&self,
dictionary: &Array2<Float>,
selected_atoms: &[usize],
) -> Array2<Float> {
let n_features = dictionary.nrows();
let n_selected = selected_atoms.len();
let mut selected_dictionary = Array2::zeros((n_features, n_selected));
for (i, &atom_idx) in selected_atoms.iter().enumerate() {
selected_dictionary
.column_mut(i)
.assign(&dictionary.column(atom_idx));
}
selected_dictionary
}
fn solve_least_squares(
&self,
dictionary: &Array2<Float>,
signal: &Array1<Float>,
) -> SklResult<Array1<Float>> {
if dictionary.ncols() == 1 {
let col = dictionary.column(0);
let norm_sq = col.dot(&col);
if norm_sq < 1e-10 {
return Ok(Array1::zeros(1));
}
let coeff = col.dot(signal) / norm_sq;
return Ok(Array1::from_vec(vec![coeff]));
}
let (u, s, vt) = dictionary
.svd(true)
.map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
let s_inv = s.mapv(|x| if x > 1e-10 { 1.0 / x } else { 0.0 });
let mut pseudoinverse = Array2::zeros((dictionary.ncols(), dictionary.nrows()));
for i in 0..s_inv.len() {
if s_inv[i] > 0.0 {
let ui = u.column(i);
let vi = vt.row(i);
for j in 0..pseudoinverse.nrows() {
for k in 0..pseudoinverse.ncols() {
pseudoinverse[[j, k]] += s_inv[i] * vi[j] * ui[k];
}
}
}
}
let coefficients = pseudoinverse.dot(signal);
Ok(coefficients)
}
}
fn soft_threshold(x: Float, threshold: Float) -> Float {
if x > threshold {
x - threshold
} else if x < -threshold {
x + threshold
} else {
0.0
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_manifold_compressed_sensing_basic() {
let data = array![
[1.0, 2.0, 3.0, 4.0],
[2.0, 3.0, 4.0, 5.0],
[3.0, 4.0, 5.0, 6.0],
[4.0, 5.0, 6.0, 7.0]
];
let cs = ManifoldCompressedSensing::new(2, 2);
let fitted = cs.fit(&data, &()).expect("operation should succeed");
let measurements = fitted.transform(&data).expect("operation should succeed");
assert_eq!(measurements.shape(), &[4, 2]);
assert!(measurements.iter().all(|&x| x.is_finite()));
let reconstructed = fitted
.reconstruct(&measurements)
.expect("operation should succeed");
assert_eq!(reconstructed.shape(), &[4, 4]);
assert!(reconstructed.iter().all(|&x| x.is_finite()));
}
#[test]
fn test_manifold_compressed_sensing_reconstruction() {
let data = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let cs = ManifoldCompressedSensing::new(2, 2)
.sparsity_level(2)
.random_state(42);
let fitted = cs.fit(&data, &()).expect("operation should succeed");
let measurements = fitted.transform(&data).expect("operation should succeed");
let reconstructed = fitted
.reconstruct(&measurements)
.expect("operation should succeed");
assert_eq!(reconstructed.shape(), &[3, 3]);
assert!(reconstructed.iter().all(|&x| x.is_finite()));
let reconstruction_error = (&data - &reconstructed).mapv(|x| x * x).sum().sqrt();
assert!(
reconstruction_error < 10000.0,
"Reconstruction error {} exceeds threshold",
reconstruction_error
);
}
#[test]
fn test_orthogonal_matching_pursuit() {
let dictionary = array![[1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0]];
let signal = array![1.0, 2.0, 3.0];
let omp = OrthogonalMatchingPursuit::new(3);
let coefficients = omp
.solve(&dictionary, &signal)
.expect("operation should succeed");
assert_eq!(coefficients.len(), 3);
assert!(coefficients.iter().all(|&x| x.is_finite()));
let reconstruction = dictionary.dot(&coefficients);
let error = (&signal - &reconstruction).mapv(|x| x * x).sum().sqrt();
assert!(error < 1e-10);
}
#[test]
fn test_orthogonal_matching_pursuit_sparse() {
let dictionary = array![[1.0, 0.0, 0.1], [0.0, 1.0, 0.1], [0.0, 0.0, 1.0]];
let signal = array![1.0, 2.0, 0.1];
let omp = OrthogonalMatchingPursuit::new(2);
let coefficients = omp
.solve(&dictionary, &signal)
.expect("operation should succeed");
assert_eq!(coefficients.len(), 3);
assert!(coefficients.iter().all(|&x| x.is_finite()));
let n_nonzero = coefficients.iter().filter(|&&x| x.abs() > 1e-10).count();
assert!(n_nonzero <= 2);
}
#[test]
fn test_soft_threshold() {
assert_abs_diff_eq!(soft_threshold(2.0, 1.0), 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(soft_threshold(-2.0, 1.0), -1.0, epsilon = 1e-10);
assert_abs_diff_eq!(soft_threshold(0.5, 1.0), 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(soft_threshold(-0.5, 1.0), 0.0, epsilon = 1e-10);
}
#[test]
fn test_manifold_compressed_sensing_invalid_parameters() {
let data = array![[1.0, 2.0], [3.0, 4.0]];
let cs = ManifoldCompressedSensing::new(3, 2);
assert!(cs.fit(&data, &()).is_err());
let cs = ManifoldCompressedSensing::new(1, 3);
assert!(cs.fit(&data, &()).is_err());
}
#[test]
fn test_manifold_compressed_sensing_properties() {
let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let cs = ManifoldCompressedSensing::new(2, 2).random_state(42);
let fitted = cs.fit(&data, &()).expect("operation should succeed");
let measurement_matrix = fitted.measurement_matrix();
assert_eq!(measurement_matrix.shape(), &[2, 3]);
for i in 0..2 {
let row_norm = measurement_matrix.row(i).mapv(|x| x * x).sum().sqrt();
assert_abs_diff_eq!(row_norm, 1.0, epsilon = 1e-10);
}
let manifold_basis = fitted.manifold_basis();
assert_eq!(manifold_basis.shape(), &[3, 2]);
let sparse_dictionary = fitted.sparse_dictionary();
assert_eq!(sparse_dictionary.nrows(), 3);
assert!(sparse_dictionary.ncols() > 0);
}
}