#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Decomposition<const K: usize> {
pub values: [f64; K],
pub vectors: [[f64; K]; K],
}
pub fn singular_values<const K: usize>(matrix: &[[f64; K]; K]) -> [f64; K] {
decompose(matrix).values
}
pub fn decompose<const K: usize>(matrix: &[[f64; K]; K]) -> Decomposition<K> {
const TOLERANCE: f64 = 1e-17;
const MAX_SWEEPS: usize = 40;
let mut work = *matrix;
let mut rotations = [[0.0f64; K]; K];
for (index, row) in rotations.iter_mut().enumerate() {
row[index] = 1.0;
}
for _ in 0..MAX_SWEEPS {
let mut rotated = false;
for p in 0..K {
for q in (p + 1)..K {
let mut alpha = 0.0;
let mut beta = 0.0;
let mut gamma = 0.0;
for row in work.iter() {
alpha += row[p] * row[p];
beta += row[q] * row[q];
gamma += row[p] * row[q];
}
if gamma == 0.0 || alpha == 0.0 || beta == 0.0 {
continue;
}
if gamma.abs() <= TOLERANCE * (alpha * beta).sqrt() {
continue;
}
let zeta = (beta - alpha) / (2.0 * gamma);
let tangent = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
let cosine = 1.0 / (1.0 + tangent * tangent).sqrt();
let sine = cosine * tangent;
for row in work.iter_mut().chain(rotations.iter_mut()) {
let left = row[p];
let right = row[q];
row[p] = cosine * left - sine * right;
row[q] = sine * left + cosine * right;
}
rotated = true;
}
}
if !rotated {
break;
}
}
let mut norms = [0.0f64; K];
for (column, value) in norms.iter_mut().enumerate() {
let mut sum = 0.0;
for row in work.iter() {
sum += row[column] * row[column];
}
*value = sum.sqrt();
}
let mut order: [usize; K] = [0; K];
for (index, slot) in order.iter_mut().enumerate() {
*slot = index;
}
order.sort_by(|a, b| norms[*b].total_cmp(&norms[*a]));
let mut values = [0.0f64; K];
let mut vectors = [[0.0f64; K]; K];
for (target, source) in order.iter().enumerate() {
values[target] = norms[*source];
for axis in 0..K {
vectors[axis][target] = rotations[axis][*source];
}
}
Decomposition { values, vectors }
}
pub fn condition_number<const K: usize>(values: &[f64; K]) -> f64 {
let largest = values[0];
let smallest = values[K - 1];
if smallest == 0.0 {
f64::INFINITY
} else {
largest / smallest
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagonal_matrix_is_trivial() {
let mut matrix = [[0.0f64; 4]; 4];
for (i, value) in [3.0, -1.0, 0.25, 8.0].into_iter().enumerate() {
matrix[i][i] = value;
}
let values = singular_values(&matrix);
assert!((values[0] - 8.0).abs() < 1e-15);
assert!((values[1] - 3.0).abs() < 1e-15);
assert!((values[2] - 1.0).abs() < 1e-15);
assert!((values[3] - 0.25).abs() < 1e-15);
}
#[test]
fn rotation_has_unit_spectrum() {
let angle = 0.7f64;
let matrix = [
[angle.cos(), -angle.sin(), 0.0],
[angle.sin(), angle.cos(), 0.0],
[0.0, 0.0, 1.0],
];
for value in singular_values(&matrix) {
assert!((value - 1.0).abs() < 1e-15);
}
}
#[test]
fn rank_deficient_matrix_yields_zero() {
let matrix = [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [-1.0, -2.0, -3.0]];
let values = singular_values(&matrix);
assert!(values[0] > 1.0);
assert!(values[1] < 1e-15, "second value {}", values[1]);
assert!(values[2] < 1e-15);
assert_eq!(condition_number(&values), f64::INFINITY);
}
#[test]
fn tiny_diagonal_entries_keep_relative_accuracy() {
let scales = [1.0, 1e-4, 1e-8, 1e-12, 1e-16, 1e-20];
let mut matrix = [[0.0f64; 6]; 6];
for (i, scale) in scales.iter().enumerate() {
matrix[i][i] = *scale;
}
let values = singular_values(&matrix);
for (found, expected) in values.iter().zip(scales.iter()) {
let relative = (found - expected).abs() / expected;
assert!(
relative < 1e-15,
"expected {expected:e}, got {found:e}, error {relative:.3e}"
);
}
}
}
#[cfg(test)]
mod vector_tests {
use super::*;
#[test]
fn right_vectors_are_orthonormal() {
let matrix = [
[3.0, 1.0, -2.0, 0.5],
[0.0, 2.5, 1.0, -1.0],
[1.0, 0.0, 4.0, 2.0],
[-0.5, 1.5, 0.0, 3.0],
];
let result = decompose(&matrix);
for i in 0..4 {
for j in 0..4 {
let dot: f64 = (0..4)
.map(|axis| result.vectors[axis][i] * result.vectors[axis][j])
.sum();
let expected = if i == j { 1.0 } else { 0.0 };
assert!((dot - expected).abs() < 1e-13, "V columns {i},{j}: {dot}");
}
}
}
#[test]
fn vectors_match_their_values() {
let matrix = [[2.0, 0.0, 1.0], [0.0, 3.0, 0.0], [1.0, 0.0, 2.0]];
let result = decompose(&matrix);
for j in 0..3 {
let mut image = [0.0f64; 3];
for (i, slot) in image.iter_mut().enumerate() {
*slot = (0..3).map(|k| matrix[i][k] * result.vectors[k][j]).sum();
}
let norm = image.iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(
(norm - result.values[j]).abs() < 1e-13,
"‖A·v{j}‖ = {norm}, but σ{j} = {}",
result.values[j]
);
}
}
#[test]
fn null_direction_is_annihilated() {
let matrix = [[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [-1.0, -2.0, -3.0]];
let result = decompose(&matrix);
let null = [
result.vectors[0][2],
result.vectors[1][2],
result.vectors[2][2],
];
for row in &matrix {
let value: f64 = (0..3).map(|k| row[k] * null[k]).sum();
assert!(value.abs() < 1e-13, "row gives {value}");
}
}
}