Skip to main content

la_stack/
lib.rs

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