#![allow(clippy::needless_range_loop)]
fn jacobi_eigen(input: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
let n = input.len();
let mut a: Vec<Vec<f64>> = input.to_vec();
let mut v: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
for _ in 0..60 {
let mut off = 0.0;
for i in 0..n {
for j in (i + 1)..n {
off += a[i][j] * a[i][j];
}
}
if off <= 1e-24 {
break;
}
for p in 0..n {
for q in (p + 1)..n {
if a[p][q].abs() < 1e-18 {
continue;
}
let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for k in 0..n {
let akp = a[k][p];
let akq = a[k][q];
a[k][p] = c * akp - s * akq;
a[k][q] = s * akp + c * akq;
}
for k in 0..n {
let apk = a[p][k];
let aqk = a[q][k];
a[p][k] = c * apk - s * aqk;
a[q][k] = s * apk + c * aqk;
}
for k in 0..n {
let vkp = v[k][p];
let vkq = v[k][q];
v[k][p] = c * vkp - s * vkq;
v[k][q] = s * vkp + c * vkq;
}
}
}
}
let values: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
let vectors: Vec<Vec<f64>> = (0..n).map(|i| (0..n).map(|k| v[k][i]).collect()).collect();
(values, vectors)
}
fn project_psd(q: &[Vec<f64>], floor: f64) -> Vec<Vec<f64>> {
let n = q.len();
let (vals, vecs) = jacobi_eigen(q);
let mut out = vec![vec![0.0f64; n]; n];
for (lambda, vec) in vals.iter().zip(vecs.iter()) {
let w = lambda.max(floor);
if w == 0.0 {
continue;
}
for i in 0..n {
for j in 0..n {
out[i][j] += w * vec[i] * vec[j];
}
}
}
out
}
pub fn min_eigenvalue(q: &[Vec<f64>]) -> f64 {
let (vals, _) = jacobi_eigen(q);
vals.into_iter().fold(f64::INFINITY, f64::min)
}
pub fn smallest_magnitude_eigenvectors(q: &[Vec<f64>], k: usize) -> Vec<Vec<f64>> {
let (vals, vecs) = jacobi_eigen(q);
let mut idx: Vec<usize> = (0..vals.len()).collect();
idx.sort_by(|&a, &b| {
vals[a]
.abs()
.partial_cmp(&vals[b].abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
idx.into_iter().take(k).map(|i| vecs[i].clone()).collect()
}
fn dot(a: &[Vec<f64>], b: &[Vec<f64>]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(ra, rb)| ra.iter().zip(rb.iter()).map(|(x, y)| x * y).sum::<f64>())
.sum()
}
fn mat_combo(alpha: f64, a: &[Vec<f64>], beta: f64, b: &[Vec<f64>]) -> Vec<Vec<f64>> {
a.iter()
.zip(b.iter())
.map(|(ra, rb)| {
ra.iter()
.zip(rb.iter())
.map(|(x, y)| alpha * x + beta * y)
.collect()
})
.collect()
}
fn solve_spd(g: &[Vec<f64>], rhs: &[f64]) -> Option<Vec<f64>> {
let n = g.len();
let mut l = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in 0..=i {
let mut s = g[i][j];
for k in 0..j {
s -= l[i][k] * l[j][k];
}
if i == j {
if s <= 1e-12 {
return None;
}
l[i][j] = s.sqrt();
} else {
l[i][j] = s / l[j][j];
}
}
}
let mut y = vec![0.0f64; n];
for i in 0..n {
let mut s = rhs[i];
for k in 0..i {
s -= l[i][k] * y[k];
}
y[i] = s / l[i][i];
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
let mut s = y[i];
for k in (i + 1)..n {
s -= l[k][i] * x[k];
}
x[i] = s / l[i][i];
}
Some(x)
}
pub struct Family {
base: Vec<Vec<f64>>,
dirs: Vec<Vec<Vec<f64>>>,
gram: Vec<Vec<f64>>,
}
impl Family {
pub fn new(base: Vec<Vec<f64>>, dirs: Vec<Vec<Vec<f64>>>) -> Self {
let k = dirs.len();
let mut gram = vec![vec![0.0f64; k]; k];
for i in 0..k {
for j in 0..=i {
let v = dot(&dirs[i], &dirs[j]);
gram[i][j] = v;
gram[j][i] = v;
}
}
Family { base, dirs, gram }
}
pub fn at(&self, t: &[f64]) -> Vec<Vec<f64>> {
let n = self.base.len();
let mut q = self.base.clone();
for (tk, dir) in t.iter().zip(self.dirs.iter()) {
if *tk == 0.0 {
continue;
}
for i in 0..n {
for j in 0..n {
q[i][j] += tk * dir[i][j];
}
}
}
q
}
fn parameters_of(&self, q: &[Vec<f64>]) -> Option<Vec<f64>> {
let n = self.base.len();
let mut delta = q.to_vec();
for i in 0..n {
for j in 0..n {
delta[i][j] -= self.base[i][j];
}
}
let rhs: Vec<f64> = self.dirs.iter().map(|d| dot(d, &delta)).collect();
solve_spd(&self.gram, &rhs)
}
pub fn search(&self, floor: f64, iters: usize) -> Option<Vec<f64>> {
self.search_from(vec![0.0f64; self.dirs.len()], floor, iters)
}
pub fn search_from(&self, start: Vec<f64>, floor: f64, iters: usize) -> Option<Vec<f64>> {
let mut t = start;
let mut q = self.at(&t);
for _ in 0..iters {
let projected = project_psd(&q, floor);
let next = self.parameters_of(&projected)?;
let moved: f64 = next
.iter()
.zip(t.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
t = next;
q = self.at(&t);
if moved < 1e-13 {
break;
}
}
if t.iter().any(|v| !v.is_finite()) {
return None;
}
Some(t)
}
pub fn douglas_rachford_from(
&self,
start: Vec<f64>,
floor: f64,
lambda: f64,
iters: usize,
) -> Option<Vec<f64>> {
let mut q = self.at(&start);
for _ in 0..iters {
let pa_t = self.parameters_of(&q)?;
let pa = self.at(&pa_t);
let r_family = mat_combo(2.0, &pa, -1.0, &q);
let pb = project_psd(&r_family, floor);
let step = mat_combo(1.0, &pb, -1.0, &pa);
let q_next = mat_combo(1.0, &q, lambda, &step);
let moved: f64 = q_next
.iter()
.zip(q.iter())
.flat_map(|(ra, rb)| ra.iter().zip(rb.iter()))
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
q = q_next;
if moved < 1e-13 {
break;
}
}
let final_t = self.parameters_of(&q)?;
if final_t.iter().any(|v| !v.is_finite()) {
return None;
}
Some(final_t)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jacobi_diagonalises_a_known_matrix() {
let a = vec![vec![2.0, 1.0], vec![1.0, 2.0]];
let (mut vals, _) = jacobi_eigen(&a);
vals.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert!((vals[0] - 1.0).abs() < 1e-10, "{vals:?}");
assert!((vals[1] - 3.0).abs() < 1e-10, "{vals:?}");
}
#[test]
fn min_eigenvalue_sees_indefiniteness() {
let a = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
assert!(min_eigenvalue(&a) < -0.9);
}
#[test]
fn psd_projection_clips_negative_eigenvalues() {
let a = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
let p = project_psd(&a, 0.0);
assert!(min_eigenvalue(&p) > -1e-9);
}
#[test]
fn search_finds_a_psd_point_of_a_family() {
let base = vec![vec![1.0, 1.0], vec![1.0, 0.0]];
let dirs = vec![vec![vec![0.0, 0.0], vec![0.0, 1.0]]];
let fam = Family::new(base, dirs);
let t = fam.search(0.25, 500).expect("search runs");
assert!(min_eigenvalue(&fam.at(&t)) > -1e-9, "t = {t:?}");
assert!(t[0] >= 1.0, "t = {t:?}");
}
#[test]
fn search_on_an_empty_family_is_a_no_op() {
let fam = Family::new(vec![vec![1.0]], Vec::new());
let t = fam.search(0.1, 10).expect("no parameters");
assert!(t.is_empty());
}
}