use crate::error::Error;
use crate::math::matmul::gemm_par_auto;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::{cheap_map_f64_parallel_threshold, sum_f64_parallel_min_elems};
use crate::{Deserialize, Serialize};
use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix2};
use ndarray_rand::rand::rngs::StdRng;
use ndarray_rand::rand::{Rng, SeedableRng};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
pub enum SVDSolver {
#[default]
Full,
Randomized(u64),
PowerIteration,
}
impl SVDSolver {
fn compute_components(
&self,
x_centered: &Array2<f64>,
n_components: usize,
) -> Result<(Array2<f64>, Array1<f64>), Error> {
match *self {
SVDSolver::Full => Self::full_svd(x_centered, n_components),
SVDSolver::Randomized(seed) => Self::randomized_svd(x_centered, n_components, seed),
SVDSolver::PowerIteration => Self::power_iteration_svd(x_centered, n_components),
}
}
fn full_svd(
x_centered: &Array2<f64>,
n_components: usize,
) -> Result<(Array2<f64>, Array1<f64>), Error> {
let n_samples = x_centered.nrows();
let n_features = x_centered.ncols();
let x_slice = x_centered
.as_slice()
.ok_or_else(|| Error::computation("Failed to convert centered data to slice"))?;
let x_mat = nalgebra::DMatrix::from_row_slice(n_samples, n_features, x_slice);
let svd = nalgebra::linalg::SVD::new(x_mat, false, true);
let v_t = svd
.v_t
.ok_or_else(|| Error::computation("SVD did not compute V^T matrix"))?;
let singular_values: Vec<f64> = svd
.singular_values
.iter()
.take(n_components)
.cloned()
.collect();
let components =
Array2::<f64>::from_shape_fn((n_components, n_features), |(i, j)| v_t[(i, j)]);
Ok((components, Array1::from_vec(singular_values)))
}
fn randomized_svd(
x_centered: &Array2<f64>,
n_components: usize,
seed: u64,
) -> Result<(Array2<f64>, Array1<f64>), Error> {
let n_samples = x_centered.nrows();
let n_features = x_centered.ncols();
let max_rank = n_samples.min(n_features);
let oversampling = 5usize;
let k = (n_components + oversampling).min(max_rank);
let mut rng = StdRng::seed_from_u64(seed);
let mut omega = Vec::with_capacity(n_features * k);
for _ in 0..(n_features * k) {
omega.push(rng.random_range(-1.0..1.0));
}
let x_slice = x_centered
.as_slice()
.ok_or_else(|| Error::computation("Failed to convert centered data to slice"))?;
let x_mat = nalgebra::DMatrix::from_row_slice(n_samples, n_features, x_slice);
let omega_mat = nalgebra::DMatrix::from_row_slice(n_features, k, &omega);
let mut q = nalgebra::linalg::QR::new(&x_mat * &omega_mat).q();
let x_t = x_mat.transpose();
let n_iter = 2usize;
for _ in 0..n_iter {
let w = nalgebra::linalg::QR::new(&x_t * &q).q();
q = nalgebra::linalg::QR::new(&x_mat * &w).q();
}
let b = &q.transpose() * &x_mat;
let svd = nalgebra::linalg::SVD::new(b, false, true);
let v_t = svd
.v_t
.ok_or_else(|| Error::computation("Randomized SVD did not compute V^T matrix"))?;
let singular_values: Vec<f64> = svd
.singular_values
.iter()
.take(n_components)
.cloned()
.collect();
let components =
Array2::<f64>::from_shape_fn((n_components, n_features), |(i, j)| v_t[(i, j)]);
Ok((components, Array1::from_vec(singular_values)))
}
fn power_iteration_svd(
x_centered: &Array2<f64>,
n_components: usize,
) -> Result<(Array2<f64>, Array1<f64>), Error> {
let n_samples = x_centered.nrows();
let n_features = x_centered.ncols();
let denom = (n_samples - 1) as f64;
let cov = gemm_par_auto(&x_centered.t(), x_centered) / denom;
let (eigenvalues, eigenvectors) =
super::linalg::top_eigenpairs_power_iteration(cov, n_components, 0, 1000, 1e-6)?;
let mut components = Array2::<f64>::zeros((n_components, n_features));
for (idx, eigenvector) in eigenvectors.iter().enumerate() {
components.row_mut(idx).assign(eigenvector);
}
let singular_values: Vec<f64> = eigenvalues
.into_iter()
.map(|lambda| {
let clamped = if lambda.is_finite() && lambda > 0.0 {
lambda
} else {
0.0
};
(clamped * denom).sqrt()
})
.collect();
Ok((components, Array1::from_vec(singular_values)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PCA {
n_components: usize,
svd_solver: SVDSolver,
mean: Option<Array1<f64>>,
components: Option<Array2<f64>>,
explained_variance: Option<Array1<f64>>,
explained_variance_ratio: Option<Array1<f64>>,
singular_values: Option<Array1<f64>>,
n_samples: Option<usize>,
n_features: Option<usize>,
}
impl Default for PCA {
fn default() -> Self {
PCA::new(2).expect("Default PCA parameters should be valid")
}
}
impl PCA {
pub fn new(n_components: usize) -> Result<Self, Error> {
if n_components == 0 {
return Err(Error::invalid_parameter(
"n_components",
"must be greater than 0",
));
}
Ok(Self {
n_components,
svd_solver: SVDSolver::Full,
mean: None,
components: None,
explained_variance: None,
explained_variance_ratio: None,
singular_values: None,
n_samples: None,
n_features: None,
})
}
pub fn with_svd_solver(mut self, svd_solver: SVDSolver) -> Self {
self.svd_solver = svd_solver;
self
}
get_field!(get_n_components, n_components, usize);
get_field!(get_svd_solver, svd_solver, SVDSolver);
get_field!(get_n_samples, n_samples, Option<usize>);
get_field!(get_n_features, n_features, Option<usize>);
get_field_as_ref!(get_mean, mean, Option<&Array1<f64>>);
get_field_as_ref!(get_components, components, Option<&Array2<f64>>);
get_field_as_ref!(
get_explained_variance,
explained_variance,
Option<&Array1<f64>>
);
get_field_as_ref!(
get_explained_variance_ratio,
explained_variance_ratio,
Option<&Array1<f64>>
);
get_field_as_ref!(get_singular_values, singular_values, Option<&Array1<f64>>);
pub fn fit<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
self.fit_internal(x)
}
pub fn transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
self.transform_internal(x)
}
pub fn fit_transform<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
2,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Stage: {msg}",
);
pb.set_message("Fitting model");
pb
};
self.fit_internal(x)?;
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Transforming data");
}
let transformed = self.transform_internal(x)?;
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.finish_with_message("Completed");
}
Ok(transformed)
}
pub fn inverse_transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
let components = self
.components
.as_ref()
.ok_or_else(|| Error::not_fitted("PCA"))?;
let mean = self.mean.as_ref().ok_or_else(|| Error::not_fitted("PCA"))?;
super::validation::check_non_empty(x)?;
if x.ncols() != components.nrows() {
return Err(Error::dimension_mismatch(components.nrows(), x.ncols()));
}
super::validation::check_finite(x)?;
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
3,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Stage: {msg}",
);
pb.set_message("Validating input");
pb
};
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Reconstructing data");
}
let mut reconstructed = gemm_par_auto(x, components);
reconstructed += mean;
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Finalizing output");
progress_bar.inc(1);
progress_bar.finish_with_message("Completed");
}
Ok(reconstructed)
}
fn fit_internal<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
super::validation::validate_fit_matrix(x)?;
super::validation::check_min_samples(x, 2, "PCA")?;
let n_samples = x.nrows();
let n_features = x.ncols();
let max_components = n_samples.min(n_features);
if self.n_components > max_components {
return Err(Error::invalid_parameter(
"n_components",
format!("should be <= {}, got {}", max_components, self.n_components),
));
}
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
5,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Stage: {msg}",
);
pb.set_message("Validating input");
pb
};
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Centering data");
}
let mut x_centered = x.to_owned();
let mean = Self::compute_mean(&x_centered);
Self::center_data(&mut x_centered, &mean);
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Computing decomposition");
}
let (mut components, singular_values) = self
.svd_solver
.compute_components(&x_centered, self.n_components)?;
Self::flip_component_signs(&mut components);
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Computing explained variance");
}
let explained_variance = singular_values.mapv(|s| (s * s) / ((n_samples - 1) as f64));
let total_variance = Self::total_variance(&x_centered, n_samples)?;
let explained_variance_ratio = if total_variance > 0.0 && total_variance.is_finite() {
explained_variance.mapv(|v| v / total_variance)
} else {
Array1::zeros(self.n_components)
};
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Finalizing model state");
}
self.mean = Some(mean);
self.components = Some(components);
self.explained_variance = Some(explained_variance);
self.explained_variance_ratio = Some(explained_variance_ratio);
self.singular_values = Some(singular_values);
self.n_samples = Some(n_samples);
self.n_features = Some(n_features);
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.finish_with_message("Completed");
}
Ok(self)
}
fn transform_internal<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
let components = self
.components
.as_ref()
.ok_or_else(|| Error::not_fitted("PCA"))?;
let mean = self.mean.as_ref().ok_or_else(|| Error::not_fitted("PCA"))?;
super::validation::validate_transform_matrix(x, components.ncols())?;
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
3,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Stage: {msg}",
);
pb.set_message("Validating input");
pb
};
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Centering data");
}
let mut x_centered = x.to_owned();
Self::center_data(&mut x_centered, mean);
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.set_message("Projecting data");
}
let transformed = gemm_par_auto(&x_centered, &components.t());
#[cfg(feature = "show_progress")]
{
progress_bar.inc(1);
progress_bar.finish_with_message("Completed");
}
Ok(transformed)
}
fn compute_mean(x: &Array2<f64>) -> Array1<f64> {
x.mean_axis(Axis(0)).expect("Input data must be non-empty")
}
fn center_data(x: &mut Array2<f64>, mean: &Array1<f64>) {
if x.len() >= cheap_map_f64_parallel_threshold() {
let mean = mean.to_owned();
x.axis_iter_mut(Axis(0))
.into_par_iter()
.for_each(|mut row| {
row -= &mean;
});
} else {
for mut row in x.axis_iter_mut(Axis(0)) {
row -= mean;
}
}
}
fn flip_component_signs(components: &mut Array2<f64>) {
for mut row in components.axis_iter_mut(Axis(0)) {
let mut max_idx = 0;
let mut max_abs = 0.0;
for (j, &v) in row.iter().enumerate() {
let abs = v.abs();
if abs > max_abs {
max_abs = abs;
max_idx = j;
}
}
if row[max_idx] < 0.0 {
row.mapv_inplace(|x| -x);
}
}
}
fn total_variance(x_centered: &Array2<f64>, n_samples: usize) -> Result<f64, Error> {
let denom = (n_samples - 1) as f64;
if denom <= 0.0 {
return Err(Error::computation(
"Variance computation requires at least 2 samples",
));
}
let sum_sq = match x_centered.as_slice() {
Some(slice) => det_reduce(
slice,
slice.len() >= sum_f64_parallel_min_elems(),
|block| block.iter().map(|v| v * v).sum::<f64>(),
|a, b| a + b,
0.0,
),
_ => x_centered.iter().map(|v| v * v).sum::<f64>(),
};
Ok(sum_sq / denom)
}
model_save_and_load_methods!(PCA);
}