use super::{DimensionMismatch, mul_matrix, mul_vector, n_as_scalar, qr_householder};
use crate::scalar::{FloatTolerance, Scalar};
use crate::storage::Storage;
pub(in crate::algorithm::matrix) const QR_ITERATIONS: usize = 100;
struct Slice<'a, T> {
data: &'a [T],
}
impl<T> Storage for Slice<'_, T> {
type Item = T;
fn len(&self) -> usize {
self.data.len()
}
fn get(&self, index: usize) -> Option<&Self::Item> {
self.data.get(index)
}
}
struct StridedColumn<'a, T> {
data: &'a [T],
col: usize,
stride: usize,
len: usize,
}
impl<T> Storage for StridedColumn<'_, T> {
type Item = T;
fn len(&self) -> usize {
self.len
}
fn get(&self, index: usize) -> Option<&Self::Item> {
if index >= self.len {
return None;
}
self.data.get(index * self.stride + self.col)
}
}
pub fn svd<S, T>(
a: &S,
rows: usize,
cols: usize,
out_u: &mut [T],
out_sigma: &mut [T],
out_v: &mut [T],
scratch: &mut [T],
) -> Result<(), DimensionMismatch>
where
S: Storage<Item = T>,
T: Scalar + FloatTolerance + PartialOrd,
{
let tolerance = n_as_scalar::<T>(rows.max(cols) * QR_ITERATIONS).mul(T::epsilon());
svd_qr_iteration(a, rows, cols, out_u, out_sigma, out_v, scratch, tolerance)
}
#[allow(clippy::too_many_arguments)]
pub fn svd_qr_iteration<S, T>(
a: &S,
rows: usize,
cols: usize,
out_u: &mut [T],
out_sigma: &mut [T],
out_v: &mut [T],
scratch: &mut [T],
tolerance: T,
) -> Result<(), DimensionMismatch>
where
S: Storage<Item = T>,
T: Scalar + PartialOrd,
{
let m = rows;
let n = cols;
let nn = n * n;
if a.len() != m * n
|| out_u.len() != m * n
|| out_sigma.len() != n
|| out_v.len() != nn
|| scratch.len() != 5 * nn + n + m
{
return Err(DimensionMismatch);
}
let (m_a, rest) = scratch.split_at_mut(nn);
let (m_b, rest) = rest.split_at_mut(nn);
let (q_buf, rest) = rest.split_at_mut(nn);
let (r_buf, rest) = rest.split_at_mut(nn);
let (v_buf, rest) = rest.split_at_mut(nn);
let (householder_scratch, av_scratch) = rest.split_at_mut(n);
let zero = T::zero();
let one = T::one();
for i in 0..n {
for j in 0..n {
let mut sum = zero;
for k in 0..m {
let (Some(&a_ki), Some(&a_kj)) = (a.get(k * n + i), a.get(k * n + j)) else {
return Err(DimensionMismatch);
};
sum = sum.add(a_ki.mul(a_kj));
}
m_a[i * n + j] = sum;
}
}
for slot in out_v.iter_mut() {
*slot = zero;
}
for i in 0..n {
out_v[i * n + i] = one;
}
let mut m_cur = m_a;
let mut m_nxt = m_b;
let mut v_cur = out_v;
let mut v_nxt = v_buf;
for _ in 0..QR_ITERATIONS {
qr_householder(
&Slice { data: m_cur },
n,
n,
q_buf,
r_buf,
householder_scratch,
)?;
mul_matrix(
&Slice { data: r_buf },
n,
n,
&Slice { data: q_buf },
n,
n,
m_nxt,
)?;
mul_matrix(
&Slice { data: v_cur },
n,
n,
&Slice { data: q_buf },
n,
n,
v_nxt,
)?;
core::mem::swap(&mut m_cur, &mut m_nxt);
core::mem::swap(&mut v_cur, &mut v_nxt);
}
for i in 0..n {
let mut max_idx = i;
let mut max_val = m_cur[i * n + i];
for j in (i + 1)..n {
let val = m_cur[j * n + j];
if val > max_val {
max_val = val;
max_idx = j;
}
}
if max_idx != i {
m_cur.swap(i * n + i, max_idx * n + max_idx);
for r in 0..n {
v_cur.swap(r * n + i, r * n + max_idx);
}
}
}
let sigma_max = if n == 0 { zero } else { m_cur[0].sqrt() };
for i in 0..n {
let lambda = m_cur[i * n + i];
let sigma_i = lambda.sqrt();
out_sigma[i] = sigma_i;
if sigma_i <= tolerance.mul(sigma_max) {
for r in 0..m {
out_u[r * n + i] = zero;
}
continue;
}
let v_i = StridedColumn {
data: v_cur,
col: i,
stride: n,
len: n,
};
mul_vector(a, m, n, &v_i, av_scratch)?;
for r in 0..m {
out_u[r * n + i] = av_scratch[r].div(sigma_i);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{svd, svd_qr_iteration};
use crate::algorithm::matrix::DimensionMismatch;
use crate::storage::StaticStorage;
fn reconstruct(u: &[f64], sigma: &[f64], v: &[f64], rows: usize, cols: usize, out: &mut [f64]) {
for i in 0..rows {
for j in 0..cols {
let mut sum = 0.0;
for k in 0..cols {
sum += u[i * cols + k] * sigma[k] * v[j * cols + k];
}
out[i * cols + j] = sum;
}
}
}
#[test]
fn svd_of_2x2_shear_matrix_reconstructs_a() {
let a = StaticStorage::new([1.0_f64, 1.0, 0.0, 1.0]);
let mut u = [0.0; 4];
let mut sigma = [0.0; 2];
let mut v = [0.0; 4];
let mut scratch = [0.0; 5 * 2 * 2 + 2 + 2];
assert_eq!(
svd_qr_iteration(&a, 2, 2, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Ok(())
);
assert!(sigma[0] >= sigma[1]);
assert!(sigma[1] >= 0.0);
let mut reconstructed = [0.0; 4];
reconstruct(&u, &sigma, &v, 2, 2, &mut reconstructed);
for (actual, expected) in reconstructed.iter().zip([1.0, 1.0, 0.0, 1.0]) {
assert!((actual - expected).abs() < 1e-6);
}
}
#[test]
fn svd_of_non_square_3x2_matrix_reconstructs_a() {
let a = StaticStorage::new([1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0]);
let mut u = [0.0; 6];
let mut sigma = [0.0; 2];
let mut v = [0.0; 4];
let mut scratch = [0.0; 5 * 2 * 2 + 2 + 3];
assert_eq!(
svd_qr_iteration(&a, 3, 2, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Ok(())
);
assert!(sigma[0] >= sigma[1]);
assert!(sigma[1] >= 0.0);
let mut reconstructed = [0.0; 6];
reconstruct(&u, &sigma, &v, 3, 2, &mut reconstructed);
let expected_a = [1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
for (actual, expected) in reconstructed.iter().zip(expected_a) {
assert!((actual - expected).abs() < 1e-6);
}
}
#[test]
fn svd_of_diagonal_matrix_sorts_singular_values_descending() {
#[rustfmt::skip]
let a = StaticStorage::new([
1.0_f64, 0.0, 0.0,
0.0, 3.0, 0.0,
0.0, 0.0, 2.0,
]);
let mut u = [0.0; 9];
let mut sigma = [0.0; 3];
let mut v = [0.0; 9];
let mut scratch = [0.0; 5 * 3 * 3 + 3 + 3];
assert_eq!(
svd_qr_iteration(&a, 3, 3, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Ok(())
);
for (actual, expected) in sigma.iter().zip([3.0, 2.0, 1.0]) {
assert!((actual - expected).abs() < 1e-9);
}
assert!(sigma[0] >= sigma[1] && sigma[1] >= sigma[2]);
for &s in &sigma {
assert!(s >= 0.0);
}
}
#[test]
fn svd_of_rank_deficient_matrix_singular_value_count_matches_rank() {
let a = StaticStorage::new([1.0_f64, 2.0, 2.0, 4.0, 3.0, 6.0]);
let mut u = [0.0; 6];
let mut sigma = [0.0; 2];
let mut v = [0.0; 4];
let mut scratch = [0.0; 5 * 2 * 2 + 2 + 3];
assert_eq!(
svd_qr_iteration(&a, 3, 2, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Ok(())
);
let nonzero_count = sigma.iter().filter(|&&s| s > 1e-6).count();
assert_eq!(nonzero_count, 1);
}
#[test]
fn svd_mismatched_output_length_is_an_error_not_a_panic() {
let a = StaticStorage::new([1.0, 1.0, 0.0, 1.0]);
let mut u = [0.0; 3];
let mut sigma = [0.0; 2];
let mut v = [0.0; 4];
let mut scratch = [0.0; 5 * 2 * 2 + 2 + 2];
assert_eq!(
svd_qr_iteration(&a, 2, 2, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Err(DimensionMismatch)
);
}
#[test]
fn svd_mismatched_scratch_length_is_an_error_not_a_panic() {
let a = StaticStorage::new([1.0, 1.0, 0.0, 1.0]);
let mut u = [0.0; 4];
let mut sigma = [0.0; 2];
let mut v = [0.0; 4];
let mut scratch = [0.0; 4];
assert_eq!(
svd_qr_iteration(&a, 2, 2, &mut u, &mut sigma, &mut v, &mut scratch, 1e-9),
Err(DimensionMismatch)
);
}
#[test]
fn svd_matches_svd_qr_iteration() {
let a = StaticStorage::new([1.0_f64, 1.0, 0.0, 1.0]);
let mut u_high_level = [0.0; 4];
let mut sigma_high_level = [0.0; 2];
let mut v_high_level = [0.0; 4];
let mut scratch_high_level = [0.0; 5 * 2 * 2 + 2 + 2];
assert_eq!(
svd(
&a,
2,
2,
&mut u_high_level,
&mut sigma_high_level,
&mut v_high_level,
&mut scratch_high_level
),
Ok(())
);
let mut u_explicit = [0.0; 4];
let mut sigma_explicit = [0.0; 2];
let mut v_explicit = [0.0; 4];
let mut scratch_explicit = [0.0; 5 * 2 * 2 + 2 + 2];
assert_eq!(
svd_qr_iteration(
&a,
2,
2,
&mut u_explicit,
&mut sigma_explicit,
&mut v_explicit,
&mut scratch_explicit,
1e-9
),
Ok(())
);
assert_eq!(u_high_level, u_explicit);
assert_eq!(sigma_high_level, sigma_explicit);
assert_eq!(v_high_level, v_explicit);
}
}