use crate::error::SolveError;
use crate::linalg::Matrix;
const PIVOT_THRESHOLD: f64 = 1e-12;
#[derive(Debug, Clone, PartialEq)]
pub struct Lu {
pub lu: Matrix,
pub perm: Vec<usize>,
pub sign: f64,
}
pub fn lu_decompose(a: &Matrix) -> Result<Lu, SolveError> {
if !a.is_square() {
return Err(SolveError::InvalidArgument("lu_decompose requires a square matrix"));
}
let n = a.rows;
let mut lu = a.clone();
let mut perm: Vec<usize> = (0..n).collect();
let mut sign = 1.0;
for k in 0..n {
let mut pivot_row = k;
let mut pivot_val = lu.get(k, k).abs();
for r in (k + 1)..n {
let v = lu.get(r, k).abs();
if v > pivot_val {
pivot_val = v;
pivot_row = r;
}
}
if pivot_val < PIVOT_THRESHOLD {
return Err(SolveError::Singular);
}
if pivot_row != k {
for c in 0..n {
let tmp = lu.get(k, c);
lu.set(k, c, lu.get(pivot_row, c));
lu.set(pivot_row, c, tmp);
}
perm.swap(k, pivot_row);
sign = -sign;
}
let pivot = lu.get(k, k);
for r in (k + 1)..n {
let factor = lu.get(r, k) / pivot;
lu.set(r, k, factor);
for c in (k + 1)..n {
let v = lu.get(r, c) - factor * lu.get(k, c);
lu.set(r, c, v);
}
}
}
Ok(Lu { lu, perm, sign })
}
impl Lu {
pub fn solve(&self, b: &[f64]) -> Result<Vec<f64>, SolveError> {
let n = self.lu.rows;
if b.len() != n {
return Err(SolveError::DimensionMismatch { expected: n, got: b.len() });
}
let mut y = vec![0.0; n];
for i in 0..n {
let mut s = b[self.perm[i]];
for j in 0..i {
s -= self.lu.get(i, j) * y[j];
}
y[i] = s;
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut s = y[i];
for j in (i + 1)..n {
s -= self.lu.get(i, j) * x[j];
}
let d = self.lu.get(i, i);
if d.abs() < PIVOT_THRESHOLD {
return Err(SolveError::Singular);
}
x[i] = s / d;
}
Ok(x)
}
pub fn solve_matrix(&self, b: &Matrix) -> Result<Matrix, SolveError> {
let n = self.lu.rows;
if b.rows != n {
return Err(SolveError::DimensionMismatch { expected: n, got: b.rows });
}
let mut out = Matrix::zeros(n, b.cols);
let mut col = vec![0.0; n];
for c in 0..b.cols {
for r in 0..n {
col[r] = b.get(r, c);
}
let x = self.solve(&col)?;
for r in 0..n {
out.set(r, c, x[r]);
}
}
Ok(out)
}
#[must_use]
pub fn determinant(&self) -> f64 {
let n = self.lu.rows;
let mut det = self.sign;
for i in 0..n {
det *= self.lu.get(i, i);
}
det
}
pub fn inverse(&self) -> Result<Matrix, SolveError> {
self.solve_matrix(&Matrix::identity(self.lu.rows))
}
}
pub fn solve(a: &Matrix, b: &[f64]) -> Result<Vec<f64>, SolveError> {
lu_decompose(a)?.solve(b)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_solve_known_system() {
let a = Matrix::from_rows(&[&[2.0, 1.0], &[1.0, 3.0]]).unwrap();
let x = solve(&a, &[5.0, 10.0]).unwrap();
assert!(approx(x[0], 1.0, 1e-12) && approx(x[1], 3.0, 1e-12));
}
#[test]
fn test_singular_detected() {
let a = Matrix::from_rows(&[&[1.0, 2.0], &[2.0, 4.0]]).unwrap();
assert_eq!(lu_decompose(&a).unwrap_err(), SolveError::Singular);
}
#[test]
fn test_non_square_rejected() {
let a = Matrix::zeros(2, 3);
assert!(matches!(lu_decompose(&a), Err(SolveError::InvalidArgument(_))));
}
#[test]
fn test_determinant_with_pivoting() {
let a = Matrix::from_rows(&[&[0.0, 1.0], &[2.0, 3.0]]).unwrap();
let lu = lu_decompose(&a).unwrap();
assert!(approx(lu.determinant(), -2.0, 1e-12));
}
#[test]
fn test_determinant_identity() {
let lu = lu_decompose(&Matrix::identity(4)).unwrap();
assert!(approx(lu.determinant(), 1.0, 1e-12));
}
#[test]
fn test_inverse_roundtrip() {
let a = Matrix::from_rows(&[&[4.0, 7.0], &[2.0, 6.0]]).unwrap();
let inv = lu_decompose(&a).unwrap().inverse().unwrap();
let prod = a.mul(&inv).unwrap();
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(approx(prod.get(i, j), expected, 1e-12));
}
}
}
#[test]
fn test_solve_matrix_multiple_rhs() {
let a = Matrix::from_rows(&[&[3.0, 1.0], &[1.0, 2.0]]).unwrap();
let b = Matrix::from_rows(&[&[9.0, 4.0], &[8.0, 3.0]]).unwrap();
let x = lu_decompose(&a).unwrap().solve_matrix(&b).unwrap();
let back = a.mul(&x).unwrap();
for i in 0..2 {
for j in 0..2 {
assert!(approx(back.get(i, j), b.get(i, j), 1e-12));
}
}
}
#[test]
fn test_solve_wrong_rhs_length() {
let a = Matrix::identity(3);
let lu = lu_decompose(&a).unwrap();
assert!(matches!(lu.solve(&[1.0]), Err(SolveError::DimensionMismatch { .. })));
}
}