Skip to main content

la_stack/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![doc = include_str!("../README.md")]
5
6#[cfg(doc)]
7mod readme_doctests {
8    //! Executable versions of README examples.
9    /// ```rust
10    /// use la_stack::prelude::*;
11    ///
12    /// # fn main() -> Result<(), LaError> {
13    /// // This system requires pivoting (a[0][0] = 0), so it's a good LU demo.
14    /// let a = Matrix::<5>::try_from_rows([
15    ///     [0.0, 1.0, 1.0, 1.0, 1.0],
16    ///     [1.0, 0.0, 1.0, 1.0, 1.0],
17    ///     [1.0, 1.0, 0.0, 1.0, 1.0],
18    ///     [1.0, 1.0, 1.0, 0.0, 1.0],
19    ///     [1.0, 1.0, 1.0, 1.0, 0.0],
20    /// ])?;
21    ///
22    /// let b = Vector::<5>::try_new([14.0, 13.0, 12.0, 11.0, 10.0])?;
23    ///
24    /// let lu = a.lu(DEFAULT_SINGULAR_TOL)?;
25    /// let x = lu.solve(b)?.into_array();
26    ///
27    /// // Floating-point rounding is expected; compare with a tolerance.
28    /// let expected = [1.0, 2.0, 3.0, 4.0, 5.0];
29    /// for (x_i, e_i) in x.iter().zip(expected.iter()) {
30    ///     assert!((*x_i - *e_i).abs() <= 1e-12);
31    /// }
32    /// # Ok(())
33    /// # }
34    /// ```
35    fn solve_5x5_example() {}
36
37    /// ```rust
38    /// use la_stack::prelude::*;
39    ///
40    /// # fn main() -> Result<(), LaError> {
41    /// // This matrix is symmetric positive-definite (A = L*L^T) so LDLT works without pivoting.
42    /// let a = Matrix::<5>::try_from_rows([
43    ///     [1.0, 1.0, 0.0, 0.0, 0.0],
44    ///     [1.0, 2.0, 1.0, 0.0, 0.0],
45    ///     [0.0, 1.0, 2.0, 1.0, 0.0],
46    ///     [0.0, 0.0, 1.0, 2.0, 1.0],
47    ///     [0.0, 0.0, 0.0, 1.0, 2.0],
48    /// ])?;
49    ///
50    /// let ldlt = match a.ldlt(DEFAULT_SINGULAR_TOL) {
51    ///     Ok(ldlt) => ldlt,
52    ///     Err(err @ LaError::Asymmetric { row, col, .. }) => {
53    ///         eprintln!("LDLT requires symmetry; first mismatch at ({row}, {col})");
54    ///         return Err(err);
55    ///     }
56    ///     Err(err) => return Err(err),
57    /// };
58    ///
59    /// let det = ldlt.det()?;
60    /// assert!((det - 1.0).abs() <= 1e-12);
61    /// # Ok(())
62    /// # }
63    /// ```
64    fn det_5x5_ldlt_example() {}
65
66    /// ```rust
67    /// use la_stack::prelude::*;
68    ///
69    /// // Evaluated entirely at compile time — no runtime cost.
70    /// const DET: Result<Option<f64>, LaError> = match Matrix::<4>::try_from_rows([
71    ///     [2.0, 0.0, 0.0, 0.0],
72    ///     [0.0, 3.0, 0.0, 0.0],
73    ///     [0.0, 0.0, 5.0, 0.0],
74    ///     [0.0, 0.0, 0.0, 7.0],
75    /// ]) {
76    ///     Ok(matrix) => matrix.det_direct(),
77    ///     Err(err) => Err(err),
78    /// };
79    ///
80    /// # fn main() -> Result<(), LaError> {
81    /// assert_eq!(DET?, Some(210.0));
82    /// # Ok(())
83    /// # }
84    /// ```
85    fn det_direct_4x4_const_example() {}
86
87    #[cfg(feature = "exact")]
88    /// ```rust
89    /// use la_stack::prelude::*;
90    ///
91    /// # fn main() -> Result<(), LaError> {
92    /// // Exact determinant
93    /// let m = Matrix::<3>::try_from_rows([
94    ///     [1.0, 2.0, 3.0],
95    ///     [4.0, 5.0, 6.0],
96    ///     [7.0, 8.0, 9.0],
97    /// ])?;
98    /// assert_eq!(m.det_sign_exact(), DeterminantSign::Zero); // exactly singular
99    ///
100    /// let det = m.det_exact()?;
101    /// assert_eq!(det, BigRational::from_integer(0.into())); // exact zero
102    /// let det_f64 = det.try_to_f64()?;
103    /// assert_eq!(det_f64, 0.0);
104    ///
105    /// // If strict exact-to-f64 conversion would require rounding, opt in
106    /// // explicitly with the rounded API.
107    /// let inexact = Matrix::<2>::try_from_rows([
108    ///     [1.0 + f64::EPSILON, 0.0],
109    ///     [0.0, 1.0 - f64::EPSILON],
110    /// ])?;
111    /// let exact_det = inexact.det_exact()?;
112    /// let rounded_det = match exact_det.try_to_f64() {
113    ///     Ok(det) => det,
114    ///     Err(err) if err.requires_rounding() => exact_det.to_rounded_f64()?,
115    ///     Err(err) => return Err(err),
116    /// };
117    /// assert_eq!(rounded_det.to_bits(), 1.0f64.to_bits());
118    ///
119    /// // If the exact determinant cannot fit in f64, keep the BigRational value.
120    /// let big = f64::MAX / 2.0;
121    /// let huge = Matrix::<3>::try_from_rows([
122    ///     [0.0, 0.0, 1.0],
123    ///     [big, 0.0, 1.0],
124    ///     [0.0, big, 1.0],
125    /// ])?;
126    /// let huge_det = huge.det_exact()?;
127    /// assert_eq!(
128    ///     huge_det
129    ///         .try_to_f64()
130    ///         .err()
131    ///         .and_then(|err| err.unrepresentable_reason()),
132    ///     Some(UnrepresentableReason::NotFinite)
133    /// );
134    /// println!("exact determinant = {huge_det}");
135    ///
136    /// // Exact linear system solve
137    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
138    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
139    /// let exact_x = a.solve_exact(b)?;
140    /// let x = exact_x.try_to_f64()?.into_array();
141    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
142    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
143    /// # Ok(())
144    /// # }
145    /// ```
146    fn exact_arithmetic_example() {}
147
148    #[cfg(feature = "exact")]
149    /// ```rust
150    /// use la_stack::prelude::*;
151    ///
152    /// fn adaptive_det_sign<const D: usize>(
153    ///     matrix: &Matrix<D>,
154    /// ) -> DeterminantSign {
155    ///     if let Ok(Some(estimate)) = matrix.det_direct_with_errbound() {
156    ///         if estimate.determinant().abs() > estimate.absolute_error_bound() {
157    ///             return if estimate.determinant() > 0.0 {
158    ///                 DeterminantSign::Positive
159    ///             } else {
160    ///                 DeterminantSign::Negative
161    ///             };
162    ///         }
163    ///     }
164    ///
165    ///     matrix.det_sign_exact()
166    /// }
167    ///
168    /// # fn main() -> Result<(), LaError> {
169    /// let identity = Matrix::<3>::identity();
170    /// assert_eq!(
171    ///     adaptive_det_sign(&identity),
172    ///     DeterminantSign::Positive
173    /// );
174    ///
175    /// let singular = Matrix::<3>::try_from_rows([
176    ///     [1.0, 2.0, 3.0],
177    ///     [4.0, 5.0, 6.0],
178    ///     [7.0, 8.0, 9.0],
179    /// ])?;
180    /// assert_eq!(adaptive_det_sign(&singular), DeterminantSign::Zero);
181    ///
182    /// let big = f64::MAX / 2.0;
183    /// let overflowing = Matrix::<3>::try_from_rows([
184    ///     [0.0, 0.0, 1.0],
185    ///     [big, 0.0, 1.0],
186    ///     [0.0, big, 1.0],
187    /// ])?;
188    /// assert_eq!(
189    ///     adaptive_det_sign(&overflowing),
190    ///     DeterminantSign::Positive
191    /// );
192    /// # Ok(())
193    /// # }
194    /// ```
195    fn adaptive_precision_example() {}
196}
197
198mod error;
199#[cfg(feature = "exact")]
200mod exact;
201mod ldlt;
202mod lu;
203mod matrix;
204mod scaled_product;
205mod tolerance;
206mod vector;
207
208#[cfg(feature = "exact")]
209#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
210pub use exact::{DeterminantSign, ExactF64Conversion};
211#[cfg(feature = "exact")]
212#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
213pub use num_bigint::BigInt;
214#[cfg(feature = "exact")]
215#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
216pub use num_rational::BigRational;
217#[cfg(feature = "exact")]
218#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
219pub use num_traits::{FromPrimitive, Signed, ToPrimitive};
220
221// ---------------------------------------------------------------------------
222// Error-bound constants for `Matrix::det_direct_with_errbound()` and
223// `Matrix::det_errbound()`.
224//
225// For `D ∈ {2, 3, 4}`, `Matrix::det_direct()` evaluates the Leibniz expansion
226// of the determinant as a tree of f64 multiplies and fused multiply-adds
227// (FMAs).  When every rounded intermediate is normal or an exact structural
228// zero, Shewchuk's error-analysis methodology (REFERENCES.md [8]) bounds the
229// absolute error of that computation by
230//
231//     |det_direct(A) - det_exact(A)|  ≤  ERR_COEFF_D · p(|A|)
232//
233// where `p(|A|)` is the **absolute Leibniz sum**
234//
235//     p(|A|) = Σ_σ ∏ᵢ |A[i, σ(i)]|,
236//
237// i.e. exactly the combinatorial matrix permanent `perm(|A|)`. The
238// implementation evaluates the corresponding fixed-size expansion in f64, so
239// the computed `permanent` value used by the bound may itself be rounded even
240// though the mathematical quantity above is exact.
241//
242// Each constant has the shape `a · EPS + b · EPS²`: the linear term bounds
243// the first-order rounding and the quadratic term absorbs the interaction
244// of errors in nested FMAs.  The coefficients `a` and `b` are conservative
245// over-estimates derived from the longest dependency chain of `det_direct`
246// at that dimension.
247//
248// These constants are NOT feature-gated — they rely only on f64 arithmetic
249// and are useful for adaptive-precision logic even without the `exact`
250// feature. Most callers should prefer `Matrix::det_direct_with_errbound()`
251// when they need the approximation and bound together, or
252// `Matrix::det_errbound()` when they need only the bound. Those methods apply
253// these constants to the actual matrix; the raw constants are
254// exposed for advanced use cases (composing the bound with a pre-reduced
255// permanent, rolling a custom adaptive filter, etc.).  See
256// `Matrix::det_sign_exact()` (behind the `exact` feature) for the
257// reference adaptive-filter that consumes these internally.
258// ---------------------------------------------------------------------------
259
260const EPS: f64 = f64::EPSILON; // 2^-52
261
262/// Absolute error coefficient for [`Matrix::<2>::det_direct`](crate::Matrix::det_direct).
263///
264/// This constant is not a caller-tuned tolerance. It is the dimension-specific
265/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
266/// bound on floating-point roundoff in the closed-form 2×2 determinant formula.
267///
268/// For a 2×2 matrix `A = [[a, b], [c, d]]` whose closed-form determinant
269/// intermediates do not undergo gradual underflow,
270///
271/// ```text
272/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_2 · (|a·d| + |b·c|)
273/// ```
274///
275/// `det_direct` evaluates `a·d - b·c` as one multiply followed by one FMA
276/// (2 rounding events); the linear `3·EPS` term bounds those roundings
277/// and the quadratic `16·EPS²` term is a conservative cushion for their
278/// interaction.  Derivation follows Shewchuk's framework; see
279/// `REFERENCES.md` \[8\].
280///
281/// Prefer
282/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
283/// unless you need only the bound or already have the absolute-Leibniz sum;
284/// see
285/// `Matrix::det_sign_exact` (under the `exact` feature) for the reference
286/// adaptive-precision filter.
287///
288/// # Example
289/// ```
290/// use la_stack::{prelude::*, ERR_COEFF_2};
291///
292/// # fn main() -> Result<(), LaError> {
293/// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
294/// let Some(det) = m.det_direct()? else {
295///     return Ok(());
296/// };
297/// assert_eq!(det, -2.0);
298/// // Compute the bound from the raw constant for illustration; most
299/// // callers would match on `m.det_errbound()?` instead.
300/// let p = (1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs();
301/// let bound = ERR_COEFF_2 * p;
302/// if det.abs() > bound {
303///     // The f64 sign is provably correct without exact arithmetic.
304/// }
305/// # Ok(())
306/// # }
307/// ```
308pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
309
310/// Absolute error coefficient for [`Matrix::<3>::det_direct`](crate::Matrix::det_direct).
311///
312/// This constant is not a caller-tuned tolerance. It is the dimension-specific
313/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
314/// bound on floating-point roundoff in the closed-form 3×3 determinant formula.
315///
316/// For a 3×3 matrix `A` whose closed-form determinant intermediates do not
317/// undergo gradual underflow,
318///
319/// ```text
320/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_3 · p(|A|)
321/// ```
322///
323/// where `p(|A|)` is the absolute Leibniz sum (the same cofactor
324/// expansion as `det_direct` but with `|·|` at every leaf).
325/// `det_direct` for D=3 uses three 2×2 FMA minors combined by a nested
326/// FMA, yielding the `8·EPS + 64·EPS²` bound.  See `REFERENCES.md`
327/// \[8\] for the Shewchuk framework these bounds follow.
328///
329/// Prefer
330/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
331/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
332/// example.
333pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
334
335/// Absolute error coefficient for [`Matrix::<4>::det_direct`](crate::Matrix::det_direct).
336///
337/// This constant is not a caller-tuned tolerance. It is the dimension-specific
338/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
339/// bound on floating-point roundoff in the closed-form 4×4 determinant formula.
340///
341/// For a 4×4 matrix `A` whose closed-form determinant intermediates do not
342/// undergo gradual underflow,
343///
344/// ```text
345/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_4 · p(|A|)
346/// ```
347///
348/// where `p(|A|)` is the absolute Leibniz sum. `det_direct` for D=4
349/// evaluates four nested 3×3 cofactors, sharing their six 2×2 minors when
350/// every coefficient in the first two rows is non-zero, and reduces them with
351/// an FMA row combination, yielding the
352/// `12·EPS + 128·EPS²` bound.  See `REFERENCES.md` \[8\] for the
353/// Shewchuk framework these bounds follow.
354///
355/// Prefer
356/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
357/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
358/// example.
359pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
360
361/// Largest dimension supported by [`try_with_stack_matrix!`].
362///
363/// The crate can represent `Matrix<D>` for any compile-time `D`, but runtime
364/// dispatch must enumerate a finite set of concrete stack types.  Dimensions
365/// `0..=7` cover downstream geometric predicate matrices while keeping the
366/// dispatch surface explicit.
367pub const MAX_STACK_MATRIX_DISPATCH_DIM: usize = 7;
368
369pub use error::{
370    ArithmeticOperation, FactorizationKind, InvalidToleranceReason, LaError, NonFiniteLocation,
371    NonFiniteOrigin, PositiveSemidefiniteViolation, SingularityReason, UnrepresentableReason,
372};
373pub use ldlt::Ldlt;
374pub use lu::Lu;
375pub use matrix::{DeterminantWithErrorBound, Matrix};
376pub use tolerance::{DEFAULT_SINGULAR_TOL, Tolerance};
377pub use vector::Vector;
378
379/// Fallibly dispatch a runtime dimension to a concrete stack-allocated matrix.
380///
381/// The macro creates a zero matrix with type `Matrix<N>` for the selected
382/// runtime dimension `N`, then evaluates the supplied closure body.  Supported
383/// runtime dimensions run from `0` through [`MAX_STACK_MATRIX_DISPATCH_DIM`].
384/// Unsupported dimensions return
385/// `Err(LaError::UnsupportedDimension { requested, max })` converted with
386/// `From<LaError>`, so downstream crates can use their own public error type.
387///
388/// # Errors
389/// Returns [`LaError::UnsupportedDimension`] (converted through `From<LaError>`)
390/// when the requested runtime dimension is greater than
391/// [`MAX_STACK_MATRIX_DISPATCH_DIM`].  The closure body may return any other
392/// error representable by its declared `Result` type.
393///
394/// # Examples
395/// ```
396/// use la_stack::prelude::*;
397///
398/// # fn main() -> Result<(), LaError> {
399/// let requested = 2usize;
400/// let det = try_with_stack_matrix!(requested, |mut m| -> Result<f64, LaError> {
401///     m.set(0, 0, 1.0)?;
402///     m.set(1, 1, 1.0)?;
403///     m.det()
404/// })?;
405///
406/// assert_eq!(det, 1.0);
407/// # Ok(())
408/// # }
409/// ```
410#[macro_export]
411macro_rules! try_with_stack_matrix {
412    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
413        let __la_stack_requested_dim: usize = $dim;
414        match __la_stack_requested_dim {
415            0 => $crate::try_with_stack_matrix!(@arm 0, $matrix, $ret, $body),
416            1 => $crate::try_with_stack_matrix!(@arm 1, $matrix, $ret, $body),
417            2 => $crate::try_with_stack_matrix!(@arm 2, $matrix, $ret, $body),
418            3 => $crate::try_with_stack_matrix!(@arm 3, $matrix, $ret, $body),
419            4 => $crate::try_with_stack_matrix!(@arm 4, $matrix, $ret, $body),
420            5 => $crate::try_with_stack_matrix!(@arm 5, $matrix, $ret, $body),
421            6 => $crate::try_with_stack_matrix!(@arm 6, $matrix, $ret, $body),
422            7 => $crate::try_with_stack_matrix!(@arm 7, $matrix, $ret, $body),
423            requested => Err(::core::convert::From::from(
424                $crate::LaError::unsupported_dimension(
425                    requested,
426                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
427                ),
428            )),
429        }
430    }};
431    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
432        let __la_stack_requested_dim: usize = $dim;
433        match __la_stack_requested_dim {
434            0 => $crate::try_with_stack_matrix!(@arm_mut 0, $matrix, $ret, $body),
435            1 => $crate::try_with_stack_matrix!(@arm_mut 1, $matrix, $ret, $body),
436            2 => $crate::try_with_stack_matrix!(@arm_mut 2, $matrix, $ret, $body),
437            3 => $crate::try_with_stack_matrix!(@arm_mut 3, $matrix, $ret, $body),
438            4 => $crate::try_with_stack_matrix!(@arm_mut 4, $matrix, $ret, $body),
439            5 => $crate::try_with_stack_matrix!(@arm_mut 5, $matrix, $ret, $body),
440            6 => $crate::try_with_stack_matrix!(@arm_mut 6, $matrix, $ret, $body),
441            7 => $crate::try_with_stack_matrix!(@arm_mut 7, $matrix, $ret, $body),
442            requested => Err(::core::convert::From::from(
443                $crate::LaError::unsupported_dimension(
444                    requested,
445                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
446                ),
447            )),
448        }
449    }};
450    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
451        let __la_stack_body = |$matrix: $crate::Matrix<$d>| -> $ret { $body };
452        __la_stack_body($crate::Matrix::<$d>::zero())
453    }};
454    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
455        let __la_stack_body = |mut $matrix: $crate::Matrix<$d>| -> $ret { $body };
456        __la_stack_body($crate::Matrix::<$d>::zero())
457    }};
458}
459
460/// Common imports for ergonomic usage.
461///
462/// This prelude re-exports the primary types and common constants: [`Matrix`],
463/// [`DeterminantWithErrorBound`], [`Vector`], [`Lu`], [`Ldlt`], [`Tolerance`],
464/// and [`LaError`]. Its typed
465/// error categories include [`ArithmeticOperation`], [`FactorizationKind`],
466/// [`InvalidToleranceReason`], [`NonFiniteLocation`], [`NonFiniteOrigin`],
467/// [`PositiveSemidefiniteViolation`], [`SingularityReason`], and
468/// [`UnrepresentableReason`]. It also re-exports [`DEFAULT_SINGULAR_TOL`],
469/// [`MAX_STACK_MATRIX_DISPATCH_DIM`], and [`try_with_stack_matrix!`] for
470/// runtime-to-const matrix dispatch. Advanced custom-filter code should import
471/// [`ERR_COEFF_2`], [`ERR_COEFF_3`], and [`ERR_COEFF_4`] explicitly from the
472/// crate root; those raw coefficients intentionally stay out of the prelude.
473///
474/// When the `exact` feature is enabled, `DeterminantSign`,
475/// `ExactF64Conversion`, `BigInt`, and `BigRational` are also re-exported.
476/// `ExactF64Conversion` converts an already-computed exact determinant or
477/// solution under either the strict or explicitly rounded binary64 contract,
478/// without repeating exact elimination. The number types let callers construct
479/// expected exact values without adding `num-bigint` / `num-rational` to their
480/// own dependencies. The most commonly needed `num-traits` items are re-exported
481/// alongside them: `FromPrimitive` for `BigRational::from_f64` / `from_i64`,
482/// `ToPrimitive` for `BigRational::to_f64` / `to_i64`, and `Signed` for
483/// `.is_positive()` / `.is_negative()` / `.abs()`.
484pub mod prelude {
485    pub use crate::{
486        ArithmeticOperation, DEFAULT_SINGULAR_TOL, DeterminantWithErrorBound, FactorizationKind,
487        InvalidToleranceReason, LaError, Ldlt, Lu, MAX_STACK_MATRIX_DISPATCH_DIM, Matrix,
488        NonFiniteLocation, NonFiniteOrigin, PositiveSemidefiniteViolation, SingularityReason,
489        Tolerance, UnrepresentableReason, Vector, try_with_stack_matrix,
490    };
491
492    #[cfg(feature = "exact")]
493    #[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
494    pub use crate::{
495        BigInt, BigRational, DeterminantSign, ExactF64Conversion, FromPrimitive, Signed,
496        ToPrimitive,
497    };
498}
499
500#[cfg(test)]
501mod tests {
502    use approx::assert_abs_diff_eq;
503    use pastey::paste;
504
505    use super::*;
506
507    macro_rules! gen_stack_matrix_dispatch_tests {
508        ($d:literal) => {
509            paste! {
510                #[test]
511                fn [<try_with_stack_matrix_dispatches_ $d d>]() {
512                    let requested = $d;
513                    let got = try_with_stack_matrix!(requested, |mut m| -> Result<usize, LaError> {
514                        if $d > 0 {
515                            m.set($d - 1, $d - 1, f64::from($d))?;
516                            assert_abs_diff_eq!(
517                                m.try_get($d - 1, $d - 1)?,
518                                f64::from($d),
519                                epsilon = 0.0
520                            );
521                        }
522                        Ok($d)
523                    });
524
525                    assert_eq!(got, Ok($d));
526                }
527            }
528        };
529    }
530
531    gen_stack_matrix_dispatch_tests!(1);
532    gen_stack_matrix_dispatch_tests!(2);
533    gen_stack_matrix_dispatch_tests!(3);
534    gen_stack_matrix_dispatch_tests!(4);
535    gen_stack_matrix_dispatch_tests!(5);
536    gen_stack_matrix_dispatch_tests!(6);
537    gen_stack_matrix_dispatch_tests!(7);
538
539    #[test]
540    fn try_with_stack_matrix_supports_zero_dimension() {
541        let got = try_with_stack_matrix!(0usize, |m| -> Result<Option<f64>, LaError> {
542            m.det_direct()
543        });
544
545        assert_eq!(got, Ok(Some(1.0)));
546    }
547
548    #[test]
549    fn try_with_stack_matrix_evaluates_dimension_once() {
550        let mut evaluations = 0;
551        let got = try_with_stack_matrix!(
552            {
553                evaluations += 1;
554                2usize
555            },
556            |matrix| -> Result<f64, LaError> { matrix.try_get(1, 1) },
557        );
558
559        assert_eq!(evaluations, 1);
560        assert_eq!(got, Ok(0.0));
561    }
562
563    #[test]
564    fn try_with_stack_matrix_reports_unsupported_dimension() {
565        let got = try_with_stack_matrix!(8usize, |m| -> Result<f64, LaError> { m.det() });
566
567        assert_eq!(
568            got,
569            Err(LaError::UnsupportedDimension {
570                requested: 8,
571                max: MAX_STACK_MATRIX_DISPATCH_DIM,
572            })
573        );
574    }
575
576    #[derive(Debug, PartialEq)]
577    struct DownstreamError(LaError);
578
579    impl From<LaError> for DownstreamError {
580        fn from(err: LaError) -> Self {
581            Self(err)
582        }
583    }
584
585    #[test]
586    fn try_with_stack_matrix_converts_unsupported_dimension_error() {
587        let got = try_with_stack_matrix!(9usize, |m| -> Result<usize, DownstreamError> {
588            assert_abs_diff_eq!(m.inf_norm()?, 0.0, epsilon = 0.0);
589            Ok(0)
590        });
591
592        assert_eq!(
593            got,
594            Err(DownstreamError(LaError::UnsupportedDimension {
595                requested: 9,
596                max: MAX_STACK_MATRIX_DISPATCH_DIM,
597            }))
598        );
599    }
600}