qualia_core_db/solvers/linear_algebra/
eigen.rs1use crate::solvers::SolversError;
16
17pub fn symmetric_eigen_3x3(a: &[f64; 9]) -> [f64; 3] {
26 let (sxx, syy, szz) = (a[0], a[4], a[8]);
27 let (sxy, szx, syz) = (a[1], a[2], a[5]);
29 let p1 = sxy * sxy + syz * syz + szx * szx;
30 let q = (sxx + syy + szz) / 3.0;
31 if p1 <= 1e-18 {
32 let mut e = [sxx, syy, szz];
34 e.sort_by(|a, b| b.partial_cmp(a).unwrap_or(core::cmp::Ordering::Equal));
35 return e;
36 }
37 let p2 = (sxx - q).powi(2) + (syy - q).powi(2) + (szz - q).powi(2) + 2.0 * p1;
38 let p = (p2 / 6.0).sqrt();
39 let (b00, b11, b22) = ((sxx - q) / p, (syy - q) / p, (szz - q) / p);
41 let (b01, b12, b02) = (sxy / p, syz / p, szx / p);
42 let det_b = b00 * (b11 * b22 - b12 * b12) - b01 * (b01 * b22 - b12 * b02)
43 + b02 * (b01 * b12 - b11 * b02);
44 let r = (det_b / 2.0).clamp(-1.0, 1.0);
45 let phi = r.acos() / 3.0;
46 let e1 = q + 2.0 * p * phi.cos();
47 let e3 = q + 2.0 * p * (phi + 2.0 * core::f64::consts::PI / 3.0).cos();
48 let e2 = 3.0 * q - e1 - e3; [e1, e2, e3]
50}
51
52pub fn symmetric_eigen(n: usize, a: &mut [f64], eigvecs: &mut [f64]) -> Result<(), SolversError> {
63 if n == 0 || a.len() != n * n || eigvecs.len() != n * n {
64 return Err(SolversError::InvalidDimension);
65 }
66 let scale = a.iter().fold(0.0_f64, |m, &v| m.max(v.abs())).max(1.0);
67 for i in 0..n {
69 for j in (i + 1)..n {
70 if (a[i * n + j] - a[j * n + i]).abs() > 1e-9 * scale {
71 return Err(SolversError::InvalidParameters);
72 }
73 }
74 }
75 for x in eigvecs.iter_mut() {
77 *x = 0.0;
78 }
79 for i in 0..n {
80 eigvecs[i * n + i] = 1.0;
81 }
82
83 const MAX_SWEEPS: usize = 100;
84 for _ in 0..MAX_SWEEPS {
85 let mut off = 0.0_f64;
87 for p in 0..n {
88 for q in (p + 1)..n {
89 off += a[p * n + q] * a[p * n + q];
90 }
91 }
92 if off.sqrt() <= 1e-15 * scale {
93 break;
94 }
95 for p in 0..n {
96 for q in (p + 1)..n {
97 let apq = a[p * n + q];
98 if apq == 0.0 {
99 continue;
100 }
101 let app = a[p * n + p];
102 let aqq = a[q * n + q];
103 let theta = (aqq - app) / (2.0 * apq);
104 let sign = if theta >= 0.0 { 1.0 } else { -1.0 };
105 let t = sign / (theta.abs() + (theta * theta + 1.0).sqrt());
106 let c = 1.0 / (t * t + 1.0).sqrt();
107 let s = t * c;
108 for k in 0..n {
110 let akp = a[k * n + p];
111 let akq = a[k * n + q];
112 a[k * n + p] = c * akp - s * akq;
113 a[k * n + q] = s * akp + c * akq;
114 }
115 for k in 0..n {
117 let apk = a[p * n + k];
118 let aqk = a[q * n + k];
119 a[p * n + k] = c * apk - s * aqk;
120 a[q * n + k] = s * apk + c * aqk;
121 }
122 for k in 0..n {
124 let vkp = eigvecs[k * n + p];
125 let vkq = eigvecs[k * n + q];
126 eigvecs[k * n + p] = c * vkp - s * vkq;
127 eigvecs[k * n + q] = s * vkp + c * vkq;
128 }
129 }
130 }
131 }
132 Ok(())
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn approx(a: f64, b: f64, tol: f64) {
140 assert!((a - b).abs() < tol, "{a} != {b} (tol {tol})");
141 }
142
143 #[test]
144 fn closed_form_diagonal() {
145 let a = [3.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0];
147 let e = symmetric_eigen_3x3(&a);
148 approx(e[0], 3.0, 1e-12);
149 approx(e[1], 2.0, 1e-12);
150 approx(e[2], 1.0, 1e-12);
151 }
152
153 #[test]
154 fn closed_form_known_symmetric() {
155 let a = [2.0, 0.0, 0.0, 0.0, 3.0, 4.0, 0.0, 4.0, 9.0];
158 let e = symmetric_eigen_3x3(&a);
159 approx(e[0], 11.0, 1e-9);
160 approx(e[1], 2.0, 1e-9);
161 approx(e[2], 1.0, 1e-9);
162 approx(e[0] + e[1] + e[2], 14.0, 1e-9);
164 }
165
166 #[test]
167 fn closed_form_matches_jacobi() {
168 let a = [4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
169 let closed = symmetric_eigen_3x3(&a);
170 let mut work = a;
171 let mut v = [0.0; 9];
172 symmetric_eigen(3, &mut work, &mut v).unwrap();
173 let mut jac = [work[0], work[4], work[8]];
174 jac.sort_by(|x, y| y.partial_cmp(x).unwrap());
175 for i in 0..3 {
176 approx(closed[i], jac[i], 1e-7);
177 }
178 }
179
180 #[test]
181 fn jacobi_eigenvectors_reconstruct() {
182 let a0 = [4.0, 1.0, 2.0, 1.0, 5.0, 3.0, 2.0, 3.0, 6.0];
184 let mut a = a0;
185 let mut v = [0.0; 9];
186 symmetric_eigen(3, &mut a, &mut v).unwrap();
187 for j in 0..3 {
188 let lambda = a[j * 3 + j];
189 let vj = [v[j], v[3 + j], v[6 + j]];
191 for i in 0..3 {
193 let mut s = 0.0;
194 for k in 0..3 {
195 s += a0[i * 3 + k] * vj[k];
196 }
197 approx(s, lambda * vj[i], 1e-7);
198 }
199 }
200 }
201
202 #[test]
203 fn jacobi_rejects_asymmetric() {
204 let mut a = [1.0, 2.0, 3.0, 4.0]; let mut v = [0.0; 4];
206 assert_eq!(
207 symmetric_eigen(2, &mut a, &mut v),
208 Err(SolversError::InvalidParameters)
209 );
210 }
211
212 #[test]
213 fn jacobi_rejects_bad_dims() {
214 let mut a = [1.0, 2.0, 3.0];
215 let mut v = [0.0; 4];
216 assert_eq!(
217 symmetric_eigen(2, &mut a, &mut v),
218 Err(SolversError::InvalidDimension)
219 );
220 }
221}