1use crate::{C64, SCALE_FLOOR};
42use comnum_core::{Error, Result};
43use ndarray::{Array2, ArrayView2, ShapeBuilder};
44
45const MIN_PIVOT: f64 = 1e-300;
48
49pub type CMat = Array2<C64>;
51
52pub fn identity(n: usize) -> CMat {
54 Array2::from_shape_fn((n, n), |(i, j)| if i == j { C64::new(1.0, 0.0) } else { C64::new(0.0, 0.0) })
55}
56
57pub fn hermitian(a: &ArrayView2<'_, C64>) -> CMat {
59 a.t().mapv(|x| x.conj())
60}
61
62pub fn matmul(a: &ArrayView2<'_, C64>, b: &ArrayView2<'_, C64>) -> CMat {
67 a.dot(b)
68}
69
70pub fn matvec(a: &ArrayView2<'_, C64>, x: &[C64]) -> Vec<C64> {
76 assert_eq!(x.len(), a.ncols(), "matvec: {} values for a matrix of {} columns", x.len(), a.ncols());
77 a.rows().into_iter().map(|r| r.iter().zip(x).map(|(u, v)| u * v).sum()).collect()
78}
79
80pub fn gram(a: &ArrayView2<'_, C64>) -> CMat {
82 hermitian(a).dot(a)
83}
84
85pub fn cholesky(a: &ArrayView2<'_, C64>) -> Result<CMat> {
91 let n = a.nrows();
92 if a.ncols() != n {
93 return Err(Error::Shape(format!("Cholesky of a {}×{} matrix", n, a.ncols())));
94 }
95 let mut l = Array2::<C64>::zeros((n, n));
96 for j in 0..n {
97 let mut d = a[[j, j]].re;
98 for k in 0..j {
99 d -= l[[j, k]].norm_sqr();
100 }
101 if d.is_nan() || d <= MIN_PIVOT {
102 return Err(Error::Numerical("the matrix is not positive definite".into()));
103 }
104 let d = d.sqrt();
105 l[[j, j]] = C64::new(d, 0.0);
106 for i in j + 1..n {
107 let mut s = a[[i, j]];
108 for k in 0..j {
109 s -= l[[i, k]] * l[[j, k]].conj();
110 }
111 l[[i, j]] = s / d;
112 }
113 }
114 Ok(l)
115}
116
117pub fn solve_hpd(a: &ArrayView2<'_, C64>, b: &ArrayView2<'_, C64>) -> Result<CMat> {
122 solve_cholesky(&cholesky(a)?.view(), b)
123}
124
125pub fn solve_cholesky(l: &ArrayView2<'_, C64>, b: &ArrayView2<'_, C64>) -> Result<CMat> {
144 let n = l.nrows();
145 if b.nrows() != n {
146 return Err(Error::Shape(format!("system of size {n} with {} right-hand rows", b.nrows())));
147 }
148 let mut x = b.to_owned();
149 for c in 0..x.ncols() {
150 for i in 0..n {
152 let mut s = x[[i, c]];
153 for k in 0..i {
154 s -= l[[i, k]] * x[[k, c]];
155 }
156 x[[i, c]] = s / l[[i, i]];
157 }
158 for i in (0..n).rev() {
160 let mut s = x[[i, c]];
161 for k in i + 1..n {
162 s -= l[[k, i]].conj() * x[[k, c]];
163 }
164 x[[i, c]] = s / l[[i, i]];
165 }
166 }
167 Ok(x)
168}
169
170pub fn inverse_hpd(a: &ArrayView2<'_, C64>) -> Result<CMat> {
175 solve_hpd(a, &identity(a.nrows()).view())
176}
177
178pub fn log2_det_hpd(a: &ArrayView2<'_, C64>) -> Result<f64> {
183 let l = cholesky(a)?;
184 Ok(2.0 * (0..l.nrows()).map(|i| l[[i, i]].re.log2()).sum::<f64>())
185}
186
187pub fn qr(a: &ArrayView2<'_, C64>) -> Result<(CMat, CMat)> {
196 let (m, n) = a.dim();
197 if m < n {
198 return Err(Error::Shape(format!("thin QR of a {m}×{n} matrix needs m ≥ n")));
199 }
200 let mut q = a.to_owned();
201 let mut r = Array2::<C64>::zeros((n, n));
202 let scale = a.iter().map(C64::norm_sqr).sum::<f64>().sqrt().max(SCALE_FLOOR);
203 for j in 0..n {
204 for _pass in 0..2 {
205 for k in 0..j {
206 let c: C64 = (0..m).map(|i| q[[i, k]].conj() * q[[i, j]]).sum();
207 r[[k, j]] += c;
208 for i in 0..m {
209 let qk = q[[i, k]];
210 q[[i, j]] -= c * qk;
211 }
212 }
213 }
214 let norm = (0..m).map(|i| q[[i, j]].norm_sqr()).sum::<f64>().sqrt();
215 if norm <= 1e-14 * scale {
216 r[[j, j]] = C64::new(0.0, 0.0);
217 for i in 0..m {
218 q[[i, j]] = C64::new(0.0, 0.0);
219 }
220 } else {
221 r[[j, j]] = C64::new(norm, 0.0);
222 for i in 0..m {
223 q[[i, j]] /= norm;
224 }
225 }
226 }
227 Ok((q, r))
228}
229
230pub fn svd(a: &ArrayView2<'_, C64>) -> (CMat, Vec<f64>, CMat) {
236 let (m, n) = a.dim();
237 if m < n {
238 let (u, s, v) = svd(&hermitian(a).view());
239 return (v, s, u);
240 }
241 let mut w = Array2::<C64>::zeros((m, n).f());
244 w.assign(a);
245 let mut v = Array2::<C64>::zeros((n, n).f());
246 v.assign(&identity(n));
247 for _sweep in 0..60 {
248 let mut rotated = false;
249 for p in 0..n {
250 for q in p + 1..n {
251 let alpha: f64 = w.column(p).iter().map(C64::norm_sqr).sum();
252 let beta: f64 = w.column(q).iter().map(C64::norm_sqr).sum();
253 let gamma: C64 = w.column(p).iter().zip(w.column(q).iter()).map(|(x, y)| x.conj() * y).sum();
254 let g = gamma.norm();
255 if g <= 1e-15 * (alpha * beta).sqrt() || g == 0.0 {
256 continue;
257 }
258 rotated = true;
259 let phase = (gamma / g).conj();
261 let zeta = (beta - alpha) / (2.0 * g);
262 let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
263 let c = 1.0 / (1.0 + t * t).sqrt();
264 let s = c * t;
265 for mat in [&mut w, &mut v] {
266 for i in 0..mat.nrows() {
267 let xp = mat[[i, p]];
268 let xq = mat[[i, q]] * phase;
269 mat[[i, p]] = xp * c - xq * s;
270 mat[[i, q]] = xp * s + xq * c;
271 }
272 }
273 }
274 }
275 if !rotated {
276 break;
277 }
278 }
279 let mut order: Vec<(f64, usize)> =
280 (0..n).map(|j| (w.column(j).iter().map(C64::norm_sqr).sum::<f64>().sqrt(), j)).collect();
281 order.sort_by(|x, y| y.0.total_cmp(&x.0));
282 let mut u = Array2::<C64>::zeros((m, n));
283 let mut vs = Array2::<C64>::zeros((n, n));
284 let mut sv = Vec::with_capacity(n);
285 let top = order.first().map_or(0.0, |o| o.0);
286 for (k, &(sk, j)) in order.iter().enumerate() {
287 sv.push(sk);
288 vs.column_mut(k).assign(&v.column(j));
289 if sk > 1e-14 * top && sk > 0.0 {
290 u.column_mut(k).assign(&w.column(j).mapv(|x| x / sk));
291 }
292 }
293 (u, sv, vs)
294}
295
296pub fn solve_tridiagonal(lower: &[f64], diag: &[f64], upper: &[f64], rhs: &[C64]) -> Vec<C64> {
309 let n = diag.len();
310 let mut c = vec![0.0; n];
311 let mut d = vec![C64::new(0.0, 0.0); n];
312 for i in 0..n {
313 let den = diag[i] - if i > 0 { lower[i] * c[i - 1] } else { 0.0 };
314 c[i] = upper[i] / den;
315 d[i] = (rhs[i] - if i > 0 { d[i - 1] * lower[i] } else { C64::new(0.0, 0.0) }) / den;
316 }
317 for i in (0..n.saturating_sub(1)).rev() {
318 let next = d[i + 1];
319 d[i] -= next * c[i];
320 }
321 d
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use comnum_core::rng::Rng;
328
329 #[test]
330 #[should_panic(expected = "matvec: 2 values for a matrix of 3 columns")]
331 fn matvec_rejects_a_short_vector_instead_of_truncating() {
332 let a = Array2::from_elem((2, 3), C64::new(1.0, 0.0));
333 let _ = matvec(&a.view(), &[C64::new(1.0, 0.0); 2]);
334 }
335
336 fn random(m: usize, n: usize, seed: u64) -> CMat {
337 let mut r = Rng::new(seed);
338 Array2::from_shape_fn((m, n), |_| r.complex_gaussian())
339 }
340
341 fn close(a: &CMat, b: &CMat, tol: f64) -> bool {
342 a.dim() == b.dim() && a.iter().zip(b.iter()).all(|(x, y)| (x - y).norm() < tol)
343 }
344
345 #[test]
346 fn qr_is_orthonormal_and_triangular() {
347 for (m, n, seed) in [(4, 4, 1), (6, 3, 2), (8, 8, 3)] {
348 let a = random(m, n, seed);
349 let (q, r) = qr(&a.view()).unwrap();
350 assert!(close(&q.dot(&r), &a, 1e-12));
351 assert!(close(&gram(&q.view()), &identity(n), 1e-12));
352 for i in 0..n {
353 assert!(r[[i, i]].im == 0.0 && r[[i, i]].re >= 0.0);
354 for j in 0..i {
355 assert_eq!(r[[i, j]], C64::new(0.0, 0.0));
356 }
357 }
358 }
359 }
360
361 #[test]
362 fn cholesky_solve_and_inverse() {
363 let h = random(6, 4, 4);
364 let a = gram(&h.view()) + identity(4).mapv(|x| x * 0.1);
365 let l = cholesky(&a.view()).unwrap();
366 assert!(close(&l.dot(&hermitian(&l.view())), &a, 1e-12));
367 let inv = inverse_hpd(&a.view()).unwrap();
368 assert!(close(&a.dot(&inv), &identity(4), 1e-10));
369 assert!(cholesky(&(identity(2).mapv(|x| -x)).view()).is_err());
370 let d = Array2::from_diag(&ndarray::arr1(&[C64::new(2.0, 0.0), C64::new(8.0, 0.0)]));
372 assert!((log2_det_hpd(&d.view()).unwrap() - 4.0).abs() < 1e-12);
373 }
374
375 #[test]
376 fn svd_rebuilds_the_matrix_with_orthonormal_factors() {
377 for (m, n, seed) in [(4, 4, 5), (6, 2, 6), (2, 5, 7), (8, 8, 8)] {
378 let a = random(m, n, seed);
379 let (u, s, v) = svd(&a.view());
380 let k = m.min(n);
381 assert_eq!(s.len(), k);
382 assert!(s.windows(2).all(|w| w[0] >= w[1]));
383 let mut us = u.clone();
384 for (j, sj) in s.iter().enumerate() {
385 us.column_mut(j).mapv_inplace(|x| x * *sj);
386 }
387 assert!(close(&us.dot(&hermitian(&v.view())), &a, 1e-11), "{m}×{n}");
388 assert!(close(&gram(&u.view()), &identity(k), 1e-11));
389 assert!(close(&gram(&v.view()), &identity(k), 1e-11));
390 let f: f64 = a.iter().map(C64::norm_sqr).sum();
392 assert!((s.iter().map(|x| x * x).sum::<f64>() - f).abs() < 1e-10 * f);
393 }
394 }
395
396 #[test]
397 fn svd_of_a_rank_one_matrix() {
398 let x = random(3, 1, 9);
399 let y = random(1, 4, 10);
400 let a = x.dot(&y);
401 let (_, s, _) = svd(&a.view());
402 assert!(s[0] > 1.0 && s[1..].iter().all(|v| *v < 1e-12 * s[0]));
403 }
404}