use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::{Mat, MatRef};
use crate::lu::{Lu, LuError};
use crate::svd::{Svd, SvdError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvError {
NotSquare,
Singular,
}
impl core::fmt::Display for InvError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotSquare => write!(f, "Matrix must be square"),
Self::Singular => write!(f, "Matrix is singular"),
}
}
}
impl std::error::Error for InvError {}
impl From<LuError> for InvError {
fn from(e: LuError) -> Self {
match e {
LuError::NotSquare { .. } => Self::NotSquare,
LuError::Singular { .. } => Self::Singular,
LuError::DimensionMismatch { .. } => Self::NotSquare,
}
}
}
#[derive(Debug, Clone)]
pub struct PinvResult<T: Scalar> {
pub pinv: Mat<T>,
pub rank: usize,
pub singular_values: Vec<T>,
}
pub fn inv<T: Field + bytemuck::Zeroable>(a: MatRef<'_, T>) -> Result<Mat<T>, InvError> {
let lu = Lu::compute(a)?;
Ok(lu.inverse()?)
}
pub fn pinv<T: Field + Real + bytemuck::Zeroable>(
a: MatRef<'_, T>,
tol: T,
) -> Result<PinvResult<T>, SvdError> {
let svd = Svd::compute(a)?;
let pinv_mat = svd.pseudoinverse(tol);
let rank = svd.rank(tol);
let singular_values = svd.singular_values().to_vec();
Ok(PinvResult {
pinv: pinv_mat,
rank,
singular_values,
})
}
pub fn pinv_default<T: Field + Real + bytemuck::Zeroable>(
a: MatRef<'_, T>,
) -> Result<PinvResult<T>, SvdError> {
let svd = Svd::compute(a)?;
let m = a.nrows();
let n = a.ncols();
let eps = <T as Scalar>::epsilon();
let sigma_max = if svd.singular_values().is_empty() {
T::one()
} else {
svd.singular_values()[0]
};
let tol = eps * T::from_f64(m.max(n) as f64).unwrap_or(T::one()) * sigma_max;
let pinv_mat = svd.pseudoinverse(tol);
let rank = svd.rank(tol);
let singular_values = svd.singular_values().to_vec();
Ok(PinvResult {
pinv: pinv_mat,
rank,
singular_values,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_inv_2x2() {
let a = Mat::from_rows(&[&[4.0f64, 7.0], &[2.0, 6.0]]);
let a_inv = inv(a.as_ref()).unwrap();
assert!(approx_eq(a_inv[(0, 0)], 0.6, 1e-10));
assert!(approx_eq(a_inv[(0, 1)], -0.7, 1e-10));
assert!(approx_eq(a_inv[(1, 0)], -0.2, 1e-10));
assert!(approx_eq(a_inv[(1, 1)], 0.4, 1e-10));
}
#[test]
fn test_inv_3x3() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[0.0, 1.0, 4.0], &[5.0, 6.0, 0.0]]);
let a_inv = inv(a.as_ref()).unwrap();
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += a[(i, k)] * a_inv[(k, j)];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(sum, expected, 1e-9),
"(A*A^-1)[{},{}] = {}, expected {}",
i,
j,
sum,
expected
);
}
}
}
#[test]
fn test_inv_identity() {
let eye = Mat::from_rows(&[&[1.0f64, 0.0, 0.0], &[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]]);
let inv_eye = inv(eye.as_ref()).unwrap();
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(approx_eq(inv_eye[(i, j)], expected, 1e-10));
}
}
}
#[test]
fn test_inv_singular() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[2.0, 4.0]]);
let result = inv(a.as_ref());
assert!(matches!(result, Err(InvError::Singular)));
}
#[test]
fn test_inv_not_square() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let result = inv(a.as_ref());
assert!(matches!(result, Err(InvError::NotSquare)));
}
#[test]
fn test_pinv_square() {
let a = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 2.0]]);
let result = pinv(a.as_ref(), 1e-10).unwrap();
assert!(approx_eq(result.pinv[(0, 0)], 1.0, 1e-10));
assert!(approx_eq(result.pinv[(1, 1)], 0.5, 1e-10));
assert!(approx_eq(result.pinv[(0, 1)], 0.0, 1e-10));
assert!(approx_eq(result.pinv[(1, 0)], 0.0, 1e-10));
}
#[test]
fn test_pinv_tall() {
let a = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0], &[0.0, 0.0]]);
let result = pinv(a.as_ref(), 1e-10).unwrap();
assert_eq!(result.pinv.nrows(), 2);
assert_eq!(result.pinv.ncols(), 3);
let mut product = Mat::zeros(3, 2);
let mut temp = Mat::zeros(3, 3);
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..2 {
sum += a[(i, k)] * result.pinv[(k, j)];
}
temp[(i, j)] = sum;
}
}
for i in 0..3 {
for j in 0..2 {
let mut sum = 0.0;
for k in 0..3 {
sum += temp[(i, k)] * a[(k, j)];
}
product[(i, j)] = sum;
}
}
for i in 0..3 {
for j in 0..2 {
assert!(
approx_eq(product[(i, j)], a[(i, j)], 1e-9),
"A*A^+*A[{},{}] = {}, expected {}",
i,
j,
product[(i, j)],
a[(i, j)]
);
}
}
}
#[test]
fn test_pinv_wide() {
let a = Mat::from_rows(&[&[1.0f64, 0.0, 0.0], &[0.0, 1.0, 0.0]]);
let result = pinv(a.as_ref(), 1e-10).unwrap();
assert_eq!(result.pinv.nrows(), 3);
assert_eq!(result.pinv.ncols(), 2);
}
#[test]
fn test_pinv_rank_deficient() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[2.0, 4.0, 6.0], &[3.0, 6.0, 9.0]]);
let result = pinv(a.as_ref(), 1e-10).unwrap();
assert_eq!(result.rank, 1);
let mut product = Mat::zeros(3, 3);
let mut temp = Mat::zeros(3, 3);
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += a[(i, k)] * result.pinv[(k, j)];
}
temp[(i, j)] = sum;
}
}
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += temp[(i, k)] * a[(k, j)];
}
product[(i, j)] = sum;
}
}
for i in 0..3 {
for j in 0..3 {
assert!(
approx_eq(product[(i, j)], a[(i, j)], 1e-9),
"A*A^+*A[{},{}] = {}, expected {}",
i,
j,
product[(i, j)],
a[(i, j)]
);
}
}
}
#[test]
fn test_inv_f32() {
let a = Mat::from_rows(&[&[4.0f32, 7.0], &[2.0, 6.0]]);
let a_inv = inv(a.as_ref()).unwrap();
assert!((a_inv[(0, 0)] - 0.6).abs() < 1e-5);
assert!((a_inv[(0, 1)] + 0.7).abs() < 1e-5);
}
}