1#![forbid(unsafe_code)]
2
3use core::hint::cold_path;
6
7use crate::norm::norm_near_overflow;
8use crate::rounding::{compare_product_with_rounded, two_sum_error};
9use crate::{ArithmeticOperation, LaError};
10
11#[must_use]
46#[non_exhaustive]
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct ScalarWithErrorBound {
49 estimate: f64,
50 absolute_error_bound: f64,
51}
52
53impl ScalarWithErrorBound {
54 #[inline]
56 #[must_use]
57 pub const fn estimate(self) -> f64 {
58 self.estimate
59 }
60
61 #[inline]
63 #[must_use]
64 pub const fn absolute_error_bound(self) -> f64 {
65 self.absolute_error_bound
66 }
67
68 #[expect(
70 clippy::inline_always,
71 reason = "avoid a measured out-of-line endpoint call for each projection"
72 )]
73 #[inline(always)]
74 #[must_use]
75 pub const fn lower_bound(self) -> f64 {
76 if self.absolute_error_bound == 0.0 {
77 return self.estimate;
78 }
79 let rounded = self.estimate - self.absolute_error_bound;
80 if two_sum_error(self.estimate, -self.absolute_error_bound, rounded) < 0.0 {
81 rounded.next_down()
82 } else {
83 rounded
84 }
85 }
86
87 #[expect(
89 clippy::inline_always,
90 reason = "avoid a measured out-of-line endpoint call for each projection"
91 )]
92 #[inline(always)]
93 #[must_use]
94 pub const fn upper_bound(self) -> f64 {
95 if self.absolute_error_bound == 0.0 {
96 return self.estimate;
97 }
98 let rounded = self.estimate + self.absolute_error_bound;
99 if two_sum_error(self.estimate, self.absolute_error_bound, rounded) > 0.0 {
100 rounded.next_up()
101 } else {
102 rounded
103 }
104 }
105
106 #[inline]
113 const fn try_new(estimate: f64, absolute_error_bound: f64) -> Option<Self> {
114 if !estimate.is_finite() || !absolute_error_bound.is_finite() || absolute_error_bound < 0.0
115 {
116 return None;
117 }
118
119 if absolute_error_bound == 0.0 {
120 return Some(Self {
121 estimate,
122 absolute_error_bound: 0.0,
123 });
124 }
125
126 let magnitude = estimate.abs();
127 let outer_rounded = magnitude + absolute_error_bound;
128 if !outer_rounded.is_finite()
129 || (outer_rounded >= f64::MAX
130 && two_sum_error(magnitude, absolute_error_bound, outer_rounded) > 0.0)
131 {
132 return None;
133 }
134
135 Some(Self {
136 estimate,
137 absolute_error_bound,
138 })
139 }
140}
141
142#[derive(Clone, Copy, Debug, PartialEq)]
153struct CertifiedReduction {
154 estimate: f64,
155 magnitude_upper: f64,
156 proof_available: bool,
157 single_exact_product: bool,
158}
159
160impl CertifiedReduction {
161 const ZERO: Self = Self {
162 estimate: 0.0,
163 magnitude_upper: 0.0,
164 proof_available: true,
165 single_exact_product: false,
166 };
167
168 #[expect(
176 clippy::inline_always,
177 reason = "measured per-coordinate reduction-state copy overhead"
178 )]
179 #[inline(always)]
180 const fn add_product(
181 mut self,
182 left: f64,
183 right: f64,
184 operation: ArithmeticOperation,
185 index: usize,
186 ) -> Result<Self, LaError> {
187 if left == 0.0 || right == 0.0 {
188 if !self.proof_available {
194 self.estimate = left.mul_add(right, self.estimate);
195 }
196 return Ok(self);
197 }
198 let prior = self.estimate;
199 let estimate = left.mul_add(right, prior);
200 if !estimate.is_normal() {
201 if !estimate.is_finite() {
202 cold_path();
203 return Err(LaError::non_finite_computation_step(operation, index));
204 }
205 if self.proof_available {
206 self.proof_available =
207 estimate == 0.0 && Self::fma_result_is_exact_zero(left, right, prior);
208 }
209 }
210 if self.proof_available {
211 match Self::add_product_magnitude_upper(self.magnitude_upper, left, right) {
212 Some((magnitude_upper, single_exact_product)) => {
213 self.magnitude_upper = magnitude_upper;
214 self.single_exact_product = single_exact_product;
215 }
216 None => self.proof_available = false,
217 }
218 }
219 self.estimate = estimate;
220 Ok(self)
221 }
222
223 #[inline]
230 const fn fma_result_is_exact_zero(left: f64, right: f64, addend: f64) -> bool {
231 let rounded_product = left * right;
232 let rounded_bits = rounded_product.to_bits();
233 let negated_addend_bits = (-addend).to_bits();
234 let same_rounded_value = rounded_bits == negated_addend_bits
235 || (rounded_bits << 1 == 0 && negated_addend_bits << 1 == 0);
236 rounded_product.is_finite()
237 && same_rounded_value
238 && compare_product_with_rounded(left, right, rounded_product) == 0
239 }
240
241 #[inline]
250 const fn add_product_magnitude_upper(
251 magnitude_upper: f64,
252 left: f64,
253 right: f64,
254 ) -> Option<(f64, bool)> {
255 let left_magnitude = left.abs();
256 let right_magnitude = right.abs();
257 let rounded_product = left_magnitude * right_magnitude;
258 if !rounded_product.is_normal() {
259 return None;
260 }
261
262 if magnitude_upper == 0.0 {
263 let exact = left_magnitude.to_bits() == 1.0_f64.to_bits()
264 || right_magnitude.to_bits() == 1.0_f64.to_bits();
265 let product_upper = if exact {
268 rounded_product
269 } else {
270 rounded_product.next_up()
271 };
272 return if product_upper.is_finite() {
273 Some((product_upper, exact))
274 } else {
275 None
276 };
277 }
278 let rounded_sum = left_magnitude.mul_add(right_magnitude, magnitude_upper);
283 if !rounded_sum.is_finite() {
284 return None;
285 }
286 let sum_upper = rounded_sum.next_up();
287 if sum_upper.is_finite() {
288 Some((sum_upper, false))
289 } else {
290 None
291 }
292 }
293
294 #[expect(
301 clippy::cast_precision_loss,
302 reason = "a usable gamma requires a term count below 2^53, where the cast is exact"
303 )]
304 #[inline]
305 const fn finish(self, term_count: Option<usize>) -> Option<ScalarWithErrorBound> {
306 if !self.proof_available {
307 return None;
308 }
309 if self.magnitude_upper == 0.0 || self.single_exact_product {
310 return ScalarWithErrorBound::try_new(self.estimate, 0.0);
311 }
312
313 let Some(term_count) = term_count else {
314 return None;
315 };
316 let scaled_roundoff = (term_count as f64) * (f64::EPSILON / 2.0);
317 if !scaled_roundoff.is_finite() || scaled_roundoff >= 1.0 {
318 return None;
319 }
320
321 let gamma = scaled_roundoff / (1.0 - scaled_roundoff);
325 let gamma_upper = gamma.next_up();
326 if !gamma_upper.is_finite() {
327 return None;
328 }
329 let rounded_bound = gamma_upper * self.magnitude_upper;
330 if !rounded_bound.is_finite() {
331 return None;
332 }
333 let absolute_error_bound = if rounded_bound == 0.0 {
334 0.0
335 } else {
336 rounded_bound.next_up()
337 };
338 ScalarWithErrorBound::try_new(self.estimate, absolute_error_bound)
339 }
340}
341
342#[must_use]
361#[derive(Clone, Copy, Debug, PartialEq)]
362pub struct Vector<const D: usize> {
363 data: [f64; D],
364}
365
366impl<const D: usize> Vector<D> {
367 #[cfg(test)]
369 #[inline]
370 pub(crate) const fn new(data: [f64; D]) -> Self {
371 match Self::try_new(data) {
372 Ok(vector) => vector,
373 Err(_) => panic!("Vector::new requires finite entries"),
374 }
375 }
376
377 #[inline]
397 pub const fn try_new(data: [f64; D]) -> Result<Self, LaError> {
398 if let Some(index) = Self::first_non_finite_entry(&data) {
399 Err(LaError::non_finite_input_vector(index))
400 } else {
401 Ok(Self { data })
402 }
403 }
404
405 #[inline]
411 pub(crate) const fn from_computation(
412 data: [f64; D],
413 operation: ArithmeticOperation,
414 ) -> Result<Self, LaError> {
415 if let Some(index) = Self::first_non_finite_entry(&data) {
416 Err(LaError::non_finite_computation_step(operation, index))
417 } else {
418 Ok(Self { data })
419 }
420 }
421
422 const fn first_non_finite_entry(data: &[f64; D]) -> Option<usize> {
427 let mut i = 0;
428 while i < D {
429 if !data[i].is_finite() {
430 return Some(i);
431 }
432 i += 1;
433 }
434 None
435 }
436
437 #[inline]
447 pub const fn zero() -> Self {
448 Self { data: [0.0; D] }
449 }
450
451 #[inline]
464 #[must_use]
465 pub const fn as_array(&self) -> &[f64; D] {
466 &self.data
467 }
468
469 #[inline]
483 #[must_use]
484 pub const fn into_array(self) -> [f64; D] {
485 self.data
486 }
487
488 #[inline]
512 pub fn angle(&self, other: &Self) -> Result<f64, LaError> {
513 crate::angle::angle_finite(self, other)
514 }
515
516 #[inline]
540 pub const fn dot(&self, other: &Self) -> Result<f64, LaError> {
541 self.dot_with_operation(other, ArithmeticOperation::VectorDotProduct)
542 }
543
544 #[expect(
596 clippy::inline_always,
597 reason = "eliminate measured Result-return overhead in repeated projections"
598 )]
599 #[inline(always)]
600 pub const fn dot_with_errbound(
601 &self,
602 other: &Self,
603 ) -> Result<Option<ScalarWithErrorBound>, LaError> {
604 let left = self.as_array();
605 let right = other.as_array();
606 let mut reduction = CertifiedReduction::ZERO;
607 let mut i = 0;
608 while i < D {
609 reduction = match reduction.add_product(
610 left[i],
611 right[i],
612 ArithmeticOperation::VectorDotProduct,
613 i,
614 ) {
615 Ok(reduction) => reduction,
616 Err(error) => return Err(error),
617 };
618 i += 1;
619 }
620 Ok(reduction.finish(Some(D)))
621 }
622
623 #[inline]
680 pub const fn dot_difference_with_errbound(
681 &self,
682 left: &Self,
683 right: &Self,
684 ) -> Result<Option<ScalarWithErrorBound>, LaError> {
685 let axis = self.as_array();
686 let left = left.as_array();
687 let right = right.as_array();
688 let mut reduction = CertifiedReduction::ZERO;
689 let mut i = 0;
690 while i < D {
691 reduction = match reduction.add_product(
692 axis[i],
693 left[i],
694 ArithmeticOperation::VectorDotDifference,
695 i,
696 ) {
697 Ok(reduction) => reduction,
698 Err(error) => return Err(error),
699 };
700 reduction = match reduction.add_product(
701 -axis[i],
702 right[i],
703 ArithmeticOperation::VectorDotDifference,
704 i,
705 ) {
706 Ok(reduction) => reduction,
707 Err(error) => return Err(error),
708 };
709 i += 1;
710 }
711 Ok(reduction.finish(D.checked_mul(2)))
712 }
713
714 const fn dot_with_operation(
716 &self,
717 other: &Self,
718 operation: ArithmeticOperation,
719 ) -> Result<f64, LaError> {
720 let lhs = self.as_array();
721 let rhs = other.as_array();
722 let mut acc = 0.0;
723 let mut i = 0;
724 while i < D {
725 acc = lhs[i].mul_add(rhs[i], acc);
726 i += 1;
727 }
728 if acc.is_finite() {
729 Ok(acc)
730 } else {
731 cold_path();
732 Err(Self::dot_non_finite_error(lhs, rhs, operation))
733 }
734 }
735
736 #[cold]
744 const fn dot_non_finite_error(
745 lhs: &[f64; D],
746 rhs: &[f64; D],
747 operation: ArithmeticOperation,
748 ) -> LaError {
749 let mut acc = 0.0;
750 let mut i = 0;
751 let last = D.saturating_sub(1);
752 while i < last {
753 acc = lhs[i].mul_add(rhs[i], acc);
754 if !acc.is_finite() {
755 return LaError::non_finite_computation_step(operation, i);
756 }
757 i += 1;
758 }
759
760 LaError::non_finite_computation_step(operation, last)
761 }
762
763 #[inline]
787 pub const fn norm_squared(&self) -> Result<f64, LaError> {
788 self.dot_with_operation(self, ArithmeticOperation::VectorSquaredNorm)
789 }
790
791 #[inline]
837 pub fn norm(&self) -> Result<f64, LaError> {
838 let mut entries = self.as_array().iter();
839 let mut scale = entries.next().copied().unwrap_or(0.0).abs();
842 let mut scaled_sum = 1.0;
843
844 for &entry in entries {
845 let magnitude = entry.abs();
846 if magnitude == 0.0 {
847 continue;
848 }
849
850 if scale < magnitude {
851 let ratio = scale / magnitude;
852 scaled_sum = (scaled_sum * ratio).mul_add(ratio, 1.0);
853 scale = magnitude;
854 } else {
855 let ratio = magnitude / scale;
856 scaled_sum = ratio.mul_add(ratio, scaled_sum);
857 }
858 }
859
860 let dimension_bits = D.bit_width();
866 let safe_scale = f64::from_bits(u64::from(2046 - dimension_bits) << 52);
867 if scale > safe_scale {
868 return norm_near_overflow(self.as_array(), scale);
869 }
870 Ok(scale * scaled_sum.sqrt())
871 }
872}
873
874impl<const D: usize> Default for Vector<D> {
875 #[inline]
876 fn default() -> Self {
877 Self::zero()
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use core::hint::black_box;
884
885 use approx::assert_abs_diff_eq;
886 use pastey::paste;
887
888 use super::*;
889
890 fn assert_certified_proof_loss_survives_normal_terms<const D: usize>() {
891 let mut left_data = [0.0; D];
892 left_data[0] = f64::MIN_POSITIVE;
893 left_data[D - 1] = 1.0;
894 let mut right_data = [1.0; D];
895 right_data[0] = 0.5;
896 let left = Vector::new(left_data);
897 let right = Vector::new(right_data);
898
899 assert_abs_diff_eq!(left.dot(&right).unwrap(), 1.0, epsilon = 0.0);
902 assert_eq!(left.dot_with_errbound(&right), Ok(None));
903 assert_eq!(
904 left.dot_difference_with_errbound(&right, &Vector::zero()),
905 Ok(None)
906 );
907 }
908
909 fn assert_certified_proof_loss_preserves_later_overflow<const D: usize>() {
910 let mut axis_data = [0.0; D];
911 axis_data[0] = f64::MIN_POSITIVE;
912 axis_data[D - 1] = f64::MAX;
913 let axis = Vector::new(axis_data);
914 let mut left_data = [0.0; D];
915 left_data[0] = 0.5;
916 left_data[D - 1] = 2.0;
917
918 assert_eq!(
919 axis.dot_with_errbound(&Vector::new(left_data)),
920 Err(LaError::non_finite_computation_step(
921 ArithmeticOperation::VectorDotProduct,
922 D - 1,
923 ))
924 );
925 let difference_error = Err(LaError::non_finite_computation_step(
926 ArithmeticOperation::VectorDotDifference,
927 D - 1,
928 ));
929 assert_eq!(
930 axis.dot_difference_with_errbound(&Vector::new(left_data), &Vector::zero()),
931 difference_error
932 );
933
934 left_data[D - 1] = 0.0;
937 let mut right_data = [0.0; D];
938 right_data[D - 1] = -2.0;
939 assert_eq!(
940 axis.dot_difference_with_errbound(&Vector::new(left_data), &Vector::new(right_data)),
941 difference_error
942 );
943 }
944
945 macro_rules! gen_certified_reduction_sequence_tests {
946 ($d:literal) => {
947 paste! {
948 #[test]
949 fn [<certified_proof_loss_survives_normal_terms_ $d d>]() {
950 assert_certified_proof_loss_survives_normal_terms::<$d>();
951 }
952
953 #[test]
954 fn [<certified_proof_loss_preserves_later_overflow_ $d d>]() {
955 assert_certified_proof_loss_preserves_later_overflow::<$d>();
956 }
957 }
958 };
959 }
960
961 gen_certified_reduction_sequence_tests!(2);
962 gen_certified_reduction_sequence_tests!(3);
963 gen_certified_reduction_sequence_tests!(4);
964 gen_certified_reduction_sequence_tests!(5);
965
966 macro_rules! gen_vector_tests {
967 ($d:literal) => {
968 paste! {
969 #[test]
970 fn [<vector_new_as_array_into_array_ $d d>]() {
971 let arr = {
972 let mut arr = [0.0f64; $d];
973 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
974 for (dst, src) in arr.iter_mut().zip(values.iter()) {
975 *dst = *src;
976 }
977 arr
978 };
979
980 let v = Vector::<$d>::new(arr);
981
982 for i in 0..$d {
983 assert_abs_diff_eq!(v.as_array()[i], arr[i], epsilon = 0.0);
984 }
985
986 let out = v.into_array();
987 for i in 0..$d {
988 assert_abs_diff_eq!(out[i], arr[i], epsilon = 0.0);
989 }
990 }
991
992 #[test]
993 fn [<vector_zero_as_array_into_array_default_ $d d>]() {
994 let z = Vector::<$d>::zero();
995 for &x in z.as_array() {
996 assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
997 }
998 for x in z.into_array() {
999 assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
1000 }
1001
1002 let d = Vector::<$d>::default();
1003 for x in d.into_array() {
1004 assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
1005 }
1006 }
1007
1008 #[test]
1009 fn [<vector_dot_and_norm_squared_ $d d>]() {
1010 let a_arr = {
1014 let mut arr = [0.0f64; $d];
1015 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
1016 for (dst, src) in arr.iter_mut().zip(values.iter()) {
1017 *dst = black_box(*src);
1018 }
1019 arr
1020 };
1021 let b_arr = {
1022 let mut arr = [0.0f64; $d];
1023 let values = [-2.0f64, 0.5, 4.0, -1.0, 2.0];
1024 for (dst, src) in arr.iter_mut().zip(values.iter()) {
1025 *dst = black_box(*src);
1026 }
1027 arr
1028 };
1029
1030 let expected_dot = {
1031 let mut acc = 0.0;
1032 let mut i = 0;
1033 while i < $d {
1034 acc = a_arr[i].mul_add(b_arr[i], acc);
1035 i += 1;
1036 }
1037 acc
1038 };
1039 let expected_norm_squared = {
1040 let mut acc = 0.0;
1041 let mut i = 0;
1042 while i < $d {
1043 acc = a_arr[i].mul_add(a_arr[i], acc);
1044 i += 1;
1045 }
1046 acc
1047 };
1048
1049 let a = Vector::<$d>::new(black_box(a_arr));
1050 let b = Vector::<$d>::new(black_box(b_arr));
1051
1052 let dot_fn: fn(&Vector<$d>, &Vector<$d>) -> Result<f64, LaError> =
1055 black_box(Vector::<$d>::dot);
1056 let norm_squared_fn: fn(&Vector<$d>) -> Result<f64, LaError> =
1057 black_box(Vector::<$d>::norm_squared);
1058
1059 assert_abs_diff_eq!(
1060 dot_fn(black_box(&a), black_box(&b)).unwrap(),
1061 expected_dot,
1062 epsilon = 1e-14
1063 );
1064 assert_abs_diff_eq!(
1065 norm_squared_fn(black_box(&a)).unwrap(),
1066 expected_norm_squared,
1067 epsilon = 1e-14
1068 );
1069 }
1070
1071 #[test]
1072 fn [<vector_certified_dot_and_difference_ $d d>]() {
1073 let mut left_data = [0.0; $d];
1074 let mut right_data = [0.0; $d];
1075 let left_values = [1.0, 2.0, 3.0, 4.0, 5.0];
1076 let right_values = [2.0, 3.0, 4.0, 5.0, 6.0];
1077 for (destination, source) in left_data.iter_mut().zip(left_values) {
1078 *destination = source;
1079 }
1080 for (destination, source) in right_data.iter_mut().zip(right_values) {
1081 *destination = source;
1082 }
1083 let left = Vector::<$d>::new(left_data);
1084 let right = Vector::<$d>::new(right_data);
1085 let zero = Vector::<$d>::zero();
1086
1087 let dot = left.dot(&right).unwrap();
1088 let dot_bound = left.dot_with_errbound(&right).unwrap().unwrap();
1089 assert_abs_diff_eq!(dot_bound.estimate(), dot, epsilon = 0.0);
1090 assert!(dot_bound.absolute_error_bound() >= 0.0);
1091 assert!(dot_bound.lower_bound() <= dot);
1092 assert!(dot <= dot_bound.upper_bound());
1093
1094 let difference_bound = left
1095 .dot_difference_with_errbound(&right, &zero)
1096 .unwrap()
1097 .unwrap();
1098 assert_abs_diff_eq!(difference_bound.estimate(), dot, epsilon = 0.0);
1099 assert!(difference_bound.lower_bound() <= dot);
1100 assert!(dot <= difference_bound.upper_bound());
1101 }
1102
1103 #[test]
1104 fn [<vector_try_new_rejects_non_finite_ $d d>]() {
1105 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1106 let mut data = [1.0f64; $d];
1107 data[$d - 1] = value;
1108 assert_eq!(
1109 Vector::<$d>::try_new(data),
1110 Err(LaError::non_finite_input_vector($d - 1))
1111 );
1112 }
1113
1114 let mut data = [1.0f64; $d];
1115 data[0] = f64::INFINITY;
1116 data[$d - 1] = f64::NAN;
1117 assert_eq!(
1118 Vector::<$d>::try_new(data),
1119 Err(LaError::non_finite_input_vector(0))
1120 );
1121 }
1122
1123 #[test]
1124 fn [<vector_from_computation_preserves_failure_provenance_ $d d>]() {
1125 let mut data = [1.0f64; $d];
1126 data[$d - 1] = f64::INFINITY;
1127
1128 assert_eq!(
1129 Vector::<$d>::from_computation(
1130 data,
1131 ArithmeticOperation::LuSolve,
1132 ),
1133 Err(LaError::non_finite_computation_step(
1134 ArithmeticOperation::LuSolve,
1135 $d - 1,
1136 ))
1137 );
1138 }
1139
1140 #[test]
1141 fn [<vector_dot_and_norm_squared_reject_overflow_ $d d>]() {
1142 let mut a_arr = [1.0f64; $d];
1143 a_arr[0] = f64::MAX;
1144 let a = Vector::<$d>::new(a_arr);
1145
1146 let mut b_arr = [1.0f64; $d];
1147 b_arr[0] = 2.0;
1148 let b = Vector::<$d>::new(b_arr);
1149
1150 assert_eq!(
1151 a.dot(&b),
1152 Err(LaError::non_finite_computation_step(
1153 ArithmeticOperation::VectorDotProduct,
1154 0,
1155 ))
1156 );
1157 assert_eq!(
1158 a.dot_with_errbound(&b),
1159 Err(LaError::non_finite_computation_step(
1160 ArithmeticOperation::VectorDotProduct,
1161 0,
1162 ))
1163 );
1164 assert_eq!(
1165 a.dot_difference_with_errbound(&b, &Vector::zero()),
1166 Err(LaError::non_finite_computation_step(
1167 ArithmeticOperation::VectorDotDifference,
1168 0,
1169 ))
1170 );
1171 assert_eq!(
1172 a.norm_squared(),
1173 Err(LaError::non_finite_computation_step(
1174 ArithmeticOperation::VectorSquaredNorm,
1175 0,
1176 ))
1177 );
1178 }
1179
1180 }
1181 };
1182 }
1183
1184 gen_vector_tests!(1);
1186 gen_vector_tests!(2);
1187 gen_vector_tests!(3);
1188 gen_vector_tests!(4);
1189 gen_vector_tests!(5);
1190 gen_vector_tests!(6);
1191 gen_vector_tests!(7);
1192 gen_vector_tests!(8);
1193
1194 fn known_norm_input<const D: usize>() -> ([f64; D], f64) {
1195 let mut data = [0.0; D];
1196 if D == 1 {
1197 data[0] = -5.0;
1198 } else if D >= 2 {
1199 data[0] = -3.0;
1200 data[1] = 4.0;
1201 }
1202 (data, if D == 0 { 0.0 } else { 5.0 })
1203 }
1204
1205 macro_rules! gen_vector_norm_known_answer_tests {
1206 ($d:literal) => {
1207 paste! {
1208 #[test]
1209 fn [<vector_norm_known_answer_ $d d>]() {
1210 let (data, expected) = known_norm_input::<$d>();
1211 let vector = Vector::<$d>::new(data);
1212
1213 assert_eq!(vector.norm(), Ok(expected));
1214 }
1215 }
1216 };
1217 }
1218
1219 gen_vector_norm_known_answer_tests!(0);
1220 gen_vector_norm_known_answer_tests!(1);
1221 gen_vector_norm_known_answer_tests!(2);
1222 gen_vector_norm_known_answer_tests!(3);
1223 gen_vector_norm_known_answer_tests!(4);
1224 gen_vector_norm_known_answer_tests!(5);
1225 gen_vector_norm_known_answer_tests!(6);
1226 gen_vector_norm_known_answer_tests!(7);
1227 gen_vector_norm_known_answer_tests!(8);
1228
1229 macro_rules! gen_vector_replay_tests {
1230 ($d:literal) => {
1231 paste! {
1232 #[test]
1233 fn [<vector_dot_and_norm_squared_report_last_overflowing_step_ $d d>]() {
1234 let mut dot_lhs = [1.0f64; $d];
1235 dot_lhs[$d - 1] = f64::MAX;
1236 let mut dot_rhs = [1.0f64; $d];
1237 dot_rhs[$d - 1] = 2.0;
1238 let dot_lhs = Vector::<$d>::new(dot_lhs);
1239 let dot_rhs = Vector::<$d>::new(dot_rhs);
1240
1241 assert_eq!(
1242 dot_lhs.dot(&dot_rhs),
1243 Err(LaError::non_finite_computation_step(
1244 ArithmeticOperation::VectorDotProduct,
1245 $d - 1,
1246 ))
1247 );
1248
1249 let mut norm_data = [1.0f64; $d];
1250 norm_data[$d - 1] = f64::MAX;
1251 let vector = Vector::<$d>::new(norm_data);
1252
1253 assert_eq!(
1254 vector.norm_squared(),
1255 Err(LaError::non_finite_computation_step(
1256 ArithmeticOperation::VectorSquaredNorm,
1257 $d - 1,
1258 ))
1259 );
1260 }
1261 }
1262 };
1263 }
1264
1265 gen_vector_replay_tests!(2);
1266 gen_vector_replay_tests!(3);
1267 gen_vector_replay_tests!(4);
1268 gen_vector_replay_tests!(5);
1269
1270 macro_rules! gen_vector_const_eval_tests {
1271 ($d:literal, $dot:literal, $norm_squared:literal) => {
1272 paste! {
1273 #[test]
1274 fn [<vector_dot_and_norm_squared_const_eval_ $d d>]() {
1275 const DOT: Result<f64, LaError> = Vector::<$d>::new([1.0; $d])
1276 .dot(&Vector::<$d>::new([2.0; $d]));
1277 const NORM_SQUARED: Result<f64, LaError> =
1278 Vector::<$d>::new([1.0; $d]).norm_squared();
1279
1280 assert_eq!(DOT, Ok($dot));
1281 assert_eq!(NORM_SQUARED, Ok($norm_squared));
1282 }
1283 }
1284 };
1285 }
1286
1287 gen_vector_const_eval_tests!(2, 4.0, 2.0);
1288 gen_vector_const_eval_tests!(3, 6.0, 3.0);
1289 gen_vector_const_eval_tests!(4, 8.0, 4.0);
1290 gen_vector_const_eval_tests!(5, 10.0, 5.0);
1291
1292 #[test]
1293 fn vector_dot_and_norm_squared_overflow_const_eval() {
1294 const DOT: Result<f64, LaError> =
1295 Vector::<2>::new([f64::MAX; 2]).dot(&Vector::<2>::new([1.0; 2]));
1296 const NORM_SQUARED: Result<f64, LaError> = Vector::<2>::new([f64::MAX; 2]).norm_squared();
1297
1298 assert_eq!(
1299 DOT,
1300 Err(LaError::non_finite_computation_step(
1301 ArithmeticOperation::VectorDotProduct,
1302 1,
1303 ))
1304 );
1305 assert_eq!(
1306 NORM_SQUARED,
1307 Err(LaError::non_finite_computation_step(
1308 ArithmeticOperation::VectorSquaredNorm,
1309 0,
1310 ))
1311 );
1312 }
1313
1314 #[test]
1315 fn vector_dot_and_norm_squared_preserve_fma_and_left_to_right_order() {
1316 let dot_large = 9_007_199_254_740_992.0;
1317 let dot_lhs = Vector::<4>::new([dot_large, 1.0, 1.0, 1.0]);
1318 let dot_rhs = Vector::<4>::new([1.0; 4]);
1319 assert_eq!(dot_lhs.dot(&dot_rhs), Ok(dot_large));
1320
1321 let fused_lhs = Vector::<2>::new([f64::MAX, f64::MAX]);
1322 let fused_rhs = Vector::<2>::new([-1.0, 2.0]);
1323 assert_eq!(fused_lhs.dot(&fused_rhs), Ok(f64::MAX));
1324
1325 let norm_large = 134_217_728.0;
1326 let vector = Vector::<4>::new([norm_large, 1.0, 1.0, 1.0]);
1327 assert_eq!(vector.norm_squared(), Ok(norm_large * norm_large));
1328 }
1329
1330 #[test]
1331 fn vector_norm_preserves_zero_sign_and_subnormal_magnitudes() {
1332 let signed_zero = Vector::<4>::new([-0.0, 0.0, -0.0, 0.0]);
1333 assert_eq!(signed_zero.norm().unwrap().to_bits(), 0.0f64.to_bits());
1334
1335 let least_subnormal = f64::from_bits(1);
1336 let subnormal = Vector::<2>::new([3.0 * least_subnormal, -4.0 * least_subnormal]);
1337 assert_eq!(
1338 subnormal.norm().unwrap().to_bits(),
1339 (5.0 * least_subnormal).to_bits()
1340 );
1341 }
1342
1343 #[test]
1344 fn vector_norm_handles_mixed_and_overflowing_magnitudes() {
1345 let large = Vector::<2>::new([1.0e200, -1.0e200]);
1346 let expected = 2.0f64.sqrt() * 1.0e200;
1347 assert_abs_diff_eq!(large.norm().unwrap(), expected, epsilon = 2.0e184);
1348 assert_eq!(
1349 large.norm_squared(),
1350 Err(LaError::non_finite_computation_step(
1351 ArithmeticOperation::VectorSquaredNorm,
1352 0,
1353 )),
1354 );
1355
1356 let mixed = Vector::<4>::new([1.0e200, 1.0e-200, -f64::from_bits(1), 0.0]);
1357 assert_eq!(mixed.norm(), Ok(1.0e200));
1358
1359 let unrepresentable = Vector::<2>::new([f64::MAX, f64::MAX]);
1360 assert_eq!(
1361 unrepresentable.norm(),
1362 Err(LaError::non_finite_computation_scalar(
1363 ArithmeticOperation::VectorNorm,
1364 ))
1365 );
1366 }
1367
1368 #[test]
1369 fn vector_norm_accepts_largest_finite_norm() {
1370 let maximum = Vector::<2>::new([f64::MAX, 0.0]);
1371
1372 assert_eq!(maximum.norm(), Ok(f64::MAX));
1373 }
1374
1375 #[test]
1376 fn certified_dot_preserves_fma_estimate_and_withholds_range_exhausted_bound() {
1377 let left = Vector::<2>::new([f64::MAX, f64::MAX]);
1378 let right = Vector::<2>::new([-1.0, 2.0]);
1379
1380 assert_eq!(left.dot(&right), Ok(f64::MAX));
1381 assert_eq!(left.dot_with_errbound(&right), Ok(None));
1382 }
1383
1384 #[test]
1385 fn certified_dot_withholds_bound_when_finite_endpoints_cannot_be_published() {
1386 let maximum = Vector::<2>::new([f64::MAX.next_down(), 1.0]);
1389 let one = Vector::<2>::new([1.0, 1.0]);
1390
1391 assert_eq!(maximum.dot(&one), Ok(f64::MAX.next_down()));
1392 assert_eq!(maximum.dot_with_errbound(&one), Ok(None));
1393 }
1394
1395 fn assert_one_exact_normal_product<const D: usize>() {
1396 let zero = Vector::<D>::new([0.0; D]);
1397 for value in [f64::MIN_POSITIVE, -2.0, f64::MAX] {
1398 let left =
1399 Vector::<D>::new(core::array::from_fn(|i| if i == 1 { value } else { -0.0 }));
1400 let right = Vector::<D>::new([1.0; D]);
1401 for (result, expected) in [
1402 (left.dot_with_errbound(&right), value),
1403 (left.dot_difference_with_errbound(&right, &zero), value),
1404 (left.dot_difference_with_errbound(&zero, &right), -value),
1405 ] {
1406 let certificate = result.unwrap().unwrap();
1407 assert_eq!(certificate.estimate().to_bits(), expected.to_bits());
1408 assert_eq!(certificate.absolute_error_bound(), 0.0);
1409 assert_eq!(certificate.lower_bound().to_bits(), expected.to_bits());
1410 assert_eq!(certificate.upper_bound().to_bits(), expected.to_bits());
1411 }
1412 }
1413 let inexact = Vector::<D>::new(core::array::from_fn(|i| if i == 1 { 0.1 } else { 0.0 }))
1414 .dot_with_errbound(&Vector::new([0.3; D]))
1415 .unwrap()
1416 .unwrap();
1417 assert!(inexact.absolute_error_bound() > 0.0);
1418 assert_eq!(
1419 Vector::<D>::new(core::array::from_fn(|i| if i == 1 {
1420 f64::MIN_POSITIVE
1421 } else {
1422 0.0
1423 }))
1424 .dot_with_errbound(&Vector::new([0.5; D])),
1425 Ok(None),
1426 );
1427 }
1428
1429 macro_rules! gen_single_exact_product_tests {
1430 ($d:literal) => {
1431 paste! {
1432 #[test]
1433 fn [<single_exact_product_has_zero_error_ $d d>]() {
1434 assert_one_exact_normal_product::<$d>();
1435 }
1436 }
1437 };
1438 }
1439
1440 gen_single_exact_product_tests!(2);
1441 gen_single_exact_product_tests!(3);
1442 gen_single_exact_product_tests!(4);
1443 gen_single_exact_product_tests!(5);
1444 gen_single_exact_product_tests!(6);
1445
1446 #[test]
1447 fn scalar_certificate_checks_both_endpoint_ranges_before_construction() {
1448 const ENDPOINTS: Option<(f64, f64)> =
1449 match ScalarWithErrorBound::try_new(f64::MAX / 2.0, f64::MAX / 2.0) {
1450 Some(value) => Some((value.lower_bound(), value.upper_bound())),
1451 None => None,
1452 };
1453 let previous = f64::MAX.next_down();
1454 let gap = f64::MAX - previous;
1455 for sign in [-1.0, 1.0] {
1456 assert_eq!(
1457 ScalarWithErrorBound::try_new(sign * f64::MAX, f64::from_bits(1)),
1458 None,
1459 );
1460 assert_eq!(
1461 ScalarWithErrorBound::try_new(sign * previous, gap.next_up()),
1462 None,
1463 );
1464 for bound in [gap.next_down(), gap] {
1465 let certificate = ScalarWithErrorBound::try_new(sign * previous, bound)
1466 .expect("both exact endpoints remain within the finite range");
1467 let (lower, upper) = if sign > 0.0 {
1468 (previous.next_down(), f64::MAX)
1469 } else {
1470 (-f64::MAX, -previous.next_down())
1471 };
1472 assert_eq!(certificate.lower_bound().to_bits(), lower.to_bits());
1473 assert_eq!(certificate.upper_bound().to_bits(), upper.to_bits());
1474 }
1475 }
1476 let symmetric = ScalarWithErrorBound::try_new(0.0, f64::MAX).unwrap();
1477 assert_eq!(symmetric.lower_bound(), -f64::MAX);
1478 assert_eq!(symmetric.upper_bound(), f64::MAX);
1479 let negative_zero = ScalarWithErrorBound::try_new(-0.0, -0.0).unwrap();
1480 assert_eq!(negative_zero.lower_bound().to_bits(), (-0.0_f64).to_bits());
1481 assert_eq!(negative_zero.upper_bound().to_bits(), (-0.0_f64).to_bits());
1482
1483 assert_eq!(ENDPOINTS, Some((0.0, f64::MAX)));
1484 }
1485
1486 #[test]
1487 fn certified_dot_withholds_bound_when_magnitude_sum_exhausts_range() {
1488 let maximum = Vector::<2>::new([f64::MAX, f64::MAX]);
1489
1490 for factor in [0.5, 0.75] {
1491 let cancelling = Vector::<2>::new([factor, -factor]);
1492 let estimate = maximum
1493 .dot(&cancelling)
1494 .expect("the cancelling FMA estimate must remain finite");
1495 assert!(estimate.is_finite());
1496 assert_eq!(maximum.dot_with_errbound(&cancelling), Ok(None));
1497 }
1498 }
1499
1500 #[test]
1501 fn certified_bounds_distinguish_conclusive_and_inconclusive_results() {
1502 let conclusive = Vector::<2>::new([1.0, 2.0])
1503 .dot_with_errbound(&Vector::new([3.0, 4.0]))
1504 .unwrap()
1505 .unwrap();
1506 assert_abs_diff_eq!(conclusive.estimate(), 11.0, epsilon = 0.0);
1507 assert!(conclusive.lower_bound() > 1.0);
1508
1509 let inconclusive = Vector::<2>::new([1.0, 1.0])
1510 .dot_with_errbound(&Vector::new([1.0, -1.0]))
1511 .unwrap()
1512 .unwrap();
1513 assert_abs_diff_eq!(inconclusive.estimate(), 0.0, epsilon = 0.0);
1514 assert!(inconclusive.absolute_error_bound() > 0.0);
1515 assert!(inconclusive.lower_bound() < 0.0);
1516 assert!(inconclusive.upper_bound() > 0.0);
1517 }
1518
1519 #[test]
1520 fn certified_dot_withholds_bound_when_finite_magnitude_sum_cannot_be_widened() {
1521 let large = Vector::<2>::new([f64::MAX / 2.0, f64::MAX / 2.0]);
1522 let cancelling = Vector::<2>::new([1.0, -1.0]);
1523
1524 assert_eq!(large.dot(&cancelling), Ok(0.0));
1527 assert_eq!(large.dot_with_errbound(&cancelling), Ok(None));
1528 }
1529
1530 #[test]
1531 fn certified_proof_loss_survives_repeated_underflow_and_a_normal_final_estimate() {
1532 let axis = Vector::<3>::new([f64::from_bits(1), f64::from_bits(1), 1.0]);
1533 let values = Vector::<3>::new([0.5, 0.5, 2.0]);
1534 let zero = Vector::<3>::zero();
1535
1536 assert_eq!(axis.dot(&values), Ok(2.0));
1539 assert_eq!(axis.dot_with_errbound(&values), Ok(None));
1540 assert_eq!(axis.dot_difference_with_errbound(&values, &zero), Ok(None));
1541 }
1542
1543 #[test]
1544 fn zero_products_preserve_proof_loss_and_signed_zero_estimates() {
1545 let operation = ArithmeticOperation::VectorDotProduct;
1546 let underflow = CertifiedReduction::ZERO
1547 .add_product(-f64::from_bits(1), 0.5, operation, 0)
1548 .unwrap();
1549 assert_eq!(underflow.estimate.to_bits(), (-0.0_f64).to_bits());
1550 let after_zero = underflow.add_product(0.0, 1.0, operation, 1).unwrap();
1551 assert_eq!(after_zero.estimate.to_bits(), 0.0_f64.to_bits());
1552 assert!(!after_zero.proof_available);
1553 assert_eq!(after_zero.finish(Some(2)), None);
1554
1555 let normal = CertifiedReduction::ZERO
1556 .add_product(1.0, 2.0, operation, 0)
1557 .unwrap();
1558 let cancelled = normal.add_product(-1.0, 2.0, operation, 1).unwrap();
1559 assert_eq!(cancelled.estimate.to_bits(), 0.0_f64.to_bits());
1560 for state in [CertifiedReduction::ZERO, normal, cancelled] {
1561 for zero in [0.0, -0.0] {
1562 for other in [f64::MAX, -f64::MIN_POSITIVE] {
1563 let next = state.add_product(zero, other, operation, 2).unwrap();
1564 assert_eq!(next, state);
1565 assert_eq!(
1566 next.estimate.to_bits(),
1567 zero.mul_add(other, state.estimate).to_bits()
1568 );
1569 }
1570 }
1571 }
1572 }
1573
1574 #[test]
1575 fn certified_zero_and_signed_zero_have_an_exact_zero_bound() {
1576 let left = Vector::<3>::new([-0.0, 0.0, -0.0]);
1577 let right = Vector::<3>::new([f64::MAX, -1.0, f64::MIN_POSITIVE]);
1578 let bounded = left.dot_with_errbound(&right).unwrap().unwrap();
1579
1580 assert_eq!(bounded.estimate().to_bits(), 0.0_f64.to_bits());
1581 assert_eq!(bounded.absolute_error_bound().to_bits(), 0.0_f64.to_bits());
1582 assert_eq!(bounded.lower_bound().to_bits(), 0.0_f64.to_bits());
1583 assert_eq!(bounded.upper_bound().to_bits(), 0.0_f64.to_bits());
1584 }
1585
1586 #[test]
1587 fn certified_reductions_withhold_bounds_for_subnormal_products() {
1588 let tiny = Vector::<1>::new([f64::MIN_POSITIVE]);
1589 let half = Vector::<1>::new([0.5]);
1590 assert_eq!(tiny.dot_with_errbound(&half), Ok(None));
1591
1592 let min_subnormal = Vector::<1>::new([f64::from_bits(1)]);
1593 assert_eq!(
1594 min_subnormal.dot_with_errbound(&Vector::new([1.0])),
1595 Ok(None)
1596 );
1597 assert_eq!(
1598 min_subnormal.dot_with_errbound(&Vector::new([0.5])),
1599 Ok(None)
1600 );
1601 assert_eq!(
1602 tiny.dot_difference_with_errbound(&half, &Vector::zero()),
1603 Ok(None)
1604 );
1605 }
1606
1607 #[test]
1608 fn certified_reduction_detects_fma_cancellation_below_subnormal_range() {
1609 let near_sqrt_min = f64::from_bits((512_u64 << 52) | 1);
1610 let rounded_product = near_sqrt_min * near_sqrt_min;
1611 assert!(rounded_product.is_normal());
1612 assert_abs_diff_eq!(
1613 near_sqrt_min.mul_add(near_sqrt_min, -rounded_product),
1614 0.0,
1615 epsilon = 0.0
1616 );
1617
1618 let left = Vector::<2>::new([-rounded_product, near_sqrt_min]);
1619 let right = Vector::<2>::new([1.0, near_sqrt_min]);
1620 assert_eq!(left.dot(&right), Ok(0.0));
1621 assert_eq!(left.dot_with_errbound(&right), Ok(None));
1622 }
1623
1624 #[test]
1625 fn certified_dot_handles_mixed_normal_magnitudes() {
1626 let left = Vector::<2>::new([1.0e100, 1.0e-100]);
1627 let right = Vector::<2>::new([1.0e-100, 1.0e100]);
1628 let bounded = left.dot_with_errbound(&right).unwrap().unwrap();
1629
1630 assert_abs_diff_eq!(bounded.estimate(), 2.0, epsilon = 0.0);
1631 assert!(bounded.lower_bound() <= 2.0);
1632 assert!(bounded.upper_bound() >= 2.0);
1633 }
1634
1635 #[test]
1636 fn certified_dot_difference_does_not_round_coordinates_first() {
1637 let scale = 18_014_398_509_481_984.0;
1638 let axis = Vector::<1>::new([scale]);
1639 let left = Vector::<1>::new([1.0]);
1640 let right = Vector::<1>::new([1.0 / scale]);
1641 assert_abs_diff_eq!(left.as_array()[0] - right.as_array()[0], 1.0, epsilon = 0.0);
1642
1643 let bounded = axis
1644 .dot_difference_with_errbound(&left, &right)
1645 .unwrap()
1646 .unwrap();
1647 assert_abs_diff_eq!(bounded.estimate(), scale, epsilon = 0.0);
1648 assert!(bounded.lower_bound() < scale);
1649 assert!(bounded.upper_bound() >= scale);
1650 }
1651
1652 #[test]
1653 fn certified_reductions_are_const_evaluable() {
1654 const DOT: Result<Option<ScalarWithErrorBound>, LaError> =
1655 Vector::<2>::new([1.0, 2.0]).dot_with_errbound(&Vector::<2>::new([3.0, 4.0]));
1656 const DIFFERENCE: Result<Option<ScalarWithErrorBound>, LaError> =
1657 Vector::<2>::new([2.0, -1.0]).dot_difference_with_errbound(
1658 &Vector::<2>::new([4.0, 1.0]),
1659 &Vector::<2>::new([1.0, 3.0]),
1660 );
1661
1662 assert_abs_diff_eq!(DOT.unwrap().unwrap().estimate(), 11.0, epsilon = 0.0);
1663 assert_abs_diff_eq!(DIFFERENCE.unwrap().unwrap().estimate(), 8.0, epsilon = 0.0);
1664 }
1665
1666 #[test]
1667 fn certified_dot_difference_reports_second_fma_overflow() {
1668 let axis = Vector::<2>::new([1.0, f64::MAX]);
1669 let left = Vector::<2>::new([0.0, 1.0]);
1670 let right = Vector::<2>::new([0.0, -1.0]);
1671
1672 assert_eq!(
1673 axis.dot_difference_with_errbound(&left, &right),
1674 Err(LaError::non_finite_computation_step(
1675 ArithmeticOperation::VectorDotDifference,
1676 1,
1677 ))
1678 );
1679 }
1680
1681 #[test]
1682 fn vector_dot_and_norm_squared_report_first_middle_overflowing_step() {
1683 let dot_lhs = Vector::<3>::new([f64::MAX, f64::MAX, 1.0]);
1684 let dot_rhs = Vector::<3>::new([1.0; 3]);
1685 assert_eq!(
1686 dot_lhs.dot(&dot_rhs),
1687 Err(LaError::non_finite_computation_step(
1688 ArithmeticOperation::VectorDotProduct,
1689 1,
1690 ))
1691 );
1692
1693 let norm_large = 1.0e154;
1694 let vector = Vector::<3>::new([norm_large, norm_large, 1.0]);
1695 assert_eq!(
1696 vector.norm_squared(),
1697 Err(LaError::non_finite_computation_step(
1698 ArithmeticOperation::VectorSquaredNorm,
1699 1,
1700 ))
1701 );
1702 }
1703
1704 #[test]
1705 fn zero_dimension_vector_has_zero_dot_and_norm() {
1706 let vector = Vector::<0>::try_new([]).unwrap();
1707
1708 assert_eq!(vector.as_array().as_slice(), []);
1709 assert_eq!(vector.into_array().as_slice(), []);
1710 assert_eq!(vector.dot(&Vector::zero()), Ok(0.0));
1711 let dot_bound = vector.dot_with_errbound(&Vector::zero()).unwrap().unwrap();
1712 assert_abs_diff_eq!(dot_bound.absolute_error_bound(), 0.0, epsilon = 0.0);
1713 let difference_bound = vector
1714 .dot_difference_with_errbound(&Vector::zero(), &Vector::zero())
1715 .unwrap()
1716 .unwrap();
1717 assert_abs_diff_eq!(difference_bound.absolute_error_bound(), 0.0, epsilon = 0.0);
1718 assert_eq!(vector.norm_squared(), Ok(0.0));
1719 }
1720
1721 #[test]
1722 fn certified_dot_withholds_bound_when_exact_product_exceeds_finite_range() {
1723 let left_value = f64::from_bits(0x7fe3_0319_b612_3729);
1724 let right_value = f64::from_bits(0x3ffa_ee21_bf46_bc00);
1725 let left = Vector::<1>::new([left_value]);
1726 let right = Vector::<1>::new([right_value]);
1727
1728 assert!(left_value.mul_add(right_value, -f64::MAX) > 0.0);
1729 assert_eq!(left.dot(&right), Ok(f64::MAX));
1730 assert_eq!(left.dot_with_errbound(&right), Ok(None));
1731 }
1732}