use rayon::prelude::*;
#[inline]
#[must_use]
pub const fn packed_upper_row_offset(n: usize, i: usize) -> usize {
i * n - (i * i).wrapping_sub(i) / 2
}
#[inline]
#[must_use]
pub const fn packed_upper_len(n: usize) -> usize {
n * (n + 1) / 2
}
const QL_MAX_SWEEPS_PER_EIGENVALUE: usize = 30;
const PARALLEL_MIN_ROWS: usize = 256;
pub fn packed_symmetric_spectrum_with_probe(
n: usize,
packed: &mut [f64],
probe: &mut [f64],
) -> Result<Vec<f64>, String> {
if packed.len() != packed_upper_len(n) {
return Err(format!(
"packed symmetric spectrum: packed triangle has {} entries but dimension {n} needs {}",
packed.len(),
packed_upper_len(n)
));
}
if probe.len() != n {
return Err(format!(
"packed symmetric spectrum: probe has {} entries but dimension is {n}",
probe.len()
));
}
if n == 0 {
return Ok(Vec::new());
}
if let Some(bad) = packed.iter().position(|value| !value.is_finite()) {
return Err(format!(
"packed symmetric spectrum: packed entry {bad} is not finite ({})",
packed[bad]
));
}
if let Some(bad) = probe.iter().position(|value| !value.is_finite()) {
return Err(format!(
"packed symmetric spectrum: probe entry {bad} is not finite ({})",
probe[bad]
));
}
let (mut diagonal, mut offdiagonal) = tridiagonalize_packed_with_probe(n, packed, probe);
let broken = diagonal
.iter()
.chain(offdiagonal.iter())
.chain(probe.iter())
.position(|value| !value.is_finite());
if let Some(index) = broken {
return Err(format!(
"packed symmetric spectrum: the Householder reduction of a finite {n}x{n} matrix \
produced a non-finite tridiagonal entry (flat index {index} over d, e, probe)"
));
}
implicit_ql_with_probe(&mut diagonal, &mut offdiagonal, probe)?;
sort_spectrum_ascending(&mut diagonal, probe);
Ok(diagonal)
}
fn tridiagonalize_packed_with_probe(
n: usize,
packed: &mut [f64],
probe: &mut [f64],
) -> (Vec<f64>, Vec<f64>) {
let mut diagonal = vec![0.0_f64; n];
let mut offdiagonal = vec![0.0_f64; n];
let mut reflector = vec![0.0_f64; n];
let mut image = vec![0.0_f64; n];
let mut partner = vec![0.0_f64; n];
let mut tail: &mut [f64] = packed;
for k in 0..n.saturating_sub(1) {
let m = n - 1 - k;
diagonal[k] = tail[0];
if m == 1 {
offdiagonal[k] = tail[1];
let (_row, rest) = tail.split_at_mut(2);
tail = rest;
continue;
}
let x = &tail[1..=m];
let largest = x.iter().fold(0.0_f64, |acc, value| acc.max(value.abs()));
let (beta, tau) = if largest == 0.0 {
(0.0, 0.0)
} else {
let alpha = x[0] / largest;
let tail_norm = vector_norm_scaled(&x[1..], largest);
if tail_norm == 0.0 {
(x[0], 0.0)
} else {
let magnitude = alpha.hypot(tail_norm);
let beta = if alpha >= 0.0 { -magnitude } else { magnitude };
let tau = (beta - alpha) / beta;
let scale = 1.0 / (alpha - beta);
reflector[0] = 1.0;
for i in 1..m {
reflector[i] = (x[i] / largest) * scale;
}
(beta * largest, tau)
}
};
offdiagonal[k] = beta;
let (_row_k, rest) = tail.split_at_mut(m + 1);
tail = rest;
if tau != 0.0 {
let v = &reflector[..m];
let p = &mut image[..m];
packed_symmetric_matvec(m, tail, v, p);
for value in p.iter_mut() {
*value *= tau;
}
let correction = -0.5 * tau * dot(p, v);
for (target, (&pi, &vi)) in partner[..m].iter_mut().zip(p.iter().zip(v.iter())) {
*target = pi + correction * vi;
}
packed_symmetric_rank2_downdate(m, tail, v, &partner[..m]);
let block = &mut probe[k + 1..];
let scale = tau * dot(v, block);
for (target, &vi) in block.iter_mut().zip(v.iter()) {
*target -= scale * vi;
}
}
}
diagonal[n - 1] = tail[0];
(diagonal, offdiagonal)
}
fn packed_symmetric_matvec(m: usize, packed: &[f64], v: &[f64], p: &mut [f64]) {
if m < PARALLEL_MIN_ROWS || rayon::current_num_threads() < 2 {
p.fill(0.0);
serial_packed_symmetric_matvec(m, packed, v, p, 0, m);
return;
}
let tasks = rayon::current_num_threads().min(m.div_ceil(PARALLEL_MIN_ROWS)).max(1);
let chunk = m.div_ceil(tasks);
let partials: Vec<Vec<f64>> = (0..tasks)
.into_par_iter()
.map(|task| {
let lo = task * chunk;
let hi = ((task + 1) * chunk).min(m);
let mut local = vec![0.0_f64; m];
if lo < hi {
serial_packed_symmetric_matvec(m, packed, v, &mut local, lo, hi);
}
local
})
.collect();
p.fill(0.0);
for local in &partials {
for (target, &value) in p.iter_mut().zip(local.iter()) {
*target += value;
}
}
}
fn serial_packed_symmetric_matvec(
m: usize,
packed: &[f64],
v: &[f64],
p: &mut [f64],
lo: usize,
hi: usize,
) {
for i in lo..hi {
let base = packed_upper_row_offset(m, i);
let row = &packed[base..base + (m - i)];
let vi = v[i];
let mut accumulated = row[0] * vi;
for (offset, &entry) in row.iter().enumerate().skip(1) {
accumulated += entry * v[i + offset];
p[i + offset] += entry * vi;
}
p[i] += accumulated;
}
}
fn packed_symmetric_rank2_downdate(m: usize, packed: &mut [f64], v: &[f64], w: &[f64]) {
if m < PARALLEL_MIN_ROWS || rayon::current_num_threads() < 2 {
serial_packed_symmetric_rank2_downdate(m, packed, v, w, 0);
return;
}
let mut rows: Vec<(usize, &mut [f64])> = Vec::with_capacity(m);
let mut rest = packed;
for i in 0..m {
let (row, next) = rest.split_at_mut(m - i);
rows.push((i, row));
rest = next;
}
rows.into_par_iter().for_each(|(i, row)| {
let vi = v[i];
let wi = w[i];
for (offset, entry) in row.iter_mut().enumerate() {
*entry -= vi * w[i + offset] + wi * v[i + offset];
}
});
}
fn serial_packed_symmetric_rank2_downdate(
m: usize,
packed: &mut [f64],
v: &[f64],
w: &[f64],
from_row: usize,
) {
for i in from_row..m {
let base = packed_upper_row_offset(m, i);
let vi = v[i];
let wi = w[i];
for offset in 0..(m - i) {
packed[base + offset] -= vi * w[i + offset] + wi * v[i + offset];
}
}
}
fn implicit_ql_with_probe(
diagonal: &mut [f64],
offdiagonal: &mut [f64],
probe: &mut [f64],
) -> Result<(), String> {
let n = diagonal.len();
if n <= 1 {
return Ok(());
}
offdiagonal[n - 1] = 0.0;
let mut norm = 0.0_f64;
for i in 0..n {
let row = diagonal[i].abs()
+ if i > 0 { offdiagonal[i - 1].abs() } else { 0.0 }
+ offdiagonal[i].abs();
norm = norm.max(row);
}
let deflation_floor = f64::EPSILON * norm;
for l in 0..n {
let mut sweeps = 0usize;
loop {
let mut split = l;
while split + 1 < n {
let scale = diagonal[split].abs() + diagonal[split + 1].abs();
if offdiagonal[split].abs() + scale == scale
|| offdiagonal[split].abs() <= deflation_floor
{
break;
}
split += 1;
}
if split == l {
break;
}
if sweeps == QL_MAX_SWEEPS_PER_EIGENVALUE {
return Err(format!(
"packed symmetric spectrum: implicit QL did not deflate eigenvalue {l} in \
{QL_MAX_SWEEPS_PER_EIGENVALUE} sweeps (block {l}..={split})"
));
}
sweeps += 1;
let mut g = (diagonal[l + 1] - diagonal[l]) / (2.0 * offdiagonal[l]);
let mut r = g.hypot(1.0);
g = diagonal[split] - diagonal[l]
+ offdiagonal[l] / (g + if g >= 0.0 { r.abs() } else { -r.abs() });
let mut s = 1.0_f64;
let mut c = 1.0_f64;
let mut p = 0.0_f64;
let mut deflated_early = false;
for i in (l..split).rev() {
let mut f = s * offdiagonal[i];
let b = c * offdiagonal[i];
r = f.hypot(g);
offdiagonal[i + 1] = r;
if r == 0.0 {
diagonal[i + 1] -= p;
offdiagonal[split] = 0.0;
deflated_early = true;
break;
}
s = f / r;
c = g / r;
g = diagonal[i + 1] - p;
r = (diagonal[i] - g) * s + 2.0 * c * b;
p = s * r;
diagonal[i + 1] = g + p;
g = c * r - b;
f = probe[i + 1];
probe[i + 1] = s * probe[i] + c * f;
probe[i] = c * probe[i] - s * f;
}
if deflated_early {
continue;
}
diagonal[l] -= p;
offdiagonal[l] = g;
offdiagonal[split] = 0.0;
}
}
Ok(())
}
fn sort_spectrum_ascending(diagonal: &mut [f64], probe: &mut [f64]) {
let n = diagonal.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
diagonal[a]
.partial_cmp(&diagonal[b])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&b))
});
let sorted_diagonal: Vec<f64> = order.iter().map(|&i| diagonal[i]).collect();
let sorted_probe: Vec<f64> = order.iter().map(|&i| probe[i]).collect();
diagonal.copy_from_slice(&sorted_diagonal);
probe.copy_from_slice(&sorted_probe);
}
fn vector_norm_scaled(values: &[f64], divisor: f64) -> f64 {
let mut sum_squares = 0.0_f64;
for &value in values {
let scaled = value / divisor;
sum_squares += scaled * scaled;
}
sum_squares.sqrt()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_finite_input_is_refused_rather_than_decomposed() {
let mut packed = vec![1.0, f64::NAN, 1.0];
let mut probe = vec![1.0, 1.0];
let error = packed_symmetric_spectrum_with_probe(2, &mut packed, &mut probe)
.expect_err("a NaN entry must refuse");
assert!(error.contains("not finite"), "unexpected error: {error}");
let mut packed = vec![1.0, 0.0, 1.0];
let mut probe = vec![1.0, f64::INFINITY];
let error = packed_symmetric_spectrum_with_probe(2, &mut packed, &mut probe)
.expect_err("a non-finite probe must refuse");
assert!(error.contains("probe entry"), "unexpected error: {error}");
}
}