1#![forbid(unsafe_code)]
2
3use crate::rounding::{compare_product_with_rounded, compare_quotient_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]
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct IntervalMatrix<const D: usize> {
99 rows: [[Interval; D]; D],
100}
101
102#[inline]
104const fn canonical_zero(value: f64) -> f64 {
105 if value == 0.0 { 0.0 } else { value }
106}
107
108#[inline]
111const fn rounded_add_bounds(
112 left: f64,
113 right: f64,
114 operation: ArithmeticOperation,
115) -> Result<(f64, f64), LaError> {
116 let rounded = left + right;
117 if !rounded.is_finite() {
118 return Err(LaError::interval_range_exhausted(operation));
119 }
120
121 let error = two_sum_error(left, right, rounded);
122 let (lower, upper) = if error < 0.0 {
124 (rounded.next_down(), rounded)
125 } else if error > 0.0 {
126 (rounded, rounded.next_up())
127 } else {
128 (rounded, rounded)
129 };
130 if !lower.is_finite() || !upper.is_finite() {
131 return Err(LaError::interval_range_exhausted(operation));
132 }
133
134 Ok((canonical_zero(lower), canonical_zero(upper)))
135}
136
137#[inline]
139const fn rounded_add_endpoint<const UPPER: bool>(
140 left: f64,
141 right: f64,
142 operation: ArithmeticOperation,
143) -> Result<f64, LaError> {
144 let rounded = left + right;
145 if !rounded.is_finite() {
146 return Err(LaError::interval_range_exhausted(operation));
147 }
148 let error = two_sum_error(left, right, rounded);
149 let endpoint = if UPPER && error > 0.0 {
150 rounded.next_up()
151 } else if !UPPER && error < 0.0 {
152 rounded.next_down()
153 } else {
154 rounded
155 };
156 if !endpoint.is_finite() {
157 return Err(LaError::interval_range_exhausted(operation));
158 }
159 Ok(canonical_zero(endpoint))
160}
161
162#[expect(
166 clippy::inline_always,
167 reason = "measured endpoint-call overhead in the interval determinant kernel"
168)]
169#[inline(always)]
170const fn rounded_product_bounds(
171 left: f64,
172 right: f64,
173 operation: ArithmeticOperation,
174) -> Result<(f64, f64), LaError> {
175 if left == 0.0 || right == 0.0 {
176 return Ok((0.0, 0.0));
177 }
178
179 let rounded = left * right;
180 if !rounded.is_finite() {
181 return Err(LaError::interval_range_exhausted(operation));
182 }
183
184 let relation = compare_product_with_rounded(left, right, rounded);
185 let (lower, upper) = if relation < 0 {
186 (rounded.next_down(), rounded)
187 } else if relation > 0 {
188 (rounded, rounded.next_up())
189 } else {
190 (rounded, rounded)
191 };
192 if !lower.is_finite() || !upper.is_finite() {
193 return Err(LaError::interval_range_exhausted(operation));
194 }
195
196 Ok((canonical_zero(lower), canonical_zero(upper)))
197}
198
199#[inline]
204const fn rounded_quotient_bounds(
205 numerator: f64,
206 denominator: f64,
207 operation: ArithmeticOperation,
208) -> Result<(f64, f64), LaError> {
209 let rounded = numerator / denominator;
210 if !rounded.is_finite() {
211 return Err(LaError::interval_range_exhausted(operation));
212 }
213 let relation = compare_quotient_with_rounded(numerator, denominator, rounded);
214 let (lower, upper) = if relation < 0 {
215 (rounded.next_down(), rounded)
216 } else if relation > 0 {
217 (rounded, rounded.next_up())
218 } else {
219 (rounded, rounded)
220 };
221 if !lower.is_finite() || !upper.is_finite() {
222 return Err(LaError::interval_range_exhausted(operation));
223 }
224 Ok((canonical_zero(lower), canonical_zero(upper)))
225}
226
227impl Interval {
228 pub const ZERO: Self = Self {
230 lower: 0.0,
231 upper: 0.0,
232 };
233
234 pub const ONE: Self = Self {
236 lower: 1.0,
237 upper: 1.0,
238 };
239
240 #[inline]
265 pub const fn try_new(lower: f64, upper: f64) -> Result<Self, LaError> {
266 if !lower.is_finite() {
267 return Err(LaError::non_finite_input_interval_bound(
268 IntervalBound::Lower,
269 ));
270 }
271 if !upper.is_finite() {
272 return Err(LaError::non_finite_input_interval_bound(
273 IntervalBound::Upper,
274 ));
275 }
276 if lower > upper {
277 return Err(LaError::inverted_interval(lower, upper));
278 }
279 Ok(Self::new_unchecked(lower, upper))
280 }
281
282 #[inline]
303 pub const fn point(value: f64) -> Result<Self, LaError> {
304 match Self::try_new(value, value) {
305 Ok(interval) => Ok(interval),
306 Err(LaError::NonFinite { .. }) => Err(LaError::non_finite_input_scalar()),
307 Err(error) => Err(error),
308 }
309 }
310
311 #[inline]
339 pub const fn try_from_subtraction(left: f64, right: f64) -> Result<Self, LaError> {
340 if !left.is_finite() {
341 return Err(LaError::non_finite_input_interval_operand(
342 IntervalOperand::Left,
343 ));
344 }
345 if !right.is_finite() {
346 return Err(LaError::non_finite_input_interval_operand(
347 IntervalOperand::Right,
348 ));
349 }
350 if right == 0.0 {
351 return Ok(Self::new_unchecked(left, left));
352 }
353 if left == 0.0 {
354 return Ok(Self::new_unchecked(-right, -right));
355 }
356 match rounded_add_bounds(left, -right, ArithmeticOperation::IntervalSubtraction) {
357 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
358 Err(error) => Err(error),
359 }
360 }
361
362 #[inline]
364 #[must_use]
365 pub const fn lower(self) -> f64 {
366 self.lower
367 }
368
369 #[inline]
371 #[must_use]
372 pub const fn upper(self) -> f64 {
373 self.upper
374 }
375
376 #[inline]
378 #[must_use]
379 pub const fn contains(self, value: f64) -> bool {
380 value.is_finite() && self.lower <= value && value <= self.upper
381 }
382
383 #[inline]
401 pub const fn try_add(&self, other: &Self) -> Result<Self, LaError> {
402 self.try_add_for(other, ArithmeticOperation::IntervalAddition)
403 }
404
405 #[inline]
426 pub const fn try_mul(&self, other: &Self) -> Result<Self, LaError> {
427 self.try_mul_for(other, ArithmeticOperation::IntervalMultiplication)
428 }
429
430 #[inline]
446 pub const fn try_recip(&self) -> Result<Self, LaError> {
447 Self::ONE.try_div_for(self, ArithmeticOperation::IntervalReciprocal)
448 }
449
450 #[inline]
474 pub const fn try_div(&self, other: &Self) -> Result<Self, LaError> {
475 self.try_div_for(other, ArithmeticOperation::IntervalDivision)
476 }
477
478 #[inline]
491 pub const fn negate(&self) -> Self {
492 Self::new_unchecked(-self.upper, -self.lower)
493 }
494
495 #[expect(
517 clippy::inline_always,
518 reason = "measured call overhead in lifted interval assembly"
519 )]
520 #[inline(always)]
521 pub const fn try_square(&self) -> Result<Self, LaError> {
522 let operation = ArithmeticOperation::IntervalSquare;
523 let left_square = match rounded_product_bounds(self.lower, self.lower, operation) {
524 Ok(bounds) => bounds,
525 Err(error) => return Err(error),
526 };
527 if self.is_point() {
528 return Ok(Self::new_unchecked(left_square.0, left_square.1));
529 }
530 let right_square = match rounded_product_bounds(self.upper, self.upper, operation) {
531 Ok(bounds) => bounds,
532 Err(error) => return Err(error),
533 };
534 let lower = if self.lower <= 0.0 && self.upper >= 0.0 {
535 0.0
536 } else if left_square.0 < right_square.0 {
537 left_square.0
538 } else {
539 right_square.0
540 };
541 let upper = if left_square.1 > right_square.1 {
542 left_square.1
543 } else {
544 right_square.1
545 };
546 Ok(Self::new_unchecked(lower, upper))
547 }
548
549 #[inline]
551 const fn new_unchecked(lower: f64, upper: f64) -> Self {
552 Self {
553 lower: canonical_zero(lower),
554 upper: canonical_zero(upper),
555 }
556 }
557
558 #[expect(
560 clippy::inline_always,
561 reason = "measured result-copy overhead in the interval determinant kernel"
562 )]
563 #[inline(always)]
564 const fn try_add_for(
565 &self,
566 other: &Self,
567 operation: ArithmeticOperation,
568 ) -> Result<Self, LaError> {
569 if self.is_zero() {
570 return Ok(*other);
571 }
572 if other.is_zero() {
573 return Ok(*self);
574 }
575 if self.is_point() && other.is_point() {
576 return match rounded_add_bounds(self.lower, other.lower, operation) {
577 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
578 Err(error) => Err(error),
579 };
580 }
581
582 let lower = match rounded_add_endpoint::<false>(self.lower, other.lower, operation) {
583 Ok(lower) => lower,
584 Err(error) => return Err(error),
585 };
586 let upper = match rounded_add_endpoint::<true>(self.upper, other.upper, operation) {
587 Ok(upper) => upper,
588 Err(error) => return Err(error),
589 };
590 Ok(Self::new_unchecked(lower, upper))
591 }
592
593 #[expect(
595 clippy::inline_always,
596 reason = "measured result-copy overhead in the interval determinant kernel"
597 )]
598 #[inline(always)]
599 const fn try_mul_for(
600 &self,
601 other: &Self,
602 operation: ArithmeticOperation,
603 ) -> Result<Self, LaError> {
604 if self.is_zero() || other.is_zero() {
605 return Ok(Self::ZERO);
606 }
607 if self.is_one() {
608 return Ok(*other);
609 }
610 if other.is_one() {
611 return Ok(*self);
612 }
613 if self.is_point() && other.is_point() {
614 return match rounded_product_bounds(self.lower, other.lower, operation) {
615 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
616 Err(error) => Err(error),
617 };
618 }
619
620 self.try_mul_by_sign(other, operation)
621 }
622
623 #[inline]
625 const fn try_mul_by_sign(
626 &self,
627 other: &Self,
628 operation: ArithmeticOperation,
629 ) -> Result<Self, LaError> {
630 let self_nonnegative = self.lower >= 0.0;
631 let self_nonpositive = self.upper <= 0.0;
632 let other_nonnegative = other.lower >= 0.0;
633 let other_nonpositive = other.upper <= 0.0;
634
635 if self_nonnegative {
636 if other_nonnegative {
637 return Self::try_product_extrema(
638 (self.lower, other.lower),
639 (self.upper, other.upper),
640 operation,
641 );
642 }
643 if other_nonpositive {
644 return Self::try_product_extrema(
645 (self.upper, other.lower),
646 (self.lower, other.upper),
647 operation,
648 );
649 }
650 return Self::try_product_extrema(
651 (self.upper, other.lower),
652 (self.upper, other.upper),
653 operation,
654 );
655 }
656 if self_nonpositive {
657 if other_nonnegative {
658 return Self::try_product_extrema(
659 (self.lower, other.upper),
660 (self.upper, other.lower),
661 operation,
662 );
663 }
664 if other_nonpositive {
665 return Self::try_product_extrema(
666 (self.upper, other.upper),
667 (self.lower, other.lower),
668 operation,
669 );
670 }
671 return Self::try_product_extrema(
672 (self.lower, other.upper),
673 (self.lower, other.lower),
674 operation,
675 );
676 }
677 if other_nonnegative {
678 return Self::try_product_extrema(
679 (self.lower, other.upper),
680 (self.upper, other.upper),
681 operation,
682 );
683 }
684 if other_nonpositive {
685 return Self::try_product_extrema(
686 (self.upper, other.lower),
687 (self.lower, other.lower),
688 operation,
689 );
690 }
691
692 let lower_left = match rounded_product_bounds(self.lower, other.upper, operation) {
693 Ok(bounds) => bounds,
694 Err(error) => return Err(error),
695 };
696 let lower_right = match rounded_product_bounds(self.upper, other.lower, operation) {
697 Ok(bounds) => bounds,
698 Err(error) => return Err(error),
699 };
700 let upper_left = match rounded_product_bounds(self.lower, other.lower, operation) {
701 Ok(bounds) => bounds,
702 Err(error) => return Err(error),
703 };
704 let upper_right = match rounded_product_bounds(self.upper, other.upper, operation) {
705 Ok(bounds) => bounds,
706 Err(error) => return Err(error),
707 };
708 let lower = if lower_left.0 < lower_right.0 {
709 lower_left.0
710 } else {
711 lower_right.0
712 };
713 let upper = if upper_left.1 > upper_right.1 {
714 upper_left.1
715 } else {
716 upper_right.1
717 };
718 Ok(Self::new_unchecked(lower, upper))
719 }
720
721 #[inline]
723 const fn try_product_extrema(
724 lower_factors: (f64, f64),
725 upper_factors: (f64, f64),
726 operation: ArithmeticOperation,
727 ) -> Result<Self, LaError> {
728 let lower = match rounded_product_bounds(lower_factors.0, lower_factors.1, operation) {
729 Ok((lower, _)) => lower,
730 Err(error) => return Err(error),
731 };
732 let upper = match rounded_product_bounds(upper_factors.0, upper_factors.1, operation) {
733 Ok((_, upper)) => upper,
734 Err(error) => return Err(error),
735 };
736 Ok(Self::new_unchecked(lower, upper))
737 }
738
739 const fn try_div_for(
741 &self,
742 other: &Self,
743 operation: ArithmeticOperation,
744 ) -> Result<Self, LaError> {
745 if other.contains(0.0) {
746 return Err(LaError::IntervalZeroDivisor {
747 lower: other.lower,
748 upper: other.upper,
749 });
750 }
751 let numerators = [self.lower, self.upper];
754 let denominators = [other.lower, other.upper];
755 let numerator_count = if self.is_point() { 1 } else { 2 };
756 let denominator_count = if other.is_point() { 1 } else { 2 };
757 let mut lower = f64::MAX;
758 let mut upper = -f64::MAX;
759 let mut i = 0;
760 while i < numerator_count {
761 let mut j = 0;
762 while j < denominator_count {
763 let (lo, hi) =
764 match rounded_quotient_bounds(numerators[i], denominators[j], operation) {
765 Ok(bounds) => bounds,
766 Err(error) => return Err(error),
767 };
768 if lo < lower {
769 lower = lo;
770 }
771 if hi > upper {
772 upper = hi;
773 }
774 j += 1;
775 }
776 i += 1;
777 }
778 Ok(Self::new_unchecked(lower, upper))
779 }
780
781 const fn try_eliminate(&self, factor: &Self, value: &Self) -> Result<Self, LaError> {
783 let product = match factor.try_mul_for(value, ArithmeticOperation::IntervalSolve) {
784 Ok(product) => product,
785 Err(error) => return Err(error),
786 };
787 self.try_add_for(&product.negate(), ArithmeticOperation::IntervalSolve)
788 }
789
790 const fn separation_from_zero(&self) -> f64 {
792 if self.lower > 0.0 {
793 self.lower
794 } else if self.upper < 0.0 {
795 -self.upper
796 } else {
797 0.0
798 }
799 }
800
801 #[inline]
803 const fn is_zero(&self) -> bool {
804 self.lower == 0.0 && self.upper == 0.0
805 }
806
807 #[inline]
809 const fn is_one(&self) -> bool {
810 self.lower.to_bits() == 1.0_f64.to_bits() && self.upper.to_bits() == 1.0_f64.to_bits()
811 }
812
813 #[inline]
815 const fn is_point(&self) -> bool {
816 self.lower.to_bits() == self.upper.to_bits()
817 }
818}
819
820impl Default for Interval {
821 #[inline]
822 fn default() -> Self {
823 Self::ZERO
824 }
825}
826
827impl<const D: usize> IntervalMatrix<D> {
828 #[inline]
832 pub const fn from_rows(rows: [[Interval; D]; D]) -> Self {
833 Self { rows }
834 }
835
836 #[inline]
846 pub const fn try_from_point_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
847 let mut intervals = [[Interval::ZERO; D]; D];
848 let mut row = 0;
849 while row < D {
850 let mut column = 0;
851 while column < D {
852 let value = rows[row][column];
853 if !value.is_finite() {
854 return Err(LaError::non_finite_input_matrix(row, column));
855 }
856 intervals[row][column] = Interval::new_unchecked(value, value);
857 column += 1;
858 }
859 row += 1;
860 }
861 Ok(Self::from_rows(intervals))
862 }
863
864 #[inline]
882 pub const fn from_matrix(matrix: &Matrix<D>) -> Self {
883 let matrix_rows = matrix.as_rows();
884 let mut intervals = [[Interval::ZERO; D]; D];
885 let mut row = 0;
886 while row < D {
887 let mut column = 0;
888 while column < D {
889 let value = matrix_rows[row][column];
890 intervals[row][column] = Interval::new_unchecked(value, value);
891 column += 1;
892 }
893 row += 1;
894 }
895 Self::from_rows(intervals)
896 }
897
898 #[inline]
900 pub const fn zero() -> Self {
901 Self::from_rows([[Interval::ZERO; D]; D])
902 }
903
904 #[inline]
906 pub const fn identity() -> Self {
907 let mut matrix = Self::zero();
908 let mut index = 0;
909 while index < D {
910 matrix.rows[index][index] = Interval::ONE;
911 index += 1;
912 }
913 matrix
914 }
915
916 #[inline]
918 #[must_use]
919 pub const fn as_rows(&self) -> &[[Interval; D]; D] {
920 &self.rows
921 }
922
923 #[inline]
925 #[must_use]
926 pub const fn into_rows(self) -> [[Interval; D]; D] {
927 self.rows
928 }
929
930 #[inline]
932 #[must_use]
933 pub const fn get(&self, row: usize, column: usize) -> Option<Interval> {
934 if row < D && column < D {
935 Some(self.rows[row][column])
936 } else {
937 None
938 }
939 }
940
941 #[inline]
948 pub const fn try_get(&self, row: usize, column: usize) -> Result<Interval, LaError> {
949 if row < D && column < D {
950 Ok(self.rows[row][column])
951 } else {
952 Err(LaError::index_out_of_bounds(row, column, D))
953 }
954 }
955
956 #[inline]
985 pub const fn set(&mut self, row: usize, column: usize, value: Interval) -> Result<(), LaError> {
986 if row >= D || column >= D {
987 return Err(LaError::index_out_of_bounds(row, column, D));
988 }
989 self.rows[row][column] = value;
990 Ok(())
991 }
992
993 pub const fn solve(&self, rhs: &[Interval; D]) -> Result<[Interval; D], LaError> {
1036 let mut rows = self.rows;
1037 let mut work_rhs = *rhs;
1038 let mut column = 0;
1039 while column < D {
1040 let mut pivot_row = column;
1041 let mut separation = 0.0;
1042 let mut row = column;
1043 while row < D {
1044 let candidate = rows[row][column].separation_from_zero();
1045 if candidate > separation {
1046 separation = candidate;
1047 pivot_row = row;
1048 }
1049 row += 1;
1050 }
1051 if separation == 0.0 {
1052 return Err(LaError::IntervalSolveInconclusive { pivot_col: column });
1053 }
1054 rows.swap(column, pivot_row);
1055 work_rhs.swap(column, pivot_row);
1056 row = column + 1;
1057 while row < D {
1058 if !rows[row][column].is_zero() {
1059 let factor = match rows[row][column]
1060 .try_div_for(&rows[column][column], ArithmeticOperation::IntervalSolve)
1061 {
1062 Ok(factor) => factor,
1063 Err(error) => return Err(error),
1064 };
1065 let mut j = column + 1;
1066 while j < D {
1067 rows[row][j] = match rows[row][j].try_eliminate(&factor, &rows[column][j]) {
1068 Ok(value) => value,
1069 Err(error) => return Err(error),
1070 };
1071 j += 1;
1072 }
1073 work_rhs[row] = match work_rhs[row].try_eliminate(&factor, &work_rhs[column]) {
1074 Ok(value) => value,
1075 Err(error) => return Err(error),
1076 };
1077 rows[row][column] = Interval::ZERO;
1080 }
1081 row += 1;
1082 }
1083 column += 1;
1084 }
1085 let mut solution = [Interval::ZERO; D];
1086 let mut row = D;
1087 while row > 0 {
1088 row -= 1;
1089 let mut residual = work_rhs[row];
1090 let mut column = row + 1;
1091 while column < D {
1092 residual = match residual.try_eliminate(&rows[row][column], &solution[column]) {
1093 Ok(value) => value,
1094 Err(error) => return Err(error),
1095 };
1096 column += 1;
1097 }
1098 solution[row] =
1099 match residual.try_div_for(&rows[row][row], ArithmeticOperation::IntervalSolve) {
1100 Ok(value) => value,
1101 Err(error) => return Err(error),
1102 };
1103 }
1104 Ok(solution)
1105 }
1106
1107 #[inline]
1141 pub const fn det(&self) -> Result<Interval, LaError> {
1142 match D {
1145 0 | 1 => self.det_with_workspace::<2>(),
1146 2 => self.det_with_workspace::<4>(),
1147 3 => self.det_with_workspace::<8>(),
1148 4 => self.det_with_workspace::<16>(),
1149 5 => self.det_with_workspace::<32>(),
1150 6 => self.det_with_workspace::<64>(),
1151 7 => self.det_with_workspace::<128>(),
1152 _ => Err(LaError::unsupported_dimension(D, MAX_INTERVAL_MATRIX_DIM)),
1153 }
1154 }
1155
1156 #[inline]
1163 const fn det_with_workspace<const STATES: usize>(&self) -> Result<Interval, LaError> {
1164 let state_count = 1_usize << D;
1165 let mut partials = [Interval::ZERO; STATES];
1166 partials[0] = Interval::ONE;
1167 let operation = ArithmeticOperation::IntervalDeterminant;
1168
1169 let mut subset = 1;
1170 while subset < state_count {
1171 let row = subset.count_ones() as usize - 1;
1172 let mut sum = Interval::ZERO;
1173 let mut remaining = subset;
1177 while remaining != 0 {
1178 let column = remaining.trailing_zeros() as usize;
1179 let column_bit = 1_usize << column;
1180 remaining &= remaining - 1;
1181 let previous = subset ^ column_bit;
1182 let mut term =
1183 match partials[previous].try_mul_for(&self.rows[row][column], operation) {
1184 Ok(term) => term,
1185 Err(error) => return Err(error),
1186 };
1187 if !remaining.count_ones().is_multiple_of(2) {
1188 term = term.negate();
1189 }
1190 sum = match sum.try_add_for(&term, operation) {
1191 Ok(next_sum) => next_sum,
1192 Err(error) => return Err(error),
1193 };
1194 }
1195 partials[subset] = sum;
1196 subset += 1;
1197 }
1198
1199 Ok(partials[state_count - 1])
1200 }
1201
1202 #[inline]
1231 pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
1232 let determinant = match self.det() {
1233 Ok(determinant) => determinant,
1234 Err(error) => return Err(error),
1235 };
1236 if determinant.lower > 0.0 {
1237 Ok(IntervalDeterminantSign::Positive)
1238 } else if determinant.upper < 0.0 {
1239 Ok(IntervalDeterminantSign::Negative)
1240 } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
1241 Ok(IntervalDeterminantSign::Zero)
1242 } else {
1243 Ok(IntervalDeterminantSign::Inconclusive)
1244 }
1245 }
1246}
1247
1248impl<const D: usize> Default for IntervalMatrix<D> {
1249 #[inline]
1250 fn default() -> Self {
1251 Self::zero()
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use core::assert_matches;
1258
1259 use pastey::paste;
1260
1261 use super::*;
1262 use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1263
1264 #[test]
1265 fn point_and_bounds_enforce_interval_invariants() {
1266 assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1267 assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1268 assert_matches!(
1269 Interval::point(f64::NAN),
1270 Err(LaError::NonFinite {
1271 location: NonFiniteLocation::Scalar,
1272 origin: NonFiniteOrigin::Input,
1273 ..
1274 })
1275 );
1276 assert_matches!(
1277 Interval::try_new(2.0, 1.0),
1278 Err(LaError::InvertedInterval {
1279 lower: 2.0,
1280 upper: 1.0,
1281 ..
1282 })
1283 );
1284 }
1285
1286 #[test]
1287 fn constructors_preserve_non_finite_input_locations() {
1288 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1289 assert_eq!(
1290 Interval::try_new(value, 0.0),
1291 Err(LaError::NonFinite {
1292 location: NonFiniteLocation::IntervalBound {
1293 bound: IntervalBound::Lower,
1294 },
1295 origin: NonFiniteOrigin::Input,
1296 })
1297 );
1298 assert_eq!(
1299 Interval::try_new(0.0, value),
1300 Err(LaError::NonFinite {
1301 location: NonFiniteLocation::IntervalBound {
1302 bound: IntervalBound::Upper,
1303 },
1304 origin: NonFiniteOrigin::Input,
1305 })
1306 );
1307 assert_eq!(
1308 Interval::try_from_subtraction(value, 0.0),
1309 Err(LaError::NonFinite {
1310 location: NonFiniteLocation::IntervalOperand {
1311 operand: IntervalOperand::Left,
1312 },
1313 origin: NonFiniteOrigin::Input,
1314 })
1315 );
1316 assert_eq!(
1317 Interval::try_from_subtraction(0.0, value),
1318 Err(LaError::NonFinite {
1319 location: NonFiniteLocation::IntervalOperand {
1320 operand: IntervalOperand::Right,
1321 },
1322 origin: NonFiniteOrigin::Input,
1323 })
1324 );
1325 }
1326
1327 assert_eq!(
1328 Interval::try_new(f64::NAN, f64::INFINITY),
1329 Err(LaError::NonFinite {
1330 location: NonFiniteLocation::IntervalBound {
1331 bound: IntervalBound::Lower,
1332 },
1333 origin: NonFiniteOrigin::Input,
1334 })
1335 );
1336 assert_eq!(
1337 Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1338 Err(LaError::NonFinite {
1339 location: NonFiniteLocation::IntervalOperand {
1340 operand: IntervalOperand::Left,
1341 },
1342 origin: NonFiniteOrigin::Input,
1343 })
1344 );
1345
1346 let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1347 assert_eq!(
1348 IntervalMatrix::<2>::try_from_point_rows(rows),
1349 Err(LaError::NonFinite {
1350 location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1351 origin: NonFiniteOrigin::Input,
1352 })
1353 );
1354 }
1355
1356 #[test]
1357 fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1358 let one = Interval::point(1.0)?;
1359 let two = Interval::point(2.0)?;
1360 assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1361 assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1362 assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1363 assert_eq!(
1364 Interval::try_new(-2.0, -1.0)?.negate(),
1365 Interval::try_new(1.0, 2.0)?
1366 );
1367 Ok(())
1368 }
1369
1370 #[test]
1371 fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1372 let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1373 let rounded_subtraction = 1.0_f64 - 0.1;
1374 assert_eq!(
1375 subtraction,
1376 Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1377 );
1378
1379 let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1380 let rounded_product = 0.1_f64 * 0.2;
1381 assert_eq!(
1382 product,
1383 Interval::try_new(rounded_product.next_down(), rounded_product)?
1384 );
1385
1386 let below_one = 1.0 - f64::EPSILON;
1387 let above_one = 1.0 + f64::EPSILON;
1388 let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1389 assert_eq!(
1390 binade_boundary,
1391 Interval::try_new(1.0_f64.next_down(), 1.0)?
1392 );
1393 Ok(())
1394 }
1395
1396 #[test]
1397 fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1398 let next = 1.0_f64.next_up();
1399 let difference = Interval::try_from_subtraction(next, 1.0)?;
1400 assert_eq!(difference, Interval::point(f64::EPSILON)?);
1401 Ok(())
1402 }
1403
1404 #[test]
1405 fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1406 for value in [
1407 -f64::MAX,
1408 -1.0,
1409 -f64::MIN_POSITIVE,
1410 -f64::from_bits(1),
1411 -0.0,
1412 0.0,
1413 f64::from_bits(1),
1414 f64::MIN_POSITIVE,
1415 1.0,
1416 f64::MAX,
1417 ] {
1418 for zero in [0.0, -0.0] {
1419 for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1420 let result = Interval::try_from_subtraction(left, right)?;
1421 let expected_bits = if expected == 0.0 {
1422 0
1423 } else {
1424 expected.to_bits()
1425 };
1426 assert_eq!(result.lower().to_bits(), expected_bits);
1427 assert_eq!(result.upper().to_bits(), expected_bits);
1428 }
1429 }
1430 }
1431 Ok(())
1432 }
1433
1434 #[test]
1435 fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1436 for (value, lower, upper) in [
1437 (0.0, 0.0_f64, 0.0),
1438 (-0.0, 0.0, 0.0),
1439 (2.0, 4.0, 4.0),
1440 (-2.0, 4.0, 4.0),
1441 (f64::from_bits(1), 0.0, f64::from_bits(1)),
1442 (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1443 ] {
1444 let squared = Interval::point(value)?.try_square()?;
1445 assert_eq!(squared.lower().to_bits(), lower.to_bits());
1446 assert_eq!(squared.upper().to_bits(), upper.to_bits());
1447 }
1448 for value in [0.1, -0.1] {
1451 let squared = Interval::point(value)?.try_square()?;
1452 assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1453 assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1454 }
1455 Ok(())
1456 }
1457
1458 #[test]
1459 fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1460 let expected = Interval::try_new(4.0, 9.0)?;
1461 for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1462 assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1463 }
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1469 let left = Interval::try_new(-1.0, 1.0)?;
1470 for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1473 let right = Interval::try_new(-radius, radius)?;
1474 assert_eq!(
1475 left.try_add(&right)?,
1476 Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1477 );
1478 }
1479 assert_eq!(
1480 left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1481 Interval::try_new(-3.0, 3.0)?
1482 );
1483 Ok(())
1484 }
1485
1486 #[test]
1487 fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1488 let least_subnormal = f64::from_bits(1);
1489 let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1490 assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1491 Ok(())
1492 }
1493
1494 #[test]
1495 fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1496 let error = Interval::point(f64::MAX)?
1497 .try_mul(&Interval::point(2.0)?)
1498 .unwrap_err();
1499 assert_eq!(
1500 error,
1501 LaError::IntervalRangeExhausted {
1502 operation: ArithmeticOperation::IntervalMultiplication,
1503 }
1504 );
1505 Ok(())
1506 }
1507
1508 #[test]
1509 fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1510 let maximum = Interval::point(f64::MAX)?;
1511 let tiny = Interval::point(f64::MIN_POSITIVE)?;
1512 assert_eq!(
1513 maximum.try_add(&maximum),
1514 Err(LaError::IntervalRangeExhausted {
1515 operation: ArithmeticOperation::IntervalAddition,
1516 })
1517 );
1518 assert_eq!(
1519 maximum.try_add(&tiny),
1520 Err(LaError::IntervalRangeExhausted {
1521 operation: ArithmeticOperation::IntervalAddition,
1522 })
1523 );
1524 let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1525 assert_eq!(
1526 nonnegative.try_add(&nonnegative),
1527 Err(LaError::IntervalRangeExhausted {
1528 operation: ArithmeticOperation::IntervalAddition,
1529 })
1530 );
1531 let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1532 for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1533 assert_eq!(
1534 left.try_add(&right),
1535 Err(LaError::IntervalRangeExhausted {
1536 operation: ArithmeticOperation::IntervalAddition,
1537 }),
1538 );
1539 }
1540
1541 let finite_difference = maximum.try_add(&tiny.negate())?;
1542 assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1543 assert!(finite_difference.lower() < finite_difference.upper());
1544 Ok(())
1545 }
1546
1547 #[test]
1548 fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1549 assert_eq!(
1550 Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1551 Err(LaError::IntervalRangeExhausted {
1552 operation: ArithmeticOperation::IntervalSubtraction,
1553 })
1554 );
1555 assert_eq!(
1556 Interval::point(f64::MAX)?.try_square(),
1557 Err(LaError::IntervalRangeExhausted {
1558 operation: ArithmeticOperation::IntervalSquare,
1559 })
1560 );
1561 assert_eq!(
1562 Interval::try_new(-1.0, f64::MAX)?.try_square(),
1563 Err(LaError::IntervalRangeExhausted {
1564 operation: ArithmeticOperation::IntervalSquare,
1565 })
1566 );
1567 Ok(())
1568 }
1569
1570 #[test]
1571 fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1572 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1573 assert_eq!(
1574 matrix.det(),
1575 Err(LaError::IntervalRangeExhausted {
1576 operation: ArithmeticOperation::IntervalDeterminant,
1577 })
1578 );
1579
1580 let accumulating =
1581 IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1582 assert_eq!(
1583 accumulating.det(),
1584 Err(LaError::IntervalRangeExhausted {
1585 operation: ArithmeticOperation::IntervalDeterminant,
1586 })
1587 );
1588 assert_eq!(
1589 accumulating.det_sign(),
1590 Err(LaError::IntervalRangeExhausted {
1591 operation: ArithmeticOperation::IntervalDeterminant,
1592 })
1593 );
1594 Ok(())
1595 }
1596
1597 #[test]
1598 fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1599 {
1600 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1601 assert_eq!(
1602 matrix.det(),
1603 Err(LaError::IntervalRangeExhausted {
1604 operation: ArithmeticOperation::IntervalDeterminant,
1605 })
1606 );
1607 Ok(())
1608 }
1609
1610 #[test]
1611 fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1612 let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1613 assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1614 Ok(())
1615 }
1616
1617 #[test]
1618 fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1619 for (left, right, expected) in [
1620 ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1621 ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1622 ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1623 ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1624 ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1625 ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1626 ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1627 ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1628 ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1629 ] {
1630 let product = Interval::try_new(left.0, left.1)?
1631 .try_mul(&Interval::try_new(right.0, right.1)?)?;
1632 assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1633 }
1634
1635 assert_eq!(
1636 Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1637 Interval::ZERO
1638 );
1639 Ok(())
1640 }
1641
1642 #[test]
1643 fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1644 let half_maximum = f64::MAX / 2.0;
1645 for (left, right) in [
1646 ((half_maximum, f64::MAX), (-2.0, -1.0)),
1647 ((half_maximum, f64::MAX), (1.0, 2.0)),
1648 ((-f64::MAX, 1.0), (-1.0, 2.0)),
1649 ((-1.0, f64::MAX), (-2.0, 1.0)),
1650 ((-f64::MAX, 1.0), (-2.0, 1.0)),
1651 ((-1.0, f64::MAX), (-1.0, 2.0)),
1652 ] {
1653 let result =
1654 Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1655 assert_eq!(
1656 result,
1657 Err(LaError::IntervalRangeExhausted {
1658 operation: ArithmeticOperation::IntervalMultiplication,
1659 }),
1660 "left={left:?}, right={right:?}"
1661 );
1662 }
1663 Ok(())
1664 }
1665
1666 macro_rules! gen_interval_identity_tests {
1667 ($d:literal) => {
1668 paste! {
1669 #[test]
1670 fn [<interval_identity_sign_is_positive_ $d d>]() {
1671 let matrix = IntervalMatrix::<$d>::identity();
1672 assert_eq!(matrix.det(), Ok(Interval::ONE));
1673 assert_eq!(
1674 matrix.det_sign(),
1675 Ok(IntervalDeterminantSign::Positive)
1676 );
1677 }
1678 }
1679 };
1680 }
1681
1682 gen_interval_identity_tests!(0);
1683 gen_interval_identity_tests!(1);
1684 gen_interval_identity_tests!(2);
1685 gen_interval_identity_tests!(3);
1686 gen_interval_identity_tests!(4);
1687 gen_interval_identity_tests!(5);
1688 gen_interval_identity_tests!(6);
1689 gen_interval_identity_tests!(7);
1690
1691 #[test]
1692 fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1693 const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1694 const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1695 Ok(interval) => interval,
1696 Err(_) => panic!("finite ordered literal bounds"),
1697 };
1698 const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1699 assert_eq!(EMPTY, Ok(Interval::ONE));
1700 assert_eq!(SINGLE, Ok(WIDE));
1701 for (lower, upper, sign) in [
1702 (
1703 -f64::MAX,
1704 -f64::from_bits(1),
1705 IntervalDeterminantSign::Negative,
1706 ),
1707 (
1708 f64::from_bits(1),
1709 f64::MAX,
1710 IntervalDeterminantSign::Positive,
1711 ),
1712 (0.0, 0.0, IntervalDeterminantSign::Zero),
1713 (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1714 ] {
1715 let interval = Interval::try_new(lower, upper)?;
1716 let matrix = IntervalMatrix::from_rows([[interval]]);
1717 assert_eq!(matrix.det()?, interval);
1718 assert_eq!(matrix.det_sign()?, sign);
1719 }
1720 Ok(())
1721 }
1722
1723 #[test]
1724 fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1725 let swapped = IntervalMatrix::<3>::try_from_point_rows([
1726 [0.0, 1.0, 0.0],
1727 [1.0, 0.0, 0.0],
1728 [0.0, 0.0, 1.0],
1729 ])?;
1730 assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1731
1732 let singular = IntervalMatrix::<3>::try_from_point_rows([
1733 [1.0, 2.0, 3.0],
1734 [1.0, 2.0, 3.0],
1735 [0.0, 0.0, 1.0],
1736 ])?;
1737 assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1738 Ok(())
1739 }
1740
1741 #[test]
1742 fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1743 let matrix = IntervalMatrix::<2>::from_rows([
1744 [Interval::ONE, Interval::ZERO],
1745 [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1746 ]);
1747 assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1748 Ok(())
1749 }
1750
1751 #[test]
1752 fn determinant_rejects_dimensions_above_supported_stack_dp() {
1753 assert_matches!(
1754 IntervalMatrix::<8>::identity().det(),
1755 Err(LaError::UnsupportedDimension {
1756 requested: 8,
1757 max: MAX_INTERVAL_MATRIX_DIM,
1758 ..
1759 })
1760 );
1761 }
1762
1763 #[test]
1764 fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1765 let source = Matrix::<2>::identity();
1766 let mut intervals = IntervalMatrix::from_matrix(&source);
1767 let value = Interval::try_new(2.0, 3.0)?;
1768 intervals.set(0, 1, value)?;
1769 assert_eq!(intervals.get(0, 1), Some(value));
1770 assert_eq!(intervals.get(2, 0), None);
1771 assert_eq!(intervals.try_get(0, 1)?, value);
1772 assert_matches!(
1773 intervals.try_get(2, 0),
1774 Err(LaError::IndexOutOfBounds {
1775 row: 2,
1776 col: 0,
1777 dim: 2,
1778 ..
1779 })
1780 );
1781 assert_eq!(intervals.as_rows()[0][1], value);
1782 assert_eq!(intervals.into_rows()[0][1], value);
1783 Ok(())
1784 }
1785
1786 #[test]
1787 fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1788 let mut matrix = IntervalMatrix::<2>::identity();
1789 let before = matrix;
1790 let value = Interval::try_new(2.0, 3.0)?;
1791
1792 assert_eq!(
1793 matrix.set(2, 0, value),
1794 Err(LaError::IndexOutOfBounds {
1795 row: 2,
1796 col: 0,
1797 dim: 2,
1798 })
1799 );
1800 assert_eq!(matrix, before);
1801 assert_eq!(
1802 matrix.set(0, 2, value),
1803 Err(LaError::IndexOutOfBounds {
1804 row: 0,
1805 col: 2,
1806 dim: 2,
1807 })
1808 );
1809 assert_eq!(matrix, before);
1810 Ok(())
1811 }
1812}