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 LdltFactorization,
30 LuSolve,
32 LdltSolve,
34 Determinant,
36 DeterminantErrorBound,
38 IntervalAddition,
40 IntervalSubtraction,
42 IntervalMultiplication,
44 IntervalSquare,
46 IntervalDeterminant,
48 VectorDotProduct,
50 VectorDotDifference,
52 VectorSquaredNorm,
54 VectorNorm,
56}
57
58impl fmt::Display for ArithmeticOperation {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(match self {
61 Self::MatrixInfinityNorm => "matrix infinity norm",
62 Self::SymmetryCheck => "symmetry check",
63 Self::LuFactorization => "LU factorization",
64 Self::LdltFactorization => "LDLT factorization",
65 Self::LuSolve => "LU solve",
66 Self::LdltSolve => "LDLT solve",
67 Self::Determinant => "determinant",
68 Self::DeterminantErrorBound => "determinant error bound",
69 Self::IntervalAddition => "interval addition",
70 Self::IntervalSubtraction => "interval subtraction",
71 Self::IntervalMultiplication => "interval multiplication",
72 Self::IntervalSquare => "interval square",
73 Self::IntervalDeterminant => "interval determinant",
74 Self::VectorDotProduct => "vector dot product",
75 Self::VectorDotDifference => "vector dot difference",
76 Self::VectorSquaredNorm => "vector squared norm",
77 Self::VectorNorm => "vector Euclidean norm",
78 })
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum FactorizationKind {
93 Lu,
95 Ldlt,
97}
98
99impl fmt::Display for FactorizationKind {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(match self {
102 Self::Lu => "LU",
103 Self::Ldlt => "LDLT",
104 })
105 }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123#[non_exhaustive]
124pub enum InvalidToleranceReason {
125 Negative,
127 NotFinite,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133#[non_exhaustive]
134pub enum IntervalBound {
135 Lower,
137 Upper,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum IntervalOperand {
145 Left,
147 Right,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166#[non_exhaustive]
167pub enum NonFiniteLocation {
168 #[non_exhaustive]
170 MatrixCell {
171 row: usize,
173 col: usize,
175 },
176 #[non_exhaustive]
178 VectorEntry {
179 index: usize,
181 },
182 #[non_exhaustive]
184 Step {
185 index: usize,
187 },
188 #[non_exhaustive]
190 IntervalBound {
191 bound: IntervalBound,
193 },
194 #[non_exhaustive]
196 IntervalOperand {
197 operand: IntervalOperand,
199 },
200 Scalar,
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220#[non_exhaustive]
221pub enum NonFiniteOrigin {
222 Input,
224 #[non_exhaustive]
226 Computation {
227 operation: ArithmeticOperation,
229 },
230}
231
232#[derive(Clone, Copy, Debug, PartialEq)]
251#[non_exhaustive]
252pub enum PositiveSemidefiniteViolation {
253 #[non_exhaustive]
255 NegativePivot {
256 value: f64,
258 },
259 #[non_exhaustive]
261 ZeroPivotCoupling {
262 row: usize,
264 value: f64,
266 },
267}
268
269#[derive(Clone, Copy, Debug, PartialEq)]
283#[non_exhaustive]
284pub enum SingularityReason {
285 Exact,
287 #[non_exhaustive]
289 Numerical {
290 factorization: FactorizationKind,
292 pivot_magnitude: f64,
294 tolerance: f64,
296 },
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319#[non_exhaustive]
320pub enum UnrepresentableReason {
321 RequiresRounding,
324 NotFinite,
326}
327
328#[derive(Clone, Copy, Debug, PartialEq)]
347#[non_exhaustive]
348pub enum LaError {
349 #[non_exhaustive]
351 Singular {
352 pivot_col: usize,
354 reason: SingularityReason,
356 },
357 #[non_exhaustive]
359 NonFinite {
360 location: NonFiniteLocation,
362 origin: NonFiniteOrigin,
364 },
365 #[non_exhaustive]
368 IntervalRangeExhausted {
369 operation: ArithmeticOperation,
372 },
373 #[non_exhaustive]
375 Unrepresentable {
376 index: Option<usize>,
378 reason: UnrepresentableReason,
380 },
381 #[non_exhaustive]
383 InvertedInterval {
384 lower: f64,
386 upper: f64,
388 },
389 #[non_exhaustive]
391 DeterminantScaleOverflow {
392 dim: usize,
394 min_exponent: i32,
396 },
397 #[non_exhaustive]
399 UnsupportedDimension {
400 requested: usize,
402 max: usize,
404 },
405 #[non_exhaustive]
407 IndexOutOfBounds {
408 row: usize,
410 col: usize,
412 dim: usize,
414 },
415 #[non_exhaustive]
417 InvalidTolerance {
418 value: f64,
420 reason: InvalidToleranceReason,
422 },
423 #[non_exhaustive]
425 Asymmetric {
426 row: usize,
428 col: usize,
430 dim: usize,
432 upper: f64,
434 lower: f64,
436 allowed_abs_diff: f64,
438 },
439 #[non_exhaustive]
445 NotPositiveSemidefinite {
446 pivot_col: usize,
448 violation: PositiveSemidefiniteViolation,
450 },
451}
452
453impl LaError {
454 #[inline]
457 #[must_use]
458 pub const fn singular_exact(pivot_col: usize) -> Self {
459 Self::Singular {
460 pivot_col,
461 reason: SingularityReason::Exact,
462 }
463 }
464
465 #[inline]
469 #[must_use]
470 pub const fn singular_numerical(
471 pivot_col: usize,
472 factorization: FactorizationKind,
473 pivot_magnitude: f64,
474 tolerance: f64,
475 ) -> Self {
476 Self::Singular {
477 pivot_col,
478 reason: SingularityReason::Numerical {
479 factorization,
480 pivot_magnitude,
481 tolerance,
482 },
483 }
484 }
485
486 #[inline]
489 #[must_use]
490 pub const fn non_finite_input_matrix(row: usize, col: usize) -> Self {
491 Self::NonFinite {
492 location: NonFiniteLocation::MatrixCell { row, col },
493 origin: NonFiniteOrigin::Input,
494 }
495 }
496
497 #[inline]
500 #[must_use]
501 pub const fn non_finite_input_vector(index: usize) -> Self {
502 Self::NonFinite {
503 location: NonFiniteLocation::VectorEntry { index },
504 origin: NonFiniteOrigin::Input,
505 }
506 }
507
508 #[inline]
511 #[must_use]
512 pub const fn non_finite_input_scalar() -> Self {
513 Self::NonFinite {
514 location: NonFiniteLocation::Scalar,
515 origin: NonFiniteOrigin::Input,
516 }
517 }
518
519 #[inline]
522 #[must_use]
523 pub const fn non_finite_input_interval_bound(bound: IntervalBound) -> Self {
524 Self::NonFinite {
525 location: NonFiniteLocation::IntervalBound { bound },
526 origin: NonFiniteOrigin::Input,
527 }
528 }
529
530 #[inline]
533 #[must_use]
534 pub const fn non_finite_input_interval_operand(operand: IntervalOperand) -> Self {
535 Self::NonFinite {
536 location: NonFiniteLocation::IntervalOperand { operand },
537 origin: NonFiniteOrigin::Input,
538 }
539 }
540
541 #[inline]
544 #[must_use]
545 pub const fn non_finite_computation_matrix(
546 operation: ArithmeticOperation,
547 row: usize,
548 col: usize,
549 ) -> Self {
550 Self::NonFinite {
551 location: NonFiniteLocation::MatrixCell { row, col },
552 origin: NonFiniteOrigin::Computation { operation },
553 }
554 }
555
556 #[inline]
559 #[must_use]
560 pub const fn non_finite_computation_step(operation: ArithmeticOperation, index: usize) -> Self {
561 Self::NonFinite {
562 location: NonFiniteLocation::Step { index },
563 origin: NonFiniteOrigin::Computation { operation },
564 }
565 }
566
567 #[inline]
570 #[must_use]
571 pub const fn non_finite_computation_scalar(operation: ArithmeticOperation) -> Self {
572 Self::NonFinite {
573 location: NonFiniteLocation::Scalar,
574 origin: NonFiniteOrigin::Computation { operation },
575 }
576 }
577
578 #[inline]
581 #[must_use]
582 pub const fn interval_range_exhausted(operation: ArithmeticOperation) -> Self {
583 Self::IntervalRangeExhausted { operation }
584 }
585
586 #[inline]
589 #[must_use]
590 pub const fn unrepresentable(index: Option<usize>, reason: UnrepresentableReason) -> Self {
591 Self::Unrepresentable { index, reason }
592 }
593
594 #[inline]
597 #[must_use]
598 pub const fn inverted_interval(lower: f64, upper: f64) -> Self {
599 Self::InvertedInterval { lower, upper }
600 }
601
602 #[inline]
605 #[must_use]
606 pub const fn unrepresentable_reason(&self) -> Option<UnrepresentableReason> {
607 match self {
608 Self::Unrepresentable { reason, .. } => Some(*reason),
609 _ => None,
610 }
611 }
612
613 #[inline]
616 #[must_use]
617 pub const fn requires_rounding(&self) -> bool {
618 matches!(
619 self,
620 Self::Unrepresentable {
621 reason: UnrepresentableReason::RequiresRounding,
622 ..
623 }
624 )
625 }
626
627 #[inline]
630 #[must_use]
631 pub const fn determinant_scale_overflow(dim: usize, min_exponent: i32) -> Self {
632 Self::DeterminantScaleOverflow { dim, min_exponent }
633 }
634
635 #[inline]
638 #[must_use]
639 pub const fn unsupported_dimension(requested: usize, max: usize) -> Self {
640 Self::UnsupportedDimension { requested, max }
641 }
642
643 #[inline]
646 #[must_use]
647 pub const fn index_out_of_bounds(row: usize, col: usize, dim: usize) -> Self {
648 Self::IndexOutOfBounds { row, col, dim }
649 }
650
651 #[inline]
659 #[must_use]
660 pub const fn invalid_tolerance(value: f64) -> Self {
661 let reason = if value.is_finite() {
662 InvalidToleranceReason::Negative
663 } else {
664 InvalidToleranceReason::NotFinite
665 };
666 Self::InvalidTolerance { value, reason }
667 }
668
669 #[inline]
673 #[must_use]
674 pub const fn asymmetric(
675 row: usize,
676 col: usize,
677 dim: usize,
678 upper: f64,
679 lower: f64,
680 allowed_abs_diff: f64,
681 ) -> Self {
682 Self::Asymmetric {
683 row,
684 col,
685 dim,
686 upper,
687 lower,
688 allowed_abs_diff,
689 }
690 }
691
692 #[inline]
695 #[must_use]
696 pub const fn not_positive_semidefinite_negative(pivot_col: usize, value: f64) -> Self {
697 Self::NotPositiveSemidefinite {
698 pivot_col,
699 violation: PositiveSemidefiniteViolation::NegativePivot { value },
700 }
701 }
702
703 #[inline]
707 #[must_use]
708 pub const fn not_positive_semidefinite_zero_coupling(
709 pivot_col: usize,
710 row: usize,
711 value: f64,
712 ) -> Self {
713 Self::NotPositiveSemidefinite {
714 pivot_col,
715 violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
716 }
717 }
718}
719
720fn write_non_finite_location(
724 f: &mut fmt::Formatter<'_>,
725 location: NonFiniteLocation,
726) -> fmt::Result {
727 match location {
728 NonFiniteLocation::MatrixCell { row, col } => {
729 write!(f, "matrix cell ({row}, {col})")
730 }
731 NonFiniteLocation::VectorEntry { index } => write!(f, "vector entry {index}"),
732 NonFiniteLocation::Step { index } => write!(f, "step {index}"),
733 NonFiniteLocation::IntervalBound {
734 bound: IntervalBound::Lower,
735 } => f.write_str("interval lower bound"),
736 NonFiniteLocation::IntervalBound {
737 bound: IntervalBound::Upper,
738 } => f.write_str("interval upper bound"),
739 NonFiniteLocation::IntervalOperand {
740 operand: IntervalOperand::Left,
741 } => f.write_str("left interval operand"),
742 NonFiniteLocation::IntervalOperand {
743 operand: IntervalOperand::Right,
744 } => f.write_str("right interval operand"),
745 NonFiniteLocation::Scalar => f.write_str("scalar value"),
746 }
747}
748
749fn write_non_finite(
752 f: &mut fmt::Formatter<'_>,
753 location: NonFiniteLocation,
754 origin: NonFiniteOrigin,
755) -> fmt::Result {
756 match (location, origin) {
757 (NonFiniteLocation::Scalar, NonFiniteOrigin::Input) => {
758 f.write_str("non-finite scalar input")
759 }
760 (NonFiniteLocation::Scalar, NonFiniteOrigin::Computation { operation }) => {
761 write!(f, "non-finite scalar result computed during {operation}")
762 }
763 (location, NonFiniteOrigin::Input) => {
764 f.write_str("non-finite input value at ")?;
765 write_non_finite_location(f, location)
766 }
767 (location, NonFiniteOrigin::Computation { operation }) => {
768 write!(f, "non-finite value computed during {operation} at ")?;
769 write_non_finite_location(f, location)
770 }
771 }
772}
773
774impl fmt::Display for LaError {
775 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
776 match *self {
777 Self::Singular {
778 pivot_col,
779 reason: SingularityReason::Exact,
780 } => write!(f, "matrix is exactly singular at pivot column {pivot_col}"),
781 Self::Singular {
782 pivot_col,
783 reason:
784 SingularityReason::Numerical {
785 factorization,
786 pivot_magnitude,
787 tolerance,
788 },
789 } => write!(
790 f,
791 "matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}"
792 ),
793 Self::NonFinite { location, origin } => write_non_finite(f, location, origin),
794 Self::IntervalRangeExhausted { operation } => write!(
795 f,
796 "exact-real intermediate or result of {operation} has no enclosure with finite binary64 endpoints"
797 ),
798 Self::Unrepresentable {
799 index: Some(index),
800 reason: UnrepresentableReason::RequiresRounding,
801 } => write!(
802 f,
803 "exact result requires rounding to fit finite f64 at index {index}"
804 ),
805 Self::Unrepresentable {
806 index: None,
807 reason: UnrepresentableReason::RequiresRounding,
808 } => f.write_str("exact result requires rounding to fit finite f64"),
809 Self::Unrepresentable {
810 index: Some(index),
811 reason: UnrepresentableReason::NotFinite,
812 } => write!(
813 f,
814 "exact result has no finite f64 representation after rounding at index {index}"
815 ),
816 Self::Unrepresentable {
817 index: None,
818 reason: UnrepresentableReason::NotFinite,
819 } => f.write_str("exact result has no finite f64 representation after rounding"),
820 Self::InvertedInterval { lower, upper } => write!(
821 f,
822 "invalid interval bounds [{lower}, {upper}]; expected lower <= upper"
823 ),
824 Self::DeterminantScaleOverflow { dim, min_exponent } => write!(
825 f,
826 "exact determinant scale exponent overflows for dimension {dim} with minimum entry exponent {min_exponent}"
827 ),
828 Self::UnsupportedDimension { requested, max } => write!(
829 f,
830 "unsupported matrix dimension {requested}; maximum supported dimension is {max}"
831 ),
832 Self::IndexOutOfBounds { row, col, dim } => write!(
833 f,
834 "matrix index ({row}, {col}) is out of bounds for dimension {dim}"
835 ),
836 Self::InvalidTolerance {
837 value,
838 reason: InvalidToleranceReason::Negative,
839 } => write!(f, "invalid tolerance {value}; expected value >= 0"),
840 Self::InvalidTolerance {
841 value,
842 reason: InvalidToleranceReason::NotFinite,
843 } => write!(f, "invalid tolerance {value}; expected a finite value"),
844 Self::Asymmetric {
845 row,
846 col,
847 dim,
848 upper,
849 lower,
850 allowed_abs_diff,
851 } => write!(
852 f,
853 "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}"
854 ),
855 Self::NotPositiveSemidefinite {
856 pivot_col,
857 violation: PositiveSemidefiniteViolation::NegativePivot { value },
858 } => write!(
859 f,
860 "LDLT rejected the matrix at pivot column {pivot_col}: computed diagonal value {value} < 0"
861 ),
862 Self::NotPositiveSemidefinite {
863 pivot_col,
864 violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
865 } => write!(
866 f,
867 "LDLT rejected the matrix at pivot column {pivot_col}: computed zero diagonal has non-zero coupling at row {row} with value {value}"
868 ),
869 }
870 }
871}
872
873impl std::error::Error for LaError {}
874
875#[cfg(test)]
876mod tests {
877 use std::error::Error;
878
879 use super::*;
880 use crate::MAX_STACK_MATRIX_DISPATCH_DIM;
881
882 #[test]
883 fn category_displays_are_concise() {
884 assert_eq!(FactorizationKind::Lu.to_string(), "LU");
885 assert_eq!(FactorizationKind::Ldlt.to_string(), "LDLT");
886 assert_eq!(
887 ArithmeticOperation::MatrixInfinityNorm.to_string(),
888 "matrix infinity norm"
889 );
890 assert_eq!(
891 ArithmeticOperation::SymmetryCheck.to_string(),
892 "symmetry check"
893 );
894 assert_eq!(
895 ArithmeticOperation::LuFactorization.to_string(),
896 "LU factorization"
897 );
898 assert_eq!(
899 ArithmeticOperation::LdltFactorization.to_string(),
900 "LDLT factorization"
901 );
902 assert_eq!(ArithmeticOperation::LuSolve.to_string(), "LU solve");
903 assert_eq!(ArithmeticOperation::LdltSolve.to_string(), "LDLT solve");
904 assert_eq!(ArithmeticOperation::Determinant.to_string(), "determinant");
905 assert_eq!(
906 ArithmeticOperation::DeterminantErrorBound.to_string(),
907 "determinant error bound"
908 );
909 assert_eq!(
910 ArithmeticOperation::VectorDotProduct.to_string(),
911 "vector dot product"
912 );
913 assert_eq!(
914 ArithmeticOperation::VectorDotDifference.to_string(),
915 "vector dot difference"
916 );
917 assert_eq!(
918 ArithmeticOperation::VectorSquaredNorm.to_string(),
919 "vector squared norm"
920 );
921 assert_eq!(
922 ArithmeticOperation::VectorNorm.to_string(),
923 "vector Euclidean norm"
924 );
925 assert_eq!(
926 ArithmeticOperation::IntervalAddition.to_string(),
927 "interval addition"
928 );
929 assert_eq!(
930 ArithmeticOperation::IntervalSubtraction.to_string(),
931 "interval subtraction"
932 );
933 assert_eq!(
934 ArithmeticOperation::IntervalMultiplication.to_string(),
935 "interval multiplication"
936 );
937 assert_eq!(
938 ArithmeticOperation::IntervalSquare.to_string(),
939 "interval square"
940 );
941 assert_eq!(
942 ArithmeticOperation::IntervalDeterminant.to_string(),
943 "interval determinant"
944 );
945 }
946
947 #[test]
948 fn singular_constructors_and_displays_preserve_reason() {
949 let exact = LaError::singular_exact(3);
950 assert_eq!(
951 exact,
952 LaError::Singular {
953 pivot_col: 3,
954 reason: SingularityReason::Exact,
955 }
956 );
957 assert_eq!(
958 exact.to_string(),
959 "matrix is exactly singular at pivot column 3"
960 );
961
962 let numerical = LaError::singular_numerical(2, FactorizationKind::Lu, 1e-14, 1e-12);
963 assert_eq!(
964 numerical,
965 LaError::Singular {
966 pivot_col: 2,
967 reason: SingularityReason::Numerical {
968 factorization: FactorizationKind::Lu,
969 pivot_magnitude: 1e-14,
970 tolerance: 1e-12,
971 },
972 }
973 );
974 assert_eq!(
975 numerical.to_string(),
976 "matrix is numerically singular during LU factorization at pivot column 2: pivot magnitude 0.00000000000001 <= tolerance 0.000000000001"
977 );
978 }
979
980 #[test]
981 fn non_finite_constructors_preserve_location_and_origin() {
982 assert_eq!(
983 LaError::non_finite_input_matrix(1, 2),
984 LaError::NonFinite {
985 location: NonFiniteLocation::MatrixCell { row: 1, col: 2 },
986 origin: NonFiniteOrigin::Input,
987 }
988 );
989 assert_eq!(
990 LaError::non_finite_input_vector(3).to_string(),
991 "non-finite input value at vector entry 3"
992 );
993 assert_eq!(
994 LaError::non_finite_input_scalar().to_string(),
995 "non-finite scalar input"
996 );
997 assert_eq!(
998 LaError::non_finite_input_interval_bound(IntervalBound::Upper).to_string(),
999 "non-finite input value at interval upper bound"
1000 );
1001 assert_eq!(
1002 LaError::non_finite_input_interval_bound(IntervalBound::Lower).to_string(),
1003 "non-finite input value at interval lower bound"
1004 );
1005 assert_eq!(
1006 LaError::non_finite_input_interval_operand(IntervalOperand::Left).to_string(),
1007 "non-finite input value at left interval operand"
1008 );
1009 assert_eq!(
1010 LaError::non_finite_input_interval_operand(IntervalOperand::Right).to_string(),
1011 "non-finite input value at right interval operand"
1012 );
1013 assert_eq!(
1014 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 2, 1)
1015 .to_string(),
1016 "non-finite value computed during LU factorization at matrix cell (2, 1)"
1017 );
1018 assert_eq!(
1019 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1).to_string(),
1020 "non-finite value computed during LU solve at step 1"
1021 );
1022 assert_eq!(
1023 LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant).to_string(),
1024 "non-finite scalar result computed during determinant"
1025 );
1026 }
1027
1028 #[test]
1029 fn unrepresentable_helpers_preserve_recovery_reason() {
1030 let rounding = LaError::unrepresentable(Some(2), UnrepresentableReason::RequiresRounding);
1031 let scalar_rounding =
1032 LaError::unrepresentable(None, UnrepresentableReason::RequiresRounding);
1033 let indexed_not_finite =
1034 LaError::unrepresentable(Some(2), UnrepresentableReason::NotFinite);
1035 let not_finite = LaError::unrepresentable(None, UnrepresentableReason::NotFinite);
1036 assert_eq!(
1037 rounding.unrepresentable_reason(),
1038 Some(UnrepresentableReason::RequiresRounding)
1039 );
1040 assert!(rounding.requires_rounding());
1041 assert_eq!(
1042 rounding.to_string(),
1043 "exact result requires rounding to fit finite f64 at index 2"
1044 );
1045 assert_eq!(
1046 scalar_rounding.to_string(),
1047 "exact result requires rounding to fit finite f64"
1048 );
1049 assert_eq!(
1050 indexed_not_finite.to_string(),
1051 "exact result has no finite f64 representation after rounding at index 2"
1052 );
1053 assert_eq!(
1054 not_finite.to_string(),
1055 "exact result has no finite f64 representation after rounding"
1056 );
1057 assert!(!not_finite.requires_rounding());
1058 assert_eq!(LaError::singular_exact(0).unrepresentable_reason(), None);
1059 }
1060
1061 #[test]
1062 fn invalid_tolerance_classifies_non_finite_before_negative() {
1063 assert_eq!(
1064 LaError::invalid_tolerance(-1.0),
1065 LaError::InvalidTolerance {
1066 value: -1.0,
1067 reason: InvalidToleranceReason::Negative,
1068 }
1069 );
1070 assert_eq!(
1071 LaError::invalid_tolerance(f64::NEG_INFINITY),
1072 LaError::InvalidTolerance {
1073 value: f64::NEG_INFINITY,
1074 reason: InvalidToleranceReason::NotFinite,
1075 }
1076 );
1077 assert_eq!(
1078 LaError::invalid_tolerance(-1.0).to_string(),
1079 "invalid tolerance -1; expected value >= 0"
1080 );
1081 assert_eq!(
1082 LaError::invalid_tolerance(f64::NEG_INFINITY).to_string(),
1083 "invalid tolerance -inf; expected a finite value"
1084 );
1085 }
1086
1087 #[test]
1088 fn inverted_interval_error_preserves_both_bounds() {
1089 let error = LaError::inverted_interval(2.0, 1.0);
1090 assert_eq!(
1091 error,
1092 LaError::InvertedInterval {
1093 lower: 2.0,
1094 upper: 1.0,
1095 }
1096 );
1097 assert_eq!(
1098 error.to_string(),
1099 "invalid interval bounds [2, 1]; expected lower <= upper"
1100 );
1101 }
1102
1103 #[test]
1104 fn interval_range_error_preserves_operation() {
1105 let error = LaError::interval_range_exhausted(ArithmeticOperation::IntervalSquare);
1106 assert_eq!(
1107 error,
1108 LaError::IntervalRangeExhausted {
1109 operation: ArithmeticOperation::IntervalSquare,
1110 }
1111 );
1112 assert_eq!(
1113 error.to_string(),
1114 "exact-real intermediate or result of interval square has no enclosure with finite binary64 endpoints"
1115 );
1116 }
1117
1118 #[test]
1119 fn asymmetric_error_retains_observed_values_and_bound() {
1120 let err = LaError::asymmetric(0, 2, 3, 1.0, 1.5, 1e-12);
1121 assert_eq!(
1122 err,
1123 LaError::Asymmetric {
1124 row: 0,
1125 col: 2,
1126 dim: 3,
1127 upper: 1.0,
1128 lower: 1.5,
1129 allowed_abs_diff: 1e-12,
1130 }
1131 );
1132 assert_eq!(
1133 err.to_string(),
1134 "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"
1135 );
1136 }
1137
1138 #[test]
1139 fn positive_semidefinite_errors_preserve_distinct_violations() {
1140 assert_eq!(
1141 LaError::not_positive_semidefinite_negative(1, -3.0).to_string(),
1142 "LDLT rejected the matrix at pivot column 1: computed diagonal value -3 < 0"
1143 );
1144 assert_eq!(
1145 LaError::not_positive_semidefinite_zero_coupling(0, 1, 2.0).to_string(),
1146 "LDLT rejected the matrix at pivot column 0: computed zero diagonal has non-zero coupling at row 1 with value 2"
1147 );
1148 }
1149
1150 #[test]
1151 fn remaining_helpers_and_displays_preserve_fields() {
1152 assert_eq!(
1153 LaError::determinant_scale_overflow(3, -1074).to_string(),
1154 "exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
1155 );
1156 assert_eq!(
1157 LaError::unsupported_dimension(8, MAX_STACK_MATRIX_DISPATCH_DIM).to_string(),
1158 "unsupported matrix dimension 8; maximum supported dimension is 7"
1159 );
1160 assert_eq!(
1161 LaError::index_out_of_bounds(3, 0, 3).to_string(),
1162 "matrix index (3, 0) is out of bounds for dimension 3"
1163 );
1164 }
1165
1166 #[test]
1167 fn is_std_error_with_no_source() {
1168 let err = LaError::singular_exact(0);
1169 let error: &dyn Error = &err;
1170 assert!(error.source().is_none());
1171 }
1172}