Skip to main content

la_stack/
interval.rs

1#![forbid(unsafe_code)]
2
3//! Outward-rounded intervals and fixed-size interval determinant signs.
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, 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 is intentionally absent.
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///
79/// # Examples
80/// ```
81/// use la_stack::prelude::*;
82///
83/// # fn main() -> Result<(), LaError> {
84/// let matrix = IntervalMatrix::<3>::try_from_point_rows([
85///     [0.0, 1.0, 0.0],
86///     [1.0, 0.0, 0.0],
87///     [0.0, 0.0, 1.0],
88/// ])?;
89/// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Negative);
90/// # Ok(())
91/// # }
92/// ```
93#[must_use]
94#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct IntervalMatrix<const D: usize> {
96    rows: [[Interval; D]; D],
97}
98
99/// Canonicalize either signed representation of real zero to `+0.0`.
100#[inline]
101const fn canonical_zero(value: f64) -> f64 {
102    if value == 0.0 { 0.0 } else { value }
103}
104
105/// Turn a finite rounded sum into the tight adjacent-float enclosure implied by
106/// its exact `FastTwoSum` residual.
107#[inline]
108const fn rounded_add_bounds(
109    left: f64,
110    right: f64,
111    operation: ArithmeticOperation,
112) -> Result<(f64, f64), LaError> {
113    let rounded = left + right;
114    if !rounded.is_finite() {
115        return Err(LaError::interval_range_exhausted(operation));
116    }
117
118    let error = two_sum_error(left, right, rounded);
119    // The ordered FastTwoSum residual is finite for finite operands and sum.
120    let (lower, upper) = if error < 0.0 {
121        (rounded.next_down(), rounded)
122    } else if error > 0.0 {
123        (rounded, rounded.next_up())
124    } else {
125        (rounded, rounded)
126    };
127    if !lower.is_finite() || !upper.is_finite() {
128        return Err(LaError::interval_range_exhausted(operation));
129    }
130
131    Ok((canonical_zero(lower), canonical_zero(upper)))
132}
133
134/// Round only the endpoint used by a non-singleton interval sum.
135#[inline]
136const fn rounded_add_endpoint<const UPPER: bool>(
137    left: f64,
138    right: f64,
139    operation: ArithmeticOperation,
140) -> Result<f64, LaError> {
141    let rounded = left + right;
142    if !rounded.is_finite() {
143        return Err(LaError::interval_range_exhausted(operation));
144    }
145    let error = two_sum_error(left, right, rounded);
146    let endpoint = if UPPER && error > 0.0 {
147        rounded.next_up()
148    } else if !UPPER && error < 0.0 {
149        rounded.next_down()
150    } else {
151        rounded
152    };
153    if !endpoint.is_finite() {
154        return Err(LaError::interval_range_exhausted(operation));
155    }
156    Ok(canonical_zero(endpoint))
157}
158
159/// Turn a finite rounded product into the tight adjacent-float enclosure of the
160/// exact binary64-input product.
161// This kernel is called for each selected endpoint inside the determinant DP.
162#[expect(
163    clippy::inline_always,
164    reason = "measured endpoint-call overhead in the interval determinant kernel"
165)]
166#[inline(always)]
167const fn rounded_product_bounds(
168    left: f64,
169    right: f64,
170    operation: ArithmeticOperation,
171) -> Result<(f64, f64), LaError> {
172    if left == 0.0 || right == 0.0 {
173        return Ok((0.0, 0.0));
174    }
175
176    let rounded = left * right;
177    if !rounded.is_finite() {
178        return Err(LaError::interval_range_exhausted(operation));
179    }
180
181    let relation = compare_product_with_rounded(left, right, rounded);
182    let (lower, upper) = if relation < 0 {
183        (rounded.next_down(), rounded)
184    } else if relation > 0 {
185        (rounded, rounded.next_up())
186    } else {
187        (rounded, rounded)
188    };
189    if !lower.is_finite() || !upper.is_finite() {
190        return Err(LaError::interval_range_exhausted(operation));
191    }
192
193    Ok((canonical_zero(lower), canonical_zero(upper)))
194}
195
196impl Interval {
197    /// Exact real zero.
198    pub const ZERO: Self = Self {
199        lower: 0.0,
200        upper: 0.0,
201    };
202
203    /// Exact real one.
204    pub const ONE: Self = Self {
205        lower: 1.0,
206        upper: 1.0,
207    };
208
209    /// Construct a closed interval from finite ordered bounds.
210    ///
211    /// Signed zero endpoints are canonicalized to `+0.0`.
212    ///
213    /// # Examples
214    /// ```
215    /// use core::assert_matches;
216    /// use la_stack::prelude::*;
217    ///
218    /// # fn main() -> Result<(), LaError> {
219    /// let range = Interval::try_new(-2.0, 3.0)?;
220    /// assert!(range.contains(1.0));
221    /// assert!(!range.contains(4.0));
222    /// assert_matches!(
223    ///     Interval::try_new(3.0, -2.0),
224    ///     Err(LaError::InvertedInterval { lower: 3.0, upper: -2.0, .. })
225    /// );
226    /// # Ok(())
227    /// # }
228    /// ```
229    ///
230    /// # Errors
231    /// Returns [`LaError::NonFinite`] when either endpoint is NaN or infinity.
232    /// Returns [`LaError::InvertedInterval`] when `lower > upper`.
233    #[inline]
234    pub const fn try_new(lower: f64, upper: f64) -> Result<Self, LaError> {
235        if !lower.is_finite() {
236            return Err(LaError::non_finite_input_interval_bound(
237                IntervalBound::Lower,
238            ));
239        }
240        if !upper.is_finite() {
241            return Err(LaError::non_finite_input_interval_bound(
242                IntervalBound::Upper,
243            ));
244        }
245        if lower > upper {
246            return Err(LaError::inverted_interval(lower, upper));
247        }
248        Ok(Self::new_unchecked(lower, upper))
249    }
250
251    /// Construct a point interval from a finite binary64 value.
252    ///
253    /// This preserves the supplied value, including any earlier rounding.
254    /// Use [`try_from_subtraction`](Self::try_from_subtraction) to enclose a
255    /// subtraction before its rounding uncertainty is lost.
256    ///
257    /// # Examples
258    /// ```
259    /// use la_stack::prelude::*;
260    ///
261    /// # fn main() -> Result<(), LaError> {
262    /// let half = Interval::point(0.5)?;
263    /// assert_eq!((half.lower(), half.upper()), (0.5, 0.5));
264    /// assert_eq!(half.try_add(&half)?, Interval::ONE);
265    /// # Ok(())
266    /// # }
267    /// ```
268    ///
269    /// # Errors
270    /// Returns [`LaError::NonFinite`] when `value` is NaN or infinity.
271    #[inline]
272    pub const fn point(value: f64) -> Result<Self, LaError> {
273        match Self::try_new(value, value) {
274            Ok(interval) => Ok(interval),
275            Err(LaError::NonFinite { .. }) => Err(LaError::non_finite_input_scalar()),
276            Err(error) => Err(error),
277        }
278    }
279
280    /// Enclose the exact-real subtraction of two finite binary64 inputs.
281    ///
282    /// Unlike subtracting first and then calling [`point`](Self::point), this
283    /// method preserves the rounding uncertainty introduced by the subtraction.
284    ///
285    /// # Examples
286    /// ```
287    /// use la_stack::prelude::*;
288    ///
289    /// # fn main() -> Result<(), LaError> {
290    /// // The exact difference 1 - 2^-54 lies between adjacent binary64 values.
291    /// let difference = Interval::try_from_subtraction(1.0, f64::EPSILON / 4.0)?;
292    /// assert_eq!(difference.lower(), 1.0_f64.next_down());
293    /// assert_eq!(difference.upper(), 1.0);
294    ///
295    /// // Subtracting first loses that uncertainty and produces a point at 1.
296    /// let rounded = Interval::point(1.0 - f64::EPSILON / 4.0)?;
297    /// assert_eq!(rounded, Interval::ONE);
298    /// # Ok(())
299    /// # }
300    /// ```
301    ///
302    /// # Errors
303    /// Returns [`LaError::NonFinite`] for a non-finite input, preserving whether
304    /// it was the left or right operand. Returns
305    /// [`LaError::IntervalRangeExhausted`] when the exact difference has no
306    /// finite binary64 enclosure.
307    #[inline]
308    pub const fn try_from_subtraction(left: f64, right: f64) -> Result<Self, LaError> {
309        if !left.is_finite() {
310            return Err(LaError::non_finite_input_interval_operand(
311                IntervalOperand::Left,
312            ));
313        }
314        if !right.is_finite() {
315            return Err(LaError::non_finite_input_interval_operand(
316                IntervalOperand::Right,
317            ));
318        }
319        if right == 0.0 {
320            return Ok(Self::new_unchecked(left, left));
321        }
322        if left == 0.0 {
323            return Ok(Self::new_unchecked(-right, -right));
324        }
325        match rounded_add_bounds(left, -right, ArithmeticOperation::IntervalSubtraction) {
326            Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
327            Err(error) => Err(error),
328        }
329    }
330
331    /// Return the finite lower bound.
332    #[inline]
333    #[must_use]
334    pub const fn lower(self) -> f64 {
335        self.lower
336    }
337
338    /// Return the finite upper bound.
339    #[inline]
340    #[must_use]
341    pub const fn upper(self) -> f64 {
342        self.upper
343    }
344
345    /// Return whether this interval contains the finite `value`.
346    #[inline]
347    #[must_use]
348    pub const fn contains(self, value: f64) -> bool {
349        value.is_finite() && self.lower <= value && value <= self.upper
350    }
351
352    /// Add two intervals with outward rounding.
353    ///
354    /// # Examples
355    /// ```
356    /// use la_stack::prelude::*;
357    ///
358    /// # fn main() -> Result<(), LaError> {
359    /// let left = Interval::try_new(1.0, 2.0)?;
360    /// let right = Interval::try_new(0.5, 1.0)?;
361    /// assert_eq!(left.try_add(&right)?, Interval::try_new(1.5, 3.0)?);
362    /// # Ok(())
363    /// # }
364    /// ```
365    ///
366    /// # Errors
367    /// Returns [`LaError::IntervalRangeExhausted`] when the exact result range
368    /// has no finite binary64 enclosure.
369    #[inline]
370    pub const fn try_add(&self, other: &Self) -> Result<Self, LaError> {
371        self.try_add_for(other, ArithmeticOperation::IntervalAddition)
372    }
373
374    /// Multiply two intervals with outward rounding.
375    ///
376    /// For a square of the same represented value, prefer
377    /// [`try_square`](Self::try_square), which can give a tighter enclosure.
378    ///
379    /// # Examples
380    /// ```
381    /// use la_stack::prelude::*;
382    ///
383    /// # fn main() -> Result<(), LaError> {
384    /// let left = Interval::try_new(-2.0, 3.0)?;
385    /// let right = Interval::try_new(-4.0, -1.0)?;
386    /// assert_eq!(left.try_mul(&right)?, Interval::try_new(-12.0, 8.0)?);
387    /// # Ok(())
388    /// # }
389    /// ```
390    ///
391    /// # Errors
392    /// Returns [`LaError::IntervalRangeExhausted`] when the exact result range
393    /// has no finite binary64 enclosure.
394    #[inline]
395    pub const fn try_mul(&self, other: &Self) -> Result<Self, LaError> {
396        self.try_mul_for(other, ArithmeticOperation::IntervalMultiplication)
397    }
398
399    /// Negate an interval exactly by swapping and negating its endpoints.
400    ///
401    /// # Examples
402    /// ```
403    /// use la_stack::prelude::*;
404    ///
405    /// # fn main() -> Result<(), LaError> {
406    /// let range = Interval::try_new(-2.0, 3.0)?;
407    /// assert_eq!(range.negate(), Interval::try_new(-3.0, 2.0)?);
408    /// # Ok(())
409    /// # }
410    /// ```
411    #[inline]
412    pub const fn negate(&self) -> Self {
413        Self::new_unchecked(-self.upper, -self.lower)
414    }
415
416    /// Square an interval with outward rounding.
417    ///
418    /// An interval spanning zero has exact lower bound zero. The upper bound is
419    /// the outward-rounded square of the endpoint with greatest magnitude.
420    ///
421    /// # Examples
422    /// ```
423    /// use la_stack::prelude::*;
424    ///
425    /// # fn main() -> Result<(), LaError> {
426    /// let range = Interval::try_new(-2.0, 3.0)?;
427    /// assert_eq!(range.try_square()?, Interval::try_new(0.0, 9.0)?);
428    /// // Multiplication treats its two operands independently and is wider.
429    /// assert_eq!(range.try_mul(&range)?, Interval::try_new(-6.0, 9.0)?);
430    /// # Ok(())
431    /// # }
432    /// ```
433    ///
434    /// # Errors
435    /// Returns [`LaError::IntervalRangeExhausted`] when the exact square range
436    /// has no finite binary64 enclosure.
437    #[expect(
438        clippy::inline_always,
439        reason = "measured call overhead in lifted interval assembly"
440    )]
441    #[inline(always)]
442    pub const fn try_square(&self) -> Result<Self, LaError> {
443        let operation = ArithmeticOperation::IntervalSquare;
444        let left_square = match rounded_product_bounds(self.lower, self.lower, operation) {
445            Ok(bounds) => bounds,
446            Err(error) => return Err(error),
447        };
448        if self.is_point() {
449            return Ok(Self::new_unchecked(left_square.0, left_square.1));
450        }
451        let right_square = match rounded_product_bounds(self.upper, self.upper, operation) {
452            Ok(bounds) => bounds,
453            Err(error) => return Err(error),
454        };
455        let lower = if self.lower <= 0.0 && self.upper >= 0.0 {
456            0.0
457        } else if left_square.0 < right_square.0 {
458            left_square.0
459        } else {
460            right_square.0
461        };
462        let upper = if left_square.1 > right_square.1 {
463            left_square.1
464        } else {
465            right_square.1
466        };
467        Ok(Self::new_unchecked(lower, upper))
468    }
469
470    /// Construct an interval after its finite ordered-bound invariant is known.
471    #[inline]
472    const fn new_unchecked(lower: f64, upper: f64) -> Self {
473        Self {
474            lower: canonical_zero(lower),
475            upper: canonical_zero(upper),
476        }
477    }
478
479    /// Add while attributing range failure to the owning public operation.
480    #[expect(
481        clippy::inline_always,
482        reason = "measured result-copy overhead in the interval determinant kernel"
483    )]
484    #[inline(always)]
485    const fn try_add_for(
486        &self,
487        other: &Self,
488        operation: ArithmeticOperation,
489    ) -> Result<Self, LaError> {
490        if self.is_zero() {
491            return Ok(*other);
492        }
493        if other.is_zero() {
494            return Ok(*self);
495        }
496        if self.is_point() && other.is_point() {
497            return match rounded_add_bounds(self.lower, other.lower, operation) {
498                Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
499                Err(error) => Err(error),
500            };
501        }
502
503        let lower = match rounded_add_endpoint::<false>(self.lower, other.lower, operation) {
504            Ok(lower) => lower,
505            Err(error) => return Err(error),
506        };
507        let upper = match rounded_add_endpoint::<true>(self.upper, other.upper, operation) {
508            Ok(upper) => upper,
509            Err(error) => return Err(error),
510        };
511        Ok(Self::new_unchecked(lower, upper))
512    }
513
514    /// Multiply while attributing range failure to the owning public operation.
515    #[expect(
516        clippy::inline_always,
517        reason = "measured result-copy overhead in the interval determinant kernel"
518    )]
519    #[inline(always)]
520    const fn try_mul_for(
521        &self,
522        other: &Self,
523        operation: ArithmeticOperation,
524    ) -> Result<Self, LaError> {
525        if self.is_zero() || other.is_zero() {
526            return Ok(Self::ZERO);
527        }
528        if self.is_one() {
529            return Ok(*other);
530        }
531        if other.is_one() {
532            return Ok(*self);
533        }
534        if self.is_point() && other.is_point() {
535            return match rounded_product_bounds(self.lower, other.lower, operation) {
536                Ok((lower, upper)) => Ok(Self::new_unchecked(lower, upper)),
537                Err(error) => Err(error),
538            };
539        }
540
541        self.try_mul_by_sign(other, operation)
542    }
543
544    /// Select only the endpoint products that can attain each range extremum.
545    #[inline]
546    const fn try_mul_by_sign(
547        &self,
548        other: &Self,
549        operation: ArithmeticOperation,
550    ) -> Result<Self, LaError> {
551        let self_nonnegative = self.lower >= 0.0;
552        let self_nonpositive = self.upper <= 0.0;
553        let other_nonnegative = other.lower >= 0.0;
554        let other_nonpositive = other.upper <= 0.0;
555
556        if self_nonnegative {
557            if other_nonnegative {
558                return Self::try_product_extrema(
559                    (self.lower, other.lower),
560                    (self.upper, other.upper),
561                    operation,
562                );
563            }
564            if other_nonpositive {
565                return Self::try_product_extrema(
566                    (self.upper, other.lower),
567                    (self.lower, other.upper),
568                    operation,
569                );
570            }
571            return Self::try_product_extrema(
572                (self.upper, other.lower),
573                (self.upper, other.upper),
574                operation,
575            );
576        }
577        if self_nonpositive {
578            if other_nonnegative {
579                return Self::try_product_extrema(
580                    (self.lower, other.upper),
581                    (self.upper, other.lower),
582                    operation,
583                );
584            }
585            if other_nonpositive {
586                return Self::try_product_extrema(
587                    (self.upper, other.upper),
588                    (self.lower, other.lower),
589                    operation,
590                );
591            }
592            return Self::try_product_extrema(
593                (self.lower, other.upper),
594                (self.lower, other.lower),
595                operation,
596            );
597        }
598        if other_nonnegative {
599            return Self::try_product_extrema(
600                (self.lower, other.upper),
601                (self.upper, other.upper),
602                operation,
603            );
604        }
605        if other_nonpositive {
606            return Self::try_product_extrema(
607                (self.upper, other.lower),
608                (self.lower, other.lower),
609                operation,
610            );
611        }
612
613        let lower_left = match rounded_product_bounds(self.lower, other.upper, operation) {
614            Ok(bounds) => bounds,
615            Err(error) => return Err(error),
616        };
617        let lower_right = match rounded_product_bounds(self.upper, other.lower, operation) {
618            Ok(bounds) => bounds,
619            Err(error) => return Err(error),
620        };
621        let upper_left = match rounded_product_bounds(self.lower, other.lower, operation) {
622            Ok(bounds) => bounds,
623            Err(error) => return Err(error),
624        };
625        let upper_right = match rounded_product_bounds(self.upper, other.upper, operation) {
626            Ok(bounds) => bounds,
627            Err(error) => return Err(error),
628        };
629        let lower = if lower_left.0 < lower_right.0 {
630            lower_left.0
631        } else {
632            lower_right.0
633        };
634        let upper = if upper_left.1 > upper_right.1 {
635            upper_left.1
636        } else {
637            upper_right.1
638        };
639        Ok(Self::new_unchecked(lower, upper))
640    }
641
642    /// Enclose the selected exact lower and upper product extrema.
643    #[inline]
644    const fn try_product_extrema(
645        lower_factors: (f64, f64),
646        upper_factors: (f64, f64),
647        operation: ArithmeticOperation,
648    ) -> Result<Self, LaError> {
649        let lower = match rounded_product_bounds(lower_factors.0, lower_factors.1, operation) {
650            Ok((lower, _)) => lower,
651            Err(error) => return Err(error),
652        };
653        let upper = match rounded_product_bounds(upper_factors.0, upper_factors.1, operation) {
654            Ok((_, upper)) => upper,
655            Err(error) => return Err(error),
656        };
657        Ok(Self::new_unchecked(lower, upper))
658    }
659
660    /// Return whether this interval is exactly real zero.
661    #[inline]
662    const fn is_zero(&self) -> bool {
663        self.lower == 0.0 && self.upper == 0.0
664    }
665
666    /// Return whether this interval is exactly real one.
667    #[inline]
668    const fn is_one(&self) -> bool {
669        self.lower.to_bits() == 1.0_f64.to_bits() && self.upper.to_bits() == 1.0_f64.to_bits()
670    }
671
672    /// Return whether this interval contains one binary64 point.
673    #[inline]
674    const fn is_point(&self) -> bool {
675        self.lower.to_bits() == self.upper.to_bits()
676    }
677}
678
679impl Default for Interval {
680    #[inline]
681    fn default() -> Self {
682        Self::ZERO
683    }
684}
685
686impl<const D: usize> IntervalMatrix<D> {
687    /// Construct an interval matrix from already-validated interval rows.
688    ///
689    /// See [`det`](Self::det) for an example with non-point entries.
690    #[inline]
691    pub const fn from_rows(rows: [[Interval; D]; D]) -> Self {
692        Self { rows }
693    }
694
695    /// Lift finite binary64 rows into point intervals.
696    ///
697    /// This preserves the stored binary64 values exactly; it does not recover
698    /// uncertainty from arithmetic performed before this call.
699    /// See [`IntervalMatrix`] for a determinant-sign example using this constructor.
700    ///
701    /// # Errors
702    /// Returns [`LaError::NonFinite`] with matrix coordinates for the first NaN
703    /// or infinity in row-major order.
704    #[inline]
705    pub const fn try_from_point_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
706        let mut intervals = [[Interval::ZERO; D]; D];
707        let mut row = 0;
708        while row < D {
709            let mut column = 0;
710            while column < D {
711                let value = rows[row][column];
712                if !value.is_finite() {
713                    return Err(LaError::non_finite_input_matrix(row, column));
714                }
715                intervals[row][column] = Interval::new_unchecked(value, value);
716                column += 1;
717            }
718            row += 1;
719        }
720        Ok(Self::from_rows(intervals))
721    }
722
723    /// Lift a finite [`Matrix`] into point intervals.
724    ///
725    /// Earlier rounded expression construction is not enclosed; use interval
726    /// operations while constructing derived coefficients when that uncertainty
727    /// belongs in the proof.
728    ///
729    /// # Examples
730    /// ```
731    /// use la_stack::prelude::*;
732    ///
733    /// # fn main() -> Result<(), LaError> {
734    /// let matrix = Matrix::<2>::try_from_rows([[2.0, 0.0], [0.0, 3.0]])?;
735    /// let intervals = IntervalMatrix::from_matrix(&matrix);
736    /// assert_eq!(intervals.det()?, Interval::point(6.0)?);
737    /// # Ok(())
738    /// # }
739    /// ```
740    #[inline]
741    pub const fn from_matrix(matrix: &Matrix<D>) -> Self {
742        let matrix_rows = matrix.as_rows();
743        let mut intervals = [[Interval::ZERO; D]; D];
744        let mut row = 0;
745        while row < D {
746            let mut column = 0;
747            while column < D {
748                let value = matrix_rows[row][column];
749                intervals[row][column] = Interval::new_unchecked(value, value);
750                column += 1;
751            }
752            row += 1;
753        }
754        Self::from_rows(intervals)
755    }
756
757    /// All-zero interval matrix.
758    #[inline]
759    pub const fn zero() -> Self {
760        Self::from_rows([[Interval::ZERO; D]; D])
761    }
762
763    /// Identity interval matrix.
764    #[inline]
765    pub const fn identity() -> Self {
766        let mut matrix = Self::zero();
767        let mut index = 0;
768        while index < D {
769            matrix.rows[index][index] = Interval::ONE;
770            index += 1;
771        }
772        matrix
773    }
774
775    /// Borrow the row-major interval storage.
776    #[inline]
777    #[must_use]
778    pub const fn as_rows(&self) -> &[[Interval; D]; D] {
779        &self.rows
780    }
781
782    /// Consume this matrix and return its row-major interval storage.
783    #[inline]
784    #[must_use]
785    pub const fn into_rows(self) -> [[Interval; D]; D] {
786        self.rows
787    }
788
789    /// Get an interval entry with bounds checking.
790    #[inline]
791    #[must_use]
792    pub const fn get(&self, row: usize, column: usize) -> Option<Interval> {
793        if row < D && column < D {
794            Some(self.rows[row][column])
795        } else {
796            None
797        }
798    }
799
800    /// Get an interval entry while preserving index context on failure.
801    ///
802    /// See [`set`](Self::set) for an example of mutation and checked access.
803    ///
804    /// # Errors
805    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
806    #[inline]
807    pub const fn try_get(&self, row: usize, column: usize) -> Result<Interval, LaError> {
808        if row < D && column < D {
809            Ok(self.rows[row][column])
810        } else {
811            Err(LaError::index_out_of_bounds(row, column, D))
812        }
813    }
814
815    /// Set an interval entry with bounds checking.
816    ///
817    /// Validation is unnecessary for the value because [`Interval`] already
818    /// carries the finite ordered-bound proof. An invalid index leaves the
819    /// matrix unchanged.
820    ///
821    /// # Examples
822    /// ```
823    /// use core::assert_matches;
824    /// use la_stack::prelude::*;
825    ///
826    /// # fn main() -> Result<(), LaError> {
827    /// let mut matrix = IntervalMatrix::<2>::identity();
828    /// let range = Interval::try_new(2.0, 3.0)?;
829    /// matrix.set(0, 0, range)?;
830    /// assert_eq!(matrix.try_get(0, 0)?, range);
831    /// let before = matrix;
832    /// assert_matches!(
833    ///     matrix.set(2, 0, Interval::ZERO),
834    ///     Err(LaError::IndexOutOfBounds { row: 2, col: 0, dim: 2, .. })
835    /// );
836    /// assert_eq!(matrix, before);
837    /// # Ok(())
838    /// # }
839    /// ```
840    ///
841    /// # Errors
842    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
843    #[inline]
844    pub const fn set(&mut self, row: usize, column: usize, value: Interval) -> Result<(), LaError> {
845        if row >= D || column >= D {
846            return Err(LaError::index_out_of_bounds(row, column, D));
847        }
848        self.rows[row][column] = value;
849        Ok(())
850    }
851
852    /// Enclose the determinant with division-free subset dynamic programming.
853    ///
854    /// For each column subset, the DP stores the determinant interval of the
855    /// leading rows and those columns. This evaluates the Leibniz expansion in
856    /// `D × 2^(D-1)` products and additions without choosing or dividing by a
857    /// pivot. The returned interval therefore encloses every exact-real
858    /// determinant represented by the input intervals, subject only to an
859    /// explicit range failure.
860    ///
861    /// The D=0 determinant follows the empty-product convention and is `[1, 1]`.
862    ///
863    /// Use [`det_sign`](Self::det_sign) when only sign evidence is needed.
864    ///
865    /// # Examples
866    /// ```
867    /// use la_stack::prelude::*;
868    ///
869    /// # fn main() -> Result<(), LaError> {
870    /// // Every represented diagonal matrix has a determinant in [8, 15].
871    /// let matrix = IntervalMatrix::<2>::from_rows([
872    ///     [Interval::try_new(2.0, 3.0)?, Interval::ZERO],
873    ///     [Interval::ZERO, Interval::try_new(4.0, 5.0)?],
874    /// ]);
875    /// assert_eq!(matrix.det()?, Interval::try_new(8.0, 15.0)?);
876    /// # Ok(())
877    /// # }
878    /// ```
879    ///
880    /// # Errors
881    /// Returns [`LaError::UnsupportedDimension`] for D>7. Returns
882    /// [`LaError::IntervalRangeExhausted`] with interval-determinant provenance
883    /// when an exact intermediate has no finite binary64 enclosure; callers can
884    /// then proceed to an exact or higher-range fallback.
885    #[inline]
886    pub const fn det(&self) -> Result<Interval, LaError> {
887        // Stable Rust cannot express [Interval; 1 << D]. Dispatch only the
888        // workspace extent; every arm uses the identical arithmetic tree.
889        match D {
890            0 | 1 => self.det_with_workspace::<2>(),
891            2 => self.det_with_workspace::<4>(),
892            3 => self.det_with_workspace::<8>(),
893            4 => self.det_with_workspace::<16>(),
894            5 => self.det_with_workspace::<32>(),
895            6 => self.det_with_workspace::<64>(),
896            7 => self.det_with_workspace::<128>(),
897            _ => Err(LaError::unsupported_dimension(D, MAX_INTERVAL_MATRIX_DIM)),
898        }
899    }
900
901    /// Evaluate with the state count established by the dimension dispatch.
902    ///
903    /// Only `det` calls this helper, after proving D is supported and STATES
904    /// covers all `2^D` subsets. Its D=0 and D=1 arms reserve two slots. These
905    /// preconditions keep shifts and indexing in range while preserving the
906    /// same expansion order for every workspace size.
907    #[inline]
908    const fn det_with_workspace<const STATES: usize>(&self) -> Result<Interval, LaError> {
909        let state_count = 1_usize << D;
910        let mut partials = [Interval::ZERO; STATES];
911        partials[0] = Interval::ONE;
912        let operation = ArithmeticOperation::IntervalDeterminant;
913
914        let mut subset = 1;
915        while subset < state_count {
916            let row = subset.count_ones() as usize - 1;
917            let mut sum = Interval::ZERO;
918            // Removing the lowest set bit visits active columns in the same
919            // ascending order as the full scan, preserving every interval
920            // operation and the first range-exhaustion error.
921            let mut remaining = subset;
922            while remaining != 0 {
923                let column = remaining.trailing_zeros() as usize;
924                let column_bit = 1_usize << column;
925                remaining &= remaining - 1;
926                let previous = subset ^ column_bit;
927                let mut term =
928                    match partials[previous].try_mul_for(&self.rows[row][column], operation) {
929                        Ok(term) => term,
930                        Err(error) => return Err(error),
931                    };
932                if !remaining.count_ones().is_multiple_of(2) {
933                    term = term.negate();
934                }
935                sum = match sum.try_add_for(&term, operation) {
936                    Ok(next_sum) => next_sum,
937                    Err(error) => return Err(error),
938                };
939            }
940            partials[subset] = sum;
941            subset += 1;
942        }
943
944        Ok(partials[state_count - 1])
945    }
946
947    /// Return proof-bearing determinant sign evidence.
948    ///
949    /// An interval strictly on one side of zero proves that sign. Only the
950    /// singleton interval `[0, 0]` proves `Zero`; every other overlap with zero
951    /// is [`IntervalDeterminantSign::Inconclusive`].
952    ///
953    /// # Examples
954    /// ```
955    /// use la_stack::prelude::*;
956    ///
957    /// # fn main() -> Result<(), LaError> {
958    /// let mut matrix = IntervalMatrix::<2>::identity();
959    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Positive);
960    ///
961    /// matrix.set(0, 0, Interval::try_new(-1.0, 1.0)?)?;
962    /// // This range includes nonsingular matrices of both signs; a caller
963    /// // needs tighter or exact input before it can decide singularity.
964    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
965    ///
966    /// matrix.set(0, 0, Interval::ZERO)?;
967    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Zero);
968    /// # Ok(())
969    /// # }
970    /// ```
971    ///
972    /// # Errors
973    /// Propagates the dimension and arithmetic range failures from
974    /// [`det`](Self::det).
975    #[inline]
976    pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
977        let determinant = match self.det() {
978            Ok(determinant) => determinant,
979            Err(error) => return Err(error),
980        };
981        if determinant.lower > 0.0 {
982            Ok(IntervalDeterminantSign::Positive)
983        } else if determinant.upper < 0.0 {
984            Ok(IntervalDeterminantSign::Negative)
985        } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
986            Ok(IntervalDeterminantSign::Zero)
987        } else {
988            Ok(IntervalDeterminantSign::Inconclusive)
989        }
990    }
991}
992
993impl<const D: usize> Default for IntervalMatrix<D> {
994    #[inline]
995    fn default() -> Self {
996        Self::zero()
997    }
998}
999
1000#[cfg(test)]
1001mod tests {
1002    use core::assert_matches;
1003
1004    use pastey::paste;
1005
1006    use super::*;
1007    use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1008
1009    #[test]
1010    fn point_and_bounds_enforce_interval_invariants() {
1011        assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1012        assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1013        assert_matches!(
1014            Interval::point(f64::NAN),
1015            Err(LaError::NonFinite {
1016                location: NonFiniteLocation::Scalar,
1017                origin: NonFiniteOrigin::Input,
1018                ..
1019            })
1020        );
1021        assert_matches!(
1022            Interval::try_new(2.0, 1.0),
1023            Err(LaError::InvertedInterval {
1024                lower: 2.0,
1025                upper: 1.0,
1026                ..
1027            })
1028        );
1029    }
1030
1031    #[test]
1032    fn constructors_preserve_non_finite_input_locations() {
1033        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1034            assert_eq!(
1035                Interval::try_new(value, 0.0),
1036                Err(LaError::NonFinite {
1037                    location: NonFiniteLocation::IntervalBound {
1038                        bound: IntervalBound::Lower,
1039                    },
1040                    origin: NonFiniteOrigin::Input,
1041                })
1042            );
1043            assert_eq!(
1044                Interval::try_new(0.0, value),
1045                Err(LaError::NonFinite {
1046                    location: NonFiniteLocation::IntervalBound {
1047                        bound: IntervalBound::Upper,
1048                    },
1049                    origin: NonFiniteOrigin::Input,
1050                })
1051            );
1052            assert_eq!(
1053                Interval::try_from_subtraction(value, 0.0),
1054                Err(LaError::NonFinite {
1055                    location: NonFiniteLocation::IntervalOperand {
1056                        operand: IntervalOperand::Left,
1057                    },
1058                    origin: NonFiniteOrigin::Input,
1059                })
1060            );
1061            assert_eq!(
1062                Interval::try_from_subtraction(0.0, value),
1063                Err(LaError::NonFinite {
1064                    location: NonFiniteLocation::IntervalOperand {
1065                        operand: IntervalOperand::Right,
1066                    },
1067                    origin: NonFiniteOrigin::Input,
1068                })
1069            );
1070        }
1071
1072        assert_eq!(
1073            Interval::try_new(f64::NAN, f64::INFINITY),
1074            Err(LaError::NonFinite {
1075                location: NonFiniteLocation::IntervalBound {
1076                    bound: IntervalBound::Lower,
1077                },
1078                origin: NonFiniteOrigin::Input,
1079            })
1080        );
1081        assert_eq!(
1082            Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1083            Err(LaError::NonFinite {
1084                location: NonFiniteLocation::IntervalOperand {
1085                    operand: IntervalOperand::Left,
1086                },
1087                origin: NonFiniteOrigin::Input,
1088            })
1089        );
1090
1091        let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1092        assert_eq!(
1093            IntervalMatrix::<2>::try_from_point_rows(rows),
1094            Err(LaError::NonFinite {
1095                location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1096                origin: NonFiniteOrigin::Input,
1097            })
1098        );
1099    }
1100
1101    #[test]
1102    fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1103        let one = Interval::point(1.0)?;
1104        let two = Interval::point(2.0)?;
1105        assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1106        assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1107        assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1108        assert_eq!(
1109            Interval::try_new(-2.0, -1.0)?.negate(),
1110            Interval::try_new(1.0, 2.0)?
1111        );
1112        Ok(())
1113    }
1114
1115    #[test]
1116    fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1117        let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1118        let rounded_subtraction = 1.0_f64 - 0.1;
1119        assert_eq!(
1120            subtraction,
1121            Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1122        );
1123
1124        let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1125        let rounded_product = 0.1_f64 * 0.2;
1126        assert_eq!(
1127            product,
1128            Interval::try_new(rounded_product.next_down(), rounded_product)?
1129        );
1130
1131        let below_one = 1.0 - f64::EPSILON;
1132        let above_one = 1.0 + f64::EPSILON;
1133        let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1134        assert_eq!(
1135            binade_boundary,
1136            Interval::try_new(1.0_f64.next_down(), 1.0)?
1137        );
1138        Ok(())
1139    }
1140
1141    #[test]
1142    fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1143        let next = 1.0_f64.next_up();
1144        let difference = Interval::try_from_subtraction(next, 1.0)?;
1145        assert_eq!(difference, Interval::point(f64::EPSILON)?);
1146        Ok(())
1147    }
1148
1149    #[test]
1150    fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1151        for value in [
1152            -f64::MAX,
1153            -1.0,
1154            -f64::MIN_POSITIVE,
1155            -f64::from_bits(1),
1156            -0.0,
1157            0.0,
1158            f64::from_bits(1),
1159            f64::MIN_POSITIVE,
1160            1.0,
1161            f64::MAX,
1162        ] {
1163            for zero in [0.0, -0.0] {
1164                for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1165                    let result = Interval::try_from_subtraction(left, right)?;
1166                    let expected_bits = if expected == 0.0 {
1167                        0
1168                    } else {
1169                        expected.to_bits()
1170                    };
1171                    assert_eq!(result.lower().to_bits(), expected_bits);
1172                    assert_eq!(result.upper().to_bits(), expected_bits);
1173                }
1174            }
1175        }
1176        Ok(())
1177    }
1178
1179    #[test]
1180    fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1181        for (value, lower, upper) in [
1182            (0.0, 0.0_f64, 0.0),
1183            (-0.0, 0.0, 0.0),
1184            (2.0, 4.0, 4.0),
1185            (-2.0, 4.0, 4.0),
1186            (f64::from_bits(1), 0.0, f64::from_bits(1)),
1187            (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1188        ] {
1189            let squared = Interval::point(value)?.try_square()?;
1190            assert_eq!(squared.lower().to_bits(), lower.to_bits());
1191            assert_eq!(squared.upper().to_bits(), upper.to_bits());
1192        }
1193        // Squaring the exact stored binary64 0.1 lies strictly between these
1194        // adjacent values; the expected bits come from its rational square.
1195        for value in [0.1, -0.1] {
1196            let squared = Interval::point(value)?.try_square()?;
1197            assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1198            assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1199        }
1200        Ok(())
1201    }
1202
1203    #[test]
1204    fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1205        let expected = Interval::try_new(4.0, 9.0)?;
1206        for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1207            assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1208        }
1209        Ok(())
1210    }
1211
1212    #[test]
1213    fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1214        let left = Interval::try_new(-1.0, 1.0)?;
1215        // A quarter ULP rounds both sums inward; three quarters rounds them
1216        // outward already. Both exact dyadic sums have the same tight bounds.
1217        for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1218            let right = Interval::try_new(-radius, radius)?;
1219            assert_eq!(
1220                left.try_add(&right)?,
1221                Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1222            );
1223        }
1224        assert_eq!(
1225            left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1226            Interval::try_new(-3.0, 3.0)?
1227        );
1228        Ok(())
1229    }
1230
1231    #[test]
1232    fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1233        let least_subnormal = f64::from_bits(1);
1234        let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1235        assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1236        Ok(())
1237    }
1238
1239    #[test]
1240    fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1241        let error = Interval::point(f64::MAX)?
1242            .try_mul(&Interval::point(2.0)?)
1243            .unwrap_err();
1244        assert_eq!(
1245            error,
1246            LaError::IntervalRangeExhausted {
1247                operation: ArithmeticOperation::IntervalMultiplication,
1248            }
1249        );
1250        Ok(())
1251    }
1252
1253    #[test]
1254    fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1255        let maximum = Interval::point(f64::MAX)?;
1256        let tiny = Interval::point(f64::MIN_POSITIVE)?;
1257        assert_eq!(
1258            maximum.try_add(&maximum),
1259            Err(LaError::IntervalRangeExhausted {
1260                operation: ArithmeticOperation::IntervalAddition,
1261            })
1262        );
1263        assert_eq!(
1264            maximum.try_add(&tiny),
1265            Err(LaError::IntervalRangeExhausted {
1266                operation: ArithmeticOperation::IntervalAddition,
1267            })
1268        );
1269        let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1270        assert_eq!(
1271            nonnegative.try_add(&nonnegative),
1272            Err(LaError::IntervalRangeExhausted {
1273                operation: ArithmeticOperation::IntervalAddition,
1274            })
1275        );
1276        let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1277        for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1278            assert_eq!(
1279                left.try_add(&right),
1280                Err(LaError::IntervalRangeExhausted {
1281                    operation: ArithmeticOperation::IntervalAddition,
1282                }),
1283            );
1284        }
1285
1286        let finite_difference = maximum.try_add(&tiny.negate())?;
1287        assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1288        assert!(finite_difference.lower() < finite_difference.upper());
1289        Ok(())
1290    }
1291
1292    #[test]
1293    fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1294        assert_eq!(
1295            Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1296            Err(LaError::IntervalRangeExhausted {
1297                operation: ArithmeticOperation::IntervalSubtraction,
1298            })
1299        );
1300        assert_eq!(
1301            Interval::point(f64::MAX)?.try_square(),
1302            Err(LaError::IntervalRangeExhausted {
1303                operation: ArithmeticOperation::IntervalSquare,
1304            })
1305        );
1306        assert_eq!(
1307            Interval::try_new(-1.0, f64::MAX)?.try_square(),
1308            Err(LaError::IntervalRangeExhausted {
1309                operation: ArithmeticOperation::IntervalSquare,
1310            })
1311        );
1312        Ok(())
1313    }
1314
1315    #[test]
1316    fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1317        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1318        assert_eq!(
1319            matrix.det(),
1320            Err(LaError::IntervalRangeExhausted {
1321                operation: ArithmeticOperation::IntervalDeterminant,
1322            })
1323        );
1324
1325        let accumulating =
1326            IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1327        assert_eq!(
1328            accumulating.det(),
1329            Err(LaError::IntervalRangeExhausted {
1330                operation: ArithmeticOperation::IntervalDeterminant,
1331            })
1332        );
1333        assert_eq!(
1334            accumulating.det_sign(),
1335            Err(LaError::IntervalRangeExhausted {
1336                operation: ArithmeticOperation::IntervalDeterminant,
1337            })
1338        );
1339        Ok(())
1340    }
1341
1342    #[test]
1343    fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1344    {
1345        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1346        assert_eq!(
1347            matrix.det(),
1348            Err(LaError::IntervalRangeExhausted {
1349                operation: ArithmeticOperation::IntervalDeterminant,
1350            })
1351        );
1352        Ok(())
1353    }
1354
1355    #[test]
1356    fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1357        let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1358        assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1359        Ok(())
1360    }
1361
1362    #[test]
1363    fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1364        for (left, right, expected) in [
1365            ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1366            ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1367            ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1368            ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1369            ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1370            ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1371            ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1372            ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1373            ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1374        ] {
1375            let product = Interval::try_new(left.0, left.1)?
1376                .try_mul(&Interval::try_new(right.0, right.1)?)?;
1377            assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1378        }
1379
1380        assert_eq!(
1381            Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1382            Interval::ZERO
1383        );
1384        Ok(())
1385    }
1386
1387    #[test]
1388    fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1389        let half_maximum = f64::MAX / 2.0;
1390        for (left, right) in [
1391            ((half_maximum, f64::MAX), (-2.0, -1.0)),
1392            ((half_maximum, f64::MAX), (1.0, 2.0)),
1393            ((-f64::MAX, 1.0), (-1.0, 2.0)),
1394            ((-1.0, f64::MAX), (-2.0, 1.0)),
1395            ((-f64::MAX, 1.0), (-2.0, 1.0)),
1396            ((-1.0, f64::MAX), (-1.0, 2.0)),
1397        ] {
1398            let result =
1399                Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1400            assert_eq!(
1401                result,
1402                Err(LaError::IntervalRangeExhausted {
1403                    operation: ArithmeticOperation::IntervalMultiplication,
1404                }),
1405                "left={left:?}, right={right:?}"
1406            );
1407        }
1408        Ok(())
1409    }
1410
1411    macro_rules! gen_interval_identity_tests {
1412        ($d:literal) => {
1413            paste! {
1414                #[test]
1415                fn [<interval_identity_sign_is_positive_ $d d>]() {
1416                    let matrix = IntervalMatrix::<$d>::identity();
1417                    assert_eq!(matrix.det(), Ok(Interval::ONE));
1418                    assert_eq!(
1419                        matrix.det_sign(),
1420                        Ok(IntervalDeterminantSign::Positive)
1421                    );
1422                }
1423            }
1424        };
1425    }
1426
1427    gen_interval_identity_tests!(0);
1428    gen_interval_identity_tests!(1);
1429    gen_interval_identity_tests!(2);
1430    gen_interval_identity_tests!(3);
1431    gen_interval_identity_tests!(4);
1432    gen_interval_identity_tests!(5);
1433    gen_interval_identity_tests!(6);
1434    gen_interval_identity_tests!(7);
1435
1436    #[test]
1437    fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1438        const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1439        const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1440            Ok(interval) => interval,
1441            Err(_) => panic!("finite ordered literal bounds"),
1442        };
1443        const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1444        assert_eq!(EMPTY, Ok(Interval::ONE));
1445        assert_eq!(SINGLE, Ok(WIDE));
1446        for (lower, upper, sign) in [
1447            (
1448                -f64::MAX,
1449                -f64::from_bits(1),
1450                IntervalDeterminantSign::Negative,
1451            ),
1452            (
1453                f64::from_bits(1),
1454                f64::MAX,
1455                IntervalDeterminantSign::Positive,
1456            ),
1457            (0.0, 0.0, IntervalDeterminantSign::Zero),
1458            (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1459        ] {
1460            let interval = Interval::try_new(lower, upper)?;
1461            let matrix = IntervalMatrix::from_rows([[interval]]);
1462            assert_eq!(matrix.det()?, interval);
1463            assert_eq!(matrix.det_sign()?, sign);
1464        }
1465        Ok(())
1466    }
1467
1468    #[test]
1469    fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1470        let swapped = IntervalMatrix::<3>::try_from_point_rows([
1471            [0.0, 1.0, 0.0],
1472            [1.0, 0.0, 0.0],
1473            [0.0, 0.0, 1.0],
1474        ])?;
1475        assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1476
1477        let singular = IntervalMatrix::<3>::try_from_point_rows([
1478            [1.0, 2.0, 3.0],
1479            [1.0, 2.0, 3.0],
1480            [0.0, 0.0, 1.0],
1481        ])?;
1482        assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1483        Ok(())
1484    }
1485
1486    #[test]
1487    fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1488        let matrix = IntervalMatrix::<2>::from_rows([
1489            [Interval::ONE, Interval::ZERO],
1490            [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1491        ]);
1492        assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1493        Ok(())
1494    }
1495
1496    #[test]
1497    fn determinant_rejects_dimensions_above_supported_stack_dp() {
1498        assert_matches!(
1499            IntervalMatrix::<8>::identity().det(),
1500            Err(LaError::UnsupportedDimension {
1501                requested: 8,
1502                max: MAX_INTERVAL_MATRIX_DIM,
1503                ..
1504            })
1505        );
1506    }
1507
1508    #[test]
1509    fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1510        let source = Matrix::<2>::identity();
1511        let mut intervals = IntervalMatrix::from_matrix(&source);
1512        let value = Interval::try_new(2.0, 3.0)?;
1513        intervals.set(0, 1, value)?;
1514        assert_eq!(intervals.get(0, 1), Some(value));
1515        assert_eq!(intervals.get(2, 0), None);
1516        assert_eq!(intervals.try_get(0, 1)?, value);
1517        assert_matches!(
1518            intervals.try_get(2, 0),
1519            Err(LaError::IndexOutOfBounds {
1520                row: 2,
1521                col: 0,
1522                dim: 2,
1523                ..
1524            })
1525        );
1526        assert_eq!(intervals.as_rows()[0][1], value);
1527        assert_eq!(intervals.into_rows()[0][1], value);
1528        Ok(())
1529    }
1530
1531    #[test]
1532    fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1533        let mut matrix = IntervalMatrix::<2>::identity();
1534        let before = matrix;
1535        let value = Interval::try_new(2.0, 3.0)?;
1536
1537        assert_eq!(
1538            matrix.set(2, 0, value),
1539            Err(LaError::IndexOutOfBounds {
1540                row: 2,
1541                col: 0,
1542                dim: 2,
1543            })
1544        );
1545        assert_eq!(matrix, before);
1546        assert_eq!(
1547            matrix.set(0, 2, value),
1548            Err(LaError::IndexOutOfBounds {
1549                row: 0,
1550                col: 2,
1551                dim: 2,
1552            })
1553        );
1554        assert_eq!(matrix, before);
1555        Ok(())
1556    }
1557}