use oxiblas_core::scalar::Field;
use oxiblas_matrix::MatRef;
use crate::lu::{Lu, LuError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetError {
NotSquare,
Singular,
}
impl core::fmt::Display for DetError {
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 DetError {}
impl From<LuError> for DetError {
fn from(e: LuError) -> Self {
match e {
LuError::NotSquare { .. } => Self::NotSquare,
LuError::Singular { .. } => Self::Singular,
LuError::DimensionMismatch { .. } => Self::NotSquare,
}
}
}
pub fn det<T: Field + bytemuck::Zeroable>(a: MatRef<'_, T>) -> Result<T, DetError> {
match Lu::compute(a) {
Ok(lu) => Ok(lu.determinant()),
Err(LuError::Singular { .. }) => Ok(T::zero()),
Err(e) => Err(e.into()),
}
}
pub fn det_lu<T: Field + bytemuck::Zeroable>(a: MatRef<'_, T>) -> Result<(T, Lu<T>), DetError> {
let lu = Lu::compute(a)?;
let d = lu.determinant();
Ok((d, lu))
}
#[cfg(test)]
mod tests {
use super::*;
use oxiblas_matrix::Mat;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_det_2x2() {
let a = Mat::from_rows(&[&[4.0f64, 7.0], &[2.0, 6.0]]);
let d = det(a.as_ref()).unwrap();
assert!(approx_eq(d, 10.0, 1e-10));
}
#[test]
fn test_det_3x3() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 10.0]]);
let d = det(a.as_ref()).unwrap();
assert!(approx_eq(d, -3.0, 1e-10));
}
#[test]
fn test_det_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 d = det(eye.as_ref()).unwrap();
assert!(approx_eq(d, 1.0, 1e-10));
}
#[test]
fn test_det_diagonal() {
let a = Mat::from_rows(&[&[2.0f64, 0.0, 0.0], &[0.0, 3.0, 0.0], &[0.0, 0.0, 4.0]]);
let d = det(a.as_ref()).unwrap();
assert!(approx_eq(d, 24.0, 1e-10));
}
#[test]
fn test_det_singular_proportional_rows() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[2.0, 4.0]]);
let d = det(a.as_ref()).expect("det() must not error on singular square input");
assert_eq!(d, 0.0, "singular matrix must yield det == 0.0 exactly");
}
#[test]
fn test_det_singular_identical_rows() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[1.0, 2.0, 3.0], &[4.0, 5.0, 7.0]]);
let d = det(a.as_ref()).expect("det() must not error on singular square input");
assert_eq!(
d, 0.0,
"two identical rows must yield det == 0.0 exactly, got {d}"
);
}
#[test]
fn test_det_small_magnitude_well_conditioned_not_falsely_singular() {
let scale = 1.0e-8f64;
let a = Mat::from_rows(&[&[scale, 0.0, 0.0], &[0.0, scale, 0.0], &[0.0, 0.0, scale]]);
let d = det(a.as_ref()).expect("well-conditioned small-magnitude matrix must not error");
let expected = scale * scale * scale;
assert!(
d != 0.0,
"small-magnitude well-conditioned matrix must not be falsely flagged as det == 0"
);
assert!(
((d - expected) / expected).abs() < 1e-9,
"det = {d}, expected ~= {expected}"
);
}
#[test]
fn test_det_not_square() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let result = det(a.as_ref());
assert!(matches!(result, Err(DetError::NotSquare)));
}
#[test]
fn test_det_lu_still_errors_on_singular() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[2.0, 4.0]]);
let result = det_lu(a.as_ref());
assert!(matches!(result, Err(DetError::Singular)));
}
#[test]
fn test_det_negative() {
let a = Mat::from_rows(&[&[0.0f64, 1.0], &[1.0, 0.0]]);
let d = det(a.as_ref()).unwrap();
assert!(approx_eq(d, -1.0, 1e-10));
}
#[test]
fn test_det_lu_reuse() {
let a = Mat::from_rows(&[&[2.0f64, 1.0], &[1.0, 3.0]]);
let (d, lu) = det_lu(a.as_ref()).unwrap();
assert!(approx_eq(d, 5.0, 1e-10));
let b = Mat::from_rows(&[&[5.0], &[7.0]]);
let x = lu.solve(b.as_ref()).unwrap();
let ax0 = 2.0 * x[(0, 0)] + 1.0 * x[(1, 0)];
let ax1 = 1.0 * x[(0, 0)] + 3.0 * x[(1, 0)];
assert!(approx_eq(ax0, 5.0, 1e-10));
assert!(approx_eq(ax1, 7.0, 1e-10));
}
#[test]
fn test_det_f32() {
let a = Mat::from_rows(&[&[4.0f32, 7.0], &[2.0, 6.0]]);
let d = det(a.as_ref()).unwrap();
assert!((d - 10.0).abs() < 1e-5);
}
}