Skip to content

Commit f8caf3b

Browse files
committed
fix: SVD loss of accuracy with small non-zero singular values
1 parent 4478a99 commit f8caf3b

2 files changed

Lines changed: 65 additions & 5 deletions

File tree

src/linalg/svd.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,20 @@ where
159159
let mut diagonal = bi_matrix.diagonal();
160160
let mut off_diagonal = bi_matrix.off_diagonal();
161161

162+
// Compute the Frobenius norm of the bidiagonal matrix for relative convergence checks,
163+
// so we can use a relative tolerance rather than absolute.
164+
// This avoids stability issues when diagonal elements are very small but not exactly zero.
165+
let mut norm_sqr = T::RealField::zero();
166+
for i in 0..diagonal.len() {
167+
norm_sqr += diagonal[i].clone() * diagonal[i].clone();
168+
}
169+
for i in 0..off_diagonal.len() {
170+
norm_sqr += off_diagonal[i].clone() * off_diagonal[i].clone();
171+
}
172+
let b_norm = norm_sqr.sqrt();
173+
// Use relative epsilon: eps * ||B|| for zero checks
174+
let eps_rel = eps.clone() * b_norm;
175+
162176
let mut niter = 0;
163177
let (mut start, mut end) = Self::delimit_subproblem(
164178
&mut diagonal,
@@ -168,6 +182,7 @@ where
168182
bi_matrix.is_upper_diagonal(),
169183
dim - 1,
170184
eps.clone(),
185+
eps_rel.clone(),
171186
);
172187

173188
while end != start {
@@ -317,6 +332,7 @@ where
317332
bi_matrix.is_upper_diagonal(),
318333
end,
319334
eps.clone(),
335+
eps_rel.clone(),
320336
);
321337
start = sub.0;
322338
end = sub.1;
@@ -372,6 +388,7 @@ where
372388
is_upper_diagonal: bool,
373389
end: usize,
374390
eps: T::RealField,
391+
eps_rel: T::RealField,
375392
) -> (usize, usize) {
376393
let mut n = end;
377394

@@ -383,7 +400,7 @@ where
383400
<= eps.clone() * (diagonal[n].clone().norm1() + diagonal[m].clone().norm1())
384401
{
385402
off_diagonal[m] = T::RealField::zero();
386-
} else if diagonal[m].clone().norm1() <= eps {
403+
} else if diagonal[m].clone().norm1() <= eps_rel {
387404
diagonal[m] = T::RealField::zero();
388405
Self::cancel_horizontal_off_diagonal_elt(
389406
diagonal,
@@ -405,7 +422,7 @@ where
405422
m - 1,
406423
);
407424
}
408-
} else if diagonal[n].clone().norm1() <= eps {
425+
} else if diagonal[n].clone().norm1() <= eps_rel {
409426
diagonal[n] = T::RealField::zero();
410427
Self::cancel_vertical_off_diagonal_elt(
411428
diagonal,
@@ -435,9 +452,7 @@ where
435452
{
436453
off_diagonal[m] = T::RealField::zero();
437454
break;
438-
}
439-
// TODO: write a test that enters this case.
440-
else if diagonal[m].clone().norm1() <= eps {
455+
} else if diagonal[m].clone().norm1() <= eps_rel {
441456
diagonal[m] = T::RealField::zero();
442457
Self::cancel_horizontal_off_diagonal_elt(
443458
diagonal,

tests/linalg/svd.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::utils::is_sorted_descending;
2+
use approx::{AbsDiffEq, RelativeEq};
23
use na::{DMatrix, Matrix6};
34

45
#[cfg(feature = "proptest-support")]
@@ -513,3 +514,47 @@ fn svd_regression_issue_1313() {
513514
let m2 = svd.recompose().unwrap();
514515
assert_relative_eq!(&m, &m2, epsilon = 1e-5);
515516
}
517+
518+
#[test]
519+
// Accuracy bug reported in issue #1172 of nalgebra (https://github.com/dimforge/nalgebra/issues/1172)
520+
fn svd_regression_issue_1172() {
521+
use nalgebra::{Complex, Matrix4};
522+
type M4C = Matrix4<Complex<f64>>;
523+
524+
let m = M4C::new(
525+
Complex::new(0.4846888711394364, -0.0000000000000002529226450498658),
526+
Complex::new(0.4997655143494952, -0.00000000000000001731471891552503),
527+
Complex::new(0.00000000000000001527512211317369, 0.49976551434949495),
528+
Complex::new(0.00000000000000009643636194372324, -0.48468887113943676),
529+
Complex::new(0.4997655143494954, -0.00000000000000023999992468308935),
530+
Complex::new(0.5153111288605629, -0.0000000000000002516108359162637),
531+
Complex::new(-0.000000000000000005044961400919933, 0.5153111288605631),
532+
Complex::new(0.000000000000000042486770760464153, -0.49976551434949495),
533+
Complex::new(-0.000000000000000032383811520876863, -0.49976551434949484),
534+
Complex::new(0.000000000000000014710206963221994, -0.5153111288605633),
535+
Complex::new(0.5153111288605635, 0.00000000000000016459157448729957),
536+
Complex::new(-0.4997655143494946, -0.00000000000000028045339796436483),
537+
Complex::new(0.00000000000000007455816688442185, 0.4846888711394367),
538+
Complex::new(0.0000000000000000641248522548626, 0.49976551434949484),
539+
Complex::new(-0.49976551434949473, -0.00000000000000018487105599880945),
540+
Complex::new(0.48468887113943704, 0.00000000000000015953066497724206),
541+
);
542+
let svd = m.svd(true, true);
543+
let sings = svd.singular_values;
544+
let u = svd.u.unwrap();
545+
let v_t = svd.v_t.unwrap();
546+
let sigma = M4C::from_diagonal(&sings.cast::<Complex<f64>>());
547+
let m1 = u * sigma * v_t;
548+
549+
// Should be accurate to machine precision
550+
assert_relative_eq!(m, m1, epsilon = 1e-12);
551+
552+
// Singular values should be sorted and non-negative
553+
assert!(sings.iter().all(|&x| x >= 0.0));
554+
555+
// The largest singular value should be close to 2.0
556+
assert!(sings[0].abs_diff_eq(&2.0, 1e-12));
557+
558+
// The smallest singular value should be small
559+
assert!(sings[3] < 1e-12);
560+
}

0 commit comments

Comments
 (0)