From 8dbb0f55af1011639bdedff8b2768c3dbad7926a Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 24 Oct 2026 23:40:00 +0200 Subject: [PATCH] Skip unused SVD output work for camera null-space queries Share the unchanged Jacobi factors between full SVD and null-vector callers. Select the same right singular vector directly instead of normalizing U and sorting copies that the caller discards. Fixes #2329 --- crates/track/src/camtrack/linalg.rs | 71 +++++++++++++++++++---------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/crates/track/src/camtrack/linalg.rs b/crates/track/src/camtrack/linalg.rs index 2efc14556..6f0b710ed 100644 --- a/crates/track/src/camtrack/linalg.rs +++ b/crates/track/src/camtrack/linalg.rs @@ -145,6 +145,33 @@ pub fn slerp(a: &M3, b: &M3, t: f64) -> M3 { /// `max(m, n) × n` (rows beyond `m` belong to zero padding), `s` is sorted descending and `V` /// is `n × n` (columns are the right singular vectors). pub fn svd(a: &[f64], m: usize, n: usize) -> (Vec, Vec, Vec) { + let rows = m.max(n); + let (mut u, mut s, v) = jacobi_factors(a, m, n); + for (j, sj) in s.iter().enumerate() { + if *sj > 1e-300 { + for r in 0..rows { + u[r * n + j] /= sj; + } + } + } + // Sort descending. + let mut order: Vec = (0..n).collect(); + order.sort_by(|&a, &b| s[b].total_cmp(&s[a])); + let (mut u2, mut v2) = (vec![0.0; rows * n], vec![0.0; n * n]); + for (k, &j) in order.iter().enumerate() { + for r in 0..rows { + u2[r * n + k] = u[r * n + j]; + } + for r in 0..n { + v2[r * n + k] = v[r * n + j]; + } + } + s = order.iter().map(|&j| s[j]).collect(); + (u2, s, v2) +} + +// Unnormalised and unsorted factors; full SVD and null-space queries share every sweep. +fn jacobi_factors(a: &[f64], m: usize, n: usize) -> (Vec, Vec, Vec) { let rows = m.max(n); let mut u = vec![0.0; rows * n]; u[..m * n].copy_from_slice(&a[..m * n]); @@ -192,28 +219,8 @@ pub fn svd(a: &[f64], m: usize, n: usize) -> (Vec, Vec, Vec) { break; } } - let mut s: Vec = (0..n).map(|j| (0..rows).map(|r| u[r * n + j] * u[r * n + j]).sum::().sqrt()).collect(); - for (j, sj) in s.iter().enumerate() { - if *sj > 1e-300 { - for r in 0..rows { - u[r * n + j] /= sj; - } - } - } - // Sort descending. - let mut order: Vec = (0..n).collect(); - order.sort_by(|&a, &b| s[b].total_cmp(&s[a])); - let (mut u2, mut v2) = (vec![0.0; rows * n], vec![0.0; n * n]); - for (k, &j) in order.iter().enumerate() { - for r in 0..rows { - u2[r * n + k] = u[r * n + j]; - } - for r in 0..n { - v2[r * n + k] = v[r * n + j]; - } - } - s = order.iter().map(|&j| s[j]).collect(); - (u2, s, v2) + let s: Vec = (0..n).map(|j| (0..rows).map(|r| u[r * n + j] * u[r * n + j]).sum::().sqrt()).collect(); + (u, s, v) } /// SVD of a 3 × 3 matrix: (U, singular values descending, V) with `m = U diag(s) Vᵀ`. @@ -235,8 +242,10 @@ pub fn svd3(m: &M3) -> (M3, V3, M3) { /// The unit vector `x` minimising `|A x|` (`m × n` row-major): the right singular vector of /// the smallest singular value. pub fn null_vector(a: &[f64], m: usize, n: usize) -> Vec { - let (_, _, v) = svd(a, m, n); - (0..n).map(|r| v[r * n + n - 1]).collect() + let (_, s, v) = jacobi_factors(a, m, n); + // The full SVD uses a stable descending sort: the last of equal minima wins. + let smallest = (0..n).min_by(|&i, &j| s[i].total_cmp(&s[j]).then_with(|| j.cmp(&i))).unwrap_or(0); + (0..n).map(|r| v[r * n + smallest]).collect() } /// Eigen-decomposition of a symmetric `n × n` matrix (cyclic Jacobi): eigenvalues ascending @@ -441,6 +450,20 @@ mod tests { assert!(s[0] >= s[1] && s[1] >= s[2]); } + #[test] + fn null_vector_matches_full_svd_bits_including_ties() { + for (m, n) in [(0, 0), (1, 1), (3, 3), (8, 9), (9, 9), (20, 3), (100, 9)] { + for scale in [0.0, 1e-100, 1.0, 1e100] { + let a: Vec = (0..m * n).map(|i| (((i * 37) % 31) as f64 - 15.0) * scale).collect(); + let (_, _, v) = svd(&a, m, n); + let expected: Vec<_> = (0..n).map(|r| v[r * n + n - 1].to_bits()).collect(); + assert_eq!(null_vector(&a, m, n).iter().map(|v| v.to_bits()).collect::>(), expected); + } + } + let a = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]; + assert_eq!(null_vector(&a, 3, 3), vec![0.0, 0.0, 1.0]); + } + #[test] fn rotations_round_trip() { let w = [0.3, -1.2, 0.7];