1#![forbid(unsafe_code)]
2
3use core::fmt;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum ArithmeticOperation {
22 MatrixInfinityNorm,
24 SymmetryCheck,
26 LuFactorization,
28 ColumnBasis,
30 LdltFactorization,
32 LuSolve,
34 LdltSolve,
36 Determinant,
38 DeterminantErrorBound,
40 IntervalAddition,
42 IntervalSubtraction,
44 IntervalMultiplication,
46 IntervalReciprocal,
48 IntervalDivision,
50 IntervalSolve,
52 IntervalSquare,
54 IntervalDeterminant,
56 VectorDotProduct,
58 VectorDotDifference,
60 VectorSquaredNorm,
62 VectorNorm,
64 VectorAngle,
66}
67
68impl fmt::Display for ArithmeticOperation {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str(match self {
71 Self::MatrixInfinityNorm => "matrix infinity norm",
72 Self::SymmetryCheck => "symmetry check",
73 Self::LuFactorization => "LU factorization",
74 Self::ColumnBasis => "column basis selection",
75 Self::LdltFactorization => "LDLT factorization",
76 Self::LuSolve => "LU solve",
77 Self::LdltSolve => "LDLT solve",
78 Self::Determinant => "determinant",
79 Self::DeterminantErrorBound => "determinant error bound",
80 Self::IntervalAddition => "interval addition",
81 Self::IntervalSubtraction => "interval subtraction",
82 Self::IntervalMultiplication => "interval multiplication",
83 Self::IntervalReciprocal => "interval reciprocal",
84 Self::IntervalDivision => "interval division",
85 Self::IntervalSolve => "interval solve",
86 Self::IntervalSquare => "interval square",
87 Self::IntervalDeterminant => "interval determinant",
88 Self::VectorDotProduct => "vector dot product",
89 Self::VectorDotDifference => "vector dot difference",
90 Self::VectorSquaredNorm => "vector squared norm",
91 Self::VectorNorm => "vector Euclidean norm",
92 Self::VectorAngle => "vector angle",
93 })
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum FactorizationKind {
108 Lu,
110 Ldlt,
112}
113
114impl fmt::Display for FactorizationKind {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 f.write_str(match self {
117 Self::Lu => "LU",
118 Self::Ldlt => "LDLT",
119 })
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138#[non_exhaustive]
139pub enum InvalidToleranceReason {
140 Negative,
142 NotFinite,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum IntervalBound {
150 Lower,
152 Upper,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum IntervalOperand {
160 Left,
162 Right,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
184#[non_exhaustive]
185pub enum VectorOperand {
186 Left,
188 Right,
190}
191
192impl fmt::Display for VectorOperand {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.write_str(match self {
195 Self::Left => "left",
196 Self::Right => "right",
197 })
198 }
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum NonFiniteLocation {
218 #[non_exhaustive]
220 PolynomialTerm {
221 index: usize,
223 exponent: u128,
225 },
226 #[non_exhaustive]
228 MatrixCell {
229 row: usize,
231 col: usize,
233 },
234 #[non_exhaustive]
236 VectorEntry {
237 index: usize,
239 },
240 #[non_exhaustive]
242 VectorOperandEntry {
243 operand: VectorOperand,
245 index: usize,
247 },
248 #[non_exhaustive]
250 Step {
251 index: usize,
253 },
254 #[non_exhaustive]
256 IntervalBound {
257 bound: IntervalBound,
259 },
260 #[non_exhaustive]
262 IntervalOperand {
263 operand: IntervalOperand,
265 },
266 Scalar,
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272#[non_exhaustive]
273pub enum PolynomialWork {
274 SubsetTransitions,
276 TermProducts,
278}
279
280impl fmt::Display for PolynomialWork {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 f.write_str(match self {
283 Self::SubsetTransitions => "subset transitions",
284 Self::TermProducts => "coefficient-pair products",
285 })
286 }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
305#[non_exhaustive]
306pub enum NonFiniteOrigin {
307 Input,
309 #[non_exhaustive]
311 Computation {
312 operation: ArithmeticOperation,
314 },
315}
316
317#[derive(Clone, Copy, Debug, PartialEq)]
336#[non_exhaustive]
337pub enum PositiveSemidefiniteViolation {
338 #[non_exhaustive]
340 NegativePivot {
341 value: f64,
343 },
344 #[non_exhaustive]
346 ZeroPivotCoupling {
347 row: usize,
349 value: f64,
351 },
352}
353
354#[derive(Clone, Copy, Debug, PartialEq)]
368#[non_exhaustive]
369pub enum SingularityReason {
370 Exact,
372 #[non_exhaustive]
374 Numerical {
375 factorization: FactorizationKind,
377 pivot_magnitude: f64,
379 tolerance: f64,
381 },
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404#[non_exhaustive]
405pub enum UnrepresentableReason {
406 RequiresRounding,
409 NotFinite,
411}
412
413#[derive(Clone, Copy, Debug, PartialEq)]
432#[non_exhaustive]
433pub enum LaError {
434 #[non_exhaustive]
436 PolynomialBudgetExceeded {
437 work: PolynomialWork,
439 limit: usize,
441 completed: usize,
443 requested: usize,
445 },
446 #[non_exhaustive]
448 PolynomialExponentOverflow {
449 row: usize,
451 col: usize,
453 left: u128,
455 right: u128,
457 },
458 #[non_exhaustive]
460 DimensionMismatch {
461 left: usize,
463 right: usize,
465 },
466 EmptyVector,
468 #[non_exhaustive]
470 ZeroVector {
471 operand: VectorOperand,
473 },
474 #[non_exhaustive]
476 Singular {
477 pivot_col: usize,
479 reason: SingularityReason,
481 },
482 #[non_exhaustive]
484 NonFinite {
485 location: NonFiniteLocation,
487 origin: NonFiniteOrigin,
489 },
490 #[non_exhaustive]
493 IntervalRangeExhausted {
494 operation: ArithmeticOperation,
497 },
498 #[non_exhaustive]
501 IntervalZeroDivisor {
502 lower: f64,
504 upper: f64,
506 },
507 #[non_exhaustive]
510 IntervalSolveInconclusive {
511 pivot_col: usize,
513 },
514 #[non_exhaustive]
516 Unrepresentable {
517 index: Option<usize>,
519 reason: UnrepresentableReason,
521 },
522 #[non_exhaustive]
524 InvertedInterval {
525 lower: f64,
527 upper: f64,
529 },
530 #[non_exhaustive]
532 DeterminantScaleOverflow {
533 dim: usize,
535 min_exponent: i32,
537 },
538 #[non_exhaustive]
540 UnsupportedDimension {
541 requested: usize,
543 max: usize,
545 },
546 #[non_exhaustive]
548 IndexOutOfBounds {
549 row: usize,
551 col: usize,
553 dim: usize,
555 },
556 #[non_exhaustive]
558 InvalidTolerance {
559 value: f64,
561 reason: InvalidToleranceReason,
563 },
564 #[non_exhaustive]
566 Asymmetric {
567 row: usize,
569 col: usize,
571 dim: usize,
573 upper: f64,
575 lower: f64,
577 allowed_abs_diff: f64,
579 },
580 #[non_exhaustive]
586 NotPositiveSemidefinite {
587 pivot_col: usize,
589 violation: PositiveSemidefiniteViolation,
591 },
592}
593
594impl LaError {
595 #[inline]
598 #[must_use]
599 pub const fn singular_exact(pivot_col: usize) -> Self {
600 Self::Singular {
601 pivot_col,
602 reason: SingularityReason::Exact,
603 }
604 }
605
606 #[inline]
610 #[must_use]
611 pub const fn singular_numerical(
612 pivot_col: usize,
613 factorization: FactorizationKind,
614 pivot_magnitude: f64,
615 tolerance: f64,
616 ) -> Self {
617 Self::Singular {
618 pivot_col,
619 reason: SingularityReason::Numerical {
620 factorization,
621 pivot_magnitude,
622 tolerance,
623 },
624 }
625 }
626
627 #[inline]
630 #[must_use]
631 pub const fn non_finite_input_matrix(row: usize, col: usize) -> Self {
632 Self::NonFinite {
633 location: NonFiniteLocation::MatrixCell { row, col },
634 origin: NonFiniteOrigin::Input,
635 }
636 }
637
638 #[inline]
641 #[must_use]
642 pub const fn non_finite_input_vector(index: usize) -> Self {
643 Self::NonFinite {
644 location: NonFiniteLocation::VectorEntry { index },
645 origin: NonFiniteOrigin::Input,
646 }
647 }
648
649 #[inline]
652 #[must_use]
653 pub const fn non_finite_input_scalar() -> Self {
654 Self::NonFinite {
655 location: NonFiniteLocation::Scalar,
656 origin: NonFiniteOrigin::Input,
657 }
658 }
659
660 #[inline]
663 #[must_use]
664 pub const fn non_finite_input_interval_bound(bound: IntervalBound) -> Self {
665 Self::NonFinite {
666 location: NonFiniteLocation::IntervalBound { bound },
667 origin: NonFiniteOrigin::Input,
668 }
669 }
670
671 #[inline]
674 #[must_use]
675 pub const fn non_finite_input_interval_operand(operand: IntervalOperand) -> Self {
676 Self::NonFinite {
677 location: NonFiniteLocation::IntervalOperand { operand },
678 origin: NonFiniteOrigin::Input,
679 }
680 }
681
682 #[inline]
685 #[must_use]
686 pub const fn non_finite_computation_matrix(
687 operation: ArithmeticOperation,
688 row: usize,
689 col: usize,
690 ) -> Self {
691 Self::NonFinite {
692 location: NonFiniteLocation::MatrixCell { row, col },
693 origin: NonFiniteOrigin::Computation { operation },
694 }
695 }
696
697 #[inline]
700 #[must_use]
701 pub const fn non_finite_computation_step(operation: ArithmeticOperation, index: usize) -> Self {
702 Self::NonFinite {
703 location: NonFiniteLocation::Step { index },
704 origin: NonFiniteOrigin::Computation { operation },
705 }
706 }
707
708 #[inline]
711 #[must_use]
712 pub const fn non_finite_computation_scalar(operation: ArithmeticOperation) -> Self {
713 Self::NonFinite {
714 location: NonFiniteLocation::Scalar,
715 origin: NonFiniteOrigin::Computation { operation },
716 }
717 }
718
719 #[inline]
722 #[must_use]
723 pub const fn interval_range_exhausted(operation: ArithmeticOperation) -> Self {
724 Self::IntervalRangeExhausted { operation }
725 }
726
727 #[inline]
730 #[must_use]
731 pub const fn unrepresentable(index: Option<usize>, reason: UnrepresentableReason) -> Self {
732 Self::Unrepresentable { index, reason }
733 }
734
735 #[inline]
738 #[must_use]
739 pub const fn inverted_interval(lower: f64, upper: f64) -> Self {
740 Self::InvertedInterval { lower, upper }
741 }
742
743 #[inline]
746 #[must_use]
747 pub const fn unrepresentable_reason(&self) -> Option<UnrepresentableReason> {
748 match self {
749 Self::Unrepresentable { reason, .. } => Some(*reason),
750 _ => None,
751 }
752 }
753
754 #[inline]
757 #[must_use]
758 pub const fn requires_rounding(&self) -> bool {
759 matches!(
760 self,
761 Self::Unrepresentable {
762 reason: UnrepresentableReason::RequiresRounding,
763 ..
764 }
765 )
766 }
767
768 #[inline]
771 #[must_use]
772 pub const fn determinant_scale_overflow(dim: usize, min_exponent: i32) -> Self {
773 Self::DeterminantScaleOverflow { dim, min_exponent }
774 }
775
776 #[inline]
779 #[must_use]
780 pub const fn unsupported_dimension(requested: usize, max: usize) -> Self {
781 Self::UnsupportedDimension { requested, max }
782 }
783
784 #[inline]
787 #[must_use]
788 pub const fn index_out_of_bounds(row: usize, col: usize, dim: usize) -> Self {
789 Self::IndexOutOfBounds { row, col, dim }
790 }
791
792 #[inline]
800 #[must_use]
801 pub const fn invalid_tolerance(value: f64) -> Self {
802 let reason = if value.is_finite() {
803 InvalidToleranceReason::Negative
804 } else {
805 InvalidToleranceReason::NotFinite
806 };
807 Self::InvalidTolerance { value, reason }
808 }
809
810 #[inline]
814 #[must_use]
815 pub const fn asymmetric(
816 row: usize,
817 col: usize,
818 dim: usize,
819 upper: f64,
820 lower: f64,
821 allowed_abs_diff: f64,
822 ) -> Self {
823 Self::Asymmetric {
824 row,
825 col,
826 dim,
827 upper,
828 lower,
829 allowed_abs_diff,
830 }
831 }
832
833 #[inline]
836 #[must_use]
837 pub const fn not_positive_semidefinite_negative(pivot_col: usize, value: f64) -> Self {
838 Self::NotPositiveSemidefinite {
839 pivot_col,
840 violation: PositiveSemidefiniteViolation::NegativePivot { value },
841 }
842 }
843
844 #[inline]
848 #[must_use]
849 pub const fn not_positive_semidefinite_zero_coupling(
850 pivot_col: usize,
851 row: usize,
852 value: f64,
853 ) -> Self {
854 Self::NotPositiveSemidefinite {
855 pivot_col,
856 violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
857 }
858 }
859}
860
861fn write_non_finite_location(
865 f: &mut fmt::Formatter<'_>,
866 location: NonFiniteLocation,
867) -> fmt::Result {
868 match location {
869 NonFiniteLocation::PolynomialTerm { index, exponent } => {
870 write!(f, "polynomial term {index} (exponent {exponent})")
871 }
872 NonFiniteLocation::MatrixCell { row, col } => {
873 write!(f, "matrix cell ({row}, {col})")
874 }
875 NonFiniteLocation::VectorEntry { index } => write!(f, "vector entry {index}"),
876 NonFiniteLocation::VectorOperandEntry { operand, index } => {
877 write!(f, "{operand} vector entry {index}")
878 }
879 NonFiniteLocation::Step { index } => write!(f, "step {index}"),
880 NonFiniteLocation::IntervalBound {
881 bound: IntervalBound::Lower,
882 } => f.write_str("interval lower bound"),
883 NonFiniteLocation::IntervalBound {
884 bound: IntervalBound::Upper,
885 } => f.write_str("interval upper bound"),
886 NonFiniteLocation::IntervalOperand {
887 operand: IntervalOperand::Left,
888 } => f.write_str("left interval operand"),
889 NonFiniteLocation::IntervalOperand {
890 operand: IntervalOperand::Right,
891 } => f.write_str("right interval operand"),
892 NonFiniteLocation::Scalar => f.write_str("scalar value"),
893 }
894}
895
896fn write_non_finite(
899 f: &mut fmt::Formatter<'_>,
900 location: NonFiniteLocation,
901 origin: NonFiniteOrigin,
902) -> fmt::Result {
903 match (location, origin) {
904 (NonFiniteLocation::Scalar, NonFiniteOrigin::Input) => {
905 f.write_str("non-finite scalar input")
906 }
907 (NonFiniteLocation::Scalar, NonFiniteOrigin::Computation { operation }) => {
908 write!(f, "non-finite scalar result computed during {operation}")
909 }
910 (location, NonFiniteOrigin::Input) => {
911 f.write_str("non-finite input value at ")?;
912 write_non_finite_location(f, location)
913 }
914 (location, NonFiniteOrigin::Computation { operation }) => {
915 write!(f, "non-finite value computed during {operation} at ")?;
916 write_non_finite_location(f, location)
917 }
918 }
919}
920
921fn write_unrepresentable(
923 f: &mut fmt::Formatter<'_>,
924 index: Option<usize>,
925 reason: UnrepresentableReason,
926) -> fmt::Result {
927 f.write_str(match reason {
928 UnrepresentableReason::RequiresRounding => {
929 "exact result requires rounding to fit finite f64"
930 }
931 UnrepresentableReason::NotFinite => {
932 "exact result has no finite f64 representation after rounding"
933 }
934 })?;
935 if let Some(index) = index {
936 write!(f, " at index {index}")?;
937 }
938 Ok(())
939}
940
941fn write_singularity(
942 f: &mut fmt::Formatter<'_>,
943 pivot_col: usize,
944 reason: SingularityReason,
945) -> fmt::Result {
946 match reason {
947 SingularityReason::Exact => {
948 write!(f, "matrix is exactly singular at pivot column {pivot_col}")
949 }
950 SingularityReason::Numerical {
951 factorization,
952 pivot_magnitude,
953 tolerance,
954 } => write!(
955 f,
956 "matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}"
957 ),
958 }
959}
960
961impl fmt::Display for LaError {
962 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963 match *self {
964 Self::PolynomialBudgetExceeded {
965 work,
966 limit,
967 completed,
968 requested,
969 } => write!(
970 f,
971 "polynomial determinant budget exceeded for {work}: limit {limit}, completed {completed}, requested {requested} more"
972 ),
973 Self::PolynomialExponentOverflow {
974 row,
975 col,
976 left,
977 right,
978 } => write!(
979 f,
980 "polynomial exponent overflow at matrix cell ({row}, {col}): {left} + {right} exceeds u128::MAX"
981 ),
982 Self::DimensionMismatch { left, right } => {
983 write!(
984 f,
985 "vector dimension mismatch: left length {left}, right length {right}"
986 )
987 }
988 Self::EmptyVector => f.write_str("an empty vector has no direction"),
989 Self::ZeroVector { operand } => write!(f, "the {operand} zero vector has no direction"),
990 Self::Singular { pivot_col, reason } => write_singularity(f, pivot_col, reason),
991 Self::NonFinite { location, origin } => write_non_finite(f, location, origin),
992 Self::IntervalRangeExhausted { operation } => write!(
993 f,
994 "exact-real intermediate or result of {operation} has no enclosure with finite binary64 endpoints"
995 ),
996 Self::IntervalZeroDivisor { lower, upper } => {
997 write!(f, "interval divisor [{lower}, {upper}] contains zero")
998 }
999 Self::IntervalSolveInconclusive { pivot_col } => write!(
1000 f,
1001 "interval solve is inconclusive at pivot column {pivot_col}; no pivot excludes zero"
1002 ),
1003 Self::Unrepresentable { index, reason } => write_unrepresentable(f, index, reason),
1004 Self::InvertedInterval { lower, upper } => write!(
1005 f,
1006 "invalid interval bounds [{lower}, {upper}]; expected lower <= upper"
1007 ),
1008 Self::DeterminantScaleOverflow { dim, min_exponent } => write!(
1009 f,
1010 "exact determinant scale exponent overflows for dimension {dim} with minimum entry exponent {min_exponent}"
1011 ),
1012 Self::UnsupportedDimension { requested, max } => write!(
1013 f,
1014 "unsupported matrix dimension {requested}; maximum supported dimension is {max}"
1015 ),
1016 Self::IndexOutOfBounds { row, col, dim } => write!(
1017 f,
1018 "matrix index ({row}, {col}) is out of bounds for dimension {dim}"
1019 ),
1020 Self::InvalidTolerance {
1021 value,
1022 reason: InvalidToleranceReason::Negative,
1023 } => write!(f, "invalid tolerance {value}; expected value >= 0"),
1024 Self::InvalidTolerance {
1025 value,
1026 reason: InvalidToleranceReason::NotFinite,
1027 } => write!(f, "invalid tolerance {value}; expected a finite value"),
1028 Self::Asymmetric {
1029 row,
1030 col,
1031 dim,
1032 upper,
1033 lower,
1034 allowed_abs_diff,
1035 } => write!(
1036 f,
1037 "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}"
1038 ),
1039 Self::NotPositiveSemidefinite {
1040 pivot_col,
1041 violation: PositiveSemidefiniteViolation::NegativePivot { value },
1042 } => write!(
1043 f,
1044 "LDLT rejected the matrix at pivot column {pivot_col}: computed diagonal value {value} < 0"
1045 ),
1046 Self::NotPositiveSemidefinite {
1047 pivot_col,
1048 violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
1049 } => write!(
1050 f,
1051 "LDLT rejected the matrix at pivot column {pivot_col}: computed zero diagonal has non-zero coupling at row {row} with value {value}"
1052 ),
1053 }
1054 }
1055}
1056
1057impl std::error::Error for LaError {}
1058
1059#[cfg(test)]
1060mod tests {
1061 use std::error::Error;
1062
1063 use super::*;
1064 use crate::MAX_STACK_MATRIX_DISPATCH_DIM;
1065
1066 #[test]
1067 fn polynomial_errors_display_work_and_location_without_exact_feature() {
1068 assert_eq!(
1069 PolynomialWork::SubsetTransitions.to_string(),
1070 "subset transitions"
1071 );
1072 assert_eq!(
1073 PolynomialWork::TermProducts.to_string(),
1074 "coefficient-pair products"
1075 );
1076 assert_eq!(
1077 LaError::PolynomialBudgetExceeded {
1078 work: PolynomialWork::TermProducts,
1079 limit: 4,
1080 completed: 4,
1081 requested: 1,
1082 }
1083 .to_string(),
1084 "polynomial determinant budget exceeded for coefficient-pair products: limit 4, completed 4, requested 1 more"
1085 );
1086 assert_eq!(
1087 LaError::PolynomialExponentOverflow {
1088 row: 1,
1089 col: 2,
1090 left: u128::MAX,
1091 right: 1,
1092 }
1093 .to_string(),
1094 format!(
1095 "polynomial exponent overflow at matrix cell (1, 2): {} + 1 exceeds u128::MAX",
1096 u128::MAX
1097 )
1098 );
1099 }
1100
1101 #[test]
1102 fn category_displays_are_concise() {
1103 assert_eq!(FactorizationKind::Lu.to_string(), "LU");
1104 assert_eq!(FactorizationKind::Ldlt.to_string(), "LDLT");
1105 assert_eq!(
1106 ArithmeticOperation::MatrixInfinityNorm.to_string(),
1107 "matrix infinity norm"
1108 );
1109 assert_eq!(
1110 ArithmeticOperation::SymmetryCheck.to_string(),
1111 "symmetry check"
1112 );
1113 assert_eq!(
1114 ArithmeticOperation::LuFactorization.to_string(),
1115 "LU factorization"
1116 );
1117 assert_eq!(
1118 ArithmeticOperation::LdltFactorization.to_string(),
1119 "LDLT factorization"
1120 );
1121 assert_eq!(ArithmeticOperation::LuSolve.to_string(), "LU solve");
1122 assert_eq!(ArithmeticOperation::LdltSolve.to_string(), "LDLT solve");
1123 assert_eq!(ArithmeticOperation::Determinant.to_string(), "determinant");
1124 assert_eq!(
1125 ArithmeticOperation::DeterminantErrorBound.to_string(),
1126 "determinant error bound"
1127 );
1128 assert_eq!(
1129 ArithmeticOperation::VectorDotProduct.to_string(),
1130 "vector dot product"
1131 );
1132 assert_eq!(
1133 ArithmeticOperation::VectorDotDifference.to_string(),
1134 "vector dot difference"
1135 );
1136 assert_eq!(
1137 ArithmeticOperation::VectorSquaredNorm.to_string(),
1138 "vector squared norm"
1139 );
1140 assert_eq!(
1141 ArithmeticOperation::VectorNorm.to_string(),
1142 "vector Euclidean norm"
1143 );
1144 assert_eq!(
1145 ArithmeticOperation::IntervalAddition.to_string(),
1146 "interval addition"
1147 );
1148 assert_eq!(
1149 ArithmeticOperation::IntervalSubtraction.to_string(),
1150 "interval subtraction"
1151 );
1152 assert_eq!(
1153 ArithmeticOperation::IntervalMultiplication.to_string(),
1154 "interval multiplication"
1155 );
1156 assert_eq!(
1157 ArithmeticOperation::IntervalSquare.to_string(),
1158 "interval square"
1159 );
1160 assert_eq!(
1161 ArithmeticOperation::IntervalDeterminant.to_string(),
1162 "interval determinant"
1163 );
1164 }
1165
1166 #[test]
1167 fn singular_constructors_and_displays_preserve_reason() {
1168 let exact = LaError::singular_exact(3);
1169 assert_eq!(
1170 exact,
1171 LaError::Singular {
1172 pivot_col: 3,
1173 reason: SingularityReason::Exact,
1174 }
1175 );
1176 assert_eq!(
1177 exact.to_string(),
1178 "matrix is exactly singular at pivot column 3"
1179 );
1180
1181 let numerical = LaError::singular_numerical(2, FactorizationKind::Lu, 1e-14, 1e-12);
1182 assert_eq!(
1183 numerical,
1184 LaError::Singular {
1185 pivot_col: 2,
1186 reason: SingularityReason::Numerical {
1187 factorization: FactorizationKind::Lu,
1188 pivot_magnitude: 1e-14,
1189 tolerance: 1e-12,
1190 },
1191 }
1192 );
1193 assert_eq!(
1194 numerical.to_string(),
1195 "matrix is numerically singular during LU factorization at pivot column 2: pivot magnitude 0.00000000000001 <= tolerance 0.000000000001"
1196 );
1197 }
1198
1199 #[test]
1200 fn non_finite_constructors_preserve_location_and_origin() {
1201 assert_eq!(
1202 LaError::non_finite_input_matrix(1, 2),
1203 LaError::NonFinite {
1204 location: NonFiniteLocation::MatrixCell { row: 1, col: 2 },
1205 origin: NonFiniteOrigin::Input,
1206 }
1207 );
1208 assert_eq!(
1209 LaError::non_finite_input_vector(3).to_string(),
1210 "non-finite input value at vector entry 3"
1211 );
1212 assert_eq!(
1213 LaError::non_finite_input_scalar().to_string(),
1214 "non-finite scalar input"
1215 );
1216 assert_eq!(
1217 LaError::non_finite_input_interval_bound(IntervalBound::Upper).to_string(),
1218 "non-finite input value at interval upper bound"
1219 );
1220 assert_eq!(
1221 LaError::non_finite_input_interval_bound(IntervalBound::Lower).to_string(),
1222 "non-finite input value at interval lower bound"
1223 );
1224 assert_eq!(
1225 LaError::non_finite_input_interval_operand(IntervalOperand::Left).to_string(),
1226 "non-finite input value at left interval operand"
1227 );
1228 assert_eq!(
1229 LaError::non_finite_input_interval_operand(IntervalOperand::Right).to_string(),
1230 "non-finite input value at right interval operand"
1231 );
1232 assert_eq!(
1233 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 2, 1)
1234 .to_string(),
1235 "non-finite value computed during LU factorization at matrix cell (2, 1)"
1236 );
1237 assert_eq!(
1238 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1).to_string(),
1239 "non-finite value computed during LU solve at step 1"
1240 );
1241 assert_eq!(
1242 LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant).to_string(),
1243 "non-finite scalar result computed during determinant"
1244 );
1245 }
1246
1247 #[test]
1248 fn unrepresentable_helpers_preserve_recovery_reason() {
1249 let rounding = LaError::unrepresentable(Some(2), UnrepresentableReason::RequiresRounding);
1250 let scalar_rounding =
1251 LaError::unrepresentable(None, UnrepresentableReason::RequiresRounding);
1252 let indexed_not_finite =
1253 LaError::unrepresentable(Some(2), UnrepresentableReason::NotFinite);
1254 let not_finite = LaError::unrepresentable(None, UnrepresentableReason::NotFinite);
1255 assert_eq!(
1256 rounding.unrepresentable_reason(),
1257 Some(UnrepresentableReason::RequiresRounding)
1258 );
1259 assert!(rounding.requires_rounding());
1260 assert_eq!(
1261 rounding.to_string(),
1262 "exact result requires rounding to fit finite f64 at index 2"
1263 );
1264 assert_eq!(
1265 scalar_rounding.to_string(),
1266 "exact result requires rounding to fit finite f64"
1267 );
1268 assert_eq!(
1269 indexed_not_finite.to_string(),
1270 "exact result has no finite f64 representation after rounding at index 2"
1271 );
1272 assert_eq!(
1273 not_finite.to_string(),
1274 "exact result has no finite f64 representation after rounding"
1275 );
1276 assert!(!not_finite.requires_rounding());
1277 assert_eq!(LaError::singular_exact(0).unrepresentable_reason(), None);
1278 }
1279
1280 #[test]
1281 fn unrepresentable_display_propagates_writer_failure() {
1282 struct LimitedWrites {
1283 remaining: usize,
1284 }
1285
1286 impl fmt::Write for LimitedWrites {
1287 fn write_str(&mut self, _: &str) -> fmt::Result {
1288 if self.remaining == 0 {
1289 return Err(fmt::Error);
1290 }
1291 self.remaining -= 1;
1292 Ok(())
1293 }
1294 }
1295
1296 for reason in [
1297 UnrepresentableReason::RequiresRounding,
1298 UnrepresentableReason::NotFinite,
1299 ] {
1300 for index in [None, Some(2)] {
1301 let error = LaError::unrepresentable(index, reason);
1302 for remaining in [0, 1] {
1303 let expected = if remaining == 1 && index.is_none() {
1306 Ok(())
1307 } else {
1308 Err(fmt::Error)
1309 };
1310 assert_eq!(
1311 fmt::write(&mut LimitedWrites { remaining }, format_args!("{error}")),
1312 expected,
1313 "reason={reason:?}, index={index:?}, remaining={remaining}"
1314 );
1315 }
1316 }
1317 }
1318 }
1319
1320 #[test]
1321 fn invalid_tolerance_classifies_non_finite_before_negative() {
1322 assert_eq!(
1323 LaError::invalid_tolerance(-1.0),
1324 LaError::InvalidTolerance {
1325 value: -1.0,
1326 reason: InvalidToleranceReason::Negative,
1327 }
1328 );
1329 assert_eq!(
1330 LaError::invalid_tolerance(f64::NEG_INFINITY),
1331 LaError::InvalidTolerance {
1332 value: f64::NEG_INFINITY,
1333 reason: InvalidToleranceReason::NotFinite,
1334 }
1335 );
1336 assert_eq!(
1337 LaError::invalid_tolerance(-1.0).to_string(),
1338 "invalid tolerance -1; expected value >= 0"
1339 );
1340 assert_eq!(
1341 LaError::invalid_tolerance(f64::NEG_INFINITY).to_string(),
1342 "invalid tolerance -inf; expected a finite value"
1343 );
1344 }
1345
1346 #[test]
1347 fn inverted_interval_error_preserves_both_bounds() {
1348 let error = LaError::inverted_interval(2.0, 1.0);
1349 assert_eq!(
1350 error,
1351 LaError::InvertedInterval {
1352 lower: 2.0,
1353 upper: 1.0,
1354 }
1355 );
1356 assert_eq!(
1357 error.to_string(),
1358 "invalid interval bounds [2, 1]; expected lower <= upper"
1359 );
1360 }
1361
1362 #[test]
1363 fn interval_range_error_preserves_operation() {
1364 let error = LaError::interval_range_exhausted(ArithmeticOperation::IntervalSquare);
1365 assert_eq!(
1366 error,
1367 LaError::IntervalRangeExhausted {
1368 operation: ArithmeticOperation::IntervalSquare,
1369 }
1370 );
1371 assert_eq!(
1372 error.to_string(),
1373 "exact-real intermediate or result of interval square has no enclosure with finite binary64 endpoints"
1374 );
1375 }
1376
1377 #[test]
1378 fn asymmetric_error_retains_observed_values_and_bound() {
1379 let err = LaError::asymmetric(0, 2, 3, 1.0, 1.5, 1e-12);
1380 assert_eq!(
1381 err,
1382 LaError::Asymmetric {
1383 row: 0,
1384 col: 2,
1385 dim: 3,
1386 upper: 1.0,
1387 lower: 1.5,
1388 allowed_abs_diff: 1e-12,
1389 }
1390 );
1391 assert_eq!(
1392 err.to_string(),
1393 "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"
1394 );
1395 }
1396
1397 #[test]
1398 fn positive_semidefinite_errors_preserve_distinct_violations() {
1399 assert_eq!(
1400 LaError::not_positive_semidefinite_negative(1, -3.0).to_string(),
1401 "LDLT rejected the matrix at pivot column 1: computed diagonal value -3 < 0"
1402 );
1403 assert_eq!(
1404 LaError::not_positive_semidefinite_zero_coupling(0, 1, 2.0).to_string(),
1405 "LDLT rejected the matrix at pivot column 0: computed zero diagonal has non-zero coupling at row 1 with value 2"
1406 );
1407 }
1408
1409 #[test]
1410 fn remaining_helpers_and_displays_preserve_fields() {
1411 assert_eq!(
1412 LaError::determinant_scale_overflow(3, -1074).to_string(),
1413 "exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
1414 );
1415 assert_eq!(
1416 LaError::unsupported_dimension(8, MAX_STACK_MATRIX_DISPATCH_DIM).to_string(),
1417 "unsupported matrix dimension 8; maximum supported dimension is 7"
1418 );
1419 assert_eq!(
1420 LaError::index_out_of_bounds(3, 0, 3).to_string(),
1421 "matrix index (3, 0) is out of bounds for dimension 3"
1422 );
1423 }
1424
1425 #[test]
1426 fn is_std_error_with_no_source() {
1427 let err = LaError::singular_exact(0);
1428 let error: &dyn Error = &err;
1429 assert!(error.source().is_none());
1430 }
1431}