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}