Skip to main content

la_stack/
interval.rs

1#![forbid(unsafe_code)]
2
3//! Outward-rounded intervals, fixed-size determinants, and solution enclosures.
4//!
5//! See `REFERENCES.md` \[17\] for `FastTwoSum`, \[9-11\] for the binary64 arithmetic
6//! model, and \[12\] for the Leibniz determinant identity. The column-subset
7//! evaluation is specialized to this crate's small dimensions. Reference
8//! \[14\] describes the broader interval standard; this module does not claim
9//! IEEE 1788 conformance. The
10//! [interval construction](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#outward-rounded-interval-expressions)
11//! explains outward endpoints and determinant enclosures.
12
13use crate::rounding::{compare_product_with_rounded, compare_quotient_with_rounded, two_sum_error};
14use crate::{ArithmeticOperation, IntervalBound, IntervalOperand, LaError, Matrix};
15
16/// Largest dimension supported by [`IntervalMatrix::det`] and
17/// [`IntervalMatrix::det_sign`].
18///
19/// A subset-DP determinant needs `2^D` partial intervals. The implementation
20/// reserves up to 128 entries inline, covering the geometry-oriented D ≤ 7 scope
21/// without heap allocation.
22pub const MAX_INTERVAL_MATRIX_DIM: usize = 7;
23
24/// A closed finite binary64 interval `[lower, upper]`.
25///
26/// Construction keeps both endpoints finite and ordered. Arithmetic rounds
27/// outward, so every successful result contains the exact-real result of the
28/// corresponding operation on all represented inputs. Both IEEE-754 signed
29/// zeros are accepted and canonicalized to `+0.0`; subnormal bounds are
30/// retained.
31///
32/// This is a deliberately small proof-bearing surface, not a general-purpose
33/// interval arithmetic package. Division requires a zero-free divisor.
34///
35/// # Examples
36/// ```
37/// use la_stack::prelude::*;
38///
39/// # fn main() -> Result<(), LaError> {
40/// let difference = Interval::try_from_subtraction(1.0, 0.1)?;
41/// let square = difference.try_square()?;
42/// assert!(difference.lower() < difference.upper());
43/// assert!(square.contains((1.0_f64 - 0.1).powi(2)));
44/// # Ok(())
45/// # }
46/// ```
47#[must_use]
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct Interval {
50    lower: f64,
51    upper: f64,
52}
53
54/// Sign evidence from an outward-rounded interval determinant.
55///
56/// `Positive`, `Negative`, and `Zero` are proofs about every determinant
57/// represented by the interval matrix. `Inconclusive` means the computed
58/// enclosure overlaps zero and must not be interpreted as exact singularity.
59#[must_use]
60#[non_exhaustive]
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum IntervalDeterminantSign {
63    /// The determinant interval is strictly greater than zero.
64    Positive,
65    /// The determinant interval is strictly less than zero.
66    Negative,
67    /// The determinant interval is exactly `[0, 0]`.
68    Zero,
69    /// The determinant interval contains zero and at least one nonzero value.
70    Inconclusive,
71}
72
73/// Fixed-size square matrix of outward-rounded [`Interval`] entries.
74///
75/// Storage is the inline array `[[Interval; D]; D]`. Determinants use a
76/// division-free Leibniz subset DP through D=7, so zero-containing pivot
77/// intervals never require a special case and no heap allocation occurs.
78/// [`solve`](Self::solve) uses interval Gaussian elimination with row pivoting
79/// and explicitly reports inconclusive pivots. It has no determinant dimension
80/// limit, uses O(D²) stack storage, and is intended for small systems.
81///
82/// # Examples
83/// ```
84/// use la_stack::prelude::*;
85///
86/// # fn main() -> Result<(), LaError> {
87/// let matrix = IntervalMatrix::<3>::try_from_point_rows([
88///     [0.0, 1.0, 0.0],
89///     [1.0, 0.0, 0.0],
90///     [0.0, 0.0, 1.0],
91/// ])?;
92/// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Negative);
93/// # Ok(())
94/// # }
95/// ```
96#[must_use]
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct IntervalMatrix<const D: usize> {
99    rows: [[Interval; D]; D],
100}
101
102/// Canonicalize either signed representation of real zero to `+0.0`.
103#[inline]
104const fn canonical_zero(value: f64) -> f64 {
105    if value == 0.0 { 0.0 } else { value }
106}
107
108/// Turn a finite rounded sum into the tight adjacent-float enclosure implied by
109/// its exact `FastTwoSum` residual.
110#[inline]
111const fn rounded_add_bounds(
112    left: f64,
113    right: f64,
114    operation: ArithmeticOperation,
115) -> Result<(f64, f64), LaError> {
116    let rounded = left + right;
117    if !rounded.is_finite() {
118        return Err(LaError::interval_range_exhausted(operation));
119    }
120
121    let error = two_sum_error(left, right, rounded);
122    // The ordered FastTwoSum residual is finite for finite operands and sum.
123    let (lower, upper) = if error < 0.0 {
124        (rounded.next_down(), rounded)
125    } else if error > 0.0 {
126        (rounded, rounded.next_up())
127    } else {
128        (rounded, rounded)
129    };
130    if !lower.is_finite() || !upper.is_finite() {
131        return Err(LaError::interval_range_exhausted(operation));
132    }
133
134    Ok((canonical_zero(lower), canonical_zero(upper)))
135}
136
137/// Round only the endpoint used by a non-singleton interval sum.
138#[inline]
139const fn rounded_add_endpoint<const UPPER: bool>(
140    left: f64,
141    right: f64,
142    operation: ArithmeticOperation,
143) -> Result<f64, LaError> {
144    let rounded = left + right;
145    if !rounded.is_finite() {
146        return Err(LaError::interval_range_exhausted(operation));
147    }
148    let error = two_sum_error(left, right, rounded);
149    let endpoint = if UPPER && error > 0.0 {
150        rounded.next_up()
151    } else if !UPPER && error < 0.0 {
152        rounded.next_down()
153    } else {
154        rounded
155    };
156    if !endpoint.is_finite() {
157        return Err(LaError::interval_range_exhausted(operation));
158    }
159    Ok(canonical_zero(endpoint))
160}
161
162/// Turn a finite rounded product into the tight adjacent-float enclosure of the
163/// exact binary64-input product.
164// This kernel is called for each selected endpoint inside the determinant DP.
165#[expect(
166    clippy::inline_always,
167    reason = "measured endpoint-call overhead in the interval determinant kernel"
168)]
169#[inline(always)]
170const fn rounded_product_bounds(
171    left: f64,
172    right: f64,
173    operation: ArithmeticOperation,
174) -> Result<(f64, f64), LaError> {
175    if left == 0.0 || right == 0.0 {
176        return Ok((0.0, 0.0));
177    }
178
179    let rounded = left * right;
180    if !rounded.is_finite() {
181        return Err(LaError::interval_range_exhausted(operation));
182    }
183
184    let relation = compare_product_with_rounded(left, right, rounded);
185    let (lower, upper) = if relation < 0 {
186        (rounded.next_down(), rounded)
187    } else if relation > 0 {
188        (rounded, rounded.next_up())
189    } else {
190        (rounded, rounded)
191    };
192    if !lower.is_finite() || !upper.is_finite() {
193        return Err(LaError::interval_range_exhausted(operation));
194    }
195
196    Ok((canonical_zero(lower), canonical_zero(upper)))
197}
198
199/// Enclose a quotient directly, including subnormal results and results rounded
200/// to zero. Callers supply a nonzero denominator. An exact integer comparison
201/// determines which adjacent float is needed, even when an FMA residual would
202/// underflow or a reciprocal intermediate would overflow.
203#[inline]
204const fn rounded_quotient_bounds(
205    numerator: f64,
206    denominator: f64,
207    operation: ArithmeticOperation,
208) -> Result<(f64, f64), LaError> {
209    let rounded = numerator / denominator;
210    if !rounded.is_finite() {
211        return Err(LaError::interval_range_exhausted(operation));
212    }
213    let relation = compare_quotient_with_rounded(numerator, denominator, rounded);
214    let (lower, upper) = if relation < 0 {
215        (rounded.next_down(), rounded)
216    } else if relation > 0 {
217        (rounded, rounded.next_up())
218    } else {
219        (rounded, rounded)
220    };
221    if !lower.is_finite() || !upper.is_finite() {
222        return Err(LaError::interval_range_exhausted(operation));
223    }
224    Ok((canonical_zero(lower), canonical_zero(upper)))
225}
226
227impl Interval {
228    /// Exact real zero.
229    pub const ZERO: Self = Self {
230        lower: 0.0,
231        upper: 0.0,
232    };
233
234    /// Exact real one.
235    pub const ONE: Self = Self {
236        lower: 1.0,
237        upper: 1.0,
238    };
239
240    /// Construct a closed interval from finite ordered bounds.
241    ///
242    /// Signed zero endpoints are canonicalized to `+0.0`.
243    ///
244    /// # Examples
245    /// ```
246    /// use core::assert_matches;
247    /// use la_stack::prelude::*;
248    ///
249    /// # fn main() -> Result<(), LaError> {
250    /// let range = Interval::try_new(-2.0, 3.0)?;
251    /// assert!(range.contains(1.0));
252    /// assert!(!range.contains(4.0));
253    /// assert_matches!(
254    ///     Interval::try_new(3.0, -2.0),
255    ///     Err(LaError::InvertedInterval { lower: 3.0, upper: -2.0, .. })
256    /// );
257    /// # Ok(())
258    /// # }
259    /// ```
260    ///
261    /// # Errors
262    /// Returns [`LaError::NonFinite`] when either endpoint is NaN or infinity.
263    /// Returns [`LaError::InvertedInterval`] when `lower > upper`.
264    #[inline]
265    pub const fn try_new(lower: f64, upper: f64) -> Result<Self, LaError> {
266        if !lower.is_finite() {
267            return Err(LaError::non_finite_input_interval_bound(
268                IntervalBound::Lower,
269            ));
270        }
271        if !upper.is_finite() {
272            return Err(LaError::non_finite_input_interval_bound(
273                IntervalBound::Upper,
274            ));
275        }
276        if lower > upper {
277            return Err(LaError::inverted_interval(lower, upper));
278        }
279        Ok(Self::new_unchecked(lower, upper))
280    }
281
282    /// Construct a point interval from a finite binary64 value.
283    ///
284    /// This preserves the supplied value, including any earlier rounding.
285    /// Use [`try_from_subtraction`](Self::try_from_subtraction) to enclose a
286    /// subtraction before its rounding uncertainty is lost.
287    ///
288    /// # Examples
289    /// ```
290    /// use la_stack::prelude::*;
291    ///
292    /// # fn main() -> Result<(), LaError> {
293    /// let half = Interval::point(0.5)?;
294    /// assert_eq!((half.lower(), half.upper()), (0.5, 0.5));
295    /// assert_eq!(half.try_add(&half)?, Interval::ONE);
296    /// # Ok(())
297    /// # }
298    /// ```
299    ///
300    /// # Errors
301    /// Returns [`LaError::NonFinite`] when `value` is NaN or infinity.
302    #[inline]
303    pub const fn point(value: f64) -> Result<Self, LaError> {
304        match Self::try_new(value, value) {
305            Ok(interval) => Ok(interval),
306            Err(LaError::NonFinite { .. }) => Err(LaError::non_finite_input_scalar()),
307            Err(error) => Err(error),
308        }
309    }
310
311    /// Enclose the exact-real subtraction of two finite binary64 inputs.
312    ///
313    /// Unlike subtracting first and then calling [`point`](Self::point), this
314    /// method preserves the rounding uncertainty introduced by the subtraction.
315    ///
316    /// # Examples
317    /// ```
318    /// use la_stack::prelude::*;
319    ///
320    /// # fn main() -> Result<(), LaError> {
321    /// // The exact difference 1 - 2^-54 lies between adjacent binary64 values.
322    /// let difference = Interval::try_from_subtraction(1.0, f64::EPSILON / 4.0)?;
323    /// assert_eq!(difference.lower(), 1.0_f64.next_down());
324    /// assert_eq!(difference.upper(), 1.0);
325    ///
326    /// // Subtracting first loses that uncertainty and produces a point at 1.
327    /// let rounded = Interval::point(1.0 - f64::EPSILON / 4.0)?;
328    /// assert_eq!(rounded, Interval::ONE);
329    /// # Ok(())
330    /// # }
331    /// ```
332    ///
333    /// # Errors
334    /// Returns [`LaError::NonFinite`] for a non-finite input, preserving whether
335    /// it was the left or right operand. Returns
336    /// [`LaError::IntervalRangeExhausted`] when the exact difference has no
337    /// finite binary64 enclosure.
338    #[inline]
339    pub const fn try_from_subtraction(left: f64, right: f64) -> Result<Self, LaError> {
340        if !left.is_finite() {
341            return Err(LaError::non_finite_input_interval_operand(
342                IntervalOperand::Left,
343            ));
344        }
345        if !right.is_finite() {
346            return Err(LaError::non_finite_input_interval_operand(
347                IntervalOperand::Right,
348            ));
349        }
350        if right == 0.0 {
351            return Ok(Self::new_unchecked(left, left));
352        }
353        if left == 0.0 {
354            return Ok(Self::new_unchecked(-right, -right));
355        }
356        match rounded_add_bounds(left, -right, ArithmeticOperation::IntervalSubtraction) {
357            Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
358            Err(error) => Err(error),
359        }
360    }
361
362    /// Return the finite lower bound.
363    #[inline]
364    #[must_use]
365    pub const fn lower(self) -> f64 {
366        self.lower
367    }
368
369    /// Return the finite upper bound.
370    #[inline]
371    #[must_use]
372    pub const fn upper(self) -> f64 {
373        self.upper
374    }
375
376    /// Return whether this interval contains the finite `value`.
377    #[inline]
378    #[must_use]
379    pub const fn contains(self, value: f64) -> bool {
380        value.is_finite() && self.lower <= value && value <= self.upper
381    }
382
383    /// Add two intervals with outward rounding.
384    ///
385    /// # Examples
386    /// ```
387    /// use la_stack::prelude::*;
388    ///
389    /// # fn main() -> Result<(), LaError> {
390    /// let left = Interval::try_new(1.0, 2.0)?;
391    /// let right = Interval::try_new(0.5, 1.0)?;
392    /// assert_eq!(left.try_add(&right)?, Interval::try_new(1.5, 3.0)?);
393    /// # Ok(())
394    /// # }
395    /// ```
396    ///
397    /// # Errors
398    /// Returns [`LaError::IntervalRangeExhausted`] when the exact result range
399    /// has no finite binary64 enclosure.
400    #[inline]
401    pub const fn try_add(&self, other: &Self) -> Result<Self, LaError> {
402        self.try_add_for(other, ArithmeticOperation::IntervalAddition)
403    }
404
405    /// Multiply two intervals with outward rounding.
406    ///
407    /// For a square of the same represented value, prefer
408    /// [`try_square`](Self::try_square), which can give a tighter enclosure.
409    ///
410    /// # Examples
411    /// ```
412    /// use la_stack::prelude::*;
413    ///
414    /// # fn main() -> Result<(), LaError> {
415    /// let left = Interval::try_new(-2.0, 3.0)?;
416    /// let right = Interval::try_new(-4.0, -1.0)?;
417    /// assert_eq!(left.try_mul(&right)?, Interval::try_new(-12.0, 8.0)?);
418    /// # Ok(())
419    /// # }
420    /// ```
421    ///
422    /// # Errors
423    /// Returns [`LaError::IntervalRangeExhausted`] when the exact result range
424    /// has no finite binary64 enclosure.
425    #[inline]
426    pub const fn try_mul(&self, other: &Self) -> Result<Self, LaError> {
427        self.try_mul_for(other, ArithmeticOperation::IntervalMultiplication)
428    }
429
430    /// Enclose `1 / x` for every real `x` in this interval.
431    ///
432    /// # Examples
433    /// ```
434    /// use la_stack::prelude::*;
435    /// # fn main() -> Result<(), LaError> {
436    /// assert_eq!(Interval::try_new(-4.0, -2.0)?.try_recip()?,
437    ///            Interval::try_new(-0.5, -0.25)?);
438    /// # Ok(())
439    /// # }
440    /// ```
441    ///
442    /// # Errors
443    /// Returns [`LaError::IntervalZeroDivisor`] if this interval contains zero,
444    /// or [`LaError::IntervalRangeExhausted`] if no finite enclosure exists.
445    #[inline]
446    pub const fn try_recip(&self) -> Result<Self, LaError> {
447        Self::ONE.try_div_for(self, ArithmeticOperation::IntervalReciprocal)
448    }
449
450    /// Enclose `x / y` for all independently represented `x` and `y`.
451    ///
452    /// Endpoint quotients are rounded outward directly, so a representable
453    /// quotient does not require a representable reciprocal. Subnormal results
454    /// retain their enclosure, including when ordinary division rounds to zero.
455    /// Repeated operands are treated independently: `x / x` need not be `[1, 1]`.
456    ///
457    /// # Examples
458    /// ```
459    /// use la_stack::prelude::*;
460    /// # fn main() -> Result<(), LaError> {
461    /// let tiny = Interval::point(f64::from_bits(1))?;
462    /// assert_eq!(tiny.try_div(&tiny)?, Interval::ONE);
463    /// assert_eq!(Interval::try_new(-2.0, 6.0)?.try_div(&Interval::try_new(2.0, 4.0)?)?,
464    ///            Interval::try_new(-1.0, 3.0)?);
465    /// # Ok(())
466    /// # }
467    /// ```
468    ///
469    /// # Errors
470    /// Returns [`LaError::IntervalZeroDivisor`] if `other` contains zero
471    /// (even for a zero numerator), or [`LaError::IntervalRangeExhausted`] if
472    /// no finite enclosure exists.
473    #[inline]
474    pub const fn try_div(&self, other: &Self) -> Result<Self, LaError> {
475        self.try_div_for(other, ArithmeticOperation::IntervalDivision)
476    }
477
478    /// Negate an interval exactly by swapping and negating its endpoints.
479    ///
480    /// # Examples
481    /// ```
482    /// use la_stack::prelude::*;
483    ///
484    /// # fn main() -> Result<(), LaError> {
485    /// let range = Interval::try_new(-2.0, 3.0)?;
486    /// assert_eq!(range.negate(), Interval::try_new(-3.0, 2.0)?);
487    /// # Ok(())
488    /// # }
489    /// ```
490    #[inline]
491    pub const fn negate(&self) -> Self {
492        Self::new_unchecked(-self.upper, -self.lower)
493    }
494
495    /// Square an interval with outward rounding.
496    ///
497    /// An interval spanning zero has exact lower bound zero. The upper bound is
498    /// the outward-rounded square of the endpoint with greatest magnitude.
499    ///
500    /// # Examples
501    /// ```
502    /// use la_stack::prelude::*;
503    ///
504    /// # fn main() -> Result<(), LaError> {
505    /// let range = Interval::try_new(-2.0, 3.0)?;
506    /// assert_eq!(range.try_square()?, Interval::try_new(0.0, 9.0)?);
507    /// // Multiplication treats its two operands independently and is wider.
508    /// assert_eq!(range.try_mul(&range)?, Interval::try_new(-6.0, 9.0)?);
509    /// # Ok(())
510    /// # }
511    /// ```
512    ///
513    /// # Errors
514    /// Returns [`LaError::IntervalRangeExhausted`] when the exact square range
515    /// has no finite binary64 enclosure.
516    #[expect(
517        clippy::inline_always,
518        reason = "measured call overhead in lifted interval assembly"
519    )]
520    #[inline(always)]
521    pub const fn try_square(&self) -> Result<Self, LaError> {
522        let operation = ArithmeticOperation::IntervalSquare;
523        let left_square = match rounded_product_bounds(self.lower, self.lower, operation) {
524            Ok(bounds) => bounds,
525            Err(error) => return Err(error),
526        };
527        if self.is_point() {
528            return Ok(Self::new_unchecked(left_square.0, left_square.1));
529        }
530        let right_square = match rounded_product_bounds(self.upper, self.upper, operation) {
531            Ok(bounds) => bounds,
532            Err(error) => return Err(error),
533        };
534        let lower = if self.lower <= 0.0 && self.upper >= 0.0 {
535            0.0
536        } else if left_square.0 < right_square.0 {
537            left_square.0
538        } else {
539            right_square.0
540        };
541        let upper = if left_square.1 > right_square.1 {
542            left_square.1
543        } else {
544            right_square.1
545        };
546        Ok(Self::new_unchecked(lower, upper))
547    }
548
549    /// Construct an interval after its finite ordered-bound invariant is known.
550    #[inline]
551    const fn new_unchecked(lower: f64, upper: f64) -> Self {
552        Self {
553            lower: canonical_zero(lower),
554            upper: canonical_zero(upper),
555        }
556    }
557
558    /// Add while attributing range failure to the owning public operation.
559    #[expect(
560        clippy::inline_always,
561        reason = "measured result-copy overhead in the interval determinant kernel"
562    )]
563    #[inline(always)]
564    const fn try_add_for(
565        &self,
566        other: &Self,
567        operation: ArithmeticOperation,
568    ) -> Result<Self, LaError> {
569        if self.is_zero() {
570            return Ok(*other);
571        }
572        if other.is_zero() {
573            return Ok(*self);
574        }
575        if self.is_point() && other.is_point() {
576            return match rounded_add_bounds(self.lower, other.lower, operation) {
577                Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
578                Err(error) => Err(error),
579            };
580        }
581
582        let lower = match rounded_add_endpoint::<false>(self.lower, other.lower, operation) {
583            Ok(lower) => lower,
584            Err(error) => return Err(error),
585        };
586        let upper = match rounded_add_endpoint::<true>(self.upper, other.upper, operation) {
587            Ok(upper) => upper,
588            Err(error) => return Err(error),
589        };
590        Ok(Self::new_unchecked(lower, upper))
591    }
592
593    /// Multiply while attributing range failure to the owning public operation.
594    #[expect(
595        clippy::inline_always,
596        reason = "measured result-copy overhead in the interval determinant kernel"
597    )]
598    #[inline(always)]
599    const fn try_mul_for(
600        &self,
601        other: &Self,
602        operation: ArithmeticOperation,
603    ) -> Result<Self, LaError> {
604        if self.is_zero() || other.is_zero() {
605            return Ok(Self::ZERO);
606        }
607        if self.is_one() {
608            return Ok(*other);
609        }
610        if other.is_one() {
611            return Ok(*self);
612        }
613        if self.is_point() && other.is_point() {
614            return match rounded_product_bounds(self.lower, other.lower, operation) {
615                Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
616                Err(error) => Err(error),
617            };
618        }
619
620        self.try_mul_by_sign(other, operation)
621    }
622
623    /// Select only the endpoint products that can attain each range extremum.
624    #[inline]
625    const fn try_mul_by_sign(
626        &self,
627        other: &Self,
628        operation: ArithmeticOperation,
629    ) -> Result<Self, LaError> {
630        let self_nonnegative = self.lower >= 0.0;
631        let self_nonpositive = self.upper <= 0.0;
632        let other_nonnegative = other.lower >= 0.0;
633        let other_nonpositive = other.upper <= 0.0;
634
635        if self_nonnegative {
636            if other_nonnegative {
637                return Self::try_product_extrema(
638                    (self.lower, other.lower),
639                    (self.upper, other.upper),
640                    operation,
641                );
642            }
643            if other_nonpositive {
644                return Self::try_product_extrema(
645                    (self.upper, other.lower),
646                    (self.lower, other.upper),
647                    operation,
648                );
649            }
650            return Self::try_product_extrema(
651                (self.upper, other.lower),
652                (self.upper, other.upper),
653                operation,
654            );
655        }
656        if self_nonpositive {
657            if other_nonnegative {
658                return Self::try_product_extrema(
659                    (self.lower, other.upper),
660                    (self.upper, other.lower),
661                    operation,
662                );
663            }
664            if other_nonpositive {
665                return Self::try_product_extrema(
666                    (self.upper, other.upper),
667                    (self.lower, other.lower),
668                    operation,
669                );
670            }
671            return Self::try_product_extrema(
672                (self.lower, other.upper),
673                (self.lower, other.lower),
674                operation,
675            );
676        }
677        if other_nonnegative {
678            return Self::try_product_extrema(
679                (self.lower, other.upper),
680                (self.upper, other.upper),
681                operation,
682            );
683        }
684        if other_nonpositive {
685            return Self::try_product_extrema(
686                (self.upper, other.lower),
687                (self.lower, other.lower),
688                operation,
689            );
690        }
691
692        let lower_left = match rounded_product_bounds(self.lower, other.upper, operation) {
693            Ok(bounds) => bounds,
694            Err(error) => return Err(error),
695        };
696        let lower_right = match rounded_product_bounds(self.upper, other.lower, operation) {
697            Ok(bounds) => bounds,
698            Err(error) => return Err(error),
699        };
700        let upper_left = match rounded_product_bounds(self.lower, other.lower, operation) {
701            Ok(bounds) => bounds,
702            Err(error) => return Err(error),
703        };
704        let upper_right = match rounded_product_bounds(self.upper, other.upper, operation) {
705            Ok(bounds) => bounds,
706            Err(error) => return Err(error),
707        };
708        let lower = if lower_left.0 < lower_right.0 {
709            lower_left.0
710        } else {
711            lower_right.0
712        };
713        let upper = if upper_left.1 > upper_right.1 {
714            upper_left.1
715        } else {
716            upper_right.1
717        };
718        Ok(Self::new_unchecked(lower, upper))
719    }
720
721    /// Enclose the selected exact lower and upper product extrema.
722    #[inline]
723    const fn try_product_extrema(
724        lower_factors: (f64, f64),
725        upper_factors: (f64, f64),
726        operation: ArithmeticOperation,
727    ) -> Result<Self, LaError> {
728        let lower = match rounded_product_bounds(lower_factors.0, lower_factors.1, operation) {
729            Ok((lower, _)) => lower,
730            Err(error) => return Err(error),
731        };
732        let upper = match rounded_product_bounds(upper_factors.0, upper_factors.1, operation) {
733            Ok((_, upper)) => upper,
734            Err(error) => return Err(error),
735        };
736        Ok(Self::new_unchecked(lower, upper))
737    }
738
739    /// Divide while attributing range failure to the owning public operation.
740    const fn try_div_for(
741        &self,
742        other: &Self,
743        operation: ArithmeticOperation,
744    ) -> Result<Self, LaError> {
745        if other.contains(0.0) {
746            return Err(LaError::IntervalZeroDivisor {
747                lower: other.lower,
748                upper: other.upper,
749            });
750        }
751        // On a rectangle with zero-free denominator, extrema occur at corners.
752        // Point operands avoid computing the same quotient four times.
753        let numerators = [self.lower, self.upper];
754        let denominators = [other.lower, other.upper];
755        let numerator_count = if self.is_point() { 1 } else { 2 };
756        let denominator_count = if other.is_point() { 1 } else { 2 };
757        let mut lower = f64::MAX;
758        let mut upper = -f64::MAX;
759        let mut i = 0;
760        while i < numerator_count {
761            let mut j = 0;
762            while j < denominator_count {
763                let (lo, hi) =
764                    match rounded_quotient_bounds(numerators[i], denominators[j], operation) {
765                        Ok(bounds) => bounds,
766                        Err(error) => return Err(error),
767                    };
768                if lo < lower {
769                    lower = lo;
770                }
771                if hi > upper {
772                    upper = hi;
773                }
774                j += 1;
775            }
776            i += 1;
777        }
778        Ok(Self::new_unchecked(lower, upper))
779    }
780
781    /// Enclose `self - factor * value` with the solve's error attribution.
782    const fn try_eliminate(&self, factor: &Self, value: &Self) -> Result<Self, LaError> {
783        let product = match factor.try_mul_for(value, ArithmeticOperation::IntervalSolve) {
784            Ok(product) => product,
785            Err(error) => return Err(error),
786        };
787        self.try_add_for(&product.negate(), ArithmeticOperation::IntervalSolve)
788    }
789
790    /// Lower bound on the magnitude of every represented value.
791    const fn separation_from_zero(&self) -> f64 {
792        if self.lower > 0.0 {
793            self.lower
794        } else if self.upper < 0.0 {
795            -self.upper
796        } else {
797            0.0
798        }
799    }
800
801    /// Return whether this interval is exactly real zero.
802    #[inline]
803    const fn is_zero(&self) -> bool {
804        self.lower == 0.0 && self.upper == 0.0
805    }
806
807    /// Return whether this interval is exactly real one.
808    #[inline]
809    const fn is_one(&self) -> bool {
810        self.lower.to_bits() == 1.0_f64.to_bits() && self.upper.to_bits() == 1.0_f64.to_bits()
811    }
812
813    /// Return whether this interval contains one binary64 point.
814    #[inline]
815    const fn is_point(&self) -> bool {
816        self.lower.to_bits() == self.upper.to_bits()
817    }
818}
819
820impl Default for Interval {
821    #[inline]
822    fn default() -> Self {
823        Self::ZERO
824    }
825}
826
827impl<const D: usize> IntervalMatrix<D> {
828    /// Construct an interval matrix from already-validated interval rows.
829    ///
830    /// See [`det`](Self::det) for an example with non-point entries.
831    #[inline]
832    pub const fn from_rows(rows: [[Interval; D]; D]) -> Self {
833        Self { rows }
834    }
835
836    /// Lift finite binary64 rows into point intervals.
837    ///
838    /// This preserves the stored binary64 values exactly; it does not recover
839    /// uncertainty from arithmetic performed before this call.
840    /// See [`IntervalMatrix`] for a determinant-sign example using this constructor.
841    ///
842    /// # Errors
843    /// Returns [`LaError::NonFinite`] with matrix coordinates for the first NaN
844    /// or infinity in row-major order.
845    #[inline]
846    pub const fn try_from_point_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
847        let mut intervals = [[Interval::ZERO; D]; D];
848        let mut row = 0;
849        while row < D {
850            let mut column = 0;
851            while column < D {
852                let value = rows[row][column];
853                if !value.is_finite() {
854                    return Err(LaError::non_finite_input_matrix(row, column));
855                }
856                intervals[row][column] = Interval::new_unchecked(value, value);
857                column += 1;
858            }
859            row += 1;
860        }
861        Ok(Self::from_rows(intervals))
862    }
863
864    /// Lift a finite [`Matrix`] into point intervals.
865    ///
866    /// Earlier rounded expression construction is not enclosed; use interval
867    /// operations while constructing derived coefficients when that uncertainty
868    /// belongs in the proof.
869    ///
870    /// # Examples
871    /// ```
872    /// use la_stack::prelude::*;
873    ///
874    /// # fn main() -> Result<(), LaError> {
875    /// let matrix = Matrix::<2>::try_from_rows([[2.0, 0.0], [0.0, 3.0]])?;
876    /// let intervals = IntervalMatrix::from_matrix(&matrix);
877    /// assert_eq!(intervals.det()?, Interval::point(6.0)?);
878    /// # Ok(())
879    /// # }
880    /// ```
881    #[inline]
882    pub const fn from_matrix(matrix: &Matrix<D>) -> Self {
883        let matrix_rows = matrix.as_rows();
884        let mut intervals = [[Interval::ZERO; D]; D];
885        let mut row = 0;
886        while row < D {
887            let mut column = 0;
888            while column < D {
889                let value = matrix_rows[row][column];
890                intervals[row][column] = Interval::new_unchecked(value, value);
891                column += 1;
892            }
893            row += 1;
894        }
895        Self::from_rows(intervals)
896    }
897
898    /// All-zero interval matrix.
899    #[inline]
900    pub const fn zero() -> Self {
901        Self::from_rows([[Interval::ZERO; D]; D])
902    }
903
904    /// Identity interval matrix.
905    #[inline]
906    pub const fn identity() -> Self {
907        let mut matrix = Self::zero();
908        let mut index = 0;
909        while index < D {
910            matrix.rows[index][index] = Interval::ONE;
911            index += 1;
912        }
913        matrix
914    }
915
916    /// Borrow the row-major interval storage.
917    #[inline]
918    #[must_use]
919    pub const fn as_rows(&self) -> &[[Interval; D]; D] {
920        &self.rows
921    }
922
923    /// Consume this matrix and return its row-major interval storage.
924    #[inline]
925    #[must_use]
926    pub const fn into_rows(self) -> [[Interval; D]; D] {
927        self.rows
928    }
929
930    /// Get an interval entry with bounds checking.
931    #[inline]
932    #[must_use]
933    pub const fn get(&self, row: usize, column: usize) -> Option<Interval> {
934        if row < D && column < D {
935            Some(self.rows[row][column])
936        } else {
937            None
938        }
939    }
940
941    /// Get an interval entry while preserving index context on failure.
942    ///
943    /// See [`set`](Self::set) for an example of mutation and checked access.
944    ///
945    /// # Errors
946    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
947    #[inline]
948    pub const fn try_get(&self, row: usize, column: usize) -> Result<Interval, LaError> {
949        if row < D && column < D {
950            Ok(self.rows[row][column])
951        } else {
952            Err(LaError::index_out_of_bounds(row, column, D))
953        }
954    }
955
956    /// Set an interval entry with bounds checking.
957    ///
958    /// Validation is unnecessary for the value because [`Interval`] already
959    /// carries the finite ordered-bound proof. An invalid index leaves the
960    /// matrix unchanged.
961    ///
962    /// # Examples
963    /// ```
964    /// use core::assert_matches;
965    /// use la_stack::prelude::*;
966    ///
967    /// # fn main() -> Result<(), LaError> {
968    /// let mut matrix = IntervalMatrix::<2>::identity();
969    /// let range = Interval::try_new(2.0, 3.0)?;
970    /// matrix.set(0, 0, range)?;
971    /// assert_eq!(matrix.try_get(0, 0)?, range);
972    /// let before = matrix;
973    /// assert_matches!(
974    ///     matrix.set(2, 0, Interval::ZERO),
975    ///     Err(LaError::IndexOutOfBounds { row: 2, col: 0, dim: 2, .. })
976    /// );
977    /// assert_eq!(matrix, before);
978    /// # Ok(())
979    /// # }
980    /// ```
981    ///
982    /// # Errors
983    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
984    #[inline]
985    pub const fn set(&mut self, row: usize, column: usize, value: Interval) -> Result<(), LaError> {
986        if row >= D || column >= D {
987            return Err(LaError::index_out_of_bounds(row, column, D));
988        }
989        self.rows[row][column] = value;
990        Ok(())
991    }
992
993    /// Enclose the solution of every real system `A x = b` admitted by these
994    /// coefficient intervals and `rhs`.
995    ///
996    /// On success every admitted `A` is nonsingular and the unique exact-real
997    /// solution satisfies `x[i] ∈ result[i]`. Entries vary independently; any
998    /// correlations introduced during expression assembly are safely widened.
999    /// The enclosure need not be tight. Wide intervals and ill-conditioning
1000    /// can prevent certification even when every admitted matrix is invertible.
1001    ///
1002    /// Uses outward-rounded Gaussian elimination and back substitution, with
1003    /// row pivots maximizing their minimum distance from zero (first row wins
1004    /// ties). No tolerance admits a zero-containing pivot. See `REFERENCES.md`
1005    /// \[20\] and the
1006    /// [enclosure proof](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#interval-linear-solves).
1007    ///
1008    /// Both shapes are fixed by `D`. All const dimensions are supported, with
1009    /// O(D³) work and O(D²) inline scratch space intended for small systems;
1010    /// D=0 returns the empty solution. Inputs are unchanged on all outcomes.
1011    /// This operation is available without the `exact` feature.
1012    ///
1013    /// # Examples
1014    /// ```
1015    /// use la_stack::prelude::*;
1016    /// # fn main() -> Result<(), LaError> {
1017    /// let matrix = IntervalMatrix::try_from_point_rows([[0.0, 2.0], [3.0, 1.0]])?;
1018    /// let rhs = [Interval::point(4.0)?, Interval::point(5.0)?];
1019    /// let x = matrix.solve(&rhs)?;
1020    /// assert!(x[0].contains(1.0) && x[1].contains(2.0));
1021    /// # Ok(())
1022    /// # }
1023    /// ```
1024    ///
1025    /// # Errors
1026    /// Returns [`LaError::IntervalSolveInconclusive`] at the first column whose
1027    /// remaining pivot intervals all contain zero, including the final column.
1028    /// This never establishes singularity, even for point inputs or `[0, 0]`
1029    /// pivots; this method never returns [`LaError::Singular`].
1030    /// Returns [`LaError::IntervalRangeExhausted`] with
1031    /// [`ArithmeticOperation::IntervalSolve`] if an intermediate or result has
1032    /// no finite enclosure. Both outcomes require the caller's fallback when a
1033    /// certified answer is needed. Invalid raw endpoints are rejected by the
1034    /// interval constructors before this method can be called.
1035    pub const fn solve(&self, rhs: &[Interval; D]) -> Result<[Interval; D], LaError> {
1036        let mut rows = self.rows;
1037        let mut work_rhs = *rhs;
1038        let mut column = 0;
1039        while column < D {
1040            let mut pivot_row = column;
1041            let mut separation = 0.0;
1042            let mut row = column;
1043            while row < D {
1044                let candidate = rows[row][column].separation_from_zero();
1045                if candidate > separation {
1046                    separation = candidate;
1047                    pivot_row = row;
1048                }
1049                row += 1;
1050            }
1051            if separation == 0.0 {
1052                return Err(LaError::IntervalSolveInconclusive { pivot_col: column });
1053            }
1054            rows.swap(column, pivot_row);
1055            work_rhs.swap(column, pivot_row);
1056            row = column + 1;
1057            while row < D {
1058                if !rows[row][column].is_zero() {
1059                    let factor = match rows[row][column]
1060                        .try_div_for(&rows[column][column], ArithmeticOperation::IntervalSolve)
1061                    {
1062                        Ok(factor) => factor,
1063                        Err(error) => return Err(error),
1064                    };
1065                    let mut j = column + 1;
1066                    while j < D {
1067                        rows[row][j] = match rows[row][j].try_eliminate(&factor, &rows[column][j]) {
1068                            Ok(value) => value,
1069                            Err(error) => return Err(error),
1070                        };
1071                        j += 1;
1072                    }
1073                    work_rhs[row] = match work_rhs[row].try_eliminate(&factor, &work_rhs[column]) {
1074                        Ok(value) => value,
1075                        Err(error) => return Err(error),
1076                    };
1077                    // The exact row operation cancels this entry identically
1078                    // for every admitted system, despite interval dependency.
1079                    rows[row][column] = Interval::ZERO;
1080                }
1081                row += 1;
1082            }
1083            column += 1;
1084        }
1085        let mut solution = [Interval::ZERO; D];
1086        let mut row = D;
1087        while row > 0 {
1088            row -= 1;
1089            let mut residual = work_rhs[row];
1090            let mut column = row + 1;
1091            while column < D {
1092                residual = match residual.try_eliminate(&rows[row][column], &solution[column]) {
1093                    Ok(value) => value,
1094                    Err(error) => return Err(error),
1095                };
1096                column += 1;
1097            }
1098            solution[row] =
1099                match residual.try_div_for(&rows[row][row], ArithmeticOperation::IntervalSolve) {
1100                    Ok(value) => value,
1101                    Err(error) => return Err(error),
1102                };
1103        }
1104        Ok(solution)
1105    }
1106
1107    /// Enclose the determinant with division-free subset dynamic programming.
1108    ///
1109    /// For each column subset, the DP stores the determinant interval of the
1110    /// leading rows and those columns. This evaluates the Leibniz expansion in
1111    /// `D × 2^(D-1)` products and additions without choosing or dividing by a
1112    /// pivot. The returned interval therefore encloses every exact-real
1113    /// determinant represented by the input intervals, subject only to an
1114    /// explicit range failure.
1115    ///
1116    /// The D=0 determinant follows the empty-product convention and is `[1, 1]`.
1117    ///
1118    /// Use [`det_sign`](Self::det_sign) when only sign evidence is needed.
1119    ///
1120    /// # Examples
1121    /// ```
1122    /// use la_stack::prelude::*;
1123    ///
1124    /// # fn main() -> Result<(), LaError> {
1125    /// // Every represented diagonal matrix has a determinant in [8, 15].
1126    /// let matrix = IntervalMatrix::<2>::from_rows([
1127    ///     [Interval::try_new(2.0, 3.0)?, Interval::ZERO],
1128    ///     [Interval::ZERO, Interval::try_new(4.0, 5.0)?],
1129    /// ]);
1130    /// assert_eq!(matrix.det()?, Interval::try_new(8.0, 15.0)?);
1131    /// # Ok(())
1132    /// # }
1133    /// ```
1134    ///
1135    /// # Errors
1136    /// Returns [`LaError::UnsupportedDimension`] for D>7. Returns
1137    /// [`LaError::IntervalRangeExhausted`] with interval-determinant provenance
1138    /// when an exact intermediate has no finite binary64 enclosure; callers can
1139    /// then proceed to an exact or higher-range fallback.
1140    #[inline]
1141    pub const fn det(&self) -> Result<Interval, LaError> {
1142        // Stable Rust cannot express [Interval; 1 << D]. Dispatch only the
1143        // workspace extent; every arm uses the identical arithmetic tree.
1144        match D {
1145            0 | 1 => self.det_with_workspace::<2>(),
1146            2 => self.det_with_workspace::<4>(),
1147            3 => self.det_with_workspace::<8>(),
1148            4 => self.det_with_workspace::<16>(),
1149            5 => self.det_with_workspace::<32>(),
1150            6 => self.det_with_workspace::<64>(),
1151            7 => self.det_with_workspace::<128>(),
1152            _ => Err(LaError::unsupported_dimension(D, MAX_INTERVAL_MATRIX_DIM)),
1153        }
1154    }
1155
1156    /// Evaluate with the state count established by the dimension dispatch.
1157    ///
1158    /// Only `det` calls this helper, after proving D is supported and STATES
1159    /// covers all `2^D` subsets. Its D=0 and D=1 arms reserve two slots. These
1160    /// preconditions keep shifts and indexing in range while preserving the
1161    /// same expansion order for every workspace size.
1162    #[inline]
1163    const fn det_with_workspace<const STATES: usize>(&self) -> Result<Interval, LaError> {
1164        let state_count = 1_usize << D;
1165        let mut partials = [Interval::ZERO; STATES];
1166        partials[0] = Interval::ONE;
1167        let operation = ArithmeticOperation::IntervalDeterminant;
1168
1169        let mut subset = 1;
1170        while subset < state_count {
1171            let row = subset.count_ones() as usize - 1;
1172            let mut sum = Interval::ZERO;
1173            // Removing the lowest set bit visits active columns in the same
1174            // ascending order as the full scan, preserving every interval
1175            // operation and the first range-exhaustion error.
1176            let mut remaining = subset;
1177            while remaining != 0 {
1178                let column = remaining.trailing_zeros() as usize;
1179                let column_bit = 1_usize << column;
1180                remaining &= remaining - 1;
1181                let previous = subset ^ column_bit;
1182                let mut term =
1183                    match partials[previous].try_mul_for(&self.rows[row][column], operation) {
1184                        Ok(term) => term,
1185                        Err(error) => return Err(error),
1186                    };
1187                if !remaining.count_ones().is_multiple_of(2) {
1188                    term = term.negate();
1189                }
1190                sum = match sum.try_add_for(&term, operation) {
1191                    Ok(next_sum) => next_sum,
1192                    Err(error) => return Err(error),
1193                };
1194            }
1195            partials[subset] = sum;
1196            subset += 1;
1197        }
1198
1199        Ok(partials[state_count - 1])
1200    }
1201
1202    /// Return proof-bearing determinant sign evidence.
1203    ///
1204    /// An interval strictly on one side of zero proves that sign. Only the
1205    /// singleton interval `[0, 0]` proves `Zero`; every other overlap with zero
1206    /// is [`IntervalDeterminantSign::Inconclusive`].
1207    ///
1208    /// # Examples
1209    /// ```
1210    /// use la_stack::prelude::*;
1211    ///
1212    /// # fn main() -> Result<(), LaError> {
1213    /// let mut matrix = IntervalMatrix::<2>::identity();
1214    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Positive);
1215    ///
1216    /// matrix.set(0, 0, Interval::try_new(-1.0, 1.0)?)?;
1217    /// // This range includes nonsingular matrices of both signs; a caller
1218    /// // needs tighter or exact input before it can decide singularity.
1219    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1220    ///
1221    /// matrix.set(0, 0, Interval::ZERO)?;
1222    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Zero);
1223    /// # Ok(())
1224    /// # }
1225    /// ```
1226    ///
1227    /// # Errors
1228    /// Propagates the dimension and arithmetic range failures from
1229    /// [`det`](Self::det).
1230    #[inline]
1231    pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
1232        let determinant = match self.det() {
1233            Ok(determinant) => determinant,
1234            Err(error) => return Err(error),
1235        };
1236        if determinant.lower > 0.0 {
1237            Ok(IntervalDeterminantSign::Positive)
1238        } else if determinant.upper < 0.0 {
1239            Ok(IntervalDeterminantSign::Negative)
1240        } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
1241            Ok(IntervalDeterminantSign::Zero)
1242        } else {
1243            Ok(IntervalDeterminantSign::Inconclusive)
1244        }
1245    }
1246}
1247
1248impl<const D: usize> Default for IntervalMatrix<D> {
1249    #[inline]
1250    fn default() -> Self {
1251        Self::zero()
1252    }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257    use core::assert_matches;
1258
1259    use pastey::paste;
1260
1261    use super::*;
1262    use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1263
1264    #[test]
1265    fn point_and_bounds_enforce_interval_invariants() {
1266        assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1267        assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1268        assert_matches!(
1269            Interval::point(f64::NAN),
1270            Err(LaError::NonFinite {
1271                location: NonFiniteLocation::Scalar,
1272                origin: NonFiniteOrigin::Input,
1273                ..
1274            })
1275        );
1276        assert_matches!(
1277            Interval::try_new(2.0, 1.0),
1278            Err(LaError::InvertedInterval {
1279                lower: 2.0,
1280                upper: 1.0,
1281                ..
1282            })
1283        );
1284    }
1285
1286    #[test]
1287    fn constructors_preserve_non_finite_input_locations() {
1288        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1289            assert_eq!(
1290                Interval::try_new(value, 0.0),
1291                Err(LaError::NonFinite {
1292                    location: NonFiniteLocation::IntervalBound {
1293                        bound: IntervalBound::Lower,
1294                    },
1295                    origin: NonFiniteOrigin::Input,
1296                })
1297            );
1298            assert_eq!(
1299                Interval::try_new(0.0, value),
1300                Err(LaError::NonFinite {
1301                    location: NonFiniteLocation::IntervalBound {
1302                        bound: IntervalBound::Upper,
1303                    },
1304                    origin: NonFiniteOrigin::Input,
1305                })
1306            );
1307            assert_eq!(
1308                Interval::try_from_subtraction(value, 0.0),
1309                Err(LaError::NonFinite {
1310                    location: NonFiniteLocation::IntervalOperand {
1311                        operand: IntervalOperand::Left,
1312                    },
1313                    origin: NonFiniteOrigin::Input,
1314                })
1315            );
1316            assert_eq!(
1317                Interval::try_from_subtraction(0.0, value),
1318                Err(LaError::NonFinite {
1319                    location: NonFiniteLocation::IntervalOperand {
1320                        operand: IntervalOperand::Right,
1321                    },
1322                    origin: NonFiniteOrigin::Input,
1323                })
1324            );
1325        }
1326
1327        assert_eq!(
1328            Interval::try_new(f64::NAN, f64::INFINITY),
1329            Err(LaError::NonFinite {
1330                location: NonFiniteLocation::IntervalBound {
1331                    bound: IntervalBound::Lower,
1332                },
1333                origin: NonFiniteOrigin::Input,
1334            })
1335        );
1336        assert_eq!(
1337            Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1338            Err(LaError::NonFinite {
1339                location: NonFiniteLocation::IntervalOperand {
1340                    operand: IntervalOperand::Left,
1341                },
1342                origin: NonFiniteOrigin::Input,
1343            })
1344        );
1345
1346        let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1347        assert_eq!(
1348            IntervalMatrix::<2>::try_from_point_rows(rows),
1349            Err(LaError::NonFinite {
1350                location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1351                origin: NonFiniteOrigin::Input,
1352            })
1353        );
1354    }
1355
1356    #[test]
1357    fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1358        let one = Interval::point(1.0)?;
1359        let two = Interval::point(2.0)?;
1360        assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1361        assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1362        assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1363        assert_eq!(
1364            Interval::try_new(-2.0, -1.0)?.negate(),
1365            Interval::try_new(1.0, 2.0)?
1366        );
1367        Ok(())
1368    }
1369
1370    #[test]
1371    fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1372        let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1373        let rounded_subtraction = 1.0_f64 - 0.1;
1374        assert_eq!(
1375            subtraction,
1376            Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1377        );
1378
1379        let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1380        let rounded_product = 0.1_f64 * 0.2;
1381        assert_eq!(
1382            product,
1383            Interval::try_new(rounded_product.next_down(), rounded_product)?
1384        );
1385
1386        let below_one = 1.0 - f64::EPSILON;
1387        let above_one = 1.0 + f64::EPSILON;
1388        let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1389        assert_eq!(
1390            binade_boundary,
1391            Interval::try_new(1.0_f64.next_down(), 1.0)?
1392        );
1393        Ok(())
1394    }
1395
1396    #[test]
1397    fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1398        let next = 1.0_f64.next_up();
1399        let difference = Interval::try_from_subtraction(next, 1.0)?;
1400        assert_eq!(difference, Interval::point(f64::EPSILON)?);
1401        Ok(())
1402    }
1403
1404    #[test]
1405    fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1406        for value in [
1407            -f64::MAX,
1408            -1.0,
1409            -f64::MIN_POSITIVE,
1410            -f64::from_bits(1),
1411            -0.0,
1412            0.0,
1413            f64::from_bits(1),
1414            f64::MIN_POSITIVE,
1415            1.0,
1416            f64::MAX,
1417        ] {
1418            for zero in [0.0, -0.0] {
1419                for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1420                    let result = Interval::try_from_subtraction(left, right)?;
1421                    let expected_bits = if expected == 0.0 {
1422                        0
1423                    } else {
1424                        expected.to_bits()
1425                    };
1426                    assert_eq!(result.lower().to_bits(), expected_bits);
1427                    assert_eq!(result.upper().to_bits(), expected_bits);
1428                }
1429            }
1430        }
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1436        for (value, lower, upper) in [
1437            (0.0, 0.0_f64, 0.0),
1438            (-0.0, 0.0, 0.0),
1439            (2.0, 4.0, 4.0),
1440            (-2.0, 4.0, 4.0),
1441            (f64::from_bits(1), 0.0, f64::from_bits(1)),
1442            (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1443        ] {
1444            let squared = Interval::point(value)?.try_square()?;
1445            assert_eq!(squared.lower().to_bits(), lower.to_bits());
1446            assert_eq!(squared.upper().to_bits(), upper.to_bits());
1447        }
1448        // Squaring the exact stored binary64 0.1 lies strictly between these
1449        // adjacent values; the expected bits come from its rational square.
1450        for value in [0.1, -0.1] {
1451            let squared = Interval::point(value)?.try_square()?;
1452            assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1453            assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1454        }
1455        Ok(())
1456    }
1457
1458    #[test]
1459    fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1460        let expected = Interval::try_new(4.0, 9.0)?;
1461        for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1462            assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1463        }
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1469        let left = Interval::try_new(-1.0, 1.0)?;
1470        // A quarter ULP rounds both sums inward; three quarters rounds them
1471        // outward already. Both exact dyadic sums have the same tight bounds.
1472        for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1473            let right = Interval::try_new(-radius, radius)?;
1474            assert_eq!(
1475                left.try_add(&right)?,
1476                Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1477            );
1478        }
1479        assert_eq!(
1480            left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1481            Interval::try_new(-3.0, 3.0)?
1482        );
1483        Ok(())
1484    }
1485
1486    #[test]
1487    fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1488        let least_subnormal = f64::from_bits(1);
1489        let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1490        assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1491        Ok(())
1492    }
1493
1494    #[test]
1495    fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1496        let error = Interval::point(f64::MAX)?
1497            .try_mul(&Interval::point(2.0)?)
1498            .unwrap_err();
1499        assert_eq!(
1500            error,
1501            LaError::IntervalRangeExhausted {
1502                operation: ArithmeticOperation::IntervalMultiplication,
1503            }
1504        );
1505        Ok(())
1506    }
1507
1508    #[test]
1509    fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1510        let maximum = Interval::point(f64::MAX)?;
1511        let tiny = Interval::point(f64::MIN_POSITIVE)?;
1512        assert_eq!(
1513            maximum.try_add(&maximum),
1514            Err(LaError::IntervalRangeExhausted {
1515                operation: ArithmeticOperation::IntervalAddition,
1516            })
1517        );
1518        assert_eq!(
1519            maximum.try_add(&tiny),
1520            Err(LaError::IntervalRangeExhausted {
1521                operation: ArithmeticOperation::IntervalAddition,
1522            })
1523        );
1524        let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1525        assert_eq!(
1526            nonnegative.try_add(&nonnegative),
1527            Err(LaError::IntervalRangeExhausted {
1528                operation: ArithmeticOperation::IntervalAddition,
1529            })
1530        );
1531        let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1532        for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1533            assert_eq!(
1534                left.try_add(&right),
1535                Err(LaError::IntervalRangeExhausted {
1536                    operation: ArithmeticOperation::IntervalAddition,
1537                }),
1538            );
1539        }
1540
1541        let finite_difference = maximum.try_add(&tiny.negate())?;
1542        assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1543        assert!(finite_difference.lower() < finite_difference.upper());
1544        Ok(())
1545    }
1546
1547    #[test]
1548    fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1549        assert_eq!(
1550            Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1551            Err(LaError::IntervalRangeExhausted {
1552                operation: ArithmeticOperation::IntervalSubtraction,
1553            })
1554        );
1555        assert_eq!(
1556            Interval::point(f64::MAX)?.try_square(),
1557            Err(LaError::IntervalRangeExhausted {
1558                operation: ArithmeticOperation::IntervalSquare,
1559            })
1560        );
1561        assert_eq!(
1562            Interval::try_new(-1.0, f64::MAX)?.try_square(),
1563            Err(LaError::IntervalRangeExhausted {
1564                operation: ArithmeticOperation::IntervalSquare,
1565            })
1566        );
1567        Ok(())
1568    }
1569
1570    #[test]
1571    fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1572        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1573        assert_eq!(
1574            matrix.det(),
1575            Err(LaError::IntervalRangeExhausted {
1576                operation: ArithmeticOperation::IntervalDeterminant,
1577            })
1578        );
1579
1580        let accumulating =
1581            IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1582        assert_eq!(
1583            accumulating.det(),
1584            Err(LaError::IntervalRangeExhausted {
1585                operation: ArithmeticOperation::IntervalDeterminant,
1586            })
1587        );
1588        assert_eq!(
1589            accumulating.det_sign(),
1590            Err(LaError::IntervalRangeExhausted {
1591                operation: ArithmeticOperation::IntervalDeterminant,
1592            })
1593        );
1594        Ok(())
1595    }
1596
1597    #[test]
1598    fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1599    {
1600        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1601        assert_eq!(
1602            matrix.det(),
1603            Err(LaError::IntervalRangeExhausted {
1604                operation: ArithmeticOperation::IntervalDeterminant,
1605            })
1606        );
1607        Ok(())
1608    }
1609
1610    #[test]
1611    fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1612        let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1613        assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1614        Ok(())
1615    }
1616
1617    #[test]
1618    fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1619        for (left, right, expected) in [
1620            ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1621            ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1622            ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1623            ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1624            ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1625            ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1626            ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1627            ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1628            ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1629        ] {
1630            let product = Interval::try_new(left.0, left.1)?
1631                .try_mul(&Interval::try_new(right.0, right.1)?)?;
1632            assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1633        }
1634
1635        assert_eq!(
1636            Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1637            Interval::ZERO
1638        );
1639        Ok(())
1640    }
1641
1642    #[test]
1643    fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1644        let half_maximum = f64::MAX / 2.0;
1645        for (left, right) in [
1646            ((half_maximum, f64::MAX), (-2.0, -1.0)),
1647            ((half_maximum, f64::MAX), (1.0, 2.0)),
1648            ((-f64::MAX, 1.0), (-1.0, 2.0)),
1649            ((-1.0, f64::MAX), (-2.0, 1.0)),
1650            ((-f64::MAX, 1.0), (-2.0, 1.0)),
1651            ((-1.0, f64::MAX), (-1.0, 2.0)),
1652        ] {
1653            let result =
1654                Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1655            assert_eq!(
1656                result,
1657                Err(LaError::IntervalRangeExhausted {
1658                    operation: ArithmeticOperation::IntervalMultiplication,
1659                }),
1660                "left={left:?}, right={right:?}"
1661            );
1662        }
1663        Ok(())
1664    }
1665
1666    macro_rules! gen_interval_identity_tests {
1667        ($d:literal) => {
1668            paste! {
1669                #[test]
1670                fn [<interval_identity_sign_is_positive_ $d d>]() {
1671                    let matrix = IntervalMatrix::<$d>::identity();
1672                    assert_eq!(matrix.det(), Ok(Interval::ONE));
1673                    assert_eq!(
1674                        matrix.det_sign(),
1675                        Ok(IntervalDeterminantSign::Positive)
1676                    );
1677                }
1678            }
1679        };
1680    }
1681
1682    gen_interval_identity_tests!(0);
1683    gen_interval_identity_tests!(1);
1684    gen_interval_identity_tests!(2);
1685    gen_interval_identity_tests!(3);
1686    gen_interval_identity_tests!(4);
1687    gen_interval_identity_tests!(5);
1688    gen_interval_identity_tests!(6);
1689    gen_interval_identity_tests!(7);
1690
1691    #[test]
1692    fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1693        const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1694        const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1695            Ok(interval) => interval,
1696            Err(_) => panic!("finite ordered literal bounds"),
1697        };
1698        const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1699        assert_eq!(EMPTY, Ok(Interval::ONE));
1700        assert_eq!(SINGLE, Ok(WIDE));
1701        for (lower, upper, sign) in [
1702            (
1703                -f64::MAX,
1704                -f64::from_bits(1),
1705                IntervalDeterminantSign::Negative,
1706            ),
1707            (
1708                f64::from_bits(1),
1709                f64::MAX,
1710                IntervalDeterminantSign::Positive,
1711            ),
1712            (0.0, 0.0, IntervalDeterminantSign::Zero),
1713            (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1714        ] {
1715            let interval = Interval::try_new(lower, upper)?;
1716            let matrix = IntervalMatrix::from_rows([[interval]]);
1717            assert_eq!(matrix.det()?, interval);
1718            assert_eq!(matrix.det_sign()?, sign);
1719        }
1720        Ok(())
1721    }
1722
1723    #[test]
1724    fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1725        let swapped = IntervalMatrix::<3>::try_from_point_rows([
1726            [0.0, 1.0, 0.0],
1727            [1.0, 0.0, 0.0],
1728            [0.0, 0.0, 1.0],
1729        ])?;
1730        assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1731
1732        let singular = IntervalMatrix::<3>::try_from_point_rows([
1733            [1.0, 2.0, 3.0],
1734            [1.0, 2.0, 3.0],
1735            [0.0, 0.0, 1.0],
1736        ])?;
1737        assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1738        Ok(())
1739    }
1740
1741    #[test]
1742    fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1743        let matrix = IntervalMatrix::<2>::from_rows([
1744            [Interval::ONE, Interval::ZERO],
1745            [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1746        ]);
1747        assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1748        Ok(())
1749    }
1750
1751    #[test]
1752    fn determinant_rejects_dimensions_above_supported_stack_dp() {
1753        assert_matches!(
1754            IntervalMatrix::<8>::identity().det(),
1755            Err(LaError::UnsupportedDimension {
1756                requested: 8,
1757                max: MAX_INTERVAL_MATRIX_DIM,
1758                ..
1759            })
1760        );
1761    }
1762
1763    #[test]
1764    fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1765        let source = Matrix::<2>::identity();
1766        let mut intervals = IntervalMatrix::from_matrix(&source);
1767        let value = Interval::try_new(2.0, 3.0)?;
1768        intervals.set(0, 1, value)?;
1769        assert_eq!(intervals.get(0, 1), Some(value));
1770        assert_eq!(intervals.get(2, 0), None);
1771        assert_eq!(intervals.try_get(0, 1)?, value);
1772        assert_matches!(
1773            intervals.try_get(2, 0),
1774            Err(LaError::IndexOutOfBounds {
1775                row: 2,
1776                col: 0,
1777                dim: 2,
1778                ..
1779            })
1780        );
1781        assert_eq!(intervals.as_rows()[0][1], value);
1782        assert_eq!(intervals.into_rows()[0][1], value);
1783        Ok(())
1784    }
1785
1786    #[test]
1787    fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1788        let mut matrix = IntervalMatrix::<2>::identity();
1789        let before = matrix;
1790        let value = Interval::try_new(2.0, 3.0)?;
1791
1792        assert_eq!(
1793            matrix.set(2, 0, value),
1794            Err(LaError::IndexOutOfBounds {
1795                row: 2,
1796                col: 0,
1797                dim: 2,
1798            })
1799        );
1800        assert_eq!(matrix, before);
1801        assert_eq!(
1802            matrix.set(0, 2, value),
1803            Err(LaError::IndexOutOfBounds {
1804                row: 0,
1805                col: 2,
1806                dim: 2,
1807            })
1808        );
1809        assert_eq!(matrix, before);
1810        Ok(())
1811    }
1812}