use num_traits::{FromPrimitive, One};
use oxiblas_core::scalar::{Field, Scalar};
use oxiblas_matrix::{Mat, MatRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LuRookError {
Singular {
index: usize,
},
NotSquare {
nrows: usize,
ncols: usize,
},
DimensionMismatch {
expected: usize,
actual: usize,
},
MaxIterationsReached {
step: usize,
},
}
impl core::fmt::Display for LuRookError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LuRookError::Singular { index } => {
write!(f, "Matrix is singular at index {index}")
}
LuRookError::NotSquare { nrows, ncols } => {
write!(f, "Matrix is not square: {nrows}×{ncols}")
}
LuRookError::DimensionMismatch { expected, actual } => {
write!(f, "Dimension mismatch: expected {expected}, got {actual}")
}
LuRookError::MaxIterationsReached { step } => {
write!(f, "Max iterations reached at step {step}")
}
}
}
}
impl std::error::Error for LuRookError {}
#[derive(Clone, Debug, Default)]
pub struct RookPivotStats {
pub column_searches: usize,
pub row_searches: usize,
pub max_rook_iterations: usize,
pub total_rook_iterations: usize,
}
#[derive(Clone, Debug)]
pub struct LuRook<T: Scalar> {
lu: Mat<T>,
row_pivot: Vec<usize>,
col_pivot: Vec<usize>,
num_row_swaps: usize,
num_col_swaps: usize,
stats: RookPivotStats,
}
impl<T: Field + bytemuck::Zeroable> LuRook<T> {
pub fn compute(a: MatRef<'_, T>) -> Result<Self, LuRookError> {
Self::compute_with_tol(a, None)
}
pub fn compute_with_tol(a: MatRef<'_, T>, tol: Option<T::Real>) -> Result<Self, LuRookError> {
let n = a.nrows();
if n != a.ncols() {
return Err(LuRookError::NotSquare {
nrows: n,
ncols: a.ncols(),
});
}
if n == 0 {
return Ok(LuRook {
lu: Mat::zeros(0, 0),
row_pivot: Vec::new(),
col_pivot: Vec::new(),
num_row_swaps: 0,
num_col_swaps: 0,
stats: RookPivotStats::default(),
});
}
let mut lu = Mat::zeros(n, n);
for j in 0..n {
for i in 0..n {
lu[(i, j)] = a[(i, j)];
}
}
let mut row_pivot = vec![0usize; n];
let mut col_pivot = vec![0usize; n];
let mut num_row_swaps = 0;
let mut num_col_swaps = 0;
let mut stats = RookPivotStats::default();
let default_tol = T::epsilon()
* <T::Real as FromPrimitive>::from_usize(n).unwrap_or(<T::Real as One>::one());
let tolerance = tol.unwrap_or(default_tol);
for k in 0..n {
let (pivot_row, pivot_col, pivot_val, iterations) =
Self::rook_pivot_search(&lu, n, k, &mut stats)?;
if pivot_val <= tolerance {
return Err(LuRookError::Singular { index: k });
}
stats.total_rook_iterations += iterations;
if iterations > stats.max_rook_iterations {
stats.max_rook_iterations = iterations;
}
row_pivot[k] = pivot_row;
col_pivot[k] = pivot_col;
if pivot_row != k {
for j in 0..n {
let tmp = lu[(k, j)];
lu[(k, j)] = lu[(pivot_row, j)];
lu[(pivot_row, j)] = tmp;
}
num_row_swaps += 1;
}
if pivot_col != k {
for i in 0..n {
let tmp = lu[(i, k)];
lu[(i, k)] = lu[(i, pivot_col)];
lu[(i, pivot_col)] = tmp;
}
num_col_swaps += 1;
}
let pivot_inv = T::one() / lu[(k, k)];
for i in (k + 1)..n {
let mult = lu[(i, k)] * pivot_inv;
lu[(i, k)] = mult;
for j in (k + 1)..n {
let val = lu[(i, j)] - mult * lu[(k, j)];
lu[(i, j)] = val;
}
}
}
Ok(LuRook {
lu,
row_pivot,
col_pivot,
num_row_swaps,
num_col_swaps,
stats,
})
}
fn rook_pivot_search(
lu: &Mat<T>,
n: usize,
k: usize,
stats: &mut RookPivotStats,
) -> Result<(usize, usize, T::Real, usize), LuRookError> {
let max_iterations = 2 * (n - k) + 1;
let mut iterations = 0;
let mut pivot_row = k;
let mut pivot_col = k;
let mut pivot_val = Scalar::abs(lu[(k, k)]);
for i in (k + 1)..n {
let val = Scalar::abs(lu[(i, k)]);
if val > pivot_val {
pivot_val = val;
pivot_row = i;
}
}
stats.column_searches += 1;
loop {
iterations += 1;
if iterations > max_iterations {
return Err(LuRookError::MaxIterationsReached { step: k });
}
let mut new_col = pivot_col;
let mut new_val = pivot_val;
for j in k..n {
let val = Scalar::abs(lu[(pivot_row, j)]);
if val > new_val {
new_val = val;
new_col = j;
}
}
stats.row_searches += 1;
if new_col == pivot_col {
break;
}
pivot_col = new_col;
pivot_val = new_val;
let mut new_row = pivot_row;
new_val = pivot_val;
for i in k..n {
let val = Scalar::abs(lu[(i, pivot_col)]);
if val > new_val {
new_val = val;
new_row = i;
}
}
stats.column_searches += 1;
if new_row == pivot_row {
break;
}
pivot_row = new_row;
pivot_val = new_val;
}
Ok((pivot_row, pivot_col, pivot_val, iterations))
}
#[inline]
pub fn size(&self) -> usize {
self.lu.nrows()
}
pub fn stats(&self) -> &RookPivotStats {
&self.stats
}
pub fn lu_matrix(&self) -> MatRef<'_, T> {
self.lu.as_ref()
}
pub fn row_pivot(&self) -> &[usize] {
&self.row_pivot
}
pub fn col_pivot(&self) -> &[usize] {
&self.col_pivot
}
pub fn determinant(&self) -> T {
let n = self.size();
if n == 0 {
return T::one();
}
let total_swaps = self.num_row_swaps + self.num_col_swaps;
let mut det = if total_swaps % 2 == 0 {
T::one()
} else {
-T::one()
};
for i in 0..n {
det = det * self.lu[(i, i)];
}
det
}
pub fn solve(&self, b: MatRef<'_, T>) -> Result<Mat<T>, LuRookError> {
let n = self.size();
if b.nrows() != n {
return Err(LuRookError::DimensionMismatch {
expected: n,
actual: b.nrows(),
});
}
let m = b.ncols();
let mut x = Mat::zeros(n, m);
let mut work = Mat::zeros(n, m);
for j in 0..m {
for i in 0..n {
work[(i, j)] = b[(i, j)];
}
}
for k in 0..n {
let pk = self.row_pivot[k];
if k != pk {
for j in 0..m {
let tmp = work[(k, j)];
work[(k, j)] = work[(pk, j)];
work[(pk, j)] = tmp;
}
}
}
for k in 0..n {
for i in (k + 1)..n {
let mult = self.lu[(i, k)];
for j in 0..m {
let val = work[(i, j)] - mult * work[(k, j)];
work[(i, j)] = val;
}
}
}
for k in (0..n).rev() {
let diag = self.lu[(k, k)];
for j in 0..m {
work[(k, j)] = work[(k, j)] / diag;
}
for i in 0..k {
let mult = self.lu[(i, k)];
for j in 0..m {
let val = work[(i, j)] - mult * work[(k, j)];
work[(i, j)] = val;
}
}
}
for j in 0..m {
for i in 0..n {
x[(i, j)] = work[(i, j)];
}
}
for k in (0..n).rev() {
let pk = self.col_pivot[k];
if k != pk {
for j in 0..m {
let tmp = x[(k, j)];
x[(k, j)] = x[(pk, j)];
x[(pk, j)] = tmp;
}
}
}
Ok(x)
}
pub fn inverse(&self) -> Result<Mat<T>, LuRookError> {
let n = self.size();
let identity = Mat::<T>::eye(n);
self.solve(identity.as_ref())
}
pub fn l_factor(&self) -> Mat<T> {
let n = self.size();
let mut l = Mat::zeros(n, n);
for i in 0..n {
l[(i, i)] = T::one();
for j in 0..i {
l[(i, j)] = self.lu[(i, j)];
}
}
l
}
pub fn u_factor(&self) -> Mat<T> {
let n = self.size();
let mut u = Mat::zeros(n, n);
for i in 0..n {
for j in i..n {
u[(i, j)] = self.lu[(i, j)];
}
}
u
}
pub fn row_permutation_matrix(&self) -> Mat<T> {
let n = self.size();
let mut p = Mat::eye(n);
for k in 0..n {
let pk = self.row_pivot[k];
if k != pk {
for j in 0..n {
let tmp = p[(k, j)];
p[(k, j)] = p[(pk, j)];
p[(pk, j)] = tmp;
}
}
}
p
}
pub fn col_permutation_matrix(&self) -> Mat<T> {
let n = self.size();
let mut q = Mat::eye(n);
for k in 0..n {
let pk = self.col_pivot[k];
if k != pk {
for i in 0..n {
let tmp = q[(i, k)];
q[(i, k)] = q[(i, pk)];
q[(i, pk)] = tmp;
}
}
}
q
}
pub fn solve_transpose(&self, b: MatRef<'_, T>) -> Result<Mat<T>, LuRookError> {
let n = self.size();
if b.nrows() != n {
return Err(LuRookError::DimensionMismatch {
expected: n,
actual: b.nrows(),
});
}
let m = b.ncols();
let mut work = Mat::zeros(n, m);
for j in 0..m {
for i in 0..n {
work[(i, j)] = b[(i, j)];
}
}
for k in 0..n {
let pk = self.col_pivot[k];
if k != pk {
for j in 0..m {
let tmp = work[(k, j)];
work[(k, j)] = work[(pk, j)];
work[(pk, j)] = tmp;
}
}
}
for k in 0..n {
let diag = self.lu[(k, k)];
for j in 0..m {
work[(k, j)] = work[(k, j)] / diag;
}
for i in (k + 1)..n {
let mult = self.lu[(k, i)];
for j in 0..m {
work[(i, j)] = work[(i, j)] - mult * work[(k, j)];
}
}
}
for k in (0..n).rev() {
for i in 0..k {
let mult = self.lu[(k, i)];
for j in 0..m {
work[(i, j)] = work[(i, j)] - mult * work[(k, j)];
}
}
}
for k in (0..n).rev() {
let pk = self.row_pivot[k];
if k != pk {
for j in 0..m {
let tmp = work[(k, j)];
work[(k, j)] = work[(pk, j)];
work[(pk, j)] = tmp;
}
}
}
Ok(work)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lu_rook_simple() {
let a: Mat<f64> = Mat::from_rows(&[&[4.0, 3.0], &[6.0, 3.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let det = lu.determinant();
assert!((det.abs() - 6.0).abs() < 1e-10, "det = {}", det);
}
#[test]
fn test_lu_rook_solve() {
let a: Mat<f64> = Mat::from_rows(&[&[2.0, 1.0], &[4.0, 3.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[3.0], &[7.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve(b.as_ref()).expect("Should solve");
assert!((x[(0, 0)] - 1.0).abs() < 1e-10, "x[0] = {}", x[(0, 0)]);
assert!((x[(1, 0)] - 1.0).abs() < 1e-10, "x[1] = {}", x[(1, 0)]);
}
#[test]
fn test_lu_rook_singular() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0], &[2.0, 4.0]]);
let result = LuRook::compute(a.as_ref());
assert!(result.is_err());
}
#[test]
fn test_lu_rook_3x3() {
let a: Mat<f64> = Mat::from_rows(&[&[2.0, 1.0, 1.0], &[4.0, 3.0, 3.0], &[8.0, 7.0, 9.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let b: Mat<f64> = Mat::from_rows(&[&[4.0], &[10.0], &[24.0]]);
let x = lu.solve(b.as_ref()).expect("Should solve");
assert!((x[(0, 0)] - 1.0).abs() < 1e-10, "x[0] = {}", x[(0, 0)]);
assert!((x[(1, 0)] - 1.0).abs() < 1e-10, "x[1] = {}", x[(1, 0)]);
assert!((x[(2, 0)] - 1.0).abs() < 1e-10, "x[2] = {}", x[(2, 0)]);
}
#[test]
fn test_lu_rook_determinant() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 10.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let det = lu.determinant();
assert!((det + 3.0).abs() < 1e-10, "det = {}", det);
}
#[test]
fn test_lu_rook_inverse() {
let a: Mat<f64> = Mat::from_rows(&[&[4.0, 7.0], &[2.0, 6.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let a_inv = lu.inverse().expect("Should invert");
assert!((a_inv[(0, 0)] - 0.6).abs() < 1e-10);
assert!((a_inv[(0, 1)] + 0.7).abs() < 1e-10);
assert!((a_inv[(1, 0)] + 0.2).abs() < 1e-10);
assert!((a_inv[(1, 1)] - 0.4).abs() < 1e-10);
}
#[test]
fn test_lu_rook_paq_lu() {
let a: Mat<f64> = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 10.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let l = lu.l_factor();
let u = lu.u_factor();
let p = lu.row_permutation_matrix();
let q = lu.col_permutation_matrix();
let n = a.nrows();
let mut lu_prod = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += l[(i, k)] * u[(k, j)];
}
lu_prod[(i, j)] = sum;
}
}
let mut pa = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += p[(i, k)] * a[(k, j)];
}
pa[(i, j)] = sum;
}
}
let mut paq = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += pa[(i, k)] * q[(k, j)];
}
paq[(i, j)] = sum;
}
}
for i in 0..n {
for j in 0..n {
assert!(
(paq[(i, j)] - lu_prod[(i, j)]).abs() < 1e-10,
"PAQ[{},{}] = {}, LU[{},{}] = {}",
i,
j,
paq[(i, j)],
i,
j,
lu_prod[(i, j)]
);
}
}
}
#[test]
fn test_lu_rook_f32() {
let a: Mat<f32> = Mat::from_rows(&[&[2.0f32, 1.0], &[4.0, 3.0]]);
let b: Mat<f32> = Mat::from_rows(&[&[3.0f32], &[7.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve(b.as_ref()).expect("Should solve");
assert!((x[(0, 0)] - 1.0).abs() < 1e-5, "x[0] = {}", x[(0, 0)]);
assert!((x[(1, 0)] - 1.0).abs() < 1e-5, "x[1] = {}", x[(1, 0)]);
}
#[test]
fn test_lu_rook_empty() {
let a: Mat<f64> = Mat::zeros(0, 0);
let lu = LuRook::compute(a.as_ref()).expect("Empty should succeed");
assert_eq!(lu.size(), 0);
}
#[test]
fn test_lu_rook_not_square() {
let a = Mat::from_rows(&[&[1.0f64, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let result = LuRook::compute(a.as_ref());
assert!(matches!(result, Err(LuRookError::NotSquare { .. })));
}
#[test]
fn test_lu_rook_identity() {
let eye: Mat<f64> = Mat::eye(3);
let lu = LuRook::compute(eye.as_ref()).expect("Identity should not be singular");
let det = lu.determinant();
assert!((det - 1.0).abs() < 1e-10);
let inv = lu.inverse().expect("Should invert");
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((inv[(i, j)] - expected).abs() < 1e-10);
}
}
}
#[test]
fn test_lu_rook_ill_conditioned() {
let a: Mat<f64> =
Mat::from_rows(&[&[1e-10, 1.0, 2.0], &[1.0, 1e-10, 3.0], &[2.0, 3.0, 1e-10]]);
let lu = LuRook::compute(a.as_ref()).expect("Should handle ill-conditioned");
let b: Mat<f64> = Mat::from_rows(&[&[1.0], &[1.0], &[1.0]]);
let x = lu.solve(b.as_ref()).expect("Should solve");
for i in 0..3 {
let mut sum = 0.0;
for j in 0..3 {
sum += a[(i, j)] * x[(j, 0)];
}
assert!((sum - b[(i, 0)]).abs() < 1e-5, "Ax[{}] = {}", i, sum);
}
}
#[test]
fn test_lu_rook_stats() {
let a: Mat<f64> = Mat::from_rows(&[
&[10.0, 2.0, 3.0, 4.0],
&[5.0, 10.0, 7.0, 8.0],
&[9.0, 10.0, 15.0, 12.0],
&[13.0, 14.0, 15.0, 20.0],
]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let stats = lu.stats();
assert!(stats.column_searches >= 4, "Should have column searches");
assert!(
stats.row_searches >= 1,
"Should have at least one row search"
);
}
#[test]
fn test_lu_rook_multiple_rhs() {
let a: Mat<f64> = Mat::from_rows(&[&[2.0, 1.0], &[4.0, 3.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[3.0, 4.0], &[7.0, 8.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve(b.as_ref()).expect("Should solve");
assert!((x[(0, 0)] - 1.0).abs() < 1e-10, "x[0,0] = {}", x[(0, 0)]);
assert!((x[(1, 0)] - 1.0).abs() < 1e-10, "x[1,0] = {}", x[(1, 0)]);
assert!((x[(0, 1)] - 2.0).abs() < 1e-10, "x[0,1] = {}", x[(0, 1)]);
assert!((x[(1, 1)] - 0.0).abs() < 1e-10, "x[1,1] = {}", x[(1, 1)]);
}
#[test]
fn test_lu_rook_transpose_solve() {
let a: Mat<f64> = Mat::from_rows(&[&[2.0, 1.0], &[4.0, 3.0]]);
let b: Mat<f64> = Mat::from_rows(&[&[6.0], &[4.0]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve_transpose(b.as_ref()).expect("Should solve");
let ax0 = 2.0 * x[(0, 0)] + 4.0 * x[(1, 0)];
let ax1 = 1.0 * x[(0, 0)] + 3.0 * x[(1, 0)];
assert!((ax0 - 6.0).abs() < 1e-10, "A^T x[0] = {}, expected 6", ax0);
assert!((ax1 - 4.0).abs() < 1e-10, "A^T x[1] = {}, expected 4", ax1);
}
#[test]
fn test_lu_rook_complex64() {
use num_complex::Complex64;
let a: Mat<Complex64> = Mat::from_rows(&[
&[Complex64::new(2.0, 1.0), Complex64::new(1.0, 0.0)],
&[Complex64::new(1.0, 0.0), Complex64::new(3.0, -1.0)],
]);
let b: Mat<Complex64> =
Mat::from_rows(&[&[Complex64::new(3.0, 1.0)], &[Complex64::new(4.0, 0.0)]]);
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve(b.as_ref()).expect("Should solve");
let ax0 = a[(0, 0)] * x[(0, 0)] + a[(0, 1)] * x[(1, 0)];
let ax1 = a[(1, 0)] * x[(0, 0)] + a[(1, 1)] * x[(1, 0)];
assert!(
(ax0 - b[(0, 0)]).norm() < 1e-10,
"ax0 = {:?}, b0 = {:?}",
ax0,
b[(0, 0)]
);
assert!(
(ax1 - b[(1, 0)]).norm() < 1e-10,
"ax1 = {:?}, b1 = {:?}",
ax1,
b[(1, 0)]
);
}
#[test]
fn test_lu_rook_large_matrix() {
let n = 50;
let mut a: Mat<f64> = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
if i == j {
a[(i, j)] = (n as f64) + 1.0;
} else {
a[(i, j)] = ((i + 1) * (j + 1)) as f64 * 0.01;
}
}
}
let mut b: Mat<f64> = Mat::zeros(n, 1);
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += a[(i, j)];
}
b[(i, 0)] = sum;
}
let lu = LuRook::compute(a.as_ref()).expect("Should not be singular");
let x = lu.solve(b.as_ref()).expect("Should solve");
for i in 0..n {
assert!(
(x[(i, 0)] - 1.0).abs() < 1e-8,
"x[{}] = {}, expected 1.0",
i,
x[(i, 0)]
);
}
}
}