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}
52
53impl ScalarWithErrorBound {
54    /// Return the rounded scalar estimate.
55    #[inline]
56    #[must_use]
57    pub const fn estimate(self) -> f64 {
58        self.estimate
59    }
60
61    /// Return the certified absolute error bound.
62    #[inline]
63    #[must_use]
64    pub const fn absolute_error_bound(self) -> f64 {
65        self.absolute_error_bound
66    }
67
68    /// Return a finite outward-rounded lower bound on the exact-real value.
69    #[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    /// Return a finite outward-rounded upper bound on the exact-real value.
88    #[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    /// Construct a validated public result with finite outward endpoints.
107    ///
108    /// The exact `|estimate| + bound` dominates both endpoint magnitudes.
109    /// A rounded sum below `MAX` has a finite successor; at `MAX` its residual
110    /// must be nonpositive. This proves both outward endpoints finite without
111    /// computing an endpoint the caller may never request.
112    #[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/// State for one certified left-to-right FMA reduction.
143///
144/// The rounded estimate continues accumulating after `proof_available` becomes
145/// false. This lets the public methods distinguish a non-finite estimate
146/// (`Err`) from a finite estimate whose proof arithmetic is unavailable
147/// (`Ok(None)`). `magnitude_upper` encloses the sum of exact product
148/// magnitudes while that proof remains available.
149/// While proof is available, `estimate` is normal or positive zero.
150/// `single_exact_product` records one nonzero product with a unit multiplier;
151/// adding zero products preserves that fact and the exact estimate.
152#[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    /// Add one exact-real product through one rounded FMA.
169    ///
170    /// A non-finite estimate becomes a typed public error. Underflow or range
171    /// loss confined to proof construction instead clears `proof_available`,
172    /// preserving the finite estimate for the eventual `Ok(None)` result.
173    // Keep the reduction state in caller registers instead of returning a
174    // Result-sized state aggregate across a call for every coordinate.
175    #[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            // While proof is available, the estimate is normal or +0: the
189            // initial estimate is +0 and exact cancellation rounds to +0.
190            // Adding either signed zero therefore leaves every bit unchanged.
191            // After proof loss an underflow may have produced -0, so retain
192            // the FMA there to preserve the specified signed-zero result.
193            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    /// Return whether `left × right + addend` is exactly zero.
224    ///
225    /// Nonzero operands are guaranteed by `add_product`. This distinguishes
226    /// exact cancellation from a nonzero value rounded to
227    /// zero. Only exact zero is admissible under the relative-error model used
228    /// by the public certified reductions.
229    #[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    /// Add an upward-rounded bound on `|left × right|` to the magnitude sum.
242    ///
243    /// The caller supplies nonzero finite operands.
244    /// The returned flag identifies a first unit product; later products clear it.
245    ///
246    /// `None` means a nonzero product was not normal or the product/sum could
247    /// not be enclosed by a finite binary64 value, so the public result must be
248    /// proof-unavailable.
249    #[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            // Multiplication by ±1 is exact. Otherwise an unconditional upward
266            // step bounds the product without computing its rounding residual.
267            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        // One upward-widened FMA encloses magnitude_upper + |left * right|.
279        // It is no wider than first rounding the product upward, adding, and
280        // widening that sum: round-to-nearest is monotone. Keep the separate
281        // product-normality check above for the relative-error proof contract.
282        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    /// Finish the reduction with an upward-rounded `gamma_n` error bound.
295    ///
296    /// Returns `None` when an earlier proof step failed, a needed nonzero bound
297    /// has a term count outside the relative-error model, or bound/endpoints cannot
298    /// remain finite. Otherwise the result satisfies every public
299    /// [`ScalarWithErrorBound`] invariant.
300    #[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        // The count conversion, multiplication by 2^-53, and subtraction from
322        // one are exact throughout the usable range. Round the division and
323        // final multiplication upward to retain a certified upper bound.
324        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/// Finite fixed-size vector of length `D`, stored inline.
343///
344/// Public construction rejects NaN and infinity through [`try_new`](Self::try_new),
345/// and the storage field is private, so a `Vector` value carries the invariant
346/// that every stored entry is finite. Algorithms therefore do not re-scan stored
347/// entries at every use; user-visible non-finite errors come from construction
348/// boundaries or from values computed during arithmetic, such as overflowed
349/// accumulators.
350///
351/// Direct field construction is intentionally unavailable to downstream callers:
352///
353/// ```compile_fail
354/// use la_stack::Vector;
355///
356/// let _ = Vector::<2> {
357///     data: [1.0, f64::NAN],
358/// };
359/// ```
360#[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    /// Test-only infallible constructor for finite literal fixtures.
368    #[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    /// Try to create a finite vector from a backing array.
378    ///
379    /// This is the public raw-storage boundary for vectors. Successful
380    /// construction makes the returned [`Vector`] a finite-storage proof.
381    ///
382    /// # Examples
383    /// ```
384    /// use la_stack::prelude::*;
385    ///
386    /// # fn main() -> Result<(), LaError> {
387    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
388    /// assert_eq!(v.into_array(), [1.0, 2.0, 3.0]);
389    /// # Ok(())
390    /// # }
391    /// ```
392    ///
393    /// # Errors
394    /// Returns [`LaError::NonFinite`] with the first offending entry index when
395    /// `data` contains NaN or infinity.
396    #[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    /// Finalize vector storage produced by an arithmetic operation.
406    ///
407    /// Keeping this validation in the type that owns the finite-storage
408    /// invariant prevents a new computation path from accidentally turning raw
409    /// non-finite storage into a [`Vector`].
410    #[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    /// Return the first non-finite stored entry in index order.
423    ///
424    /// Used by the public raw-storage boundary to report the first offending
425    /// index with [`LaError::NonFinite`].
426    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    /// All-zeros finite vector.
438    ///
439    /// # Examples
440    /// ```
441    /// use la_stack::prelude::*;
442    ///
443    /// let z = Vector::<2>::zero();
444    /// assert_eq!(z.into_array(), [0.0, 0.0]);
445    /// ```
446    #[inline]
447    pub const fn zero() -> Self {
448        Self { data: [0.0; D] }
449    }
450
451    /// Borrow the finite backing array.
452    ///
453    /// # Examples
454    /// ```
455    /// use la_stack::prelude::*;
456    ///
457    /// # fn main() -> Result<(), LaError> {
458    /// let v = Vector::<2>::try_new([1.0, -2.0])?;
459    /// assert_eq!(v.as_array(), &[1.0, -2.0]);
460    /// # Ok(())
461    /// # }
462    /// ```
463    #[inline]
464    #[must_use]
465    pub const fn as_array(&self) -> &[f64; D] {
466        &self.data
467    }
468
469    /// Consume and return the finite backing array.
470    ///
471    /// # Examples
472    /// ```
473    /// use la_stack::prelude::*;
474    ///
475    /// # fn main() -> Result<(), LaError> {
476    /// let v = Vector::<2>::try_new([1.0, 2.0])?;
477    /// let a = v.into_array();
478    /// assert_eq!(a, [1.0, 2.0]);
479    /// # Ok(())
480    /// # }
481    /// ```
482    #[inline]
483    #[must_use]
484    pub const fn into_array(self) -> [f64; D] {
485        self.data
486    }
487
488    /// Unsigned angle to another nonzero vector, in radians in `[0, π]`.
489    ///
490    /// Shares the allocation-free numerical implementation and rounded semantics
491    /// of [`crate::VectorAngle::angle`], using this type's finite-storage invariant.
492    /// Arbitrary finite magnitudes are supported even when a norm would overflow.
493    /// No certified absolute error bound or exact parallelism test is provided.
494    ///
495    /// # Errors
496    /// Returns [`LaError::EmptyVector`] for `D = 0`, [`LaError::ZeroVector`] for
497    /// an all-zero operand (`self` first), or [`LaError::NonFinite`] for a
498    /// non-finite computed angle, with vector-angle computation provenance.
499    ///
500    /// # Examples
501    /// ```
502    /// use la_stack::prelude::*;
503    ///
504    /// # fn main() -> Result<(), LaError> {
505    /// let x = Vector::try_new([1.0, 0.0, 0.0])?;
506    /// let y = Vector::try_new([1.0, 2.0_f64.powi(-30), 0.0])?;
507    /// assert!((x.angle(&y)? - 2.0_f64.powi(-30)).abs() < 1e-24);
508    /// # Ok(())
509    /// # }
510    /// ```
511    #[inline]
512    pub fn angle(&self, other: &Self) -> Result<f64, LaError> {
513        crate::angle::angle_finite(self, other)
514    }
515
516    /// Dot product.
517    ///
518    /// Terms are accumulated in `f64` using [`f64::mul_add`] at each index.
519    /// Intermediate rounding occurs, and this method does not provide a
520    /// certified absolute rounding bound for the returned dot product. Raw
521    /// `Vector` values are finite by construction, so this method only checks
522    /// whether the accumulation overflows to NaN or infinity.
523    ///
524    /// # Examples
525    /// ```
526    /// use la_stack::prelude::*;
527    ///
528    /// # fn main() -> Result<(), LaError> {
529    /// let a = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
530    /// let b = Vector::<3>::try_new([-2.0, 0.5, 4.0])?;
531    /// assert!((a.dot(&b)? - 11.0).abs() <= 1e-12);
532    /// # Ok(())
533    /// # }
534    /// ```
535    ///
536    /// # Errors
537    /// Returns [`LaError::NonFinite`] when the accumulated dot product overflows
538    /// to NaN or infinity.
539    #[inline]
540    pub const fn dot(&self, other: &Self) -> Result<f64, LaError> {
541        self.dot_with_operation(other, ArithmeticOperation::VectorDotProduct)
542    }
543
544    /// Dot product with a certified absolute roundoff bound.
545    ///
546    /// The estimate uses the deterministic left-to-right recurrence
547    /// `s[0] = 0` and `s[i + 1] = self[i].mul_add(other[i], s[i])`. When the
548    /// relative-error model is valid, the returned certificate bounds the
549    /// difference between `s[D]` and the exact-real expression
550    /// `Σᵢ self[i] × other[i]` over the stored binary64 inputs.
551    ///
552    /// The general bound is `gamma_D × Σᵢ |self[i] × other[i]|`, where
553    /// `gamma_D = D u / (1 - D u)` and `u = 2^-53`. The magnitude sum and the
554    /// published bound are rounded upward. See `REFERENCES.md` \[9-11\].
555    /// A single nonzero normal product with a unit (±1) multiplier receives a zero
556    /// bound: every other FMA adds exact zero and preserves that product.
557    ///
558    /// `Ok(None)` means no certificate is available because a nonzero product
559    /// or FMA result entered the subnormal range, the reduction dimension made
560    /// `gamma_D` invalid, or a proof-only magnitude/bound calculation exhausted
561    /// the finite binary64 range. It does not mean the exact dot product is
562    /// zero. Unlike a user-selected tolerance, a returned error bound describes
563    /// rounding in this specific arithmetic tree.
564    ///
565    /// # Examples
566    /// ```
567    /// use la_stack::prelude::*;
568    ///
569    /// # fn main() -> Result<(), LaError> {
570    /// let left = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
571    /// let right = Vector::<3>::try_new([4.0, 5.0, 6.0])?;
572    /// let positive = left.dot_with_errbound(&right)?.and_then(|bounded| {
573    ///     if bounded.lower_bound() > 0.0 {
574    ///         Some(true)
575    ///     } else if bounded.upper_bound() <= 0.0 {
576    ///         Some(false)
577    ///     } else {
578    ///         None // The enclosure cannot establish whether the result is positive.
579    ///     }
580    /// });
581    /// assert_eq!(positive, Some(true));
582    ///
583    /// // Finite inputs can also lack a certificate; use an exact fallback
584    /// // before deciding the sign in this case.
585    /// let tiny = Vector::<2>::try_new([1e-200, 1e-200])?;
586    /// assert_eq!(tiny.dot_with_errbound(&tiny)?, None);
587    /// # Ok(())
588    /// # }
589    /// ```
590    ///
591    /// # Errors
592    /// Returns [`LaError::NonFinite`] with the first failing reduction index and
593    /// [`ArithmeticOperation::VectorDotProduct`] when an FMA estimate becomes
594    /// non-finite.
595    #[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    /// Certified dot product with an unrounded vector difference.
624    ///
625    /// This evaluates the exact-real expression
626    /// `Σᵢ self[i] × (left[i] - right[i])` without first rounding
627    /// `left - right` into a [`Vector`]. Its deterministic arithmetic tree is
628    ///
629    /// ```text
630    /// s[0]       = 0
631    /// s[2i + 1]  = self[i].mul_add(left[i], s[2i])
632    /// s[2i + 2]  = (-self[i]).mul_add(right[i], s[2i + 1]).
633    /// ```
634    ///
635    /// A returned certificate therefore includes all `2D` FMA rounding events
636    /// in that tree and bounds the intended expression over the original
637    /// binary64 coordinates. The general absolute bound is
638    /// `gamma_2D × Σᵢ (|self[i] × left[i]| + |self[i] × right[i]|)`, where
639    /// `gamma_2D = 2D u / (1 - 2D u)` and `u = 2^-53`; every magnitude and the
640    /// final bound are rounded upward. Its
641    /// [`lower_bound`](ScalarWithErrorBound::lower_bound) and
642    /// [`upper_bound`](ScalarWithErrorBound::upper_bound) can certify a sign or
643    /// separation from a caller's threshold. An overlapping endpoint range
644    /// remains inconclusive and should trigger the caller's exact fallback.
645    /// As with [`dot_with_errbound`](Self::dot_with_errbound), a single nonzero
646    /// normal product with a unit (±1) multiplier receives a zero bound.
647    ///
648    /// `Ok(None)` has the same proof-unavailable meaning as in
649    /// [`dot_with_errbound`](Self::dot_with_errbound), including gradual
650    /// underflow and proof-only range exhaustion.
651    ///
652    /// # Examples
653    /// ```
654    /// use la_stack::prelude::*;
655    ///
656    /// # fn main() -> Result<(), LaError> {
657    /// let axis = Vector::<2>::try_new([2.0, -1.0])?;
658    /// let left = Vector::<2>::try_new([4.0, 1.0])?;
659    /// let right = Vector::<2>::try_new([1.0, 3.0])?;
660    /// let separated = axis.dot_difference_with_errbound(&left, &right)?.and_then(|bounded| {
661    ///     if bounded.lower_bound() > 1.0 {
662    ///         Some(true)
663    ///     } else if bounded.upper_bound() <= 1.0 {
664    ///         Some(false)
665    ///     } else {
666    ///         None // An exact fallback is needed to decide this threshold test.
667    ///     }
668    /// });
669    /// // An unavailable certificate also remains None through and_then.
670    /// assert_eq!(separated, Some(true));
671    /// # Ok(())
672    /// # }
673    /// ```
674    ///
675    /// # Errors
676    /// Returns [`LaError::NonFinite`] with the first failing coordinate index
677    /// and [`ArithmeticOperation::VectorDotDifference`] when either FMA for
678    /// that coordinate produces a non-finite estimate.
679    #[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    /// Accumulate a dot product while retaining the public operation that owns it.
715    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    /// Replay a non-finite dot product to locate the first failing step.
737    ///
738    /// This runs only after the success-path traversal has produced a non-finite
739    /// final accumulator. Stored entries are finite, so once a fused multiply-add
740    /// produces a non-finite accumulator, later steps cannot make it finite again.
741    /// Replaying the same left-to-right operations must therefore find the first
742    /// failing index.
743    #[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    /// Squared Euclidean norm.
764    ///
765    /// This is computed as `dot(self, self)`, so `norm_squared` has the same
766    /// `f64` [`mul_add`](f64::mul_add) accumulation behavior as [`dot`](Self::dot).
767    /// Intermediate rounding occurs, and this method does not provide a
768    /// certified absolute rounding bound for the returned squared norm.
769    /// `Vector` values are finite by construction, so this method only checks
770    /// whether the accumulation overflows to NaN or infinity.
771    ///
772    /// # Examples
773    /// ```
774    /// use la_stack::prelude::*;
775    ///
776    /// # fn main() -> Result<(), LaError> {
777    /// let v = Vector::<3>::try_new([1.0, 2.0, 3.0])?;
778    /// assert!((v.norm_squared()? - 14.0).abs() <= 1e-12);
779    /// # Ok(())
780    /// # }
781    /// ```
782    ///
783    /// # Errors
784    /// Returns [`LaError::NonFinite`] when the accumulated norm overflows to NaN
785    /// or infinity.
786    #[inline]
787    pub const fn norm_squared(&self) -> Result<f64, LaError> {
788        self.dot_with_operation(self, ArithmeticOperation::VectorSquaredNorm)
789    }
790
791    /// Overflow- and underflow-safe Euclidean norm.
792    ///
793    /// This computes `sqrt(Σᵢ self[i]²)` with a deterministic left-to-right
794    /// scaled sum-of-squares recurrence. Each non-zero magnitude is divided by
795    /// the largest magnitude seen so far before it is squared, so intermediate
796    /// squares cannot overflow and an all-subnormal vector is scaled into the
797    /// normal range. See `REFERENCES.md` \[15\].
798    ///
799    /// The divisions, fused multiply-adds, square root, and final rescaling are
800    /// rounded in binary64. This method does not claim correct rounding or
801    /// provide a certified absolute error bound. Because [`Vector`] entries are
802    /// finite by construction, it returns a finite non-negative result unless
803    /// the exact Euclidean norm rounds outside the finite binary64 range.
804    /// Near that boundary, a fixed-size stack accumulator sums the coordinate
805    /// squares exactly and compares squared rounding midpoints. This fallback
806    /// prevents accumulated roundoff from causing or hiding overflow and does
807    /// not require the `exact` feature.
808    ///
809    /// Unlike [`norm_squared`](Self::norm_squared), this method does not require the
810    /// squared norm to be representable. For example, the norm of
811    /// `[1.0e200, 1.0e200]` is finite even though its squared norm is not.
812    ///
813    /// # Examples
814    /// ```
815    /// use la_stack::prelude::*;
816    ///
817    /// # fn main() -> Result<(), LaError> {
818    /// let ordinary = Vector::<2>::try_new([3.0, 4.0])?;
819    /// assert_eq!(ordinary.norm()?, 5.0);
820    /// assert_eq!(ordinary.norm_squared()?, 25.0);
821    ///
822    /// let large = Vector::<2>::try_new([1.0e200, 1.0e200])?;
823    /// assert!(large.norm()?.is_finite());
824    /// assert_eq!(
825    ///     large.norm_squared(),
826    ///     Err(LaError::non_finite_computation_step(ArithmeticOperation::VectorSquaredNorm, 0)),
827    /// );
828    /// # Ok(())
829    /// # }
830    /// ```
831    ///
832    /// # Errors
833    /// Returns [`LaError::NonFinite`] with
834    /// [`ArithmeticOperation::VectorNorm`] when the exact Euclidean norm rounds
835    /// to infinity under round-to-nearest, ties-to-even.
836    #[inline]
837    pub fn norm(&self) -> Result<f64, LaError> {
838        let mut entries = self.as_array().iter();
839        // The first coordinate establishes the scale without a division or FMA.
840        // A zero (or absent) first coordinate preserves the empty-prefix state.
841        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        // With b = bit_length(D), D < 2^b. If scale <= 2^(1023-b),
861        // even the L1 upper bound D*scale is below 2^1023. Both the exact
862        // norm and the rounded recurrence therefore have ample range margin.
863        // Checking scale, rather than only the computed norm, also catches
864        // true overflow that rounding in the recurrence could hide.
865        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        // The first product is subnormal; the last restores a normal estimate,
900        // but cannot restore the lost certificate for the complete reduction.
901        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        // Overflow in the second FMA must also remain observable after the
935        // earlier coordinate has already made certification unavailable.
936        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                    // Use black_box to avoid constant-folding/inlining eliminating the actual dot loop,
1011                    // which can make coverage tools report the mul_add line as uncovered.
1012
1013                    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                    // Call via (black_boxed) fn pointers to discourage inlining, improving line-level coverage
1053                    // attribution for the loop body.
1054                    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    // Mirror delaunay-style multi-dimension tests.
1185    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        // The magnitude bound reaches MAX but stays finite, so rejection must
1387        // come from publishing the estimate plus its positive error bound.
1388        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        // The magnitude FMA reaches finite MAX; its upward successor is
1525        // infinite. Exact estimate cancellation cannot bypass proof loss.
1526        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        // Both tiny products round to zero. The final term produces a normal
1537        // estimate, but cannot restore a proof lost before that term.
1538        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}