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 remaining = subset;
922 while remaining != 0 {
923 let column = remaining.trailing_zeros() as usize;
924 let column_bit = 1_usize << column;
925 remaining &= remaining - 1;
926 let previous = subset ^ column_bit;
927 let mut term =
928 match partials[previous].try_mul_for(&self.rows[row][column], operation) {
929 Ok(term) => term,
930 Err(error) => return Err(error),
931 };
932 if !remaining.count_ones().is_multiple_of(2) {
933 term = term.negate();
934 }
935 sum = match sum.try_add_for(&term, operation) {
936 Ok(next_sum) => next_sum,
937 Err(error) => return Err(error),
938 };
939 }
940 partials[subset] = sum;
941 subset += 1;
942 }
943
944 Ok(partials[state_count - 1])
945 }
946
947 #[inline]
976 pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
977 let determinant = match self.det() {
978 Ok(determinant) => determinant,
979 Err(error) => return Err(error),
980 };
981 if determinant.lower > 0.0 {
982 Ok(IntervalDeterminantSign::Positive)
983 } else if determinant.upper < 0.0 {
984 Ok(IntervalDeterminantSign::Negative)
985 } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
986 Ok(IntervalDeterminantSign::Zero)
987 } else {
988 Ok(IntervalDeterminantSign::Inconclusive)
989 }
990 }
991}
992
993impl<const D: usize> Default for IntervalMatrix<D> {
994 #[inline]
995 fn default() -> Self {
996 Self::zero()
997 }
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use core::assert_matches;
1003
1004 use pastey::paste;
1005
1006 use super::*;
1007 use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1008
1009 #[test]
1010 fn point_and_bounds_enforce_interval_invariants() {
1011 assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1012 assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1013 assert_matches!(
1014 Interval::point(f64::NAN),
1015 Err(LaError::NonFinite {
1016 location: NonFiniteLocation::Scalar,
1017 origin: NonFiniteOrigin::Input,
1018 ..
1019 })
1020 );
1021 assert_matches!(
1022 Interval::try_new(2.0, 1.0),
1023 Err(LaError::InvertedInterval {
1024 lower: 2.0,
1025 upper: 1.0,
1026 ..
1027 })
1028 );
1029 }
1030
1031 #[test]
1032 fn constructors_preserve_non_finite_input_locations() {
1033 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1034 assert_eq!(
1035 Interval::try_new(value, 0.0),
1036 Err(LaError::NonFinite {
1037 location: NonFiniteLocation::IntervalBound {
1038 bound: IntervalBound::Lower,
1039 },
1040 origin: NonFiniteOrigin::Input,
1041 })
1042 );
1043 assert_eq!(
1044 Interval::try_new(0.0, value),
1045 Err(LaError::NonFinite {
1046 location: NonFiniteLocation::IntervalBound {
1047 bound: IntervalBound::Upper,
1048 },
1049 origin: NonFiniteOrigin::Input,
1050 })
1051 );
1052 assert_eq!(
1053 Interval::try_from_subtraction(value, 0.0),
1054 Err(LaError::NonFinite {
1055 location: NonFiniteLocation::IntervalOperand {
1056 operand: IntervalOperand::Left,
1057 },
1058 origin: NonFiniteOrigin::Input,
1059 })
1060 );
1061 assert_eq!(
1062 Interval::try_from_subtraction(0.0, value),
1063 Err(LaError::NonFinite {
1064 location: NonFiniteLocation::IntervalOperand {
1065 operand: IntervalOperand::Right,
1066 },
1067 origin: NonFiniteOrigin::Input,
1068 })
1069 );
1070 }
1071
1072 assert_eq!(
1073 Interval::try_new(f64::NAN, f64::INFINITY),
1074 Err(LaError::NonFinite {
1075 location: NonFiniteLocation::IntervalBound {
1076 bound: IntervalBound::Lower,
1077 },
1078 origin: NonFiniteOrigin::Input,
1079 })
1080 );
1081 assert_eq!(
1082 Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1083 Err(LaError::NonFinite {
1084 location: NonFiniteLocation::IntervalOperand {
1085 operand: IntervalOperand::Left,
1086 },
1087 origin: NonFiniteOrigin::Input,
1088 })
1089 );
1090
1091 let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1092 assert_eq!(
1093 IntervalMatrix::<2>::try_from_point_rows(rows),
1094 Err(LaError::NonFinite {
1095 location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1096 origin: NonFiniteOrigin::Input,
1097 })
1098 );
1099 }
1100
1101 #[test]
1102 fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1103 let one = Interval::point(1.0)?;
1104 let two = Interval::point(2.0)?;
1105 assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1106 assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1107 assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1108 assert_eq!(
1109 Interval::try_new(-2.0, -1.0)?.negate(),
1110 Interval::try_new(1.0, 2.0)?
1111 );
1112 Ok(())
1113 }
1114
1115 #[test]
1116 fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1117 let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1118 let rounded_subtraction = 1.0_f64 - 0.1;
1119 assert_eq!(
1120 subtraction,
1121 Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1122 );
1123
1124 let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1125 let rounded_product = 0.1_f64 * 0.2;
1126 assert_eq!(
1127 product,
1128 Interval::try_new(rounded_product.next_down(), rounded_product)?
1129 );
1130
1131 let below_one = 1.0 - f64::EPSILON;
1132 let above_one = 1.0 + f64::EPSILON;
1133 let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1134 assert_eq!(
1135 binade_boundary,
1136 Interval::try_new(1.0_f64.next_down(), 1.0)?
1137 );
1138 Ok(())
1139 }
1140
1141 #[test]
1142 fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1143 let next = 1.0_f64.next_up();
1144 let difference = Interval::try_from_subtraction(next, 1.0)?;
1145 assert_eq!(difference, Interval::point(f64::EPSILON)?);
1146 Ok(())
1147 }
1148
1149 #[test]
1150 fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1151 for value in [
1152 -f64::MAX,
1153 -1.0,
1154 -f64::MIN_POSITIVE,
1155 -f64::from_bits(1),
1156 -0.0,
1157 0.0,
1158 f64::from_bits(1),
1159 f64::MIN_POSITIVE,
1160 1.0,
1161 f64::MAX,
1162 ] {
1163 for zero in [0.0, -0.0] {
1164 for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1165 let result = Interval::try_from_subtraction(left, right)?;
1166 let expected_bits = if expected == 0.0 {
1167 0
1168 } else {
1169 expected.to_bits()
1170 };
1171 assert_eq!(result.lower().to_bits(), expected_bits);
1172 assert_eq!(result.upper().to_bits(), expected_bits);
1173 }
1174 }
1175 }
1176 Ok(())
1177 }
1178
1179 #[test]
1180 fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1181 for (value, lower, upper) in [
1182 (0.0, 0.0_f64, 0.0),
1183 (-0.0, 0.0, 0.0),
1184 (2.0, 4.0, 4.0),
1185 (-2.0, 4.0, 4.0),
1186 (f64::from_bits(1), 0.0, f64::from_bits(1)),
1187 (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1188 ] {
1189 let squared = Interval::point(value)?.try_square()?;
1190 assert_eq!(squared.lower().to_bits(), lower.to_bits());
1191 assert_eq!(squared.upper().to_bits(), upper.to_bits());
1192 }
1193 for value in [0.1, -0.1] {
1196 let squared = Interval::point(value)?.try_square()?;
1197 assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1198 assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1199 }
1200 Ok(())
1201 }
1202
1203 #[test]
1204 fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1205 let expected = Interval::try_new(4.0, 9.0)?;
1206 for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1207 assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1208 }
1209 Ok(())
1210 }
1211
1212 #[test]
1213 fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1214 let left = Interval::try_new(-1.0, 1.0)?;
1215 for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1218 let right = Interval::try_new(-radius, radius)?;
1219 assert_eq!(
1220 left.try_add(&right)?,
1221 Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1222 );
1223 }
1224 assert_eq!(
1225 left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1226 Interval::try_new(-3.0, 3.0)?
1227 );
1228 Ok(())
1229 }
1230
1231 #[test]
1232 fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1233 let least_subnormal = f64::from_bits(1);
1234 let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1235 assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1236 Ok(())
1237 }
1238
1239 #[test]
1240 fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1241 let error = Interval::point(f64::MAX)?
1242 .try_mul(&Interval::point(2.0)?)
1243 .unwrap_err();
1244 assert_eq!(
1245 error,
1246 LaError::IntervalRangeExhausted {
1247 operation: ArithmeticOperation::IntervalMultiplication,
1248 }
1249 );
1250 Ok(())
1251 }
1252
1253 #[test]
1254 fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1255 let maximum = Interval::point(f64::MAX)?;
1256 let tiny = Interval::point(f64::MIN_POSITIVE)?;
1257 assert_eq!(
1258 maximum.try_add(&maximum),
1259 Err(LaError::IntervalRangeExhausted {
1260 operation: ArithmeticOperation::IntervalAddition,
1261 })
1262 );
1263 assert_eq!(
1264 maximum.try_add(&tiny),
1265 Err(LaError::IntervalRangeExhausted {
1266 operation: ArithmeticOperation::IntervalAddition,
1267 })
1268 );
1269 let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1270 assert_eq!(
1271 nonnegative.try_add(&nonnegative),
1272 Err(LaError::IntervalRangeExhausted {
1273 operation: ArithmeticOperation::IntervalAddition,
1274 })
1275 );
1276 let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1277 for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1278 assert_eq!(
1279 left.try_add(&right),
1280 Err(LaError::IntervalRangeExhausted {
1281 operation: ArithmeticOperation::IntervalAddition,
1282 }),
1283 );
1284 }
1285
1286 let finite_difference = maximum.try_add(&tiny.negate())?;
1287 assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1288 assert!(finite_difference.lower() < finite_difference.upper());
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1294 assert_eq!(
1295 Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1296 Err(LaError::IntervalRangeExhausted {
1297 operation: ArithmeticOperation::IntervalSubtraction,
1298 })
1299 );
1300 assert_eq!(
1301 Interval::point(f64::MAX)?.try_square(),
1302 Err(LaError::IntervalRangeExhausted {
1303 operation: ArithmeticOperation::IntervalSquare,
1304 })
1305 );
1306 assert_eq!(
1307 Interval::try_new(-1.0, f64::MAX)?.try_square(),
1308 Err(LaError::IntervalRangeExhausted {
1309 operation: ArithmeticOperation::IntervalSquare,
1310 })
1311 );
1312 Ok(())
1313 }
1314
1315 #[test]
1316 fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1317 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1318 assert_eq!(
1319 matrix.det(),
1320 Err(LaError::IntervalRangeExhausted {
1321 operation: ArithmeticOperation::IntervalDeterminant,
1322 })
1323 );
1324
1325 let accumulating =
1326 IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1327 assert_eq!(
1328 accumulating.det(),
1329 Err(LaError::IntervalRangeExhausted {
1330 operation: ArithmeticOperation::IntervalDeterminant,
1331 })
1332 );
1333 assert_eq!(
1334 accumulating.det_sign(),
1335 Err(LaError::IntervalRangeExhausted {
1336 operation: ArithmeticOperation::IntervalDeterminant,
1337 })
1338 );
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1344 {
1345 let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1346 assert_eq!(
1347 matrix.det(),
1348 Err(LaError::IntervalRangeExhausted {
1349 operation: ArithmeticOperation::IntervalDeterminant,
1350 })
1351 );
1352 Ok(())
1353 }
1354
1355 #[test]
1356 fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1357 let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1358 assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1359 Ok(())
1360 }
1361
1362 #[test]
1363 fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1364 for (left, right, expected) in [
1365 ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1366 ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1367 ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1368 ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1369 ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1370 ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1371 ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1372 ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1373 ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1374 ] {
1375 let product = Interval::try_new(left.0, left.1)?
1376 .try_mul(&Interval::try_new(right.0, right.1)?)?;
1377 assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1378 }
1379
1380 assert_eq!(
1381 Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1382 Interval::ZERO
1383 );
1384 Ok(())
1385 }
1386
1387 #[test]
1388 fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1389 let half_maximum = f64::MAX / 2.0;
1390 for (left, right) in [
1391 ((half_maximum, f64::MAX), (-2.0, -1.0)),
1392 ((half_maximum, f64::MAX), (1.0, 2.0)),
1393 ((-f64::MAX, 1.0), (-1.0, 2.0)),
1394 ((-1.0, f64::MAX), (-2.0, 1.0)),
1395 ((-f64::MAX, 1.0), (-2.0, 1.0)),
1396 ((-1.0, f64::MAX), (-1.0, 2.0)),
1397 ] {
1398 let result =
1399 Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1400 assert_eq!(
1401 result,
1402 Err(LaError::IntervalRangeExhausted {
1403 operation: ArithmeticOperation::IntervalMultiplication,
1404 }),
1405 "left={left:?}, right={right:?}"
1406 );
1407 }
1408 Ok(())
1409 }
1410
1411 macro_rules! gen_interval_identity_tests {
1412 ($d:literal) => {
1413 paste! {
1414 #[test]
1415 fn [<interval_identity_sign_is_positive_ $d d>]() {
1416 let matrix = IntervalMatrix::<$d>::identity();
1417 assert_eq!(matrix.det(), Ok(Interval::ONE));
1418 assert_eq!(
1419 matrix.det_sign(),
1420 Ok(IntervalDeterminantSign::Positive)
1421 );
1422 }
1423 }
1424 };
1425 }
1426
1427 gen_interval_identity_tests!(0);
1428 gen_interval_identity_tests!(1);
1429 gen_interval_identity_tests!(2);
1430 gen_interval_identity_tests!(3);
1431 gen_interval_identity_tests!(4);
1432 gen_interval_identity_tests!(5);
1433 gen_interval_identity_tests!(6);
1434 gen_interval_identity_tests!(7);
1435
1436 #[test]
1437 fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1438 const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1439 const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1440 Ok(interval) => interval,
1441 Err(_) => panic!("finite ordered literal bounds"),
1442 };
1443 const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1444 assert_eq!(EMPTY, Ok(Interval::ONE));
1445 assert_eq!(SINGLE, Ok(WIDE));
1446 for (lower, upper, sign) in [
1447 (
1448 -f64::MAX,
1449 -f64::from_bits(1),
1450 IntervalDeterminantSign::Negative,
1451 ),
1452 (
1453 f64::from_bits(1),
1454 f64::MAX,
1455 IntervalDeterminantSign::Positive,
1456 ),
1457 (0.0, 0.0, IntervalDeterminantSign::Zero),
1458 (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1459 ] {
1460 let interval = Interval::try_new(lower, upper)?;
1461 let matrix = IntervalMatrix::from_rows([[interval]]);
1462 assert_eq!(matrix.det()?, interval);
1463 assert_eq!(matrix.det_sign()?, sign);
1464 }
1465 Ok(())
1466 }
1467
1468 #[test]
1469 fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1470 let swapped = IntervalMatrix::<3>::try_from_point_rows([
1471 [0.0, 1.0, 0.0],
1472 [1.0, 0.0, 0.0],
1473 [0.0, 0.0, 1.0],
1474 ])?;
1475 assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1476
1477 let singular = IntervalMatrix::<3>::try_from_point_rows([
1478 [1.0, 2.0, 3.0],
1479 [1.0, 2.0, 3.0],
1480 [0.0, 0.0, 1.0],
1481 ])?;
1482 assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1483 Ok(())
1484 }
1485
1486 #[test]
1487 fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1488 let matrix = IntervalMatrix::<2>::from_rows([
1489 [Interval::ONE, Interval::ZERO],
1490 [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1491 ]);
1492 assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1493 Ok(())
1494 }
1495
1496 #[test]
1497 fn determinant_rejects_dimensions_above_supported_stack_dp() {
1498 assert_matches!(
1499 IntervalMatrix::<8>::identity().det(),
1500 Err(LaError::UnsupportedDimension {
1501 requested: 8,
1502 max: MAX_INTERVAL_MATRIX_DIM,
1503 ..
1504 })
1505 );
1506 }
1507
1508 #[test]
1509 fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1510 let source = Matrix::<2>::identity();
1511 let mut intervals = IntervalMatrix::from_matrix(&source);
1512 let value = Interval::try_new(2.0, 3.0)?;
1513 intervals.set(0, 1, value)?;
1514 assert_eq!(intervals.get(0, 1), Some(value));
1515 assert_eq!(intervals.get(2, 0), None);
1516 assert_eq!(intervals.try_get(0, 1)?, value);
1517 assert_matches!(
1518 intervals.try_get(2, 0),
1519 Err(LaError::IndexOutOfBounds {
1520 row: 2,
1521 col: 0,
1522 dim: 2,
1523 ..
1524 })
1525 );
1526 assert_eq!(intervals.as_rows()[0][1], value);
1527 assert_eq!(intervals.into_rows()[0][1], value);
1528 Ok(())
1529 }
1530
1531 #[test]
1532 fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1533 let mut matrix = IntervalMatrix::<2>::identity();
1534 let before = matrix;
1535 let value = Interval::try_new(2.0, 3.0)?;
1536
1537 assert_eq!(
1538 matrix.set(2, 0, value),
1539 Err(LaError::IndexOutOfBounds {
1540 row: 2,
1541 col: 0,
1542 dim: 2,
1543 })
1544 );
1545 assert_eq!(matrix, before);
1546 assert_eq!(
1547 matrix.set(0, 2, value),
1548 Err(LaError::IndexOutOfBounds {
1549 row: 0,
1550 col: 2,
1551 dim: 2,
1552 })
1553 );
1554 assert_eq!(matrix, before);
1555 Ok(())
1556 }
1557}