#![forbid(unsafe_code)]
pub use la_stack::LaError;
use la_stack::{BigRational, Matrix as LaMatrix};
pub(crate) use la_stack::{DEFAULT_SINGULAR_TOL, SingularityReason, Vector as LaVector};
use thiserror::Error;
pub const MAX_STACK_MATRIX_DIM: usize = la_stack::MAX_STACK_MATRIX_DISPATCH_DIM;
pub type Matrix<const D: usize> = LaMatrix<D>;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum MatrixError {
#[error("Matrix is singular!")]
SingularMatrix,
#[error("matrix index out of bounds: ({row}, {column}) for {dimension}x{dimension}")]
OutOfBounds {
row: usize,
column: usize,
dimension: usize,
},
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub(crate) enum StackMatrixDispatchError {
#[error("unsupported stack matrix size: {k} (max {max})")]
UnsupportedDim {
k: usize,
max: usize,
},
#[error("active matrix block size {k} does not match concrete matrix dimension {dim}")]
ActiveBlockDimensionMismatch {
k: usize,
dim: usize,
},
#[error(transparent)]
La {
source: LaError,
},
#[error(transparent)]
Matrix {
#[from]
source: MatrixError,
},
}
impl From<LaError> for StackMatrixDispatchError {
fn from(source: LaError) -> Self {
match source {
LaError::UnsupportedDimension { requested, max, .. } => {
Self::UnsupportedDim { k: requested, max }
}
LaError::IndexOutOfBounds { row, col, dim, .. } => Self::Matrix {
source: MatrixError::OutOfBounds {
row,
column: col,
dimension: dim,
},
},
source => Self::La { source },
}
}
}
#[cfg(test)]
macro_rules! with_la_stack_matrix {
($k:expr, |$m:ident| $body:block) => {{
la_stack::try_with_stack_matrix!($k, |mut $m| -> Result<_, la_stack::LaError> { Ok($body) })
.expect("test requested an unsupported stack matrix size")
}};
}
macro_rules! try_with_la_stack_matrix {
($k:expr, |$m:ident| $body:block) => {{
la_stack::try_with_stack_matrix!($k, |mut $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>,
row: usize,
column: usize,
) -> Result<f64, StackMatrixDispatchError> {
m.try_get(row, column).map_err(Into::into)
}
#[inline]
pub(crate) fn matrix_set<const D: usize>(
m: &mut Matrix<D>,
row: usize,
column: usize,
value: f64,
) -> Result<(), StackMatrixDispatchError> {
m.set(row, column, value).map_err(Into::into)
}
pub(crate) fn solve_exact_runtime_system(
matrix: &[Vec<f64>],
rhs: &[f64],
) -> Option<Result<Vec<BigRational>, StackMatrixDispatchError>> {
let dimension = rhs.len();
if matrix.len() != dimension || matrix.iter().any(|row| row.len() != dimension) {
return None;
}
Some(try_with_la_stack_matrix!(dimension, |stack_matrix| {
for (row, values) in matrix.iter().enumerate() {
for (column, value) in values.iter().copied().enumerate() {
matrix_set(&mut stack_matrix, row, column, value)?;
}
}
let rhs_vector = LaVector::try_new(std::array::from_fn(|index| rhs[index]))?;
stack_matrix
.solve_exact(rhs_vector)
.map(|solution| solution.into_iter().collect())
.map_err(Into::into)
}))
}
#[inline]
pub(crate) fn matrix_fast_filter<const D: usize>(
m: &Matrix<D>,
) -> Result<Option<(f64, f64)>, StackMatrixDispatchError> {
match m.det_direct_with_errbound() {
Ok(Some(estimate)) => Ok(Some((
estimate.determinant(),
estimate.absolute_error_bound(),
))),
Ok(None) | Err(LaError::NonFinite { .. }) => Ok(None),
Err(source) => Err(source.into()),
}
}
#[inline]
pub fn determinant<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
match m.det() {
Ok(det) => Ok(det),
Err(LaError::Singular { .. }) => Ok(0.0),
Err(source) => Err(source),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
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 {
k: requested,
max
}) if requested == k && max == MAX_STACK_MATRIX_DIM
);
}
#[test]
fn solve_exact_runtime_system_rejects_malformed_shapes() {
assert_eq!(
solve_exact_runtime_system(&[vec![1.0, 0.0]], &[1.0, 0.0]),
None
);
assert_eq!(
solve_exact_runtime_system(&[vec![1.0], vec![0.0, 1.0]], &[1.0, 0.0]),
None
);
}
#[test]
fn la_index_error_maps_to_matrix_error_with_context() {
let err = StackMatrixDispatchError::from(LaError::index_out_of_bounds(3, 4, 2));
assert_eq!(
err,
StackMatrixDispatchError::Matrix {
source: MatrixError::OutOfBounds {
row: 3,
column: 4,
dimension: 2,
},
}
);
}
#[test]
fn stack_matrix_dispatch_error_clones_la_error_source() {
let source = LaError::singular_exact(3);
let error = StackMatrixDispatchError::La { source };
assert_eq!(error.clone(), error);
assert_eq!(
error.to_string(),
StackMatrixDispatchError::La { source }.to_string()
);
}
#[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).unwrap();
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).unwrap(), 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).unwrap(), 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).unwrap(), 0.0);
assert_relative_eq!(matrix_get(&zero, k - 1, k - 1).unwrap(), 0.0);
});
}
}
#[test]
fn matrix_get_returns_error_on_out_of_bounds_index() {
let matrix = Matrix::<2>::zero();
let err = matrix_get(&matrix, 2, 0).unwrap_err();
assert_eq!(
err,
StackMatrixDispatchError::Matrix {
source: MatrixError::OutOfBounds {
row: 2,
column: 0,
dimension: 2,
},
}
);
}
#[test]
fn matrix_set_returns_error_on_out_of_bounds_index() {
let mut matrix = Matrix::<2>::zero();
let err = matrix_set(&mut matrix, 0, 2, 1.0).unwrap_err();
assert_eq!(
err,
StackMatrixDispatchError::Matrix {
source: MatrixError::OutOfBounds {
row: 0,
column: 2,
dimension: 2,
},
}
);
}
#[test]
fn determinant_returns_finite_value_for_regular_matrix() {
let matrix = Matrix::<2>::try_from_rows([[4.0, 2.0], [1.0, 3.0]]).unwrap();
assert_relative_eq!(determinant(&matrix).unwrap(), 10.0);
}
#[test]
fn determinant_returns_zero_for_singular_matrix() {
let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [2.0, 4.0]]).unwrap();
assert_relative_eq!(determinant(&matrix).unwrap(), 0.0);
}
#[test]
fn determinant_preserves_nonfinite_backend_error() {
let matrix = Matrix::<2>::try_from_rows([[1.0e200, 0.0], [0.0, 1.0e200]]).unwrap();
assert_matches!(determinant(&matrix), Err(LaError::NonFinite { .. }));
}
}