1#![forbid(unsafe_code)]
2
3use crate::rounding::{compare_product_with_rounded, two_sum_error};
14use crate::{ArithmeticOperation, IntervalBound, IntervalOperand, LaError, Matrix};
15
16pub const MAX_INTERVAL_MATRIX_DIM: usize = 7;
23
24#[must_use]
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct Interval {
50 lower: f64,
51 upper: f64,
52}
53
54#[must_use]
60#[non_exhaustive]
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum IntervalDeterminantSign {
63 Positive,
65 Negative,
67 Zero,
69 Inconclusive,
71}
72
73#[must_use]
94#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct IntervalMatrix<const D: usize> {
96 rows: [[Interval; D]; D],
97}
98
99#[inline]
101const fn canonical_zero(value: f64) -> f64 {
102 if value == 0.0 { 0.0 } else { value }
103}
104
105#[inline]
108const fn rounded_add_bounds(
109 left: f64,
110 right: f64,
111 operation: ArithmeticOperation,
112) -> Result<(f64, f64), LaError> {
113 let rounded = left + right;
114 if !rounded.is_finite() {
115 return Err(LaError::interval_range_exhausted(operation));
116 }
117
118 let error = two_sum_error(left, right, rounded);
119 if !error.is_finite() {
120 return Err(LaError::non_finite_computation_scalar(operation));
121 }
122 let (lower, upper) = if error < 0.0 {
123 (rounded.next_down(), rounded)
124 } else if error > 0.0 {
125 (rounded, rounded.next_up())
126 } else {
127 (rounded, rounded)
128 };
129 if !lower.is_finite() || !upper.is_finite() {
130 return Err(LaError::interval_range_exhausted(operation));
131 }
132
133 Ok((canonical_zero(lower), canonical_zero(upper)))
134}
135
136#[inline]
139const fn rounded_product_bounds(
140 left: f64,
141 right: f64,
142 operation: ArithmeticOperation,
143) -> Result<(f64, f64), LaError> {
144 if left == 0.0 || right == 0.0 {
145 return Ok((0.0, 0.0));
146 }
147
148 let rounded = left * right;
149 if !rounded.is_finite() {
150 return Err(LaError::interval_range_exhausted(operation));
151 }
152
153 let relation = compare_product_with_rounded(left, right, rounded);
154 let (lower, upper) = if relation < 0 {
155 (rounded.next_down(), rounded)
156 } else if relation > 0 {
157 (rounded, rounded.next_up())
158 } else {
159 (rounded, rounded)
160 };
161 if !lower.is_finite() || !upper.is_finite() {
162 return Err(LaError::interval_range_exhausted(operation));
163 }
164
165 Ok((canonical_zero(lower), canonical_zero(upper)))
166}
167
168impl Interval {
169 pub const ZERO: Self = Self {
171 lower: 0.0,
172 upper: 0.0,
173 };
174
175 pub const ONE: Self = Self {
177 lower: 1.0,
178 upper: 1.0,
179 };
180
181 #[inline]
206 pub const fn try_new(lower: f64, upper: f64) -> Result<Self, LaError> {
207 if !lower.is_finite() {
208 return Err(LaError::non_finite_input_interval_bound(
209 IntervalBound::Lower,
210 ));
211 }
212 if !upper.is_finite() {
213 return Err(LaError::non_finite_input_interval_bound(
214 IntervalBound::Upper,
215 ));
216 }
217 if lower > upper {
218 return Err(LaError::inverted_interval(lower, upper));
219 }
220 Ok(Self::new_unchecked(lower, upper))
221 }
222
223 #[inline]
244 pub const fn point(value: f64) -> Result<Self, LaError> {
245 match Self::try_new(value, value) {
246 Ok(interval) => Ok(interval),
247 Err(LaError::NonFinite { .. }) => Err(LaError::non_finite_input_scalar()),
248 Err(error) => Err(error),
249 }
250 }
251
252 #[inline]
280 pub const fn try_from_subtraction(left: f64, right: f64) -> Result<Self, LaError> {
281 if !left.is_finite() {
282 return Err(LaError::non_finite_input_interval_operand(
283 IntervalOperand::Left,
284 ));
285 }
286 if !right.is_finite() {
287 return Err(LaError::non_finite_input_interval_operand(
288 IntervalOperand::Right,
289 ));
290 }
291 match rounded_add_bounds(left, -right, ArithmeticOperation::IntervalSubtraction) {
292 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
293 Err(error) => Err(error),
294 }
295 }
296
297 #[inline]
299 #[must_use]
300 pub const fn lower(self) -> f64 {
301 self.lower
302 }
303
304 #[inline]
306 #[must_use]
307 pub const fn upper(self) -> f64 {
308 self.upper
309 }
310
311 #[inline]
313 #[must_use]
314 pub const fn contains(self, value: f64) -> bool {
315 value.is_finite() && self.lower <= value && value <= self.upper
316 }
317
318 #[inline]
336 pub const fn try_add(&self, other: &Self) -> Result<Self, LaError> {
337 self.try_add_for(other, ArithmeticOperation::IntervalAddition)
338 }
339
340 #[inline]
361 pub const fn try_mul(&self, other: &Self) -> Result<Self, LaError> {
362 self.try_mul_for(other, ArithmeticOperation::IntervalMultiplication)
363 }
364
365 #[inline]
378 pub const fn negate(&self) -> Self {
379 Self::new_unchecked(-self.upper, -self.lower)
380 }
381
382 #[inline]
404 pub const fn try_square(&self) -> Result<Self, LaError> {
405 let operation = ArithmeticOperation::IntervalSquare;
406 let left_square = match rounded_product_bounds(self.lower, self.lower, operation) {
407 Ok(bounds) => bounds,
408 Err(error) => return Err(error),
409 };
410 let right_square = match rounded_product_bounds(self.upper, self.upper, operation) {
411 Ok(bounds) => bounds,
412 Err(error) => return Err(error),
413 };
414 let lower = if self.lower <= 0.0 && self.upper >= 0.0 {
415 0.0
416 } else if left_square.0 < right_square.0 {
417 left_square.0
418 } else {
419 right_square.0
420 };
421 let upper = if left_square.1 > right_square.1 {
422 left_square.1
423 } else {
424 right_square.1
425 };
426 Ok(Self::new_unchecked(lower, upper))
427 }
428
429 #[inline]
431 const fn new_unchecked(lower: f64, upper: f64) -> Self {
432 Self {
433 lower: canonical_zero(lower),
434 upper: canonical_zero(upper),
435 }
436 }
437
438 #[inline]
440 const fn try_add_for(
441 &self,
442 other: &Self,
443 operation: ArithmeticOperation,
444 ) -> Result<Self, LaError> {
445 if self.is_zero() {
446 return Ok(*other);
447 }
448 if other.is_zero() {
449 return Ok(*self);
450 }
451
452 let lower = match rounded_add_bounds(self.lower, other.lower, operation) {
453 Ok((lower, _)) => lower,
454 Err(error) => return Err(error),
455 };
456 let upper = match rounded_add_bounds(self.upper, other.upper, operation) {
457 Ok((_, upper)) => upper,
458 Err(error) => return Err(error),
459 };
460 Ok(Self::new_unchecked(lower, upper))
461 }
462
463 #[inline]
465 const fn try_mul_for(
466 &self,
467 other: &Self,
468 operation: ArithmeticOperation,
469 ) -> Result<Self, LaError> {
470 if self.is_zero() || other.is_zero() {
471 return Ok(Self::ZERO);
472 }
473 if self.is_one() {
474 return Ok(*other);
475 }
476 if other.is_one() {
477 return Ok(*self);
478 }
479 if self.is_point() && other.is_point() {
480 return match rounded_product_bounds(self.lower, other.lower, operation) {
481 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
482 Err(error) => Err(error),
483 };
484 }
485
486 self.try_mul_by_sign(other, operation)
487 }
488
489 #[inline]
491 const fn try_mul_by_sign(
492 &self,
493 other: &Self,
494 operation: ArithmeticOperation,
495 ) -> Result<Self, LaError> {
496 let self_nonnegative = self.lower >= 0.0;
497 let self_nonpositive = self.upper <= 0.0;
498 let other_nonnegative = other.lower >= 0.0;
499 let other_nonpositive = other.upper <= 0.0;
500
501 if self_nonnegative {
502 if other_nonnegative {
503 return Self::try_product_extrema(
504 (self.lower, other.lower),
505 (self.upper, other.upper),
506 operation,
507 );
508 }
509 if other_nonpositive {
510 return Self::try_product_extrema(
511 (self.upper, other.lower),
512 (self.lower, other.upper),
513 operation,
514 );
515 }
516 return Self::try_product_extrema(
517 (self.upper, other.lower),
518 (self.upper, other.upper),
519 operation,
520 );
521 }
522 if self_nonpositive {
523 if other_nonnegative {
524 return Self::try_product_extrema(
525 (self.lower, other.upper),
526 (self.upper, other.lower),
527 operation,
528 );
529 }
530 if other_nonpositive {
531 return Self::try_product_extrema(
532 (self.upper, other.upper),
533 (self.lower, other.lower),
534 operation,
535 );
536 }
537 return Self::try_product_extrema(
538 (self.lower, other.upper),
539 (self.lower, other.lower),
540 operation,
541 );
542 }
543 if other_nonnegative {
544 return Self::try_product_extrema(
545 (self.lower, other.upper),
546 (self.upper, other.upper),
547 operation,
548 );
549 }
550 if other_nonpositive {
551 return Self::try_product_extrema(
552 (self.upper, other.lower),
553 (self.lower, other.lower),
554 operation,
555 );
556 }
557
558 let lower_left = match rounded_product_bounds(self.lower, other.upper, operation) {
559 Ok(bounds) => bounds,
560 Err(error) => return Err(error),
561 };
562 let lower_right = match rounded_product_bounds(self.upper, other.lower, operation) {
563 Ok(bounds) => bounds,
564 Err(error) => return Err(error),
565 };
566 let upper_left = match rounded_product_bounds(self.lower, other.lower, operation) {
567 Ok(bounds) => bounds,
568 Err(error) => return Err(error),
569 };
570 let upper_right = match rounded_product_bounds(self.upper, other.upper, operation) {
571 Ok(bounds) => bounds,
572 Err(error) => return Err(error),
573 };
574 let lower = if lower_left.0 < lower_right.0 {
575 lower_left.0
576 } else {
577 lower_right.0
578 };
579 let upper = if upper_left.1 > upper_right.1 {
580 upper_left.1
581 } else {
582 upper_right.1
583 };
584 Ok(Self::new_unchecked(lower, upper))
585 }
586
587 #[inline]
589 const fn try_product_extrema(
590 lower_factors: (f64, f64),
591 upper_factors: (f64, f64),
592 operation: ArithmeticOperation,
593 ) -> Result<Self, LaError> {
594 let lower = match rounded_product_bounds(lower_factors.0, lower_factors.1, operation) {
595 Ok((lower, _)) => lower,
596 Err(error) => return Err(error),
597 };
598 let upper = match rounded_product_bounds(upper_factors.0, upper_factors.1, operation) {
599 Ok((_, upper)) => upper,
600 Err(error) => return Err(error),
601 };
602 Ok(Self::new_unchecked(lower, upper))
603 }
604
605 #[inline]
607 const fn is_zero(&self) -> bool {
608 self.lower == 0.0 && self.upper == 0.0
609 }
610
611 #[inline]
613 const fn is_one(&self) -> bool {
614 self.lower.to_bits() == 1.0_f64.to_bits() && self.upper.to_bits() == 1.0_f64.to_bits()
615 }
616
617 #[inline]
619 const fn is_point(&self) -> bool {
620 self.lower.to_bits() == self.upper.to_bits()
621 }
622}
623
624impl Default for Interval {
625 #[inline]
626 fn default() -> Self {
627 Self::ZERO
628 }
629}
630
631impl<const D: usize> IntervalMatrix<D> {
632 #[inline]
636 pub const fn from_rows(rows: [[Interval; D]; D]) -> Self {
637 Self { rows }
638 }
639
640 #[inline]
650 pub const fn try_from_point_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
651 let mut intervals = [[Interval::ZERO; D]; D];
652 let mut row = 0;
653 while row < D {
654 let mut column = 0;
655 while column < D {
656 let value = rows[row][column];
657 if !value.is_finite() {
658 return Err(LaError::non_finite_input_matrix(row, column));
659 }
660 intervals[row][column] = Interval::new_unchecked(value, value);
661 column += 1;
662 }
663 row += 1;
664 }
665 Ok(Self::from_rows(intervals))
666 }
667
668 #[inline]
686 pub const fn from_matrix(matrix: &Matrix<D>) -> Self {
687 let matrix_rows = matrix.as_rows();
688 let mut intervals = [[Interval::ZERO; D]; D];
689 let mut row = 0;
690 while row < D {
691 let mut column = 0;
692 while column < D {
693 let value = matrix_rows[row][column];
694 intervals[row][column] = Interval::new_unchecked(value, value);
695 column += 1;
696 }
697 row += 1;
698 }
699 Self::from_rows(intervals)
700 }
701
702 #[inline]
704 pub const fn zero() -> Self {
705 Self::from_rows([[Interval::ZERO; D]; D])
706 }
707
708 #[inline]
710 pub const fn identity() -> Self {
711 let mut matrix = Self::zero();
712 let mut index = 0;
713 while index < D {
714 matrix.rows[index][index] = Interval::ONE;
715 index += 1;
716 }
717 matrix
718 }
719
720 #[inline]
722 pub const fn as_rows(&self) -> &[[Interval; D]; D] {
723 &self.rows
724 }
725
726 #[inline]
728 pub const fn into_rows(self) -> [[Interval; D]; D] {
729 self.rows
730 }
731
732 #[inline]
734 #[must_use]
735 pub const fn get(&self, row: usize, column: usize) -> Option<Interval> {
736 if row < D && column < D {
737 Some(self.rows[row][column])
738 } else {
739 None
740 }
741 }
742
743 #[inline]
750 pub const fn try_get(&self, row: usize, column: usize) -> Result<Interval, LaError> {
751 if row < D && column < D {
752 Ok(self.rows[row][column])
753 } else {
754 Err(LaError::index_out_of_bounds(row, column, D))
755 }
756 }
757
758 #[inline]
787 pub const fn set(&mut self, row: usize, column: usize, value: Interval) -> Result<(), LaError> {
788 if row >= D || column >= D {
789 return Err(LaError::index_out_of_bounds(row, column, D));
790 }
791 self.rows[row][column] = value;
792 Ok(())
793 }
794
795 #[inline]
829 pub const fn det(&self) -> Result<Interval, LaError> {
830 if D > MAX_INTERVAL_MATRIX_DIM {
831 return Err(LaError::unsupported_dimension(D, MAX_INTERVAL_MATRIX_DIM));
832 }
833
834 let state_count = 1_usize << D;
835 let mut partials = [Interval::ZERO; 1 << MAX_INTERVAL_MATRIX_DIM];
836 partials[0] = Interval::ONE;
837 let operation = ArithmeticOperation::IntervalDeterminant;
838
839 let mut subset = 1;
840 while subset < state_count {
841 let row = subset.count_ones() as usize - 1;
842 let mut sum = Interval::ZERO;
843 let mut column = 0;
844 while column < D {
845 let column_bit = 1_usize << column;
846 if subset & column_bit != 0 {
847 let previous = subset ^ column_bit;
848 let mut term =
849 match partials[previous].try_mul_for(&self.rows[row][column], operation) {
850 Ok(term) => term,
851 Err(error) => return Err(error),
852 };
853 let columns_after = (subset >> (column + 1)).count_ones();
854 if !columns_after.is_multiple_of(2) {
855 term = term.negate();
856 }
857 sum = match sum.try_add_for(&term, operation) {
858 Ok(next_sum) => next_sum,
859 Err(error) => return Err(error),
860 };
861 }
862 column += 1;
863 }
864 partials[subset] = sum;
865 subset += 1;
866 }
867
868 Ok(partials[state_count - 1])
869 }
870
871 #[inline]
900 pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
901 let determinant = match self.det() {
902 Ok(determinant) => determinant,
903 Err(error) => return Err(error),
904 };
905 if determinant.lower > 0.0 {
906 Ok(IntervalDeterminantSign::Positive)
907 } else if determinant.upper < 0.0 {
908 Ok(IntervalDeterminantSign::Negative)
909 } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
910 Ok(IntervalDeterminantSign::Zero)
911 } else {
912 Ok(IntervalDeterminantSign::Inconclusive)
913 }
914 }
915}
916
917impl<const D: usize> Default for IntervalMatrix<D> {
918 #[inline]
919 fn default() -> Self {
920 Self::zero()
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use core::assert_matches;
927
928 use pastey::paste;
929
930 use super::*;
931 use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
932
933 #[test]
934 fn point_and_bounds_enforce_interval_invariants() {
935 assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
936 assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
937 assert_matches!(
938 Interval::point(f64::NAN),
939 Err(LaError::NonFinite {
940 location: NonFiniteLocation::Scalar,
941 origin: NonFiniteOrigin::Input,
942 ..
943 })
944 );
945 assert_matches!(
946 Interval::try_new(2.0, 1.0),
947 Err(LaError::InvertedInterval {
948 lower: 2.0,
949 upper: 1.0,
950 ..
951 })
952 );
953 }
954
955 #[test]
956 fn constructors_preserve_non_finite_input_locations() {
957 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
958 assert_eq!(
959 Interval::try_new(value, 0.0),
960 Err(LaError::NonFinite {
961 location: NonFiniteLocation::IntervalBound {
962 bound: IntervalBound::Lower,
963 },
964 origin: NonFiniteOrigin::Input,
965 })
966 );
967 assert_eq!(
968 Interval::try_new(0.0, value),
969 Err(LaError::NonFinite {
970 location: NonFiniteLocation::IntervalBound {
971 bound: IntervalBound::Upper,
972 },
973 origin: NonFiniteOrigin::Input,
974 })
975 );
976 assert_eq!(
977 Interval::try_from_subtraction(value, 0.0),
978 Err(LaError::NonFinite {
979 location: NonFiniteLocation::IntervalOperand {
980 operand: IntervalOperand::Left,
981 },
982 origin: NonFiniteOrigin::Input,
983 })
984 );
985 assert_eq!(
986 Interval::try_from_subtraction(0.0, value),
987 Err(LaError::NonFinite {
988 location: NonFiniteLocation::IntervalOperand {
989 operand: IntervalOperand::Right,
990 },
991 origin: NonFiniteOrigin::Input,
992 })
993 );
994 }
995
996 assert_eq!(
997 Interval::try_new(f64::NAN, f64::INFINITY),
998 Err(LaError::NonFinite {
999 location: NonFiniteLocation::IntervalBound {
1000 bound: IntervalBound::Lower,
1001 },
1002 origin: NonFiniteOrigin::Input,
1003 })
1004 );
1005 assert_eq!(
1006 Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1007 Err(LaError::NonFinite {
1008 location: NonFiniteLocation::IntervalOperand {
1009 operand: IntervalOperand::Left,
1010 },
1011 origin: NonFiniteOrigin::Input,
1012 })
1013 );
1014
1015 let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1016 assert_eq!(
1017 IntervalMatrix::<2>::try_from_point_rows(rows),
1018 Err(LaError::NonFinite {
1019 location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1020 origin: NonFiniteOrigin::Input,
1021 })
1022 );
1023 }
1024
1025 #[test]
1026 fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1027 let one = Interval::point(1.0)?;
1028 let two = Interval::point(2.0)?;
1029 assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1030 assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1031 assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1032 assert_eq!(
1033 Interval::try_new(-2.0, -1.0)?.negate(),
1034 Interval::try_new(1.0, 2.0)?
1035 );
1036 Ok(())
1037 }
1038
1039 #[test]
1040 fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1041 let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1042 let rounded_subtraction = 1.0_f64 - 0.1;
1043 assert_eq!(
1044 subtraction,
1045 Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1046 );
1047
1048 let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1049 let rounded_product = 0.1_f64 * 0.2;
1050 assert_eq!(
1051 product,
1052 Interval::try_new(rounded_product.next_down(), rounded_product)?
1053 );
1054
1055 let below_one = 1.0 - f64::EPSILON;
1056 let above_one = 1.0 + f64::EPSILON;
1057 let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1058 assert_eq!(
1059 binade_boundary,
1060 Interval::try_new(1.0_f64.next_down(), 1.0)?
1061 );
1062 Ok(())
1063 }
1064
1065 #[test]
1066 fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1067 let next = 1.0_f64.next_up();
1068 let difference = Interval::try_from_subtraction(next, 1.0)?;
1069 assert_eq!(difference, Interval::point(f64::EPSILON)?);
1070 Ok(())
1071 }
1072
1073 #[test]
1074 fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1075 let least_subnormal = f64::from_bits(1);
1076 let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1077 assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1083 let error = Interval::point(f64::MAX)?
1084 .try_mul(&Interval::point(2.0)?)
1085 .unwrap_err();
1086 assert_eq!(
1087 error,
1088 LaError::IntervalRangeExhausted {
1089 operation: ArithmeticOperation::IntervalMultiplication,
1090 }
1091 );
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1097 let maximum = Interval::point(f64::MAX)?;
1098 let tiny = Interval::point(f64::MIN_POSITIVE)?;
1099 assert_eq!(
1100 maximum.try_add(&maximum),
1101 Err(LaError::IntervalRangeExhausted {
1102 operation: ArithmeticOperation::IntervalAddition,
1103 })
1104 );
1105 assert_eq!(
1106 maximum.try_add(&tiny),
1107 Err(LaError::IntervalRangeExhausted {
1108 operation: ArithmeticOperation::IntervalAddition,
1109 })
1110 );
1111 let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1112 assert_eq!(
1113 nonnegative.try_add(&nonnegative),
1114 Err(LaError::IntervalRangeExhausted {
1115 operation: ArithmeticOperation::IntervalAddition,
1116 })
1117 );
1118
1119 let finite_difference = maximum.try_add(&tiny.negate())?;
1120 assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1121 assert!(finite_difference.lower() < finite_difference.upper());
1122 Ok(())
1123 }
1124
1125 #[test]
1126 fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1127 assert_eq!(
1128 Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1129 Err(LaError::IntervalRangeExhausted {
1130 operation: ArithmeticOperation::IntervalSubtraction,
1131 })
1132 );
1133 assert_eq!(
1134 Interval::point(f64::MAX)?.try_square(),
1135 Err(LaError::IntervalRangeExhausted {
1136 operation: ArithmeticOperation::IntervalSquare,
1137 })
1138 );
1139 assert_eq!(
1140 Interval::try_new(-1.0, f64::MAX)?.try_square(),
1141 Err(LaError::IntervalRangeExhausted {
1142 operation: ArithmeticOperation::IntervalSquare,
1143 })
1144 );
1145 Ok(())
1146 }
1147
1148 #[test]
1149 fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1150 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1151 assert_eq!(
1152 matrix.det(),
1153 Err(LaError::IntervalRangeExhausted {
1154 operation: ArithmeticOperation::IntervalDeterminant,
1155 })
1156 );
1157
1158 let accumulating =
1159 IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1160 assert_eq!(
1161 accumulating.det(),
1162 Err(LaError::IntervalRangeExhausted {
1163 operation: ArithmeticOperation::IntervalDeterminant,
1164 })
1165 );
1166 assert_eq!(
1167 accumulating.det_sign(),
1168 Err(LaError::IntervalRangeExhausted {
1169 operation: ArithmeticOperation::IntervalDeterminant,
1170 })
1171 );
1172 Ok(())
1173 }
1174
1175 #[test]
1176 fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1177 {
1178 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1179 assert_eq!(
1180 matrix.det(),
1181 Err(LaError::IntervalRangeExhausted {
1182 operation: ArithmeticOperation::IntervalDeterminant,
1183 })
1184 );
1185 Ok(())
1186 }
1187
1188 #[test]
1189 fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1190 let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1191 assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1192 Ok(())
1193 }
1194
1195 #[test]
1196 fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1197 for (left, right, expected) in [
1198 ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1199 ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1200 ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1201 ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1202 ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1203 ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1204 ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1205 ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1206 ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1207 ] {
1208 let product = Interval::try_new(left.0, left.1)?
1209 .try_mul(&Interval::try_new(right.0, right.1)?)?;
1210 assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1211 }
1212
1213 assert_eq!(
1214 Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1215 Interval::ZERO
1216 );
1217 Ok(())
1218 }
1219
1220 #[test]
1221 fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1222 let half_maximum = f64::MAX / 2.0;
1223 for (left, right) in [
1224 ((half_maximum, f64::MAX), (-2.0, -1.0)),
1225 ((half_maximum, f64::MAX), (1.0, 2.0)),
1226 ((-f64::MAX, 1.0), (-1.0, 2.0)),
1227 ((-1.0, f64::MAX), (-2.0, 1.0)),
1228 ((-f64::MAX, 1.0), (-2.0, 1.0)),
1229 ((-1.0, f64::MAX), (-1.0, 2.0)),
1230 ] {
1231 let result =
1232 Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1233 assert_eq!(
1234 result,
1235 Err(LaError::IntervalRangeExhausted {
1236 operation: ArithmeticOperation::IntervalMultiplication,
1237 }),
1238 "left={left:?}, right={right:?}"
1239 );
1240 }
1241 Ok(())
1242 }
1243
1244 macro_rules! gen_interval_identity_tests {
1245 ($d:literal) => {
1246 paste! {
1247 #[test]
1248 fn [<interval_identity_sign_is_positive_ $d d>]() {
1249 let matrix = IntervalMatrix::<$d>::identity();
1250 assert_eq!(matrix.det(), Ok(Interval::ONE));
1251 assert_eq!(
1252 matrix.det_sign(),
1253 Ok(IntervalDeterminantSign::Positive)
1254 );
1255 }
1256 }
1257 };
1258 }
1259
1260 gen_interval_identity_tests!(2);
1261 gen_interval_identity_tests!(3);
1262 gen_interval_identity_tests!(4);
1263 gen_interval_identity_tests!(5);
1264 gen_interval_identity_tests!(6);
1265 gen_interval_identity_tests!(7);
1266
1267 #[test]
1268 fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1269 let swapped = IntervalMatrix::<3>::try_from_point_rows([
1270 [0.0, 1.0, 0.0],
1271 [1.0, 0.0, 0.0],
1272 [0.0, 0.0, 1.0],
1273 ])?;
1274 assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1275
1276 let singular = IntervalMatrix::<3>::try_from_point_rows([
1277 [1.0, 2.0, 3.0],
1278 [1.0, 2.0, 3.0],
1279 [0.0, 0.0, 1.0],
1280 ])?;
1281 assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1282 Ok(())
1283 }
1284
1285 #[test]
1286 fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1287 let matrix = IntervalMatrix::<2>::from_rows([
1288 [Interval::ONE, Interval::ZERO],
1289 [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1290 ]);
1291 assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1292 Ok(())
1293 }
1294
1295 #[test]
1296 fn determinant_rejects_dimensions_above_supported_stack_dp() {
1297 assert_matches!(
1298 IntervalMatrix::<8>::identity().det(),
1299 Err(LaError::UnsupportedDimension {
1300 requested: 8,
1301 max: MAX_INTERVAL_MATRIX_DIM,
1302 ..
1303 })
1304 );
1305 }
1306
1307 #[test]
1308 fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1309 let source = Matrix::<2>::identity();
1310 let mut intervals = IntervalMatrix::from_matrix(&source);
1311 let value = Interval::try_new(2.0, 3.0)?;
1312 intervals.set(0, 1, value)?;
1313 assert_eq!(intervals.get(0, 1), Some(value));
1314 assert_eq!(intervals.get(2, 0), None);
1315 assert_eq!(intervals.try_get(0, 1)?, value);
1316 assert_matches!(
1317 intervals.try_get(2, 0),
1318 Err(LaError::IndexOutOfBounds {
1319 row: 2,
1320 col: 0,
1321 dim: 2,
1322 ..
1323 })
1324 );
1325 assert_eq!(intervals.as_rows()[0][1], value);
1326 assert_eq!(intervals.into_rows()[0][1], value);
1327 Ok(())
1328 }
1329
1330 #[test]
1331 fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1332 let mut matrix = IntervalMatrix::<2>::identity();
1333 let before = matrix;
1334 let value = Interval::try_new(2.0, 3.0)?;
1335
1336 assert_eq!(
1337 matrix.set(2, 0, value),
1338 Err(LaError::IndexOutOfBounds {
1339 row: 2,
1340 col: 0,
1341 dim: 2,
1342 })
1343 );
1344 assert_eq!(matrix, before);
1345 assert_eq!(
1346 matrix.set(0, 2, value),
1347 Err(LaError::IndexOutOfBounds {
1348 row: 0,
1349 col: 2,
1350 dim: 2,
1351 })
1352 );
1353 assert_eq!(matrix, before);
1354 Ok(())
1355 }
1356}