use scirs2_core::ndarray::{par_azip, Array1, Array2, ArrayView2, Axis};
use scirs2_core::parallel_ops::*;
use scirs2_core::random::{Rng, RngExt};
use scirs2_core::validation::{check_not_empty, check_positive};
use crate::error::{Result, TransformError};
use crate::utils::{DataChunker, PerfUtils, ProcessingStrategy, StatUtils};
use statrs::statistics::Statistics;
pub struct EnhancedStandardScaler {
means: Option<Array1<f64>>,
stds: Option<Array1<f64>>,
robust: bool,
strategy: ProcessingStrategy,
memory_limitmb: usize,
}
impl EnhancedStandardScaler {
pub fn new(robust: bool, memory_limitmb: usize) -> Self {
EnhancedStandardScaler {
means: None,
stds: None,
robust,
strategy: ProcessingStrategy::Standard,
memory_limitmb,
}
}
pub fn fit(&mut self, x: &ArrayView2<f64>) -> Result<()> {
check_not_empty(x, "x")?;
for &val in x.iter() {
if !val.is_finite() {
return Err(crate::error::TransformError::DataValidationError(
"Data contains non-finite values".to_string(),
));
}
}
let (n_samples, n_features) = x.dim();
self.strategy =
PerfUtils::choose_processing_strategy(n_samples, n_features, self.memory_limitmb);
match &self.strategy {
ProcessingStrategy::OutOfCore { chunk_size } => self.fit_out_of_core(x, *chunk_size),
ProcessingStrategy::Parallel => self.fit_parallel(x),
ProcessingStrategy::Simd => self.fit_simd(x),
ProcessingStrategy::Standard => self.fit_standard(x),
}
}
fn fit_out_of_core(&mut self, x: &ArrayView2<f64>, _chunksize: usize) -> Result<()> {
let (n_samples, n_features) = x.dim();
let chunker = DataChunker::new(self.memory_limitmb);
if self.robust {
return self.fit_robust_out_of_core(x);
}
let mut means = Array1::zeros(n_features);
let mut m2 = Array1::zeros(n_features); let mut count = 0;
for (start_idx, end_idx) in chunker.chunk_indices(n_samples, n_features) {
let chunk = x.slice(scirs2_core::ndarray::s![start_idx..end_idx, ..]);
for row in chunk.rows().into_iter() {
count += 1;
let delta = &row - &means;
means = &means + &delta / count as f64;
let delta2 = &row - &means;
m2 = &m2 + &delta * &delta2;
}
}
let variances = if count > 1 {
&m2 / (count - 1) as f64
} else {
Array1::ones(n_features)
};
let stds = variances.mapv(|v| if v > 1e-15 { v.sqrt() } else { 1.0 });
self.means = Some(means);
self.stds = Some(stds);
Ok(())
}
fn fit_parallel(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let (_, n_features) = x.dim();
if self.robust {
let (medians, mads) = StatUtils::robust_stats_columns(x)?;
let stds = mads.mapv(|mad| if mad > 1e-15 { mad * 1.4826 } else { 1.0 });
self.means = Some(medians);
self.stds = Some(stds);
} else {
let means: Result<Array1<f64>> = (0..n_features)
.into_par_iter()
.map(|j| {
let col = x.column(j);
Ok(col.mean())
})
.collect::<Result<Vec<_>>>()
.map(Array1::from_vec);
let means = means?;
let stds: Result<Array1<f64>> = (0..n_features)
.into_par_iter()
.map(|j| {
let col = x.column(j);
let mean = means[j];
let var = col.iter().map(|&val| (val - mean).powi(2)).sum::<f64>()
/ (col.len() - 1).max(1) as f64;
Ok(if var > 1e-15 { var.sqrt() } else { 1.0 })
})
.collect::<Result<Vec<_>>>()
.map(Array1::from_vec);
let stds = stds?;
self.means = Some(means);
self.stds = Some(stds);
}
Ok(())
}
fn fit_simd(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let means = x.mean_axis(Axis(0)).expect("Operation failed");
let (_n_samples, n_features) = x.dim();
let mut variances = Array1::zeros(n_features);
for j in 0..n_features {
let col = x.column(j);
let mean = means[j];
let variance = if col.len() > 1 {
let sum_sq_diff = col.iter().map(|&val| (val - mean).powi(2)).sum::<f64>();
sum_sq_diff / (col.len() - 1) as f64
} else {
1.0
};
variances[j] = variance;
}
let stds = variances.mapv(|v| if v > 1e-15 { v.sqrt() } else { 1.0 });
self.means = Some(means);
self.stds = Some(stds);
Ok(())
}
fn fit_standard(&mut self, x: &ArrayView2<f64>) -> Result<()> {
if self.robust {
let (medians, mads) = StatUtils::robust_stats_columns(x)?;
let stds = mads.mapv(|mad| if mad > 1e-15 { mad * 1.4826 } else { 1.0 });
self.means = Some(medians);
self.stds = Some(stds);
} else {
let means = x.mean_axis(Axis(0)).expect("Operation failed");
let stds = x.std_axis(Axis(0), 0.0);
let stds = stds.mapv(|s| if s > 1e-15 { s } else { 1.0 });
self.means = Some(means);
self.stds = Some(stds);
}
Ok(())
}
fn fit_robust_out_of_core(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let (_, n_features) = x.dim();
let chunker = DataChunker::new(self.memory_limitmb);
let mut medians = Array1::zeros(n_features);
let mut mads = Array1::zeros(n_features);
for j in 0..n_features {
let mut column_data = Vec::new();
for (start_idx, end_idx) in chunker.chunk_indices(x.nrows(), 1) {
let chunk = x.slice(scirs2_core::ndarray::s![start_idx..end_idx, j..j + 1]);
column_data.extend(chunk.iter().copied());
}
let col_array = Array1::from_vec(column_data);
let (median, mad) = StatUtils::robust_stats(&col_array.view())?;
medians[j] = median;
mads[j] = if mad > 1e-15 { mad * 1.4826 } else { 1.0 };
}
self.means = Some(medians);
self.stds = Some(mads);
Ok(())
}
pub fn transform(&self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
let means = self
.means
.as_ref()
.ok_or_else(|| TransformError::NotFitted("StandardScaler not fitted".to_string()))?;
let stds = self
.stds
.as_ref()
.ok_or_else(|| TransformError::NotFitted("StandardScaler not fitted".to_string()))?;
check_not_empty(x, "x")?;
for &val in x.iter() {
if !val.is_finite() {
return Err(crate::error::TransformError::DataValidationError(
"Data contains non-finite values".to_string(),
));
}
}
let (_n_samples, n_features) = x.dim();
if n_features != means.len() {
return Err(TransformError::InvalidInput(format!(
"Number of features {} doesn't match fitted features {}",
n_features,
means.len()
)));
}
match &self.strategy {
ProcessingStrategy::OutOfCore { chunk_size } => {
self.transform_out_of_core(x, means, stds, *chunk_size)
}
ProcessingStrategy::Parallel => self.transform_parallel(x, means, stds),
ProcessingStrategy::Simd => self.transform_simd(x, means, stds),
ProcessingStrategy::Standard => self.transform_standard(x, means, stds),
}
}
fn transform_out_of_core(
&self,
x: &ArrayView2<f64>,
means: &Array1<f64>,
stds: &Array1<f64>,
_chunk_size: usize,
) -> Result<Array2<f64>> {
let (n_samples, n_features) = x.dim();
let mut result = Array2::zeros((n_samples, n_features));
let chunker = DataChunker::new(self.memory_limitmb);
for (start_idx, end_idx) in chunker.chunk_indices(n_samples, n_features) {
let chunk = x.slice(scirs2_core::ndarray::s![start_idx..end_idx, ..]);
let transformed_chunk =
(&chunk - &means.view().insert_axis(Axis(0))) / stds.view().insert_axis(Axis(0));
result
.slice_mut(scirs2_core::ndarray::s![start_idx..end_idx, ..])
.assign(&transformed_chunk);
}
Ok(result)
}
fn transform_parallel(
&self,
x: &ArrayView2<f64>,
means: &Array1<f64>,
stds: &Array1<f64>,
) -> Result<Array2<f64>> {
let (n_samples, n_features) = x.dim();
let mut result = Array2::zeros((n_samples, n_features));
for (j, ((mean, std), col)) in means
.iter()
.zip(stds.iter())
.zip(result.columns_mut())
.enumerate()
{
let x_col = x.column(j);
par_azip!((out in col, &inp in x_col) {
*out = (inp - mean) / std;
});
}
Ok(result)
}
fn transform_simd(
&self,
x: &ArrayView2<f64>,
means: &Array1<f64>,
stds: &Array1<f64>,
) -> Result<Array2<f64>> {
let centered = x - &means.view().insert_axis(Axis(0));
let result = ¢ered / &stds.view().insert_axis(Axis(0));
Ok(result)
}
fn transform_standard(
&self,
x: &ArrayView2<f64>,
means: &Array1<f64>,
stds: &Array1<f64>,
) -> Result<Array2<f64>> {
let result = (x - &means.view().insert_axis(Axis(0))) / stds.view().insert_axis(Axis(0));
Ok(result)
}
pub fn fit_transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
self.fit(x)?;
self.transform(x)
}
pub fn means(&self) -> Option<&Array1<f64>> {
self.means.as_ref()
}
pub fn stds(&self) -> Option<&Array1<f64>> {
self.stds.as_ref()
}
pub fn processing_strategy(&self) -> &ProcessingStrategy {
&self.strategy
}
}
pub struct EnhancedPCA {
n_components: usize,
center: bool,
components: Option<Array2<f64>>,
explained_variance: Option<Array1<f64>>,
explained_variance_ratio: Option<Array1<f64>>,
mean: Option<Array1<f64>>,
strategy: ProcessingStrategy,
memory_limitmb: usize,
use_randomized: bool,
}
impl EnhancedPCA {
pub fn new(n_components: usize, center: bool, memory_limitmb: usize) -> Result<Self> {
check_positive(n_components, "n_components")?;
Ok(EnhancedPCA {
n_components,
center: true,
components: None,
explained_variance: None,
explained_variance_ratio: None,
mean: None,
strategy: ProcessingStrategy::Standard,
memory_limitmb,
use_randomized: false,
})
}
pub fn with_randomized_svd(mut self, userandomized: bool) -> Self {
self.use_randomized = userandomized;
self
}
pub fn fit(&mut self, x: &ArrayView2<f64>) -> Result<()> {
check_not_empty(x, "x")?;
for &val in x.iter() {
if !val.is_finite() {
return Err(crate::error::TransformError::DataValidationError(
"Data contains non-finite values".to_string(),
));
}
}
let (n_samples, n_features) = x.dim();
if self.n_components > n_features.min(n_samples) {
return Err(TransformError::InvalidInput(
"n_components cannot be larger than min(n_samples, n_features)".to_string(),
));
}
self.strategy =
PerfUtils::choose_processing_strategy(n_samples, n_features, self.memory_limitmb);
if n_samples > 50000 && n_features > 1000 {
self.use_randomized = true;
}
match &self.strategy {
ProcessingStrategy::OutOfCore { chunk_size } => {
self.fit_incremental_pca(x, *chunk_size)
}
_ => {
if self.use_randomized {
self.fit_randomized_pca(x)
} else {
self.fit_standard_pca(x)
}
}
}
}
fn fit_incremental_pca(&mut self, x: &ArrayView2<f64>, chunksize: usize) -> Result<()> {
let (n_samples, n_features) = x.dim();
let chunker = DataChunker::new(self.memory_limitmb);
let mut running_mean = Array1::<f64>::zeros(n_features);
let _running_var = Array1::<f64>::zeros(n_features);
let mut n_samples_seen = 0;
for (start_idx, end_idx) in chunker.chunk_indices(n_samples, n_features) {
let chunk = x.slice(scirs2_core::ndarray::s![start_idx..end_idx, ..]);
let chunk_mean = chunk.mean_axis(Axis(0)).expect("Operation failed");
let chunksize = end_idx - start_idx;
let total_samples = n_samples_seen + chunksize;
running_mean = (running_mean * n_samples_seen as f64 + chunk_mean * chunksize as f64)
/ total_samples as f64;
n_samples_seen = total_samples;
}
self.mean = if self.center {
Some(running_mean.clone())
} else {
None
};
self.fit_streaming_incremental_pca(x, &running_mean, chunksize)
}
fn fit_streaming_incremental_pca(
&mut self,
x: &ArrayView2<f64>,
mean: &Array1<f64>,
_chunk_size: usize,
) -> Result<()> {
let (n_samples, n_features) = x.dim();
let chunker = DataChunker::new(self.memory_limitmb);
let mut u = Array2::zeros((0, self.n_components)); let mut sigma = Array1::zeros(self.n_components);
let mut vt = Array2::zeros((self.n_components, n_features));
let mut n_samples_seen = 0;
let forgetting_factor = 0.95;
for (start_idx, end_idx) in chunker.chunk_indices(n_samples, n_features) {
let chunk = x.slice(scirs2_core::ndarray::s![start_idx..end_idx, ..]);
let chunk_size_actual = end_idx - start_idx;
let chunk_centered = if self.center {
&chunk - &mean.view().insert_axis(Axis(0))
} else {
chunk.to_owned()
};
self.incremental_svd_update(
&chunk_centered,
&mut u,
&mut sigma,
&mut vt,
n_samples_seen,
forgetting_factor,
)?;
n_samples_seen += chunk_size_actual;
if n_samples_seen > 10000 {
sigma.mapv_inplace(|x| x * forgetting_factor);
}
}
self.components = Some(
vt.t()
.to_owned()
.slice(scirs2_core::ndarray::s![.., ..self.n_components])
.to_owned(),
);
let total_variance = sigma.iter().map(|&s| s * s).sum::<f64>();
if total_variance > 0.0 {
let variance_ratios = sigma.mapv(|s| (s * s) / total_variance);
self.explained_variance_ratio = Some(variance_ratios);
} else {
self.explained_variance_ratio =
Some(Array1::ones(self.n_components) / self.n_components as f64);
}
Ok(())
}
fn incremental_svd_update(
&self,
new_chunk: &Array2<f64>,
u: &mut Array2<f64>,
sigma: &mut Array1<f64>,
vt: &mut Array2<f64>,
n_samples_seen: usize,
forgetting_factor: f64,
) -> Result<()> {
let (chunk_rows, n_features) = new_chunk.dim();
if n_samples_seen == 0 {
return self.initialize_svd_from_chunk(new_chunk, u, sigma, vt);
}
let projected = new_chunk.dot(&vt.t());
let reconstructed = projected.dot(vt);
let residual = new_chunk - &reconstructed;
let (q_residual, r_residual) = self.qr_decomposition_chunked(&residual)?;
let extended_u = if u.nrows() > 0 {
let mut new_u = Array2::zeros((u.nrows() + chunk_rows, u.ncols() + q_residual.ncols()));
new_u
.slice_mut(scirs2_core::ndarray::s![..u.nrows(), ..u.ncols()])
.assign(u);
if q_residual.ncols() > 0 {
new_u
.slice_mut(scirs2_core::ndarray::s![u.nrows().., u.ncols()..])
.assign(&q_residual);
}
new_u
} else {
q_residual.clone()
};
let mut augmented_sigma = Array2::zeros((
sigma.len() + r_residual.nrows(),
sigma.len() + r_residual.ncols(),
));
for (i, &s) in sigma.iter().enumerate() {
augmented_sigma[[i, i]] = s * forgetting_factor.sqrt(); }
if r_residual.nrows() > 0 && r_residual.ncols() > 0 {
let start_row = sigma.len();
let start_col = sigma.len();
let end_row = (start_row + r_residual.nrows()).min(augmented_sigma.nrows());
let end_col = (start_col + r_residual.ncols()).min(augmented_sigma.ncols());
if end_row > start_row && end_col > start_col {
augmented_sigma
.slice_mut(scirs2_core::ndarray::s![
start_row..end_row,
start_col..end_col
])
.assign(&r_residual.slice(scirs2_core::ndarray::s![
..(end_row - start_row),
..(end_col - start_col)
]));
}
}
let (u_aug, sigma_new, vt_aug) = self.svd_small_matrix(&augmented_sigma)?;
let k = self.n_components.min(sigma_new.len());
*sigma = sigma_new.slice(scirs2_core::ndarray::s![..k]).to_owned();
if extended_u.ncols() >= u_aug.nrows() && u_aug.ncols() >= k {
*u = extended_u
.slice(scirs2_core::ndarray::s![.., ..u_aug.nrows()])
.dot(&u_aug.slice(scirs2_core::ndarray::s![.., ..k]));
}
if vt_aug.nrows() >= k && vt.ncols() == vt_aug.ncols() {
*vt = vt_aug.slice(scirs2_core::ndarray::s![..k, ..]).to_owned();
}
Ok(())
}
fn initialize_svd_from_chunk(
&self,
chunk: &Array2<f64>,
u: &mut Array2<f64>,
sigma: &mut Array1<f64>,
vt: &mut Array2<f64>,
) -> Result<()> {
let (chunk_u, chunk_sigma, chunk_vt) = self.svd_small_matrix(chunk)?;
let k = self.n_components.min(chunk_sigma.len());
*u = chunk_u.slice(scirs2_core::ndarray::s![.., ..k]).to_owned();
*sigma = chunk_sigma.slice(scirs2_core::ndarray::s![..k]).to_owned();
*vt = chunk_vt.slice(scirs2_core::ndarray::s![..k, ..]).to_owned();
Ok(())
}
fn qr_decomposition_chunked(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
if m == 0 || n == 0 {
return Ok((Array2::zeros((m, 0)), Array2::zeros((0, n))));
}
let mut q = Array2::zeros((m, n.min(m)));
let mut r = Array2::zeros((n.min(m), n));
for j in 0..n.min(m) {
let mut col = matrix.column(j).to_owned();
for i in 0..j {
let q_col = q.column(i);
let proj = col.dot(&q_col);
col = &col - &(&q_col * proj);
r[[i, j]] = proj;
}
let norm = col.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-12 {
col /= norm;
r[[j, j]] = norm;
} else {
r[[j, j]] = 0.0;
}
q.column_mut(j).assign(&col);
}
Ok((q, r))
}
fn svd_small_matrix(
&self,
matrix: &Array2<f64>,
) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
if m == 0 || n == 0 {
return Ok((
Array2::zeros((m, 0)),
Array1::zeros(0),
Array2::zeros((0, n)),
));
}
let ata = matrix.t().dot(matrix);
let (eigenvals, eigenvecs) = self.symmetric_eigendecomposition(&ata)?;
let singular_values = eigenvals.mapv(|x| x.max(0.0).sqrt());
let vt = eigenvecs.t().to_owned();
let mut u = Array2::zeros((m, eigenvals.len()));
for (i, &sigma) in singular_values.iter().enumerate() {
if sigma > 1e-12 {
let v_col = eigenvecs.column(i);
let u_col = matrix.dot(&v_col) / sigma;
u.column_mut(i).assign(&u_col);
}
}
Ok((u, singular_values, vt))
}
fn symmetric_eigendecomposition(
&self,
matrix: &Array2<f64>,
) -> Result<(Array1<f64>, Array2<f64>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(TransformError::ComputationError(
"Matrix must be square".to_string(),
));
}
if n == 0 {
return Ok((Array1::zeros(0), Array2::zeros((0, 0))));
}
let a = matrix.clone(); let mut eigenvals = Array1::zeros(n);
let mut eigenvecs = Array2::eye(n);
if n == 1 {
eigenvals[0] = a[[0, 0]];
return Ok((eigenvals, eigenvecs));
}
if n == 2 {
let trace = a[[0, 0]] + a[[1, 1]];
let det = a[[0, 0]] * a[[1, 1]] - a[[0, 1]] * a[[1, 0]];
let discriminant = (trace * trace - 4.0 * det).sqrt();
eigenvals[0] = (trace + discriminant) / 2.0;
eigenvals[1] = (trace - discriminant) / 2.0;
if a[[0, 1]].abs() > 1e-12 {
let norm0 = (a[[0, 1]] * a[[0, 1]] + (eigenvals[0] - a[[0, 0]]).powi(2)).sqrt();
eigenvecs[[0, 0]] = a[[0, 1]] / norm0;
eigenvecs[[1, 0]] = (eigenvals[0] - a[[0, 0]]) / norm0;
eigenvecs[[0, 1]] = -eigenvecs[[1, 0]];
eigenvecs[[1, 1]] = eigenvecs[[0, 0]];
}
if eigenvals[1] > eigenvals[0] {
eigenvals.swap(0, 1);
let temp0 = eigenvecs.column(0).to_owned();
let temp1 = eigenvecs.column(1).to_owned();
eigenvecs.column_mut(0).assign(&temp1);
eigenvecs.column_mut(1).assign(&temp0);
}
return Ok((eigenvals, eigenvecs));
}
let mut matrix_work = a.clone();
for i in 0..n.min(self.n_components) {
let mut v = Array1::<f64>::ones(n);
v /= v.dot(&v).sqrt();
let mut eigenval = 0.0;
for _iter in 0..1000 {
let new_v = matrix_work.dot(&v);
eigenval = v.dot(&new_v); let norm = new_v.dot(&new_v).sqrt();
if norm < 1e-15 {
break;
}
let new_v_normalized = &new_v / norm;
let diff = (&new_v_normalized - &v)
.dot(&(&new_v_normalized - &v))
.sqrt();
v = new_v_normalized;
if diff < 1e-12 {
break;
}
}
eigenvals[i] = eigenval;
eigenvecs.column_mut(i).assign(&v);
let vv = v
.view()
.insert_axis(Axis(1))
.dot(&v.view().insert_axis(Axis(0)));
matrix_work = matrix_work - eigenval * vv;
}
Ok((eigenvals, eigenvecs))
}
fn fit_randomized_pca(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let _n_samples_n_features = x.dim();
let mean = if self.center {
Some(x.mean_axis(Axis(0)).expect("Operation failed"))
} else {
None
};
let x_centered = if let Some(ref m) = mean {
x - &m.view().insert_axis(Axis(0))
} else {
x.to_owned()
};
self.mean = mean;
self.fit_randomized_svd(&x_centered.view())
}
fn fit_randomized_svd(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let (n_samples, n_features) = x.dim();
let oversampling = if n_features > 1000 { 10 } else { 5 };
let sketch_size = (self.n_components + oversampling).min(n_features.min(n_samples));
let n_power_iterations = if n_features > 5000 { 2 } else { 1 };
let random_matrix = self.generate_random_gaussian_matrix(n_features, sketch_size)?;
let mut y = x.dot(&random_matrix);
for _ in 0..n_power_iterations {
let xty = x.t().dot(&y);
y = x.dot(&xty);
}
let (q, r) = self.qr_decomposition_chunked(&y)?;
let b = q.t().dot(x);
let (u_b, sigma, vt) = self.svd_small_matrix(&b)?;
let _u = q.dot(&u_b);
let k = self.n_components.min(sigma.len());
let components = vt.slice(scirs2_core::ndarray::s![..k, ..]).t().to_owned();
self.components = Some(components.t().to_owned());
let total_variance = sigma.iter().take(k).map(|&s| s * s).sum::<f64>();
if total_variance > 0.0 {
let explained_variance = sigma.slice(scirs2_core::ndarray::s![..k]).mapv(|s| s * s);
let variance_ratios = &explained_variance / total_variance;
self.explained_variance_ratio = Some(variance_ratios);
self.explained_variance = Some(explained_variance);
} else {
let uniform_variance = Array1::ones(k) / k as f64;
self.explained_variance_ratio = Some(uniform_variance.clone());
self.explained_variance = Some(uniform_variance);
}
Ok(())
}
fn generate_random_gaussian_matrix(&self, rows: usize, cols: usize) -> Result<Array2<f64>> {
let mut rng = scirs2_core::random::rng();
let mut random_matrix = Array2::zeros((rows, cols));
for i in 0..rows {
for j in 0..cols {
let u1 = rng.random_range(0.0..1.0);
let u2 = rng.random_range(0.0..1.0);
let u1 = if u1 == 0.0 { f64::EPSILON } else { u1 };
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
random_matrix[[i, j]] = z;
}
}
for j in 0..cols {
let mut col = random_matrix.column_mut(j);
let norm = col.dot(&col).sqrt();
if norm > f64::EPSILON {
col /= norm;
}
}
Ok(random_matrix)
}
fn fit_standard_pca(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let mean = if self.center {
Some(x.mean_axis(Axis(0)).expect("Operation failed"))
} else {
None
};
let x_centered = if let Some(ref m) = mean {
x - &m.view().insert_axis(Axis(0))
} else {
x.to_owned()
};
self.mean = mean;
self.fit_standard_pca_on_data(&x_centered.view())
}
fn fit_standard_pca_on_data(&mut self, x: &ArrayView2<f64>) -> Result<()> {
let (n_samples, n_features) = x.dim();
let cov = if n_samples > n_features {
let xt = x.t();
xt.dot(x) / (n_samples - 1) as f64
} else {
x.dot(&x.t()) / (n_samples - 1) as f64
};
let min_dim = n_features.min(n_samples);
let n_components = self.n_components.min(min_dim);
let (eigenvals, eigenvecs) = self.compute_top_eigenpairs(&cov, n_components)?;
let mut eigen_pairs: Vec<(f64, Array1<f64>)> = eigenvals
.iter()
.zip(eigenvecs.columns())
.map(|(&val, vec)| (val, vec.to_owned()))
.collect();
eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let explained_variance = Array1::from_iter(eigen_pairs.iter().map(|(val_, _)| *val_));
let mut components = Array2::zeros((n_components, cov.ncols()));
for (i, (_, eigenvec)) in eigen_pairs.iter().enumerate() {
components.row_mut(i).assign(eigenvec);
}
self.components = Some(components.t().to_owned());
self.explained_variance = Some(explained_variance);
Ok(())
}
pub fn transform(&self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
let components = self
.components
.as_ref()
.ok_or_else(|| TransformError::NotFitted("PCA not fitted".to_string()))?;
check_not_empty(x, "x")?;
for &val in x.iter() {
if !val.is_finite() {
return Err(crate::error::TransformError::DataValidationError(
"Data contains non-finite values".to_string(),
));
}
}
let x_processed = if let Some(ref mean) = self.mean {
x - &mean.view().insert_axis(Axis(0))
} else {
x.to_owned()
};
let transformed = if components.shape()[1] == x_processed.shape()[1] {
x_processed.dot(&components.t())
} else if components.shape()[0] == x_processed.shape()[1] {
x_processed.dot(components)
} else {
return Err(crate::error::TransformError::InvalidInput(format!(
"Component dimensions {:?} are incompatible with data dimensions {:?}",
components.shape(),
x_processed.shape()
)));
};
Ok(transformed)
}
pub fn fit_transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
self.fit(x)?;
self.transform(x)
}
pub fn explained_variance_ratio(&self) -> Option<Array1<f64>> {
self.explained_variance.as_ref().map(|ev| {
let total_var = ev.sum();
ev / total_var
})
}
pub fn components(&self) -> Option<&Array2<f64>> {
self.components.as_ref()
}
pub fn processing_strategy(&self) -> &ProcessingStrategy {
&self.strategy
}
fn compute_top_eigenpairs(
&self,
matrix: &Array2<f64>,
n_components: usize,
) -> Result<(Array1<f64>, Array2<f64>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(TransformError::ComputationError(
"Matrix must be square for eigendecomposition".to_string(),
));
}
let mut eigenvalues = Array1::zeros(n_components);
let mut eigenvectors = Array2::zeros((n, n_components));
let mut working_matrix = matrix.clone();
for k in 0..n_components {
let (eigenval, eigenvec) = self.power_iteration(&working_matrix)?;
eigenvalues[k] = eigenval;
eigenvectors.column_mut(k).assign(&eigenvec);
let outer_product = &eigenvec
.view()
.insert_axis(Axis(1))
.dot(&eigenvec.view().insert_axis(Axis(0)));
working_matrix = &working_matrix - &(eigenval * outer_product);
}
Ok((eigenvalues, eigenvectors))
}
fn power_iteration(&self, matrix: &Array2<f64>) -> Result<(f64, Array1<f64>)> {
let n = matrix.nrows();
let max_iterations = 1000;
let tolerance = 1e-10;
use scirs2_core::random::{Rng, RngExt};
let mut rng = scirs2_core::random::rng();
let mut vector: Array1<f64> =
Array1::from_shape_fn(n, |_| rng.random_range(0.0..1.0) - 0.5);
let norm = vector.dot(&vector).sqrt();
if norm > f64::EPSILON {
vector /= norm;
} else {
vector = Array1::zeros(n);
vector[0] = 1.0;
}
let mut eigenvalue = 0.0;
let mut prev_eigenvalue = 0.0;
for iteration in 0..max_iterations {
let new_vector = matrix.dot(&vector);
let numerator = vector.dot(&new_vector);
let denominator = vector.dot(&vector);
if denominator < f64::EPSILON {
return Err(TransformError::ComputationError(
"Vector became zero during power iteration".to_string(),
));
}
eigenvalue = numerator / denominator;
let norm = new_vector.dot(&new_vector).sqrt();
if norm > f64::EPSILON {
vector = new_vector / norm;
} else {
break;
}
if iteration > 0 && (eigenvalue - prev_eigenvalue).abs() < tolerance {
break;
}
prev_eigenvalue = eigenvalue;
}
let norm = vector.dot(&vector).sqrt();
if norm > f64::EPSILON {
vector /= norm;
}
Ok((eigenvalue, vector))
}
#[allow(dead_code)]
fn qr_eigendecomposition(
&self,
matrix: &Array2<f64>,
n_components: usize,
) -> Result<(Array1<f64>, Array2<f64>)> {
let n = matrix.nrows();
if n != matrix.ncols() {
return Err(TransformError::ComputationError(
"Matrix must be square for QR eigendecomposition".to_string(),
));
}
if n > 100 {
return self.compute_top_eigenpairs(matrix, n_components);
}
let max_iterations = 100;
let tolerance = 1e-12;
let mut a = matrix.clone();
let mut q_total = Array2::eye(n);
for _iteration in 0..max_iterations {
let (q, r) = self.qr_decomposition(&a)?;
a = r.dot(&q);
q_total = q_total.dot(&q);
let mut off_diagonal_norm = 0.0;
for i in 0..n {
for j in 0..n {
if i != j {
off_diagonal_norm += a[[i, j]] * a[[i, j]];
}
}
}
if off_diagonal_norm.sqrt() < tolerance {
break;
}
}
let eigenvals: Vec<f64> = (0..n).map(|i| a[[i, i]]).collect();
let eigenvecs = q_total;
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&i, &j| {
eigenvals[j]
.partial_cmp(&eigenvals[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let top_eigenvals =
Array1::from_iter(indices.iter().take(n_components).map(|&i| eigenvals[i]));
let mut top_eigenvecs = Array2::zeros((n, n_components));
for (k, &i) in indices.iter().take(n_components).enumerate() {
top_eigenvecs.column_mut(k).assign(&eigenvecs.column(i));
}
Ok((top_eigenvals, top_eigenvecs))
}
#[allow(dead_code)]
fn qr_decomposition(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
let mut q = Array2::zeros((m, n));
let mut r = Array2::zeros((n, n));
for j in 0..n {
let mut v = matrix.column(j).to_owned();
for i in 0..j {
let q_i = q.column(i);
let projection = q_i.dot(&v);
r[[i, j]] = projection;
v = v - projection * &q_i;
}
let norm = v.dot(&v).sqrt();
if norm > f64::EPSILON {
r[[j, j]] = norm;
q.column_mut(j).assign(&(&v / norm));
} else {
r[[j, j]] = 0.0;
q.column_mut(j).fill(0.0);
}
}
Ok((q, r))
}
fn qr_decomposition_full(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
let mut q = Array2::zeros((m, m)); let mut r = Array2::zeros((m, n));
let (q_reduced, r_reduced) = self.qr_decomposition(matrix)?;
q.slice_mut(scirs2_core::ndarray::s![.., ..n])
.assign(&q_reduced);
r.slice_mut(scirs2_core::ndarray::s![..n, ..])
.assign(&r_reduced);
for j in n..m {
let mut v = Array1::zeros(m);
v[j] = 1.0;
for i in 0..j {
let q_i = q.column(i);
let projection = q_i.dot(&v);
v = v - projection * &q_i;
}
let norm = v.dot(&v).sqrt();
if norm > f64::EPSILON {
q.column_mut(j).assign(&(&v / norm));
}
}
Ok((q, r))
}
}
pub struct AdvancedMemoryPool {
matrix_pools: std::collections::HashMap<(usize, usize), Vec<Array2<f64>>>,
vector_pools: std::collections::HashMap<usize, Vec<Array1<f64>>>,
max_matrices_per_size: usize,
max_vectors_per_size: usize,
stats: PoolStats,
}
#[derive(Debug, Clone)]
pub struct PoolStats {
pub total_allocations: usize,
pub pool_hits: usize,
pub pool_misses: usize,
pub total_memory_mb: f64,
pub peak_memory_mb: f64,
pub current_matrices: usize,
pub current_vectors: usize,
pub transform_count: u64,
pub total_transform_time_ns: u64,
pub throughput_samples_per_sec: f64,
pub cache_hit_rate: f64,
}
impl AdvancedMemoryPool {
pub fn new(max_matrices: usize, max_vectors: usize, initialcapacity: usize) -> Self {
let mut pool = AdvancedMemoryPool {
matrix_pools: std::collections::HashMap::with_capacity(initialcapacity),
vector_pools: std::collections::HashMap::with_capacity(initialcapacity),
max_matrices_per_size: max_matrices,
max_vectors_per_size: max_vectors,
stats: PoolStats {
total_allocations: 0,
pool_hits: 0,
pool_misses: 0,
total_memory_mb: 0.0,
peak_memory_mb: 0.0,
current_matrices: 0,
current_vectors: 0,
transform_count: 0,
total_transform_time_ns: 0,
throughput_samples_per_sec: 0.0,
cache_hit_rate: 0.0,
},
};
pool.prewarm_common_sizes();
pool
}
fn prewarm_common_sizes(&mut self) {
let common_matrix_sizes = vec![
(100, 10),
(500, 20),
(1000, 50),
(5000, 100),
(10000, 200),
(50000, 500),
];
for (rows, cols) in common_matrix_sizes {
let pool = self.matrix_pools.entry((rows, cols)).or_default();
for _ in 0..(self.max_matrices_per_size / 4) {
pool.push(Array2::zeros((rows, cols)));
self.stats.current_matrices += 1;
}
}
let common_vector_sizes = vec![10, 20, 50, 100, 200, 500, 1000, 5000];
for size in common_vector_sizes {
let pool = self.vector_pools.entry(size).or_default();
for _ in 0..(self.max_vectors_per_size / 4) {
pool.push(Array1::zeros(size));
self.stats.current_vectors += 1;
}
}
self.update_memory_stats();
}
pub fn get_matrix(&mut self, rows: usize, cols: usize) -> Array2<f64> {
self.stats.total_allocations += 1;
if let Some(pool) = self.matrix_pools.get_mut(&(rows, cols)) {
if let Some(mut matrix) = pool.pop() {
matrix.fill(0.0);
self.stats.pool_hits += 1;
self.stats.current_matrices -= 1;
return matrix;
}
}
self.stats.pool_misses += 1;
Array2::zeros((rows, cols))
}
pub fn get_vector(&mut self, size: usize) -> Array1<f64> {
self.stats.total_allocations += 1;
if let Some(pool) = self.vector_pools.get_mut(&size) {
if let Some(mut vector) = pool.pop() {
vector.fill(0.0);
self.stats.pool_hits += 1;
self.stats.current_vectors -= 1;
return vector;
}
}
self.stats.pool_misses += 1;
Array1::zeros(size)
}
pub fn return_matrix(&mut self, matrix: Array2<f64>) {
let shape = (matrix.nrows(), matrix.ncols());
let pool = self.matrix_pools.entry(shape).or_default();
if pool.len() < self.max_matrices_per_size {
pool.push(matrix);
self.stats.current_matrices += 1;
self.update_memory_stats();
}
}
pub fn return_vector(&mut self, vector: Array1<f64>) {
let size = vector.len();
let pool = self.vector_pools.entry(size).or_default();
if pool.len() < self.max_vectors_per_size {
pool.push(vector);
self.stats.current_vectors += 1;
self.update_memory_stats();
}
}
fn update_memory_stats(&mut self) {
let mut total_memory = 0.0;
for ((rows, cols), pool) in &self.matrix_pools {
total_memory += (rows * cols * 8 * pool.len()) as f64; }
for (size, pool) in &self.vector_pools {
total_memory += (size * 8 * pool.len()) as f64; }
self.stats.total_memory_mb = total_memory / (1024.0 * 1024.0);
if self.stats.total_memory_mb > self.stats.peak_memory_mb {
self.stats.peak_memory_mb = self.stats.total_memory_mb;
}
self.update_cache_hit_rate();
}
pub fn stats(&self) -> &PoolStats {
&self.stats
}
pub fn efficiency(&self) -> f64 {
if self.stats.total_allocations == 0 {
0.0
} else {
self.stats.pool_hits as f64 / self.stats.total_allocations as f64
}
}
fn update_cache_hit_rate(&mut self) {
self.stats.cache_hit_rate = self.efficiency();
}
pub fn update_stats(&mut self, transform_time_ns: u64, samplesprocessed: usize) {
self.stats.transform_count += 1;
self.stats.total_transform_time_ns += transform_time_ns;
if self.stats.transform_count > 0 {
let avg_time_per_transform =
self.stats.total_transform_time_ns / self.stats.transform_count;
if avg_time_per_transform > 0 {
self.stats.throughput_samples_per_sec =
(samplesprocessed as f64) / (avg_time_per_transform as f64 / 1_000_000_000.0);
}
}
self.update_memory_stats();
}
pub fn clear(&mut self) {
self.matrix_pools.clear();
self.vector_pools.clear();
self.stats.current_matrices = 0;
self.stats.current_vectors = 0;
self.update_memory_stats();
}
pub fn adaptive_resize(&mut self) {
let efficiency = self.efficiency();
if efficiency > 0.8 {
self.max_matrices_per_size = (self.max_matrices_per_size as f32 * 1.2) as usize;
self.max_vectors_per_size = (self.max_vectors_per_size as f32 * 1.2) as usize;
} else if efficiency < 0.3 {
self.max_matrices_per_size = (self.max_matrices_per_size as f32 * 0.8) as usize;
self.max_vectors_per_size = (self.max_vectors_per_size as f32 * 0.8) as usize;
for pool in self.matrix_pools.values_mut() {
pool.truncate(self.max_matrices_per_size);
}
for pool in self.vector_pools.values_mut() {
pool.truncate(self.max_vectors_per_size);
}
}
self.update_memory_stats();
}
pub fn get_array(&mut self, rows: usize, cols: usize) -> Array2<f64> {
self.get_matrix(rows, cols)
}
pub fn return_array(&mut self, array: Array2<f64>) {
self.return_matrix(array);
}
pub fn get_temp_array(&mut self, size: usize) -> Array1<f64> {
self.get_vector(size)
}
pub fn return_temp_array(&mut self, temp: Array1<f64>) {
self.return_vector(temp);
}
pub fn optimize(&mut self) {
self.adaptive_resize();
}
}
pub struct AdvancedPCA {
enhanced_pca: EnhancedPCA,
memory_pool: AdvancedMemoryPool,
processing_cache: std::collections::HashMap<(usize, usize), CachedPCAResult>,
}
#[derive(Clone)]
struct CachedPCAResult {
#[allow(dead_code)]
components: Array2<f64>,
#[allow(dead_code)]
explained_variance_ratio: Array1<f64>,
data_hash: u64,
timestamp: std::time::Instant,
}
impl AdvancedPCA {
pub fn new(_n_components: usize, _n_samples_hint: usize, hint: usize) -> Self {
let enhanced_pca = EnhancedPCA::new(_n_components, true, 1024).expect("Operation failed");
let memory_pool = AdvancedMemoryPool::new(
100, 200, 20, );
AdvancedPCA {
enhanced_pca,
memory_pool,
processing_cache: std::collections::HashMap::new(),
}
}
pub fn fit(&mut self, x: &ArrayView2<f64>) -> Result<()> {
self.enhanced_pca.fit(x)
}
pub fn fit_transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
self.enhanced_pca.fit_transform(x)
}
pub fn components(&self) -> Option<&Array2<f64>> {
self.enhanced_pca.components()
}
pub fn mean(&self) -> Option<&Array1<f64>> {
self.enhanced_pca.mean.as_ref()
}
pub fn explained_variance_ratio(&self) -> Result<Array1<f64>> {
self.enhanced_pca.explained_variance_ratio().ok_or_else(|| {
TransformError::NotFitted(
"PCA must be fitted before getting explained variance ratio".to_string(),
)
})
}
pub fn fast_transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
let (n_samples, n_features) = x.dim();
let data_hash = self.compute_data_hash(x);
if let Some(cached) = self.processing_cache.get(&(n_samples, n_features)) {
if cached.data_hash == data_hash && cached.timestamp.elapsed().as_secs() < 300 {
let result = self
.memory_pool
.get_matrix(n_samples, self.enhanced_pca.n_components);
return Ok(result);
}
}
let result = self.enhanced_pca.transform(x)?;
if let (Some(components), Some(explained_variance_ratio)) = (
self.enhanced_pca.components().cloned(),
self.enhanced_pca.explained_variance_ratio(),
) {
self.processing_cache.insert(
(n_samples, n_features),
CachedPCAResult {
components,
explained_variance_ratio,
data_hash,
timestamp: std::time::Instant::now(),
},
);
}
Ok(result)
}
fn compute_data_hash(&self, x: &ArrayView2<f64>) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
x.shape().hash(&mut hasher);
let (n_samples, n_features) = x.dim();
let sample_step = ((n_samples * n_features) / 1000).max(1);
for (i, &val) in x.iter().step_by(sample_step).enumerate() {
if i > 1000 {
break;
} (val.to_bits()).hash(&mut hasher);
}
hasher.finish()
}
pub fn performance_stats(&self) -> &PoolStats {
self.memory_pool.stats()
}
pub fn cleanup_cache(&mut self) {
let now = std::time::Instant::now();
self.processing_cache.retain(|_, cached| {
now.duration_since(cached.timestamp).as_secs() < 1800 });
}
pub fn transform(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
let start_time = std::time::Instant::now();
let result = self.enhanced_pca.transform(x)?;
let duration = start_time.elapsed();
let samples = x.shape()[0];
self.memory_pool
.update_stats(duration.as_nanos() as u64, samples);
Ok(result)
}
pub fn qr_decomposition_optimized(
&self,
matrix: &Array2<f64>,
) -> Result<(Array2<f64>, Array2<f64>)> {
self.enhanced_pca.qr_decomposition_full(matrix)
}
pub fn svd_small_matrix(
&self,
matrix: &Array2<f64>,
) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
self.enhanced_pca.svd_small_matrix(matrix)
}
}
#[cfg(test)]
#[path = "performance_tests.rs"]
mod tests;