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            let mut column = 0;
919            while column < D {
920                let column_bit = 1_usize << column;
921                if subset & column_bit != 0 {
922                    let previous = subset ^ column_bit;
923                    let mut term =
924                        match partials[previous].try_mul_for(&self.rows[row][column], operation) {
925                            Ok(term) => term,
926                            Err(error) => return Err(error),
927                        };
928                    let columns_after = (subset >> (column + 1)).count_ones();
929                    if !columns_after.is_multiple_of(2) {
930                        term = term.negate();
931                    }
932                    sum = match sum.try_add_for(&term, operation) {
933                        Ok(next_sum) => next_sum,
934                        Err(error) => return Err(error),
935                    };
936                }
937                column += 1;
938            }
939            partials[subset] = sum;
940            subset += 1;
941        }
942
943        Ok(partials[state_count - 1])
944    }
945
946    /// Return proof-bearing determinant sign evidence.
947    ///
948    /// An interval strictly on one side of zero proves that sign. Only the
949    /// singleton interval `[0, 0]` proves `Zero`; every other overlap with zero
950    /// is [`IntervalDeterminantSign::Inconclusive`].
951    ///
952    /// # Examples
953    /// ```
954    /// use la_stack::prelude::*;
955    ///
956    /// # fn main() -> Result<(), LaError> {
957    /// let mut matrix = IntervalMatrix::<2>::identity();
958    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Positive);
959    ///
960    /// matrix.set(0, 0, Interval::try_new(-1.0, 1.0)?)?;
961    /// // This range includes nonsingular matrices of both signs; a caller
962    /// // needs tighter or exact input before it can decide singularity.
963    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
964    ///
965    /// matrix.set(0, 0, Interval::ZERO)?;
966    /// assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Zero);
967    /// # Ok(())
968    /// # }
969    /// ```
970    ///
971    /// # Errors
972    /// Propagates the dimension and arithmetic range failures from
973    /// [`det`](Self::det).
974    #[inline]
975    pub const fn det_sign(&self) -> Result<IntervalDeterminantSign, LaError> {
976        let determinant = match self.det() {
977            Ok(determinant) => determinant,
978            Err(error) => return Err(error),
979        };
980        if determinant.lower > 0.0 {
981            Ok(IntervalDeterminantSign::Positive)
982        } else if determinant.upper < 0.0 {
983            Ok(IntervalDeterminantSign::Negative)
984        } else if determinant.lower == 0.0 && determinant.upper == 0.0 {
985            Ok(IntervalDeterminantSign::Zero)
986        } else {
987            Ok(IntervalDeterminantSign::Inconclusive)
988        }
989    }
990}
991
992impl<const D: usize> Default for IntervalMatrix<D> {
993    #[inline]
994    fn default() -> Self {
995        Self::zero()
996    }
997}
998
999#[cfg(test)]
1000mod tests {
1001    use core::assert_matches;
1002
1003    use pastey::paste;
1004
1005    use super::*;
1006    use crate::{IntervalBound, IntervalOperand, NonFiniteLocation, NonFiniteOrigin};
1007
1008    #[test]
1009    fn point_and_bounds_enforce_interval_invariants() {
1010        assert_eq!(Interval::point(-0.0).unwrap().lower().to_bits(), 0);
1011        assert_eq!(Interval::try_new(-0.0, 0.0).unwrap(), Interval::ZERO);
1012        assert_matches!(
1013            Interval::point(f64::NAN),
1014            Err(LaError::NonFinite {
1015                location: NonFiniteLocation::Scalar,
1016                origin: NonFiniteOrigin::Input,
1017                ..
1018            })
1019        );
1020        assert_matches!(
1021            Interval::try_new(2.0, 1.0),
1022            Err(LaError::InvertedInterval {
1023                lower: 2.0,
1024                upper: 1.0,
1025                ..
1026            })
1027        );
1028    }
1029
1030    #[test]
1031    fn constructors_preserve_non_finite_input_locations() {
1032        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1033            assert_eq!(
1034                Interval::try_new(value, 0.0),
1035                Err(LaError::NonFinite {
1036                    location: NonFiniteLocation::IntervalBound {
1037                        bound: IntervalBound::Lower,
1038                    },
1039                    origin: NonFiniteOrigin::Input,
1040                })
1041            );
1042            assert_eq!(
1043                Interval::try_new(0.0, value),
1044                Err(LaError::NonFinite {
1045                    location: NonFiniteLocation::IntervalBound {
1046                        bound: IntervalBound::Upper,
1047                    },
1048                    origin: NonFiniteOrigin::Input,
1049                })
1050            );
1051            assert_eq!(
1052                Interval::try_from_subtraction(value, 0.0),
1053                Err(LaError::NonFinite {
1054                    location: NonFiniteLocation::IntervalOperand {
1055                        operand: IntervalOperand::Left,
1056                    },
1057                    origin: NonFiniteOrigin::Input,
1058                })
1059            );
1060            assert_eq!(
1061                Interval::try_from_subtraction(0.0, value),
1062                Err(LaError::NonFinite {
1063                    location: NonFiniteLocation::IntervalOperand {
1064                        operand: IntervalOperand::Right,
1065                    },
1066                    origin: NonFiniteOrigin::Input,
1067                })
1068            );
1069        }
1070
1071        assert_eq!(
1072            Interval::try_new(f64::NAN, f64::INFINITY),
1073            Err(LaError::NonFinite {
1074                location: NonFiniteLocation::IntervalBound {
1075                    bound: IntervalBound::Lower,
1076                },
1077                origin: NonFiniteOrigin::Input,
1078            })
1079        );
1080        assert_eq!(
1081            Interval::try_from_subtraction(f64::NAN, f64::INFINITY),
1082            Err(LaError::NonFinite {
1083                location: NonFiniteLocation::IntervalOperand {
1084                    operand: IntervalOperand::Left,
1085                },
1086                origin: NonFiniteOrigin::Input,
1087            })
1088        );
1089
1090        let rows = [[0.0, f64::NAN], [f64::INFINITY, 0.0]];
1091        assert_eq!(
1092            IntervalMatrix::<2>::try_from_point_rows(rows),
1093            Err(LaError::NonFinite {
1094                location: NonFiniteLocation::MatrixCell { row: 0, col: 1 },
1095                origin: NonFiniteOrigin::Input,
1096            })
1097        );
1098    }
1099
1100    #[test]
1101    fn exact_operations_remain_point_intervals() -> Result<(), LaError> {
1102        let one = Interval::point(1.0)?;
1103        let two = Interval::point(2.0)?;
1104        assert_eq!(one.try_add(&two)?, Interval::point(3.0)?);
1105        assert_eq!(two.try_mul(&two)?, Interval::point(4.0)?);
1106        assert_eq!(Interval::try_from_subtraction(3.0, 2.0)?, one);
1107        assert_eq!(
1108            Interval::try_new(-2.0, -1.0)?.negate(),
1109            Interval::try_new(1.0, 2.0)?
1110        );
1111        Ok(())
1112    }
1113
1114    #[test]
1115    fn inexact_operations_expand_only_in_the_required_direction() -> Result<(), LaError> {
1116        let subtraction = Interval::try_from_subtraction(1.0, 0.1)?;
1117        let rounded_subtraction = 1.0_f64 - 0.1;
1118        assert_eq!(
1119            subtraction,
1120            Interval::try_new(rounded_subtraction.next_down(), rounded_subtraction)?
1121        );
1122
1123        let product = Interval::point(0.1)?.try_mul(&Interval::point(0.2)?)?;
1124        let rounded_product = 0.1_f64 * 0.2;
1125        assert_eq!(
1126            product,
1127            Interval::try_new(rounded_product.next_down(), rounded_product)?
1128        );
1129
1130        let below_one = 1.0 - f64::EPSILON;
1131        let above_one = 1.0 + f64::EPSILON;
1132        let binade_boundary = Interval::point(below_one)?.try_mul(&Interval::point(above_one)?)?;
1133        assert_eq!(
1134            binade_boundary,
1135            Interval::try_new(1.0_f64.next_down(), 1.0)?
1136        );
1137        Ok(())
1138    }
1139
1140    #[test]
1141    fn cancellation_preserves_an_exact_ulp_difference() -> Result<(), LaError> {
1142        let next = 1.0_f64.next_up();
1143        let difference = Interval::try_from_subtraction(next, 1.0)?;
1144        assert_eq!(difference, Interval::point(f64::EPSILON)?);
1145        Ok(())
1146    }
1147
1148    #[test]
1149    fn subtraction_by_or_from_zero_preserves_exact_endpoint_bits() -> Result<(), LaError> {
1150        for value in [
1151            -f64::MAX,
1152            -1.0,
1153            -f64::MIN_POSITIVE,
1154            -f64::from_bits(1),
1155            -0.0,
1156            0.0,
1157            f64::from_bits(1),
1158            f64::MIN_POSITIVE,
1159            1.0,
1160            f64::MAX,
1161        ] {
1162            for zero in [0.0, -0.0] {
1163                for (left, right, expected) in [(value, zero, value), (zero, value, -value)] {
1164                    let result = Interval::try_from_subtraction(left, right)?;
1165                    let expected_bits = if expected == 0.0 {
1166                        0
1167                    } else {
1168                        expected.to_bits()
1169                    };
1170                    assert_eq!(result.lower().to_bits(), expected_bits);
1171                    assert_eq!(result.upper().to_bits(), expected_bits);
1172                }
1173            }
1174        }
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn singleton_squares_keep_tight_bounds_including_underflow() -> Result<(), LaError> {
1180        for (value, lower, upper) in [
1181            (0.0, 0.0_f64, 0.0),
1182            (-0.0, 0.0, 0.0),
1183            (2.0, 4.0, 4.0),
1184            (-2.0, 4.0, 4.0),
1185            (f64::from_bits(1), 0.0, f64::from_bits(1)),
1186            (-f64::from_bits(1), 0.0, f64::from_bits(1)),
1187        ] {
1188            let squared = Interval::point(value)?.try_square()?;
1189            assert_eq!(squared.lower().to_bits(), lower.to_bits());
1190            assert_eq!(squared.upper().to_bits(), upper.to_bits());
1191        }
1192        // Squaring the exact stored binary64 0.1 lies strictly between these
1193        // adjacent values; the expected bits come from its rational square.
1194        for value in [0.1, -0.1] {
1195            let squared = Interval::point(value)?.try_square()?;
1196            assert_eq!(squared.lower().to_bits(), 0x3f84_7ae1_47ae_147b);
1197            assert_eq!(squared.upper().to_bits(), 0x3f84_7ae1_47ae_147c);
1198        }
1199        Ok(())
1200    }
1201
1202    #[test]
1203    fn same_sign_squares_select_the_nearest_and_farthest_endpoints() -> Result<(), LaError> {
1204        let expected = Interval::try_new(4.0, 9.0)?;
1205        for (lower, upper) in [(-3.0, -2.0), (2.0, 3.0)] {
1206            assert_eq!(Interval::try_new(lower, upper)?.try_square()?, expected);
1207        }
1208        Ok(())
1209    }
1210
1211    #[test]
1212    fn wide_addition_rounds_only_the_required_endpoints() -> Result<(), LaError> {
1213        let left = Interval::try_new(-1.0, 1.0)?;
1214        // A quarter ULP rounds both sums inward; three quarters rounds them
1215        // outward already. Both exact dyadic sums have the same tight bounds.
1216        for radius in [f64::EPSILON / 4.0, 3.0 * f64::EPSILON / 4.0] {
1217            let right = Interval::try_new(-radius, radius)?;
1218            assert_eq!(
1219                left.try_add(&right)?,
1220                Interval::try_new((-1.0_f64).next_down(), 1.0_f64.next_up())?
1221            );
1222        }
1223        assert_eq!(
1224            left.try_add(&Interval::try_new(-2.0, 2.0)?)?,
1225            Interval::try_new(-3.0, 3.0)?
1226        );
1227        Ok(())
1228    }
1229
1230    #[test]
1231    fn underflowed_product_still_encloses_the_positive_exact_result() -> Result<(), LaError> {
1232        let least_subnormal = f64::from_bits(1);
1233        let product = Interval::point(least_subnormal)?.try_mul(&Interval::point(0.5)?)?;
1234        assert_eq!(product, Interval::try_new(0.0, least_subnormal)?);
1235        Ok(())
1236    }
1237
1238    #[test]
1239    fn range_failure_preserves_interval_operation() -> Result<(), LaError> {
1240        let error = Interval::point(f64::MAX)?
1241            .try_mul(&Interval::point(2.0)?)
1242            .unwrap_err();
1243        assert_eq!(
1244            error,
1245            LaError::IntervalRangeExhausted {
1246                operation: ArithmeticOperation::IntervalMultiplication,
1247            }
1248        );
1249        Ok(())
1250    }
1251
1252    #[test]
1253    fn rounded_maximum_detects_exact_sum_beyond_finite_range() -> Result<(), LaError> {
1254        let maximum = Interval::point(f64::MAX)?;
1255        let tiny = Interval::point(f64::MIN_POSITIVE)?;
1256        assert_eq!(
1257            maximum.try_add(&maximum),
1258            Err(LaError::IntervalRangeExhausted {
1259                operation: ArithmeticOperation::IntervalAddition,
1260            })
1261        );
1262        assert_eq!(
1263            maximum.try_add(&tiny),
1264            Err(LaError::IntervalRangeExhausted {
1265                operation: ArithmeticOperation::IntervalAddition,
1266            })
1267        );
1268        let nonnegative = Interval::try_new(0.0, f64::MAX)?;
1269        assert_eq!(
1270            nonnegative.try_add(&nonnegative),
1271            Err(LaError::IntervalRangeExhausted {
1272                operation: ArithmeticOperation::IntervalAddition,
1273            })
1274        );
1275        let positive = Interval::try_new(f64::from_bits(1), 1.0)?;
1276        for (left, right) in [(maximum, positive), (maximum.negate(), positive.negate())] {
1277            assert_eq!(
1278                left.try_add(&right),
1279                Err(LaError::IntervalRangeExhausted {
1280                    operation: ArithmeticOperation::IntervalAddition,
1281                }),
1282            );
1283        }
1284
1285        let finite_difference = maximum.try_add(&tiny.negate())?;
1286        assert_eq!(finite_difference.upper().to_bits(), f64::MAX.to_bits());
1287        assert!(finite_difference.lower() < finite_difference.upper());
1288        Ok(())
1289    }
1290
1291    #[test]
1292    fn subtraction_and_square_preserve_distinct_range_operations() -> Result<(), LaError> {
1293        assert_eq!(
1294            Interval::try_from_subtraction(f64::MAX, -f64::MIN_POSITIVE),
1295            Err(LaError::IntervalRangeExhausted {
1296                operation: ArithmeticOperation::IntervalSubtraction,
1297            })
1298        );
1299        assert_eq!(
1300            Interval::point(f64::MAX)?.try_square(),
1301            Err(LaError::IntervalRangeExhausted {
1302                operation: ArithmeticOperation::IntervalSquare,
1303            })
1304        );
1305        assert_eq!(
1306            Interval::try_new(-1.0, f64::MAX)?.try_square(),
1307            Err(LaError::IntervalRangeExhausted {
1308                operation: ArithmeticOperation::IntervalSquare,
1309            })
1310        );
1311        Ok(())
1312    }
1313
1314    #[test]
1315    fn determinant_overflow_reports_interval_determinant_range_failure() -> Result<(), LaError> {
1316        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, 0.0], [0.0, 2.0]])?;
1317        assert_eq!(
1318            matrix.det(),
1319            Err(LaError::IntervalRangeExhausted {
1320                operation: ArithmeticOperation::IntervalDeterminant,
1321            })
1322        );
1323
1324        let accumulating =
1325            IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [-1.0, 1.0]])?;
1326        assert_eq!(
1327            accumulating.det(),
1328            Err(LaError::IntervalRangeExhausted {
1329                operation: ArithmeticOperation::IntervalDeterminant,
1330            })
1331        );
1332        assert_eq!(
1333            accumulating.det_sign(),
1334            Err(LaError::IntervalRangeExhausted {
1335                operation: ArithmeticOperation::IntervalDeterminant,
1336            })
1337        );
1338        Ok(())
1339    }
1340
1341    #[test]
1342    fn determinant_reports_intermediate_exhaustion_before_exact_cancellation() -> Result<(), LaError>
1343    {
1344        let matrix = IntervalMatrix::<2>::try_from_point_rows([[f64::MAX, f64::MAX], [2.0, 2.0]])?;
1345        assert_eq!(
1346            matrix.det(),
1347            Err(LaError::IntervalRangeExhausted {
1348                operation: ArithmeticOperation::IntervalDeterminant,
1349            })
1350        );
1351        Ok(())
1352    }
1353
1354    #[test]
1355    fn square_spanning_zero_has_exact_zero_lower_bound() -> Result<(), LaError> {
1356        let square = Interval::try_new(-2.0, 3.0)?.try_square()?;
1357        assert_eq!(square, Interval::try_new(0.0, 9.0)?);
1358        Ok(())
1359    }
1360
1361    #[test]
1362    fn multiplication_selects_correct_extrema_in_every_sign_quadrant() -> Result<(), LaError> {
1363        for (left, right, expected) in [
1364            ((2.0, 3.0), (4.0, 5.0), (8.0, 15.0)),
1365            ((2.0, 3.0), (-5.0, -4.0), (-15.0, -8.0)),
1366            ((2.0, 3.0), (-5.0, 4.0), (-15.0, 12.0)),
1367            ((-3.0, -2.0), (4.0, 5.0), (-15.0, -8.0)),
1368            ((-3.0, -2.0), (-5.0, -4.0), (8.0, 15.0)),
1369            ((-3.0, -2.0), (-5.0, 4.0), (-12.0, 15.0)),
1370            ((-3.0, 2.0), (4.0, 5.0), (-15.0, 10.0)),
1371            ((-3.0, 2.0), (-5.0, -4.0), (-10.0, 15.0)),
1372            ((-3.0, 2.0), (-5.0, 4.0), (-12.0, 15.0)),
1373        ] {
1374            let product = Interval::try_new(left.0, left.1)?
1375                .try_mul(&Interval::try_new(right.0, right.1)?)?;
1376            assert_eq!(product, Interval::try_new(expected.0, expected.1)?);
1377        }
1378
1379        assert_eq!(
1380            Interval::ZERO.try_mul(&Interval::try_new(-f64::MAX, f64::MAX)?)?,
1381            Interval::ZERO
1382        );
1383        Ok(())
1384    }
1385
1386    #[test]
1387    fn multiplication_rejects_unrepresentable_selected_extrema() -> Result<(), LaError> {
1388        let half_maximum = f64::MAX / 2.0;
1389        for (left, right) in [
1390            ((half_maximum, f64::MAX), (-2.0, -1.0)),
1391            ((half_maximum, f64::MAX), (1.0, 2.0)),
1392            ((-f64::MAX, 1.0), (-1.0, 2.0)),
1393            ((-1.0, f64::MAX), (-2.0, 1.0)),
1394            ((-f64::MAX, 1.0), (-2.0, 1.0)),
1395            ((-1.0, f64::MAX), (-1.0, 2.0)),
1396        ] {
1397            let result =
1398                Interval::try_new(left.0, left.1)?.try_mul(&Interval::try_new(right.0, right.1)?);
1399            assert_eq!(
1400                result,
1401                Err(LaError::IntervalRangeExhausted {
1402                    operation: ArithmeticOperation::IntervalMultiplication,
1403                }),
1404                "left={left:?}, right={right:?}"
1405            );
1406        }
1407        Ok(())
1408    }
1409
1410    macro_rules! gen_interval_identity_tests {
1411        ($d:literal) => {
1412            paste! {
1413                #[test]
1414                fn [<interval_identity_sign_is_positive_ $d d>]() {
1415                    let matrix = IntervalMatrix::<$d>::identity();
1416                    assert_eq!(matrix.det(), Ok(Interval::ONE));
1417                    assert_eq!(
1418                        matrix.det_sign(),
1419                        Ok(IntervalDeterminantSign::Positive)
1420                    );
1421                }
1422            }
1423        };
1424    }
1425
1426    gen_interval_identity_tests!(0);
1427    gen_interval_identity_tests!(1);
1428    gen_interval_identity_tests!(2);
1429    gen_interval_identity_tests!(3);
1430    gen_interval_identity_tests!(4);
1431    gen_interval_identity_tests!(5);
1432    gen_interval_identity_tests!(6);
1433    gen_interval_identity_tests!(7);
1434
1435    #[test]
1436    fn smallest_determinants_preserve_const_evaluation_and_interval_signs() -> Result<(), LaError> {
1437        const EMPTY: Result<Interval, LaError> = IntervalMatrix::<0>::zero().det();
1438        const WIDE: Interval = match Interval::try_new(-f64::MAX, f64::MAX) {
1439            Ok(interval) => interval,
1440            Err(_) => panic!("finite ordered literal bounds"),
1441        };
1442        const SINGLE: Result<Interval, LaError> = IntervalMatrix::from_rows([[WIDE]]).det();
1443        assert_eq!(EMPTY, Ok(Interval::ONE));
1444        assert_eq!(SINGLE, Ok(WIDE));
1445        for (lower, upper, sign) in [
1446            (
1447                -f64::MAX,
1448                -f64::from_bits(1),
1449                IntervalDeterminantSign::Negative,
1450            ),
1451            (
1452                f64::from_bits(1),
1453                f64::MAX,
1454                IntervalDeterminantSign::Positive,
1455            ),
1456            (0.0, 0.0, IntervalDeterminantSign::Zero),
1457            (-f64::MAX, f64::MAX, IntervalDeterminantSign::Inconclusive),
1458        ] {
1459            let interval = Interval::try_new(lower, upper)?;
1460            let matrix = IntervalMatrix::from_rows([[interval]]);
1461            assert_eq!(matrix.det()?, interval);
1462            assert_eq!(matrix.det_sign()?, sign);
1463        }
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn determinant_sign_handles_row_swap_and_exact_singularity() -> Result<(), LaError> {
1469        let swapped = IntervalMatrix::<3>::try_from_point_rows([
1470            [0.0, 1.0, 0.0],
1471            [1.0, 0.0, 0.0],
1472            [0.0, 0.0, 1.0],
1473        ])?;
1474        assert_eq!(swapped.det_sign()?, IntervalDeterminantSign::Negative);
1475
1476        let singular = IntervalMatrix::<3>::try_from_point_rows([
1477            [1.0, 2.0, 3.0],
1478            [1.0, 2.0, 3.0],
1479            [0.0, 0.0, 1.0],
1480        ])?;
1481        assert_eq!(singular.det_sign()?, IntervalDeterminantSign::Zero);
1482        Ok(())
1483    }
1484
1485    #[test]
1486    fn wide_determinant_interval_is_inconclusive() -> Result<(), LaError> {
1487        let matrix = IntervalMatrix::<2>::from_rows([
1488            [Interval::ONE, Interval::ZERO],
1489            [Interval::ZERO, Interval::try_new(-1.0, 1.0)?],
1490        ]);
1491        assert_eq!(matrix.det_sign()?, IntervalDeterminantSign::Inconclusive);
1492        Ok(())
1493    }
1494
1495    #[test]
1496    fn determinant_rejects_dimensions_above_supported_stack_dp() {
1497        assert_matches!(
1498            IntervalMatrix::<8>::identity().det(),
1499            Err(LaError::UnsupportedDimension {
1500                requested: 8,
1501                max: MAX_INTERVAL_MATRIX_DIM,
1502                ..
1503            })
1504        );
1505    }
1506
1507    #[test]
1508    fn matrix_accessors_preserve_validated_storage() -> Result<(), LaError> {
1509        let source = Matrix::<2>::identity();
1510        let mut intervals = IntervalMatrix::from_matrix(&source);
1511        let value = Interval::try_new(2.0, 3.0)?;
1512        intervals.set(0, 1, value)?;
1513        assert_eq!(intervals.get(0, 1), Some(value));
1514        assert_eq!(intervals.get(2, 0), None);
1515        assert_eq!(intervals.try_get(0, 1)?, value);
1516        assert_matches!(
1517            intervals.try_get(2, 0),
1518            Err(LaError::IndexOutOfBounds {
1519                row: 2,
1520                col: 0,
1521                dim: 2,
1522                ..
1523            })
1524        );
1525        assert_eq!(intervals.as_rows()[0][1], value);
1526        assert_eq!(intervals.into_rows()[0][1], value);
1527        Ok(())
1528    }
1529
1530    #[test]
1531    fn rejected_matrix_set_is_failure_atomic() -> Result<(), LaError> {
1532        let mut matrix = IntervalMatrix::<2>::identity();
1533        let before = matrix;
1534        let value = Interval::try_new(2.0, 3.0)?;
1535
1536        assert_eq!(
1537            matrix.set(2, 0, value),
1538            Err(LaError::IndexOutOfBounds {
1539                row: 2,
1540                col: 0,
1541                dim: 2,
1542            })
1543        );
1544        assert_eq!(matrix, before);
1545        assert_eq!(
1546            matrix.set(0, 2, value),
1547            Err(LaError::IndexOutOfBounds {
1548                row: 0,
1549                col: 2,
1550                dim: 2,
1551            })
1552        );
1553        assert_eq!(matrix, before);
1554        Ok(())
1555    }
1556}