Skip to main content

la_stack/
vector.rs

1#![forbid(unsafe_code)]
2
3//! Fixed-size, stack-allocated vectors.
4
5use 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/// A scalar estimate paired with a certified absolute error bound.
12///
13/// Values of this type are produced by [`Vector::dot_with_errbound`] and
14/// [`Vector::dot_difference_with_errbound`]. The exact-real value of the
15/// corresponding expression over the stored binary64 inputs lies between
16/// [`lower_bound`](Self::lower_bound) and [`upper_bound`](Self::upper_bound),
17/// and differs from [`estimate`](Self::estimate) by at most
18/// [`absolute_error_bound`](Self::absolute_error_bound).
19///
20/// The bound certifies floating-point roundoff in one specified arithmetic
21/// tree. It is not a caller-selected numerical tolerance and does not classify
22/// an interval containing zero as equality.
23///
24/// Callers cannot construct this type directly. Every value has a finite
25/// estimate, a finite non-negative absolute error bound, and finite ordered
26/// endpoints.
27///
28/// # Examples
29/// ```
30/// use core::assert_matches;
31/// use la_stack::prelude::*;
32///
33/// # fn main() -> Result<(), LaError> {
34/// let left = Vector::<2>::try_new([1.0, 2.0])?;
35/// let right = Vector::<2>::try_new([3.0, 4.0])?;
36/// let certificate = left.dot_with_errbound(&right)?;
37/// assert_eq!(certificate.map(ScalarWithErrorBound::estimate), Some(11.0));
38/// // Preserve None: without a certificate, an exact fallback is needed
39/// // before making a claim about the exact-real result.
40/// let enclosure = certificate.map(|bounded| (bounded.lower_bound(), bounded.upper_bound()));
41/// assert_matches!(enclosure, Some((lower, upper)) if lower <= 11.0 && 11.0 <= upper);
42/// # Ok(())
43/// # }
44/// ```
45#[must_use]
46#[non_exhaustive]
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct ScalarWithErrorBound {
49    estimate: f64,
50    absolute_error_bound: f64,
51    lower_bound: f64,
52    upper_bound: f64,
53}
54
55impl ScalarWithErrorBound {
56    /// Return the rounded scalar estimate.
57    #[inline]
58    #[must_use]
59    pub const fn estimate(self) -> f64 {
60        self.estimate
61    }
62
63    /// Return the certified absolute error bound.
64    #[inline]
65    #[must_use]
66    pub const fn absolute_error_bound(self) -> f64 {
67        self.absolute_error_bound
68    }
69
70    /// Return a finite outward-rounded lower bound on the exact-real value.
71    #[inline]
72    #[must_use]
73    pub const fn lower_bound(self) -> f64 {
74        self.lower_bound
75    }
76
77    /// Return a finite outward-rounded upper bound on the exact-real value.
78    #[inline]
79    #[must_use]
80    pub const fn upper_bound(self) -> f64 {
81        self.upper_bound
82    }
83
84    /// Construct a validated public result with finite outward endpoints.
85    ///
86    /// Exact addition residuals determine whether either rounded endpoint must
87    /// move by one binary64 value. Returning `None` instead of publishing an
88    /// infinite endpoint enforces the public proof-unavailable contract.
89    const fn try_new(estimate: f64, absolute_error_bound: f64) -> Option<Self> {
90        if !estimate.is_finite() || !absolute_error_bound.is_finite() || absolute_error_bound < 0.0
91        {
92            return None;
93        }
94
95        if absolute_error_bound == 0.0 {
96            return Some(Self {
97                estimate,
98                absolute_error_bound: 0.0,
99                lower_bound: estimate,
100                upper_bound: estimate,
101            });
102        }
103
104        let lower_rounded = estimate - absolute_error_bound;
105        let upper_rounded = estimate + absolute_error_bound;
106        if !lower_rounded.is_finite() || !upper_rounded.is_finite() {
107            return None;
108        }
109
110        let lower_error = two_sum_error(estimate, -absolute_error_bound, lower_rounded);
111        let upper_error = two_sum_error(estimate, absolute_error_bound, upper_rounded);
112        if !lower_error.is_finite() || !upper_error.is_finite() {
113            return None;
114        }
115
116        let lower_bound = if lower_error < 0.0 {
117            lower_rounded.next_down()
118        } else {
119            lower_rounded
120        };
121        let upper_bound = if upper_error > 0.0 {
122            upper_rounded.next_up()
123        } else {
124            upper_rounded
125        };
126        if !lower_bound.is_finite() || !upper_bound.is_finite() {
127            return None;
128        }
129
130        Some(Self {
131            estimate,
132            absolute_error_bound,
133            lower_bound,
134            upper_bound,
135        })
136    }
137}
138
139/// State for one certified left-to-right FMA reduction.
140///
141/// The rounded estimate continues accumulating after `proof_available` becomes
142/// false. This lets the public methods distinguish a non-finite estimate
143/// (`Err`) from a finite estimate whose proof arithmetic is unavailable
144/// (`Ok(None)`). `magnitude_upper` encloses the sum of exact product
145/// magnitudes while that proof remains available.
146#[derive(Clone, Copy, Debug, PartialEq)]
147struct CertifiedReduction {
148    estimate: f64,
149    magnitude_upper: f64,
150    proof_available: bool,
151}
152
153impl CertifiedReduction {
154    const ZERO: Self = Self {
155        estimate: 0.0,
156        magnitude_upper: 0.0,
157        proof_available: true,
158    };
159
160    /// Add one exact-real product through one rounded FMA.
161    ///
162    /// A non-finite estimate becomes a typed public error. Underflow or range
163    /// loss confined to proof construction instead clears `proof_available`,
164    /// preserving the finite estimate for the eventual `Ok(None)` result.
165    const fn add_product(
166        mut self,
167        left: f64,
168        right: f64,
169        operation: ArithmeticOperation,
170        index: usize,
171    ) -> Result<Self, LaError> {
172        let prior = self.estimate;
173        let estimate = left.mul_add(right, prior);
174        if !estimate.is_finite() {
175            cold_path();
176            return Err(LaError::non_finite_computation_step(operation, index));
177        }
178
179        if self.proof_available {
180            self.proof_available = estimate.is_normal()
181                || (estimate == 0.0 && Self::fma_result_is_exact_zero(left, right, prior));
182        }
183        if self.proof_available {
184            match Self::add_product_magnitude_upper(self.magnitude_upper, left, right) {
185                Some(magnitude_upper) => self.magnitude_upper = magnitude_upper,
186                None => self.proof_available = false,
187            }
188        }
189        self.estimate = estimate;
190        Ok(self)
191    }
192
193    /// Return whether `left × right + addend` is exactly zero.
194    ///
195    /// This distinguishes exact cancellation from a nonzero value rounded to
196    /// zero. Only exact zero is admissible under the relative-error model used
197    /// by the public certified reductions.
198    const fn fma_result_is_exact_zero(left: f64, right: f64, addend: f64) -> bool {
199        if left == 0.0 || right == 0.0 {
200            return addend == 0.0;
201        }
202
203        let rounded_product = left * right;
204        let rounded_bits = rounded_product.to_bits();
205        let negated_addend_bits = (-addend).to_bits();
206        let same_rounded_value = rounded_bits == negated_addend_bits
207            || (rounded_bits << 1 == 0 && negated_addend_bits << 1 == 0);
208        rounded_product.is_finite()
209            && same_rounded_value
210            && compare_product_with_rounded(left, right, rounded_product) == 0
211    }
212
213    /// Add an upward-rounded bound on `|left × right|` to the magnitude sum.
214    ///
215    /// `None` means a nonzero product was not normal or the product/sum could
216    /// not be enclosed by a finite binary64 value, so the public result must be
217    /// proof-unavailable.
218    const fn add_product_magnitude_upper(
219        magnitude_upper: f64,
220        left: f64,
221        right: f64,
222    ) -> Option<f64> {
223        if left == 0.0 || right == 0.0 {
224            return Some(magnitude_upper);
225        }
226
227        let left_magnitude = left.abs();
228        let right_magnitude = right.abs();
229        let rounded_product = left_magnitude * right_magnitude;
230        if !rounded_product.is_normal() {
231            return None;
232        }
233
234        let product_upper =
235            if compare_product_with_rounded(left_magnitude, right_magnitude, rounded_product) > 0 {
236                rounded_product.next_up()
237            } else {
238                rounded_product
239            };
240        if !product_upper.is_finite() {
241            return None;
242        }
243
244        if magnitude_upper == 0.0 {
245            return Some(product_upper);
246        }
247        let rounded_sum = magnitude_upper + product_upper;
248        if !rounded_sum.is_finite() {
249            return None;
250        }
251        let sum_upper = rounded_sum.next_up();
252        if sum_upper.is_finite() {
253            Some(sum_upper)
254        } else {
255            None
256        }
257    }
258
259    /// Finish the reduction with an upward-rounded `gamma_n` error bound.
260    ///
261    /// Returns `None` when an earlier proof step failed, the term count cannot
262    /// support the relative-error model, or the final bound/endpoints cannot
263    /// remain finite. Otherwise the result satisfies every public
264    /// [`ScalarWithErrorBound`] invariant.
265    #[expect(
266        clippy::cast_precision_loss,
267        reason = "a usable gamma requires a term count below 2^53, where the cast is exact"
268    )]
269    const fn finish(self, term_count: Option<usize>) -> Option<ScalarWithErrorBound> {
270        if !self.proof_available {
271            return None;
272        }
273        if self.magnitude_upper == 0.0 {
274            return ScalarWithErrorBound::try_new(self.estimate, 0.0);
275        }
276
277        let Some(term_count) = term_count else {
278            return None;
279        };
280        let scaled_roundoff = (term_count as f64) * (f64::EPSILON / 2.0);
281        if !scaled_roundoff.is_finite() || scaled_roundoff >= 1.0 {
282            return None;
283        }
284
285        // The count conversion, multiplication by 2^-53, and subtraction from
286        // one are exact throughout the usable range. Round the division and
287        // final multiplication upward to retain a certified upper bound.
288        let gamma = scaled_roundoff / (1.0 - scaled_roundoff);
289        let gamma_upper = gamma.next_up();
290        if !gamma_upper.is_finite() {
291            return None;
292        }
293        let rounded_bound = gamma_upper * self.magnitude_upper;
294        if !rounded_bound.is_finite() {
295            return None;
296        }
297        let absolute_error_bound = if rounded_bound == 0.0 {
298            0.0
299        } else {
300            rounded_bound.next_up()
301        };
302        ScalarWithErrorBound::try_new(self.estimate, absolute_error_bound)
303    }
304}
305
306/// Finite fixed-size vector of length `D`, stored inline.
307///
308/// Public construction rejects NaN and infinity through [`try_new`](Self::try_new),
309/// and the storage field is private, so a `Vector` value carries the invariant
310/// that every stored entry is finite. Algorithms therefore do not re-scan stored
311/// entries at every use; user-visible non-finite errors come from construction
312/// boundaries or from values computed during arithmetic, such as overflowed
313/// accumulators.
314///
315/// Direct field construction is intentionally unavailable to downstream callers:
316///
317/// ```compile_fail
318/// use la_stack::Vector;
319///
320/// let _ = Vector::<2> {
321///     data: [1.0, f64::NAN],
322/// };
323/// ```
324#[must_use]
325#[derive(Clone, Copy, Debug, PartialEq)]
326pub struct Vector<const D: usize> {
327    data: [f64; D],
328}
329
330impl<const D: usize> Vector<D> {
331    /// Test-only infallible constructor for finite literal fixtures.
332    #[cfg(test)]
333    #[inline]
334    pub(crate) const fn new(data: [f64; D]) -> Self {
335        match Self::try_new(data) {
336            Ok(vector) => vector,
337            Err(_) => panic!("Vector::new requires finite entries"),
338        }
339    }
340
341    /// Try to create a finite vector from a backing array.
342    ///
343    /// This is the public raw-storage boundary for vectors. Successful
344    /// construction makes the returned [`Vector`] a finite-storage proof.
345    ///
346    /// # Examples
347    /// ```
348    /// use la_stack::prelude::*;
349    ///
350    /// # fn main() -> Result<(), LaError> {
351    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
352    /// assert_eq!(v.into_array(), [1.0, 2.0, 3.0]);
353    /// # Ok(())
354    /// # }
355    /// ```
356    ///
357    /// # Errors
358    /// Returns [`LaError::NonFinite`] with the first offending entry index when
359    /// `data` contains NaN or infinity.
360    #[inline]
361    pub const fn try_new(data: [f64; D]) -> Result<Self, LaError> {
362        if let Some(index) = Self::first_non_finite_entry(&data) {
363            Err(LaError::non_finite_input_vector(index))
364        } else {
365            Ok(Self { data })
366        }
367    }
368
369    /// Finalize vector storage produced by an arithmetic operation.
370    ///
371    /// Keeping this validation in the type that owns the finite-storage
372    /// invariant prevents a new computation path from accidentally turning raw
373    /// non-finite storage into a [`Vector`].
374    #[inline]
375    pub(crate) const fn from_computation(
376        data: [f64; D],
377        operation: ArithmeticOperation,
378    ) -> Result<Self, LaError> {
379        if let Some(index) = Self::first_non_finite_entry(&data) {
380            Err(LaError::non_finite_computation_step(operation, index))
381        } else {
382            Ok(Self { data })
383        }
384    }
385
386    /// Return the first non-finite stored entry in index order.
387    ///
388    /// Used by the public raw-storage boundary to report the first offending
389    /// index with [`LaError::NonFinite`].
390    const fn first_non_finite_entry(data: &[f64; D]) -> Option<usize> {
391        let mut i = 0;
392        while i < D {
393            if !data[i].is_finite() {
394                return Some(i);
395            }
396            i += 1;
397        }
398        None
399    }
400
401    /// All-zeros finite vector.
402    ///
403    /// # Examples
404    /// ```
405    /// use la_stack::prelude::*;
406    ///
407    /// let z = Vector::<2>::zero();
408    /// assert_eq!(z.into_array(), [0.0, 0.0]);
409    /// ```
410    #[inline]
411    pub const fn zero() -> Self {
412        Self { data: [0.0; D] }
413    }
414
415    /// Borrow the finite backing array.
416    ///
417    /// # Examples
418    /// ```
419    /// use la_stack::prelude::*;
420    ///
421    /// # fn main() -> Result<(), LaError> {
422    /// let v = Vector::<2>::try_new([1.0, -2.0])?;
423    /// assert_eq!(v.as_array(), &[1.0, -2.0]);
424    /// # Ok(())
425    /// # }
426    /// ```
427    #[inline]
428    #[must_use]
429    pub const fn as_array(&self) -> &[f64; D] {
430        &self.data
431    }
432
433    /// Consume and return the finite backing array.
434    ///
435    /// # Examples
436    /// ```
437    /// use la_stack::prelude::*;
438    ///
439    /// # fn main() -> Result<(), LaError> {
440    /// let v = Vector::<2>::try_new([1.0, 2.0])?;
441    /// let a = v.into_array();
442    /// assert_eq!(a, [1.0, 2.0]);
443    /// # Ok(())
444    /// # }
445    /// ```
446    #[inline]
447    #[must_use]
448    pub const fn into_array(self) -> [f64; D] {
449        self.data
450    }
451
452    /// Dot product.
453    ///
454    /// Terms are accumulated in `f64` using [`f64::mul_add`] at each index.
455    /// Intermediate rounding occurs, and this method does not provide a
456    /// certified absolute rounding bound for the returned dot product. Raw
457    /// `Vector` values are finite by construction, so this method only checks
458    /// whether the accumulation overflows to NaN or infinity.
459    ///
460    /// # Examples
461    /// ```
462    /// use la_stack::prelude::*;
463    ///
464    /// # fn main() -> Result<(), LaError> {
465    /// let a = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
466    /// let b = Vector::<3>::try_new([-2.0, 0.5, 4.0])?;
467    /// assert!((a.dot(&b)? - 11.0).abs() <= 1e-12);
468    /// # Ok(())
469    /// # }
470    /// ```
471    ///
472    /// # Errors
473    /// Returns [`LaError::NonFinite`] when the accumulated dot product overflows
474    /// to NaN or infinity.
475    #[inline]
476    pub const fn dot(&self, other: &Self) -> Result<f64, LaError> {
477        self.dot_with_operation(other, ArithmeticOperation::VectorDotProduct)
478    }
479
480    /// Dot product with a certified absolute roundoff bound.
481    ///
482    /// The estimate uses the deterministic left-to-right recurrence
483    /// `s[0] = 0` and `s[i + 1] = self[i].mul_add(other[i], s[i])`. When the
484    /// relative-error model is valid, the returned certificate bounds the
485    /// difference between `s[D]` and the exact-real expression
486    /// `Σᵢ self[i] × other[i]` over the stored binary64 inputs.
487    ///
488    /// The bound is `gamma_D × Σᵢ |self[i] × other[i]|`, where
489    /// `gamma_D = D u / (1 - D u)` and `u = 2^-53`. The magnitude sum and the
490    /// published bound are rounded upward. See `REFERENCES.md` \[9-11\].
491    ///
492    /// `Ok(None)` means no certificate is available because a nonzero product
493    /// or FMA result entered the subnormal range, the reduction dimension made
494    /// `gamma_D` invalid, or a proof-only magnitude/bound calculation exhausted
495    /// the finite binary64 range. It does not mean the exact dot product is
496    /// zero. Unlike a user-selected tolerance, a returned error bound describes
497    /// rounding in this specific arithmetic tree.
498    ///
499    /// # Examples
500    /// ```
501    /// use la_stack::prelude::*;
502    ///
503    /// # fn main() -> Result<(), LaError> {
504    /// let left = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
505    /// let right = Vector::<3>::try_new([4.0, 5.0, 6.0])?;
506    /// let positive = left.dot_with_errbound(&right)?.and_then(|bounded| {
507    ///     if bounded.lower_bound() > 0.0 {
508    ///         Some(true)
509    ///     } else if bounded.upper_bound() <= 0.0 {
510    ///         Some(false)
511    ///     } else {
512    ///         None // The enclosure cannot establish whether the result is positive.
513    ///     }
514    /// });
515    /// assert_eq!(positive, Some(true));
516    ///
517    /// // Finite inputs can also lack a certificate; use an exact fallback
518    /// // before deciding the sign in this case.
519    /// let tiny = Vector::<2>::try_new([1e-200, 1e-200])?;
520    /// assert_eq!(tiny.dot_with_errbound(&tiny)?, None);
521    /// # Ok(())
522    /// # }
523    /// ```
524    ///
525    /// # Errors
526    /// Returns [`LaError::NonFinite`] with the first failing reduction index and
527    /// [`ArithmeticOperation::VectorDotProduct`] when an FMA estimate becomes
528    /// non-finite.
529    #[inline]
530    pub const fn dot_with_errbound(
531        &self,
532        other: &Self,
533    ) -> Result<Option<ScalarWithErrorBound>, LaError> {
534        let left = self.as_array();
535        let right = other.as_array();
536        let mut reduction = CertifiedReduction::ZERO;
537        let mut i = 0;
538        while i < D {
539            reduction = match reduction.add_product(
540                left[i],
541                right[i],
542                ArithmeticOperation::VectorDotProduct,
543                i,
544            ) {
545                Ok(reduction) => reduction,
546                Err(error) => return Err(error),
547            };
548            i += 1;
549        }
550        Ok(reduction.finish(Some(D)))
551    }
552
553    /// Certified dot product with an unrounded vector difference.
554    ///
555    /// This evaluates the exact-real expression
556    /// `Σᵢ self[i] × (left[i] - right[i])` without first rounding
557    /// `left - right` into a [`Vector`]. Its deterministic arithmetic tree is
558    ///
559    /// ```text
560    /// s[0]       = 0
561    /// s[2i + 1]  = self[i].mul_add(left[i], s[2i])
562    /// s[2i + 2]  = (-self[i]).mul_add(right[i], s[2i + 1]).
563    /// ```
564    ///
565    /// A returned certificate therefore includes all `2D` FMA rounding events
566    /// in that tree and bounds the intended expression over the original
567    /// binary64 coordinates. When available, its absolute bound is
568    /// `gamma_2D × Σᵢ (|self[i] × left[i]| + |self[i] × right[i]|)`, where
569    /// `gamma_2D = 2D u / (1 - 2D u)` and `u = 2^-53`; every magnitude and the
570    /// final bound are rounded upward. Its
571    /// [`lower_bound`](ScalarWithErrorBound::lower_bound) and
572    /// [`upper_bound`](ScalarWithErrorBound::upper_bound) can certify a sign or
573    /// separation from a caller's threshold. An overlapping endpoint range
574    /// remains inconclusive and should trigger the caller's exact fallback.
575    ///
576    /// `Ok(None)` has the same proof-unavailable meaning as in
577    /// [`dot_with_errbound`](Self::dot_with_errbound), including gradual
578    /// underflow and proof-only range exhaustion.
579    ///
580    /// # Examples
581    /// ```
582    /// use la_stack::prelude::*;
583    ///
584    /// # fn main() -> Result<(), LaError> {
585    /// let axis = Vector::<2>::try_new([2.0, -1.0])?;
586    /// let left = Vector::<2>::try_new([4.0, 1.0])?;
587    /// let right = Vector::<2>::try_new([1.0, 3.0])?;
588    /// let separated = axis.dot_difference_with_errbound(&left, &right)?.and_then(|bounded| {
589    ///     if bounded.lower_bound() > 1.0 {
590    ///         Some(true)
591    ///     } else if bounded.upper_bound() <= 1.0 {
592    ///         Some(false)
593    ///     } else {
594    ///         None // An exact fallback is needed to decide this threshold test.
595    ///     }
596    /// });
597    /// // An unavailable certificate also remains None through and_then.
598    /// assert_eq!(separated, Some(true));
599    /// # Ok(())
600    /// # }
601    /// ```
602    ///
603    /// # Errors
604    /// Returns [`LaError::NonFinite`] with the first failing coordinate index
605    /// and [`ArithmeticOperation::VectorDotDifference`] when either FMA for
606    /// that coordinate produces a non-finite estimate.
607    #[inline]
608    pub const fn dot_difference_with_errbound(
609        &self,
610        left: &Self,
611        right: &Self,
612    ) -> Result<Option<ScalarWithErrorBound>, LaError> {
613        let axis = self.as_array();
614        let left = left.as_array();
615        let right = right.as_array();
616        let mut reduction = CertifiedReduction::ZERO;
617        let mut i = 0;
618        while i < D {
619            reduction = match reduction.add_product(
620                axis[i],
621                left[i],
622                ArithmeticOperation::VectorDotDifference,
623                i,
624            ) {
625                Ok(reduction) => reduction,
626                Err(error) => return Err(error),
627            };
628            reduction = match reduction.add_product(
629                -axis[i],
630                right[i],
631                ArithmeticOperation::VectorDotDifference,
632                i,
633            ) {
634                Ok(reduction) => reduction,
635                Err(error) => return Err(error),
636            };
637            i += 1;
638        }
639        Ok(reduction.finish(D.checked_mul(2)))
640    }
641
642    /// Accumulate a dot product while retaining the public operation that owns it.
643    const fn dot_with_operation(
644        &self,
645        other: &Self,
646        operation: ArithmeticOperation,
647    ) -> Result<f64, LaError> {
648        let lhs = self.as_array();
649        let rhs = other.as_array();
650        let mut acc = 0.0;
651        let mut i = 0;
652        while i < D {
653            acc = lhs[i].mul_add(rhs[i], acc);
654            i += 1;
655        }
656        if acc.is_finite() {
657            Ok(acc)
658        } else {
659            cold_path();
660            Err(Self::dot_non_finite_error(lhs, rhs, operation))
661        }
662    }
663
664    /// Replay a non-finite dot product to locate the first failing step.
665    ///
666    /// This runs only after the success-path traversal has produced a non-finite
667    /// final accumulator. Stored entries are finite, so once a fused multiply-add
668    /// produces a non-finite accumulator, later steps cannot make it finite again.
669    /// Replaying the same left-to-right operations must therefore find the first
670    /// failing index.
671    #[cold]
672    const fn dot_non_finite_error(
673        lhs: &[f64; D],
674        rhs: &[f64; D],
675        operation: ArithmeticOperation,
676    ) -> LaError {
677        let mut acc = 0.0;
678        let mut i = 0;
679        let last = D.saturating_sub(1);
680        while i < last {
681            acc = lhs[i].mul_add(rhs[i], acc);
682            if !acc.is_finite() {
683                return LaError::non_finite_computation_step(operation, i);
684            }
685            i += 1;
686        }
687
688        LaError::non_finite_computation_step(operation, last)
689    }
690
691    /// Squared Euclidean norm.
692    ///
693    /// This is computed as `dot(self, self)`, so `norm_squared` has the same
694    /// `f64` [`mul_add`](f64::mul_add) accumulation behavior as [`dot`](Self::dot).
695    /// Intermediate rounding occurs, and this method does not provide a
696    /// certified absolute rounding bound for the returned squared norm.
697    /// `Vector` values are finite by construction, so this method only checks
698    /// whether the accumulation overflows to NaN or infinity.
699    ///
700    /// # Examples
701    /// ```
702    /// use la_stack::prelude::*;
703    ///
704    /// # fn main() -> Result<(), LaError> {
705    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
706    /// assert!((v.norm_squared()? - 14.0).abs() <= 1e-12);
707    /// # Ok(())
708    /// # }
709    /// ```
710    ///
711    /// # Errors
712    /// Returns [`LaError::NonFinite`] when the accumulated norm overflows to NaN
713    /// or infinity.
714    #[inline]
715    pub const fn norm_squared(&self) -> Result<f64, LaError> {
716        self.dot_with_operation(self, ArithmeticOperation::VectorSquaredNorm)
717    }
718
719    /// Overflow- and underflow-safe Euclidean norm.
720    ///
721    /// This computes `sqrt(Σᵢ self[i]²)` with a deterministic left-to-right
722    /// scaled sum-of-squares recurrence. Each non-zero magnitude is divided by
723    /// the largest magnitude seen so far before it is squared, so intermediate
724    /// squares cannot overflow and an all-subnormal vector is scaled into the
725    /// normal range. See `REFERENCES.md` \[15\].
726    ///
727    /// The divisions, fused multiply-adds, square root, and final rescaling are
728    /// rounded in binary64. This method does not claim correct rounding or
729    /// provide a certified absolute error bound. Because [`Vector`] entries are
730    /// finite by construction, it returns a finite non-negative result unless
731    /// the exact Euclidean norm rounds outside the finite binary64 range.
732    /// Near that boundary, a fixed-size stack accumulator sums the coordinate
733    /// squares exactly and compares squared rounding midpoints. This fallback
734    /// prevents accumulated roundoff from causing or hiding overflow and does
735    /// not require the `exact` feature.
736    ///
737    /// Unlike [`norm_squared`](Self::norm_squared), this method does not require the
738    /// squared norm to be representable. For example, the norm of
739    /// `[1.0e200, 1.0e200]` is finite even though its squared norm is not.
740    ///
741    /// # Examples
742    /// ```
743    /// use la_stack::prelude::*;
744    ///
745    /// # fn main() -> Result<(), LaError> {
746    /// let ordinary = Vector::<2>::try_new([3.0, 4.0])?;
747    /// assert_eq!(ordinary.norm()?, 5.0);
748    /// assert_eq!(ordinary.norm_squared()?, 25.0);
749    ///
750    /// let large = Vector::<2>::try_new([1.0e200, 1.0e200])?;
751    /// assert!(large.norm()?.is_finite());
752    /// assert_eq!(
753    ///     large.norm_squared(),
754    ///     Err(LaError::non_finite_computation_step(ArithmeticOperation::VectorSquaredNorm, 0)),
755    /// );
756    /// # Ok(())
757    /// # }
758    /// ```
759    ///
760    /// # Errors
761    /// Returns [`LaError::NonFinite`] with
762    /// [`ArithmeticOperation::VectorNorm`] when the exact Euclidean norm rounds
763    /// to infinity under round-to-nearest, ties-to-even.
764    #[inline]
765    pub fn norm(&self) -> Result<f64, LaError> {
766        let mut entries = self.as_array().iter();
767        // The first coordinate establishes the scale without a division or FMA.
768        // A zero (or absent) first coordinate preserves the empty-prefix state.
769        let mut scale = entries.next().copied().unwrap_or(0.0).abs();
770        let mut scaled_sum = 1.0;
771
772        for &entry in entries {
773            let magnitude = entry.abs();
774            if magnitude == 0.0 {
775                continue;
776            }
777
778            if scale < magnitude {
779                let ratio = scale / magnitude;
780                scaled_sum = (scaled_sum * ratio).mul_add(ratio, 1.0);
781                scale = magnitude;
782            } else {
783                let ratio = magnitude / scale;
784                scaled_sum = ratio.mul_add(ratio, scaled_sum);
785            }
786        }
787
788        // With b = bit_length(D), D < 2^b. If scale <= 2^(1023-b),
789        // even the L1 upper bound D*scale is below 2^1023. Both the exact
790        // norm and the rounded recurrence therefore have ample range margin.
791        // Checking scale, rather than only the computed norm, also catches
792        // true overflow that rounding in the recurrence could hide.
793        let dimension_bits = usize::BITS - D.leading_zeros();
794        let safe_scale = f64::from_bits(u64::from(2046 - dimension_bits) << 52);
795        if scale > safe_scale {
796            return norm_near_overflow(self.as_array(), scale);
797        }
798        Ok(scale * scaled_sum.sqrt())
799    }
800}
801
802impl<const D: usize> Default for Vector<D> {
803    #[inline]
804    fn default() -> Self {
805        Self::zero()
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use core::hint::black_box;
812
813    use approx::assert_abs_diff_eq;
814    use pastey::paste;
815
816    use super::*;
817
818    fn assert_certified_proof_loss_survives_normal_terms<const D: usize>() {
819        let mut left_data = [0.0; D];
820        left_data[0] = f64::MIN_POSITIVE;
821        left_data[D - 1] = 1.0;
822        let mut right_data = [1.0; D];
823        right_data[0] = 0.5;
824        let left = Vector::new(left_data);
825        let right = Vector::new(right_data);
826
827        // The first product is subnormal; the last restores a normal estimate,
828        // but cannot restore the lost certificate for the complete reduction.
829        assert_abs_diff_eq!(left.dot(&right).unwrap(), 1.0, epsilon = 0.0);
830        assert_eq!(left.dot_with_errbound(&right), Ok(None));
831        assert_eq!(
832            left.dot_difference_with_errbound(&right, &Vector::zero()),
833            Ok(None)
834        );
835    }
836
837    fn assert_certified_proof_loss_preserves_later_overflow<const D: usize>() {
838        let mut axis_data = [0.0; D];
839        axis_data[0] = f64::MIN_POSITIVE;
840        axis_data[D - 1] = f64::MAX;
841        let axis = Vector::new(axis_data);
842        let mut left_data = [0.0; D];
843        left_data[0] = 0.5;
844        left_data[D - 1] = 2.0;
845
846        assert_eq!(
847            axis.dot_with_errbound(&Vector::new(left_data)),
848            Err(LaError::non_finite_computation_step(
849                ArithmeticOperation::VectorDotProduct,
850                D - 1,
851            ))
852        );
853        let difference_error = Err(LaError::non_finite_computation_step(
854            ArithmeticOperation::VectorDotDifference,
855            D - 1,
856        ));
857        assert_eq!(
858            axis.dot_difference_with_errbound(&Vector::new(left_data), &Vector::zero()),
859            difference_error
860        );
861
862        // Overflow in the second FMA must also remain observable after the
863        // earlier coordinate has already made certification unavailable.
864        left_data[D - 1] = 0.0;
865        let mut right_data = [0.0; D];
866        right_data[D - 1] = -2.0;
867        assert_eq!(
868            axis.dot_difference_with_errbound(&Vector::new(left_data), &Vector::new(right_data)),
869            difference_error
870        );
871    }
872
873    macro_rules! gen_certified_reduction_sequence_tests {
874        ($d:literal) => {
875            paste! {
876                #[test]
877                fn [<certified_proof_loss_survives_normal_terms_ $d d>]() {
878                    assert_certified_proof_loss_survives_normal_terms::<$d>();
879                }
880
881                #[test]
882                fn [<certified_proof_loss_preserves_later_overflow_ $d d>]() {
883                    assert_certified_proof_loss_preserves_later_overflow::<$d>();
884                }
885            }
886        };
887    }
888
889    gen_certified_reduction_sequence_tests!(2);
890    gen_certified_reduction_sequence_tests!(3);
891    gen_certified_reduction_sequence_tests!(4);
892    gen_certified_reduction_sequence_tests!(5);
893
894    macro_rules! gen_vector_tests {
895        ($d:literal) => {
896            paste! {
897                #[test]
898                fn [<vector_new_as_array_into_array_ $d d>]() {
899                    let arr = {
900                        let mut arr = [0.0f64; $d];
901                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
902                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
903                            *dst = *src;
904                        }
905                        arr
906                    };
907
908                    let v = Vector::<$d>::new(arr);
909
910                    for i in 0..$d {
911                        assert_abs_diff_eq!(v.as_array()[i], arr[i], epsilon = 0.0);
912                    }
913
914                    let out = v.into_array();
915                    for i in 0..$d {
916                        assert_abs_diff_eq!(out[i], arr[i], epsilon = 0.0);
917                    }
918                }
919
920                #[test]
921                fn [<vector_zero_as_array_into_array_default_ $d d>]() {
922                    let z = Vector::<$d>::zero();
923                    for &x in z.as_array() {
924                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
925                    }
926                    for x in z.into_array() {
927                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
928                    }
929
930                    let d = Vector::<$d>::default();
931                    for x in d.into_array() {
932                        assert_abs_diff_eq!(x, 0.0, epsilon = 0.0);
933                    }
934                }
935
936                #[test]
937                fn [<vector_dot_and_norm_squared_ $d d>]() {
938                    // Use black_box to avoid constant-folding/inlining eliminating the actual dot loop,
939                    // which can make coverage tools report the mul_add line as uncovered.
940
941                    let a_arr = {
942                        let mut arr = [0.0f64; $d];
943                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
944                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
945                            *dst = black_box(*src);
946                        }
947                        arr
948                    };
949                    let b_arr = {
950                        let mut arr = [0.0f64; $d];
951                        let values = [-2.0f64, 0.5, 4.0, -1.0, 2.0];
952                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
953                            *dst = black_box(*src);
954                        }
955                        arr
956                    };
957
958                    let expected_dot = {
959                        let mut acc = 0.0;
960                        let mut i = 0;
961                        while i < $d {
962                            acc = a_arr[i].mul_add(b_arr[i], acc);
963                            i += 1;
964                        }
965                        acc
966                    };
967                    let expected_norm_squared = {
968                        let mut acc = 0.0;
969                        let mut i = 0;
970                        while i < $d {
971                            acc = a_arr[i].mul_add(a_arr[i], acc);
972                            i += 1;
973                        }
974                        acc
975                    };
976
977                    let a = Vector::<$d>::new(black_box(a_arr));
978                    let b = Vector::<$d>::new(black_box(b_arr));
979
980                    // Call via (black_boxed) fn pointers to discourage inlining, improving line-level coverage
981                    // attribution for the loop body.
982                    let dot_fn: fn(&Vector<$d>, &Vector<$d>) -> Result<f64, LaError> =
983                        black_box(Vector::<$d>::dot);
984                    let norm_squared_fn: fn(&Vector<$d>) -> Result<f64, LaError> =
985                        black_box(Vector::<$d>::norm_squared);
986
987                    assert_abs_diff_eq!(
988                        dot_fn(black_box(&a), black_box(&b)).unwrap(),
989                        expected_dot,
990                        epsilon = 1e-14
991                    );
992                    assert_abs_diff_eq!(
993                        norm_squared_fn(black_box(&a)).unwrap(),
994                        expected_norm_squared,
995                        epsilon = 1e-14
996                    );
997                }
998
999                #[test]
1000                fn [<vector_certified_dot_and_difference_ $d d>]() {
1001                    let mut left_data = [0.0; $d];
1002                    let mut right_data = [0.0; $d];
1003                    let left_values = [1.0, 2.0, 3.0, 4.0, 5.0];
1004                    let right_values = [2.0, 3.0, 4.0, 5.0, 6.0];
1005                    for (destination, source) in left_data.iter_mut().zip(left_values) {
1006                        *destination = source;
1007                    }
1008                    for (destination, source) in right_data.iter_mut().zip(right_values) {
1009                        *destination = source;
1010                    }
1011                    let left = Vector::<$d>::new(left_data);
1012                    let right = Vector::<$d>::new(right_data);
1013                    let zero = Vector::<$d>::zero();
1014
1015                    let dot = left.dot(&right).unwrap();
1016                    let dot_bound = left.dot_with_errbound(&right).unwrap().unwrap();
1017                    assert_abs_diff_eq!(dot_bound.estimate(), dot, epsilon = 0.0);
1018                    assert!(dot_bound.absolute_error_bound() >= 0.0);
1019                    assert!(dot_bound.lower_bound() <= dot);
1020                    assert!(dot <= dot_bound.upper_bound());
1021
1022                    let difference_bound = left
1023                        .dot_difference_with_errbound(&right, &zero)
1024                        .unwrap()
1025                        .unwrap();
1026                    assert_abs_diff_eq!(difference_bound.estimate(), dot, epsilon = 0.0);
1027                    assert!(difference_bound.lower_bound() <= dot);
1028                    assert!(dot <= difference_bound.upper_bound());
1029                }
1030
1031                #[test]
1032                fn [<vector_try_new_rejects_non_finite_ $d d>]() {
1033                    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1034                        let mut data = [1.0f64; $d];
1035                        data[$d - 1] = value;
1036                        assert_eq!(
1037                            Vector::<$d>::try_new(data),
1038                            Err(LaError::non_finite_input_vector($d - 1))
1039                        );
1040                    }
1041
1042                    let mut data = [1.0f64; $d];
1043                    data[0] = f64::INFINITY;
1044                    data[$d - 1] = f64::NAN;
1045                    assert_eq!(
1046                        Vector::<$d>::try_new(data),
1047                        Err(LaError::non_finite_input_vector(0))
1048                    );
1049                }
1050
1051                #[test]
1052                fn [<vector_from_computation_preserves_failure_provenance_ $d d>]() {
1053                    let mut data = [1.0f64; $d];
1054                    data[$d - 1] = f64::INFINITY;
1055
1056                    assert_eq!(
1057                        Vector::<$d>::from_computation(
1058                            data,
1059                            ArithmeticOperation::LuSolve,
1060                        ),
1061                        Err(LaError::non_finite_computation_step(
1062                            ArithmeticOperation::LuSolve,
1063                            $d - 1,
1064                        ))
1065                    );
1066                }
1067
1068                #[test]
1069                fn [<vector_dot_and_norm_squared_reject_overflow_ $d d>]() {
1070                    let mut a_arr = [1.0f64; $d];
1071                    a_arr[0] = f64::MAX;
1072                    let a = Vector::<$d>::new(a_arr);
1073
1074                    let mut b_arr = [1.0f64; $d];
1075                    b_arr[0] = 2.0;
1076                    let b = Vector::<$d>::new(b_arr);
1077
1078                    assert_eq!(
1079                        a.dot(&b),
1080                        Err(LaError::non_finite_computation_step(
1081                            ArithmeticOperation::VectorDotProduct,
1082                            0,
1083                        ))
1084                    );
1085                    assert_eq!(
1086                        a.dot_with_errbound(&b),
1087                        Err(LaError::non_finite_computation_step(
1088                            ArithmeticOperation::VectorDotProduct,
1089                            0,
1090                        ))
1091                    );
1092                    assert_eq!(
1093                        a.dot_difference_with_errbound(&b, &Vector::zero()),
1094                        Err(LaError::non_finite_computation_step(
1095                            ArithmeticOperation::VectorDotDifference,
1096                            0,
1097                        ))
1098                    );
1099                    assert_eq!(
1100                        a.norm_squared(),
1101                        Err(LaError::non_finite_computation_step(
1102                            ArithmeticOperation::VectorSquaredNorm,
1103                            0,
1104                        ))
1105                    );
1106                }
1107
1108            }
1109        };
1110    }
1111
1112    // Mirror delaunay-style multi-dimension tests.
1113    gen_vector_tests!(1);
1114    gen_vector_tests!(2);
1115    gen_vector_tests!(3);
1116    gen_vector_tests!(4);
1117    gen_vector_tests!(5);
1118    gen_vector_tests!(6);
1119    gen_vector_tests!(7);
1120    gen_vector_tests!(8);
1121
1122    fn known_norm_input<const D: usize>() -> ([f64; D], f64) {
1123        let mut data = [0.0; D];
1124        if D == 1 {
1125            data[0] = -5.0;
1126        } else if D >= 2 {
1127            data[0] = -3.0;
1128            data[1] = 4.0;
1129        }
1130        (data, if D == 0 { 0.0 } else { 5.0 })
1131    }
1132
1133    macro_rules! gen_vector_norm_known_answer_tests {
1134        ($d:literal) => {
1135            paste! {
1136                #[test]
1137                fn [<vector_norm_known_answer_ $d d>]() {
1138                    let (data, expected) = known_norm_input::<$d>();
1139                    let vector = Vector::<$d>::new(data);
1140
1141                    assert_eq!(vector.norm(), Ok(expected));
1142                }
1143            }
1144        };
1145    }
1146
1147    gen_vector_norm_known_answer_tests!(0);
1148    gen_vector_norm_known_answer_tests!(1);
1149    gen_vector_norm_known_answer_tests!(2);
1150    gen_vector_norm_known_answer_tests!(3);
1151    gen_vector_norm_known_answer_tests!(4);
1152    gen_vector_norm_known_answer_tests!(5);
1153    gen_vector_norm_known_answer_tests!(6);
1154    gen_vector_norm_known_answer_tests!(7);
1155    gen_vector_norm_known_answer_tests!(8);
1156
1157    macro_rules! gen_vector_replay_tests {
1158        ($d:literal) => {
1159            paste! {
1160                #[test]
1161                fn [<vector_dot_and_norm_squared_report_last_overflowing_step_ $d d>]() {
1162                    let mut dot_lhs = [1.0f64; $d];
1163                    dot_lhs[$d - 1] = f64::MAX;
1164                    let mut dot_rhs = [1.0f64; $d];
1165                    dot_rhs[$d - 1] = 2.0;
1166                    let dot_lhs = Vector::<$d>::new(dot_lhs);
1167                    let dot_rhs = Vector::<$d>::new(dot_rhs);
1168
1169                    assert_eq!(
1170                        dot_lhs.dot(&dot_rhs),
1171                        Err(LaError::non_finite_computation_step(
1172                            ArithmeticOperation::VectorDotProduct,
1173                            $d - 1,
1174                        ))
1175                    );
1176
1177                    let mut norm_data = [1.0f64; $d];
1178                    norm_data[$d - 1] = f64::MAX;
1179                    let vector = Vector::<$d>::new(norm_data);
1180
1181                    assert_eq!(
1182                        vector.norm_squared(),
1183                        Err(LaError::non_finite_computation_step(
1184                            ArithmeticOperation::VectorSquaredNorm,
1185                            $d - 1,
1186                        ))
1187                    );
1188                }
1189            }
1190        };
1191    }
1192
1193    gen_vector_replay_tests!(2);
1194    gen_vector_replay_tests!(3);
1195    gen_vector_replay_tests!(4);
1196    gen_vector_replay_tests!(5);
1197
1198    macro_rules! gen_vector_const_eval_tests {
1199        ($d:literal, $dot:literal, $norm_squared:literal) => {
1200            paste! {
1201                #[test]
1202                fn [<vector_dot_and_norm_squared_const_eval_ $d d>]() {
1203                    const DOT: Result<f64, LaError> = Vector::<$d>::new([1.0; $d])
1204                        .dot(&Vector::<$d>::new([2.0; $d]));
1205                    const NORM_SQUARED: Result<f64, LaError> =
1206                        Vector::<$d>::new([1.0; $d]).norm_squared();
1207
1208                    assert_eq!(DOT, Ok($dot));
1209                    assert_eq!(NORM_SQUARED, Ok($norm_squared));
1210                }
1211            }
1212        };
1213    }
1214
1215    gen_vector_const_eval_tests!(2, 4.0, 2.0);
1216    gen_vector_const_eval_tests!(3, 6.0, 3.0);
1217    gen_vector_const_eval_tests!(4, 8.0, 4.0);
1218    gen_vector_const_eval_tests!(5, 10.0, 5.0);
1219
1220    #[test]
1221    fn vector_dot_and_norm_squared_overflow_const_eval() {
1222        const DOT: Result<f64, LaError> =
1223            Vector::<2>::new([f64::MAX; 2]).dot(&Vector::<2>::new([1.0; 2]));
1224        const NORM_SQUARED: Result<f64, LaError> = Vector::<2>::new([f64::MAX; 2]).norm_squared();
1225
1226        assert_eq!(
1227            DOT,
1228            Err(LaError::non_finite_computation_step(
1229                ArithmeticOperation::VectorDotProduct,
1230                1,
1231            ))
1232        );
1233        assert_eq!(
1234            NORM_SQUARED,
1235            Err(LaError::non_finite_computation_step(
1236                ArithmeticOperation::VectorSquaredNorm,
1237                0,
1238            ))
1239        );
1240    }
1241
1242    #[test]
1243    fn vector_dot_and_norm_squared_preserve_fma_and_left_to_right_order() {
1244        let dot_large = 9_007_199_254_740_992.0;
1245        let dot_lhs = Vector::<4>::new([dot_large, 1.0, 1.0, 1.0]);
1246        let dot_rhs = Vector::<4>::new([1.0; 4]);
1247        assert_eq!(dot_lhs.dot(&dot_rhs), Ok(dot_large));
1248
1249        let fused_lhs = Vector::<2>::new([f64::MAX, f64::MAX]);
1250        let fused_rhs = Vector::<2>::new([-1.0, 2.0]);
1251        assert_eq!(fused_lhs.dot(&fused_rhs), Ok(f64::MAX));
1252
1253        let norm_large = 134_217_728.0;
1254        let vector = Vector::<4>::new([norm_large, 1.0, 1.0, 1.0]);
1255        assert_eq!(vector.norm_squared(), Ok(norm_large * norm_large));
1256    }
1257
1258    #[test]
1259    fn vector_norm_preserves_zero_sign_and_subnormal_magnitudes() {
1260        let signed_zero = Vector::<4>::new([-0.0, 0.0, -0.0, 0.0]);
1261        assert_eq!(signed_zero.norm().unwrap().to_bits(), 0.0f64.to_bits());
1262
1263        let least_subnormal = f64::from_bits(1);
1264        let subnormal = Vector::<2>::new([3.0 * least_subnormal, -4.0 * least_subnormal]);
1265        assert_eq!(
1266            subnormal.norm().unwrap().to_bits(),
1267            (5.0 * least_subnormal).to_bits()
1268        );
1269    }
1270
1271    #[test]
1272    fn vector_norm_handles_mixed_and_overflowing_magnitudes() {
1273        let large = Vector::<2>::new([1.0e200, -1.0e200]);
1274        let expected = 2.0f64.sqrt() * 1.0e200;
1275        assert_abs_diff_eq!(large.norm().unwrap(), expected, epsilon = 2.0e184);
1276        assert_eq!(
1277            large.norm_squared(),
1278            Err(LaError::non_finite_computation_step(
1279                ArithmeticOperation::VectorSquaredNorm,
1280                0,
1281            )),
1282        );
1283
1284        let mixed = Vector::<4>::new([1.0e200, 1.0e-200, -f64::from_bits(1), 0.0]);
1285        assert_eq!(mixed.norm(), Ok(1.0e200));
1286
1287        let unrepresentable = Vector::<2>::new([f64::MAX, f64::MAX]);
1288        assert_eq!(
1289            unrepresentable.norm(),
1290            Err(LaError::non_finite_computation_scalar(
1291                ArithmeticOperation::VectorNorm,
1292            ))
1293        );
1294    }
1295
1296    #[test]
1297    fn vector_norm_accepts_largest_finite_norm() {
1298        let maximum = Vector::<2>::new([f64::MAX, 0.0]);
1299
1300        assert_eq!(maximum.norm(), Ok(f64::MAX));
1301    }
1302
1303    #[test]
1304    fn certified_dot_preserves_fma_estimate_and_withholds_range_exhausted_bound() {
1305        let left = Vector::<2>::new([f64::MAX, f64::MAX]);
1306        let right = Vector::<2>::new([-1.0, 2.0]);
1307
1308        assert_eq!(left.dot(&right), Ok(f64::MAX));
1309        assert_eq!(left.dot_with_errbound(&right), Ok(None));
1310    }
1311
1312    #[test]
1313    fn certified_dot_withholds_bound_when_finite_endpoints_cannot_be_published() {
1314        let maximum = Vector::<1>::new([f64::MAX]);
1315        let one = Vector::<1>::new([1.0]);
1316
1317        assert_eq!(maximum.dot(&one), Ok(f64::MAX));
1318        assert_eq!(maximum.dot_with_errbound(&one), Ok(None));
1319    }
1320
1321    #[test]
1322    fn certified_dot_withholds_bound_when_magnitude_sum_exhausts_range() {
1323        let maximum = Vector::<2>::new([f64::MAX, f64::MAX]);
1324
1325        for factor in [0.5, 0.75] {
1326            let cancelling = Vector::<2>::new([factor, -factor]);
1327            let estimate = maximum
1328                .dot(&cancelling)
1329                .expect("the cancelling FMA estimate must remain finite");
1330            assert!(estimate.is_finite());
1331            assert_eq!(maximum.dot_with_errbound(&cancelling), Ok(None));
1332        }
1333    }
1334
1335    #[test]
1336    fn certified_bounds_distinguish_conclusive_and_inconclusive_results() {
1337        let conclusive = Vector::<2>::new([1.0, 2.0])
1338            .dot_with_errbound(&Vector::new([3.0, 4.0]))
1339            .unwrap()
1340            .unwrap();
1341        assert_abs_diff_eq!(conclusive.estimate(), 11.0, epsilon = 0.0);
1342        assert!(conclusive.lower_bound() > 1.0);
1343
1344        let inconclusive = Vector::<2>::new([1.0, 1.0])
1345            .dot_with_errbound(&Vector::new([1.0, -1.0]))
1346            .unwrap()
1347            .unwrap();
1348        assert_abs_diff_eq!(inconclusive.estimate(), 0.0, epsilon = 0.0);
1349        assert!(inconclusive.absolute_error_bound() > 0.0);
1350        assert!(inconclusive.lower_bound() < 0.0);
1351        assert!(inconclusive.upper_bound() > 0.0);
1352    }
1353
1354    #[test]
1355    fn certified_zero_and_signed_zero_have_an_exact_zero_bound() {
1356        let left = Vector::<3>::new([-0.0, 0.0, -0.0]);
1357        let right = Vector::<3>::new([f64::MAX, -1.0, f64::MIN_POSITIVE]);
1358        let bounded = left.dot_with_errbound(&right).unwrap().unwrap();
1359
1360        assert_abs_diff_eq!(bounded.estimate(), 0.0, epsilon = 0.0);
1361        assert_abs_diff_eq!(bounded.absolute_error_bound(), 0.0, epsilon = 0.0);
1362        assert_abs_diff_eq!(bounded.lower_bound(), 0.0, epsilon = 0.0);
1363        assert_abs_diff_eq!(bounded.upper_bound(), 0.0, epsilon = 0.0);
1364    }
1365
1366    #[test]
1367    fn certified_reductions_withhold_bounds_for_subnormal_products() {
1368        let tiny = Vector::<1>::new([f64::MIN_POSITIVE]);
1369        let half = Vector::<1>::new([0.5]);
1370        assert_eq!(tiny.dot_with_errbound(&half), Ok(None));
1371
1372        let min_subnormal = Vector::<1>::new([f64::from_bits(1)]);
1373        assert_eq!(
1374            min_subnormal.dot_with_errbound(&Vector::new([1.0])),
1375            Ok(None)
1376        );
1377        assert_eq!(
1378            min_subnormal.dot_with_errbound(&Vector::new([0.5])),
1379            Ok(None)
1380        );
1381        assert_eq!(
1382            tiny.dot_difference_with_errbound(&half, &Vector::zero()),
1383            Ok(None)
1384        );
1385    }
1386
1387    #[test]
1388    fn certified_reduction_detects_fma_cancellation_below_subnormal_range() {
1389        let near_sqrt_min = f64::from_bits((512_u64 << 52) | 1);
1390        let rounded_product = near_sqrt_min * near_sqrt_min;
1391        assert!(rounded_product.is_normal());
1392        assert_abs_diff_eq!(
1393            near_sqrt_min.mul_add(near_sqrt_min, -rounded_product),
1394            0.0,
1395            epsilon = 0.0
1396        );
1397
1398        let left = Vector::<2>::new([-rounded_product, near_sqrt_min]);
1399        let right = Vector::<2>::new([1.0, near_sqrt_min]);
1400        assert_eq!(left.dot(&right), Ok(0.0));
1401        assert_eq!(left.dot_with_errbound(&right), Ok(None));
1402    }
1403
1404    #[test]
1405    fn certified_dot_handles_mixed_normal_magnitudes() {
1406        let left = Vector::<2>::new([1.0e100, 1.0e-100]);
1407        let right = Vector::<2>::new([1.0e-100, 1.0e100]);
1408        let bounded = left.dot_with_errbound(&right).unwrap().unwrap();
1409
1410        assert_abs_diff_eq!(bounded.estimate(), 2.0, epsilon = 0.0);
1411        assert!(bounded.lower_bound() <= 2.0);
1412        assert!(bounded.upper_bound() >= 2.0);
1413    }
1414
1415    #[test]
1416    fn certified_dot_difference_does_not_round_coordinates_first() {
1417        let scale = 18_014_398_509_481_984.0;
1418        let axis = Vector::<1>::new([scale]);
1419        let left = Vector::<1>::new([1.0]);
1420        let right = Vector::<1>::new([1.0 / scale]);
1421        assert_abs_diff_eq!(left.as_array()[0] - right.as_array()[0], 1.0, epsilon = 0.0);
1422
1423        let bounded = axis
1424            .dot_difference_with_errbound(&left, &right)
1425            .unwrap()
1426            .unwrap();
1427        assert_abs_diff_eq!(bounded.estimate(), scale, epsilon = 0.0);
1428        assert!(bounded.lower_bound() < scale);
1429        assert!(bounded.upper_bound() >= scale);
1430    }
1431
1432    #[test]
1433    fn certified_reductions_are_const_evaluable() {
1434        const DOT: Result<Option<ScalarWithErrorBound>, LaError> =
1435            Vector::<2>::new([1.0, 2.0]).dot_with_errbound(&Vector::<2>::new([3.0, 4.0]));
1436        const DIFFERENCE: Result<Option<ScalarWithErrorBound>, LaError> =
1437            Vector::<2>::new([2.0, -1.0]).dot_difference_with_errbound(
1438                &Vector::<2>::new([4.0, 1.0]),
1439                &Vector::<2>::new([1.0, 3.0]),
1440            );
1441
1442        assert_abs_diff_eq!(DOT.unwrap().unwrap().estimate(), 11.0, epsilon = 0.0);
1443        assert_abs_diff_eq!(DIFFERENCE.unwrap().unwrap().estimate(), 8.0, epsilon = 0.0);
1444    }
1445
1446    #[test]
1447    fn certified_dot_difference_reports_second_fma_overflow() {
1448        let axis = Vector::<2>::new([1.0, f64::MAX]);
1449        let left = Vector::<2>::new([0.0, 1.0]);
1450        let right = Vector::<2>::new([0.0, -1.0]);
1451
1452        assert_eq!(
1453            axis.dot_difference_with_errbound(&left, &right),
1454            Err(LaError::non_finite_computation_step(
1455                ArithmeticOperation::VectorDotDifference,
1456                1,
1457            ))
1458        );
1459    }
1460
1461    #[test]
1462    fn vector_dot_and_norm_squared_report_first_middle_overflowing_step() {
1463        let dot_lhs = Vector::<3>::new([f64::MAX, f64::MAX, 1.0]);
1464        let dot_rhs = Vector::<3>::new([1.0; 3]);
1465        assert_eq!(
1466            dot_lhs.dot(&dot_rhs),
1467            Err(LaError::non_finite_computation_step(
1468                ArithmeticOperation::VectorDotProduct,
1469                1,
1470            ))
1471        );
1472
1473        let norm_large = 1.0e154;
1474        let vector = Vector::<3>::new([norm_large, norm_large, 1.0]);
1475        assert_eq!(
1476            vector.norm_squared(),
1477            Err(LaError::non_finite_computation_step(
1478                ArithmeticOperation::VectorSquaredNorm,
1479                1,
1480            ))
1481        );
1482    }
1483
1484    #[test]
1485    fn zero_dimension_vector_has_zero_dot_and_norm() {
1486        let vector = Vector::<0>::try_new([]).unwrap();
1487
1488        assert!(vector.as_array().is_empty());
1489        assert!(vector.into_array().is_empty());
1490        assert_eq!(vector.dot(&Vector::zero()), Ok(0.0));
1491        let dot_bound = vector.dot_with_errbound(&Vector::zero()).unwrap().unwrap();
1492        assert_abs_diff_eq!(dot_bound.absolute_error_bound(), 0.0, epsilon = 0.0);
1493        let difference_bound = vector
1494            .dot_difference_with_errbound(&Vector::zero(), &Vector::zero())
1495            .unwrap()
1496            .unwrap();
1497        assert_abs_diff_eq!(difference_bound.absolute_error_bound(), 0.0, epsilon = 0.0);
1498        assert_eq!(vector.norm_squared(), Ok(0.0));
1499    }
1500
1501    #[test]
1502    fn certified_dot_withholds_bound_when_exact_product_exceeds_finite_range() {
1503        let left_value = f64::from_bits(0x7fe3_0319_b612_3729);
1504        let right_value = f64::from_bits(0x3ffa_ee21_bf46_bc00);
1505        let left = Vector::<1>::new([left_value]);
1506        let right = Vector::<1>::new([right_value]);
1507
1508        assert!(left_value.mul_add(right_value, -f64::MAX) > 0.0);
1509        assert_eq!(left.dot(&right), Ok(f64::MAX));
1510        assert_eq!(left.dot_with_errbound(&right), Ok(None));
1511    }
1512}