#[derive(Debug, Clone, PartialEq)]
pub enum MatrixError {
Singular { col: usize },
}
impl std::fmt::Display for MatrixError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatrixError::Singular { col } => {
write!(f, "matrix is singular: zero pivot at column {col}")
}
}
}
}
impl std::error::Error for MatrixError {}
#[derive(Debug, Clone)]
pub struct SquareMatrix {
n: usize,
data: Vec<f64>,
}
impl SquareMatrix {
pub fn new(n: usize) -> Self {
Self {
n,
data: vec![0.0; n * n],
}
}
pub fn n(&self) -> usize {
self.n
}
#[inline]
pub fn get(&self, i: usize, j: usize) -> f64 {
match self.data.get(self.offset(i, j)) {
Some(&v) => v,
None => f64::NAN,
}
}
#[inline]
fn offset(&self, i: usize, j: usize) -> usize {
i.saturating_mul(self.n).saturating_add(j)
}
#[inline]
pub fn set(&mut self, i: usize, j: usize, v: f64) {
let idx = self.offset(i, j);
if let Some(slot) = self.data.get_mut(idx) {
*slot = v;
}
}
#[inline]
pub fn add(&mut self, i: usize, j: usize, v: f64) {
let idx = self.offset(i, j);
if let Some(slot) = self.data.get_mut(idx) {
*slot += v;
}
}
#[inline]
fn swap_entries(&mut self, i1: usize, j1: usize, i2: usize, j2: usize) {
let a = self.offset(i1, j1);
let b = self.offset(i2, j2);
if a < self.data.len() && b < self.data.len() {
self.data.swap(a, b);
}
}
pub fn fill_zero(&mut self) {
self.data.iter_mut().for_each(|x| *x = 0.0);
}
pub fn lu_decompose(&mut self) -> Vec<usize> {
let n = self.n;
let mut pivot: Vec<usize> = Vec::with_capacity(n);
let mut vv: Vec<f64> = self
.data
.chunks_exact(n.max(1))
.take(n)
.map(|row| {
let mx = row.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
if mx > 0.0 {
1.0 / mx
} else {
0.0
}
})
.collect();
for j in 0..n {
for i in 0..j {
let mut sum = self.get(i, j);
for k in 0..i {
sum -= self.get(i, k) * self.get(k, j);
}
self.set(i, j, sum);
}
let mut i_max = j;
let mut largest = 0.0_f64;
for i in j..n {
let mut sum = self.get(i, j);
for k in 0..j {
sum -= self.get(i, k) * self.get(k, j);
}
self.set(i, j, sum);
let tmp = vv.get(i).copied().unwrap_or(0.0) * sum.abs();
if tmp >= largest {
largest = tmp;
i_max = i;
}
}
pivot.push(i_max);
if j != i_max {
for k in 0..n {
self.swap_entries(j, k, i_max, k);
}
if let Some(scale_j) = vv.get(j).copied() {
if let Some(slot) = vv.get_mut(i_max) {
*slot = scale_j;
}
}
}
if self.get(j, j) == 0.0 {
self.set(j, j, f64::EPSILON);
}
if j + 1 < n {
let r = 1.0 / self.get(j, j);
for i in (j + 1)..n {
let scaled = self.get(i, j) * r;
self.set(i, j, scaled);
}
}
}
pivot
}
pub fn lu_back_substitute(&self, pivot: &[usize], b: &mut Vec<f64>) {
let n = self.n;
let mut first_nz: Option<usize> = None;
for i in 0..n {
let (Some(&ip), Some(b_i)) = (pivot.get(i), b.get(i).copied()) else {
return;
};
let Some(mut sum) = b.get(ip).copied() else {
return;
};
if let Some(slot) = b.get_mut(ip) {
*slot = b_i;
}
if let Some(start) = first_nz {
for j in start..i {
sum -= self.get(i, j) * b.get(j).copied().unwrap_or(0.0);
}
} else if sum != 0.0 {
first_nz = Some(i);
}
if let Some(slot) = b.get_mut(i) {
*slot = sum;
}
}
for i in (0..n).rev() {
let Some(mut sum) = b.get(i).copied() else {
return;
};
for j in (i + 1)..n {
sum -= self.get(i, j) * b.get(j).copied().unwrap_or(0.0);
}
let diagonal = self.get(i, i);
if let Some(slot) = b.get_mut(i) {
*slot = sum / diagonal;
}
}
}
pub fn solve(&self, rhs: &[f64]) -> Result<Vec<f64>, MatrixError> {
let mut a = self.clone();
let pivot = a.lu_decompose();
for i in 0..self.n {
if a.get(i, i) == f64::EPSILON {
return Err(MatrixError::Singular { col: i });
}
}
let mut b = rhs.to_vec();
a.lu_back_substitute(&pivot, &mut b);
Ok(b)
}
}
#[cfg(test)]
mod tests {
use petir::mathf::RealMath;
use super::*;
#[test]
fn lu_solve_2x2() {
let mut m = SquareMatrix::new(2);
m.set(0, 0, 2.0);
m.set(0, 1, 1.0);
m.set(1, 0, 1.0);
m.set(1, 1, 3.0);
let x = m.solve(&[5.0, 10.0]).unwrap();
assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
}
#[test]
fn lu_solve_3x3() {
let mut m = SquareMatrix::new(3);
m.set(0, 0, 2.0);
m.set(0, 1, 1.0);
m.set(0, 2, 1.0);
m.set(1, 0, 1.0);
m.set(1, 1, 3.0);
m.set(1, 2, 1.0);
m.set(2, 0, 1.0);
m.set(2, 1, 1.0);
m.set(2, 2, 4.0);
let x = m.solve(&[7.0, 10.0, 15.0]).unwrap();
assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
assert!((x[1] - 2.0).abs() < 1e-12, "y={}", x[1]);
assert!((x[2] - 3.0).abs() < 1e-12, "z={}", x[2]);
}
#[test]
fn lu_solve_needs_pivoting() {
let mut m = SquareMatrix::new(2);
m.set(0, 0, 0.0);
m.set(0, 1, 1.0);
m.set(1, 0, 2.0);
m.set(1, 1, 0.0);
let x = m.solve(&[3.0, 4.0]).unwrap();
assert!((x[0] - 2.0).abs() < 1e-12, "x={}", x[0]);
assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
}
#[test]
fn lu_identity() {
let mut m = SquareMatrix::new(3);
m.set(0, 0, 1.0);
m.set(1, 1, 1.0);
m.set(2, 2, 1.0);
let x = m.solve(&[7.0, -2.0, 5.0]).unwrap();
assert!((x[0] - 7.0).abs() < 1e-12);
assert!((x[1] + 2.0).abs() < 1e-12);
assert!((x[2] - 5.0).abs() < 1e-12);
}
#[test]
fn singular_matrix_returns_err() {
let mut m = SquareMatrix::new(2);
m.set(0, 0, 1.0);
m.set(0, 1, 1.0);
m.set(1, 0, 1.0);
m.set(1, 1, 1.0);
let result = m.solve(&[2.0, 2.0]);
assert!(
matches!(result, Err(MatrixError::Singular { .. })),
"expected Singular, got {:?}",
result
);
}
#[test]
fn fully_zero_matrix_returns_err() {
let m = SquareMatrix::new(3); let result = m.solve(&[1.0, 0.0, 0.0]);
assert!(
matches!(result, Err(MatrixError::Singular { col: 0 })),
"expected Singular at col 0, got {:?}",
result
);
}
#[test]
fn hilbert_5x5_residual_acceptable() {
let n = 5;
let mut m = SquareMatrix::new(n);
for i in 0..n {
for j in 0..n {
m.set(i, j, 1.0 / (i + j + 1) as f64);
}
}
let b: Vec<f64> = (0..n).map(|_| 1.0).collect();
let x = m
.solve(&b)
.expect("Hilbert 5×5 should not be detected as singular");
let mut m2 = SquareMatrix::new(n);
for i in 0..n {
for j in 0..n {
m2.set(i, j, 1.0 / (i + j + 1) as f64);
}
}
let mut r_norm_sq = 0.0_f64;
let mut b_norm_sq = 0.0_f64;
for i in 0..n {
let ax_i: f64 = (0..n).map(|j| m2.get(i, j) * x[j]).sum();
r_norm_sq += (ax_i - b[i]).r_powi(2);
b_norm_sq += b[i].r_powi(2);
}
let rel_residual = r_norm_sq.sqrt() / b_norm_sq.sqrt();
assert!(
rel_residual < 1e-8,
"relative residual {rel_residual:.3e} too large"
);
}
#[test]
fn scaled_pivoting_large_row_contrast() {
let mut m = SquareMatrix::new(2);
m.set(0, 0, 1.0e8);
m.set(0, 1, 1.0);
m.set(1, 0, 1.0);
m.set(1, 1, 1.0e8);
let rhs = [1.0e8 + 1.0, 1.0e8 + 1.0];
let x = m.solve(&rhs).unwrap();
assert!((x[0] - 1.0).abs() < 1e-6, "x[0]={}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-6, "x[1]={}", x[1]);
}
#[test]
fn lu_multiple_rhs() {
let mut m = SquareMatrix::new(2);
m.set(0, 0, 3.0);
m.set(0, 1, 1.0);
m.set(1, 0, 1.0);
m.set(1, 1, 2.0);
let pivot = m.lu_decompose();
let mut b1 = vec![5.0, 5.0];
m.lu_back_substitute(&pivot, &mut b1);
assert!((b1[0] - 1.0).abs() < 1e-12, "b1[0]={}", b1[0]);
assert!((b1[1] - 2.0).abs() < 1e-12, "b1[1]={}", b1[1]);
let mut b2 = vec![13.0, 11.0];
m.lu_back_substitute(&pivot, &mut b2);
assert!((b2[0] - 3.0).abs() < 1e-12, "b2[0]={}", b2[0]);
assert!((b2[1] - 4.0).abs() < 1e-12, "b2[1]={}", b2[1]);
}
}