#![forbid(unsafe_code)]
use la_stack::{LaError, Matrix as LaMatrix};
use thiserror::Error;
pub const MAX_STACK_MATRIX_DIM: usize = 7;
pub type Matrix<const D: usize> = LaMatrix<D>;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum MatrixError {
#[error("Matrix is singular!")]
SingularMatrix,
}
#[derive(Debug, Error)]
pub(crate) enum StackMatrixDispatchError {
#[error("unsupported stack matrix size: {k} (max {max})")]
UnsupportedDim {
k: usize,
max: usize,
},
#[error(transparent)]
La(#[from] LaError),
}
pub const SINGULARITY_TOLERANCE: f64 = 1e-12;
macro_rules! with_la_stack_matrix {
($k:expr, |$m:ident| $body:block) => {{
with_la_stack_matrix!(@dispatch $k, $m, $body,
0, 1, 2, 3, 4, 5, 6, 7)
}};
(@dispatch $k:expr, $m:ident, $body:block, $($n:literal),+) => {{
match $k {
$(
$n => {
#[allow(unused_mut)]
let mut $m = $crate::geometry::matrix::Matrix::<$n>::zero();
$body
}
)+
_ => panic!(
"unsupported stack matrix size: {k} (max {max})",
k = $k,
max = $crate::geometry::matrix::MAX_STACK_MATRIX_DIM
),
}
}};
}
macro_rules! try_with_la_stack_matrix {
($k:expr, |$m:ident| $body:block) => {{
let k = $k;
if k > $crate::geometry::matrix::MAX_STACK_MATRIX_DIM {
Err(
$crate::geometry::matrix::StackMatrixDispatchError::UnsupportedDim {
k,
max: $crate::geometry::matrix::MAX_STACK_MATRIX_DIM,
}
.into(),
)
} else {
with_la_stack_matrix!(k, |$m| $body)
}
}};
}
#[inline]
pub(crate) fn matrix_zero_like<const D: usize>(_template: &Matrix<D>) -> Matrix<D> {
Matrix::<D>::zero()
}
#[inline]
pub(crate) fn matrix_get<const D: usize>(m: &Matrix<D>, r: usize, c: usize) -> f64 {
m.get(r, c)
.unwrap_or_else(|| unreachable!("matrix index out of bounds: ({r}, {c}) for {D}x{D}"))
}
#[inline]
pub(crate) fn matrix_set<const D: usize>(m: &mut Matrix<D>, r: usize, c: usize, value: f64) {
let ok = m.set(r, c, value);
assert!(ok, "matrix index out of bounds: ({r}, {c}) for {D}x{D}");
}
#[inline]
#[must_use]
pub fn determinant<const D: usize>(m: &Matrix<D>) -> f64 {
match m.det(0.0) {
Ok(det) => det,
Err(LaError::Singular { .. }) => 0.0,
Err(LaError::NonFinite { .. }) => f64::NAN,
}
}
#[must_use]
pub fn adaptive_tolerance<const D: usize>(matrix: &Matrix<D>, base_tol: f64) -> f64 {
let nrows = D;
let ncols = D;
let last_col_is_all_ones = ncols > 0
&& (0..nrows).all(|i| (matrix_get(matrix, i, ncols - 1) - 1.0).abs() <= f64::EPSILON);
let mut max_row_sum = 0.0f64;
for i in 0..nrows {
let mut row_sum = 0.0f64;
let col_limit = if last_col_is_all_ones {
ncols - 1
} else {
ncols
};
for j in 0..col_limit {
row_sum += matrix_get(matrix, i, j).abs();
}
if row_sum > max_row_sum {
max_row_sum = row_sum;
}
}
let rel_factor = 1e-12f64;
rel_factor.mul_add(max_row_sum, base_tol)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn try_with_la_stack_matrix_returns_err_on_unsupported_dim() {
let k = MAX_STACK_MATRIX_DIM + 1;
let res: Result<(), StackMatrixDispatchError> =
try_with_la_stack_matrix!(k, |_m| { Ok(()) });
assert!(matches!(
res,
Err(StackMatrixDispatchError::UnsupportedDim { .. })
));
}
macro_rules! gen_adaptive_tol_tests {
($d:literal) => {
pastey::paste! {
#[test]
fn [<adaptive_tolerance_ignores_constant_one_last_col_ $d d>]() {
let n = $d + 1; let base = 1e-12;
let tol = with_la_stack_matrix!(n, |m| {
for i in 0..n {
matrix_set(&mut m, i, n - 1, 1.0);
}
adaptive_tolerance(&m, base)
});
assert_relative_eq!(tol, base, epsilon = 1e-18);
}
#[test]
fn [<adaptive_tolerance_includes_non_one_last_col_ $d d>]() {
let n = $d + 1;
let base = 1e-12;
let tol = with_la_stack_matrix!(n, |m| {
for i in 0..n {
matrix_set(&mut m, i, n - 1, 2.0);
}
adaptive_tolerance(&m, base)
});
let expected = base + 2.0e-12;
assert_relative_eq!(tol, expected, epsilon = 1e-24);
}
}
};
}
gen_adaptive_tol_tests!(2);
gen_adaptive_tol_tests!(3);
gen_adaptive_tol_tests!(4);
gen_adaptive_tol_tests!(5);
#[test]
fn matrix_zero_like_returns_zero_matrix_of_same_size() {
let k = 4;
with_la_stack_matrix!(k, |original| {
let mut val = 1.0_f64;
for i in 0..k {
for j in 0..k {
matrix_set(&mut original, i, j, val);
val += 1.0;
}
}
let zero = matrix_zero_like(&original);
for i in 0..k {
for j in 0..k {
assert_relative_eq!(matrix_get(&zero, i, j), 0.0);
}
}
let mut expected = 1.0_f64;
for i in 0..k {
for j in 0..k {
assert_relative_eq!(matrix_get(&original, i, j), expected);
expected += 1.0;
}
}
});
}
#[test]
fn matrix_zero_like_works_across_dispatch_sizes() {
for &k in &[2_usize, 3, 6, MAX_STACK_MATRIX_DIM] {
with_la_stack_matrix!(k, |m| {
let zero = matrix_zero_like(&m);
assert_relative_eq!(matrix_get(&zero, 0, 0), 0.0);
assert_relative_eq!(matrix_get(&zero, k - 1, k - 1), 0.0);
});
}
}
}