use crate::kernels::aggregate::{neumaier_add, reduce_min_max_f64};
use crate::kernels::scientific::blas_lapack;
use minarrow::enums::error::KernelError;
#[inline(always)]
pub fn matrix_vector_product(
m: i32,
n: i32,
a: &[f64],
x: &[f64],
y: &mut [f64],
alpha: f64,
beta: f64,
trans: bool,
) -> Result<(), KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
let (rows_a, cols_a, rows_y, rows_x) = if trans {
(m, n, n, m) } else {
(m, n, m, n) };
if a.len() < (rows_a * cols_a) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if x.len() < rows_x as usize {
return Err(KernelError::InvalidArguments("x buffer too small".into()));
}
if y.len() < rows_y as usize {
return Err(KernelError::InvalidArguments("y buffer too small".into()));
}
blas_lapack::gemv(
m, n, a, m, x, 1, y, 1, alpha, beta, trans,
)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
#[inline(always)]
pub fn matmul(
m: i32,
n: i32,
k: i32,
alpha: f64,
a: &[f64],
lda: i32,
b: &[f64],
ldb: i32,
beta: f64,
c: &mut [f64],
ldc: i32,
trans_a: bool,
trans_b: bool,
) -> Result<(), KernelError> {
if m <= 0 || n <= 0 || k <= 0 {
return Err(KernelError::InvalidArguments(
"m, n, k must be positive".into(),
));
}
if lda < if trans_a { k } else { m } {
return Err(KernelError::InvalidArguments("lda is too small".into()));
}
if ldb < if trans_b { n } else { k } {
return Err(KernelError::InvalidArguments("ldb is too small".into()));
}
if ldc < m {
return Err(KernelError::InvalidArguments("ldc is too small".into()));
}
let (_rows_a, cols_a) = if trans_a { (k, m) } else { (m, k) };
let (_rows_b, cols_b) = if trans_b { (n, k) } else { (k, n) };
if a.len() < (lda * cols_a) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if b.len() < (ldb * cols_b) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
if c.len() < (ldc * n) as usize {
return Err(KernelError::InvalidArguments("C buffer too small".into()));
}
blas_lapack::blocked_gemm(
m, n, k, alpha, a, lda, b, ldb, beta, c, ldc, trans_a, trans_b,
)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
#[inline(always)]
pub fn matrix_min_max(
data: &[f64],
rows: usize,
cols: usize,
lda: usize, ) -> (f64, f64) {
let n_elems = rows * cols;
if lda == rows && data.len() >= n_elems {
reduce_min_max_f64(&data[..n_elems], None, Some(0))
.unwrap_or((f64::INFINITY, f64::NEG_INFINITY))
} else {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for col in 0..cols {
let offset = col * lda;
let col_slice = &data[offset..offset + rows];
if let Some((cmin, cmax)) = reduce_min_max_f64(col_slice, None, Some(0)) {
min = min.min(cmin);
max = max.max(cmax);
}
}
(min, max)
}
}
pub fn _lu(m: i32, n: i32, a: &mut [f64], lda: i32, ipiv: &mut [i32]) -> Result<(), KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let k = m.min(n) as usize;
if ipiv.len() < k {
return Err(KernelError::InvalidArguments(
"ipiv buffer too small".into(),
));
}
blas_lapack::lu_with_piv(m, n, a, lda, ipiv)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn qr(m: i32, n: i32, a: &mut [f64], lda: i32, taus: &mut [f64]) -> Result<(), KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let k = m.min(n) as usize;
if taus.len() < k {
return Err(KernelError::InvalidArguments(
"taus buffer too small".into(),
));
}
blas_lapack::qr_block(m, n, a, lda, taus)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn qr_q(
m: i32,
n: i32,
k: i32,
a: &mut [f64],
lda: i32,
taus: &[f64],
) -> Result<(), KernelError> {
if m <= 0 || n <= 0 || k <= 0 {
return Err(KernelError::InvalidArguments(
"m, n, k must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if taus.len() < k as usize {
return Err(KernelError::InvalidArguments(
"taus buffer too small".into(),
));
}
blas_lapack::qr_form_q(m, n, k, a, lda, taus)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn cholesky(n: i32, a: &mut [f64], lda: i32) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
blas_lapack::spd_cholesky(n, a, lda).map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn svd(
m: i32,
n: i32,
a: &mut [f64],
lda: i32,
s: &mut [f64],
u: &mut [f64],
ldu: i32,
vt: &mut [f64],
ldvt: i32,
economy: bool,
) -> Result<(), KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let k = m.min(n);
if s.len() < k as usize {
return Err(KernelError::InvalidArguments("s buffer too small".into()));
}
if ldu < m {
return Err(KernelError::InvalidArguments("ldu must be >= m".into()));
}
let (jobu, jobvt) = if economy { (b'S', b'S') } else { (b'A', b'A') };
let u_cols = if economy { k } else { m };
if u.len() < (ldu * u_cols) as usize {
return Err(KernelError::InvalidArguments("U buffer too small".into()));
}
let min_ldvt = if economy { k } else { n };
if ldvt < min_ldvt {
return Err(KernelError::InvalidArguments("ldvt too small".into()));
}
if vt.len() < (ldvt * n) as usize {
return Err(KernelError::InvalidArguments("Vt buffer too small".into()));
}
blas_lapack::svd_block(jobu, jobvt, m, n, a, lda, s, u, ldu, vt, ldvt)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn eig_symmetric(
n: i32,
a: &mut [f64],
lda: i32,
eigenvalues: &mut [f64],
) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if eigenvalues.len() < n as usize {
return Err(KernelError::InvalidArguments(
"eigenvalues buffer too small".into(),
));
}
blas_lapack::symeig_full(n, a, lda, eigenvalues)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn solve(
n: i32,
nrhs: i32,
a: &mut [f64],
lda: i32,
ipiv: &mut [i32],
b: &mut [f64],
ldb: i32,
) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if nrhs <= 0 {
return Err(KernelError::InvalidArguments(
"nrhs must be positive".into(),
));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if ldb < n {
return Err(KernelError::InvalidArguments("ldb must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if ipiv.len() < n as usize {
return Err(KernelError::InvalidArguments(
"ipiv buffer too small".into(),
));
}
if b.len() < (ldb * nrhs) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
_lu(n, n, a, lda, ipiv)?;
blas_lapack::getrs(n, nrhs, a, lda, ipiv, b, ldb)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn spd_solve(
n: i32,
nrhs: i32,
l: &[f64],
ldl: i32,
b: &mut [f64],
ldb: i32,
) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if nrhs <= 0 {
return Err(KernelError::InvalidArguments(
"nrhs must be positive".into(),
));
}
if ldl < n {
return Err(KernelError::InvalidArguments("ldl must be >= n".into()));
}
if ldb < n {
return Err(KernelError::InvalidArguments("ldb must be >= n".into()));
}
if l.len() < (ldl * n) as usize {
return Err(KernelError::InvalidArguments("L buffer too small".into()));
}
if b.len() < (ldb * nrhs) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
blas_lapack::spd_solve(n, nrhs, l, ldl, b, ldb)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn solve_triangular_upper(
n: i32,
nrhs: i32,
u: &[f64],
ldu: i32,
b: &mut [f64],
ldb: i32,
) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if ldu < n {
return Err(KernelError::InvalidArguments("ldu must be >= n".into()));
}
if ldb < n {
return Err(KernelError::InvalidArguments("ldb must be >= n".into()));
}
if u.len() < (ldu * n) as usize {
return Err(KernelError::InvalidArguments("U buffer too small".into()));
}
if b.len() < (ldb * nrhs) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
blas_lapack::trisolve_upper(n, nrhs, u, ldu, b, ldb)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn solve_triangular_lower(
n: i32,
nrhs: i32,
l: &[f64],
ldl: i32,
b: &mut [f64],
ldb: i32,
) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if ldl < n {
return Err(KernelError::InvalidArguments("ldl must be >= n".into()));
}
if ldb < n {
return Err(KernelError::InvalidArguments("ldb must be >= n".into()));
}
if l.len() < (ldl * n) as usize {
return Err(KernelError::InvalidArguments("L buffer too small".into()));
}
if b.len() < (ldb * nrhs) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
blas_lapack::trisolve_lower(n, nrhs, l, ldl, b, ldb)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn inverse(n: i32, a: &mut [f64], lda: i32) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let mut ipiv = vec![0i32; n as usize];
_lu(n, n, a, lda, &mut ipiv)?;
blas_lapack::getri(n, a, lda, &ipiv).map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn spd_inverse(n: i32, a: &mut [f64], lda: i32) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
blas_lapack::spd_cholesky(n, a, lda)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
blas_lapack::spd_inverse(n, a, lda)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
let nu = n as usize;
let ldau = lda as usize;
for col in 0..nu {
for row in 0..col {
a[row + col * ldau] = a[col + row * ldau];
}
}
Ok(())
}
pub fn determinant(n: i32, a: &mut [f64], lda: i32) -> Result<f64, KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let mut ipiv = vec![0i32; n as usize];
_lu(n, n, a, lda, &mut ipiv)?;
let nu = n as usize;
let ldau = lda as usize;
let mut det = 1.0_f64;
let mut swaps = 0;
for i in 0..nu {
det *= a[i + i * ldau]; if ipiv[i] != (i as i32 + 1) {
swaps += 1;
}
}
if swaps % 2 == 1 {
det = -det;
}
Ok(det)
}
pub fn rank(m: i32, n: i32, a: &mut [f64], lda: i32, tol: f64) -> Result<usize, KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let k = m.min(n) as usize;
let mut s = vec![0.0_f64; k];
let mut u_dummy = [0.0_f64; 1];
let mut vt_dummy = [0.0_f64; 1];
blas_lapack::svd_block(
b'N',
b'N',
m,
n,
a,
lda,
&mut s,
&mut u_dummy,
1,
&mut vt_dummy,
1,
)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
let r = s.iter().filter(|&&sv| sv > tol).count();
Ok(r)
}
pub fn least_squares(
m: i32,
n: i32,
nrhs: i32,
a: &mut [f64],
lda: i32,
b: &mut [f64],
ldb: i32,
) -> Result<(), KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if nrhs <= 0 {
return Err(KernelError::InvalidArguments(
"nrhs must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if ldb < m.max(n) {
return Err(KernelError::InvalidArguments(
"ldb must be >= max(m, n)".into(),
));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
if b.len() < (ldb * nrhs) as usize {
return Err(KernelError::InvalidArguments("B buffer too small".into()));
}
blas_lapack::least_squares_qr(m, n, nrhs, a, lda, b, ldb)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
pub fn condition_number(m: i32, n: i32, a: &mut [f64], lda: i32) -> Result<f64, KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let k = m.min(n) as usize;
let mut s = vec![0.0_f64; k];
let mut u_dummy = [0.0_f64; 1];
let mut vt_dummy = [0.0_f64; 1];
blas_lapack::svd_block(
b'N',
b'N',
m,
n,
a,
lda,
&mut s,
&mut u_dummy,
1,
&mut vt_dummy,
1,
)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
let s_max = s[0]; let s_min = s[k - 1];
if s_min == 0.0 {
Ok(f64::INFINITY)
} else {
Ok(s_max / s_min)
}
}
pub fn norm(m: i32, n: i32, a: &[f64], lda: i32, kind: &str) -> Result<f64, KernelError> {
if m <= 0 || n <= 0 {
return Err(KernelError::InvalidArguments(
"m, n must be positive".into(),
));
}
if lda < m {
return Err(KernelError::InvalidArguments("lda must be >= m".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
let mu = m as usize;
let nu = n as usize;
let ldau = lda as usize;
match kind {
"fro" => {
let mut sum = 0.0_f64;
let mut comp = 0.0_f64;
for col in 0..nu {
for row in 0..mu {
let v = a[row + col * ldau];
neumaier_add(&mut sum, &mut comp, v * v);
}
}
Ok((sum + comp).sqrt())
}
"l1" => {
let mut max_col_sum = 0.0_f64;
for col in 0..nu {
let mut col_sum = 0.0_f64;
let mut col_comp = 0.0_f64;
for row in 0..mu {
neumaier_add(&mut col_sum, &mut col_comp, a[row + col * ldau].abs());
}
let total = col_sum + col_comp;
if total > max_col_sum {
max_col_sum = total;
}
}
Ok(max_col_sum)
}
"linf" => {
let mut row_sums = vec![0.0_f64; mu];
let mut row_comps = vec![0.0_f64; mu];
for col in 0..nu {
for row in 0..mu {
neumaier_add(
&mut row_sums[row],
&mut row_comps[row],
a[row + col * ldau].abs(),
);
}
}
let max_row_sum = row_sums
.iter()
.zip(row_comps.iter())
.map(|(s, c)| s + c)
.fold(0.0_f64, f64::max);
Ok(max_row_sum)
}
_ => Err(KernelError::InvalidArguments(format!(
"Unknown norm kind '{}'. Use \"fro\", \"l1\", or \"linf\".",
kind
))),
}
}
pub fn covariance(
n_obs: i32,
n_feat: i32,
x: &[f64],
ldx: i32,
cov: &mut [f64],
ldc: i32,
ddof: usize,
) -> Result<(), KernelError> {
if n_obs <= 0 || n_feat <= 0 {
return Err(KernelError::InvalidArguments(
"n_obs, n_feat must be positive".into(),
));
}
if ldx < n_obs {
return Err(KernelError::InvalidArguments("ldx must be >= n_obs".into()));
}
if ldc < n_feat {
return Err(KernelError::InvalidArguments(
"ldc must be >= n_feat".into(),
));
}
if x.len() < (ldx * n_feat) as usize {
return Err(KernelError::InvalidArguments("X buffer too small".into()));
}
if cov.len() < (ldc * n_feat) as usize {
return Err(KernelError::InvalidArguments("cov buffer too small".into()));
}
let n_obs_u = n_obs as usize;
let n_feat_u = n_feat as usize;
let ldx_u = ldx as usize;
let denom = n_obs_u
.checked_sub(ddof)
.ok_or_else(|| KernelError::InvalidArguments("ddof >= n_obs: division by zero".into()))?;
if denom == 0 {
return Err(KernelError::InvalidArguments(
"n_obs - ddof is zero: division by zero".into(),
));
}
let mut means = vec![0.0_f64; n_feat_u];
for j in 0..n_feat_u {
let col_offset = j * ldx_u;
let mut s = 0.0_f64;
let mut comp = 0.0_f64;
for i in 0..n_obs_u {
neumaier_add(&mut s, &mut comp, x[col_offset + i]);
}
means[j] = (s + comp) / n_obs_u as f64;
}
let mut xc = vec![0.0_f64; n_obs_u * n_feat_u];
for j in 0..n_feat_u {
let src_offset = j * ldx_u;
let dst_offset = j * n_obs_u;
let m = means[j];
for i in 0..n_obs_u {
xc[dst_offset + i] = x[src_offset + i] - m;
}
}
blas_lapack::blocked_gemm(
n_feat, n_feat, n_obs, 1.0, &xc, n_obs, &xc, n_obs, 0.0, cov, ldc, true, false, )
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
let ldc_u = ldc as usize;
let scale = 1.0 / denom as f64;
for j in 0..n_feat_u {
for i in 0..n_feat_u {
cov[i + j * ldc_u] *= scale;
}
}
for col in 0..n_feat_u {
for row in 0..col {
cov[row + col * ldc_u] = cov[col + row * ldc_u];
}
}
Ok(())
}
pub fn log_det_spd(n: i32, a: &mut [f64], lda: i32) -> Result<f64, KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if lda < n {
return Err(KernelError::InvalidArguments("lda must be >= n".into()));
}
if a.len() < (lda * n) as usize {
return Err(KernelError::InvalidArguments("A buffer too small".into()));
}
cholesky(n, a, lda)?;
let nu = n as usize;
let ldau = lda as usize;
let mut log_det = 0.0_f64;
let mut comp = 0.0_f64;
for i in 0..nu {
let diag = a[i + i * ldau];
if diag <= 0.0 {
return Err(KernelError::InvalidArguments(
"Cholesky diagonal is non-positive: matrix is not SPD".into(),
));
}
neumaier_add(&mut log_det, &mut comp, diag.ln());
}
Ok(2.0 * (log_det + comp))
}
pub fn mahalanobis(
n_obs: i32,
n_feat: i32,
x: &[f64],
ldx: i32,
mean: &[f64],
sigma_chol: &[f64],
lds: i32,
distances: &mut [f64],
) -> Result<(), KernelError> {
if n_obs <= 0 || n_feat <= 0 {
return Err(KernelError::InvalidArguments(
"n_obs, n_feat must be positive".into(),
));
}
if ldx < n_obs {
return Err(KernelError::InvalidArguments("ldx must be >= n_obs".into()));
}
if lds < n_feat {
return Err(KernelError::InvalidArguments(
"lds must be >= n_feat".into(),
));
}
if x.len() < (ldx * n_feat) as usize {
return Err(KernelError::InvalidArguments("X buffer too small".into()));
}
if mean.len() < n_feat as usize {
return Err(KernelError::InvalidArguments(
"mean buffer too small".into(),
));
}
if sigma_chol.len() < (lds * n_feat) as usize {
return Err(KernelError::InvalidArguments(
"sigma_chol buffer too small".into(),
));
}
if distances.len() < n_obs as usize {
return Err(KernelError::InvalidArguments(
"distances buffer too small".into(),
));
}
let n_obs_u = n_obs as usize;
let n_feat_u = n_feat as usize;
let ldx_u = ldx as usize;
let mut diff = vec![0.0_f64; n_feat_u];
for i in 0..n_obs_u {
for j in 0..n_feat_u {
diff[j] = x[i + j * ldx_u] - mean[j];
}
blas_lapack::trisolve_lower(n_feat, 1, sigma_chol, lds, &mut diff, n_feat)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))?;
let mut sum_sq = 0.0_f64;
let mut comp = 0.0_f64;
for &v in &diff {
neumaier_add(&mut sum_sq, &mut comp, v * v);
}
distances[i] = (sum_sq + comp).sqrt();
}
Ok(())
}
pub fn triangular_inverse(n: i32, t: &mut [f64], ldt: i32, upper: bool) -> Result<(), KernelError> {
if n <= 0 {
return Err(KernelError::InvalidArguments("n must be positive".into()));
}
if ldt < n {
return Err(KernelError::InvalidArguments("ldt must be >= n".into()));
}
if t.len() < (ldt * n) as usize {
return Err(KernelError::InvalidArguments("T buffer too small".into()));
}
blas_lapack::tri_inverse(n, t, ldt, upper)
.map_err(|e| KernelError::InvalidArguments(e.to_string()))
}
#[cfg(test)]
#[cfg(feature = "linear_algebra")]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn mm(m: i32, n: i32, k: i32, a: &[f64], b: &[f64]) -> Vec<f64> {
let mut c = vec![0.0; (m * n) as usize];
matmul(m, n, k, 1.0, a, m, b, k, 0.0, &mut c, m, false, false).unwrap();
c
}
fn mm_at_b(m: i32, n: i32, k: i32, a: &[f64], lda: i32, b: &[f64], ldb: i32) -> Vec<f64> {
let mut c = vec![0.0; (m * n) as usize];
matmul(m, n, k, 1.0, a, lda, b, ldb, 0.0, &mut c, m, true, false).unwrap();
c
}
#[test]
fn test_lu_3x3_reconstruct() {
let mut a = vec![2.0, 4.0, 8.0, 1.0, 3.0, 7.0, 1.0, 3.0, 9.0];
let a_orig = a.clone();
let mut ipiv = vec![0i32; 3];
_lu(3, 3, &mut a, 3, &mut ipiv).unwrap();
let mut l = vec![0.0_f64; 9];
let mut u = vec![0.0_f64; 9];
for col in 0..3_usize {
for row in 0..3_usize {
let idx = row + col * 3;
if row > col {
l[idx] = a[idx]; } else if row == col {
l[idx] = 1.0; u[idx] = a[idx];
} else {
u[idx] = a[idx]; }
}
}
let lu = mm(3, 3, 3, &l, &u);
let mut pa = a_orig.clone();
for i in 0..3_usize {
let swap_row = (ipiv[i] - 1) as usize; if swap_row != i {
for col in 0..3_usize {
pa.swap(i + col * 3, swap_row + col * 3);
}
}
}
for idx in 0..9 {
assert!(
approx_eq(lu[idx], pa[idx], TOL),
"LU[{idx}] = {}, PA[{idx}] = {}, diff = {}",
lu[idx],
pa[idx],
(lu[idx] - pa[idx]).abs()
);
}
}
#[test]
fn test_lu_singular_matrix() {
let mut a = vec![1.0, 4.0, 5.0, 2.0, 5.0, 7.0, 3.0, 6.0, 9.0];
let mut ipiv = vec![0i32; 3];
let result = _lu(3, 3, &mut a, 3, &mut ipiv);
if result.is_ok() {
let has_zero_diag = (0..3).any(|i| a[i + i * 3].abs() < 1e-12);
assert!(
has_zero_diag,
"Singular matrix should have zero diagonal in U"
);
}
}
#[test]
fn test_qr_4x3_orthogonal_and_reconstruct() {
let a_orig = vec![
1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, ];
let mut a_qr = a_orig.clone();
let mut taus = vec![0.0_f64; 3]; qr(4, 3, &mut a_qr, 4, &mut taus).unwrap();
let mut r = vec![0.0_f64; 3 * 3]; for col in 0..3_usize {
for row in 0..=col.min(2) {
r[row + col * 3] = a_qr[row + col * 4]; }
}
let mut q = a_qr.clone(); qr_q(4, 3, 3, &mut q, 4, &taus).unwrap();
let qtq = mm_at_b(3, 3, 4, &q, 4, &q, 4);
for col in 0..3_usize {
for row in 0..3_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 3;
assert!(
approx_eq(qtq[idx], expected, TOL),
"Q^T*Q[{row},{col}] = {}, expected {expected}",
qtq[idx]
);
}
}
let qr_product = mm(4, 3, 3, &q, &r);
for idx in 0..12 {
assert!(
approx_eq(qr_product[idx], a_orig[idx], TOL),
"QR[{idx}] = {}, A[{idx}] = {}, diff = {}",
qr_product[idx],
a_orig[idx],
(qr_product[idx] - a_orig[idx]).abs()
);
}
}
#[test]
fn test_cholesky_3x3_reconstruct() {
let mut a = vec![4.0, 2.0, 1.0, 2.0, 5.0, 3.0, 1.0, 3.0, 6.0];
let a_orig = a.clone();
cholesky(3, &mut a, 3).unwrap();
let mut l = vec![0.0_f64; 9];
for col in 0..3_usize {
for row in col..3_usize {
l[row + col * 3] = a[row + col * 3];
}
}
let mut llt = vec![0.0_f64; 9];
matmul(3, 3, 3, 1.0, &l, 3, &l, 3, 0.0, &mut llt, 3, false, true).unwrap();
for idx in 0..9 {
assert!(
approx_eq(llt[idx], a_orig[idx], TOL),
"L*Lt[{idx}] = {}, A[{idx}] = {}, diff = {}",
llt[idx],
a_orig[idx],
(llt[idx] - a_orig[idx]).abs()
);
}
}
#[test]
fn test_cholesky_non_spd_errors() {
let mut a = vec![1.0, 2.0, 2.0, 1.0];
let result = cholesky(2, &mut a, 2);
assert!(result.is_err(), "Non-SPD matrix should fail Cholesky");
}
#[test]
fn test_svd_3x2_economy() {
let mut a = vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0];
let mut s = vec![0.0_f64; 2];
let mut u = vec![0.0_f64; 3 * 2]; let mut vt = vec![0.0_f64; 2 * 2];
svd(3, 2, &mut a, 3, &mut s, &mut u, 3, &mut vt, 2, true).unwrap();
assert!(s[0] >= 0.0, "s[0] should be non-negative");
assert!(s[1] >= 0.0, "s[1] should be non-negative");
assert!(s[0] >= s[1], "singular values should be descending");
let mut us = vec![0.0_f64; 3 * 2];
for col in 0..2_usize {
for row in 0..3_usize {
us[row + col * 3] = u[row + col * 3] * s[col];
}
}
let a_reconstructed = mm(3, 2, 2, &us, &vt);
let a_orig = vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0];
for idx in 0..6 {
assert!(
approx_eq(a_reconstructed[idx], a_orig[idx], TOL),
"USVt[{idx}] = {}, A[{idx}] = {}, diff = {}",
a_reconstructed[idx],
a_orig[idx],
(a_reconstructed[idx] - a_orig[idx]).abs()
);
}
}
#[test]
fn test_eig_symmetric_3x3() {
let mut a = vec![2.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0];
let mut eigenvalues = vec![0.0_f64; 3];
eig_symmetric(3, &mut a, 3, &mut eigenvalues).unwrap();
let eig_sum: f64 = eigenvalues.iter().sum();
assert!(
approx_eq(eig_sum, 7.0, TOL),
"Eigenvalue sum = {eig_sum}, expected 7.0"
);
for i in 1..3 {
assert!(
eigenvalues[i] >= eigenvalues[i - 1] - TOL,
"Eigenvalues should be ascending: {} >= {}",
eigenvalues[i],
eigenvalues[i - 1]
);
}
let vtv = mm_at_b(3, 3, 3, &a, 3, &a, 3);
for col in 0..3_usize {
for row in 0..3_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 3;
assert!(
approx_eq(vtv[idx], expected, TOL),
"V^T*V[{row},{col}] = {}, expected {expected}",
vtv[idx]
);
}
}
}
#[test]
fn test_solve_3x3() {
let mut a = vec![1.0, 4.0, 7.0, 2.0, 5.0, 8.0, 3.0, 6.0, 10.0];
let mut b = vec![14.0, 32.0, 53.0];
let mut ipiv = vec![0i32; 3];
solve(3, 1, &mut a, 3, &mut ipiv, &mut b, 3).unwrap();
assert!(approx_eq(b[0], 1.0, TOL), "x[0] = {}, expected 1.0", b[0]);
assert!(approx_eq(b[1], 2.0, TOL), "x[1] = {}, expected 2.0", b[1]);
assert!(approx_eq(b[2], 3.0, TOL), "x[2] = {}, expected 3.0", b[2]);
}
#[test]
fn test_spd_solve_2x2() {
let mut sigma = vec![4.0, 1.0, 1.0, 2.0];
cholesky(2, &mut sigma, 2).unwrap();
let mut b = vec![5.0, 3.0];
spd_solve(2, 1, &sigma, 2, &mut b, 2).unwrap();
assert!(approx_eq(b[0], 1.0, TOL), "x[0] = {}, expected 1.0", b[0]);
assert!(approx_eq(b[1], 1.0, TOL), "x[1] = {}, expected 1.0", b[1]);
}
#[test]
fn test_solve_triangular_upper_3x3() {
let u = vec![2.0, 0.0, 0.0, 1.0, 4.0, 0.0, 3.0, 2.0, 5.0];
let mut b = vec![13.0, 14.0, 15.0];
solve_triangular_upper(3, 1, &u, 3, &mut b, 3).unwrap();
assert!(approx_eq(b[0], 1.0, TOL), "x[0] = {}, expected 1.0", b[0]);
assert!(approx_eq(b[1], 2.0, TOL), "x[1] = {}, expected 2.0", b[1]);
assert!(approx_eq(b[2], 3.0, TOL), "x[2] = {}, expected 3.0", b[2]);
}
#[test]
fn test_solve_triangular_lower_3x3() {
let l = vec![3.0, 1.0, 2.0, 0.0, 5.0, 4.0, 0.0, 0.0, 6.0];
let mut b = vec![3.0, 11.0, 28.0];
solve_triangular_lower(3, 1, &l, 3, &mut b, 3).unwrap();
assert!(approx_eq(b[0], 1.0, TOL), "x[0] = {}, expected 1.0", b[0]);
assert!(approx_eq(b[1], 2.0, TOL), "x[1] = {}, expected 2.0", b[1]);
assert!(approx_eq(b[2], 3.0, TOL), "x[2] = {}, expected 3.0", b[2]);
}
#[test]
fn test_least_squares_overdetermined() {
let mut a = vec![1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 3.0, 4.0];
let mut b = vec![1.0, 2.0, 4.0, 4.0];
least_squares(4, 2, 1, &mut a, 4, &mut b, 4).unwrap();
assert!(
approx_eq(b[0], 0.0, TOL),
"intercept = {}, expected 0.0",
b[0]
);
assert!(approx_eq(b[1], 1.1, TOL), "slope = {}, expected 1.1", b[1]);
}
#[test]
fn test_inverse_3x3() {
let mut a = vec![1.0, 0.0, 1.0, 2.0, 4.0, 0.0, 3.0, 5.0, 6.0];
let a_orig = a.clone();
inverse(3, &mut a, 3).unwrap();
let product = mm(3, 3, 3, &a_orig, &a);
for col in 0..3_usize {
for row in 0..3_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 3;
assert!(
approx_eq(product[idx], expected, TOL),
"A*Ainv[{row},{col}] = {}, expected {expected}",
product[idx]
);
}
}
}
#[test]
fn test_spd_inverse_2x2() {
let mut a = vec![4.0, 1.0, 1.0, 2.0];
let a_orig = a.clone();
spd_inverse(2, &mut a, 2).unwrap();
let product = mm(2, 2, 2, &a_orig, &a);
for col in 0..2_usize {
for row in 0..2_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 2;
assert!(
approx_eq(product[idx], expected, TOL),
"A*Ainv[{row},{col}] = {}, expected {expected}",
product[idx]
);
}
}
}
#[test]
fn test_determinant_3x3() {
let mut a = vec![1.0, 0.0, 1.0, 2.0, 4.0, 0.0, 3.0, 5.0, 6.0];
let det = determinant(3, &mut a, 3).unwrap();
assert!(approx_eq(det, 22.0, TOL), "det = {det}, expected 22.0");
}
#[test]
fn test_rank_full() {
let mut a = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
let r = rank(3, 3, &mut a, 3, 1e-12).unwrap();
assert_eq!(r, 3, "Identity should have rank 3");
}
#[test]
fn test_rank_deficient() {
let mut a = vec![1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 2.0];
let r = rank(3, 3, &mut a, 3, 1e-12).unwrap();
assert_eq!(r, 2, "Rank-deficient matrix should have rank 2");
}
#[test]
fn test_condition_number_identity() {
let mut a = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
let kappa = condition_number(3, 3, &mut a, 3).unwrap();
assert!(
approx_eq(kappa, 1.0, TOL),
"Identity condition number = {kappa}, expected 1.0"
);
}
#[test]
fn test_condition_number_ill_conditioned() {
let mut a = vec![1.0, 0.5, 0.5, 1.0 / 3.0];
let kappa = condition_number(2, 2, &mut a, 2).unwrap();
assert!(
kappa > 10.0,
"Ill-conditioned matrix should have kappa > 10, got {kappa}"
);
}
#[test]
fn test_norm_frobenius() {
let a = vec![1.0, 3.0, 2.0, 4.0];
let n = norm(2, 2, &a, 2, "fro").unwrap();
assert!(
approx_eq(n, 30.0_f64.sqrt(), TOL),
"Frobenius norm = {n}, expected {}",
30.0_f64.sqrt()
);
}
#[test]
fn test_norm_l1() {
let a = vec![1.0, 3.0, -2.0, 4.0];
let n = norm(2, 2, &a, 2, "l1").unwrap();
assert!(approx_eq(n, 6.0, TOL), "L1 norm = {n}, expected 6.0");
}
#[test]
fn test_norm_linf() {
let a = vec![1.0, 3.0, -2.0, 4.0];
let n = norm(2, 2, &a, 2, "linf").unwrap();
assert!(approx_eq(n, 7.0, TOL), "Linf norm = {n}, expected 7.0");
}
#[test]
fn test_covariance_perfectly_correlated_ddof1() {
let x = vec![
1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, ];
let mut cov = vec![0.0_f64; 9]; covariance(4, 3, &x, 4, &mut cov, 3, 1).unwrap();
for col in 0..3_usize {
for row in 0..3_usize {
let idx = row + col * 3;
assert!(
approx_eq(cov[idx], 15.0, TOL),
"cov[{row},{col}] = {}, expected 15.0",
cov[idx]
);
}
}
for col in 0..3_usize {
for row in 0..col {
assert!(
cov[row + col * 3] == cov[col + row * 3],
"cov should be symmetric: cov[{row},{col}] != cov[{col},{row}]"
);
}
}
}
#[test]
fn test_covariance_ddof0() {
let x = vec![
1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0,
];
let mut cov = vec![0.0_f64; 9];
covariance(4, 3, &x, 4, &mut cov, 3, 0).unwrap();
for col in 0..3_usize {
for row in 0..3_usize {
let idx = row + col * 3;
assert!(
approx_eq(cov[idx], 11.25, TOL),
"cov_pop[{row},{col}] = {}, expected 11.25",
cov[idx]
);
}
}
}
#[test]
fn test_covariance_uncorrelated() {
let x = vec![
1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, ];
let mut cov = vec![0.0_f64; 4];
covariance(4, 2, &x, 4, &mut cov, 2, 1).unwrap();
let expected_var = 4.0 / 3.0;
assert!(
approx_eq(cov[0], expected_var, TOL),
"cov[0,0] = {}, expected {}",
cov[0],
expected_var
);
assert!(
approx_eq(cov[1], 0.0, TOL),
"cov[1,0] = {}, expected 0.0",
cov[1]
);
assert!(
approx_eq(cov[2], 0.0, TOL),
"cov[0,1] = {}, expected 0.0",
cov[2]
);
assert!(
approx_eq(cov[3], expected_var, TOL),
"cov[1,1] = {}, expected {}",
cov[3],
expected_var
);
}
#[test]
fn test_log_det_spd_2x2() {
let mut a = vec![4.0, 2.0, 2.0, 3.0];
let ld = log_det_spd(2, &mut a, 2).unwrap();
assert!(
approx_eq(ld, 8.0_f64.ln(), TOL),
"log_det = {ld}, expected {}",
8.0_f64.ln()
);
}
#[test]
fn test_log_det_spd_3x3() {
let mut a = vec![4.0, 2.0, 1.0, 2.0, 5.0, 3.0, 1.0, 3.0, 6.0];
let ld = log_det_spd(3, &mut a, 3).unwrap();
assert!(
approx_eq(ld, 67.0_f64.ln(), TOL),
"log_det = {ld}, expected {}",
67.0_f64.ln()
);
}
#[test]
fn test_mahalanobis_identity_covariance() {
let x = vec![3.0, 4.0]; let mean = vec![0.0, 0.0];
let sigma_chol = vec![1.0, 0.0, 0.0, 1.0]; let mut distances = vec![0.0_f64; 1];
mahalanobis(1, 2, &x, 1, &mean, &sigma_chol, 2, &mut distances).unwrap();
assert!(
approx_eq(distances[0], 5.0, TOL),
"distance = {}, expected 5.0",
distances[0]
);
}
#[test]
fn test_mahalanobis_non_identity() {
let x = vec![2.0, 1.0]; let mean = vec![0.0, 0.0];
let sigma_chol = vec![2.0, 0.0, 0.0, 1.0]; let mut distances = vec![0.0_f64; 1];
mahalanobis(1, 2, &x, 1, &mean, &sigma_chol, 2, &mut distances).unwrap();
assert!(
approx_eq(distances[0], 2.0_f64.sqrt(), TOL),
"distance = {}, expected {}",
distances[0],
2.0_f64.sqrt()
);
}
#[test]
fn test_mahalanobis_multiple_observations() {
let x = vec![1.0, 4.0, 1.0, 1.0, 1.0, 5.0]; let mean = vec![1.0, 1.0];
let sigma_chol = vec![1.0, 0.0, 0.0, 1.0]; let mut distances = vec![0.0_f64; 3];
mahalanobis(3, 2, &x, 3, &mean, &sigma_chol, 2, &mut distances).unwrap();
assert!(
approx_eq(distances[0], 0.0, TOL),
"d[0] = {}, expected 0.0",
distances[0]
);
assert!(
approx_eq(distances[1], 3.0, TOL),
"d[1] = {}, expected 3.0",
distances[1]
);
assert!(
approx_eq(distances[2], 4.0, TOL),
"d[2] = {}, expected 4.0",
distances[2]
);
}
#[test]
fn test_triangular_inverse_lower() {
let mut l = vec![2.0, 1.0, 4.0, 0.0, 3.0, 5.0, 0.0, 0.0, 6.0];
let l_orig = l.clone();
triangular_inverse(3, &mut l, 3, false).unwrap();
let product = mm(3, 3, 3, &l_orig, &l);
for col in 0..3_usize {
for row in 0..3_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 3;
assert!(
approx_eq(product[idx], expected, TOL),
"L*Linv[{row},{col}] = {}, expected {expected}",
product[idx]
);
}
}
}
#[test]
fn test_triangular_inverse_upper() {
let mut u = vec![2.0, 0.0, 0.0, 1.0, 4.0, 0.0, 3.0, 2.0, 5.0];
let u_orig = u.clone();
triangular_inverse(3, &mut u, 3, true).unwrap();
let product = mm(3, 3, 3, &u_orig, &u);
for col in 0..3_usize {
for row in 0..3_usize {
let expected = if row == col { 1.0 } else { 0.0 };
let idx = row + col * 3;
assert!(
approx_eq(product[idx], expected, TOL),
"U*Uinv[{row},{col}] = {}, expected {expected}",
product[idx]
);
}
}
}
}