#![forbid(unsafe_code)]
use core::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArithmeticOperation {
MatrixInfinityNorm,
SymmetryCheck,
LuFactorization,
LdltFactorization,
LuSolve,
LdltSolve,
Determinant,
DeterminantErrorBound,
VectorDotProduct,
VectorSquaredNorm,
}
impl fmt::Display for ArithmeticOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::MatrixInfinityNorm => "matrix infinity norm",
Self::SymmetryCheck => "symmetry check",
Self::LuFactorization => "LU factorization",
Self::LdltFactorization => "LDLT factorization",
Self::LuSolve => "LU solve",
Self::LdltSolve => "LDLT solve",
Self::Determinant => "determinant",
Self::DeterminantErrorBound => "determinant error bound",
Self::VectorDotProduct => "vector dot product",
Self::VectorSquaredNorm => "vector squared norm",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FactorizationKind {
Lu,
Ldlt,
}
impl fmt::Display for FactorizationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Lu => "LU",
Self::Ldlt => "LDLT",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidToleranceReason {
Negative,
NotFinite,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NonFiniteLocation {
#[non_exhaustive]
MatrixCell {
row: usize,
col: usize,
},
#[non_exhaustive]
VectorEntry {
index: usize,
},
#[non_exhaustive]
Step {
index: usize,
},
Scalar,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum NonFiniteOrigin {
Input,
#[non_exhaustive]
Computation {
operation: ArithmeticOperation,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PositiveSemidefiniteViolation {
#[non_exhaustive]
NegativePivot {
value: f64,
},
#[non_exhaustive]
ZeroPivotCoupling {
row: usize,
value: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum SingularityReason {
Exact,
#[non_exhaustive]
Numerical {
factorization: FactorizationKind,
pivot_magnitude: f64,
tolerance: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnrepresentableReason {
RequiresRounding,
NotFinite,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum LaError {
#[non_exhaustive]
Singular {
pivot_col: usize,
reason: SingularityReason,
},
#[non_exhaustive]
NonFinite {
location: NonFiniteLocation,
origin: NonFiniteOrigin,
},
#[non_exhaustive]
Unrepresentable {
index: Option<usize>,
reason: UnrepresentableReason,
},
#[non_exhaustive]
DeterminantScaleOverflow {
dim: usize,
min_exponent: i32,
},
#[non_exhaustive]
UnsupportedDimension {
requested: usize,
max: usize,
},
#[non_exhaustive]
IndexOutOfBounds {
row: usize,
col: usize,
dim: usize,
},
#[non_exhaustive]
InvalidTolerance {
value: f64,
reason: InvalidToleranceReason,
},
#[non_exhaustive]
Asymmetric {
row: usize,
col: usize,
dim: usize,
upper: f64,
lower: f64,
allowed_abs_diff: f64,
},
#[non_exhaustive]
NotPositiveSemidefinite {
pivot_col: usize,
violation: PositiveSemidefiniteViolation,
},
}
impl LaError {
#[inline]
#[must_use]
pub const fn singular_exact(pivot_col: usize) -> Self {
Self::Singular {
pivot_col,
reason: SingularityReason::Exact,
}
}
#[inline]
#[must_use]
pub const fn singular_numerical(
pivot_col: usize,
factorization: FactorizationKind,
pivot_magnitude: f64,
tolerance: f64,
) -> Self {
Self::Singular {
pivot_col,
reason: SingularityReason::Numerical {
factorization,
pivot_magnitude,
tolerance,
},
}
}
#[inline]
#[must_use]
pub const fn non_finite_input_matrix(row: usize, col: usize) -> Self {
Self::NonFinite {
location: NonFiniteLocation::MatrixCell { row, col },
origin: NonFiniteOrigin::Input,
}
}
#[inline]
#[must_use]
pub const fn non_finite_input_vector(index: usize) -> Self {
Self::NonFinite {
location: NonFiniteLocation::VectorEntry { index },
origin: NonFiniteOrigin::Input,
}
}
#[inline]
#[must_use]
pub const fn non_finite_input_scalar() -> Self {
Self::NonFinite {
location: NonFiniteLocation::Scalar,
origin: NonFiniteOrigin::Input,
}
}
#[inline]
#[must_use]
pub const fn non_finite_computation_matrix(
operation: ArithmeticOperation,
row: usize,
col: usize,
) -> Self {
Self::NonFinite {
location: NonFiniteLocation::MatrixCell { row, col },
origin: NonFiniteOrigin::Computation { operation },
}
}
#[inline]
#[must_use]
pub const fn non_finite_computation_step(operation: ArithmeticOperation, index: usize) -> Self {
Self::NonFinite {
location: NonFiniteLocation::Step { index },
origin: NonFiniteOrigin::Computation { operation },
}
}
#[inline]
#[must_use]
pub const fn non_finite_computation_scalar(operation: ArithmeticOperation) -> Self {
Self::NonFinite {
location: NonFiniteLocation::Scalar,
origin: NonFiniteOrigin::Computation { operation },
}
}
#[inline]
#[must_use]
pub const fn unrepresentable(index: Option<usize>, reason: UnrepresentableReason) -> Self {
Self::Unrepresentable { index, reason }
}
#[inline]
#[must_use]
pub const fn unrepresentable_reason(&self) -> Option<UnrepresentableReason> {
match self {
Self::Unrepresentable { reason, .. } => Some(*reason),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn requires_rounding(&self) -> bool {
matches!(
self,
Self::Unrepresentable {
reason: UnrepresentableReason::RequiresRounding,
..
}
)
}
#[inline]
#[must_use]
pub const fn determinant_scale_overflow(dim: usize, min_exponent: i32) -> Self {
Self::DeterminantScaleOverflow { dim, min_exponent }
}
#[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 {
let reason = if value.is_finite() {
InvalidToleranceReason::Negative
} else {
InvalidToleranceReason::NotFinite
};
Self::InvalidTolerance { value, reason }
}
#[inline]
#[must_use]
pub const fn asymmetric(
row: usize,
col: usize,
dim: usize,
upper: f64,
lower: f64,
allowed_abs_diff: f64,
) -> Self {
Self::Asymmetric {
row,
col,
dim,
upper,
lower,
allowed_abs_diff,
}
}
#[inline]
#[must_use]
pub const fn not_positive_semidefinite_negative(pivot_col: usize, value: f64) -> Self {
Self::NotPositiveSemidefinite {
pivot_col,
violation: PositiveSemidefiniteViolation::NegativePivot { value },
}
}
#[inline]
#[must_use]
pub const fn not_positive_semidefinite_zero_coupling(
pivot_col: usize,
row: usize,
value: f64,
) -> Self {
Self::NotPositiveSemidefinite {
pivot_col,
violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
}
}
}
fn write_non_finite_location(
f: &mut fmt::Formatter<'_>,
location: NonFiniteLocation,
) -> fmt::Result {
match location {
NonFiniteLocation::MatrixCell { row, col } => {
write!(f, "matrix cell ({row}, {col})")
}
NonFiniteLocation::VectorEntry { index } => write!(f, "vector entry {index}"),
NonFiniteLocation::Step { index } => write!(f, "step {index}"),
NonFiniteLocation::Scalar => f.write_str("scalar value"),
}
}
fn write_non_finite(
f: &mut fmt::Formatter<'_>,
location: NonFiniteLocation,
origin: NonFiniteOrigin,
) -> fmt::Result {
match (location, origin) {
(NonFiniteLocation::Scalar, NonFiniteOrigin::Input) => {
f.write_str("non-finite scalar input")
}
(NonFiniteLocation::Scalar, NonFiniteOrigin::Computation { operation }) => {
write!(f, "non-finite scalar result computed during {operation}")
}
(location, NonFiniteOrigin::Input) => {
f.write_str("non-finite input value at ")?;
write_non_finite_location(f, location)
}
(location, NonFiniteOrigin::Computation { operation }) => {
write!(f, "non-finite value computed during {operation} at ")?;
write_non_finite_location(f, location)
}
}
}
impl fmt::Display for LaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Singular {
pivot_col,
reason: SingularityReason::Exact,
} => write!(f, "matrix is exactly singular at pivot column {pivot_col}"),
Self::Singular {
pivot_col,
reason:
SingularityReason::Numerical {
factorization,
pivot_magnitude,
tolerance,
},
} => write!(
f,
"matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}"
),
Self::NonFinite { location, origin } => write_non_finite(f, location, origin),
Self::Unrepresentable {
index: Some(index),
reason: UnrepresentableReason::RequiresRounding,
} => write!(
f,
"exact result requires rounding to fit finite f64 at index {index}"
),
Self::Unrepresentable {
index: None,
reason: UnrepresentableReason::RequiresRounding,
} => f.write_str("exact result requires rounding to fit finite f64"),
Self::Unrepresentable {
index: Some(index),
reason: UnrepresentableReason::NotFinite,
} => write!(
f,
"exact result has no finite f64 representation after rounding at index {index}"
),
Self::Unrepresentable {
index: None,
reason: UnrepresentableReason::NotFinite,
} => f.write_str("exact result has no finite f64 representation after rounding"),
Self::DeterminantScaleOverflow { dim, min_exponent } => write!(
f,
"exact determinant scale exponent overflows for dimension {dim} with minimum entry exponent {min_exponent}"
),
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,
reason: InvalidToleranceReason::Negative,
} => write!(f, "invalid tolerance {value}; expected value >= 0"),
Self::InvalidTolerance {
value,
reason: InvalidToleranceReason::NotFinite,
} => write!(f, "invalid tolerance {value}; expected a finite value"),
Self::Asymmetric {
row,
col,
dim,
upper,
lower,
allowed_abs_diff,
} => write!(
f,
"matrix is not symmetric for dimension {dim}: entry ({row}, {col}) = {upper} and entry ({col}, {row}) = {lower} differ by more than allowed absolute difference {allowed_abs_diff}"
),
Self::NotPositiveSemidefinite {
pivot_col,
violation: PositiveSemidefiniteViolation::NegativePivot { value },
} => write!(
f,
"LDLT rejected the matrix at pivot column {pivot_col}: computed diagonal value {value} < 0"
),
Self::NotPositiveSemidefinite {
pivot_col,
violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
} => write!(
f,
"LDLT rejected the matrix at pivot column {pivot_col}: computed zero diagonal has non-zero coupling at row {row} with value {value}"
),
}
}
}
impl std::error::Error for LaError {}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
use crate::MAX_STACK_MATRIX_DISPATCH_DIM;
#[test]
fn category_displays_are_concise() {
assert_eq!(FactorizationKind::Lu.to_string(), "LU");
assert_eq!(FactorizationKind::Ldlt.to_string(), "LDLT");
assert_eq!(
ArithmeticOperation::MatrixInfinityNorm.to_string(),
"matrix infinity norm"
);
assert_eq!(
ArithmeticOperation::SymmetryCheck.to_string(),
"symmetry check"
);
assert_eq!(
ArithmeticOperation::LuFactorization.to_string(),
"LU factorization"
);
assert_eq!(
ArithmeticOperation::LdltFactorization.to_string(),
"LDLT factorization"
);
assert_eq!(ArithmeticOperation::LuSolve.to_string(), "LU solve");
assert_eq!(ArithmeticOperation::LdltSolve.to_string(), "LDLT solve");
assert_eq!(ArithmeticOperation::Determinant.to_string(), "determinant");
assert_eq!(
ArithmeticOperation::DeterminantErrorBound.to_string(),
"determinant error bound"
);
assert_eq!(
ArithmeticOperation::VectorDotProduct.to_string(),
"vector dot product"
);
assert_eq!(
ArithmeticOperation::VectorSquaredNorm.to_string(),
"vector squared norm"
);
}
#[test]
fn singular_constructors_and_displays_preserve_reason() {
let exact = LaError::singular_exact(3);
assert_eq!(
exact,
LaError::Singular {
pivot_col: 3,
reason: SingularityReason::Exact,
}
);
assert_eq!(
exact.to_string(),
"matrix is exactly singular at pivot column 3"
);
let numerical = LaError::singular_numerical(2, FactorizationKind::Lu, 1e-14, 1e-12);
assert_eq!(
numerical,
LaError::Singular {
pivot_col: 2,
reason: SingularityReason::Numerical {
factorization: FactorizationKind::Lu,
pivot_magnitude: 1e-14,
tolerance: 1e-12,
},
}
);
assert_eq!(
numerical.to_string(),
"matrix is numerically singular during LU factorization at pivot column 2: pivot magnitude 0.00000000000001 <= tolerance 0.000000000001"
);
}
#[test]
fn non_finite_constructors_preserve_location_and_origin() {
assert_eq!(
LaError::non_finite_input_matrix(1, 2),
LaError::NonFinite {
location: NonFiniteLocation::MatrixCell { row: 1, col: 2 },
origin: NonFiniteOrigin::Input,
}
);
assert_eq!(
LaError::non_finite_input_vector(3).to_string(),
"non-finite input value at vector entry 3"
);
assert_eq!(
LaError::non_finite_input_scalar().to_string(),
"non-finite scalar input"
);
assert_eq!(
LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 2, 1)
.to_string(),
"non-finite value computed during LU factorization at matrix cell (2, 1)"
);
assert_eq!(
LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1).to_string(),
"non-finite value computed during LU solve at step 1"
);
assert_eq!(
LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant).to_string(),
"non-finite scalar result computed during determinant"
);
}
#[test]
fn unrepresentable_helpers_preserve_recovery_reason() {
let rounding = LaError::unrepresentable(Some(2), UnrepresentableReason::RequiresRounding);
let scalar_rounding =
LaError::unrepresentable(None, UnrepresentableReason::RequiresRounding);
let indexed_not_finite =
LaError::unrepresentable(Some(2), UnrepresentableReason::NotFinite);
let not_finite = LaError::unrepresentable(None, UnrepresentableReason::NotFinite);
assert_eq!(
rounding.unrepresentable_reason(),
Some(UnrepresentableReason::RequiresRounding)
);
assert!(rounding.requires_rounding());
assert_eq!(
rounding.to_string(),
"exact result requires rounding to fit finite f64 at index 2"
);
assert_eq!(
scalar_rounding.to_string(),
"exact result requires rounding to fit finite f64"
);
assert_eq!(
indexed_not_finite.to_string(),
"exact result has no finite f64 representation after rounding at index 2"
);
assert_eq!(
not_finite.to_string(),
"exact result has no finite f64 representation after rounding"
);
assert!(!not_finite.requires_rounding());
assert_eq!(LaError::singular_exact(0).unrepresentable_reason(), None);
}
#[test]
fn invalid_tolerance_classifies_non_finite_before_negative() {
assert_eq!(
LaError::invalid_tolerance(-1.0),
LaError::InvalidTolerance {
value: -1.0,
reason: InvalidToleranceReason::Negative,
}
);
assert_eq!(
LaError::invalid_tolerance(f64::NEG_INFINITY),
LaError::InvalidTolerance {
value: f64::NEG_INFINITY,
reason: InvalidToleranceReason::NotFinite,
}
);
assert_eq!(
LaError::invalid_tolerance(-1.0).to_string(),
"invalid tolerance -1; expected value >= 0"
);
assert_eq!(
LaError::invalid_tolerance(f64::NEG_INFINITY).to_string(),
"invalid tolerance -inf; expected a finite value"
);
}
#[test]
fn asymmetric_error_retains_observed_values_and_bound() {
let err = LaError::asymmetric(0, 2, 3, 1.0, 1.5, 1e-12);
assert_eq!(
err,
LaError::Asymmetric {
row: 0,
col: 2,
dim: 3,
upper: 1.0,
lower: 1.5,
allowed_abs_diff: 1e-12,
}
);
assert_eq!(
err.to_string(),
"matrix is not symmetric for dimension 3: entry (0, 2) = 1 and entry (2, 0) = 1.5 differ by more than allowed absolute difference 0.000000000001"
);
}
#[test]
fn positive_semidefinite_errors_preserve_distinct_violations() {
assert_eq!(
LaError::not_positive_semidefinite_negative(1, -3.0).to_string(),
"LDLT rejected the matrix at pivot column 1: computed diagonal value -3 < 0"
);
assert_eq!(
LaError::not_positive_semidefinite_zero_coupling(0, 1, 2.0).to_string(),
"LDLT rejected the matrix at pivot column 0: computed zero diagonal has non-zero coupling at row 1 with value 2"
);
}
#[test]
fn remaining_helpers_and_displays_preserve_fields() {
assert_eq!(
LaError::determinant_scale_overflow(3, -1074).to_string(),
"exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
);
assert_eq!(
LaError::unsupported_dimension(8, MAX_STACK_MATRIX_DISPATCH_DIM).to_string(),
"unsupported matrix dimension 8; maximum stack-dispatch dimension is 7"
);
assert_eq!(
LaError::index_out_of_bounds(3, 0, 3).to_string(),
"matrix index (3, 0) is out of bounds for dimension 3"
);
}
#[test]
fn is_std_error_with_no_source() {
let err = LaError::singular_exact(0);
let error: &dyn Error = &err;
assert!(error.source().is_none());
}
}