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