use crate::svd::bidiag_reduce::{BidiagVect, gebrd, orgbr};
use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::{Mat, MatRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QrSvdError {
EmptyMatrix,
NotConverged {
num_unconverged: usize,
},
BidiagError,
}
impl core::fmt::Display for QrSvdError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptyMatrix => write!(f, "Matrix is empty"),
Self::NotConverged { num_unconverged } => {
write!(
f,
"SVD did not converge: {} singular values unconverged",
num_unconverged
)
}
Self::BidiagError => write!(f, "Bidiagonal reduction failed"),
}
}
}
impl std::error::Error for QrSvdError {}
#[derive(Debug, Clone)]
pub struct QrSvd<T: Scalar> {
u: Mat<T>,
sigma: Vec<T>,
vt: Mat<T>,
m: usize,
n: usize,
}
impl<T: Field + Real + bytemuck::Zeroable> QrSvd<T> {
const MAX_BIDIAG_ITER: usize = 30;
pub fn compute(a: MatRef<'_, T>) -> Result<Self, QrSvdError> {
let m = a.nrows();
let n = a.ncols();
if m == 0 || n == 0 {
return Err(QrSvdError::EmptyMatrix);
}
if m == 1 && n == 1 {
let val = a[(0, 0)];
let sigma = vec![Scalar::abs(val)];
let mut u = Mat::zeros(1, 1);
let mut vt = Mat::zeros(1, 1);
u[(0, 0)] = if val >= T::zero() {
T::one()
} else {
-T::one()
};
vt[(0, 0)] = T::one();
return Ok(Self { u, sigma, vt, m, n });
}
let k = m.min(n);
let factors = gebrd(a).map_err(|_| QrSvdError::BidiagError)?;
let d = factors.d.clone();
let e = factors.e.clone();
let (u_bidiag, sigma, vt_bidiag) = Self::bidiagonal_svd_qr(&d, &e)?;
let q = orgbr(&factors, BidiagVect::Q).map_err(|_| QrSvdError::BidiagError)?;
let p = orgbr(&factors, BidiagVect::P).map_err(|_| QrSvdError::BidiagError)?;
let q_rows = q.nrows();
let q_cols = q.ncols();
let p_rows = p.nrows();
let p_cols = p.ncols();
let mut u = Mat::zeros(q_rows, k);
for i in 0..q_rows {
for j in 0..k {
let mut sum = T::zero();
for l in 0..k.min(q_cols) {
sum = sum + q[(i, l)] * u_bidiag[(l, j)];
}
u[(i, j)] = sum;
}
}
let mut vt = Mat::zeros(k, p_cols);
for i in 0..k {
for j in 0..p_cols {
let mut sum = T::zero();
for l in 0..k.min(p_rows) {
if j < p_rows {
sum = sum + vt_bidiag[(i, l)] * p[(j, l)];
}
}
vt[(i, j)] = sum;
}
}
Ok(Self { u, sigma, vt, m, n })
}
fn bidiagonal_svd_qr(d: &[T], e: &[T]) -> Result<(Mat<T>, Vec<T>, Mat<T>), QrSvdError> {
Self::bidiagonal_svd_qr_with_limit(d, e, Self::MAX_BIDIAG_ITER)
}
fn bidiagonal_svd_qr_with_limit(
d: &[T],
e: &[T],
max_iter_per_value: usize,
) -> Result<(Mat<T>, Vec<T>, Mat<T>), QrSvdError> {
let n = d.len();
if n == 0 {
return Ok((Mat::zeros(0, 0), vec![], Mat::zeros(0, 0)));
}
let mut d_work: Vec<T> = d.to_vec();
let mut e_work: Vec<T> = e.to_vec();
let mut u = Mat::zeros(n, n);
let mut vt = Mat::zeros(n, n);
for i in 0..n {
u[(i, i)] = T::one();
vt[(i, i)] = T::one();
}
let eps = <T as Scalar>::epsilon();
let tol = eps * T::from_f64(100.0).unwrap_or(T::one());
let max_sweeps = max_iter_per_value.saturating_mul(n).saturating_mul(n);
for _iter in 0..max_sweeps {
for i in 0..e_work.len() {
if is_negligible_superdiag(e_work[i], d_work[i], d_work[i + 1], tol) {
e_work[i] = T::zero();
}
}
let mut hi = e_work.len();
while hi > 0 && e_work[hi - 1] == T::zero() {
hi -= 1;
}
if hi == 0 {
break;
}
let mut lo = hi - 1;
while lo > 0 && e_work[lo - 1] != T::zero() {
lo -= 1;
}
if Scalar::abs(d_work[lo]) <= tol * (Scalar::abs(d_work[hi]) + Scalar::abs(e_work[lo]))
{
Self::deflate_zero_diagonal(&mut d_work, &mut e_work, &mut u, lo, hi + 1);
continue;
}
if Scalar::abs(d_work[hi])
<= tol * (Scalar::abs(d_work[lo]) + Scalar::abs(e_work[hi - 1]))
{
Self::deflate_zero_diagonal_bottom(&mut d_work, &mut e_work, &mut vt, lo, hi + 1);
continue;
}
Self::golub_kahan_step(&mut d_work, &mut e_work, &mut u, &mut vt, lo, hi + 1);
}
let num_unconverged = count_unconverged_superdiags(&d_work, &e_work, tol);
if num_unconverged > 0 {
return Err(QrSvdError::NotConverged { num_unconverged });
}
for i in 0..n {
if d_work[i] < T::zero() {
d_work[i] = -d_work[i];
for j in 0..n {
u[(j, i)] = -u[(j, i)];
}
}
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
if d_work[b] > d_work[a] {
core::cmp::Ordering::Greater
} else if d_work[b] < d_work[a] {
core::cmp::Ordering::Less
} else {
core::cmp::Ordering::Equal
}
});
let mut sigma = vec![T::zero(); n];
let mut u_sorted = Mat::zeros(n, n);
let mut vt_sorted = Mat::zeros(n, n);
for (new_idx, &old_idx) in indices.iter().enumerate() {
sigma[new_idx] = d_work[old_idx];
for j in 0..n {
u_sorted[(j, new_idx)] = u[(j, old_idx)];
vt_sorted[(new_idx, j)] = vt[(old_idx, j)];
}
}
Ok((u_sorted, sigma, vt_sorted))
}
fn golub_kahan_step(
d: &mut [T],
e: &mut [T],
u: &mut Mat<T>,
vt: &mut Mat<T>,
start: usize,
end: usize,
) {
let last = end - 1;
if Scalar::abs(d[start]) >= Scalar::abs(d[last]) {
let mu = wilkinson_shift_leading(d, e, start, end);
Self::chase_up(d, e, u, vt, start, end, mu);
} else {
let mu = wilkinson_shift_trailing(d, e, start, end);
Self::chase_down(d, e, u, vt, start, end, mu);
}
}
fn chase_down(
d: &mut [T],
e: &mut [T],
u: &mut Mat<T>,
vt: &mut Mat<T>,
start: usize,
end: usize,
mu: T,
) {
let n = u.nrows();
let last = end - 1;
let mut f = d[start] * d[start] - mu;
let mut g = d[start] * e[start];
for k in start..last {
let (c, s, r) = givens_rotation(f, g);
if k > start {
e[k - 1] = r;
}
f = c * d[k] + s * e[k];
e[k] = -s * d[k] + c * e[k];
g = s * d[k + 1];
d[k + 1] = c * d[k + 1];
for j in 0..n {
let vk = vt[(k, j)];
let vk1 = vt[(k + 1, j)];
vt[(k, j)] = c * vk + s * vk1;
vt[(k + 1, j)] = -s * vk + c * vk1;
}
let (c, s, r) = givens_rotation(f, g);
d[k] = r;
f = c * e[k] + s * d[k + 1];
d[k + 1] = -s * e[k] + c * d[k + 1];
if k < last - 1 {
g = s * e[k + 1];
e[k + 1] = c * e[k + 1];
}
for j in 0..n {
let uk = u[(j, k)];
let uk1 = u[(j, k + 1)];
u[(j, k)] = c * uk + s * uk1;
u[(j, k + 1)] = -s * uk + c * uk1;
}
}
e[last - 1] = f;
}
fn chase_up(
d: &mut [T],
e: &mut [T],
u: &mut Mat<T>,
vt: &mut Mat<T>,
start: usize,
end: usize,
mu: T,
) {
let n = u.nrows();
let last = end - 1;
let mut f = d[last] * d[last] - mu;
let mut g = d[last] * e[last - 1];
for k in (start..last).rev() {
let (c, s, r) = givens_rotation(f, g);
if k < last - 1 {
e[k + 1] = r;
}
f = c * d[k + 1] + s * e[k];
e[k] = -s * d[k + 1] + c * e[k];
g = s * d[k];
d[k] = c * d[k];
for j in 0..n {
let uk = u[(j, k)];
let uk1 = u[(j, k + 1)];
u[(j, k + 1)] = c * uk1 + s * uk;
u[(j, k)] = -s * uk1 + c * uk;
}
let (c, s, r) = givens_rotation(f, g);
d[k + 1] = r;
f = c * e[k] + s * d[k];
d[k] = -s * e[k] + c * d[k];
if k > start {
g = s * e[k - 1];
e[k - 1] = c * e[k - 1];
}
for j in 0..n {
let vk = vt[(k, j)];
let vk1 = vt[(k + 1, j)];
vt[(k + 1, j)] = c * vk1 + s * vk;
vt[(k, j)] = -s * vk1 + c * vk;
}
}
e[start] = f;
}
fn deflate_zero_diagonal(d: &mut [T], e: &mut [T], u: &mut Mat<T>, start: usize, end: usize) {
let n = u.nrows();
let last = end - 1;
let mut extra = e[start];
e[start] = T::zero();
for k in (start + 1)..=last {
let (c, s, _r) = givens_rotation(d[k], extra);
d[k] = c * d[k] + s * extra;
for j in 0..n {
let uk = u[(j, k)];
let us = u[(j, start)];
u[(j, k)] = c * uk + s * us;
u[(j, start)] = -s * uk + c * us;
}
if k < last {
extra = -s * e[k];
e[k] = c * e[k];
}
}
}
fn deflate_zero_diagonal_bottom(
d: &mut [T],
e: &mut [T],
vt: &mut Mat<T>,
start: usize,
end: usize,
) {
let n = vt.ncols();
let last = end - 1;
let mut extra = e[last - 1];
e[last - 1] = T::zero();
for k in (start..last).rev() {
let (c, s, _r) = givens_rotation(d[k], extra);
d[k] = c * d[k] + s * extra;
for j in 0..n {
let vk = vt[(k, j)];
let vl = vt[(last, j)];
vt[(k, j)] = c * vk + s * vl;
vt[(last, j)] = -s * vk + c * vl;
}
if k > start {
extra = -s * e[k - 1];
e[k - 1] = c * e[k - 1];
}
}
}
pub fn u(&self) -> &Mat<T> {
&self.u
}
pub fn singular_values(&self) -> &[T] {
&self.sigma
}
pub fn vt(&self) -> &Mat<T> {
&self.vt
}
pub fn dims(&self) -> (usize, usize) {
(self.m, self.n)
}
pub fn reconstruct(&self) -> Mat<T> {
let mut result = Mat::zeros(self.m, self.n);
let k = self.sigma.len();
let u_cols = self.u.ncols();
let vt_rows = self.vt.nrows();
for i in 0..self.m {
for j in 0..self.n {
let mut sum = T::zero();
for l in 0..k.min(u_cols).min(vt_rows) {
sum = sum + self.u[(i, l)] * self.sigma[l] * self.vt[(l, j)];
}
result[(i, j)] = sum;
}
}
result
}
pub fn condition_number(&self) -> T {
match (self.sigma.first(), self.sigma.last()) {
(Some(&max_sv), Some(&min_sv)) => {
if min_sv > T::zero() {
max_sv / min_sv
} else {
<T as Scalar>::max_value()
}
}
_ => T::one(),
}
}
pub fn rank(&self, tol: T) -> usize {
self.sigma.iter().filter(|&&s| s > tol).count()
}
}
#[inline]
fn is_negligible_superdiag<T: Field + Real>(e_i: T, d_i: T, d_ip1: T, tol: T) -> bool {
Scalar::abs(e_i) <= tol * (Scalar::abs(d_i) + Scalar::abs(d_ip1))
}
fn count_unconverged_superdiags<T: Field + Real>(d: &[T], e: &[T], tol: T) -> usize {
e.iter()
.enumerate()
.filter(|&(i, &ev)| !is_negligible_superdiag(ev, d[i], d[i + 1], tol))
.count()
}
fn wilkinson_shift_trailing<T: Field + Real>(d: &[T], e: &[T], start: usize, end: usize) -> T {
let last = end - 1;
let e_last = e[last - 1]; let e_above = if last - 1 > start {
e[last - 2]
} else {
T::zero()
};
let t22 = d[last] * d[last] + e_last * e_last;
let t11 = d[last - 1] * d[last - 1] + e_above * e_above;
let t12 = d[last - 1] * e_last;
wilkinson_eig_closest_to(t11, t12, t22)
}
fn wilkinson_shift_leading<T: Field + Real>(d: &[T], e: &[T], start: usize, _end: usize) -> T {
let t11 = d[start] * d[start];
let t12 = d[start] * e[start];
let t22 = d[start + 1] * d[start + 1] + e[start] * e[start];
wilkinson_eig_closest_to(t22, t12, t11)
}
fn wilkinson_eig_closest_to<T: Field + Real>(t11: T, t12: T, t_anchor: T) -> T {
if Scalar::abs(t12) <= <T as Scalar>::epsilon() * (Scalar::abs(t11) + Scalar::abs(t_anchor)) {
return t_anchor;
}
let two = T::one() + T::one();
let delta = (t11 - t_anchor) / two;
let sign_delta = if delta >= T::zero() {
T::one()
} else {
-T::one()
};
let denom = delta + sign_delta * Real::sqrt(delta * delta + t12 * t12);
if Scalar::abs(denom) <= <T as Scalar>::min_positive() {
t_anchor
} else {
t_anchor - t12 * t12 / denom
}
}
fn givens_rotation<T: Field + Real>(f: T, g: T) -> (T, T, T) {
let eps = <T as Scalar>::epsilon();
if Scalar::abs(g) < eps {
(T::one(), T::zero(), f)
} else if Scalar::abs(f) < eps {
(
T::zero(),
if g >= T::zero() { T::one() } else { -T::one() },
Scalar::abs(g),
)
} else {
let h = Real::sqrt(f * f + g * g);
let c = Scalar::abs(f) / h;
let s = g / h * (if f >= T::zero() { T::one() } else { -T::one() });
let r = if f >= T::zero() { h } else { -h };
(c, s, r)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_qr_svd_diagonal() {
let a = Mat::from_rows(&[&[3.0f64, 0.0], &[0.0, 4.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let sigma = svd.singular_values();
assert!(approx_eq(sigma[0], 4.0, 1e-8), "sigma[0] = {}", sigma[0]);
assert!(approx_eq(sigma[1], 3.0, 1e-8), "sigma[1] = {}", sigma[1]);
}
#[test]
fn test_qr_svd_identity() {
let a: Mat<f64> = Mat::eye(3);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let sigma = svd.singular_values();
for (i, &s) in sigma.iter().enumerate() {
assert!(approx_eq(s, 1.0, 1e-8), "sigma[{}] = {}", i, s);
}
}
#[test]
fn test_qr_svd_reconstruction() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let reconstructed = svd.reconstruct();
for i in 0..3 {
for j in 0..2 {
assert!(
approx_eq(reconstructed[(i, j)], a[(i, j)], 1e-8),
"Mismatch at ({}, {}): {} vs {}",
i,
j,
reconstructed[(i, j)],
a[(i, j)]
);
}
}
}
#[test]
fn test_qr_svd_singular_values_descending() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let sigma = svd.singular_values();
for i in 0..sigma.len() - 1 {
assert!(
sigma[i] >= sigma[i + 1],
"Singular values not descending: sigma[{}]={} < sigma[{}]={}",
i,
sigma[i],
i + 1,
sigma[i + 1]
);
}
}
#[test]
fn test_qr_svd_u_orthogonal() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let u = svd.u();
let k = u.ncols();
for i in 0..k {
for j in 0..k {
let mut dot = 0.0;
for l in 0..u.nrows() {
dot += u[(l, i)] * u[(l, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(dot, expected, 1e-8),
"U^T*U not identity at ({}, {}): {} vs {}",
i,
j,
dot,
expected
);
}
}
}
#[test]
fn test_qr_svd_vt_orthogonal() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let vt = svd.vt();
let k = vt.nrows();
for i in 0..k {
for j in 0..k {
let mut dot = 0.0;
for l in 0..vt.ncols() {
dot += vt[(i, l)] * vt[(j, l)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(dot, expected, 1e-8),
"V^T*V not identity at ({}, {}): {} vs {}",
i,
j,
dot,
expected
);
}
}
}
#[test]
fn test_qr_svd_condition_number() {
let a = Mat::from_rows(&[&[2.0f64, 0.0], &[0.0, 1.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let cond = svd.condition_number();
assert!(approx_eq(cond, 2.0, 1e-8), "cond = {}", cond);
}
#[test]
fn test_qr_svd_rank() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[2.0, 4.0, 6.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let r = svd.rank(1e-8);
assert_eq!(r, 1, "rank = {}", r);
}
#[test]
fn test_qr_svd_1x1() {
let a = Mat::from_rows(&[&[-5.0f64]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let sigma = svd.singular_values();
assert!(approx_eq(sigma[0], 5.0, 1e-10));
}
#[test]
fn test_qr_svd_wide_matrix() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0, 4.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let sigma = svd.singular_values();
assert!(
approx_eq(sigma[0], 30.0f64.sqrt(), 1e-8),
"sigma[0] = {}",
sigma[0]
);
}
#[test]
fn test_qr_svd_square_matrix() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[3.0, 4.0]]);
let svd = QrSvd::compute(a.as_ref()).unwrap();
let reconstructed = svd.reconstruct();
for i in 0..2 {
for j in 0..2 {
assert!(
approx_eq(reconstructed[(i, j)], a[(i, j)], 1e-8),
"Mismatch at ({}, {}): {} vs {}",
i,
j,
reconstructed[(i, j)],
a[(i, j)]
);
}
}
}
fn lcg(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*state >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
}
fn lcg_matrix(n: usize) -> Mat<f64> {
let mut state: u64 = 0x2545_f491_4f6c_dd1d ^ (n as u64);
let mut a = Mat::<f64>::zeros(n, n);
for i in 0..n {
for j in 0..n {
a[(i, j)] = lcg(&mut state);
}
}
a
}
#[test]
fn test_qr_svd_lcg_across_sizes() {
use crate::svd::Svd;
for &n in &[10usize, 25, 50, 100, 150] {
let a = lcg_matrix(n);
let qr = QrSvd::compute(a.as_ref())
.unwrap_or_else(|e| panic!("n={n}: QrSvd failed to converge: {e}"));
let reference = Svd::compute(a.as_ref()).unwrap();
let s_qr = qr.singular_values();
let s_ref = reference.singular_values();
let smax = s_ref[0].abs().max(1.0);
assert_eq!(s_qr.len(), n, "n={n}: wrong number of singular values");
for k in 0..n {
assert!(
(s_qr[k] - s_ref[k]).abs() < 1e-8 * smax,
"n={n}: sigma[{k}] qr={} jacobi={}",
s_qr[k],
s_ref[k]
);
}
for k in 0..n {
assert!(
s_qr[k] >= -1e-12,
"n={n}: sigma[{k}]={} is negative",
s_qr[k]
);
}
for k in 1..n {
assert!(
s_qr[k - 1] >= s_qr[k] - 1e-12,
"n={n}: sigma not descending at {k}"
);
}
let rec = qr.reconstruct();
let mut rec_err = 0.0f64;
for i in 0..n {
for j in 0..n {
rec_err = rec_err.max((rec[(i, j)] - a[(i, j)]).abs());
}
}
assert!(
rec_err < 1e-8 * smax,
"n={n}: reconstruction error {rec_err}"
);
}
}
#[test]
fn test_qr_svd_chase_up_invariant() {
let d0 = [3.0f64, 5.0, 2.0, 7.0];
let e0 = [1.5f64, 0.8, 2.1];
let n = d0.len();
let mut b_old = Mat::<f64>::zeros(n, n);
for i in 0..n {
b_old[(i, i)] = d0[i];
if i + 1 < n {
b_old[(i, i + 1)] = e0[i];
}
}
let mut d = d0.to_vec();
let mut e = e0.to_vec();
let mut u = Mat::<f64>::zeros(n, n);
let mut vt = Mat::<f64>::zeros(n, n);
for i in 0..n {
u[(i, i)] = 1.0;
vt[(i, i)] = 1.0;
}
let mu = wilkinson_shift_leading(&d, &e, 0, n);
QrSvd::<f64>::chase_up(&mut d, &mut e, &mut u, &mut vt, 0, n, mu);
let mut b_new = Mat::<f64>::zeros(n, n);
for i in 0..n {
b_new[(i, i)] = d[i];
if i + 1 < n {
b_new[(i, i + 1)] = e[i];
}
}
let mut ub = Mat::<f64>::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += u[(i, k)] * b_new[(k, j)];
}
ub[(i, j)] = s;
}
}
let mut recon = Mat::<f64>::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += ub[(i, k)] * vt[(k, j)];
}
recon[(i, j)] = s;
}
}
let mut err = 0.0f64;
for i in 0..n {
for j in 0..n {
err = err.max((recon[(i, j)] - b_old[(i, j)]).abs());
}
}
assert!(err < 1e-10, "chase_up invariant violated: err={err}");
}
#[test]
fn test_qr_svd_lcg_50x50_converges() {
let a = lcg_matrix(50);
let svd = QrSvd::compute(a.as_ref())
.expect("50x50 LCG matrix must converge after the Wilkinson-shift fix");
assert_eq!(svd.singular_values().len(), 50);
}
#[test]
fn test_qr_svd_rank_deficient() {
use crate::svd::Svd;
let n = 30usize;
let mut a = Mat::<f64>::zeros(n, n);
let mut s1 = 0x1234_5678u64;
let mut s2 = 0x9abc_def0u64;
for i in 0..n {
for j in 0..n {
let mut acc = 0.0;
for _r in 0..3 {
acc += lcg(&mut s1) * lcg(&mut s2);
}
a[(i, j)] = acc;
}
}
let qr = QrSvd::compute(a.as_ref())
.expect("rank-deficient matrix must converge (zero-diagonal path)");
let reference = Svd::compute(a.as_ref()).unwrap();
let s_qr = qr.singular_values();
let s_ref = reference.singular_values();
let smax = s_ref[0].abs().max(1.0);
for k in 0..n {
assert!(
(s_qr[k] - s_ref[k]).abs() < 1e-7 * smax,
"rank-deficient sigma[{k}] qr={} jacobi={}",
s_qr[k],
s_ref[k]
);
}
let rec = qr.reconstruct();
let mut rec_err = 0.0f64;
for i in 0..n {
for j in 0..n {
rec_err = rec_err.max((rec[(i, j)] - a[(i, j)]).abs());
}
}
assert!(rec_err < 1e-7 * smax, "reconstruction error {rec_err}");
}
#[test]
fn test_qr_svd_repeated_singular_values() {
let n = 20usize;
let a: Mat<f64> = Mat::eye(n);
let qr = QrSvd::compute(a.as_ref()).expect("identity must converge");
for &s in qr.singular_values() {
assert!((s - 1.0).abs() < 1e-10, "sigma = {s}");
}
}
#[test]
fn test_qr_svd_zero_positions_stress() {
use crate::svd::Svd;
for n in [3usize, 6, 12] {
for zero_pos in 0..n {
let mut a = Mat::<f64>::zeros(n, n);
for i in 0..n {
a[(i, i)] = if i == zero_pos { 0.0 } else { (i + 1) as f64 };
}
if zero_pos + 1 < n {
a[(zero_pos, zero_pos + 1)] = 0.0;
}
let qr = QrSvd::compute(a.as_ref())
.unwrap_or_else(|e| panic!("n={n} zero_pos={zero_pos}: failed: {e}"));
let reference = Svd::compute(a.as_ref()).unwrap();
let s_qr = qr.singular_values();
let s_ref = reference.singular_values();
let smax = s_ref[0].abs().max(1.0);
for k in 0..n {
assert!(
(s_qr[k] - s_ref[k]).abs() < 1e-8 * smax,
"n={n} zero_pos={zero_pos}: sigma[{k}] qr={} ref={}",
s_qr[k],
s_ref[k]
);
}
}
}
let n = 10usize;
let mut a = Mat::<f64>::zeros(n, n);
let mut st = 0xdead_beefu64;
for i in 0..(n - 1) {
for j in 0..n {
a[(i, j)] = lcg(&mut st);
}
}
let qr = QrSvd::compute(a.as_ref()).expect("zero-row matrix must converge");
let reference = Svd::compute(a.as_ref()).unwrap();
let smax = reference.singular_values()[0].abs().max(1.0);
for k in 0..n {
assert!(
(qr.singular_values()[k] - reference.singular_values()[k]).abs() < 1e-7 * smax,
"zero-row sigma[{k}] qr={} ref={}",
qr.singular_values()[k],
reference.singular_values()[k]
);
}
}
#[test]
fn test_qr_svd_all_zeros() {
let a: Mat<f64> = Mat::zeros(8, 8);
let qr = QrSvd::compute(a.as_ref()).expect("zero matrix must converge");
for &s in qr.singular_values() {
assert!(s.abs() < 1e-12, "sigma = {s}");
}
}
}