use std::array::from_fn;
use num_bigint::{BigInt, Sign};
use num_rational::BigRational;
use num_traits::ToPrimitive;
use crate::LaError;
use crate::matrix::Matrix;
use crate::vector::Vector;
fn validate_finite<const D: usize>(m: &Matrix<D>) -> Result<(), LaError> {
for r in 0..D {
for c in 0..D {
if !m.rows[r][c].is_finite() {
return Err(LaError::NonFinite {
row: Some(r),
col: c,
});
}
}
}
Ok(())
}
fn validate_finite_vec<const D: usize>(v: &Vector<D>) -> Result<(), LaError> {
for (i, &x) in v.data.iter().enumerate() {
if !x.is_finite() {
return Err(LaError::NonFinite { row: None, col: i });
}
}
Ok(())
}
fn f64_decompose(x: f64) -> Option<(u64, i32, bool)> {
let bits = x.to_bits();
let biased_exp = ((bits >> 52) & 0x7FF) as i32;
let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
if biased_exp == 0 && fraction == 0 {
return None;
}
assert!(biased_exp != 0x7FF, "non-finite f64 in exact conversion");
let (mantissa, raw_exp) = if biased_exp == 0 {
(fraction, -1074_i32)
} else {
((1u64 << 52) | fraction, biased_exp - 1075)
};
let tz = mantissa.trailing_zeros();
let mantissa = mantissa >> tz;
let exponent = raw_exp + tz.cast_signed();
let is_negative = bits >> 63 != 0;
Some((mantissa, exponent, is_negative))
}
fn f64_to_bigrational(x: f64) -> BigRational {
let Some((mantissa, exponent, is_negative)) = f64_decompose(x) else {
return BigRational::from_integer(BigInt::from(0));
};
let numer = if is_negative {
-BigInt::from(mantissa)
} else {
BigInt::from(mantissa)
};
if exponent >= 0 {
BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
} else {
BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
}
}
fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
if value == BigInt::from(0) {
return BigRational::from_integer(BigInt::from(0));
}
if exp < 0
&& let Some(tz) = value.trailing_zeros()
{
let reduce = tz.min(u64::from((-exp).cast_unsigned()));
value >>= reduce;
exp += i32::try_from(reduce).expect("reduce ≤ -exp which fits in i32");
}
if exp >= 0 {
BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
} else {
BigRational::new_raw(value, BigInt::from(1u32) << (-exp).cast_unsigned())
}
}
fn bareiss_det_int<const D: usize>(m: &Matrix<D>) -> (BigInt, i32) {
if D == 0 {
return (BigInt::from(1), 0);
}
if D == 1 {
return match f64_decompose(m.rows[0][0]) {
None => (BigInt::from(0), 0),
Some((mant, exp, neg)) => {
let v = if neg {
-BigInt::from(mant)
} else {
BigInt::from(mant)
};
(v, exp)
}
};
}
let mut components = [[(0u64, 0i32, false); D]; D];
let mut e_min = i32::MAX;
for (r, row) in m.rows.iter().enumerate() {
for (c, &entry) in row.iter().enumerate() {
if let Some((mant, exp, neg)) = f64_decompose(entry) {
components[r][c] = (mant, exp, neg);
e_min = e_min.min(exp);
}
}
}
if e_min == i32::MAX {
return (BigInt::from(0), 0);
}
let mut a: [[BigInt; D]; D] = from_fn(|r| {
from_fn(|c| {
let (mant, exp, neg) = components[r][c];
if mant == 0 {
BigInt::from(0)
} else {
let shift = (exp - e_min).cast_unsigned();
let v = BigInt::from(mant) << shift;
if neg { -v } else { v }
}
})
});
let zero = BigInt::from(0);
let mut prev_pivot = BigInt::from(1);
let mut sign: i8 = 1;
for k in 0..D {
if a[k][k] == zero {
let mut found = false;
for i in (k + 1)..D {
if a[i][k] != zero {
a.swap(k, i);
sign = -sign;
found = true;
break;
}
}
if !found {
return (BigInt::from(0), 0);
}
}
for i in (k + 1)..D {
for j in (k + 1)..D {
a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
}
a[i][k].clone_from(&zero);
}
prev_pivot.clone_from(&a[k][k]);
}
let det_int = if sign < 0 {
-&a[D - 1][D - 1]
} else {
a[D - 1][D - 1].clone()
};
let d_i32 = i32::try_from(D).expect("dimension exceeds i32");
let total_exp = e_min
.checked_mul(d_i32)
.expect("exponent overflow in bareiss_det_int");
(det_int, total_exp)
}
fn bareiss_det<const D: usize>(m: &Matrix<D>) -> BigRational {
let (det_int, total_exp) = bareiss_det_int(m);
bigint_exp_to_bigrational(det_int, total_exp)
}
fn gauss_solve<const D: usize>(m: &Matrix<D>, b: &Vector<D>) -> Result<[BigRational; D], LaError> {
let zero = BigRational::from_integer(BigInt::from(0));
let mut mat: [[BigRational; D]; D] = from_fn(|r| from_fn(|c| f64_to_bigrational(m.rows[r][c])));
let mut rhs: [BigRational; D] = from_fn(|r| f64_to_bigrational(b.data[r]));
for k in 0..D {
if mat[k][k] == zero {
if let Some(swap_row) = ((k + 1)..D).find(|&i| mat[i][k] != zero) {
mat.swap(k, swap_row);
rhs.swap(k, swap_row);
} else {
return Err(LaError::Singular { pivot_col: k });
}
}
let pivot = mat[k][k].clone();
for i in (k + 1)..D {
if mat[i][k] != zero {
let factor = &mat[i][k] / &pivot;
#[allow(clippy::needless_range_loop)]
for j in (k + 1)..D {
let term = &factor * &mat[k][j];
mat[i][j] -= term;
}
let rhs_term = &factor * &rhs[k];
rhs[i] -= rhs_term;
mat[i][k] = zero.clone();
}
}
}
let mut x: [BigRational; D] = from_fn(|_| zero.clone());
for i in (0..D).rev() {
let mut sum = rhs[i].clone();
for j in (i + 1)..D {
sum -= &mat[i][j] * &x[j];
}
x[i] = sum / &mat[i][i];
}
Ok(x)
}
impl<const D: usize> Matrix<D> {
#[inline]
pub fn det_exact(&self) -> Result<BigRational, LaError> {
validate_finite(self)?;
Ok(bareiss_det(self))
}
#[inline]
pub fn det_exact_f64(&self) -> Result<f64, LaError> {
let exact = self.det_exact()?;
let val = exact.to_f64().unwrap_or(f64::INFINITY);
if val.is_finite() {
Ok(val)
} else {
Err(LaError::Overflow { index: None })
}
}
#[inline]
pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
validate_finite(self)?;
validate_finite_vec(&b)?;
gauss_solve(self, &b)
}
#[inline]
pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
let exact = self.solve_exact(b)?;
let mut result = [0.0f64; D];
for (i, val) in exact.iter().enumerate() {
let f = val.to_f64().unwrap_or(f64::INFINITY);
if !f.is_finite() {
return Err(LaError::Overflow { index: Some(i) });
}
result[i] = f;
}
Ok(Vector::new(result))
}
#[inline]
pub fn det_sign_exact(&self) -> Result<i8, LaError> {
validate_finite(self)?;
if let (Some(det_f64), Some(err)) = (self.det_direct(), self.det_errbound()) {
if det_f64.is_finite() {
if det_f64 > err {
return Ok(1);
}
if det_f64 < -err {
return Ok(-1);
}
}
}
let (det_int, _) = bareiss_det_int(self);
Ok(match det_int.sign() {
Sign::Plus => 1,
Sign::Minus => -1,
Sign::NoSign => 0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DEFAULT_PIVOT_TOL;
use pastey::paste;
macro_rules! gen_det_exact_tests {
($d:literal) => {
paste! {
#[test]
fn [<det_exact_identity_ $d d>]() {
let det = Matrix::<$d>::identity().det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn [<det_exact_err_on_nan_ $d d>]() {
let mut m = Matrix::<$d>::identity();
m.set(0, 0, f64::NAN);
assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
#[test]
fn [<det_exact_err_on_inf_ $d d>]() {
let mut m = Matrix::<$d>::identity();
m.set(0, 0, f64::INFINITY);
assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
}
};
}
gen_det_exact_tests!(2);
gen_det_exact_tests!(3);
gen_det_exact_tests!(4);
gen_det_exact_tests!(5);
macro_rules! gen_det_exact_f64_tests {
($d:literal) => {
paste! {
#[test]
fn [<det_exact_f64_identity_ $d d>]() {
let det = Matrix::<$d>::identity().det_exact_f64().unwrap();
assert!((det - 1.0).abs() <= f64::EPSILON);
}
#[test]
fn [<det_exact_f64_err_on_nan_ $d d>]() {
let mut m = Matrix::<$d>::identity();
m.set(0, 0, f64::NAN);
assert_eq!(m.det_exact_f64(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
}
};
}
gen_det_exact_f64_tests!(2);
gen_det_exact_f64_tests!(3);
gen_det_exact_f64_tests!(4);
gen_det_exact_f64_tests!(5);
macro_rules! gen_det_exact_f64_agrees_with_det_direct {
($d:literal) => {
paste! {
#[test]
#[allow(clippy::cast_precision_loss)]
fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
for r in 0..$d {
for c in 0..$d {
rows[r][c] = if r == c {
(r as f64) + f64::from($d) + 1.0
} else {
0.1 / ((r + c + 1) as f64)
};
}
}
let m = Matrix::<$d>::from_rows(rows);
let exact = m.det_exact_f64().unwrap();
let direct = m.det_direct().unwrap();
let eps = direct.abs().mul_add(1e-12, 1e-12);
assert!((exact - direct).abs() <= eps);
}
}
};
}
gen_det_exact_f64_agrees_with_det_direct!(2);
gen_det_exact_f64_agrees_with_det_direct!(3);
gen_det_exact_f64_agrees_with_det_direct!(4);
#[test]
fn det_sign_exact_d0_is_positive() {
assert_eq!(Matrix::<0>::zero().det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_d1_positive() {
let m = Matrix::<1>::from_rows([[42.0]]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_d1_negative() {
let m = Matrix::<1>::from_rows([[-3.5]]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_d1_zero() {
let m = Matrix::<1>::from_rows([[0.0]]);
assert_eq!(m.det_sign_exact().unwrap(), 0);
}
#[test]
fn det_sign_exact_identity_2d() {
assert_eq!(Matrix::<2>::identity().det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_identity_3d() {
assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_identity_4d() {
assert_eq!(Matrix::<4>::identity().det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_identity_5d() {
assert_eq!(Matrix::<5>::identity().det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_singular_duplicate_rows() {
let m = Matrix::<3>::from_rows([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[1.0, 2.0, 3.0], ]);
assert_eq!(m.det_sign_exact().unwrap(), 0);
}
#[test]
fn det_sign_exact_singular_linear_combination() {
let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
assert_eq!(m.det_sign_exact().unwrap(), 0);
}
#[test]
fn det_sign_exact_negative_det_row_swap() {
let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_negative_det_known() {
let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_agrees_with_det_for_spd() {
let m = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
assert!(m.det(DEFAULT_PIVOT_TOL).unwrap() > 0.0);
}
#[test]
fn det_sign_exact_near_singular_perturbation() {
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); let m = Matrix::<3>::from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_fast_filter_positive_4x4() {
let m = Matrix::<4>::from_rows([
[2.0, 1.0, 0.0, 0.0],
[1.0, 3.0, 1.0, 0.0],
[0.0, 1.0, 4.0, 1.0],
[0.0, 0.0, 1.0, 5.0],
]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_fast_filter_negative_4x4() {
let m = Matrix::<4>::from_rows([
[1.0, 3.0, 1.0, 0.0],
[2.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 4.0, 1.0],
[0.0, 0.0, 1.0, 5.0],
]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_subnormal_entries() {
let tiny = 5e-324_f64; assert!(tiny.is_subnormal());
let m = Matrix::<2>::from_rows([[tiny, 0.0], [0.0, tiny]]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
}
#[test]
fn det_sign_exact_returns_err_on_nan() {
let m = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
assert_eq!(
m.det_sign_exact(),
Err(LaError::NonFinite {
row: Some(0),
col: 0
})
);
}
#[test]
fn det_sign_exact_returns_err_on_infinity() {
let m = Matrix::<2>::from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]);
assert_eq!(
m.det_sign_exact(),
Err(LaError::NonFinite {
row: Some(0),
col: 0
})
);
}
#[test]
fn det_sign_exact_returns_err_on_nan_5x5() {
let mut m = Matrix::<5>::identity();
m.set(2, 3, f64::NAN);
assert_eq!(
m.det_sign_exact(),
Err(LaError::NonFinite {
row: Some(2),
col: 3
})
);
}
#[test]
fn det_sign_exact_returns_err_on_infinity_5x5() {
let mut m = Matrix::<5>::identity();
m.set(0, 0, f64::INFINITY);
assert_eq!(
m.det_sign_exact(),
Err(LaError::NonFinite {
row: Some(0),
col: 0
})
);
}
#[test]
fn det_sign_exact_pivot_needed_5x5() {
let m = Matrix::<5>::from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
]);
assert_eq!(m.det_sign_exact().unwrap(), -1);
}
#[test]
fn det_sign_exact_5x5_known() {
let m = Matrix::<5>::from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
}
#[test]
fn det_errbound_d0_is_zero() {
assert_eq!(Matrix::<0>::zero().det_errbound(), Some(0.0));
}
#[test]
fn det_errbound_d1_is_zero() {
assert_eq!(Matrix::<1>::from_rows([[42.0]]).det_errbound(), Some(0.0));
}
#[test]
fn det_errbound_d2_positive() {
let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let bound = m.det_errbound().unwrap();
assert!(bound > 0.0);
assert!(crate::ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
}
#[test]
fn det_errbound_d3_positive() {
let m = Matrix::<3>::identity();
let bound = m.det_errbound().unwrap();
assert!(bound > 0.0);
}
#[test]
fn det_errbound_d3_non_identity() {
let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]]);
let bound = m.det_errbound().unwrap();
assert!(bound > 0.0);
}
#[test]
fn det_errbound_d4_positive() {
let m = Matrix::<4>::identity();
let bound = m.det_errbound().unwrap();
assert!(bound > 0.0);
}
#[test]
fn det_errbound_d4_non_identity() {
let m = Matrix::<4>::from_rows([
[1.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, 3.0, 0.0],
[0.0, 0.0, 0.0, 4.0],
]);
let bound = m.det_errbound().unwrap();
assert!(bound > 0.0);
}
#[test]
fn det_errbound_d5_is_none() {
assert_eq!(Matrix::<5>::identity().det_errbound(), None);
}
#[test]
fn f64_decompose_zero() {
assert!(f64_decompose(0.0).is_none());
assert!(f64_decompose(-0.0).is_none());
}
#[test]
fn f64_decompose_one() {
let (mant, exp, neg) = f64_decompose(1.0).unwrap();
assert_eq!(mant, 1);
assert_eq!(exp, 0);
assert!(!neg);
}
#[test]
fn f64_decompose_negative() {
let (mant, exp, neg) = f64_decompose(-3.5).unwrap();
assert_eq!(mant, 7);
assert_eq!(exp, -1);
assert!(neg);
}
#[test]
fn f64_decompose_subnormal() {
let tiny = 5e-324_f64;
assert!(tiny.is_subnormal());
let (mant, exp, neg) = f64_decompose(tiny).unwrap();
assert_eq!(mant, 1);
assert_eq!(exp, -1074);
assert!(!neg);
}
#[test]
fn f64_decompose_power_of_two() {
let (mant, exp, neg) = f64_decompose(1024.0).unwrap();
assert_eq!(mant, 1);
assert_eq!(exp, 10); assert!(!neg);
}
#[test]
#[should_panic(expected = "non-finite f64 in exact conversion")]
fn f64_decompose_panics_on_nan() {
f64_decompose(f64::NAN);
}
#[test]
fn bareiss_det_int_d0() {
let (det, exp) = bareiss_det_int(&Matrix::<0>::zero());
assert_eq!(det, BigInt::from(1));
assert_eq!(exp, 0);
}
#[test]
fn bareiss_det_int_d1_value() {
let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[7.0]]));
assert_eq!(det, BigInt::from(7));
assert_eq!(exp, 0);
}
#[test]
fn bareiss_det_int_d1_zero() {
let (det, _) = bareiss_det_int(&Matrix::<1>::from_rows([[0.0]]));
assert_eq!(det, BigInt::from(0));
}
#[test]
fn bareiss_det_int_d2_known() {
let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let (det_int, total_exp) = bareiss_det_int(&m);
let det = bigint_exp_to_bigrational(det_int, total_exp);
assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
}
#[test]
fn bareiss_det_int_all_zeros() {
let (det, _) = bareiss_det_int(&Matrix::<3>::zero());
assert_eq!(det, BigInt::from(0));
}
#[test]
fn bareiss_det_int_sign_matches_det_sign_exact() {
let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let (det_int, _) = bareiss_det_int(&m);
assert_eq!(det_int.sign(), Sign::Minus); }
#[test]
fn bareiss_det_int_fractional_entries() {
let m = Matrix::<2>::from_rows([[0.5, 0.25], [1.0, 1.0]]);
let (det_int, total_exp) = bareiss_det_int(&m);
let det = bigint_exp_to_bigrational(det_int, total_exp);
assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
}
#[test]
fn bareiss_det_int_d1_negative() {
let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[-3.5]]));
assert_eq!(det, BigInt::from(-7));
assert_eq!(exp, -1);
}
#[test]
fn bareiss_det_int_d1_fractional() {
let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[0.5]]));
assert_eq!(det, BigInt::from(1));
assert_eq!(exp, -1);
}
#[test]
fn bareiss_det_int_d3_with_pivoting() {
let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let (det_int, total_exp) = bareiss_det_int(&m);
let det = bigint_exp_to_bigrational(det_int, total_exp);
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
macro_rules! gen_bareiss_det_int_identity_tests {
($d:literal) => {
paste! {
#[test]
fn [<bareiss_det_int_identity_ $d d>]() {
let (det_int, total_exp) = bareiss_det_int(&Matrix::<$d>::identity());
let det = bigint_exp_to_bigrational(det_int, total_exp);
assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
}
}
};
}
gen_bareiss_det_int_identity_tests!(2);
gen_bareiss_det_int_identity_tests!(3);
gen_bareiss_det_int_identity_tests!(4);
gen_bareiss_det_int_identity_tests!(5);
#[test]
fn bigint_exp_to_bigrational_zero() {
let r = bigint_exp_to_bigrational(BigInt::from(0), -50);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn bigint_exp_to_bigrational_positive_exp() {
let r = bigint_exp_to_bigrational(BigInt::from(3), 2);
assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
}
#[test]
fn bigint_exp_to_bigrational_negative_exp_reduced() {
let r = bigint_exp_to_bigrational(BigInt::from(6), -2);
assert_eq!(*r.numer(), BigInt::from(3));
assert_eq!(*r.denom(), BigInt::from(2));
}
#[test]
fn bigint_exp_to_bigrational_negative_exp_already_odd() {
let r = bigint_exp_to_bigrational(BigInt::from(3), -2);
assert_eq!(*r.numer(), BigInt::from(3));
assert_eq!(*r.denom(), BigInt::from(4));
}
#[test]
fn bigint_exp_to_bigrational_negative_value() {
let r = bigint_exp_to_bigrational(BigInt::from(-5), 1);
assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
}
#[test]
fn bigint_exp_to_bigrational_negative_value_with_denominator() {
let r = bigint_exp_to_bigrational(BigInt::from(-3), -2);
assert_eq!(*r.numer(), BigInt::from(-3));
assert_eq!(*r.denom(), BigInt::from(4));
}
#[test]
fn bareiss_det_d0_is_one() {
let det = bareiss_det(&Matrix::<0>::zero());
assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn bareiss_det_d1_returns_entry() {
let det = bareiss_det(&Matrix::<1>::from_rows([[7.0]]));
assert_eq!(det, f64_to_bigrational(7.0));
}
#[test]
fn bareiss_det_d3_with_pivoting() {
let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let det = bareiss_det(&m);
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn bareiss_det_singular_all_zeros_in_column() {
let m = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let det = bareiss_det(&m);
assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn det_sign_exact_overflow_determinant_finite_entries() {
let big = f64::MAX / 2.0;
assert!(big.is_finite());
let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
assert_eq!(m.det_sign_exact().unwrap(), 1);
}
#[test]
fn det_exact_d0_is_one() {
let det = Matrix::<0>::zero().det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn det_exact_known_2x2() {
let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
}
#[test]
fn det_exact_singular_returns_zero() {
let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]);
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn det_exact_near_singular_perturbation() {
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); let m = Matrix::<3>::from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]);
let det = m.det_exact().unwrap();
let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
assert_eq!(det, expected);
}
#[test]
fn det_exact_5x5_permutation() {
let m = Matrix::<5>::from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
]);
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn det_exact_f64_known_2x2() {
let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let det = m.det_exact_f64().unwrap();
assert!((det - (-2.0)).abs() <= f64::EPSILON);
}
#[test]
fn det_exact_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
assert_eq!(m.det_exact_f64(), Err(LaError::Overflow { index: None }));
}
fn arbitrary_rhs<const D: usize>() -> Vector<D> {
let values = [1.0, -2.5, 3.0, 0.25, -4.0];
let mut arr = [0.0f64; D];
for (dst, src) in arr.iter_mut().zip(values.iter()) {
*dst = *src;
}
Vector::<D>::new(arr)
}
macro_rules! gen_solve_exact_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_identity_ $d d>]() {
let a = Matrix::<$d>::identity();
let b = arbitrary_rhs::<$d>();
let x = a.solve_exact(b).unwrap();
for (i, xi) in x.iter().enumerate() {
assert_eq!(*xi, f64_to_bigrational(b.data[i]));
}
}
#[test]
fn [<solve_exact_err_on_nan_matrix_ $d d>]() {
let mut a = Matrix::<$d>::identity();
a.set(0, 0, f64::NAN);
let b = arbitrary_rhs::<$d>();
assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
#[test]
fn [<solve_exact_err_on_inf_matrix_ $d d>]() {
let mut a = Matrix::<$d>::identity();
a.set(0, 0, f64::INFINITY);
let b = arbitrary_rhs::<$d>();
assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
#[test]
fn [<solve_exact_err_on_nan_vector_ $d d>]() {
let a = Matrix::<$d>::identity();
let mut b_arr = [1.0f64; $d];
b_arr[0] = f64::NAN;
let b = Vector::<$d>::new(b_arr);
assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: 0 }));
}
#[test]
fn [<solve_exact_err_on_inf_vector_ $d d>]() {
let a = Matrix::<$d>::identity();
let mut b_arr = [1.0f64; $d];
b_arr[$d - 1] = f64::INFINITY;
let b = Vector::<$d>::new(b_arr);
assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: $d - 1 }));
}
#[test]
fn [<solve_exact_singular_ $d d>]() {
let a = Matrix::<$d>::zero();
let b = arbitrary_rhs::<$d>();
assert_eq!(a.solve_exact(b), Err(LaError::Singular { pivot_col: 0 }));
}
}
};
}
gen_solve_exact_tests!(2);
gen_solve_exact_tests!(3);
gen_solve_exact_tests!(4);
gen_solve_exact_tests!(5);
macro_rules! gen_solve_exact_f64_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_f64_identity_ $d d>]() {
let a = Matrix::<$d>::identity();
let b = arbitrary_rhs::<$d>();
let x = a.solve_exact_f64(b).unwrap().into_array();
for i in 0..$d {
assert!((x[i] - b.data[i]).abs() <= f64::EPSILON);
}
}
#[test]
fn [<solve_exact_f64_err_on_nan_ $d d>]() {
let mut a = Matrix::<$d>::identity();
a.set(0, 0, f64::NAN);
let b = arbitrary_rhs::<$d>();
assert_eq!(a.solve_exact_f64(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
}
}
};
}
gen_solve_exact_f64_tests!(2);
gen_solve_exact_f64_tests!(3);
gen_solve_exact_f64_tests!(4);
gen_solve_exact_f64_tests!(5);
macro_rules! gen_solve_exact_f64_agrees_with_lu {
($d:literal) => {
paste! {
#[test]
#[allow(clippy::cast_precision_loss)]
fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
for r in 0..$d {
for c in 0..$d {
rows[r][c] = if r == c {
(r as f64) + f64::from($d) + 1.0
} else {
0.1 / ((r + c + 1) as f64)
};
}
}
let a = Matrix::<$d>::from_rows(rows);
let b = arbitrary_rhs::<$d>();
let exact = a.solve_exact_f64(b).unwrap().into_array();
let lu_sol = a.lu(DEFAULT_PIVOT_TOL).unwrap()
.solve_vec(b).unwrap().into_array();
for i in 0..$d {
let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
assert!((exact[i] - lu_sol[i]).abs() <= eps);
}
}
}
};
}
gen_solve_exact_f64_agrees_with_lu!(2);
gen_solve_exact_f64_agrees_with_lu!(3);
gen_solve_exact_f64_agrees_with_lu!(4);
gen_solve_exact_f64_agrees_with_lu!(5);
#[test]
fn solve_exact_d0_returns_empty() {
let a = Matrix::<0>::zero();
let b = Vector::<0>::zero();
let x = a.solve_exact(b).unwrap();
assert!(x.is_empty());
}
#[test]
fn solve_exact_known_2x2() {
let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let b = Vector::<2>::new([5.0, 11.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
}
#[test]
fn solve_exact_pivoting_needed() {
let a = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let b = Vector::<3>::new([2.0, 3.0, 4.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], f64_to_bigrational(3.0));
assert_eq!(x[1], f64_to_bigrational(2.0));
assert_eq!(x[2], f64_to_bigrational(4.0));
}
#[test]
fn solve_exact_fractional_result() {
let a = Matrix::<2>::from_rows([[2.0, 1.0], [1.0, 3.0]]);
let b = Vector::<2>::new([1.0, 1.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
}
#[test]
fn solve_exact_singular_duplicate_rows() {
let a = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]]);
let b = Vector::<3>::new([1.0, 2.0, 3.0]);
assert!(matches!(a.solve_exact(b), Err(LaError::Singular { .. })));
}
#[test]
fn solve_exact_5x5_permutation() {
let a = Matrix::<5>::from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
]);
let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], f64_to_bigrational(20.0));
assert_eq!(x[1], f64_to_bigrational(10.0));
assert_eq!(x[2], f64_to_bigrational(30.0));
assert_eq!(x[3], f64_to_bigrational(40.0));
assert_eq!(x[4], f64_to_bigrational(50.0));
}
#[test]
fn solve_exact_f64_known_2x2() {
let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
let b = Vector::<2>::new([5.0, 11.0]);
let x = a.solve_exact_f64(b).unwrap().into_array();
assert!((x[0] - 1.0).abs() <= f64::EPSILON);
assert!((x[1] - 2.0).abs() <= f64::EPSILON);
}
#[test]
fn solve_exact_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let a = Matrix::<2>::from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]);
let b = Vector::<2>::new([big, big]);
assert_eq!(
a.solve_exact_f64(b),
Err(LaError::Overflow { index: Some(0) })
);
}
#[test]
fn gauss_solve_d0_returns_empty() {
let a = Matrix::<0>::zero();
let b = Vector::<0>::zero();
assert_eq!(gauss_solve(&a, &b).unwrap().len(), 0);
}
#[test]
fn gauss_solve_d1() {
let a = Matrix::<1>::from_rows([[2.0]]);
let b = Vector::<1>::new([6.0]);
let x = gauss_solve(&a, &b).unwrap();
assert_eq!(x[0], f64_to_bigrational(3.0));
}
#[test]
fn gauss_solve_singular_column_all_zero() {
let a = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let b = Vector::<3>::new([1.0, 2.0, 3.0]);
assert_eq!(gauss_solve(&a, &b), Err(LaError::Singular { pivot_col: 1 }));
}
#[test]
fn f64_to_bigrational_positive_zero() {
let r = f64_to_bigrational(0.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn f64_to_bigrational_negative_zero() {
let r = f64_to_bigrational(-0.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn f64_to_bigrational_one() {
let r = f64_to_bigrational(1.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn f64_to_bigrational_negative_one() {
let r = f64_to_bigrational(-1.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn f64_to_bigrational_half() {
let r = f64_to_bigrational(0.5);
assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
}
#[test]
fn f64_to_bigrational_quarter() {
let r = f64_to_bigrational(0.25);
assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
}
#[test]
fn f64_to_bigrational_negative_three_and_a_half() {
let r = f64_to_bigrational(-3.5);
assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
}
#[test]
fn f64_to_bigrational_integer() {
let r = f64_to_bigrational(42.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
}
#[test]
fn f64_to_bigrational_power_of_two() {
let r = f64_to_bigrational(1024.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
}
#[test]
fn f64_to_bigrational_subnormal() {
let tiny = 5e-324_f64; assert!(tiny.is_subnormal());
let r = f64_to_bigrational(tiny);
assert_eq!(
r,
BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
);
}
#[test]
fn f64_to_bigrational_already_lowest_terms() {
let r = f64_to_bigrational(0.5);
assert_eq!(*r.numer(), BigInt::from(1));
assert_eq!(*r.denom(), BigInt::from(2));
}
#[test]
fn f64_to_bigrational_round_trip() {
let values = [
0.0,
1.0,
-1.0,
0.5,
0.25,
0.1,
42.0,
-3.5,
1e10,
1e-10,
f64::MAX / 2.0,
f64::MIN_POSITIVE,
5e-324,
];
for &v in &values {
let r = f64_to_bigrational(v);
let back = r.to_f64().expect("round-trip to_f64 failed");
assert!(
v.to_bits() == back.to_bits(),
"round-trip failed for {v}: got {back}"
);
}
}
#[test]
#[should_panic(expected = "non-finite f64 in exact conversion")]
fn f64_to_bigrational_panics_on_nan() {
f64_to_bigrational(f64::NAN);
}
#[test]
#[should_panic(expected = "non-finite f64 in exact conversion")]
fn f64_to_bigrational_panics_on_inf() {
f64_to_bigrational(f64::INFINITY);
}
#[test]
#[should_panic(expected = "non-finite f64 in exact conversion")]
fn f64_to_bigrational_panics_on_neg_inf() {
f64_to_bigrational(f64::NEG_INFINITY);
}
#[test]
fn validate_finite_vec_ok() {
assert!(validate_finite_vec(&Vector::<3>::new([1.0, 2.0, 3.0])).is_ok());
}
#[test]
fn validate_finite_vec_err_on_nan() {
assert_eq!(
validate_finite_vec(&Vector::<2>::new([f64::NAN, 1.0])),
Err(LaError::NonFinite { row: None, col: 0 })
);
}
#[test]
fn validate_finite_vec_err_on_inf() {
assert_eq!(
validate_finite_vec(&Vector::<2>::new([1.0, f64::NEG_INFINITY])),
Err(LaError::NonFinite { row: None, col: 1 })
);
}
#[test]
fn validate_finite_ok_for_finite() {
assert!(validate_finite(&Matrix::<3>::identity()).is_ok());
}
#[test]
fn validate_finite_err_on_nan() {
let mut m = Matrix::<2>::identity();
m.set(1, 0, f64::NAN);
assert_eq!(
validate_finite(&m),
Err(LaError::NonFinite {
row: Some(1),
col: 0
})
);
}
#[test]
fn validate_finite_err_on_inf() {
let mut m = Matrix::<2>::identity();
m.set(0, 1, f64::NEG_INFINITY);
assert_eq!(
validate_finite(&m),
Err(LaError::NonFinite {
row: Some(0),
col: 1
})
);
}
}