use crate::Complex64;
use crate::constants::{C_FALSE, C_TRUE};
use crate::dev_lapack;
use crate::lapack::{self, LapackError};
const TOL: f64 = 1e-10;
fn get(a: &[f64], nrow: usize, i: usize, j: usize) -> f64 {
a[i + j * nrow]
}
fn cget(a: &[Complex64], nrow: usize, i: usize, j: usize) -> Complex64 {
a[i + j * nrow]
}
fn sample(n: usize) -> Vec<f64> {
let mut a = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
a[i + j * n] = if i == j {
(n as f64) + 1.0 + (i as f64) * 0.5
} else {
(((i * 7 + j * 13) % 11) as f64) - 5.0
};
}
}
a
}
fn complex_sample(n: usize) -> Vec<Complex64> {
let mut a = vec![Complex64::new(0.0, 0.0); n * n];
for i in 0..n {
for j in 0..n {
let re = if i == j {
(n as f64) + 2.0 + (i as f64) * 0.5
} else {
(((i * 5 + j * 7) % 9) as f64) - 4.0
};
let im = (((i * 3 + j * 11) % 7) as f64) - 3.0 + if i == j { 1.0 } else { 0.0 };
a[i + j * n] = Complex64::new(re, im);
}
}
a
}
fn symmetric_sample(n: usize) -> Vec<f64> {
let raw = sample(n);
let mut a = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let (x, y) = if i <= j { (i, j) } else { (j, i) };
a[i + j * n] = get(&raw, n, x, y);
}
}
a
}
fn rect_sample(m: usize, n: usize) -> Vec<f64> {
let mut a = vec![0.0; m * n];
for i in 0..m {
for j in 0..n {
a[i + j * m] = (((i * 7 + j * 13 + 3) % 11) as f64) - 5.0 + (i as f64) * 0.25 - (j as f64) * 0.1;
}
}
a
}
fn hermitian_sample(n: usize) -> Vec<Complex64> {
let raw = complex_sample(n);
let mut h = vec![Complex64::new(0.0, 0.0); n * n];
for i in 0..n {
for j in 0..n {
let s = cget(&raw, n, i, j) + cget(&raw, n, j, i).conj();
h[i + j * n] = Complex64::new(0.5 * s.re, 0.5 * s.im);
}
}
h
}
fn compare_vectors(label: &str, got: &[f64], want: &[f64], tol: f64) {
assert_eq!(got.len(), want.len(), "{label}: length mismatch");
for i in 0..got.len() {
let scale = want[i].abs().max(1.0);
let err = (got[i] - want[i]).abs() / scale;
assert!(
err < tol,
"{label}: component {i} is {} but the reference says {} (relative error {err:e})",
got[i],
want[i]
);
}
}
#[test]
fn dgesv_agrees_with_the_reference() {
for n in [1usize, 2, 3, 5, 9] {
let a = sample(n);
let x_true: Vec<f64> = (0..n).map(|i| 1.0 + (i as f64) * 0.25).collect();
let mut b_native = vec![0.0; n];
crate::blas::dgemv(1.0, &a, &x_true, 0.0, &mut b_native, n, n);
let mut b_ref = b_native.clone();
let mut a_native = a.clone();
lapack::dgesv(&mut a_native, &mut b_native).unwrap();
let mut a_ref = a.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
let n_i32 = n as i32;
unsafe {
dev_lapack::c_dgesv(
&n_i32,
&1i32,
a_ref.as_mut_ptr(),
&n_i32,
ipiv.as_mut_ptr(),
b_ref.as_mut_ptr(),
&n_i32,
&mut info,
);
}
assert_eq!(info, 0, "reference dgesv failed for n={n}");
compare_vectors(&format!("dgesv solution (n={n})"), &b_native, &b_ref, TOL);
compare_vectors(&format!("dgesv vs known x (n={n})"), &b_native, &x_true, TOL);
}
}
#[test]
fn dgetrf_and_dgetri_agree_with_the_reference() {
for n in [1usize, 2, 4, 7] {
let a = sample(n);
let mut inv_native = a.clone();
let (ipiv_n, _det) = lapack::dgetrf(&mut inv_native, n).unwrap();
lapack::dgetri(&mut inv_native, n, &ipiv_n).unwrap();
let mut lu_ref = a.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
let n_i32 = n as i32;
let mut work = vec![0.0f64; n.max(1)];
let lwork = n_i32;
unsafe {
dev_lapack::c_dgetrf(&n_i32, &n_i32, lu_ref.as_mut_ptr(), &n_i32, ipiv.as_mut_ptr(), &mut info);
assert_eq!(info, 0, "reference dgetrf failed for n={n}");
dev_lapack::c_dgetri(&n_i32, lu_ref.as_mut_ptr(), &n_i32, ipiv.as_ptr(), work.as_mut_ptr(), &lwork, &mut info);
assert_eq!(info, 0, "reference dgetri failed for n={n}");
}
for i in 0..n {
for j in 0..n {
let got = get(&inv_native, n, i, j);
let want = get(&lu_ref, n, i, j);
let scale = want.abs().max(1.0);
assert!(
(got - want).abs() / scale < TOL,
"dgetri ({n}): element ({i},{j}) is {got} but the reference says {want}"
);
}
}
}
}
#[test]
fn dpotrf_agrees_with_the_reference() {
for n in [1usize, 2, 3, 6] {
let raw = sample(n);
let mut t = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += get(&raw, n, k, i) * get(&raw, n, k, j);
}
t[i + j * n] = if i == j { sum + n as f64 } else { sum };
}
}
let mut l_native = t.clone();
lapack::dpotrf(&mut l_native, n).unwrap();
let mut l_ref = t.clone();
let mut info = 0i32;
let n_i32 = n as i32;
unsafe {
dev_lapack::c_dpotrf(C_FALSE, &n_i32, l_ref.as_mut_ptr(), &n_i32, &mut info);
}
assert_eq!(info, 0, "reference dpotrf failed for n={n}");
for i in 0..n {
for j in 0..=i {
let got = get(&l_native, n, i, j);
let want = get(&l_ref, n, i, j);
let scale = want.abs().max(1.0);
assert!(
(got - want).abs() / scale < TOL,
"dpotrf ({n}): L({i},{j}) is {got} but the reference says {want}"
);
}
}
}
}
#[test]
fn singular_detection_agrees_with_the_reference() {
for n in [2usize, 3, 5] {
let mut sing = sample(n);
for i in 0..n {
let v = get(&sing, n, i, 0) * 2.0;
sing[i + n] = v;
}
let mut native = sing.clone();
let native_result = lapack::dgetrf(&mut native, n);
let mut a_ref = sing.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
let n_i32 = n as i32;
unsafe {
dev_lapack::c_dgetrf(&n_i32, &n_i32, a_ref.as_mut_ptr(), &n_i32, ipiv.as_mut_ptr(), &mut info);
}
assert_eq!(
native_result,
Err(LapackError::Singular),
"the native dgetrf must report a singular matrix (n={n})"
);
assert!(info > 0, "the reference dgetrf must report a singular matrix (n={n})");
}
}
#[test]
fn dhseqr_eigenvalues_agree_with_the_reference() {
for n in [1usize, 2, 3, 5, 8, 15] {
let a = sample(n);
let mut t = a.clone();
lapack::eig::dgehrd(&mut t, n, None);
let mut wr = vec![0.0; n];
let mut wi = vec![0.0; n];
lapack::eig::dhseqr(&mut t, &mut wr, &mut wi, n).unwrap();
for j in 0..n {
for i in (j + 2)..n {
let t_ij = get(&t, n, i, j);
assert!(t_ij.abs() < 1e-12, "n={n}: T({i},{j}) = {t_ij} is below the first subdiagonal");
}
}
let mut i = 0;
while i < n {
if i + 1 < n && get(&t, n, i + 1, i) != 0.0 {
assert!(
(get(&t, n, i, i) - get(&t, n, i + 1, i + 1)).abs() < 1e-10,
"n={n}: the 2x2 block at {i} has unequal diagonal entries"
);
assert!(
get(&t, n, i + 1, i) * get(&t, n, i, i + 1) < 0.0,
"n={n}: the 2x2 block at {i} is not in standard form"
);
assert!(wi[i] > 0.0, "n={n}: the first of a pair must have wi > 0, got {}", wi[i]);
assert!((wi[i] + wi[i + 1]).abs() < 1e-10, "n={n}: the pair at {i} is not conjugate");
i += 2;
} else {
assert_eq!(wi[i], 0.0, "n={n}: a real eigenvalue must have wi == 0");
i += 1;
}
}
let mut a_ref = a.clone();
let mut wr_ref = vec![0.0; n];
let mut wi_ref = vec![0.0; n];
let info = dev_lapack::query::dgeev(&mut a_ref, n, &mut wr_ref, &mut wi_ref, None);
assert_eq!(info, 0, "reference dgeev failed for n={n}");
let mut got: Vec<(f64, f64)> = (0..n).map(|i| (wr[i], wi[i])).collect();
let mut want: Vec<(f64, f64)> = (0..n).map(|i| (wr_ref[i], wi_ref[i])).collect();
got.sort_by(|x, y| x.partial_cmp(y).unwrap());
want.sort_by(|x, y| x.partial_cmp(y).unwrap());
for i in 0..n {
let scale = want[i].0.abs().max(want[i].1.abs()).max(1.0);
assert!(
(got[i].0 - want[i].0).abs() / scale < TOL && (got[i].1 - want[i].1).abs() / scale < TOL,
"n={n}: eigenvalue {i} is {}+{}i but the reference says {}+{}i",
got[i].0,
got[i].1,
want[i].0,
want[i].1
);
}
}
}
#[test]
fn complex_cholesky_agrees_with_the_reference() {
for n in [1usize, 2, 3, 6] {
let raw = complex_sample(n);
let mut t = vec![Complex64::new(0.0, 0.0); n * n];
for i in 0..n {
for j in 0..n {
let mut s = Complex64::new(0.0, 0.0);
for k in 0..n {
s += cget(&raw, n, k, i).conj() * cget(&raw, n, k, j);
}
t[i + j * n] = s;
}
}
for i in 0..n {
let d = t[i + i * n];
t[i + i * n] = Complex64::new(d.re + n as f64, 0.0);
}
for upper in [false, true] {
let mut l_native = t.clone();
let mut l_ref = t.clone();
lapack::complex::zpotrf(&mut l_native, n, upper).expect("native Cholesky");
let mut info = 0i32;
let n_i32 = n as i32;
let c_upper = if upper { C_TRUE } else { C_FALSE };
unsafe {
dev_lapack::c_zpotrf(c_upper, &n_i32, l_ref.as_mut_ptr(), &n_i32, &mut info);
}
assert_eq!(info, 0, "reference zpotrf failed for n={n} upper={upper}");
for j in 0..n {
for i in 0..n {
if (upper && i <= j) || (!upper && i >= j) {
let got = cget(&l_native, n, i, j);
let want = cget(&l_ref, n, i, j);
let scale = want.norm().max(1.0);
assert!(
(got - want).norm() / scale < TOL,
"n={n} upper={upper}: Cholesky differs at ({i},{j}): {got} vs {want}"
);
}
}
}
}
}
}
#[test]
fn complex_inverse_agrees_with_the_reference() {
for n in [1usize, 2, 3, 5, 8] {
let a = complex_sample(n);
let mut inv_native = a.clone();
let ipiv_n = lapack::complex::zgetrf(&mut inv_native, n).expect("native zgetrf");
lapack::complex::zgetri(&mut inv_native, n, &ipiv_n).expect("native zgetri");
let n_i32 = n as i32;
let mut lu = a.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
unsafe {
dev_lapack::c_zgetrf(&n_i32, &n_i32, lu.as_mut_ptr(), &n_i32, ipiv.as_mut_ptr(), &mut info);
}
assert_eq!(info, 0, "reference zgetrf failed for n={n}");
let mut work = vec![Complex64::new(0.0, 0.0); n.max(1)];
unsafe {
dev_lapack::c_zgetri(
&n_i32,
lu.as_mut_ptr(),
&n_i32,
ipiv.as_ptr(),
work.as_mut_ptr(),
&(work.len() as i32),
&mut info,
);
}
assert_eq!(info, 0, "reference zgetri failed for n={n}");
for i in 0..n {
for j in 0..n {
let got = cget(&inv_native, n, i, j);
let want = cget(&lu, n, i, j);
let scale = want.norm().max(1.0);
assert!((got - want).norm() / scale < TOL, "n={n}: inverse differs at ({i},{j}): {got} vs {want}");
}
}
}
}
#[test]
fn complex_solve_agrees_with_the_reference() {
for n in [1usize, 2, 3, 5, 8] {
let a = complex_sample(n);
let x_true: Vec<Complex64> = (0..n).map(|i| Complex64::new(1.0 + (i as f64) * 0.5, -0.25 * (i as f64))).collect();
let mut b = vec![Complex64::new(0.0, 0.0); n];
for i in 0..n {
for k in 0..n {
b[i] += cget(&a, n, i, k) * x_true[k];
}
}
let mut a_work = a.clone();
let mut b_native = b.clone();
lapack::complex::zgesv(&mut a_work, &mut b_native).expect("native solve");
for i in 0..n {
let scale = x_true[i].norm().max(1.0);
assert!(
(b_native[i] - x_true[i]).norm() / scale < TOL,
"n={n}: native solve recovered {} but the true x[{i}] is {}",
b_native[i],
x_true[i]
);
}
let n_i32 = n as i32;
let mut a_ref = a.clone();
let mut b_ref = b.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
unsafe {
dev_lapack::c_zgesv(
&n_i32,
&1i32,
a_ref.as_mut_ptr(),
&n_i32,
ipiv.as_mut_ptr(),
b_ref.as_mut_ptr(),
&n_i32,
&mut info,
);
}
assert_eq!(info, 0, "reference zgesv failed for n={n}");
for i in 0..n {
let scale = b_ref[i].norm().max(1.0);
assert!(
(b_native[i] - b_ref[i]).norm() / scale < TOL,
"n={n}: solve differs at {i}: {} vs the reference's {}",
b_native[i],
b_ref[i]
);
}
}
}
#[test]
fn dsyev_agrees_with_the_reference() {
for n in [1usize, 2, 3, 5, 8, 12] {
let a = symmetric_sample(n);
let mut results = Vec::new();
for upper in [false, true] {
let mut work = a.clone();
let mut l = vec![0.0; n];
lapack::sym::dsyev(&mut work, &mut l, n, upper).expect("native dsyev");
let mut t_ref = a.clone();
let mut l_ref = vec![0.0; n];
let info = dev_lapack::query::dsyev(&mut t_ref, n, &mut l_ref, true);
assert_eq!(info, 0, "reference dsyev failed for n={n} upper={upper}");
let scale = l_ref.iter().fold(0.0f64, |m, x| m.max(x.abs())).max(1.0);
for i in 0..n {
assert!(
(l[i] - l_ref[i]).abs() / scale < TOL,
"n={n} upper={upper}: eigenvalue {i} is {} but the reference says {}",
l[i],
l_ref[i]
);
}
for i in 0..n {
for j in 0..n {
let mut dot = 0.0;
for k in 0..n {
dot += get(&work, n, k, i) * get(&work, n, k, j);
}
let want = if i == j { 1.0 } else { 0.0 };
assert!(
(dot - want).abs() < 1e-10,
"n={n} upper={upper}: columns {i},{j} are not orthonormal (dot = {dot})"
);
}
}
for j in 0..n {
for i in 0..n {
let mut av = 0.0;
for k in 0..n {
av += get(&a, n, i, k) * get(&work, n, k, j);
}
let resid = (av - l[j] * get(&work, n, i, j)).abs();
assert!(
resid / scale < 1e-10,
"n={n} upper={upper}: (A v - lambda v)[{i}] = {resid} for eigenvalue {j}"
);
}
}
results.push((l, work));
}
let (l_lo, _) = &results[0];
let (l_up, _) = &results[1];
for i in 0..n {
assert!(
(l_lo[i] - l_up[i]).abs() < 1e-12,
"n={n}: reading the lower triangle gives {} but the upper gives {}",
l_lo[i],
l_up[i]
);
}
}
}
#[test]
fn dgeev_lr_left_eigenvectors_satisfy_their_defining_equation() {
for n in [1usize, 2, 3, 5, 8] {
let a = sample(n);
let mut work = a.clone();
let mut u_real = vec![0.0; n * n];
let mut u_imag = vec![0.0; n * n];
let mut v_real = vec![0.0; n * n];
let mut v_imag = vec![0.0; n * n];
let (l_real, l_imag) = lapack::eig::dgeev_lr(&mut u_real, &mut u_imag, &mut v_real, &mut v_imag, &mut work, n).expect("native dgeev_lr");
let scale = a.iter().fold(0.0f64, |m, x| m.max(x.abs())).max(1.0);
let mut j = 0usize;
while j < n {
if l_imag[j] != 0.0 {
let (lr, li) = (l_real[j], l_imag[j]);
let mut worst = 0.0f64;
for i in 0..n {
let mut sr = 0.0;
let mut si = 0.0;
for k in 0..n {
let ak = get(&a, n, k, i);
sr += ak * get(&u_real, n, k, j);
si += ak * get(&u_imag, n, k, j);
}
let ur = get(&u_real, n, i, j);
let ui = get(&u_imag, n, i, j);
let dr = sr - (lr * ur + li * ui);
let di = si - (lr * ui - li * ur);
worst = worst.max(dr.abs().max(di.abs()));
}
assert!(
worst / scale < 1e-10,
"n={n}: the complex left eigenvector for lambda = {lr}+{li}i has residual {worst} (scale {scale})"
);
j += 2;
} else {
let l = l_real[j];
let mut worst = 0.0f64;
for i in 0..n {
let mut s = 0.0;
for k in 0..n {
s += get(&a, n, k, i) * get(&u_real, n, k, j);
}
worst = worst.max((s - l * get(&u_real, n, i, j)).abs());
}
assert!(
worst / scale < 1e-10,
"n={n}: the real left eigenvector for lambda = {l} has residual {worst} (scale {scale})"
);
j += 1;
}
}
let want_scale = l_real.iter().fold(0.0f64, |m, x| m.max(x.abs())).max(1.0);
for j in 0..n {
assert!(l_real[j].is_finite() && l_imag[j].is_finite(), "n={n}: eigenvalue {j} is not finite");
}
let _ = (v_real, v_imag, want_scale);
}
}
#[test]
fn dgesvd_agrees_with_the_reference() {
for (m, n) in [(1usize, 1usize), (2, 2), (3, 2), (2, 3), (4, 3), (3, 4), (6, 4), (4, 6), (5, 5)] {
let a = rect_sample(m, n);
check_svd(&a, m, n, false);
}
for (m, n) in [(4usize, 3usize), (5, 5)] {
let mut a = rect_sample(m, n);
for i in 0..m {
let v = get(&a, m, i, 0);
a[i + (n - 1) * m] = v;
}
check_svd(&a, m, n, true);
}
}
fn check_svd(a: &[f64], m: usize, n: usize, rank_deficient: bool) {
let min_mn = m.min(n);
let label = format!("{m}x{n}{}", if rank_deficient { " (rank deficient)" } else { "" });
let mut work = a.to_vec();
let mut s = vec![0.0; min_mn];
let mut u = vec![0.0; m * m];
let mut vt = vec![0.0; n * n];
lapack::svd::dgesvd(&mut work, m, n, &mut s, &mut u, &mut vt).expect("native dgesvd");
let scale = a.iter().fold(0.0f64, |acc, x| acc.max(x.abs())).max(1.0);
let mut a_ref = a.to_vec();
let mut s_ref = vec![0.0; min_mn];
let mut u_ref = vec![0.0; m * m];
let mut vt_ref = vec![0.0; n * n];
let info = dev_lapack::query::dgesvd(&mut a_ref, m, n, &mut s_ref, &mut u_ref, &mut vt_ref);
assert_eq!(info, 0, "{label}: reference dgesvd failed");
for i in 0..min_mn {
let tol = if rank_deficient && i == min_mn - 1 { 1e-8 } else { TOL };
assert!(
(s[i] - s_ref[i]).abs() / scale < tol,
"{label}: singular value {i} is {} but the reference says {}",
s[i],
s_ref[i]
);
}
for i in 0..min_mn {
assert!(s[i] >= 0.0, "{label}: singular value {i} is negative: {}", s[i]);
if i > 0 {
assert!(
s[i] <= s[i - 1] + 1e-12 * scale,
"{label}: singular values are not descending: {} then {}",
s[i - 1],
s[i]
);
}
}
for i in 0..m {
for j in 0..m {
let mut dot = 0.0;
for k in 0..m {
dot += get(&u, m, k, i) * get(&u, m, k, j);
}
let want = if i == j { 1.0 } else { 0.0 };
assert!((dot - want).abs() < 1e-10, "{label}: U columns {i},{j} are not orthonormal ({dot})");
}
}
for i in 0..n {
for j in 0..n {
let mut dot = 0.0;
for k in 0..n {
dot += get(&vt, n, i, k) * get(&vt, n, j, k);
}
let want = if i == j { 1.0 } else { 0.0 };
assert!((dot - want).abs() < 1e-10, "{label}: V columns {i},{j} are not orthonormal ({dot})");
}
}
let mut worst = 0.0f64;
for i in 0..m {
for j in 0..n {
let mut sum = 0.0;
for k in 0..min_mn {
sum += get(&u, m, i, k) * s[k] * get(&vt, n, k, j);
}
worst = worst.max((sum - get(a, m, i, j)).abs());
}
}
assert!(
worst / scale < 1e-10,
"{label}: U S V^T does not reconstruct A (worst element error {worst}, scale {scale})"
);
}
#[test]
fn zheev_agrees_with_the_reference() {
let zero = Complex64::new(0.0, 0.0);
for n in [1usize, 2, 3, 5, 8, 12] {
let a = hermitian_sample(n);
let scale = a.iter().fold(0.0f64, |m, x| m.max(x.norm())).max(1.0);
let has_imag = (0..n).any(|j| (0..n).any(|i| i != j && cget(&a, n, i, j).im.abs() > 0.25));
assert!(has_imag || n < 2, "the zheev test data must be genuinely complex (n={n})");
let mut results = Vec::new();
for upper in [false, true] {
let mut work = a.clone();
let mut l = vec![0.0; n];
lapack::zeig::zheev(&mut work, &mut l, n, upper).expect("native zheev");
let mut t_ref = a.clone();
let mut l_ref = vec![0.0; n];
let info = dev_lapack::query::zheev(&mut t_ref, n, &mut l_ref, true);
assert_eq!(info, 0, "reference zheev failed for n={n} upper={upper}");
for i in 0..n {
assert!(
(l[i] - l_ref[i]).abs() / scale < TOL,
"n={n} upper={upper}: eigenvalue {i} is {} but the reference says {}",
l[i],
l_ref[i]
);
}
for i in 1..n {
assert!(
l[i] >= l[i - 1] - 1e-12 * scale,
"n={n} upper={upper}: eigenvalues are not ascending: {} then {}",
l[i - 1],
l[i]
);
}
for i in 0..n {
for j in 0..n {
let mut dot = zero;
for k in 0..n {
dot += cget(&work, n, k, i).conj() * cget(&work, n, k, j);
}
let want = if i == j { 1.0 } else { 0.0 };
assert!(
(dot - Complex64::new(want, 0.0)).norm() < 1e-10,
"n={n} upper={upper}: columns {i},{j} are not orthonormal (v_i^H v_j = {dot})"
);
}
}
for j in 0..n {
for i in 0..n {
let mut av = zero;
for k in 0..n {
av += cget(&a, n, i, k) * cget(&work, n, k, j);
}
let resid = (av - cget(&work, n, i, j) * l[j]).norm();
assert!(
resid / scale < 1e-10,
"n={n} upper={upper}: (A v - lambda v)[{i}] = {resid} for eigenvalue {j}"
);
}
}
results.push((l, work));
}
let (l_lo, _) = &results[0];
let (l_up, _) = &results[1];
for i in 0..n {
assert!(
(l_lo[i] - l_up[i]).abs() / scale < 1e-12,
"n={n}: reading the lower triangle gives {} but the upper gives {}",
l_lo[i],
l_up[i]
);
}
}
}
#[test]
fn dggev_satisfies_its_defining_equation() {
for n in [1usize, 2, 3, 5, 8] {
let ra = sample(n);
let mut b = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += get(&ra, n, k, i) * get(&ra, n, k, j);
}
b[i + j * n] = if i == j { sum + n as f64 } else { sum };
}
}
let (a_orig, b_orig) = (ra.clone(), b.clone());
let mut a = ra.clone();
let mut b_work = b.clone();
let mut alpha_r = vec![0.0; n];
let mut alpha_i = vec![0.0; n];
let mut beta = vec![0.0; n];
let mut v = vec![0.0; n * n];
lapack::qz::dggev(
false,
true,
n,
&mut a,
&mut b_work,
&mut alpha_r,
&mut alpha_i,
&mut beta,
&mut [],
&mut v,
)
.expect("native dggev");
let scale_b = b_orig.iter().fold(0.0f64, |m, x| m.max(x.abs())).max(1.0);
let mut j = 0usize;
while j < n {
assert!(
beta[j].abs() > 1e-12,
"beta[{j}] is {}; the data should give no infinite eigenvalue",
beta[j]
);
let lr = alpha_r[j] / beta[j];
let li = alpha_i[j] / beta[j];
if li != 0.0 {
let mut worst = 0.0f64;
for i in 0..n {
let mut av_r = 0.0;
let mut av_i = 0.0;
let mut bv_r = 0.0;
let mut bv_i = 0.0;
for k in 0..n {
let u_r = get(&v, n, k, j);
let u_i = get(&v, n, k, j + 1);
av_r += get(&a_orig, n, i, k) * u_r;
av_i += get(&a_orig, n, i, k) * u_i;
bv_r += get(&b_orig, n, i, k) * u_r;
bv_i += get(&b_orig, n, i, k) * u_i;
}
let rhs_r = lr * bv_r - li * bv_i;
let rhs_i = lr * bv_i + li * bv_r;
worst = worst.max((av_r - rhs_r).abs().max((av_i - rhs_i).abs()));
}
assert!(
worst / scale_b < 1e-10,
"n={n}: the complex generalized eigenvector for lambda = {lr}+{li}i has residual {worst}"
);
j += 2;
} else {
let mut worst = 0.0f64;
for i in 0..n {
let mut av = 0.0;
let mut bv = 0.0;
for k in 0..n {
av += get(&a_orig, n, i, k) * get(&v, n, k, j);
bv += get(&b_orig, n, i, k) * get(&v, n, k, j);
}
worst = worst.max((av - lr * bv).abs());
}
assert!(
worst / scale_b < 1e-10,
"n={n}: the real generalized eigenvector for lambda = {lr} has residual {worst}"
);
j += 1;
}
}
}
}
fn triangular(n: usize, lower: bool) -> Vec<f64> {
let mut a = vec![0.0; n * n];
for j in 0..n {
for i in 0..n {
let stored = if lower { i >= j } else { i <= j };
if !stored {
continue;
}
a[i + j * n] = if i == j {
4.0 + i as f64
} else {
0.25 * (((i * 3 + j * 5) % 4) as f64 - 1.5)
};
}
}
a
}
fn matvec(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
let mut b = vec![0.0; n];
crate::blas::dgemv(1.0, a, x, 0.0, &mut b, n, n);
b
}
fn transpose(a: &[f64], n: usize) -> Vec<f64> {
let mut at = vec![0.0; n * n];
for j in 0..n {
for i in 0..n {
at[i + j * n] = a[j + i * n];
}
}
at
}
fn reference_solve(a: &mut [f64], b: &mut [f64], n: usize, nrhs: usize) {
let n_i32 = n as i32;
let nrhs_i32 = nrhs as i32;
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
unsafe {
dev_lapack::c_dgesv(
&n_i32,
&nrhs_i32,
a.as_mut_ptr(),
&n_i32,
ipiv.as_mut_ptr(),
b.as_mut_ptr(),
&n_i32,
&mut info,
);
}
assert_eq!(info, 0, "reference dgesv failed for n={n}");
}
#[test]
fn triangular_vector_solves_agree_with_the_reference() {
for n in [1usize, 2, 4, 7] {
for lower in [true, false] {
let a = triangular(n, lower);
let x_true: Vec<f64> = (0..n).map(|i| 1.0 + (i as f64) * 0.5).collect();
let b = matvec(&a, &x_true, n);
let mut native = b.clone();
crate::blas::dtrsv(&a, &mut native, n, lower, false);
let mut b_ref = b.clone();
reference_solve(&mut a.clone(), &mut b_ref, n, 1);
compare_vectors(&format!("dtrsv (n={n}, lower={lower})"), &native, &b_ref, TOL);
let at = transpose(&a, n);
let bt = matvec(&at, &x_true, n);
let mut native = bt.clone();
crate::blas::dtrsv_transpose(&a, &mut native, n, lower, false);
let mut bt_ref = bt.clone();
reference_solve(&mut at.clone(), &mut bt_ref, n, 1);
compare_vectors(&format!("dtrsv_transpose (n={n}, lower={lower})"), &native, &bt_ref, TOL);
}
}
}
#[test]
fn triangular_matrix_solves_agree_with_the_reference() {
for n in [2usize, 5] {
let a = triangular(n, true);
let nrhs = 2;
let x_true: Vec<f64> = (0..n * nrhs).map(|i| 1.0 + (i % 5) as f64).collect();
let mut b = vec![0.0; n * nrhs];
for j in 0..nrhs {
for i in 0..n {
for k in 0..n {
b[i + j * n] += a[i + k * n] * x_true[k + j * n];
}
}
}
let mut native = b.clone();
crate::blas::dtrsm(1.0, &a, &mut native, n, nrhs, true, true, false);
let mut b_ref = b.clone();
reference_solve(&mut a.clone(), &mut b_ref, n, nrhs);
compare_vectors(&format!("dtrsm (n={n})"), &native, &b_ref, TOL);
}
}
fn complex_triangular(n: usize, lower: bool) -> Vec<Complex64> {
let mut a = vec![Complex64::new(0.0, 0.0); n * n];
for j in 0..n {
for i in 0..n {
let stored = if lower { i >= j } else { i <= j };
if !stored {
continue;
}
a[i + j * n] = if i == j {
Complex64::new(4.0 + i as f64, 1.0)
} else {
Complex64::new(0.25 * (((i * 3 + j * 5) % 4) as f64 - 1.5), 0.125)
};
}
}
a
}
fn compare_complex_vectors(label: &str, got: &[Complex64], want: &[Complex64], tol: f64) {
assert_eq!(got.len(), want.len(), "{label}: length mismatch");
for i in 0..got.len() {
let scale = want[i].norm().max(1.0);
let err = (got[i] - want[i]).norm() / scale;
assert!(
err < tol,
"{label}: component {i} is {} but the reference says {} (relative error {err:e})",
got[i],
want[i]
);
}
}
#[test]
fn complex_triangular_vector_solves_agree_with_the_reference() {
for n in [1usize, 3, 6] {
let a = complex_triangular(n, true);
let x_true: Vec<Complex64> = (0..n).map(|i| Complex64::new(1.0 + i as f64, -0.5)).collect();
let mut b = vec![Complex64::new(0.0, 0.0); n];
for i in 0..n {
for k in 0..n {
b[i] += a[i + k * n] * x_true[k];
}
}
let mut native = b.clone();
crate::blas::ztrsv(&a, &mut native, n, true, false);
let n_i32 = n as i32;
let mut a_ref = a.clone();
let mut b_ref = b.clone();
let mut ipiv = vec![0i32; n];
let mut info = 0i32;
unsafe {
dev_lapack::c_zgesv(
&n_i32,
&1i32,
a_ref.as_mut_ptr(),
&n_i32,
ipiv.as_mut_ptr(),
b_ref.as_mut_ptr(),
&n_i32,
&mut info,
);
}
assert_eq!(info, 0, "reference zgesv failed for n={n}");
compare_complex_vectors(&format!("ztrsv (n={n})"), &native, &b_ref, TOL);
}
}
#[test]
fn panel_lu_solves_square_panels_like_the_reference() {
for n in [1usize, 2, 4, 7] {
let a = sample(n);
let x_true: Vec<f64> = (0..n).map(|i| 1.0 + (i as f64) * 0.5).collect();
let b = matvec(&a, &x_true, n);
let mut lu = a.clone();
let mut ipiv = vec![0usize; n];
lapack::dgetrf_rect(&mut lu, n, n, &mut ipiv).unwrap();
let mut x = b.clone();
for k in 0..n {
x.swap(k, ipiv[k]);
}
crate::blas::dtrsv(&lu, &mut x, n, true, true);
crate::blas::dtrsv(&lu, &mut x, n, false, false);
let mut b_ref = b.clone();
reference_solve(&mut a.clone(), &mut b_ref, n, 1);
compare_vectors(&format!("dgetrf_rect solve (n={n})"), &x, &b_ref, TOL);
}
}