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