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 let (lower, upper) = if error < 0.0 {
121 (rounded.next_down(), rounded)
122 } else if error > 0.0 {
123 (rounded, rounded.next_up())
124 } else {
125 (rounded, rounded)
126 };
127 if !lower.is_finite() || !upper.is_finite() {
128 return Err(LaError::interval_range_exhausted(operation));
129 }
130
131 Ok((canonical_zero(lower), canonical_zero(upper)))
132}
133
134#[inline]
136const fn rounded_add_endpoint<const UPPER: bool>(
137 left: f64,
138 right: f64,
139 operation: ArithmeticOperation,
140) -> Result<f64, LaError> {
141 let rounded = left + right;
142 if !rounded.is_finite() {
143 return Err(LaError::interval_range_exhausted(operation));
144 }
145 let error = two_sum_error(left, right, rounded);
146 let endpoint = if UPPER && error > 0.0 {
147 rounded.next_up()
148 } else if !UPPER && error < 0.0 {
149 rounded.next_down()
150 } else {
151 rounded
152 };
153 if !endpoint.is_finite() {
154 return Err(LaError::interval_range_exhausted(operation));
155 }
156 Ok(canonical_zero(endpoint))
157}
158
159#[expect(
163 clippy::inline_always,
164 reason = "measured endpoint-call overhead in the interval determinant kernel"
165)]
166#[inline(always)]
167const fn rounded_product_bounds(
168 left: f64,
169 right: f64,
170 operation: ArithmeticOperation,
171) -> Result<(f64, f64), LaError> {
172 if left == 0.0 || right == 0.0 {
173 return Ok((0.0, 0.0));
174 }
175
176 let rounded = left * right;
177 if !rounded.is_finite() {
178 return Err(LaError::interval_range_exhausted(operation));
179 }
180
181 let relation = compare_product_with_rounded(left, right, rounded);
182 let (lower, upper) = if relation < 0 {
183 (rounded.next_down(), rounded)
184 } else if relation > 0 {
185 (rounded, rounded.next_up())
186 } else {
187 (rounded, rounded)
188 };
189 if !lower.is_finite() || !upper.is_finite() {
190 return Err(LaError::interval_range_exhausted(operation));
191 }
192
193 Ok((canonical_zero(lower), canonical_zero(upper)))
194}
195
196impl Interval {
197 pub const ZERO: Self = Self {
199 lower: 0.0,
200 upper: 0.0,
201 };
202
203 pub const ONE: Self = Self {
205 lower: 1.0,
206 upper: 1.0,
207 };
208
209 #[inline]
234 pub const fn try_new(lower: f64, upper: f64) -> Result<Self, LaError> {
235 if !lower.is_finite() {
236 return Err(LaError::non_finite_input_interval_bound(
237 IntervalBound::Lower,
238 ));
239 }
240 if !upper.is_finite() {
241 return Err(LaError::non_finite_input_interval_bound(
242 IntervalBound::Upper,
243 ));
244 }
245 if lower > upper {
246 return Err(LaError::inverted_interval(lower, upper));
247 }
248 Ok(Self::new_unchecked(lower, upper))
249 }
250
251 #[inline]
272 pub const fn point(value: f64) -> Result<Self, LaError> {
273 match Self::try_new(value, value) {
274 Ok(interval) => Ok(interval),
275 Err(LaError::NonFinite { .. }) => Err(LaError::non_finite_input_scalar()),
276 Err(error) => Err(error),
277 }
278 }
279
280 #[inline]
308 pub const fn try_from_subtraction(left: f64, right: f64) -> Result<Self, LaError> {
309 if !left.is_finite() {
310 return Err(LaError::non_finite_input_interval_operand(
311 IntervalOperand::Left,
312 ));
313 }
314 if !right.is_finite() {
315 return Err(LaError::non_finite_input_interval_operand(
316 IntervalOperand::Right,
317 ));
318 }
319 if right == 0.0 {
320 return Ok(Self::new_unchecked(left, left));
321 }
322 if left == 0.0 {
323 return Ok(Self::new_unchecked(-right, -right));
324 }
325 match rounded_add_bounds(left, -right, ArithmeticOperation::IntervalSubtraction) {
326 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
327 Err(error) => Err(error),
328 }
329 }
330
331 #[inline]
333 #[must_use]
334 pub const fn lower(self) -> f64 {
335 self.lower
336 }
337
338 #[inline]
340 #[must_use]
341 pub const fn upper(self) -> f64 {
342 self.upper
343 }
344
345 #[inline]
347 #[must_use]
348 pub const fn contains(self, value: f64) -> bool {
349 value.is_finite() && self.lower <= value && value <= self.upper
350 }
351
352 #[inline]
370 pub const fn try_add(&self, other: &Self) -> Result<Self, LaError> {
371 self.try_add_for(other, ArithmeticOperation::IntervalAddition)
372 }
373
374 #[inline]
395 pub const fn try_mul(&self, other: &Self) -> Result<Self, LaError> {
396 self.try_mul_for(other, ArithmeticOperation::IntervalMultiplication)
397 }
398
399 #[inline]
412 pub const fn negate(&self) -> Self {
413 Self::new_unchecked(-self.upper, -self.lower)
414 }
415
416 #[expect(
438 clippy::inline_always,
439 reason = "measured call overhead in lifted interval assembly"
440 )]
441 #[inline(always)]
442 pub const fn try_square(&self) -> Result<Self, LaError> {
443 let operation = ArithmeticOperation::IntervalSquare;
444 let left_square = match rounded_product_bounds(self.lower, self.lower, operation) {
445 Ok(bounds) => bounds,
446 Err(error) => return Err(error),
447 };
448 if self.is_point() {
449 return Ok(Self::new_unchecked(left_square.0, left_square.1));
450 }
451 let right_square = match rounded_product_bounds(self.upper, self.upper, operation) {
452 Ok(bounds) => bounds,
453 Err(error) => return Err(error),
454 };
455 let lower = if self.lower <= 0.0 && self.upper >= 0.0 {
456 0.0
457 } else if left_square.0 < right_square.0 {
458 left_square.0
459 } else {
460 right_square.0
461 };
462 let upper = if left_square.1 > right_square.1 {
463 left_square.1
464 } else {
465 right_square.1
466 };
467 Ok(Self::new_unchecked(lower, upper))
468 }
469
470 #[inline]
472 const fn new_unchecked(lower: f64, upper: f64) -> Self {
473 Self {
474 lower: canonical_zero(lower),
475 upper: canonical_zero(upper),
476 }
477 }
478
479 #[expect(
481 clippy::inline_always,
482 reason = "measured result-copy overhead in the interval determinant kernel"
483 )]
484 #[inline(always)]
485 const fn try_add_for(
486 &self,
487 other: &Self,
488 operation: ArithmeticOperation,
489 ) -> Result<Self, LaError> {
490 if self.is_zero() {
491 return Ok(*other);
492 }
493 if other.is_zero() {
494 return Ok(*self);
495 }
496 if self.is_point() && other.is_point() {
497 return match rounded_add_bounds(self.lower, other.lower, operation) {
498 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
499 Err(error) => Err(error),
500 };
501 }
502
503 let lower = match rounded_add_endpoint::<false>(self.lower, other.lower, operation) {
504 Ok(lower) => lower,
505 Err(error) => return Err(error),
506 };
507 let upper = match rounded_add_endpoint::<true>(self.upper, other.upper, operation) {
508 Ok(upper) => upper,
509 Err(error) => return Err(error),
510 };
511 Ok(Self::new_unchecked(lower, upper))
512 }
513
514 #[expect(
516 clippy::inline_always,
517 reason = "measured result-copy overhead in the interval determinant kernel"
518 )]
519 #[inline(always)]
520 const fn try_mul_for(
521 &self,
522 other: &Self,
523 operation: ArithmeticOperation,
524 ) -> Result<Self, LaError> {
525 if self.is_zero() || other.is_zero() {
526 return Ok(Self::ZERO);
527 }
528 if self.is_one() {
529 return Ok(*other);
530 }
531 if other.is_one() {
532 return Ok(*self);
533 }
534 if self.is_point() && other.is_point() {
535 return match rounded_product_bounds(self.lower, other.lower, operation) {
536 Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
537 Err(error) => Err(error),
538 };
539 }
540
541 self.try_mul_by_sign(other, operation)
542 }
543
544 #[inline]
546 const fn try_mul_by_sign(
547 &self,
548 other: &Self,
549 operation: ArithmeticOperation,
550 ) -> Result<Self, LaError> {
551 let self_nonnegative = self.lower >= 0.0;
552 let self_nonpositive = self.upper <= 0.0;
553 let other_nonnegative = other.lower >= 0.0;
554 let other_nonpositive = other.upper <= 0.0;
555
556 if self_nonnegative {
557 if other_nonnegative {
558 return Self::try_product_extrema(
559 (self.lower, other.lower),
560 (self.upper, other.upper),
561 operation,
562 );
563 }
564 if other_nonpositive {
565 return Self::try_product_extrema(
566 (self.upper, other.lower),
567 (self.lower, other.upper),
568 operation,
569 );
570 }
571 return Self::try_product_extrema(
572 (self.upper, other.lower),
573 (self.upper, other.upper),
574 operation,
575 );
576 }
577 if self_nonpositive {
578 if other_nonnegative {
579 return Self::try_product_extrema(
580 (self.lower, other.upper),
581 (self.upper, other.lower),
582 operation,
583 );
584 }
585 if other_nonpositive {
586 return Self::try_product_extrema(
587 (self.upper, other.upper),
588 (self.lower, other.lower),
589 operation,
590 );
591 }
592 return Self::try_product_extrema(
593 (self.lower, other.upper),
594 (self.lower, other.lower),
595 operation,
596 );
597 }
598 if other_nonnegative {
599 return Self::try_product_extrema(
600 (self.lower, other.upper),
601 (self.upper, other.upper),
602 operation,
603 );
604 }
605 if other_nonpositive {
606 return Self::try_product_extrema(
607 (self.upper, other.lower),
608 (self.lower, other.lower),
609 operation,
610 );
611 }
612
613 let lower_left = match rounded_product_bounds(self.lower, other.upper, operation) {
614 Ok(bounds) => bounds,
615 Err(error) => return Err(error),
616 };
617 let lower_right = match rounded_product_bounds(self.upper, other.lower, operation) {
618 Ok(bounds) => bounds,
619 Err(error) => return Err(error),
620 };
621 let upper_left = match rounded_product_bounds(self.lower, other.lower, operation) {
622 Ok(bounds) => bounds,
623 Err(error) => return Err(error),
624 };
625 let upper_right = match rounded_product_bounds(self.upper, other.upper, operation) {
626 Ok(bounds) => bounds,
627 Err(error) => return Err(error),
628 };
629 let lower = if lower_left.0 < lower_right.0 {
630 lower_left.0
631 } else {
632 lower_right.0
633 };
634 let upper = if upper_left.1 > upper_right.1 {
635 upper_left.1
636 } else {
637 upper_right.1
638 };
639 Ok(Self::new_unchecked(lower, upper))
640 }
641
642 #[inline]
644 const fn try_product_extrema(
645 lower_factors: (f64, f64),
646 upper_factors: (f64, f64),
647 operation: ArithmeticOperation,
648 ) -> Result<Self, LaError> {
649 let lower = match rounded_product_bounds(lower_factors.0, lower_factors.1, operation) {
650 Ok((lower, _)) => lower,
651 Err(error) => return Err(error),
652 };
653 let upper = match rounded_product_bounds(upper_factors.0, upper_factors.1, operation) {
654 Ok((_, upper)) => upper,
655 Err(error) => return Err(error),
656 };
657 Ok(Self::new_unchecked(lower, upper))
658 }
659
660 #[inline]
662 const fn is_zero(&self) -> bool {
663 self.lower == 0.0 && self.upper == 0.0
664 }
665
666 #[inline]
668 const fn is_one(&self) -> bool {
669 self.lower.to_bits() == 1.0_f64.to_bits() && self.upper.to_bits() == 1.0_f64.to_bits()
670 }
671
672 #[inline]
674 const fn is_point(&self) -> bool {
675 self.lower.to_bits() == self.upper.to_bits()
676 }
677}
678
679impl Default for Interval {
680 #[inline]
681 fn default() -> Self {
682 Self::ZERO
683 }
684}
685
686impl<const D: usize> IntervalMatrix<D> {
687 #[inline]
691 pub const fn from_rows(rows: [[Interval; D]; D]) -> Self {
692 Self { rows }
693 }
694
695 #[inline]
705 pub const fn try_from_point_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
706 let mut intervals = [[Interval::ZERO; D]; D];
707 let mut row = 0;
708 while row < D {
709 let mut column = 0;
710 while column < D {
711 let value = rows[row][column];
712 if !value.is_finite() {
713 return Err(LaError::non_finite_input_matrix(row, column));
714 }
715 intervals[row][column] = Interval::new_unchecked(value, value);
716 column += 1;
717 }
718 row += 1;
719 }
720 Ok(Self::from_rows(intervals))
721 }
722
723 #[inline]
741 pub const fn from_matrix(matrix: &Matrix<D>) -> Self {
742 let matrix_rows = matrix.as_rows();
743 let mut intervals = [[Interval::ZERO; D]; D];
744 let mut row = 0;
745 while row < D {
746 let mut column = 0;
747 while column < D {
748 let value = matrix_rows[row][column];
749 intervals[row][column] = Interval::new_unchecked(value, value);
750 column += 1;
751 }
752 row += 1;
753 }
754 Self::from_rows(intervals)
755 }
756
757 #[inline]
759 pub const fn zero() -> Self {
760 Self::from_rows([[Interval::ZERO; D]; D])
761 }
762
763 #[inline]
765 pub const fn identity() -> Self {
766 let mut matrix = Self::zero();
767 let mut index = 0;
768 while index < D {
769 matrix.rows[index][index] = Interval::ONE;
770 index += 1;
771 }
772 matrix
773 }
774
775 #[inline]
777 #[must_use]
778 pub const fn as_rows(&self) -> &[[Interval; D]; D] {
779 &self.rows
780 }
781
782 #[inline]
784 #[must_use]
785 pub const fn into_rows(self) -> [[Interval; D]; D] {
786 self.rows
787 }
788
789 #[inline]
791 #[must_use]
792 pub const fn get(&self, row: usize, column: usize) -> Option<Interval> {
793 if row < D && column < D {
794 Some(self.rows[row][column])
795 } else {
796 None
797 }
798 }
799
800 #[inline]
807 pub const fn try_get(&self, row: usize, column: usize) -> Result<Interval, LaError> {
808 if row < D && column < D {
809 Ok(self.rows[row][column])
810 } else {
811 Err(LaError::index_out_of_bounds(row, column, D))
812 }
813 }
814
815 #[inline]
844 pub const fn set(&mut self, row: usize, column: usize, value: Interval) -> Result<(), LaError> {
845 if row >= D || column >= D {
846 return Err(LaError::index_out_of_bounds(row, column, D));
847 }
848 self.rows[row][column] = value;
849 Ok(())
850 }
851
852 #[inline]
886 pub const fn det(&self) -> Result<Interval, LaError> {
887 match D {
890 0 | 1 => self.det_with_workspace::<2>(),
891 2 => self.det_with_workspace::<4>(),
892 3 => self.det_with_workspace::<8>(),
893 4 => self.det_with_workspace::<16>(),
894 5 => self.det_with_workspace::<32>(),
895 6 => self.det_with_workspace::<64>(),
896 7 => self.det_with_workspace::<128>(),
897 _ => Err(LaError::unsupported_dimension(D, MAX_INTERVAL_MATRIX_DIM)),
898 }
899 }
900
901 #[inline]
908 const fn det_with_workspace<const STATES: usize>(&self) -> Result<Interval, LaError> {
909 let state_count = 1_usize << D;
910 let mut partials = [Interval::ZERO; STATES];
911 partials[0] = Interval::ONE;
912 let operation = ArithmeticOperation::IntervalDeterminant;
913
914 let mut subset = 1;
915 while subset < state_count {
916 let row = subset.count_ones() as usize - 1;
917 let mut sum = Interval::ZERO;
918 let mut column = 0;
919 while column < D {
920 let column_bit = 1_usize << column;
921 if subset & column_bit != 0 {
922 let previous = subset ^ column_bit;
923 let mut term =
924 match partials[previous].try_mul_for(&self.rows[row][column], operation) {
925 Ok(term) => term,
926 Err(error) => return Err(error),
927 };
928 let columns_after = (subset >> (column + 1)).count_ones();
929 if !columns_after.is_multiple_of(2) {
930 term = term.negate();
931 }
932 sum = match sum.try_add_for(&term, operation) {
933 Ok(next_sum) => next_sum,
934 Err(error) => return Err(error),
935 };
936 }
937 column += 1;
938 }
939 partials[subset] = sum;
940 subset += 1;
941 }
942
943 Ok(partials[state_count - 1])
944 }
945
946 #[inline]
975 pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
976 let determinant = match self.det() {
977 Ok(determinant) => determinant,
978 Err(error) => return Err(error),
979 };
980 if determinant.lower > 0.0 {
981 Ok(IntervalDeterminantSign::Positive)
982 } else if determinant.upper < 0.0 {
983 Ok(IntervalDeterminantSign::Negative)
984 } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
985 Ok(IntervalDeterminantSign::Zero)
986 } else {
987 Ok(IntervalDeterminantSign::Inconclusive)
988 }
989 }
990}
991
992impl<const D: usize> Default for IntervalMatrix<D> {
993 #[inline]
994 fn default() -> Self {
995 Self::zero()
996 }
997}
998
999#[cfg(test)]
1000mod tests {
1001 use core::assert_matches;
1002
1003 use pastey::paste;
1004
1005 use super::*;
1006 use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1007
1008 #[test]
1009 fn point_and_bounds_enforce_interval_invariants() {
1010 assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1011 assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1012 assert_matches!(
1013 Interval::point(f64::NAN),
1014 Err(LaError::NonFinite {
1015 location: NonFiniteLocation::Scalar,
1016 origin: NonFiniteOrigin::Input,
1017 ..
1018 })
1019 );
1020 assert_matches!(
1021 Interval::try_new(2.0, 1.0),
1022 Err(LaError::InvertedInterval {
1023 lower: 2.0,
1024 upper: 1.0,
1025 ..
1026 })
1027 );
1028 }
1029
1030 #[test]
1031 fn constructors_preserve_non_finite_input_locations() {
1032 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1033 assert_eq!(
1034 Interval::try_new(value, 0.0),
1035 Err(LaError::NonFinite {
1036 location: NonFiniteLocation::IntervalBound {
1037 bound: IntervalBound::Lower,
1038 },
1039 origin: NonFiniteOrigin::Input,
1040 })
1041 );
1042 assert_eq!(
1043 Interval::try_new(0.0, value),
1044 Err(LaError::NonFinite {
1045 location: NonFiniteLocation::IntervalBound {
1046 bound: IntervalBound::Upper,
1047 },
1048 origin: NonFiniteOrigin::Input,
1049 })
1050 );
1051 assert_eq!(
1052 Interval::try_from_subtraction(value, 0.0),
1053 Err(LaError::NonFinite {
1054 location: NonFiniteLocation::IntervalOperand {
1055 operand: IntervalOperand::Left,
1056 },
1057 origin: NonFiniteOrigin::Input,
1058 })
1059 );
1060 assert_eq!(
1061 Interval::try_from_subtraction(0.0, value),
1062 Err(LaError::NonFinite {
1063 location: NonFiniteLocation::IntervalOperand {
1064 operand: IntervalOperand::Right,
1065 },
1066 origin: NonFiniteOrigin::Input,
1067 })
1068 );
1069 }
1070
1071 assert_eq!(
1072 Interval::try_new(f64::NAN, f64::INFINITY),
1073 Err(LaError::NonFinite {
1074 location: NonFiniteLocation::IntervalBound {
1075 bound: IntervalBound::Lower,
1076 },
1077 origin: NonFiniteOrigin::Input,
1078 })
1079 );
1080 assert_eq!(
1081 Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1082 Err(LaError::NonFinite {
1083 location: NonFiniteLocation::IntervalOperand {
1084 operand: IntervalOperand::Left,
1085 },
1086 origin: NonFiniteOrigin::Input,
1087 })
1088 );
1089
1090 let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1091 assert_eq!(
1092 IntervalMatrix::<2>::try_from_point_rows(rows),
1093 Err(LaError::NonFinite {
1094 location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1095 origin: NonFiniteOrigin::Input,
1096 })
1097 );
1098 }
1099
1100 #[test]
1101 fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1102 let one = Interval::point(1.0)?;
1103 let two = Interval::point(2.0)?;
1104 assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1105 assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1106 assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1107 assert_eq!(
1108 Interval::try_new(-2.0, -1.0)?.negate(),
1109 Interval::try_new(1.0, 2.0)?
1110 );
1111 Ok(())
1112 }
1113
1114 #[test]
1115 fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1116 let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1117 let rounded_subtraction = 1.0_f64 - 0.1;
1118 assert_eq!(
1119 subtraction,
1120 Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1121 );
1122
1123 let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1124 let rounded_product = 0.1_f64 * 0.2;
1125 assert_eq!(
1126 product,
1127 Interval::try_new(rounded_product.next_down(), rounded_product)?
1128 );
1129
1130 let below_one = 1.0 - f64::EPSILON;
1131 let above_one = 1.0 + f64::EPSILON;
1132 let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1133 assert_eq!(
1134 binade_boundary,
1135 Interval::try_new(1.0_f64.next_down(), 1.0)?
1136 );
1137 Ok(())
1138 }
1139
1140 #[test]
1141 fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1142 let next = 1.0_f64.next_up();
1143 let difference = Interval::try_from_subtraction(next, 1.0)?;
1144 assert_eq!(difference, Interval::point(f64::EPSILON)?);
1145 Ok(())
1146 }
1147
1148 #[test]
1149 fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1150 for value in [
1151 -f64::MAX,
1152 -1.0,
1153 -f64::MIN_POSITIVE,
1154 -f64::from_bits(1),
1155 -0.0,
1156 0.0,
1157 f64::from_bits(1),
1158 f64::MIN_POSITIVE,
1159 1.0,
1160 f64::MAX,
1161 ] {
1162 for zero in [0.0, -0.0] {
1163 for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1164 let result = Interval::try_from_subtraction(left, right)?;
1165 let expected_bits = if expected == 0.0 {
1166 0
1167 } else {
1168 expected.to_bits()
1169 };
1170 assert_eq!(result.lower().to_bits(), expected_bits);
1171 assert_eq!(result.upper().to_bits(), expected_bits);
1172 }
1173 }
1174 }
1175 Ok(())
1176 }
1177
1178 #[test]
1179 fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1180 for (value, lower, upper) in [
1181 (0.0, 0.0_f64, 0.0),
1182 (-0.0, 0.0, 0.0),
1183 (2.0, 4.0, 4.0),
1184 (-2.0, 4.0, 4.0),
1185 (f64::from_bits(1), 0.0, f64::from_bits(1)),
1186 (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1187 ] {
1188 let squared = Interval::point(value)?.try_square()?;
1189 assert_eq!(squared.lower().to_bits(), lower.to_bits());
1190 assert_eq!(squared.upper().to_bits(), upper.to_bits());
1191 }
1192 for value in [0.1, -0.1] {
1195 let squared = Interval::point(value)?.try_square()?;
1196 assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1197 assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1198 }
1199 Ok(())
1200 }
1201
1202 #[test]
1203 fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1204 let expected = Interval::try_new(4.0, 9.0)?;
1205 for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1206 assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1207 }
1208 Ok(())
1209 }
1210
1211 #[test]
1212 fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1213 let left = Interval::try_new(-1.0, 1.0)?;
1214 for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1217 let right = Interval::try_new(-radius, radius)?;
1218 assert_eq!(
1219 left.try_add(&right)?,
1220 Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1221 );
1222 }
1223 assert_eq!(
1224 left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1225 Interval::try_new(-3.0, 3.0)?
1226 );
1227 Ok(())
1228 }
1229
1230 #[test]
1231 fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1232 let least_subnormal = f64::from_bits(1);
1233 let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1234 assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1235 Ok(())
1236 }
1237
1238 #[test]
1239 fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1240 let error = Interval::point(f64::MAX)?
1241 .try_mul(&Interval::point(2.0)?)
1242 .unwrap_err();
1243 assert_eq!(
1244 error,
1245 LaError::IntervalRangeExhausted {
1246 operation: ArithmeticOperation::IntervalMultiplication,
1247 }
1248 );
1249 Ok(())
1250 }
1251
1252 #[test]
1253 fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1254 let maximum = Interval::point(f64::MAX)?;
1255 let tiny = Interval::point(f64::MIN_POSITIVE)?;
1256 assert_eq!(
1257 maximum.try_add(&maximum),
1258 Err(LaError::IntervalRangeExhausted {
1259 operation: ArithmeticOperation::IntervalAddition,
1260 })
1261 );
1262 assert_eq!(
1263 maximum.try_add(&tiny),
1264 Err(LaError::IntervalRangeExhausted {
1265 operation: ArithmeticOperation::IntervalAddition,
1266 })
1267 );
1268 let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1269 assert_eq!(
1270 nonnegative.try_add(&nonnegative),
1271 Err(LaError::IntervalRangeExhausted {
1272 operation: ArithmeticOperation::IntervalAddition,
1273 })
1274 );
1275 let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1276 for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1277 assert_eq!(
1278 left.try_add(&right),
1279 Err(LaError::IntervalRangeExhausted {
1280 operation: ArithmeticOperation::IntervalAddition,
1281 }),
1282 );
1283 }
1284
1285 let finite_difference = maximum.try_add(&tiny.negate())?;
1286 assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1287 assert!(finite_difference.lower() < finite_difference.upper());
1288 Ok(())
1289 }
1290
1291 #[test]
1292 fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1293 assert_eq!(
1294 Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1295 Err(LaError::IntervalRangeExhausted {
1296 operation: ArithmeticOperation::IntervalSubtraction,
1297 })
1298 );
1299 assert_eq!(
1300 Interval::point(f64::MAX)?.try_square(),
1301 Err(LaError::IntervalRangeExhausted {
1302 operation: ArithmeticOperation::IntervalSquare,
1303 })
1304 );
1305 assert_eq!(
1306 Interval::try_new(-1.0, f64::MAX)?.try_square(),
1307 Err(LaError::IntervalRangeExhausted {
1308 operation: ArithmeticOperation::IntervalSquare,
1309 })
1310 );
1311 Ok(())
1312 }
1313
1314 #[test]
1315 fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1316 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1317 assert_eq!(
1318 matrix.det(),
1319 Err(LaError::IntervalRangeExhausted {
1320 operation: ArithmeticOperation::IntervalDeterminant,
1321 })
1322 );
1323
1324 let accumulating =
1325 IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1326 assert_eq!(
1327 accumulating.det(),
1328 Err(LaError::IntervalRangeExhausted {
1329 operation: ArithmeticOperation::IntervalDeterminant,
1330 })
1331 );
1332 assert_eq!(
1333 accumulating.det_sign(),
1334 Err(LaError::IntervalRangeExhausted {
1335 operation: ArithmeticOperation::IntervalDeterminant,
1336 })
1337 );
1338 Ok(())
1339 }
1340
1341 #[test]
1342 fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1343 {
1344 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1345 assert_eq!(
1346 matrix.det(),
1347 Err(LaError::IntervalRangeExhausted {
1348 operation: ArithmeticOperation::IntervalDeterminant,
1349 })
1350 );
1351 Ok(())
1352 }
1353
1354 #[test]
1355 fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1356 let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1357 assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1358 Ok(())
1359 }
1360
1361 #[test]
1362 fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1363 for (left, right, expected) in [
1364 ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1365 ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1366 ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1367 ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1368 ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1369 ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1370 ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1371 ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1372 ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1373 ] {
1374 let product = Interval::try_new(left.0, left.1)?
1375 .try_mul(&Interval::try_new(right.0, right.1)?)?;
1376 assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1377 }
1378
1379 assert_eq!(
1380 Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1381 Interval::ZERO
1382 );
1383 Ok(())
1384 }
1385
1386 #[test]
1387 fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1388 let half_maximum = f64::MAX / 2.0;
1389 for (left, right) in [
1390 ((half_maximum, f64::MAX), (-2.0, -1.0)),
1391 ((half_maximum, f64::MAX), (1.0, 2.0)),
1392 ((-f64::MAX, 1.0), (-1.0, 2.0)),
1393 ((-1.0, f64::MAX), (-2.0, 1.0)),
1394 ((-f64::MAX, 1.0), (-2.0, 1.0)),
1395 ((-1.0, f64::MAX), (-1.0, 2.0)),
1396 ] {
1397 let result =
1398 Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1399 assert_eq!(
1400 result,
1401 Err(LaError::IntervalRangeExhausted {
1402 operation: ArithmeticOperation::IntervalMultiplication,
1403 }),
1404 "left={left:?}, right={right:?}"
1405 );
1406 }
1407 Ok(())
1408 }
1409
1410 macro_rules! gen_interval_identity_tests {
1411 ($d:literal) => {
1412 paste! {
1413 #[test]
1414 fn [<interval_identity_sign_is_positive_ $d d>]() {
1415 let matrix = IntervalMatrix::<$d>::identity();
1416 assert_eq!(matrix.det(), Ok(Interval::ONE));
1417 assert_eq!(
1418 matrix.det_sign(),
1419 Ok(IntervalDeterminantSign::Positive)
1420 );
1421 }
1422 }
1423 };
1424 }
1425
1426 gen_interval_identity_tests!(0);
1427 gen_interval_identity_tests!(1);
1428 gen_interval_identity_tests!(2);
1429 gen_interval_identity_tests!(3);
1430 gen_interval_identity_tests!(4);
1431 gen_interval_identity_tests!(5);
1432 gen_interval_identity_tests!(6);
1433 gen_interval_identity_tests!(7);
1434
1435 #[test]
1436 fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1437 const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1438 const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1439 Ok(interval) => interval,
1440 Err(_) => panic!("finite ordered literal bounds"),
1441 };
1442 const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1443 assert_eq!(EMPTY, Ok(Interval::ONE));
1444 assert_eq!(SINGLE, Ok(WIDE));
1445 for (lower, upper, sign) in [
1446 (
1447 -f64::MAX,
1448 -f64::from_bits(1),
1449 IntervalDeterminantSign::Negative,
1450 ),
1451 (
1452 f64::from_bits(1),
1453 f64::MAX,
1454 IntervalDeterminantSign::Positive,
1455 ),
1456 (0.0, 0.0, IntervalDeterminantSign::Zero),
1457 (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1458 ] {
1459 let interval = Interval::try_new(lower, upper)?;
1460 let matrix = IntervalMatrix::from_rows([[interval]]);
1461 assert_eq!(matrix.det()?, interval);
1462 assert_eq!(matrix.det_sign()?, sign);
1463 }
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1469 let swapped = IntervalMatrix::<3>::try_from_point_rows([
1470 [0.0, 1.0, 0.0],
1471 [1.0, 0.0, 0.0],
1472 [0.0, 0.0, 1.0],
1473 ])?;
1474 assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1475
1476 let singular = IntervalMatrix::<3>::try_from_point_rows([
1477 [1.0, 2.0, 3.0],
1478 [1.0, 2.0, 3.0],
1479 [0.0, 0.0, 1.0],
1480 ])?;
1481 assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1482 Ok(())
1483 }
1484
1485 #[test]
1486 fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1487 let matrix = IntervalMatrix::<2>::from_rows([
1488 [Interval::ONE, Interval::ZERO],
1489 [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1490 ]);
1491 assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1492 Ok(())
1493 }
1494
1495 #[test]
1496 fn determinant_rejects_dimensions_above_supported_stack_dp() {
1497 assert_matches!(
1498 IntervalMatrix::<8>::identity().det(),
1499 Err(LaError::UnsupportedDimension {
1500 requested: 8,
1501 max: MAX_INTERVAL_MATRIX_DIM,
1502 ..
1503 })
1504 );
1505 }
1506
1507 #[test]
1508 fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1509 let source = Matrix::<2>::identity();
1510 let mut intervals = IntervalMatrix::from_matrix(&source);
1511 let value = Interval::try_new(2.0, 3.0)?;
1512 intervals.set(0, 1, value)?;
1513 assert_eq!(intervals.get(0, 1), Some(value));
1514 assert_eq!(intervals.get(2, 0), None);
1515 assert_eq!(intervals.try_get(0, 1)?, value);
1516 assert_matches!(
1517 intervals.try_get(2, 0),
1518 Err(LaError::IndexOutOfBounds {
1519 row: 2,
1520 col: 0,
1521 dim: 2,
1522 ..
1523 })
1524 );
1525 assert_eq!(intervals.as_rows()[0][1], value);
1526 assert_eq!(intervals.into_rows()[0][1], value);
1527 Ok(())
1528 }
1529
1530 #[test]
1531 fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1532 let mut matrix = IntervalMatrix::<2>::identity();
1533 let before = matrix;
1534 let value = Interval::try_new(2.0, 3.0)?;
1535
1536 assert_eq!(
1537 matrix.set(2, 0, value),
1538 Err(LaError::IndexOutOfBounds {
1539 row: 2,
1540 col: 0,
1541 dim: 2,
1542 })
1543 );
1544 assert_eq!(matrix, before);
1545 assert_eq!(
1546 matrix.set(0, 2, value),
1547 Err(LaError::IndexOutOfBounds {
1548 row: 0,
1549 col: 2,
1550 dim: 2,
1551 })
1552 );
1553 assert_eq!(matrix, before);
1554 Ok(())
1555 }
1556}