use ndarray::{Array1, Array2};
use single_utilities::traits::FloatOpsTS;
pub trait SvdFloat:
FloatOpsTS + ndarray::ScalarOperand + sprs::MulAcc + std::ops::DivAssign + 'static
{
fn eps() -> Self;
fn eps34() -> Self;
fn to_f64(self) -> f64;
fn from_f64_val(v: f64) -> Self;
fn close(a: Self, b: Self) -> bool {
num_traits::Float::abs(b - a) < Self::eps()
}
}
impl SvdFloat for f32 {
#[inline]
fn eps() -> Self {
f32::EPSILON
}
#[inline]
fn eps34() -> Self {
const V: f32 = 6.4155306e-6;
V
}
#[inline]
fn to_f64(self) -> f64 {
self as f64
}
#[inline]
fn from_f64_val(v: f64) -> Self {
v as f32
}
}
impl SvdFloat for f64 {
#[inline]
fn eps() -> Self {
f64::EPSILON
}
#[inline]
fn eps34() -> Self {
const V: f64 = 1.8189894035458565e-12;
V
}
#[inline]
fn to_f64(self) -> f64 {
self
}
#[inline]
fn from_f64_val(v: f64) -> Self {
v
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
Las2,
Irlba,
Randomized,
BlockKrylov,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Detail<T> {
Lanczos {
iterations: usize,
lanczos_steps: usize,
ritz_values_stabilized: usize,
end_interval: [T; 2],
kappa: T,
},
Irlba {
restarts: usize,
converged: bool,
tolerance: T,
max_residual: T,
},
Randomized {
oversamples: usize,
power_iterations: usize,
block_size: usize,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostics<T> {
pub algorithm: Algorithm,
pub non_zero: usize,
pub dimensions: usize,
pub significant_values: usize,
pub transposed: bool,
pub random_seed: u64,
pub matvecs: usize,
pub detail: Detail<T>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SvdRec<T> {
pub d: usize,
pub u: Array2<T>,
pub s: Array1<T>,
pub vt: Array2<T>,
pub total_squared_norm: T,
pub diagnostics: Diagnostics<T>,
}
impl<T: SvdFloat> SvdRec<T> {
pub fn recompose(&self) -> Array2<T> {
let scaled = &self.u * &self.s.view().insert_axis(ndarray::Axis(0));
scaled.dot(&self.vt)
}
pub fn converged(&self) -> bool {
match self.diagnostics.detail {
Detail::Irlba { converged, .. } => converged,
Detail::Lanczos { .. } | Detail::Randomized { .. } => true,
}
}
pub fn max_residual(&self) -> Option<T> {
match self.diagnostics.detail {
Detail::Irlba { max_residual, .. } => Some(max_residual),
_ => None,
}
}
pub fn scores(&self) -> Array2<T> {
&self.u * &self.s.view().insert_axis(ndarray::Axis(0))
}
pub fn explained_variance(&self) -> Array1<T> {
let denom = self.variance_denominator();
self.s.mapv(|si| si * si / denom)
}
pub fn explained_variance_ratio(&self) -> Array1<T> {
if self.total_squared_norm <= T::zero() {
return Array1::zeros(self.s.len());
}
self.s.mapv(|si| si * si / self.total_squared_norm)
}
pub fn total_variance(&self) -> T {
self.total_squared_norm / self.variance_denominator()
}
fn variance_denominator(&self) -> T {
T::from_f64_val(self.nrows().saturating_sub(1).max(1) as f64)
}
pub fn nrows(&self) -> usize {
self.u.nrows()
}
pub fn ncols(&self) -> usize {
self.vt.ncols()
}
pub fn truncate(&mut self, k: usize) {
let k = k.min(self.d);
if k == self.d {
return;
}
self.u = self.u.slice(ndarray::s![.., ..k]).to_owned();
self.s = self.s.slice(ndarray::s![..k]).to_owned();
self.vt = self.vt.slice(ndarray::s![..k, ..]).to_owned();
self.d = k;
self.diagnostics.significant_values = k;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eps34_constants_match_computed() {
approx::assert_relative_eq!(f32::eps34(), f32::EPSILON.powf(0.75), max_relative = 1e-6);
approx::assert_relative_eq!(f64::eps34(), f64::EPSILON.powf(0.75), max_relative = 1e-12);
}
#[test]
fn recompose_is_orientation_correct_for_non_square() {
let u = ndarray::arr2(&[[1.0f64], [2.0], [3.0]]);
let s = ndarray::arr1(&[2.0f64]);
let vt = ndarray::arr2(&[[1.0f64, 10.0]]);
let rec = SvdRec {
d: 1,
u,
s,
vt,
total_squared_norm: 0.0,
diagnostics: Diagnostics {
algorithm: Algorithm::Las2,
non_zero: 6,
dimensions: 1,
significant_values: 1,
transposed: false,
random_seed: 0,
matvecs: 0,
detail: Detail::Lanczos {
iterations: 0,
lanczos_steps: 0,
ritz_values_stabilized: 0,
end_interval: [0.0, 0.0],
kappa: 0.0,
},
},
};
let a = rec.recompose();
assert_eq!(a.dim(), (3, 2));
assert_eq!(a[[0, 0]], 2.0);
assert_eq!(a[[0, 1]], 20.0);
assert_eq!(a[[2, 0]], 6.0);
assert_eq!(a[[2, 1]], 60.0);
}
}