#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
#[cfg(doc)]
mod readme_doctests {
fn solve_5x5_example() {}
fn det_5x5_ldlt_example() {}
}
#[cfg(feature = "exact")]
mod exact;
#[cfg(feature = "exact")]
pub use num_bigint::BigInt;
#[cfg(feature = "exact")]
pub use num_rational::BigRational;
#[cfg(feature = "exact")]
pub use num_traits::{FromPrimitive, Signed, ToPrimitive};
mod ldlt;
mod lu;
mod matrix;
mod vector;
use core::fmt;
const EPS: f64 = f64::EPSILON;
pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
pub const MAX_STACK_MATRIX_DISPATCH_DIM: usize = 7;
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tolerance {
value: f64,
}
impl Tolerance {
pub(crate) const fn new_unchecked(value: f64) -> Self {
Self { value }
}
#[inline]
pub const fn new(value: f64) -> Result<Self, LaError> {
if value >= 0.0 && value.is_finite() {
Ok(Self::new_unchecked(value))
} else {
Err(LaError::invalid_tolerance(value))
}
}
#[inline]
#[must_use]
pub const fn get(self) -> f64 {
self.value
}
}
pub const DEFAULT_SINGULAR_TOL: Tolerance = Tolerance::new_unchecked(1e-12);
pub const DEFAULT_PIVOT_TOL: Tolerance = DEFAULT_SINGULAR_TOL;
pub(crate) const LDLT_SYMMETRY_REL_TOL: Tolerance = Tolerance::new_unchecked(1e-12);
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum LaError {
Singular {
pivot_col: usize,
},
NonFinite {
row: Option<usize>,
col: usize,
},
Overflow {
index: Option<usize>,
},
UnsupportedDimension {
requested: usize,
max: usize,
},
IndexOutOfBounds {
row: usize,
col: usize,
dim: usize,
},
InvalidTolerance {
value: f64,
},
Asymmetric {
row: usize,
col: usize,
dim: usize,
},
NotPositiveSemidefinite {
pivot_col: usize,
value: f64,
},
}
impl LaError {
#[inline]
#[must_use]
pub const fn non_finite_cell(row: usize, col: usize) -> Self {
Self::NonFinite {
row: Some(row),
col,
}
}
#[inline]
#[must_use]
pub const fn non_finite_at(col: usize) -> Self {
Self::NonFinite { row: None, col }
}
#[inline]
#[must_use]
pub const fn unsupported_dimension(requested: usize, max: usize) -> Self {
Self::UnsupportedDimension { requested, max }
}
#[inline]
#[must_use]
pub const fn index_out_of_bounds(row: usize, col: usize, dim: usize) -> Self {
Self::IndexOutOfBounds { row, col, dim }
}
#[inline]
#[must_use]
pub const fn invalid_tolerance(value: f64) -> Self {
Self::InvalidTolerance { value }
}
#[inline]
#[must_use]
pub const fn asymmetric(row: usize, col: usize, dim: usize) -> Self {
Self::Asymmetric { row, col, dim }
}
#[inline]
#[must_use]
pub const fn not_positive_semidefinite(pivot_col: usize, value: f64) -> Self {
Self::NotPositiveSemidefinite { pivot_col, value }
}
#[inline]
pub const fn validate_tolerance(value: f64) -> Result<Tolerance, Self> {
Tolerance::new(value)
}
}
impl fmt::Display for LaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Singular { pivot_col } => {
write!(f, "singular matrix at pivot column {pivot_col}")
}
Self::NonFinite { row: Some(r), col } => {
write!(f, "non-finite value at ({r}, {col})")
}
Self::NonFinite { row: None, col } => {
write!(f, "non-finite value at index {col}")
}
Self::Overflow { index: Some(i) } => {
write!(
f,
"exact result overflows the target representation at index {i}"
)
}
Self::Overflow { index: None } => {
write!(f, "exact result overflows the target representation")
}
Self::UnsupportedDimension { requested, max } => {
write!(
f,
"unsupported matrix dimension {requested}; maximum stack-dispatch dimension is {max}"
)
}
Self::IndexOutOfBounds { row, col, dim } => {
write!(
f,
"matrix index ({row}, {col}) is out of bounds for dimension {dim}"
)
}
Self::InvalidTolerance { value } => {
write!(f, "invalid tolerance {value}; expected finite value >= 0")
}
Self::Asymmetric { row, col, dim } => {
write!(
f,
"matrix is not symmetric for dimension {dim}: asymmetric pair ({row}, {col})"
)
}
Self::NotPositiveSemidefinite { pivot_col, value } => {
write!(
f,
"matrix is not positive semidefinite at LDLT pivot column {pivot_col}: diagonal value {value} < 0"
)
}
}
}
}
impl std::error::Error for LaError {}
pub use ldlt::Ldlt;
pub use lu::Lu;
pub use matrix::Matrix;
pub use vector::Vector;
#[macro_export]
macro_rules! try_with_stack_matrix {
($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
let __la_stack_requested_dim: usize = $dim;
match __la_stack_requested_dim {
0 => $crate::try_with_stack_matrix!(@arm 0, $matrix, $ret, $body),
1 => $crate::try_with_stack_matrix!(@arm 1, $matrix, $ret, $body),
2 => $crate::try_with_stack_matrix!(@arm 2, $matrix, $ret, $body),
3 => $crate::try_with_stack_matrix!(@arm 3, $matrix, $ret, $body),
4 => $crate::try_with_stack_matrix!(@arm 4, $matrix, $ret, $body),
5 => $crate::try_with_stack_matrix!(@arm 5, $matrix, $ret, $body),
6 => $crate::try_with_stack_matrix!(@arm 6, $matrix, $ret, $body),
7 => $crate::try_with_stack_matrix!(@arm 7, $matrix, $ret, $body),
requested => Err(::core::convert::From::from(
$crate::LaError::unsupported_dimension(
requested,
$crate::MAX_STACK_MATRIX_DISPATCH_DIM,
),
)),
}
}};
($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
let __la_stack_requested_dim: usize = $dim;
match __la_stack_requested_dim {
0 => $crate::try_with_stack_matrix!(@arm_mut 0, $matrix, $ret, $body),
1 => $crate::try_with_stack_matrix!(@arm_mut 1, $matrix, $ret, $body),
2 => $crate::try_with_stack_matrix!(@arm_mut 2, $matrix, $ret, $body),
3 => $crate::try_with_stack_matrix!(@arm_mut 3, $matrix, $ret, $body),
4 => $crate::try_with_stack_matrix!(@arm_mut 4, $matrix, $ret, $body),
5 => $crate::try_with_stack_matrix!(@arm_mut 5, $matrix, $ret, $body),
6 => $crate::try_with_stack_matrix!(@arm_mut 6, $matrix, $ret, $body),
7 => $crate::try_with_stack_matrix!(@arm_mut 7, $matrix, $ret, $body),
requested => Err(::core::convert::From::from(
$crate::LaError::unsupported_dimension(
requested,
$crate::MAX_STACK_MATRIX_DISPATCH_DIM,
),
)),
}
}};
(@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
let __la_stack_body = |$matrix: $crate::Matrix<$d>| -> $ret { $body };
__la_stack_body($crate::Matrix::<$d>::zero())
}};
(@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
let __la_stack_body = |mut $matrix: $crate::Matrix<$d>| -> $ret { $body };
__la_stack_body($crate::Matrix::<$d>::zero())
}};
}
pub mod prelude {
pub use crate::{
DEFAULT_PIVOT_TOL, DEFAULT_SINGULAR_TOL, ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4, LaError,
Ldlt, Lu, MAX_STACK_MATRIX_DISPATCH_DIM, Matrix, Tolerance, Vector, try_with_stack_matrix,
};
#[cfg(feature = "exact")]
pub use crate::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive};
}
#[cfg(test)]
mod tests {
use core::assert_matches;
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn default_singular_tol_is_expected() {
assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL.get(), 1e-12, epsilon = 0.0);
assert_abs_diff_eq!(
DEFAULT_PIVOT_TOL.get(),
DEFAULT_SINGULAR_TOL.get(),
epsilon = 0.0
);
}
#[test]
fn tolerance_new_accepts_finite_non_negative_values() {
assert_eq!(
Tolerance::new(0.0).unwrap().get().to_bits(),
0.0f64.to_bits()
);
assert_eq!(
Tolerance::new(1e-12).unwrap().get().to_bits(),
1e-12f64.to_bits()
);
assert_eq!(
Tolerance::new(f64::MAX).unwrap().get().to_bits(),
f64::MAX.to_bits()
);
}
#[test]
fn tolerance_new_rejects_negative_nan_and_infinity() {
assert_eq!(
Tolerance::new(-1.0),
Err(LaError::InvalidTolerance { value: -1.0 })
);
assert_matches!(
Tolerance::new(f64::NAN),
Err(LaError::InvalidTolerance { value }) if value.is_nan()
);
assert_eq!(
Tolerance::new(f64::INFINITY),
Err(LaError::InvalidTolerance {
value: f64::INFINITY,
})
);
assert_eq!(
Tolerance::new(f64::NEG_INFINITY),
Err(LaError::InvalidTolerance {
value: f64::NEG_INFINITY,
})
);
}
#[test]
fn validate_tolerance_matches_tolerance_new() {
for value in [0.0, 1e-12, f64::MAX] {
assert_eq!(LaError::validate_tolerance(value), Tolerance::new(value));
}
assert_eq!(
LaError::validate_tolerance(-1.0),
Err(LaError::InvalidTolerance { value: -1.0 })
);
assert_matches!(
LaError::validate_tolerance(f64::NAN),
Err(LaError::InvalidTolerance { value }) if value.is_nan()
);
assert_eq!(
LaError::validate_tolerance(f64::INFINITY),
Err(LaError::InvalidTolerance {
value: f64::INFINITY,
})
);
assert_eq!(
LaError::validate_tolerance(f64::NEG_INFINITY),
Err(LaError::InvalidTolerance {
value: f64::NEG_INFINITY,
})
);
}
#[test]
fn laerror_display_formats_singular() {
let err = LaError::Singular { pivot_col: 3 };
assert_eq!(err.to_string(), "singular matrix at pivot column 3");
}
#[test]
fn laerror_display_formats_nonfinite_with_row() {
let err = LaError::NonFinite {
row: Some(1),
col: 2,
};
assert_eq!(err.to_string(), "non-finite value at (1, 2)");
}
#[test]
fn laerror_display_formats_nonfinite_without_row() {
let err = LaError::NonFinite { row: None, col: 3 };
assert_eq!(err.to_string(), "non-finite value at index 3");
}
#[test]
fn laerror_display_formats_overflow() {
let err = LaError::Overflow { index: None };
assert_eq!(
err.to_string(),
"exact result overflows the target representation"
);
}
#[test]
fn laerror_display_formats_overflow_with_index() {
let err = LaError::Overflow { index: Some(2) };
assert_eq!(
err.to_string(),
"exact result overflows the target representation at index 2"
);
}
#[test]
fn laerror_display_formats_unsupported_dimension() {
let err = LaError::UnsupportedDimension {
requested: 8,
max: MAX_STACK_MATRIX_DISPATCH_DIM,
};
assert_eq!(
err.to_string(),
"unsupported matrix dimension 8; maximum stack-dispatch dimension is 7"
);
}
#[test]
fn laerror_display_formats_index_out_of_bounds() {
let err = LaError::IndexOutOfBounds {
row: 3,
col: 0,
dim: 3,
};
assert_eq!(
err.to_string(),
"matrix index (3, 0) is out of bounds for dimension 3"
);
}
#[test]
fn laerror_display_formats_invalid_tolerance() {
let err = LaError::InvalidTolerance { value: -1.0 };
assert_eq!(
err.to_string(),
"invalid tolerance -1; expected finite value >= 0"
);
}
#[test]
fn laerror_display_formats_asymmetric() {
let err = LaError::Asymmetric {
row: 0,
col: 2,
dim: 3,
};
assert_eq!(
err.to_string(),
"matrix is not symmetric for dimension 3: asymmetric pair (0, 2)"
);
}
#[test]
fn laerror_display_formats_not_positive_semidefinite() {
let err = LaError::NotPositiveSemidefinite {
pivot_col: 1,
value: -3.0,
};
assert_eq!(
err.to_string(),
"matrix is not positive semidefinite at LDLT pivot column 1: diagonal value -3 < 0"
);
}
#[test]
fn laerror_is_std_error_with_no_source() {
let err = LaError::Singular { pivot_col: 0 };
let e: &dyn std::error::Error = &err;
assert!(e.source().is_none());
}
#[test]
fn prelude_reexports_compile_and_work() {
use crate::prelude::*;
let m = Matrix::<2>::identity();
let v = Vector::<2>::new([1.0, 2.0]);
let _ = m.lu(DEFAULT_PIVOT_TOL).unwrap().solve_vec(v).unwrap();
let _ = m.ldlt(DEFAULT_SINGULAR_TOL).unwrap().solve_vec(v).unwrap();
assert_eq!(MAX_STACK_MATRIX_DISPATCH_DIM, 7);
}
macro_rules! gen_stack_matrix_dispatch_tests {
($d:literal) => {
pastey::paste! {
#[test]
fn [<try_with_stack_matrix_dispatches_ $d d>]() {
let requested = $d;
let got = try_with_stack_matrix!(requested, |mut m| -> Result<usize, LaError> {
if $d > 0 {
m.set_checked($d - 1, $d - 1, f64::from($d))?;
assert_abs_diff_eq!(
m.get_checked($d - 1, $d - 1)?,
f64::from($d),
epsilon = 0.0
);
}
Ok($d)
});
assert_eq!(got, Ok($d));
}
}
};
}
gen_stack_matrix_dispatch_tests!(2);
gen_stack_matrix_dispatch_tests!(3);
gen_stack_matrix_dispatch_tests!(4);
gen_stack_matrix_dispatch_tests!(5);
gen_stack_matrix_dispatch_tests!(6);
gen_stack_matrix_dispatch_tests!(7);
#[test]
fn try_with_stack_matrix_supports_zero_dimension() {
let got = try_with_stack_matrix!(0usize, |m| -> Result<Option<f64>, LaError> {
m.det_direct()
});
assert_eq!(got, Ok(Some(1.0)));
}
#[test]
fn try_with_stack_matrix_reports_unsupported_dimension() {
let got = try_with_stack_matrix!(8usize, |m| -> Result<f64, LaError> { m.det() });
assert_eq!(
got,
Err(LaError::UnsupportedDimension {
requested: 8,
max: MAX_STACK_MATRIX_DISPATCH_DIM,
})
);
}
#[derive(Debug, PartialEq)]
struct DownstreamError(LaError);
impl From<LaError> for DownstreamError {
fn from(err: LaError) -> Self {
Self(err)
}
}
#[test]
fn try_with_stack_matrix_converts_unsupported_dimension_error() {
let got = try_with_stack_matrix!(9usize, |m| -> Result<usize, DownstreamError> {
assert_abs_diff_eq!(m.inf_norm().unwrap(), 0.0, epsilon = 0.0);
Ok(0)
});
assert_eq!(
got,
Err(DownstreamError(LaError::UnsupportedDimension {
requested: 9,
max: MAX_STACK_MATRIX_DISPATCH_DIM,
}))
);
}
#[cfg(feature = "exact")]
#[test]
fn prelude_exact_reexports_compile_and_work() {
use crate::prelude::*;
let n = BigInt::from(7);
let r = BigRational::from_integer(n.clone());
assert_eq!(*r.numer(), n);
let half = BigRational::from_f64(0.5).unwrap();
let two = BigRational::from_i64(2).unwrap();
assert_eq!(
half.clone() + half.clone(),
BigRational::from_integer(BigInt::from(1))
);
assert!(half.is_positive());
assert!(!half.is_negative());
let neg = -half.clone();
assert!(neg.is_negative());
assert_eq!(neg.abs(), half);
assert!((half.to_f64().unwrap() - 0.5).abs() <= f64::EPSILON);
assert_eq!(two.to_i64().unwrap(), 2);
}
}