use crate::error::{Result, TransformError};
use scirs2_core::ndarray::{Array1, Array2, Axis};
pub fn add_gaussian_noise(data: &Array2<f64>, noise_std: f64) -> Result<Array2<f64>> {
if noise_std < 0.0 {
return Err(TransformError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
if noise_std == 0.0 {
return Ok(data.clone());
}
let n_elems = data.nrows() * data.ncols();
let noise = lcg_normal_samples(n_elems, noise_std);
let noise_arr = Array2::from_shape_vec((data.nrows(), data.ncols()), noise)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
Ok(data + &noise_arr)
}
fn lcg_normal_samples(n: usize, std: f64) -> Vec<f64> {
const A: u64 = 1_664_525;
const C: u64 = 1_013_904_223;
const M: u64 = 1 << 32;
let mut state: u64 = 2_463_534_242; let mut samples = Vec::with_capacity(n);
let mut i = 0;
while i < n {
state = (A.wrapping_mul(state).wrapping_add(C)) % M;
let u1 = state as f64 / M as f64;
state = (A.wrapping_mul(state).wrapping_add(C)) % M;
let u2 = state as f64 / M as f64;
let u1 = u1.max(1e-12); let mag = std * (-2.0 * u1.ln()).sqrt();
let z0 = mag * (2.0 * std::f64::consts::PI * u2).cos();
let z1 = mag * (2.0 * std::f64::consts::PI * u2).sin();
samples.push(z0);
i += 1;
if i < n {
samples.push(z1);
i += 1;
}
}
samples
}
fn column_means(data: &Array2<f64>) -> Array1<f64> {
data.mean_axis(Axis(0)).unwrap_or_else(|| Array1::zeros(data.ncols()))
}
fn centre(data: &Array2<f64>, means: &Array1<f64>) -> Array2<f64> {
let mut out = data.clone();
for mut row in out.rows_mut() {
for (v, &m) in row.iter_mut().zip(means.iter()) {
*v -= m;
}
}
out
}
fn matmul(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
let (m, k1) = (a.nrows(), a.ncols());
let (k2, n) = (b.nrows(), b.ncols());
if k1 != k2 {
return Err(TransformError::ComputationError(format!(
"matmul dimension mismatch: ({m},{k1}) × ({k2},{n})"
)));
}
let mut out = Array2::<f64>::zeros((m, n));
for i in 0..m {
for j in 0..n {
let mut s = 0.0f64;
for l in 0..k1 {
s += a[[i, l]] * b[[l, j]];
}
out[[i, j]] = s;
}
}
Ok(out)
}
fn transpose(a: &Array2<f64>) -> Array2<f64> {
let (m, n) = (a.nrows(), a.ncols());
let mut out = Array2::<f64>::zeros((n, m));
for i in 0..m {
for j in 0..n {
out[[j, i]] = a[[i, j]];
}
}
out
}
fn mse(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
let n = (a.nrows() * a.ncols()) as f64;
if n == 0.0 {
return 0.0;
}
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y) * (x - y))
.sum::<f64>()
/ n
}
#[derive(Debug, Clone)]
pub struct LinearAutoencoder {
pub encoder_weights: Array2<f64>,
pub decoder_weights: Array2<f64>,
pub means: Array1<f64>,
pub n_features: usize,
pub n_components: usize,
pub loss_history: Vec<f64>,
}
impl LinearAutoencoder {
pub fn encode(&self, data: &Array2<f64>) -> Result<Array2<f64>> {
if data.ncols() != self.n_features {
return Err(TransformError::InvalidInput(format!(
"data has {} features but autoencoder expects {}",
data.ncols(),
self.n_features
)));
}
let centred = centre(data, &self.means);
encode(¢red, &self.encoder_weights)
}
pub fn decode(&self, latent: &Array2<f64>) -> Result<Array2<f64>> {
if latent.ncols() != self.n_components {
return Err(TransformError::InvalidInput(format!(
"latent has {} dims but autoencoder has {} components",
latent.ncols(),
self.n_components
)));
}
let recon = decode(latent, &self.decoder_weights)?;
let mut out = recon;
for mut row in out.rows_mut() {
for (v, &m) in row.iter_mut().zip(self.means.iter()) {
*v += m;
}
}
Ok(out)
}
pub fn reconstruction_loss(&self, data: &Array2<f64>) -> Result<f64> {
let latent = self.encode(data)?;
let recon_centred = decode(&latent, &self.decoder_weights)?;
let centred = centre(data, &self.means);
Ok(mse(¢red, &recon_centred))
}
}
pub fn encode(data: &Array2<f64>, weights: &Array2<f64>) -> Result<Array2<f64>> {
matmul(data, weights)
}
pub fn decode(latent: &Array2<f64>, weights: &Array2<f64>) -> Result<Array2<f64>> {
matmul(latent, weights)
}
pub fn fit_linear_ae(
data: &Array2<f64>,
n_components: usize,
lr: f64,
n_iter: usize,
) -> Result<LinearAutoencoder> {
validate_ae_params(data, n_components, lr, n_iter)?;
let means = column_means(data);
let x = centre(data, &means);
let n_features = data.ncols();
let n_samples = data.nrows();
let enc_flat = lcg_normal_samples(n_features * n_components, 0.01);
let mut w_enc = Array2::from_shape_vec((n_features, n_components), enc_flat)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
w_enc = gram_schmidt_orthonorm(&w_enc)?;
let mut loss_history = Vec::with_capacity(n_iter);
for _ in 0..n_iter {
let z = matmul(&x, &w_enc)?; let w_dec = transpose(&w_enc); let x_hat = matmul(&z, &w_dec)?;
let residual = {
let mut r = x_hat.clone();
for i in 0..n_samples {
for j in 0..n_features {
r[[i, j]] -= x[[i, j]];
}
}
r
};
let loss = residual.iter().map(|v| v * v).sum::<f64>() / (n_samples * n_features) as f64;
loss_history.push(loss);
let xt = transpose(&x); let resid_wenc = matmul(&residual, &w_enc)?; let grad_a = matmul(&xt, &resid_wenc)?; let scale = 2.0 / (n_samples * n_features) as f64;
for i in 0..n_features {
for j in 0..n_components {
w_enc[[i, j]] -= lr * scale * 2.0 * grad_a[[i, j]];
}
}
}
let w_dec = transpose(&w_enc);
Ok(LinearAutoencoder {
encoder_weights: w_enc,
decoder_weights: w_dec,
means,
n_features,
n_components,
loss_history,
})
}
fn gram_schmidt_orthonorm(mat: &Array2<f64>) -> Result<Array2<f64>> {
let (m, n) = (mat.nrows(), mat.ncols());
let mut out = mat.clone();
for j in 0..n {
for k in 0..j {
let dot: f64 = (0..m).map(|i| out[[i, k]] * out[[i, j]]).sum();
let norm_sq: f64 = (0..m).map(|i| out[[i, k]] * out[[i, k]]).sum();
if norm_sq > 1e-12 {
let coeff = dot / norm_sq;
for i in 0..m {
out[[i, j]] -= coeff * out[[i, k]];
}
}
}
let norm: f64 = (0..m).map(|i| out[[i, j]] * out[[i, j]]).sum::<f64>().sqrt();
if norm > 1e-12 {
for i in 0..m {
out[[i, j]] /= norm;
}
}
}
Ok(out)
}
#[derive(Debug, Clone)]
pub struct DenoisingAE {
pub encoder_weights: Array2<f64>,
pub decoder_weights: Array2<f64>,
pub means: Array1<f64>,
pub n_features: usize,
pub n_components: usize,
pub noise_std: f64,
pub loss_history: Vec<f64>,
}
impl DenoisingAE {
pub fn encode(&self, data: &Array2<f64>) -> Result<Array2<f64>> {
if data.ncols() != self.n_features {
return Err(TransformError::InvalidInput(format!(
"data has {} features but DAE expects {}",
data.ncols(),
self.n_features
)));
}
let centred = centre(data, &self.means);
encode(¢red, &self.encoder_weights)
}
pub fn decode(&self, latent: &Array2<f64>) -> Result<Array2<f64>> {
if latent.ncols() != self.n_components {
return Err(TransformError::InvalidInput(format!(
"latent has {} dims but DAE has {} components",
latent.ncols(),
self.n_components
)));
}
let recon = decode(latent, &self.decoder_weights)?;
let mut out = recon;
for mut row in out.rows_mut() {
for (v, &m) in row.iter_mut().zip(self.means.iter()) {
*v += m;
}
}
Ok(out)
}
pub fn reconstruction_loss(&self, data: &Array2<f64>) -> Result<f64> {
let centred = centre(data, &self.means);
let latent = encode(¢red, &self.encoder_weights)?;
let recon = decode(&latent, &self.decoder_weights)?;
Ok(mse(¢red, &recon))
}
}
pub fn fit_denoising_ae(
clean_data: &Array2<f64>,
n_components: usize,
noise_std: f64,
lr: f64,
n_iter: usize,
) -> Result<DenoisingAE> {
validate_ae_params(clean_data, n_components, lr, n_iter)?;
if noise_std < 0.0 {
return Err(TransformError::InvalidInput(
"noise_std must be non-negative".to_string(),
));
}
let means = column_means(clean_data);
let x_clean = centre(clean_data, &means);
let n_features = clean_data.ncols();
let n_samples = clean_data.nrows();
let enc_flat = lcg_normal_samples(n_features * n_components, 0.01);
let mut w_enc = Array2::from_shape_vec((n_features, n_components), enc_flat)
.map_err(|e| TransformError::ComputationError(e.to_string()))?;
w_enc = gram_schmidt_orthonorm(&w_enc)?;
let mut loss_history = Vec::with_capacity(n_iter);
for iter in 0..n_iter {
let noise_scale = if noise_std > 0.0 {
let t = iter as f64 / n_iter.max(1) as f64;
noise_std * (0.8 + 0.4 * t)
} else {
0.0
};
let noisy = add_gaussian_noise(&x_clean, noise_scale)?;
let z = matmul(&noisy, &w_enc)?;
let w_dec = transpose(&w_enc);
let x_hat = matmul(&z, &w_dec)?;
let residual = {
let mut r = x_hat.clone();
for i in 0..n_samples {
for j in 0..n_features {
r[[i, j]] -= x_clean[[i, j]];
}
}
r
};
let loss = residual.iter().map(|v| v * v).sum::<f64>() / (n_samples * n_features) as f64;
loss_history.push(loss);
let xt_noisy = transpose(&noisy);
let resid_wenc = matmul(&residual, &w_enc)?; let grad_enc = matmul(&xt_noisy, &resid_wenc)?;
let rt = transpose(&residual); let grad_dec = matmul(&rt, &z)?;
let scale = 2.0 / (n_samples * n_features) as f64;
for i in 0..n_features {
for j in 0..n_components {
let g = scale * (grad_enc[[i, j]] + grad_dec[[i, j]]);
w_enc[[i, j]] -= lr * g;
}
}
}
let w_dec = transpose(&w_enc);
Ok(DenoisingAE {
encoder_weights: w_enc,
decoder_weights: w_dec,
means,
n_features,
n_components,
noise_std,
loss_history,
})
}
fn validate_ae_params(
data: &Array2<f64>,
n_components: usize,
lr: f64,
n_iter: usize,
) -> Result<()> {
if data.nrows() == 0 || data.ncols() == 0 {
return Err(TransformError::InvalidInput(
"data must have at least one sample and one feature".to_string(),
));
}
if n_components == 0 {
return Err(TransformError::InvalidInput(
"n_components must be > 0".to_string(),
));
}
if n_components > data.ncols() {
return Err(TransformError::InvalidInput(format!(
"n_components ({}) cannot exceed n_features ({})",
n_components,
data.ncols()
)));
}
if lr <= 0.0 {
return Err(TransformError::InvalidInput(
"lr (learning rate) must be > 0".to_string(),
));
}
if n_iter == 0 {
return Err(TransformError::InvalidInput(
"n_iter must be > 0".to_string(),
));
}
Ok(())
}
pub fn fit_linear_ae_weights(
data: &Array2<f64>,
n_components: usize,
lr: f64,
n_iter: usize,
) -> Result<(Array2<f64>, Array2<f64>)> {
let ae = fit_linear_ae(data, n_components, lr, n_iter)?;
Ok((ae.encoder_weights, ae.decoder_weights))
}
pub fn fit_denoising_ae_weights(
clean_data: &Array2<f64>,
n_components: usize,
noise_std: f64,
lr: f64,
n_iter: usize,
) -> Result<(Array2<f64>, Array2<f64>)> {
let ae = fit_denoising_ae(clean_data, n_components, noise_std, lr, n_iter)?;
Ok((ae.encoder_weights, ae.decoder_weights))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn make_data() -> Array2<f64> {
let n = 20usize;
let mut vals = Vec::with_capacity(n * 4);
for i in 0..n {
let t = i as f64 / n as f64;
vals.push(t);
vals.push(2.0 * t);
vals.push(-t);
vals.push(0.5 * t + 0.1);
}
Array2::from_shape_vec((n, 4), vals).expect("shape ok")
}
#[test]
fn test_add_gaussian_noise_shape() {
let data = make_data();
let noisy = add_gaussian_noise(&data, 0.1).expect("noise ok");
assert_eq!(noisy.shape(), data.shape());
}
#[test]
fn test_add_gaussian_noise_zero() {
let data = make_data();
let noisy = add_gaussian_noise(&data, 0.0).expect("no noise ok");
assert_eq!(noisy, data);
}
#[test]
fn test_encode_decode_shapes() {
let data = make_data();
let ae = fit_linear_ae(&data, 2, 0.01, 50).expect("fit ok");
let latent = ae.encode(&data).expect("encode ok");
assert_eq!(latent.shape(), &[20, 2]);
let recon = ae.decode(&latent).expect("decode ok");
assert_eq!(recon.shape(), data.shape());
}
#[test]
fn test_reconstruction_loss_nonneg() {
let data = make_data();
let ae = fit_linear_ae(&data, 2, 0.01, 50).expect("fit ok");
let loss = ae.reconstruction_loss(&data).expect("loss ok");
assert!(loss >= 0.0);
}
#[test]
fn test_denoising_ae_shapes() {
let data = make_data();
let dae = fit_denoising_ae(&data, 2, 0.05, 0.01, 50).expect("dae fit ok");
let latent = dae.encode(&data).expect("encode ok");
assert_eq!(latent.shape(), &[20, 2]);
let recon = dae.decode(&latent).expect("decode ok");
assert_eq!(recon.shape(), data.shape());
}
#[test]
fn test_fit_weights_convenience() {
let data = make_data();
let (enc, dec) = fit_linear_ae_weights(&data, 2, 0.01, 30).expect("weights ok");
assert_eq!(enc.shape(), &[4, 2]);
assert_eq!(dec.shape(), &[2, 4]);
}
#[test]
fn test_denoising_ae_weights_convenience() {
let data = make_data();
let (enc, dec) = fit_denoising_ae_weights(&data, 2, 0.05, 0.01, 30).expect("weights ok");
assert_eq!(enc.shape(), &[4, 2]);
assert_eq!(dec.shape(), &[2, 4]);
}
#[test]
fn test_invalid_n_components_zero() {
let data = make_data();
assert!(fit_linear_ae(&data, 0, 0.01, 10).is_err());
}
#[test]
fn test_invalid_n_components_too_large() {
let data = make_data();
assert!(fit_linear_ae(&data, 10, 0.01, 10).is_err());
}
#[test]
fn test_invalid_lr() {
let data = make_data();
assert!(fit_linear_ae(&data, 2, -0.01, 10).is_err());
}
#[test]
fn test_standalone_encode_decode() {
let data = make_data();
let mut enc = Array2::<f64>::zeros((4, 2));
enc[[0, 0]] = 1.0;
enc[[1, 1]] = 1.0;
let mut dec = Array2::<f64>::zeros((2, 4));
dec[[0, 0]] = 1.0;
dec[[1, 1]] = 1.0;
let latent = encode(&data, &enc).expect("encode ok");
assert_eq!(latent.shape(), &[20, 2]);
let recon = decode(&latent, &dec).expect("decode ok");
assert_eq!(recon.shape(), &[20, 4]);
}
}