#![forbid(unsafe_code)]
use core::fmt;
use crate::Tolerance;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnrepresentableReason {
RequiresRounding,
NotFinite,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum LaError {
Singular {
pivot_col: usize,
},
NonFinite {
row: Option<usize>,
col: usize,
},
Unrepresentable {
index: Option<usize>,
reason: UnrepresentableReason,
},
DeterminantScaleOverflow {
dim: usize,
min_exponent: i32,
},
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 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 {
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::Unrepresentable {
index: Some(i),
reason: UnrepresentableReason::RequiresRounding,
} => write!(
f,
"exact result requires rounding to fit finite f64 at index {i}"
),
Self::Unrepresentable {
index: None,
reason: UnrepresentableReason::RequiresRounding,
} => write!(f, "exact result requires rounding to fit finite f64"),
Self::Unrepresentable {
index: Some(i),
reason: UnrepresentableReason::NotFinite,
} => write!(f, "exact result does not round to finite f64 at index {i}"),
Self::Unrepresentable {
index: None,
reason: UnrepresentableReason::NotFinite,
} => write!(f, "exact result does not round to finite f64"),
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 } => {
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 {}
#[cfg(test)]
mod tests {
use super::*;
use core::assert_matches;
#[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_unrepresentable_requires_rounding() {
let err = LaError::Unrepresentable {
index: None,
reason: UnrepresentableReason::RequiresRounding,
};
assert_eq!(
err.to_string(),
"exact result requires rounding to fit finite f64"
);
}
#[test]
fn laerror_display_formats_unrepresentable_requires_rounding_with_index() {
let err = LaError::Unrepresentable {
index: Some(2),
reason: UnrepresentableReason::RequiresRounding,
};
assert_eq!(
err.to_string(),
"exact result requires rounding to fit finite f64 at index 2"
);
}
#[test]
fn laerror_display_formats_unrepresentable_not_finite() {
let err = LaError::Unrepresentable {
index: None,
reason: UnrepresentableReason::NotFinite,
};
assert_eq!(err.to_string(), "exact result does not round to finite f64");
}
#[test]
fn laerror_display_formats_unrepresentable_not_finite_with_index() {
let err = LaError::Unrepresentable {
index: Some(2),
reason: UnrepresentableReason::NotFinite,
};
assert_eq!(
err.to_string(),
"exact result does not round to finite f64 at index 2"
);
}
#[test]
fn laerror_unrepresentable_reason_reports_typed_reason() {
let rounding = LaError::Unrepresentable {
index: Some(2),
reason: UnrepresentableReason::RequiresRounding,
};
let not_finite = LaError::Unrepresentable {
index: None,
reason: UnrepresentableReason::NotFinite,
};
assert_eq!(
rounding.unrepresentable_reason(),
Some(UnrepresentableReason::RequiresRounding)
);
assert_eq!(
not_finite.unrepresentable_reason(),
Some(UnrepresentableReason::NotFinite)
);
assert_eq!(
LaError::Singular { pivot_col: 0 }.unrepresentable_reason(),
None
);
}
#[test]
fn laerror_requires_rounding_only_matches_rounding_reason() {
assert!(
LaError::Unrepresentable {
index: Some(2),
reason: UnrepresentableReason::RequiresRounding,
}
.requires_rounding()
);
assert!(
!LaError::Unrepresentable {
index: None,
reason: UnrepresentableReason::NotFinite,
}
.requires_rounding()
);
assert!(!LaError::Singular { pivot_col: 0 }.requires_rounding());
}
#[test]
fn laerror_display_formats_determinant_scale_overflow() {
let err = LaError::DeterminantScaleOverflow {
dim: 3,
min_exponent: -1074,
};
assert_eq!(
err.to_string(),
"exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
);
}
#[test]
fn laerror_display_formats_unsupported_dimension() {
let err = LaError::UnsupportedDimension {
requested: 8,
max: crate::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 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_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());
}
}