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 version of the README quickstart.
9    /// ```rust
10    /// use la_stack::prelude::*;
11    ///
12    /// fn main() -> Result<(), LaError> {
13    ///     // The zero leading entry requires LU pivoting.
14    ///     let a = Matrix::<5>::try_from_rows([
15    ///         [0.0, 2.0, -1.0, 1.0, 3.0],
16    ///         [4.0, -1.0, 2.0, 0.0, 1.0],
17    ///         [1.0, 3.0, 5.0, -2.0, 0.0],
18    ///         [2.0, 0.0, -1.0, 4.0, 1.0],
19    ///         [-1.0, 2.0, 0.0, 1.0, 6.0],
20    ///     ])?;
21    ///     let b = Vector::try_new([20.0, 13.0, 14.0, 20.0, 37.0])?;
22    ///     let lu = a.lu(DEFAULT_SINGULAR_TOL)?;
23    ///     let x = lu.solve(b)?;
24    ///
25    ///     for (&actual, expected) in x.as_array().iter().zip([1.0, 2.0, 3.0, 4.0, 5.0]) {
26    ///         assert!((actual - expected).abs() <= 1e-12);
27    ///     }
28    ///     Ok(())
29    /// }
30    /// ```
31    fn solve_5x5_example() {}
32}
33
34// Documentation-only workflows keep the README overview compact without adding
35// a runtime API. Their examples are executed by the default/exact doctest gates.
36#[cfg(doc)]
37pub mod guide {
38    //! Worked examples and contracts for choosing and combining APIs.
39    //!
40    //! Start with [`prelude`](crate::prelude) for common imports. The crate's
41    //! generated reference lists the complete public surface; these examples
42    //! demonstrate how the pieces fit together.
43    //!
44    //! Explore the topic guides for more workflows:
45    //!
46    //! - [LDLT determinants and symmetry](ldlt)
47    //! - [Compile-time determinants](compile_time)
48    //! - [Outward-rounded interval determinants](intervals)
49    //! - [Overflow-safe Euclidean norms](norms)
50    //! - [Certified dot products and affine differences](certified)
51    //! - [Adaptive determinant filtering](adaptive)
52    //!
53    //! Enabling `exact` also adds the exact-arithmetic guide to the module list.
54    //!
55    //! # Solving and reusing factors
56    //!
57    //! [`Matrix::lu`](crate::Matrix::lu) computes a partially pivoted
58    //! factorization. Keep the resulting [`Lu`](crate::Lu) to solve multiple
59    //! right-hand sides without repeating factorization. This 5×5 system has a
60    //! zero leading entry, so the first elimination step requires pivoting.
61    //!
62    //! ```rust
63    //! use la_stack::prelude::*;
64    //!
65    //! # fn main() -> Result<(), LaError> {
66    //! let a = Matrix::<5>::try_from_rows([
67    //!     [0.0, 2.0, -1.0, 1.0, 3.0],
68    //!     [4.0, -1.0, 2.0, 0.0, 1.0],
69    //!     [1.0, 3.0, 5.0, -2.0, 0.0],
70    //!     [2.0, 0.0, -1.0, 4.0, 1.0],
71    //!     [-1.0, 2.0, 0.0, 1.0, 6.0],
72    //! ])?;
73    //! let lu = a.lu(DEFAULT_SINGULAR_TOL)?;
74    //! let systems = [
75    //!     ([20.0, 13.0, 14.0, 20.0, 37.0], [1.0, 2.0, 3.0, 4.0, 5.0]),
76    //!     ([5.0, 6.0, 7.0, 6.0, 8.0], [1.0; 5]),
77    //! ];
78    //! for (rhs, expected) in systems {
79    //!     let solution = lu.solve(Vector::try_new(rhs)?)?;
80    //!     for (&actual, expected) in solution.as_array().iter().zip(expected) {
81    //!         assert!((actual - expected).abs() <= 1e-12);
82    //!     }
83    //! }
84    //! # Ok(())
85    //! # }
86    //! ```
87    //!
88    //! The assertions use a tolerance suitable for this known fixture; they do
89    //! not establish a general error bound for LU. Factorization tolerances
90    //! reject small pivots and are not accuracy guarantees.
91    //! [`Ldlt`](crate::Ldlt) offers the same solve/determinant workflow for
92    //! exactly symmetric positive-definite input, without pivoting. Approximate
93    //! symmetry from [`Matrix::is_symmetric`](crate::Matrix::is_symmetric) or
94    //! [`Matrix::first_asymmetry`](crate::Matrix::first_asymmetry) does not prove
95    //! the exact symmetry required by [`Matrix::ldlt`](crate::Matrix::ldlt).
96    //!
97    //! # Gram matrices
98    //!
99    //! [`gram_matrix`](crate::gram_matrix) accepts `M` vectors of dimension `N`
100    //! and returns a `Matrix<M>` of pairwise inner products. Here five vectors
101    //! in six dimensions produce a 5×5 matrix. Each dot product is computed once
102    //! and mirrored, so the result is bit-for-bit symmetric.
103    //!
104    //! ```rust
105    //! use la_stack::prelude::*;
106    //!
107    //! # fn main() -> Result<(), LaError> {
108    //! let vectors = [
109    //!     Vector::try_new([1.0, 1.0, 0.0, 0.0, 0.0, 0.0])?,
110    //!     Vector::try_new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0])?,
111    //!     Vector::try_new([0.0, 0.0, 1.0, 1.0, 0.0, 0.0])?,
112    //!     Vector::try_new([0.0, 0.0, 0.0, 1.0, 1.0, 0.0])?,
113    //!     Vector::try_new([0.0, 0.0, 0.0, 0.0, 1.0, 1.0])?,
114    //! ];
115    //! let gram = gram_matrix(&vectors)?;
116    //! assert_eq!(gram.norm_inf()?, 4.0);
117    //! assert!(gram.is_symmetric(Tolerance::try_new(0.0)?)?);
118    //!
119    //! // This fixture's exact Gram matrix is tridiagonal: 2 on the diagonal,
120    //! // 1 immediately above/below it. Its 5×5 determinant is 6.
121    //! let determinant = gram.ldlt(DEFAULT_SINGULAR_TOL)?.det()?;
122    //! assert!((determinant - 6.0).abs() <= 1e-12);
123    //! # Ok(())
124    //! # }
125    //! ```
126    //!
127    //! Gram construction provides no certified rounding-error bound and does
128    //! not prove rank or positive definiteness. The generated function
129    //! documentation explains its conditioning and geometric interpretation.
130    //!
131    //! # Dimension dispatch
132    //!
133    //! [`try_with_stack_matrix!`](crate::try_with_stack_matrix) selects a
134    //! concrete `Matrix<N>` for runtime dimensions 0 through
135    //! [`MAX_STACK_MATRIX_DISPATCH_DIM`](crate::MAX_STACK_MATRIX_DISPATCH_DIM)
136    //! (7). The closure receives a zero matrix and returns its declared result.
137    //!
138    //! ```rust
139    //! use core::assert_matches;
140    //!
141    //! use la_stack::prelude::*;
142    //!
143    //! # fn main() -> Result<(), LaError> {
144    //! let requested = 5usize;
145    //! let determinant = try_with_stack_matrix!(requested, |mut matrix| -> Result<f64, LaError> {
146    //!     for row in 0..requested {
147    //!         matrix.set(row, row, 2.0)?;
148    //!         if row + 1 < requested {
149    //!             matrix.set(row, row + 1, 1.0)?;
150    //!             matrix.set(row + 1, row, 1.0)?;
151    //!         }
152    //!     }
153    //!     matrix.det()
154    //! })?;
155    //! assert!((determinant - 6.0).abs() <= 1e-12);
156    //!
157    //! let unsupported = try_with_stack_matrix!(8, |matrix| -> Result<f64, LaError> {
158    //!     matrix.det()
159    //! });
160    //! assert_matches!(
161    //!     unsupported,
162    //!     Err(LaError::UnsupportedDimension { requested: 8, max: 7, .. })
163    //! );
164    //! # Ok(())
165    //! # }
166    //! ```
167    //!
168    //! [`try_with_interval_matrix!`](crate::try_with_interval_matrix) similarly
169    //! dispatches dimensions 0 through
170    //! [`MAX_INTERVAL_MATRIX_DIM`](crate::MAX_INTERVAL_MATRIX_DIM) (7) to an
171    //! [`IntervalMatrix`](crate::IntervalMatrix). These macros are useful when
172    //! stable Rust cannot express a derived const dimension such as `D + 1`.
173    //! Larger dimensions produce [`LaError::UnsupportedDimension`](crate::LaError::UnsupportedDimension),
174    //! converted through `From<LaError>` into the closure's declared error type.
175    //! Dispatch preserves const-generic storage; it does not create a dynamically
176    //! sized matrix representation or limit dimensions chosen directly at compile time.
177    //!
178    //! # Storage, access, and errors
179    //!
180    //! [`Matrix<D>`](crate::Matrix) and [`Vector<D>`](crate::Vector) store
181    //! `[[f64; D]; D]` and `[f64; D]` inline. Constructors validate non-finite
182    //! inputs, and the types preserve that finite-storage invariant. Factorization
183    //! kernels therefore avoid a repeated O(D²) input scan; computed factor
184    //! matrices are still checked before becoming observable results.
185    //!
186    //! [`Matrix::as_rows`](crate::Matrix::as_rows) and
187    //! [`Vector::as_array`](crate::Vector::as_array) borrow validated backing
188    //! arrays. [`Matrix::into_rows`](crate::Matrix::into_rows) and
189    //! [`Vector::into_array`](crate::Vector::into_array) consume the value and
190    //! return owned fixed-size arrays.
191    //! [`Matrix::get`](crate::Matrix::get) returns `None` for invalid coordinates;
192    //! [`Matrix::try_get`](crate::Matrix::try_get) preserves them in a typed error.
193    //! [`Matrix::set`](crate::Matrix::set) checks coordinates and finiteness
194    //! before mutation. [`Matrix::norm_inf`](crate::Matrix::norm_inf) computes
195    //! the maximum absolute row sum.
196    //!
197    //! [`Vector::dot`](crate::Vector::dot), [`Vector::norm`](crate::Vector::norm),
198    //! and [`Vector::norm_squared`](crate::Vector::norm_squared) provide ordinary
199    //! vector reductions. [`ScalarWithErrorBound`](crate::ScalarWithErrorBound)
200    //! is the opaque result of the certified dot and affine-difference methods;
201    //! it exposes the estimate, absolute bound, and outward-rounded endpoints.
202    //! [`DeterminantWithErrorBound`](crate::DeterminantWithErrorBound) pairs a
203    //! direct determinant with its certified absolute bound. Use
204    //! [`Matrix::det_errbound`](crate::Matrix::det_errbound) for the bound alone.
205    //!
206    //! [`Interval`](crate::Interval) stores two finite ordered bounds and supports
207    //! point construction, outward-rounded subtraction, addition, multiplication,
208    //! negation, and square. [`IntervalMatrix`](crate::IntervalMatrix) stores
209    //! `[[Interval; D]; D]` inline and uses a fixed 128-entry stack workspace for
210    //! supported determinant dimensions. [`IntervalDeterminantSign`](crate::IntervalDeterminantSign)
211    //! distinguishes positive, negative, exact zero, and inconclusive evidence.
212    //!
213    //! Parse numerical thresholds through [`Tolerance::try_new`](crate::Tolerance::try_new).
214    //! [`LaError`](crate::LaError) and its reason/location enums are non-exhaustive;
215    //! use wildcard match arms and `..` for struct-style variants. In particular:
216    //!
217    //! - [`SingularityReason`](crate::SingularityReason) separates exact singularity
218    //!   from numerical rejection, retaining the [`FactorizationKind`](crate::FactorizationKind),
219    //!   observed pivot magnitude, and tolerance for the latter.
220    //! - [`NonFiniteOrigin`](crate::NonFiniteOrigin), [`NonFiniteLocation`](crate::NonFiniteLocation),
221    //!   and [`ArithmeticOperation`](crate::ArithmeticOperation) distinguish invalid
222    //!   inputs from computed non-finite values.
223    //! - [`LaError::InvertedInterval`](crate::LaError::InvertedInterval) preserves
224    //!   rejected finite endpoints; [`LaError::IntervalRangeExhausted`](crate::LaError::IntervalRangeExhausted)
225    //!   distinguishes finite-input interval range loss from a non-finite value.
226    //! - [`InvalidToleranceReason`](crate::InvalidToleranceReason) distinguishes
227    //!   negative and non-finite tolerances.
228    //! - [`PositiveSemidefiniteViolation`](crate::PositiveSemidefiniteViolation)
229    //!   distinguishes a negative LDLT pivot from a zero pivot with nonzero coupling.
230
231    pub mod ldlt {
232        //! LDLT determinants and exact symmetry.
233        //!
234        //! Compute a determinant for a symmetric positive-definite matrix via LDLT (no
235        //! pivoting).
236        //!
237        //! For these matrices, `LDLᵀ` is a square-root-free Cholesky form. Multiplying each
238        //! column of `L` by the square root of the corresponding diagonal entry yields a
239        //! Cholesky factor:
240        //!
241        //! ```rust
242        //! use la_stack::prelude::*;
243        //!
244        //! fn main() -> Result<(), LaError> {
245        //!     // This matrix is symmetric positive-definite (A = L*L^T) so LDLT works without pivoting.
246        //!     let a = Matrix::<5>::try_from_rows([
247        //!         [1.0, 1.0, 0.0, 0.0, 0.0],
248        //!         [1.0, 2.0, 1.0, 0.0, 0.0],
249        //!         [0.0, 1.0, 2.0, 1.0, 0.0],
250        //!         [0.0, 0.0, 1.0, 2.0, 1.0],
251        //!         [0.0, 0.0, 0.0, 1.0, 2.0],
252        //!     ])?;
253        //!
254        //!     let ldlt = match a.ldlt(DEFAULT_SINGULAR_TOL) {
255        //!         Ok(ldlt) => ldlt,
256        //!         Err(err @ LaError::Asymmetric {
257        //!             row,
258        //!             col,
259        //!             upper,
260        //!             lower,
261        //!             allowed_abs_diff,
262        //!             ..
263        //!         }) => {
264        //!             eprintln!(
265        //!                 "LDLT mismatch at ({row}, {col}): {upper} vs {lower} (allowed {allowed_abs_diff})"
266        //!             );
267        //!             return Err(err);
268        //!         }
269        //!         Err(err) => return Err(err),
270        //!     };
271        //!
272        //!     let det = ldlt.det()?;
273        //!     assert!((det - 1.0).abs() <= 1e-12);
274        //!
275        //!     Ok(())
276        //! }
277        //! ```
278        //!
279        //! > ⚠️ **LDLT invariant:** The input matrix must be **exactly symmetric**: every
280        //! > mirrored pair must compare equal (`+0.0 == -0.0` is accepted). Asymmetric
281        //! > inputs passed to
282        //! > [`Matrix::ldlt`](crate::Matrix::ldlt)
283        //! > return a typed `LaError::Asymmetric` containing both observed values and the
284        //! > required allowed difference of zero. The tolerance-based
285        //! > [`Matrix::first_asymmetry`](crate::Matrix::first_asymmetry)
286        //! > and [`Matrix::is_symmetric`](crate::Matrix::is_symmetric) methods remain useful diagnostics, but do not prove
287        //! > the exact precondition required by LDLT. Use `lu()` when exact symmetry or
288        //! > positive definiteness is not guaranteed. A negative LDLT diagonal or a zero
289        //! > diagonal with nonzero remaining coupling returns
290        //! > `LaError::NotPositiveSemidefinite` with a typed
291        //! > `PositiveSemidefiniteViolation`. An uncoupled zero or positive pivot
292        //! > at or below the caller's tolerance returns `LaError::Singular` with a
293        //! > numerical `SingularityReason`. Because these pivots are computed in binary64,
294        //! > success is not an exact positive-definiteness certificate for the stored
295        //! > matrix.
296    }
297
298    pub mod compile_time {
299        //! Compile-time determinants and dimension dispatch.
300        //!
301        //! [`det_direct()`](crate::Matrix::det_direct) is a `const fn` providing closed-form determinants for D=0–4,
302        //! using fused multiply-add where applicable. It returns `Ok(Some(det))` for those
303        //! dimensions and `Ok(None)` for D ≥ 5. `Matrix::<0>::zero().det_direct()` returns
304        //! `Ok(Some(1.0))` (the empty-product convention). For D=1–4, direct formulas
305        //! bypass LU factorization entirely. This enables compile-time evaluation when
306        //! inputs are known:
307        //!
308        //! ```rust
309        //! use la_stack::prelude::*;
310        //!
311        //! // Evaluated entirely at compile time — no runtime cost.
312        //! const DET: Result<Option<f64>, LaError> = match Matrix::<4>::try_from_rows([
313        //!     [2.0, 0.0, 0.0, 0.0],
314        //!     [0.0, 3.0, 0.0, 0.0],
315        //!     [0.0, 0.0, 5.0, 0.0],
316        //!     [0.0, 0.0, 0.0, 7.0],
317        //! ]) {
318        //!     Ok(matrix) => matrix.det_direct(),
319        //!     Err(err) => Err(err),
320        //! };
321        //!
322        //! fn main() -> Result<(), LaError> {
323        //!     assert_eq!(DET?, Some(210.0));
324        //!     Ok(())
325        //! }
326        //! ```
327        //!
328        //! The public `det()` method automatically dispatches through the closed-form path
329        //! for D ≤ 4 and falls back to zero-tolerance LU for D ≥ 5. Tiny nonzero
330        //! determinants are not flattened by a configured pivot tolerance. The LU fallback
331        //! returns `LaError::Singular` when floating-point elimination cannot produce a
332        //! non-zero pivot; it does not misreport that numerical failure as an exact zero.
333        //! Use `lu()` directly when you need a different tolerance policy, and use the
334        //! exact determinant APIs when exact singularity classification matters.
335    }
336
337    pub mod intervals {
338        //! Outward-rounded interval expressions and determinant signs.
339        //!
340        //! `Interval` encloses expression construction that has not yet been reduced to a
341        //! single stored `f64`. Point intervals preserve finite binary64 values exactly;
342        //! `try_from_subtraction`, `try_add`, `try_mul`, `negate`, and `try_square` enclose
343        //! the corresponding exact-real operations. [`IntervalMatrix<D>::det_sign()`](crate::IntervalMatrix::det_sign) then
344        //! uses a division-free subset expansion through D=7, returning positive,
345        //! negative, zero, or inconclusive evidence.
346        //!
347        //! ```rust
348        //! use la_stack::prelude::*;
349        //!
350        //! fn main() -> Result<(), LaError> {
351        //!     // Relative coordinates and the lifted norm retain their construction error.
352        //!     let x = Interval::try_from_subtraction(0.1, 0.0)?;
353        //!     let y = Interval::try_from_subtraction(0.1, 0.0)?;
354        //!     let z = Interval::try_from_subtraction(0.1, 0.0)?;
355        //!     let lifted = x
356        //!         .try_square()?
357        //!         .try_add(&y.try_square()?)?
358        //!         .try_add(&z.try_square()?)?;
359        //!
360        //!     let matrix = IntervalMatrix::<4>::from_rows([
361        //!         [Interval::ONE, Interval::ZERO, Interval::ZERO, Interval::ONE],
362        //!         [Interval::ZERO, Interval::ONE, Interval::ZERO, Interval::ONE],
363        //!         [Interval::ZERO, Interval::ZERO, Interval::ONE, Interval::ONE],
364        //!         [x, y, z, lifted],
365        //!     ]);
366        //!     assert_eq!(
367        //!         matrix.det_sign()?,
368        //!         IntervalDeterminantSign::Negative,
369        //!     );
370        //!     Ok(())
371        //! }
372        //! ```
373        //!
374        //! Every successful interval keeps finite ordered endpoints. Subnormal bounds are
375        //! preserved, both signed zeros are treated as real zero and canonicalized to
376        //! `+0.0`, and underflowed nonzero products widen toward the least subnormal value.
377        //! If an exact result range cannot fit between finite binary64 endpoints, the
378        //! operation returns `LaError::IntervalRangeExhausted` with its interval operation
379        //! recorded in `ArithmeticOperation`.
380        //!
381        //! `Positive`, `Negative`, and `Zero` are proofs. `Inconclusive` only means that
382        //! the determinant enclosure overlaps zero; it must not be converted to equality
383        //! or singularity. A filtered-exact caller should rebuild the same derived
384        //! expression with `RationalMatrix` and call `det_sign()` when the interval result
385        //! is inconclusive or reports range failure. Lifting a finished `Matrix` with
386        //! [`IntervalMatrix::from_matrix`](crate::IntervalMatrix::from_matrix) encloses its stored entries, but cannot recover
387        //! rounding that occurred while those entries were assembled.
388    }
389
390    pub mod norms {
391        //! Overflow-safe Euclidean norms and squared norms.
392        //!
393        //! `Vector::norm()` computes the Euclidean norm with a deterministic scaled
394        //! sum-of-squares recurrence, so large or subnormal finite coordinates do not fail
395        //! merely because their raw squares overflow or underflow. It returns positive zero
396        //! for empty and all-zero vectors and reports `LaError::NonFinite` with
397        //! `ArithmeticOperation::VectorNorm` only when the exact norm rounds to infinity.
398        //! Near the upper range, a fixed-size stack accumulator sums squares exactly and
399        //! compares squared rounding midpoints to prevent false or hidden overflow. This
400        //! fallback needs no optional dependencies. The general binary64 result remains
401        //! approximate and has no certified error bound.
402        //!
403        //! `Vector::norm_squared()` remains the direct left-to-right FMA sum of squares for
404        //! callers that need the squared norm. Its distinct contract deliberately reports
405        //! overflow when that square is not finite, even when `norm()` can return a finite
406        //! norm.
407        //!
408        //! # Large and subnormal coordinates
409        //!
410        //! ```rust
411        //! use core::assert_matches;
412        //!
413        //! use la_stack::prelude::*;
414        //!
415        //! # fn main() -> Result<(), LaError> {
416        //! let large = Vector::<5>::try_new([3e200, 4e200, 0.0, 0.0, 0.0])?;
417        //! assert!((large.norm()? / 1e200 - 5.0).abs() <= 1e-12);
418        //! assert_matches!(
419        //!     large.norm_squared(),
420        //!     Err(LaError::NonFinite {
421        //!         origin: NonFiniteOrigin::Computation {
422        //!             operation: ArithmeticOperation::VectorSquaredNorm, ..
423        //!         },
424        //!         location: NonFiniteLocation::Step { index: 0, .. },
425        //!         ..
426        //!     })
427        //! );
428        //!
429        //! let tiny = f64::from_bits(16);
430        //! let small = Vector::<5>::try_new([3.0 * tiny, 4.0 * tiny, 0.0, 0.0, 0.0])?;
431        //! assert_eq!(small.norm()?, 5.0 * tiny);
432        //! assert_eq!(small.norm_squared()?, 0.0); // The raw squares underflow.
433        //! # Ok(())
434        //! # }
435        //! ```
436        //!
437        //! The large-vector assertion uses a tolerance for this fixture, not a certified
438        //! error bound. The small vector shows why taking the square root of
439        //! `norm_squared()` can lose a representable nonzero norm.
440    }
441
442    pub mod certified {
443        //! Certified dot products and affine differences.
444        //!
445        //! `Vector::dot_with_errbound()` evaluates the same left-to-right FMA tree as
446        //! `Vector::dot()` and returns its estimate together with a certified absolute
447        //! roundoff bound. `Vector::dot_difference_with_errbound()` directly evaluates
448        //!
449        //! ```text
450        //! Σᵢ axis[i] × (left[i] - right[i])
451        //! ```
452        //!
453        //! as two FMAs per coordinate. It does not first round `left - right` into a new
454        //! `Vector`, so the certificate covers the intended expression over the original
455        //! stored binary64 coordinates.
456        //!
457        //! The opaque [`ScalarWithErrorBound`](crate::ScalarWithErrorBound) exposes the estimate, absolute error bound,
458        //! and finite outward-rounded lower and upper bounds. Those endpoints support
459        //! positive, negative, and caller-selected threshold proofs:
460        //!
461        //! # A certified dot-product sign
462        //!
463        //! ```rust
464        //! use la_stack::prelude::*;
465        //!
466        //! # fn main() -> Result<(), LaError> {
467        //! let axis = Vector::<5>::try_new([2.0, -1.0, 3.0, 1.0, -2.0])?;
468        //! let point = Vector::<5>::try_new([4.0, 1.0, 2.0, 3.0, 1.0])?;
469        //! let positive = axis.dot_with_errbound(&point)?.and_then(|value| {
470        //!     if value.lower_bound() > 0.0 {
471        //!         Some(true)
472        //!     } else if value.upper_bound() <= 0.0 {
473        //!         Some(false)
474        //!     } else {
475        //!         None // An enclosure overlapping zero is inconclusive.
476        //!     }
477        //! });
478        //! assert_eq!(positive, Some(true));
479        //! # Ok(())
480        //! # }
481        //! ```
482        //!
483        //! # An affine threshold test
484        //!
485        //! ```rust
486        //! use la_stack::prelude::*;
487        //!
488        //! fn is_separated<const D: usize>(
489        //!     axis: &Vector<D>,
490        //!     left: &Vector<D>,
491        //!     right: &Vector<D>,
492        //!     threshold: f64,
493        //! ) -> Result<Option<bool>, LaError> {
494        //!     let Some(value) = axis.dot_difference_with_errbound(left, right)? else {
495        //!         return Ok(None);
496        //!     };
497        //!     if value.lower_bound() > threshold {
498        //!         Ok(Some(true))
499        //!     } else if value.upper_bound() <= threshold {
500        //!         Ok(Some(false))
501        //!     } else {
502        //!         Ok(None)
503        //!     }
504        //! }
505        //!
506        //! # fn main() -> Result<(), LaError> {
507        //! let axis = Vector::<2>::try_new([2.0, -1.0])?;
508        //! let left = Vector::<2>::try_new([4.0, 1.0])?;
509        //! let right = Vector::<2>::try_new([1.0, 3.0])?;
510        //! assert_eq!(is_separated(&axis, &left, &right, 1.0)?, Some(true));
511        //! # Ok(())
512        //! # }
513        //! ```
514        //!
515        //! An interval that overlaps the threshold is inconclusive, not equal. Likewise,
516        //! `Ok(None)` means gradual underflow or proof-only range exhaustion prevented a
517        //! certificate. A filtered-exact caller should rebuild the same dot or affine
518        //! expression in `BigRational` (available through the `exact` feature) or another
519        //! exact backend. A `LaError::NonFinite` instead reports that the specified FMA
520        //! estimate itself overflowed. These certified bounds describe roundoff in a fixed
521        //! arithmetic tree; they are distinct from user-selected numerical tolerances.
522    }
523
524    pub mod adaptive {
525        //! Adaptive determinant filtering with certified bounds.
526        //!
527        //! [`det_direct_with_errbound()`](crate::Matrix::det_direct_with_errbound) returns a closed-form determinant together with
528        //! the conservative absolute error bound used by the fast filter, computed from
529        //! one call that evaluates the determinant once and computes its matching bound.
530        //! It returns `None` when a D ≤ 4 computation may be affected by gradual
531        //! underflow, as well as for unsupported D ≥ 5 dimensions.
532        //! It returns `LaError::NonFinite` if the determinant or bound computation
533        //! overflows to NaN or infinity.
534        //! This method does NOT require the `exact` feature — it uses pure f64 arithmetic
535        //! and is available by default. Use [`det_errbound()`](crate::Matrix::det_errbound) when only the bound is needed.
536        //! The paired API enables custom adaptive-precision logic for geometric predicates:
537        //!
538        //! ```rust
539        //! use la_stack::prelude::*;
540        //!
541        //! # fn main() -> Result<(), LaError> {
542        //! let matrix = Matrix::<3>::identity();
543        //! let sign = matrix.det_direct_with_errbound()?.and_then(|value| {
544        //!     if value.determinant() > value.absolute_error_bound() {
545        //!         Some(1)
546        //!     } else if -value.determinant() > value.absolute_error_bound() {
547        //!         Some(-1)
548        //!     } else {
549        //!         None // The bound cannot establish a sign.
550        //!     }
551        //! });
552        //! assert_eq!(sign, Some(1));
553        //! # Ok(())
554        //! # }
555        //! ```
556        //!
557        //! With the `exact` feature, `Matrix::det_sign_exact()`
558        //! already handles filtering and exact fallback. The
559        //! [custom adaptive example](https://docs.rs/la-stack/latest/la_stack/guide/exact/index.html#adaptive-determinant-filtering)
560        //! shows positive, singular, and overflowing filter cases. It requires `exact`.
561        //!
562        //! The error coefficients (`ERR_COEFF_2`, `ERR_COEFF_3`, `ERR_COEFF_4`) are
563        //! conservative, dimension-specific constants, not caller-tunable tolerances. The
564        //! [mathematical basis](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/mathematical_basis.md#determinants-and-certified-sign-filtering)
565        //! documents the bound and states its range preconditions. The constants are explicit
566        //! crate-root exports for advanced users who want to compose the same bound:
567        //! `use la_stack::{ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4};`. They intentionally stay
568        //! out of the common prelude.
569    }
570
571    #[cfg(feature = "exact")]
572    pub mod exact {
573        //! Exact arithmetic over stored binary64 and rational inputs.
574        //!
575        //! The default build has **zero runtime dependencies**. Enable the optional
576        //! `exact` Cargo feature to add exact arithmetic methods using arbitrary-precision
577        //! rationals (this pulls in `num-bigint`, `num-rational`, and `num-traits` for
578        //! `BigRational`):
579        //!
580        //! See the [crate-level installation instructions](crate) for Cargo configuration.
581        //!
582        //! The feature exposes two deliberate input domains:
583        //!
584        //! - `Matrix<D>` / `Vector<D>` store finite binary64 inputs. Their exact methods
585        //!   treat each stored bit pattern as its exact rational value, so the determinant
586        //!   or solve stage introduces no further roundoff. They cannot recover information
587        //!   already lost before construction.
588        //! - `RationalMatrix<D>` / `RationalVector<D>` accept coefficients already
589        //!   assembled as `BigRational`. They preserve derived differences, squared norms,
590        //!   affine coefficients, and other rational expressions without an intermediate
591        //!   `f64` conversion.
592        //!
593        //! **Determinants:**
594        //!
595        //! - **`det_exact()`** — returns the exact determinant as a `BigRational`
596        //! - **`det_exact_f64()`** — returns the exact determinant as `f64` only when
597        //!   it is exactly representable (or `LaError::Unrepresentable` otherwise)
598        //! - **`det_exact_rounded_f64()`** — returns the exact determinant rounded to a
599        //!   finite `f64` using IEEE 754 round-to-nearest, ties-to-even
600        //! - **`det_sign_exact()`** — infallibly returns the provably correct
601        //!   `DeterminantSign` variant (`Negative`, `Zero`, or `Positive`)
602        //!
603        //! **Linear system solve:**
604        //!
605        //! - **`solve_exact(b)`** — solves `Ax = b` exactly, returning a
606        //!   `RationalVector<D>`
607        //! - **`solve_exact_f64(b)`** — solves `Ax = b` exactly, returning `Vector<D>` only when
608        //!   every component is exactly representable as `f64`
609        //! - **`solve_exact_rounded_f64(b)`** — solves `Ax = b` exactly, returning each
610        //!   component rounded to finite `f64` using IEEE 754 round-to-nearest,
611        //!   ties-to-even
612        //! - **`ExactF64Conversion`** — converts an existing exact determinant or solution
613        //!   under the strict or rounded contract without repeating exact elimination
614        //!
615        //! **Already-exact rational input:**
616        //!
617        //! - **`RationalMatrix::det_sign()`** — returns the exact sign without constructing
618        //!   a rational determinant
619        //! - **`RationalMatrix::det()`** — returns the exact `BigRational` determinant
620        //! - **`RationalMatrix::solve(&rhs)`** — returns a `RationalVector<D>` exact
621        //!   solution
622        //! - **`try_with_rational_matrix!`** — dispatches a runtime-selected dimension
623        //!   through D=8 to a const-generic rational matrix on stable Rust
624        //!
625        //! The `Matrix::det_exact*` value and conversion methods return
626        //! `LaError::DeterminantScaleOverflow` if their aggregate power-of-two scaling
627        //! exceeds the internal exponent representation. `RationalMatrix::det()` is
628        //! infallible because it clears rational row denominators without an exponent-scale
629        //! conversion. The exact solve methods for both input domains return
630        //! `LaError::Singular` with `SingularityReason::Exact` when the stored matrix is
631        //! exactly singular.
632        //!
633        //! For exact-to-f64 output, strict conversions use
634        //! `UnrepresentableReason::RequiresRounding` when explicit rounding can produce a
635        //! finite value and `UnrepresentableReason::NotFinite` otherwise. Rounded
636        //! conversions opt into nearest-even rounding but still report `NotFinite` when no
637        //! finite `f64` exists.
638        //!
639        //! # Preserving rational inputs
640        //!
641        //! The following 5×5 system has exact determinant 2^-60. Its exact rational inputs
642        //! therefore produce a unique solution through the general Bareiss path. Supplying
643        //! the same coefficients as `f64` inputs loses the `2^-60` perturbation at `1.0`,
644        //! making the leading rows identical and the binary64 system singular.
645        //!
646        //! ```rust
647        //! use core::assert_matches;
648        //!
649        //! use la_stack::prelude::*;
650        //!
651        //! fn main() -> Result<(), LaError> {
652        //!     // This is far below one binary64 ULP at 1.0, so 1.0 + 2^-60 rounds to 1.0.
653        //!     let epsilon = BigRational::new(1.into(), (1_u64 << 60).into());
654        //!     let one = BigRational::from_integer(1.into());
655        //!     let zero = BigRational::from_integer(0.into());
656        //!
657        //!     // The leading block is [[1, 1], [1, 1 + 2^-60]]. The remaining diagonal
658        //!     // extends the example to D=5, where the general Bareiss path is used.
659        //!     let matrix = RationalMatrix::<5>::try_from_fn(|row, col| match (row, col) {
660        //!         (0, 0 | 1) | (1, 0) => one.clone(),
661        //!         (1, 1) => &one + &epsilon,
662        //!         _ if row == col => one.clone(),
663        //!         _ => zero.clone(),
664        //!     })?;
665        //!     assert_eq!(matrix.det_sign(), DeterminantSign::Positive);
666        //!     assert_eq!(matrix.det(), epsilon);
667        //!
668        //!     let rhs = RationalVector::try_new([
669        //!         zero,
670        //!         -&epsilon,
671        //!         BigRational::from_integer(2.into()),
672        //!         BigRational::from_integer(3.into()),
673        //!         BigRational::from_integer(4.into()),
674        //!     ])?;
675        //!     let exact_solution = matrix.solve(&rhs)?;
676        //!     assert_eq!(
677        //!         exact_solution.as_array(),
678        //!         &[
679        //!             BigRational::from_integer(1.into()),
680        //!             BigRational::from_integer((-1).into()),
681        //!             BigRational::from_integer(2.into()),
682        //!             BigRational::from_integer(3.into()),
683        //!             BigRational::from_integer(4.into()),
684        //!         ]
685        //!     );
686        //!
687        //!     // Supplying the same coefficients as f64 inputs destroys the perturbation
688        //!     // and makes the matrix singular, even though the exact solution is integral.
689        //!     let epsilon_f64 = epsilon.try_to_f64()?;
690        //!     assert_eq!((1.0 + epsilon_f64).to_bits(), 1.0_f64.to_bits());
691        //!     let f64_matrix = Matrix::<5>::try_from_rows([
692        //!         [1.0, 1.0, 0.0, 0.0, 0.0],
693        //!         [1.0, 1.0 + epsilon_f64, 0.0, 0.0, 0.0],
694        //!         [0.0, 0.0, 1.0, 0.0, 0.0],
695        //!         [0.0, 0.0, 0.0, 1.0, 0.0],
696        //!         [0.0, 0.0, 0.0, 0.0, 1.0],
697        //!     ])?;
698        //!     let f64_rhs = Vector::<5>::try_new([0.0, -epsilon_f64, 2.0, 3.0, 4.0])?;
699        //!     let f64_solve = f64_matrix
700        //!         .lu(DEFAULT_SINGULAR_TOL)
701        //!         .and_then(|lu| lu.solve(f64_rhs));
702        //!     assert_matches!(
703        //!         f64_solve,
704        //!         Err(LaError::Singular { .. })
705        //!     );
706        //!     Ok(())
707        //! }
708        //! ```
709        //!
710        //! # Stored binary64 inputs and output conversion
711        //!
712        //! ```rust
713        //! use la_stack::prelude::*;
714        //!
715        //! fn main() -> Result<(), LaError> {
716        //!     // Exact determinant
717        //!     let m = Matrix::<3>::try_from_rows([
718        //!         [1.0, 2.0, 3.0],
719        //!         [4.0, 5.0, 6.0],
720        //!         [7.0, 8.0, 9.0],
721        //!     ])?;
722        //!     assert_eq!(m.det_sign_exact(), DeterminantSign::Zero); // exactly singular
723        //!
724        //!     let det = m.det_exact()?;
725        //!     assert_eq!(det, BigRational::from_integer(0.into())); // exact zero
726        //!     let det_f64 = det.try_to_f64()?;
727        //!     assert_eq!(det_f64, 0.0);
728        //!
729        //!     // If strict exact-to-f64 conversion would require rounding, opt in
730        //!     // explicitly with the rounded API.
731        //!     let inexact = Matrix::<2>::try_from_rows([
732        //!         [1.0 + f64::EPSILON, 0.0],
733        //!         [0.0, 1.0 - f64::EPSILON],
734        //!     ])?;
735        //!     let exact_det = inexact.det_exact()?;
736        //!     let rounded_det = match exact_det.try_to_f64() {
737        //!         Ok(det) => det,
738        //!         Err(err) if err.requires_rounding() => exact_det.to_rounded_f64()?,
739        //!         Err(err) => return Err(err),
740        //!     };
741        //!     assert_eq!(rounded_det.to_bits(), 1.0f64.to_bits());
742        //!
743        //!     // If the exact determinant cannot fit in f64, keep the BigRational value.
744        //!     let big = f64::MAX / 2.0;
745        //!     let huge = Matrix::<3>::try_from_rows([
746        //!         [0.0, 0.0, 1.0],
747        //!         [big, 0.0, 1.0],
748        //!         [0.0, big, 1.0],
749        //!     ])?;
750        //!     let huge_det = huge.det_exact()?;
751        //!     assert_eq!(
752        //!         huge_det
753        //!             .try_to_f64()
754        //!             .err()
755        //!             .and_then(|err| err.unrepresentable_reason()),
756        //!         Some(UnrepresentableReason::NotFinite)
757        //!     );
758        //!     println!("exact determinant = {huge_det}");
759        //!
760        //!     // Exact linear system solve
761        //!     let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
762        //!     let b = Vector::<2>::try_new([5.0, 11.0])?;
763        //!     let exact_x = a.solve_exact(b)?;
764        //!     let x = exact_x.try_to_f64()?.into_array();
765        //!     assert!((x[0] - 1.0).abs() <= f64::EPSILON);
766        //!     assert!((x[1] - 2.0).abs() <= f64::EPSILON);
767        //!
768        //!     Ok(())
769        //! }
770        //! ```
771        //!
772        //! With the `exact` feature enabled, `RationalMatrix`, `RationalVector`,
773        //! `DeterminantSign`, `ExactF64Conversion`, `BigInt`, and `BigRational` are
774        //! re-exported from the crate root and prelude,
775        //! alongside the most commonly needed `num-traits` items (`FromPrimitive`,
776        //! `ToPrimitive`, `Signed`). This lets consumers construct exact values
777        //! (`BigRational::from_f64`, `from_i64`), query sign (`is_positive` /
778        //! `is_negative`), and convert back to `f64` (`try_to_f64`, `to_rounded_f64`, or
779        //! the raw `to_f64`) with a single
780        //! `use la_stack::prelude::*;` — no need to add `num-bigint`, `num-rational`,
781        //! or `num-traits` to their own `Cargo.toml`. Use
782        //! `DeterminantSign::as_i8()` only when numeric −1/0/+1 interoperability is
783        //! required.
784        //!
785        //! For `det_sign_exact()`, D ≤ 4 matrices first use a fast f64 filter
786        //! (error-bounded [`det_direct_with_errbound()`](crate::Matrix::det_direct_with_errbound)) when its rounded intermediates stay in the normal
787        //! range or are exact structural zeros. An inconclusive filter falls back to the
788        //! same direct determinant expansion in `BigInt`. D ≥ 5 skips the closed-form
789        //! filter and uses fraction-free Bareiss elimination in `BigInt`.
790        //! Because `Matrix` stores only finite entries, arithmetic range failures in the
791        //! filter are inconclusive rather than errors and the exact fallback is total.
792        //!
793        //! # A five-dimensional rational solve
794        //!
795        //! ```rust
796        //! use core::assert_matches;
797        //!
798        //! use la_stack::prelude::*;
799        //!
800        //! # fn main() -> Result<(), LaError> {
801        //! // A tridiagonal exact matrix with determinant 6.
802        //! let matrix = RationalMatrix::<5>::try_from_fn(|row, col| {
803        //!     BigRational::from_integer(if row == col {
804        //!         2.into()
805        //!     } else if row.abs_diff(col) == 1 {
806        //!         1.into()
807        //!     } else {
808        //!         0.into()
809        //!     })
810        //! })?;
811        //! let numerators = [4, 8, 12, 16, 14];
812        //! let rhs = RationalVector::try_from_fn(|row| {
813        //!     BigRational::new(numerators[row].into(), 3.into())
814        //! })?;
815        //! let solution = matrix.solve(&rhs)?;
816        //! let expected = [1, 2, 3, 4, 5].map(|n| BigRational::new(n.into(), 3.into()));
817        //! assert_eq!(solution.as_array(), &expected);
818        //!
819        //! // Keep the exact solution until the caller explicitly opts into rounding.
820        //! assert_matches!(
821        //!     solution.try_to_f64(),
822        //!     Err(LaError::Unrepresentable {
823        //!         index: Some(0),
824        //!         reason: UnrepresentableReason::RequiresRounding,
825        //!         ..
826        //!     })
827        //! );
828        //! let rounded = solution.to_rounded_f64()?;
829        //! assert_eq!(rounded.as_array(), &[1.0 / 3.0, 2.0 / 3.0, 1.0, 4.0 / 3.0, 5.0 / 3.0]);
830        //! # Ok(())
831        //! # }
832        //! ```
833        //!
834        //! # Rational dimension dispatch
835        //!
836        //! [`try_with_rational_matrix!`](crate::try_with_rational_matrix) selects a
837        //! concrete `RationalMatrix<N>` for dimensions 0 through
838        //! [`MAX_RATIONAL_MATRIX_DISPATCH_DIM`](crate::MAX_RATIONAL_MATRIX_DISPATCH_DIM)
839        //! (8). Larger dimensions return [`LaError::UnsupportedDimension`](crate::LaError::UnsupportedDimension),
840        //! converted through `From<LaError>` into the closure's declared error type.
841        //! The macro preserves the const-generic representation.
842        //!
843        //! Rational constructors include `try_from_rows` / `try_new` and
844        //! `try_from_fn`; `as_rows` / `as_array` and `get` borrow their stored
845        //! values, while `into_rows` / `into_array` return the owned arrays.
846        //!
847        //! # Adaptive determinant filtering
848        //!
849        //! This example requires `exact` and illustrates a custom filter with exact
850        //! fallback. Use [`Matrix::det_sign_exact`](crate::Matrix::det_sign_exact) directly
851        //! when no custom filtering policy is needed.
852        //!
853        //! ```rust
854        //! use la_stack::prelude::*;
855        //!
856        //! fn adaptive_det_sign<const D: usize>(
857        //!     matrix: &Matrix<D>,
858        //! ) -> DeterminantSign {
859        //!     if let Ok(Some(estimate)) = matrix.det_direct_with_errbound() {
860        //!         if estimate.determinant().abs() > estimate.absolute_error_bound() {
861        //!             return if estimate.determinant() > 0.0 {
862        //!                 DeterminantSign::Positive
863        //!             } else {
864        //!                 DeterminantSign::Negative
865        //!             };
866        //!         }
867        //!     }
868        //!
869        //!     matrix.det_sign_exact()
870        //! }
871        //!
872        //! fn main() -> Result<(), LaError> {
873        //!     let identity = Matrix::<3>::identity();
874        //!     assert_eq!(
875        //!         adaptive_det_sign(&identity),
876        //!         DeterminantSign::Positive
877        //!     );
878        //!
879        //!     // A zero determinant cannot pass the f64 sign filter, so this exercises
880        //!     // the exact fallback.
881        //!     let singular = Matrix::<3>::try_from_rows([
882        //!         [1.0, 2.0, 3.0],
883        //!         [4.0, 5.0, 6.0],
884        //!         [7.0, 8.0, 9.0],
885        //!     ])?;
886        //!     assert_eq!(adaptive_det_sign(&singular), DeterminantSign::Zero);
887        //!
888        //!     // The f64 filter overflows for this finite matrix, but the exact fallback
889        //!     // still resolves its positive determinant sign.
890        //!     let big = f64::MAX / 2.0;
891        //!     let overflowing = Matrix::<3>::try_from_rows([
892        //!         [0.0, 0.0, 1.0],
893        //!         [big, 0.0, 1.0],
894        //!         [0.0, big, 1.0],
895        //!     ])?;
896        //!     assert_eq!(
897        //!         adaptive_det_sign(&overflowing),
898        //!         DeterminantSign::Positive
899        //!     );
900        //!
901        //!     Ok(())
902        //! }
903        //! ```
904    }
905}
906mod angle;
907mod error;
908#[cfg(feature = "exact")]
909mod exact;
910mod gram;
911mod interval;
912mod ldlt;
913mod lu;
914mod matrix;
915mod norm;
916#[cfg(feature = "exact")]
917mod rational;
918mod rounding;
919mod scaled_product;
920mod tolerance;
921mod vector;
922
923#[cfg(feature = "exact")]
924#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
925pub use exact::{DeterminantSign, ExactF64Conversion};
926#[cfg(feature = "exact")]
927#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
928pub use num_bigint::BigInt;
929#[cfg(feature = "exact")]
930#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
931pub use num_rational::BigRational;
932#[cfg(feature = "exact")]
933#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
934pub use num_traits::{FromPrimitive, Signed, ToPrimitive};
935#[cfg(feature = "exact")]
936#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
937pub use rational::{RationalMatrix, RationalVector};
938
939// ---------------------------------------------------------------------------
940// Error-bound constants for `Matrix::det_direct_with_errbound()` and
941// `Matrix::det_errbound()`.
942//
943// For `D ∈ {2, 3, 4}`, `Matrix::det_direct()` evaluates the Leibniz expansion
944// of the determinant as a tree of f64 multiplies and fused multiply-adds
945// (FMAs).  When every rounded intermediate is normal or an exact structural
946// zero, Shewchuk's error-analysis methodology (REFERENCES.md [8]) bounds the
947// absolute error of that computation by
948//
949//     |det_direct(A) - det_exact(A)|  ≤  ERR_COEFF_D · p(|A|)
950//
951// where `p(|A|)` is the **absolute Leibniz sum**
952//
953//     p(|A|) = Σ_σ ∏ᵢ |A[i, σ(i)]|,
954//
955// i.e. exactly the combinatorial matrix permanent `perm(|A|)`. The
956// implementation evaluates the corresponding fixed-size expansion in f64, so
957// the computed `permanent` value used by the bound may itself be rounded even
958// though the mathematical quantity above is exact.
959//
960// The longest rounding paths in the determinant and permanent have lengths
961// k = 2, 5, and 9. With EPS = 2^-52, gamma_k = k*EPS / (1 - k*EPS) bounds
962// their accumulated relative perturbations. Each exactly representable
963// coefficient satisfies c >= gamma_k / ((1 - EPS)*(1 - gamma_k)), including
964// possible downward rounding of both the permanent and the final product.
965// See docs/mathematical_basis.md, "Derivation of the returned determinant
966// bound", for the full argument and the underflow assumptions.
967//
968// These constants are NOT feature-gated — they rely only on f64 arithmetic
969// and are useful for adaptive-precision logic even without the `exact`
970// feature. Most callers should prefer `Matrix::det_direct_with_errbound()`
971// when they need the approximation and bound together, or
972// `Matrix::det_errbound()` when they need only the bound. Those methods apply
973// these constants to the actual matrix; the raw constants are
974// exposed for advanced use cases (composing the bound with a pre-reduced
975// permanent, rolling a custom adaptive filter, etc.).  See
976// `Matrix::det_sign_exact()` (behind the `exact` feature) for the
977// reference adaptive-filter that consumes these internally.
978// ---------------------------------------------------------------------------
979
980const EPS: f64 = f64::EPSILON; // 2^-52
981
982/// Absolute error coefficient for [`Matrix::<2>::det_direct`](crate::Matrix::det_direct).
983///
984/// This constant is not a caller-tuned tolerance. It is the dimension-specific
985/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
986/// bound on floating-point roundoff in the closed-form 2×2 determinant formula.
987///
988/// For a 2×2 matrix `A = [[a, b], [c, d]]` whose closed-form determinant
989/// intermediates do not undergo gradual underflow,
990///
991/// ```text
992/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_2 · (|a·d| + |b·c|)
993/// ```
994///
995/// `det_direct` evaluates `a·d - b·c` as one multiply followed by one FMA
996/// (2 rounding events on the longest path). The permanent also has a
997/// two-event path. The coefficient covers both trees and the final rounded
998/// multiplication; see the
999/// [returned-bound derivation](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#derivation-of-the-returned-determinant-bound).
1000/// The analysis follows Shewchuk's framework and the binary64 arithmetic
1001/// model; see `REFERENCES.md` \[8-11\].
1002///
1003/// Prefer
1004/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1005/// unless you need only the bound or already have the absolute-Leibniz sum;
1006/// see
1007/// `Matrix::det_sign_exact` (under the `exact` feature) for the reference
1008/// adaptive-precision filter.
1009///
1010/// # Example
1011/// ```
1012/// use la_stack::{prelude::*, ERR_COEFF_2};
1013///
1014/// # fn main() -> Result<(), LaError> {
1015/// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1016/// let det = m.det_direct()?;
1017/// assert_eq!(det, Some(-2.0));
1018/// // Compute the bound from the raw constant for illustration; most
1019/// // callers would match on `m.det_errbound()?` instead.
1020/// let p = (1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs();
1021/// let bound = ERR_COEFF_2 * p;
1022/// // The f64 sign is provably correct without exact arithmetic.
1023/// assert_eq!(det.map(|value| value.abs() > bound), Some(true));
1024/// # Ok(())
1025/// # }
1026/// ```
1027pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
1028
1029/// Absolute error coefficient for [`Matrix::<3>::det_direct`](crate::Matrix::det_direct).
1030///
1031/// This constant is not a caller-tuned tolerance. It is the dimension-specific
1032/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
1033/// bound on floating-point roundoff in the closed-form 3×3 determinant formula.
1034///
1035/// For a 3×3 matrix `A` whose closed-form determinant intermediates do not
1036/// undergo gradual underflow,
1037///
1038/// ```text
1039/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_3 · p(|A|)
1040/// ```
1041///
1042/// where `p(|A|)` is the absolute Leibniz sum (the same cofactor
1043/// expansion as `det_direct` but with `|·|` at every leaf).
1044/// `det_direct` for D=3 uses three 2×2 FMA minors combined by a nested
1045/// FMA. The determinant and permanent each have at most five rounding events
1046/// per monomial; `8·EPS + 64·EPS²` also covers rounding the permanent and final
1047/// bound product. See [`ERR_COEFF_2`] for the derivation link and
1048/// `REFERENCES.md` \[8-11\] for the analysis framework.
1049///
1050/// Prefer
1051/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1052/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
1053/// example.
1054pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
1055
1056/// Absolute error coefficient for [`Matrix::<4>::det_direct`](crate::Matrix::det_direct).
1057///
1058/// This constant is not a caller-tuned tolerance. It is the dimension-specific
1059/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
1060/// bound on floating-point roundoff in the closed-form 4×4 determinant formula.
1061///
1062/// For a 4×4 matrix `A` whose closed-form determinant intermediates do not
1063/// undergo gradual underflow,
1064///
1065/// ```text
1066/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_4 · p(|A|)
1067/// ```
1068///
1069/// where `p(|A|)` is the absolute Leibniz sum. `det_direct` for D=4
1070/// evaluates four nested 3×3 cofactors, sharing their six 2×2 minors when
1071/// every coefficient in the first two rows is non-zero, and reduces them with
1072/// an FMA row combination. Dense and sparse trees each have at most nine
1073/// rounding events per monomial, as does the permanent tree. The coefficient
1074/// `12·EPS + 128·EPS²` also covers rounding the permanent and final bound
1075/// product. See [`ERR_COEFF_2`] for the derivation link and `REFERENCES.md`
1076/// \[8-11\] for the analysis framework.
1077///
1078/// Prefer
1079/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1080/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
1081/// example.
1082pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
1083
1084/// Largest dimension supported by [`try_with_stack_matrix!`].
1085///
1086/// The crate can represent `Matrix<D>` for any compile-time `D`, but runtime
1087/// dispatch must enumerate a finite set of concrete stack types.  Dimensions
1088/// `0..=7` cover downstream geometric predicate matrices while keeping the
1089/// dispatch surface explicit.
1090pub const MAX_STACK_MATRIX_DISPATCH_DIM: usize = 7;
1091
1092/// Largest dimension supported by [`try_with_rational_matrix!`].
1093///
1094/// This bound covers exact downstream geometric systems through dimension 8
1095/// while keeping runtime-to-const dispatch explicit and available on stable
1096/// Rust.
1097#[cfg(feature = "exact")]
1098#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1099pub const MAX_RATIONAL_MATRIX_DISPATCH_DIM: usize = 8;
1100
1101pub use angle::VectorAngle;
1102pub use error::{
1103    ArithmeticOperation, FactorizationKind, IntervalBound, IntervalOperand, InvalidToleranceReason,
1104    LaError, NonFiniteLocation, NonFiniteOrigin, PositiveSemidefiniteViolation, SingularityReason,
1105    UnrepresentableReason, VectorOperand,
1106};
1107pub use gram::gram_matrix;
1108pub use interval::{Interval, IntervalDeterminantSign, IntervalMatrix, MAX_INTERVAL_MATRIX_DIM};
1109pub use ldlt::Ldlt;
1110pub use lu::Lu;
1111pub use matrix::{DeterminantWithErrorBound, Matrix};
1112pub use tolerance::{DEFAULT_SINGULAR_TOL, Tolerance};
1113pub use vector::{ScalarWithErrorBound, Vector};
1114
1115/// Fallibly dispatch a runtime dimension to a concrete stack-allocated matrix.
1116///
1117/// The macro creates a zero matrix with type `Matrix<N>` for the selected
1118/// runtime dimension `N`, then evaluates the supplied closure body.  Supported
1119/// runtime dimensions run from `0` through [`MAX_STACK_MATRIX_DISPATCH_DIM`].
1120/// The body may mutate or consume captured values. It is not evaluated for
1121/// unsupported dimensions.
1122/// Unsupported dimensions return
1123/// `Err(LaError::UnsupportedDimension { requested, max })` converted with
1124/// `From<LaError>`, so downstream crates can use their own public error type.
1125///
1126/// # Errors
1127/// Returns [`LaError::UnsupportedDimension`] (converted through `From<LaError>`)
1128/// when the requested runtime dimension is greater than
1129/// [`MAX_STACK_MATRIX_DISPATCH_DIM`].  The closure body may return any other
1130/// error representable by its declared `Result` type.
1131///
1132/// # Examples
1133/// ```
1134/// use la_stack::prelude::*;
1135///
1136/// # fn main() -> Result<(), LaError> {
1137/// let requested = 2usize;
1138/// let det = try_with_stack_matrix!(requested, |mut m| -> Result<f64, LaError> {
1139///     m.set(0, 0, 1.0)?;
1140///     m.set(1, 1, 1.0)?;
1141///     m.det()
1142/// })?;
1143///
1144/// assert_eq!(det, 1.0);
1145/// # Ok(())
1146/// # }
1147/// ```
1148#[macro_export]
1149macro_rules! try_with_stack_matrix {
1150    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1151        let __la_stack_requested_dim: usize = $dim;
1152        match __la_stack_requested_dim {
1153            0 => $crate::try_with_stack_matrix!(@arm 0, $matrix, $ret, $body),
1154            1 => $crate::try_with_stack_matrix!(@arm 1, $matrix, $ret, $body),
1155            2 => $crate::try_with_stack_matrix!(@arm 2, $matrix, $ret, $body),
1156            3 => $crate::try_with_stack_matrix!(@arm 3, $matrix, $ret, $body),
1157            4 => $crate::try_with_stack_matrix!(@arm 4, $matrix, $ret, $body),
1158            5 => $crate::try_with_stack_matrix!(@arm 5, $matrix, $ret, $body),
1159            6 => $crate::try_with_stack_matrix!(@arm 6, $matrix, $ret, $body),
1160            7 => $crate::try_with_stack_matrix!(@arm 7, $matrix, $ret, $body),
1161            requested => Err(::core::convert::From::from(
1162                $crate::LaError::unsupported_dimension(
1163                    requested,
1164                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
1165                ),
1166            )),
1167        }
1168    }};
1169    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1170        let __la_stack_requested_dim: usize = $dim;
1171        match __la_stack_requested_dim {
1172            0 => $crate::try_with_stack_matrix!(@arm_mut 0, $matrix, $ret, $body),
1173            1 => $crate::try_with_stack_matrix!(@arm_mut 1, $matrix, $ret, $body),
1174            2 => $crate::try_with_stack_matrix!(@arm_mut 2, $matrix, $ret, $body),
1175            3 => $crate::try_with_stack_matrix!(@arm_mut 3, $matrix, $ret, $body),
1176            4 => $crate::try_with_stack_matrix!(@arm_mut 4, $matrix, $ret, $body),
1177            5 => $crate::try_with_stack_matrix!(@arm_mut 5, $matrix, $ret, $body),
1178            6 => $crate::try_with_stack_matrix!(@arm_mut 6, $matrix, $ret, $body),
1179            7 => $crate::try_with_stack_matrix!(@arm_mut 7, $matrix, $ret, $body),
1180            requested => Err(::core::convert::From::from(
1181                $crate::LaError::unsupported_dimension(
1182                    requested,
1183                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
1184                ),
1185            )),
1186        }
1187    }};
1188    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1189        let mut __la_stack_body = |$matrix: $crate::Matrix<$d>| -> $ret { $body };
1190        __la_stack_body($crate::Matrix::<$d>::zero())
1191    }};
1192    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1193        let mut __la_stack_body = |mut $matrix: $crate::Matrix<$d>| -> $ret { $body };
1194        __la_stack_body($crate::Matrix::<$d>::zero())
1195    }};
1196}
1197
1198/// Fallibly dispatch a runtime dimension to a concrete interval matrix.
1199///
1200/// The macro creates a zero [`IntervalMatrix`] with the selected const-generic
1201/// dimension, then evaluates the closure body. Supported dimensions run from
1202/// `0` through [`MAX_INTERVAL_MATRIX_DIM`]. Unsupported dimensions return
1203/// [`LaError::UnsupportedDimension`] converted through `From<LaError>`.
1204/// The body may mutate or consume captured values. It is not evaluated for
1205/// unsupported dimensions.
1206///
1207/// # Errors
1208/// Returns [`LaError::UnsupportedDimension`] (converted through
1209/// `From<LaError>`) when the requested dimension is greater than
1210/// [`MAX_INTERVAL_MATRIX_DIM`]. The closure body may return any other error
1211/// representable by its declared `Result` type.
1212///
1213/// # Examples
1214/// ```
1215/// use la_stack::prelude::*;
1216///
1217/// # fn main() -> Result<(), LaError> {
1218/// let requested = 3usize;
1219/// let sign = try_with_interval_matrix!(requested, |mut matrix| -> Result<
1220///     IntervalDeterminantSign,
1221///     LaError,
1222/// > {
1223///     for index in 0..requested {
1224///         matrix.set(index, index, Interval::ONE)?;
1225///     }
1226///     matrix.det_sign()
1227/// })?;
1228/// assert_eq!(sign, IntervalDeterminantSign::Positive);
1229/// # Ok(())
1230/// # }
1231/// ```
1232#[macro_export]
1233macro_rules! try_with_interval_matrix {
1234    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1235        let __la_stack_requested_dim: usize = $dim;
1236        match __la_stack_requested_dim {
1237            0 => $crate::try_with_interval_matrix!(@arm 0, $matrix, $ret, $body),
1238            1 => $crate::try_with_interval_matrix!(@arm 1, $matrix, $ret, $body),
1239            2 => $crate::try_with_interval_matrix!(@arm 2, $matrix, $ret, $body),
1240            3 => $crate::try_with_interval_matrix!(@arm 3, $matrix, $ret, $body),
1241            4 => $crate::try_with_interval_matrix!(@arm 4, $matrix, $ret, $body),
1242            5 => $crate::try_with_interval_matrix!(@arm 5, $matrix, $ret, $body),
1243            6 => $crate::try_with_interval_matrix!(@arm 6, $matrix, $ret, $body),
1244            7 => $crate::try_with_interval_matrix!(@arm 7, $matrix, $ret, $body),
1245            requested => Err(::core::convert::From::from(
1246                $crate::LaError::unsupported_dimension(
1247                    requested,
1248                    $crate::MAX_INTERVAL_MATRIX_DIM,
1249                ),
1250            )),
1251        }
1252    }};
1253    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1254        let __la_stack_requested_dim: usize = $dim;
1255        match __la_stack_requested_dim {
1256            0 => $crate::try_with_interval_matrix!(@arm_mut 0, $matrix, $ret, $body),
1257            1 => $crate::try_with_interval_matrix!(@arm_mut 1, $matrix, $ret, $body),
1258            2 => $crate::try_with_interval_matrix!(@arm_mut 2, $matrix, $ret, $body),
1259            3 => $crate::try_with_interval_matrix!(@arm_mut 3, $matrix, $ret, $body),
1260            4 => $crate::try_with_interval_matrix!(@arm_mut 4, $matrix, $ret, $body),
1261            5 => $crate::try_with_interval_matrix!(@arm_mut 5, $matrix, $ret, $body),
1262            6 => $crate::try_with_interval_matrix!(@arm_mut 6, $matrix, $ret, $body),
1263            7 => $crate::try_with_interval_matrix!(@arm_mut 7, $matrix, $ret, $body),
1264            requested => Err(::core::convert::From::from(
1265                $crate::LaError::unsupported_dimension(
1266                    requested,
1267                    $crate::MAX_INTERVAL_MATRIX_DIM,
1268                ),
1269            )),
1270        }
1271    }};
1272    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1273        let mut __la_stack_body = |$matrix: $crate::IntervalMatrix<$d>| -> $ret { $body };
1274        __la_stack_body($crate::IntervalMatrix::<$d>::zero())
1275    }};
1276    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1277        let mut __la_stack_body = |mut $matrix: $crate::IntervalMatrix<$d>| -> $ret { $body };
1278        __la_stack_body($crate::IntervalMatrix::<$d>::zero())
1279    }};
1280}
1281
1282/// Fallibly dispatch a runtime dimension to a concrete exact rational matrix.
1283///
1284/// The macro creates a zero [`RationalMatrix`] with the selected const-generic
1285/// dimension, then evaluates the supplied closure body. Dimensions `0..=8` are
1286/// supported on stable Rust. The closure may fill the matrix through
1287/// [`RationalMatrix::set`] or replace it with a value built by
1288/// [`RationalMatrix::try_from_fn`].
1289/// The body may mutate or consume captured values. It is not evaluated for
1290/// unsupported dimensions.
1291///
1292/// # Errors
1293/// Returns [`LaError::UnsupportedDimension`] (converted through
1294/// `From<LaError>`) when the requested dimension is greater than
1295/// [`MAX_RATIONAL_MATRIX_DISPATCH_DIM`]. The closure body may return any other
1296/// error representable by its declared `Result` type.
1297///
1298/// # Examples
1299/// ```
1300/// use la_stack::prelude::*;
1301///
1302/// # fn main() -> Result<(), LaError> {
1303/// let requested = 3usize;
1304/// let sign = try_with_rational_matrix!(requested, |mut matrix| -> Result<
1305///     DeterminantSign,
1306///     LaError,
1307/// > {
1308///     for index in 0..requested {
1309///         matrix.set(index, index, BigRational::from_integer(1.into()))?;
1310///     }
1311///     Ok(matrix.det_sign())
1312/// })?;
1313/// assert_eq!(sign, DeterminantSign::Positive);
1314/// # Ok(())
1315/// # }
1316/// ```
1317#[cfg(feature = "exact")]
1318#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1319#[macro_export]
1320macro_rules! try_with_rational_matrix {
1321    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1322        let __la_stack_requested_dim: usize = $dim;
1323        match __la_stack_requested_dim {
1324            0 => $crate::try_with_rational_matrix!(@arm 0, $matrix, $ret, $body),
1325            1 => $crate::try_with_rational_matrix!(@arm 1, $matrix, $ret, $body),
1326            2 => $crate::try_with_rational_matrix!(@arm 2, $matrix, $ret, $body),
1327            3 => $crate::try_with_rational_matrix!(@arm 3, $matrix, $ret, $body),
1328            4 => $crate::try_with_rational_matrix!(@arm 4, $matrix, $ret, $body),
1329            5 => $crate::try_with_rational_matrix!(@arm 5, $matrix, $ret, $body),
1330            6 => $crate::try_with_rational_matrix!(@arm 6, $matrix, $ret, $body),
1331            7 => $crate::try_with_rational_matrix!(@arm 7, $matrix, $ret, $body),
1332            8 => $crate::try_with_rational_matrix!(@arm 8, $matrix, $ret, $body),
1333            requested => Err(::core::convert::From::from(
1334                $crate::LaError::unsupported_dimension(
1335                    requested,
1336                    $crate::MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1337                ),
1338            )),
1339        }
1340    }};
1341    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1342        let __la_stack_requested_dim: usize = $dim;
1343        match __la_stack_requested_dim {
1344            0 => $crate::try_with_rational_matrix!(@arm_mut 0, $matrix, $ret, $body),
1345            1 => $crate::try_with_rational_matrix!(@arm_mut 1, $matrix, $ret, $body),
1346            2 => $crate::try_with_rational_matrix!(@arm_mut 2, $matrix, $ret, $body),
1347            3 => $crate::try_with_rational_matrix!(@arm_mut 3, $matrix, $ret, $body),
1348            4 => $crate::try_with_rational_matrix!(@arm_mut 4, $matrix, $ret, $body),
1349            5 => $crate::try_with_rational_matrix!(@arm_mut 5, $matrix, $ret, $body),
1350            6 => $crate::try_with_rational_matrix!(@arm_mut 6, $matrix, $ret, $body),
1351            7 => $crate::try_with_rational_matrix!(@arm_mut 7, $matrix, $ret, $body),
1352            8 => $crate::try_with_rational_matrix!(@arm_mut 8, $matrix, $ret, $body),
1353            requested => Err(::core::convert::From::from(
1354                $crate::LaError::unsupported_dimension(
1355                    requested,
1356                    $crate::MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1357                ),
1358            )),
1359        }
1360    }};
1361    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1362        let mut __la_stack_body = |$matrix: $crate::RationalMatrix<$d>| -> $ret { $body };
1363        __la_stack_body($crate::RationalMatrix::<$d>::zero())
1364    }};
1365    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1366        let mut __la_stack_body = |mut $matrix: $crate::RationalMatrix<$d>| -> $ret { $body };
1367        __la_stack_body($crate::RationalMatrix::<$d>::zero())
1368    }};
1369}
1370
1371/// Common imports for ergonomic usage.
1372///
1373/// This prelude re-exports the primary types and common constants: [`Matrix`],
1374/// [`DeterminantWithErrorBound`], [`Interval`], [`IntervalMatrix`],
1375/// [`IntervalDeterminantSign`], [`ScalarWithErrorBound`], [`Vector`], [`Lu`],
1376/// [`Ldlt`], [`Tolerance`],
1377/// and [`LaError`]. It also includes the [`VectorAngle`] extension trait for
1378/// unsigned angles between borrowed coordinate slices and [`gram_matrix`] for
1379/// constructing a symmetric matrix of pairwise vector inner products. Its typed
1380/// error categories include [`ArithmeticOperation`], [`FactorizationKind`],
1381/// [`IntervalBound`], [`IntervalOperand`], [`InvalidToleranceReason`],
1382/// [`NonFiniteLocation`], [`NonFiniteOrigin`], [`PositiveSemidefiniteViolation`],
1383/// [`SingularityReason`], [`UnrepresentableReason`], and [`VectorOperand`]. It also re-exports
1384/// [`DEFAULT_SINGULAR_TOL`],
1385/// [`MAX_STACK_MATRIX_DISPATCH_DIM`], [`MAX_INTERVAL_MATRIX_DIM`],
1386/// [`try_with_stack_matrix!`], and [`try_with_interval_matrix!`] for
1387/// runtime-to-const matrix dispatch. Advanced custom-filter code should import
1388/// [`ERR_COEFF_2`], [`ERR_COEFF_3`], and [`ERR_COEFF_4`] explicitly from the
1389/// crate root; those raw coefficients intentionally stay out of the prelude.
1390#[cfg_attr(feature = "exact", doc = "")]
1391#[cfg_attr(
1392    feature = "exact",
1393    doc = "When the `exact` feature is enabled, [`RationalMatrix`], [`RationalVector`],"
1394)]
1395#[cfg_attr(
1396    feature = "exact",
1397    doc = "[`DeterminantSign`], [`ExactF64Conversion`], [`BigInt`], and [`BigRational`]"
1398)]
1399#[cfg_attr(
1400    feature = "exact",
1401    doc = "are also re-exported, together with [`MAX_RATIONAL_MATRIX_DISPATCH_DIM`] and"
1402)]
1403#[cfg_attr(
1404    feature = "exact",
1405    doc = "[`try_with_rational_matrix!`] for runtime-to-const exact-matrix dispatch."
1406)]
1407#[cfg_attr(
1408    feature = "exact",
1409    doc = "[`ExactF64Conversion`] converts an already-computed exact determinant or solution"
1410)]
1411#[cfg_attr(
1412    feature = "exact",
1413    doc = "under either the strict or explicitly rounded binary64 contract, without repeating"
1414)]
1415#[cfg_attr(
1416    feature = "exact",
1417    doc = "exact elimination. The number types let callers construct expected exact values"
1418)]
1419#[cfg_attr(
1420    feature = "exact",
1421    doc = "without adding `num-bigint` / `num-rational` to their own dependencies. The most"
1422)]
1423#[cfg_attr(
1424    feature = "exact",
1425    doc = "commonly needed `num-traits` items are re-exported alongside them: [`FromPrimitive`]"
1426)]
1427#[cfg_attr(
1428    feature = "exact",
1429    doc = "for `BigRational::from_f64` / `from_i64`, [`ToPrimitive`] for"
1430)]
1431#[cfg_attr(
1432    feature = "exact",
1433    doc = "`BigRational::to_f64` / `to_i64`, and [`Signed`] for `is_positive` / `is_negative` /"
1434)]
1435#[cfg_attr(feature = "exact", doc = "`abs`.")]
1436pub mod prelude {
1437    pub use crate::{
1438        ArithmeticOperation, DEFAULT_SINGULAR_TOL, DeterminantWithErrorBound, FactorizationKind,
1439        Interval, IntervalBound, IntervalDeterminantSign, IntervalMatrix, IntervalOperand,
1440        InvalidToleranceReason, LaError, Ldlt, Lu, MAX_INTERVAL_MATRIX_DIM,
1441        MAX_STACK_MATRIX_DISPATCH_DIM, Matrix, NonFiniteLocation, NonFiniteOrigin,
1442        PositiveSemidefiniteViolation, ScalarWithErrorBound, SingularityReason, Tolerance,
1443        UnrepresentableReason, Vector, VectorAngle, VectorOperand, gram_matrix,
1444        try_with_interval_matrix, try_with_stack_matrix,
1445    };
1446
1447    #[cfg(feature = "exact")]
1448    #[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1449    pub use crate::{
1450        BigInt, BigRational, DeterminantSign, ExactF64Conversion, FromPrimitive,
1451        MAX_RATIONAL_MATRIX_DISPATCH_DIM, RationalMatrix, RationalVector, Signed, ToPrimitive,
1452        try_with_rational_matrix,
1453    };
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458    use approx::assert_abs_diff_eq;
1459    use pastey::paste;
1460
1461    use super::*;
1462
1463    macro_rules! gen_stack_matrix_dispatch_tests {
1464        ($d:literal) => {
1465            paste! {
1466                #[test]
1467                fn [<try_with_stack_matrix_dispatches_ $d d>]() {
1468                    let requested = $d;
1469                    let got = try_with_stack_matrix!(requested, |mut m| -> Result<usize, LaError> {
1470                        if $d > 0 {
1471                            m.set($d - 1, $d - 1, f64::from($d))?;
1472                            assert_abs_diff_eq!(
1473                                m.try_get($d - 1, $d - 1)?,
1474                                f64::from($d),
1475                                epsilon = 0.0
1476                            );
1477                        }
1478                        Ok($d)
1479                    });
1480
1481                    assert_eq!(got, Ok($d));
1482                }
1483            }
1484        };
1485    }
1486
1487    gen_stack_matrix_dispatch_tests!(1);
1488    gen_stack_matrix_dispatch_tests!(2);
1489    gen_stack_matrix_dispatch_tests!(3);
1490    gen_stack_matrix_dispatch_tests!(4);
1491    gen_stack_matrix_dispatch_tests!(5);
1492    gen_stack_matrix_dispatch_tests!(6);
1493    gen_stack_matrix_dispatch_tests!(7);
1494
1495    macro_rules! gen_interval_matrix_dispatch_tests {
1496        ($d:literal) => {
1497            paste! {
1498                #[test]
1499                fn [<try_with_interval_matrix_dispatches_ $d d>]() {
1500                    let requested = $d;
1501                    let got = try_with_interval_matrix!(
1502                        requested,
1503                        |mut matrix| -> Result<IntervalDeterminantSign, LaError> {
1504                            let mut index = 0;
1505                            while index < $d {
1506                                matrix.set(index, index, Interval::ONE)?;
1507                                index += 1;
1508                            }
1509                            matrix.det_sign()
1510                        },
1511                    );
1512
1513                    assert_eq!(got, Ok(IntervalDeterminantSign::Positive));
1514                }
1515            }
1516        };
1517    }
1518
1519    gen_interval_matrix_dispatch_tests!(1);
1520    gen_interval_matrix_dispatch_tests!(2);
1521    gen_interval_matrix_dispatch_tests!(3);
1522    gen_interval_matrix_dispatch_tests!(4);
1523    gen_interval_matrix_dispatch_tests!(5);
1524    gen_interval_matrix_dispatch_tests!(6);
1525    gen_interval_matrix_dispatch_tests!(7);
1526
1527    #[cfg(feature = "exact")]
1528    macro_rules! gen_rational_matrix_dispatch_tests {
1529        ($d:literal) => {
1530            paste! {
1531                #[test]
1532                fn [<try_with_rational_matrix_dispatches_ $d d>]() {
1533                    let requested = $d;
1534                    let got = try_with_rational_matrix!(
1535                        requested,
1536                        |mut matrix| -> Result<DeterminantSign, LaError> {
1537                            let mut index = 0;
1538                            while index < $d {
1539                                matrix.set(
1540                                    index,
1541                                    index,
1542                                    BigRational::from_integer(BigInt::from(1)),
1543                                )?;
1544                                index += 1;
1545                            }
1546                            Ok(matrix.det_sign())
1547                        },
1548                    );
1549
1550                    assert_eq!(got, Ok(DeterminantSign::Positive));
1551                }
1552            }
1553        };
1554    }
1555
1556    #[cfg(feature = "exact")]
1557    gen_rational_matrix_dispatch_tests!(1);
1558    #[cfg(feature = "exact")]
1559    gen_rational_matrix_dispatch_tests!(2);
1560    #[cfg(feature = "exact")]
1561    gen_rational_matrix_dispatch_tests!(3);
1562    #[cfg(feature = "exact")]
1563    gen_rational_matrix_dispatch_tests!(4);
1564    #[cfg(feature = "exact")]
1565    gen_rational_matrix_dispatch_tests!(5);
1566    #[cfg(feature = "exact")]
1567    gen_rational_matrix_dispatch_tests!(6);
1568    #[cfg(feature = "exact")]
1569    gen_rational_matrix_dispatch_tests!(7);
1570    #[cfg(feature = "exact")]
1571    gen_rational_matrix_dispatch_tests!(8);
1572
1573    #[cfg(feature = "exact")]
1574    #[test]
1575    fn try_with_rational_matrix_dispatches_zero_dimension() {
1576        let got = try_with_rational_matrix!(0usize, |matrix| -> Result<DeterminantSign, LaError> {
1577            Ok(matrix.det_sign())
1578        });
1579
1580        assert_eq!(got, Ok(DeterminantSign::Positive));
1581    }
1582
1583    #[test]
1584    fn try_with_stack_matrix_supports_zero_dimension() {
1585        let got = try_with_stack_matrix!(0usize, |m| -> Result<Option<f64>, LaError> {
1586            m.det_direct()
1587        });
1588
1589        assert_eq!(got, Ok(Some(1.0)));
1590    }
1591
1592    #[test]
1593    fn try_with_interval_matrix_supports_zero_dimension() {
1594        let got = try_with_interval_matrix!(0usize, |matrix| -> Result<
1595            IntervalDeterminantSign,
1596            LaError,
1597        > { matrix.det_sign() },);
1598
1599        assert_eq!(got, Ok(IntervalDeterminantSign::Positive));
1600    }
1601
1602    #[test]
1603    fn try_with_stack_matrix_evaluates_dimension_once() {
1604        let mut evaluations = 0;
1605        let got = try_with_stack_matrix!(
1606            {
1607                evaluations += 1;
1608                2usize
1609            },
1610            |matrix| -> Result<f64, LaError> { matrix.try_get(1, 1) },
1611        );
1612
1613        assert_eq!(evaluations, 1);
1614        assert_eq!(got, Ok(0.0));
1615    }
1616
1617    #[test]
1618    fn try_with_interval_matrix_evaluates_dimension_once() {
1619        let mut evaluations = 0;
1620        let got = try_with_interval_matrix!(
1621            {
1622                evaluations += 1;
1623                2usize
1624            },
1625            |matrix| -> Result<Interval, LaError> { matrix.try_get(1, 1) },
1626        );
1627
1628        assert_eq!(evaluations, 1);
1629        assert_eq!(got, Ok(Interval::ZERO));
1630    }
1631
1632    #[test]
1633    fn try_with_stack_matrix_reports_unsupported_dimension() {
1634        let got = try_with_stack_matrix!(8usize, |m| -> Result<f64, LaError> { m.det() });
1635
1636        assert_eq!(
1637            got,
1638            Err(LaError::UnsupportedDimension {
1639                requested: 8,
1640                max: MAX_STACK_MATRIX_DISPATCH_DIM,
1641            })
1642        );
1643    }
1644
1645    #[test]
1646    fn try_with_interval_matrix_reports_unsupported_dimension() {
1647        let got = try_with_interval_matrix!(8usize, |matrix| -> Result<
1648            IntervalDeterminantSign,
1649            LaError,
1650        > { matrix.det_sign() },);
1651
1652        assert_eq!(
1653            got,
1654            Err(LaError::UnsupportedDimension {
1655                requested: 8,
1656                max: MAX_INTERVAL_MATRIX_DIM,
1657            })
1658        );
1659    }
1660
1661    #[derive(Debug, PartialEq)]
1662    struct DownstreamError(LaError);
1663
1664    impl From<LaError> for DownstreamError {
1665        fn from(err: LaError) -> Self {
1666            Self(err)
1667        }
1668    }
1669
1670    #[test]
1671    fn try_with_stack_matrix_converts_unsupported_dimension_error() {
1672        let got = try_with_stack_matrix!(9usize, |m| -> Result<usize, DownstreamError> {
1673            assert_abs_diff_eq!(m.norm_inf()?, 0.0, epsilon = 0.0);
1674            Ok(0)
1675        });
1676
1677        assert_eq!(
1678            got,
1679            Err(DownstreamError(LaError::UnsupportedDimension {
1680                requested: 9,
1681                max: MAX_STACK_MATRIX_DISPATCH_DIM,
1682            }))
1683        );
1684    }
1685
1686    #[test]
1687    fn try_with_interval_matrix_converts_unsupported_dimension_error() {
1688        let got = try_with_interval_matrix!(8usize, |matrix| -> Result<
1689            IntervalDeterminantSign,
1690            DownstreamError,
1691        > { Ok(matrix.det_sign()?) },);
1692
1693        assert_eq!(
1694            got,
1695            Err(DownstreamError(LaError::UnsupportedDimension {
1696                requested: 8,
1697                max: MAX_INTERVAL_MATRIX_DIM,
1698            }))
1699        );
1700    }
1701
1702    #[cfg(feature = "exact")]
1703    #[test]
1704    fn try_with_rational_matrix_reports_unsupported_dimension() {
1705        let got = try_with_rational_matrix!(9usize, |matrix| -> Result<BigRational, LaError> {
1706            Ok(matrix.det())
1707        });
1708
1709        assert_eq!(
1710            got,
1711            Err(LaError::UnsupportedDimension {
1712                requested: 9,
1713                max: MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1714            })
1715        );
1716    }
1717
1718    #[cfg(feature = "exact")]
1719    #[test]
1720    fn try_with_rational_matrix_converts_unsupported_dimension_error() {
1721        let got =
1722            try_with_rational_matrix!(9usize, |matrix| -> Result<BigRational, DownstreamError> {
1723                Ok(matrix.det())
1724            });
1725
1726        assert_eq!(
1727            got,
1728            Err(DownstreamError(LaError::UnsupportedDimension {
1729                requested: 9,
1730                max: MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1731            }))
1732        );
1733    }
1734}