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}