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 error;
907#[cfg(feature = "exact")]
908mod exact;
909mod gram;
910mod interval;
911mod ldlt;
912mod lu;
913mod matrix;
914mod norm;
915#[cfg(feature = "exact")]
916mod rational;
917mod rounding;
918mod scaled_product;
919mod tolerance;
920mod vector;
921
922#[cfg(feature = "exact")]
923#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
924pub use exact::{DeterminantSign, ExactF64Conversion};
925#[cfg(feature = "exact")]
926#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
927pub use num_bigint::BigInt;
928#[cfg(feature = "exact")]
929#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
930pub use num_rational::BigRational;
931#[cfg(feature = "exact")]
932#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
933pub use num_traits::{FromPrimitive, Signed, ToPrimitive};
934#[cfg(feature = "exact")]
935#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
936pub use rational::{RationalMatrix, RationalVector};
937
938// ---------------------------------------------------------------------------
939// Error-bound constants for `Matrix::det_direct_with_errbound()` and
940// `Matrix::det_errbound()`.
941//
942// For `D ∈ {2, 3, 4}`, `Matrix::det_direct()` evaluates the Leibniz expansion
943// of the determinant as a tree of f64 multiplies and fused multiply-adds
944// (FMAs).  When every rounded intermediate is normal or an exact structural
945// zero, Shewchuk's error-analysis methodology (REFERENCES.md [8]) bounds the
946// absolute error of that computation by
947//
948//     |det_direct(A) - det_exact(A)|  ≤  ERR_COEFF_D · p(|A|)
949//
950// where `p(|A|)` is the **absolute Leibniz sum**
951//
952//     p(|A|) = Σ_σ ∏ᵢ |A[i, σ(i)]|,
953//
954// i.e. exactly the combinatorial matrix permanent `perm(|A|)`. The
955// implementation evaluates the corresponding fixed-size expansion in f64, so
956// the computed `permanent` value used by the bound may itself be rounded even
957// though the mathematical quantity above is exact.
958//
959// The longest rounding paths in the determinant and permanent have lengths
960// k = 2, 5, and 9. With EPS = 2^-52, gamma_k = k*EPS / (1 - k*EPS) bounds
961// their accumulated relative perturbations. Each exactly representable
962// coefficient satisfies c >= gamma_k / ((1 - EPS)*(1 - gamma_k)), including
963// possible downward rounding of both the permanent and the final product.
964// See docs/mathematical_basis.md, "Derivation of the returned determinant
965// bound", for the full argument and the underflow assumptions.
966//
967// These constants are NOT feature-gated — they rely only on f64 arithmetic
968// and are useful for adaptive-precision logic even without the `exact`
969// feature. Most callers should prefer `Matrix::det_direct_with_errbound()`
970// when they need the approximation and bound together, or
971// `Matrix::det_errbound()` when they need only the bound. Those methods apply
972// these constants to the actual matrix; the raw constants are
973// exposed for advanced use cases (composing the bound with a pre-reduced
974// permanent, rolling a custom adaptive filter, etc.).  See
975// `Matrix::det_sign_exact()` (behind the `exact` feature) for the
976// reference adaptive-filter that consumes these internally.
977// ---------------------------------------------------------------------------
978
979const EPS: f64 = f64::EPSILON; // 2^-52
980
981/// Absolute error coefficient for [`Matrix::<2>::det_direct`](crate::Matrix::det_direct).
982///
983/// This constant is not a caller-tuned tolerance. It is the dimension-specific
984/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
985/// bound on floating-point roundoff in the closed-form 2×2 determinant formula.
986///
987/// For a 2×2 matrix `A = [[a, b], [c, d]]` whose closed-form determinant
988/// intermediates do not undergo gradual underflow,
989///
990/// ```text
991/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_2 · (|a·d| + |b·c|)
992/// ```
993///
994/// `det_direct` evaluates `a·d - b·c` as one multiply followed by one FMA
995/// (2 rounding events on the longest path). The permanent also has a
996/// two-event path. The coefficient covers both trees and the final rounded
997/// multiplication; see the
998/// [returned-bound derivation](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#derivation-of-the-returned-determinant-bound).
999/// The analysis follows Shewchuk's framework and the binary64 arithmetic
1000/// model; see `REFERENCES.md` \[8-11\].
1001///
1002/// Prefer
1003/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1004/// unless you need only the bound or already have the absolute-Leibniz sum;
1005/// see
1006/// `Matrix::det_sign_exact` (under the `exact` feature) for the reference
1007/// adaptive-precision filter.
1008///
1009/// # Example
1010/// ```
1011/// use la_stack::{prelude::*, ERR_COEFF_2};
1012///
1013/// # fn main() -> Result<(), LaError> {
1014/// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1015/// let det = m.det_direct()?;
1016/// assert_eq!(det, Some(-2.0));
1017/// // Compute the bound from the raw constant for illustration; most
1018/// // callers would match on `m.det_errbound()?` instead.
1019/// let p = (1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs();
1020/// let bound = ERR_COEFF_2 * p;
1021/// // The f64 sign is provably correct without exact arithmetic.
1022/// assert_eq!(det.map(|value| value.abs() > bound), Some(true));
1023/// # Ok(())
1024/// # }
1025/// ```
1026pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
1027
1028/// Absolute error coefficient for [`Matrix::<3>::det_direct`](crate::Matrix::det_direct).
1029///
1030/// This constant is not a caller-tuned tolerance. It is the dimension-specific
1031/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
1032/// bound on floating-point roundoff in the closed-form 3×3 determinant formula.
1033///
1034/// For a 3×3 matrix `A` whose closed-form determinant intermediates do not
1035/// undergo gradual underflow,
1036///
1037/// ```text
1038/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_3 · p(|A|)
1039/// ```
1040///
1041/// where `p(|A|)` is the absolute Leibniz sum (the same cofactor
1042/// expansion as `det_direct` but with `|·|` at every leaf).
1043/// `det_direct` for D=3 uses three 2×2 FMA minors combined by a nested
1044/// FMA. The determinant and permanent each have at most five rounding events
1045/// per monomial; `8·EPS + 64·EPS²` also covers rounding the permanent and final
1046/// bound product. See [`ERR_COEFF_2`] for the derivation link and
1047/// `REFERENCES.md` \[8-11\] for the analysis framework.
1048///
1049/// Prefer
1050/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1051/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
1052/// example.
1053pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
1054
1055/// Absolute error coefficient for [`Matrix::<4>::det_direct`](crate::Matrix::det_direct).
1056///
1057/// This constant is not a caller-tuned tolerance. It is the dimension-specific
1058/// multiplier that turns the matrix's absolute Leibniz sum into a conservative
1059/// bound on floating-point roundoff in the closed-form 4×4 determinant formula.
1060///
1061/// For a 4×4 matrix `A` whose closed-form determinant intermediates do not
1062/// undergo gradual underflow,
1063///
1064/// ```text
1065/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_4 · p(|A|)
1066/// ```
1067///
1068/// where `p(|A|)` is the absolute Leibniz sum. `det_direct` for D=4
1069/// evaluates four nested 3×3 cofactors, sharing their six 2×2 minors when
1070/// every coefficient in the first two rows is non-zero, and reduces them with
1071/// an FMA row combination. Dense and sparse trees each have at most nine
1072/// rounding events per monomial, as does the permanent tree. The coefficient
1073/// `12·EPS + 128·EPS²` also covers rounding the permanent and final bound
1074/// product. See [`ERR_COEFF_2`] for the derivation link and `REFERENCES.md`
1075/// \[8-11\] for the analysis framework.
1076///
1077/// Prefer
1078/// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound)
1079/// over this constant for typical use; see [`ERR_COEFF_2`] for a worked
1080/// example.
1081pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
1082
1083/// Largest dimension supported by [`try_with_stack_matrix!`].
1084///
1085/// The crate can represent `Matrix<D>` for any compile-time `D`, but runtime
1086/// dispatch must enumerate a finite set of concrete stack types.  Dimensions
1087/// `0..=7` cover downstream geometric predicate matrices while keeping the
1088/// dispatch surface explicit.
1089pub const MAX_STACK_MATRIX_DISPATCH_DIM: usize = 7;
1090
1091/// Largest dimension supported by [`try_with_rational_matrix!`].
1092///
1093/// This bound covers exact downstream geometric systems through dimension 8
1094/// while keeping runtime-to-const dispatch explicit and available on stable
1095/// Rust.
1096#[cfg(feature = "exact")]
1097#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1098pub const MAX_RATIONAL_MATRIX_DISPATCH_DIM: usize = 8;
1099
1100pub use error::{
1101    ArithmeticOperation, FactorizationKind, IntervalBound, IntervalOperand, InvalidToleranceReason,
1102    LaError, NonFiniteLocation, NonFiniteOrigin, PositiveSemidefiniteViolation, SingularityReason,
1103    UnrepresentableReason,
1104};
1105pub use gram::gram_matrix;
1106pub use interval::{Interval, IntervalDeterminantSign, IntervalMatrix, MAX_INTERVAL_MATRIX_DIM};
1107pub use ldlt::Ldlt;
1108pub use lu::Lu;
1109pub use matrix::{DeterminantWithErrorBound, Matrix};
1110pub use tolerance::{DEFAULT_SINGULAR_TOL, Tolerance};
1111pub use vector::{ScalarWithErrorBound, Vector};
1112
1113/// Fallibly dispatch a runtime dimension to a concrete stack-allocated matrix.
1114///
1115/// The macro creates a zero matrix with type `Matrix<N>` for the selected
1116/// runtime dimension `N`, then evaluates the supplied closure body.  Supported
1117/// runtime dimensions run from `0` through [`MAX_STACK_MATRIX_DISPATCH_DIM`].
1118/// The body may mutate or consume captured values. It is not evaluated for
1119/// unsupported dimensions.
1120/// Unsupported dimensions return
1121/// `Err(LaError::UnsupportedDimension { requested, max })` converted with
1122/// `From<LaError>`, so downstream crates can use their own public error type.
1123///
1124/// # Errors
1125/// Returns [`LaError::UnsupportedDimension`] (converted through `From<LaError>`)
1126/// when the requested runtime dimension is greater than
1127/// [`MAX_STACK_MATRIX_DISPATCH_DIM`].  The closure body may return any other
1128/// error representable by its declared `Result` type.
1129///
1130/// # Examples
1131/// ```
1132/// use la_stack::prelude::*;
1133///
1134/// # fn main() -> Result<(), LaError> {
1135/// let requested = 2usize;
1136/// let det = try_with_stack_matrix!(requested, |mut m| -> Result<f64, LaError> {
1137///     m.set(0, 0, 1.0)?;
1138///     m.set(1, 1, 1.0)?;
1139///     m.det()
1140/// })?;
1141///
1142/// assert_eq!(det, 1.0);
1143/// # Ok(())
1144/// # }
1145/// ```
1146#[macro_export]
1147macro_rules! try_with_stack_matrix {
1148    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1149        let __la_stack_requested_dim: usize = $dim;
1150        match __la_stack_requested_dim {
1151            0 => $crate::try_with_stack_matrix!(@arm 0, $matrix, $ret, $body),
1152            1 => $crate::try_with_stack_matrix!(@arm 1, $matrix, $ret, $body),
1153            2 => $crate::try_with_stack_matrix!(@arm 2, $matrix, $ret, $body),
1154            3 => $crate::try_with_stack_matrix!(@arm 3, $matrix, $ret, $body),
1155            4 => $crate::try_with_stack_matrix!(@arm 4, $matrix, $ret, $body),
1156            5 => $crate::try_with_stack_matrix!(@arm 5, $matrix, $ret, $body),
1157            6 => $crate::try_with_stack_matrix!(@arm 6, $matrix, $ret, $body),
1158            7 => $crate::try_with_stack_matrix!(@arm 7, $matrix, $ret, $body),
1159            requested => Err(::core::convert::From::from(
1160                $crate::LaError::unsupported_dimension(
1161                    requested,
1162                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
1163                ),
1164            )),
1165        }
1166    }};
1167    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1168        let __la_stack_requested_dim: usize = $dim;
1169        match __la_stack_requested_dim {
1170            0 => $crate::try_with_stack_matrix!(@arm_mut 0, $matrix, $ret, $body),
1171            1 => $crate::try_with_stack_matrix!(@arm_mut 1, $matrix, $ret, $body),
1172            2 => $crate::try_with_stack_matrix!(@arm_mut 2, $matrix, $ret, $body),
1173            3 => $crate::try_with_stack_matrix!(@arm_mut 3, $matrix, $ret, $body),
1174            4 => $crate::try_with_stack_matrix!(@arm_mut 4, $matrix, $ret, $body),
1175            5 => $crate::try_with_stack_matrix!(@arm_mut 5, $matrix, $ret, $body),
1176            6 => $crate::try_with_stack_matrix!(@arm_mut 6, $matrix, $ret, $body),
1177            7 => $crate::try_with_stack_matrix!(@arm_mut 7, $matrix, $ret, $body),
1178            requested => Err(::core::convert::From::from(
1179                $crate::LaError::unsupported_dimension(
1180                    requested,
1181                    $crate::MAX_STACK_MATRIX_DISPATCH_DIM,
1182                ),
1183            )),
1184        }
1185    }};
1186    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1187        let mut __la_stack_body = |$matrix: $crate::Matrix<$d>| -> $ret { $body };
1188        __la_stack_body($crate::Matrix::<$d>::zero())
1189    }};
1190    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1191        let mut __la_stack_body = |mut $matrix: $crate::Matrix<$d>| -> $ret { $body };
1192        __la_stack_body($crate::Matrix::<$d>::zero())
1193    }};
1194}
1195
1196/// Fallibly dispatch a runtime dimension to a concrete interval matrix.
1197///
1198/// The macro creates a zero [`IntervalMatrix`] with the selected const-generic
1199/// dimension, then evaluates the closure body. Supported dimensions run from
1200/// `0` through [`MAX_INTERVAL_MATRIX_DIM`]. Unsupported dimensions return
1201/// [`LaError::UnsupportedDimension`] converted through `From<LaError>`.
1202/// The body may mutate or consume captured values. It is not evaluated for
1203/// unsupported dimensions.
1204///
1205/// # Errors
1206/// Returns [`LaError::UnsupportedDimension`] (converted through
1207/// `From<LaError>`) when the requested dimension is greater than
1208/// [`MAX_INTERVAL_MATRIX_DIM`]. The closure body may return any other error
1209/// representable by its declared `Result` type.
1210///
1211/// # Examples
1212/// ```
1213/// use la_stack::prelude::*;
1214///
1215/// # fn main() -> Result<(), LaError> {
1216/// let requested = 3usize;
1217/// let sign = try_with_interval_matrix!(requested, |mut matrix| -> Result<
1218///     IntervalDeterminantSign,
1219///     LaError,
1220/// > {
1221///     for index in 0..requested {
1222///         matrix.set(index, index, Interval::ONE)?;
1223///     }
1224///     matrix.det_sign()
1225/// })?;
1226/// assert_eq!(sign, IntervalDeterminantSign::Positive);
1227/// # Ok(())
1228/// # }
1229/// ```
1230#[macro_export]
1231macro_rules! try_with_interval_matrix {
1232    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1233        let __la_stack_requested_dim: usize = $dim;
1234        match __la_stack_requested_dim {
1235            0 => $crate::try_with_interval_matrix!(@arm 0, $matrix, $ret, $body),
1236            1 => $crate::try_with_interval_matrix!(@arm 1, $matrix, $ret, $body),
1237            2 => $crate::try_with_interval_matrix!(@arm 2, $matrix, $ret, $body),
1238            3 => $crate::try_with_interval_matrix!(@arm 3, $matrix, $ret, $body),
1239            4 => $crate::try_with_interval_matrix!(@arm 4, $matrix, $ret, $body),
1240            5 => $crate::try_with_interval_matrix!(@arm 5, $matrix, $ret, $body),
1241            6 => $crate::try_with_interval_matrix!(@arm 6, $matrix, $ret, $body),
1242            7 => $crate::try_with_interval_matrix!(@arm 7, $matrix, $ret, $body),
1243            requested => Err(::core::convert::From::from(
1244                $crate::LaError::unsupported_dimension(
1245                    requested,
1246                    $crate::MAX_INTERVAL_MATRIX_DIM,
1247                ),
1248            )),
1249        }
1250    }};
1251    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1252        let __la_stack_requested_dim: usize = $dim;
1253        match __la_stack_requested_dim {
1254            0 => $crate::try_with_interval_matrix!(@arm_mut 0, $matrix, $ret, $body),
1255            1 => $crate::try_with_interval_matrix!(@arm_mut 1, $matrix, $ret, $body),
1256            2 => $crate::try_with_interval_matrix!(@arm_mut 2, $matrix, $ret, $body),
1257            3 => $crate::try_with_interval_matrix!(@arm_mut 3, $matrix, $ret, $body),
1258            4 => $crate::try_with_interval_matrix!(@arm_mut 4, $matrix, $ret, $body),
1259            5 => $crate::try_with_interval_matrix!(@arm_mut 5, $matrix, $ret, $body),
1260            6 => $crate::try_with_interval_matrix!(@arm_mut 6, $matrix, $ret, $body),
1261            7 => $crate::try_with_interval_matrix!(@arm_mut 7, $matrix, $ret, $body),
1262            requested => Err(::core::convert::From::from(
1263                $crate::LaError::unsupported_dimension(
1264                    requested,
1265                    $crate::MAX_INTERVAL_MATRIX_DIM,
1266                ),
1267            )),
1268        }
1269    }};
1270    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1271        let mut __la_stack_body = |$matrix: $crate::IntervalMatrix<$d>| -> $ret { $body };
1272        __la_stack_body($crate::IntervalMatrix::<$d>::zero())
1273    }};
1274    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1275        let mut __la_stack_body = |mut $matrix: $crate::IntervalMatrix<$d>| -> $ret { $body };
1276        __la_stack_body($crate::IntervalMatrix::<$d>::zero())
1277    }};
1278}
1279
1280/// Fallibly dispatch a runtime dimension to a concrete exact rational matrix.
1281///
1282/// The macro creates a zero [`RationalMatrix`] with the selected const-generic
1283/// dimension, then evaluates the supplied closure body. Dimensions `0..=8` are
1284/// supported on stable Rust. The closure may fill the matrix through
1285/// [`RationalMatrix::set`] or replace it with a value built by
1286/// [`RationalMatrix::try_from_fn`].
1287/// The body may mutate or consume captured values. It is not evaluated for
1288/// unsupported dimensions.
1289///
1290/// # Errors
1291/// Returns [`LaError::UnsupportedDimension`] (converted through
1292/// `From<LaError>`) when the requested dimension is greater than
1293/// [`MAX_RATIONAL_MATRIX_DISPATCH_DIM`]. The closure body may return any other
1294/// error representable by its declared `Result` type.
1295///
1296/// # Examples
1297/// ```
1298/// use la_stack::prelude::*;
1299///
1300/// # fn main() -> Result<(), LaError> {
1301/// let requested = 3usize;
1302/// let sign = try_with_rational_matrix!(requested, |mut matrix| -> Result<
1303///     DeterminantSign,
1304///     LaError,
1305/// > {
1306///     for index in 0..requested {
1307///         matrix.set(index, index, BigRational::from_integer(1.into()))?;
1308///     }
1309///     Ok(matrix.det_sign())
1310/// })?;
1311/// assert_eq!(sign, DeterminantSign::Positive);
1312/// # Ok(())
1313/// # }
1314/// ```
1315#[cfg(feature = "exact")]
1316#[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1317#[macro_export]
1318macro_rules! try_with_rational_matrix {
1319    ($dim:expr, |$matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1320        let __la_stack_requested_dim: usize = $dim;
1321        match __la_stack_requested_dim {
1322            0 => $crate::try_with_rational_matrix!(@arm 0, $matrix, $ret, $body),
1323            1 => $crate::try_with_rational_matrix!(@arm 1, $matrix, $ret, $body),
1324            2 => $crate::try_with_rational_matrix!(@arm 2, $matrix, $ret, $body),
1325            3 => $crate::try_with_rational_matrix!(@arm 3, $matrix, $ret, $body),
1326            4 => $crate::try_with_rational_matrix!(@arm 4, $matrix, $ret, $body),
1327            5 => $crate::try_with_rational_matrix!(@arm 5, $matrix, $ret, $body),
1328            6 => $crate::try_with_rational_matrix!(@arm 6, $matrix, $ret, $body),
1329            7 => $crate::try_with_rational_matrix!(@arm 7, $matrix, $ret, $body),
1330            8 => $crate::try_with_rational_matrix!(@arm 8, $matrix, $ret, $body),
1331            requested => Err(::core::convert::From::from(
1332                $crate::LaError::unsupported_dimension(
1333                    requested,
1334                    $crate::MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1335                ),
1336            )),
1337        }
1338    }};
1339    ($dim:expr, |mut $matrix:ident| -> $ret:ty $body:block $(,)?) => {{
1340        let __la_stack_requested_dim: usize = $dim;
1341        match __la_stack_requested_dim {
1342            0 => $crate::try_with_rational_matrix!(@arm_mut 0, $matrix, $ret, $body),
1343            1 => $crate::try_with_rational_matrix!(@arm_mut 1, $matrix, $ret, $body),
1344            2 => $crate::try_with_rational_matrix!(@arm_mut 2, $matrix, $ret, $body),
1345            3 => $crate::try_with_rational_matrix!(@arm_mut 3, $matrix, $ret, $body),
1346            4 => $crate::try_with_rational_matrix!(@arm_mut 4, $matrix, $ret, $body),
1347            5 => $crate::try_with_rational_matrix!(@arm_mut 5, $matrix, $ret, $body),
1348            6 => $crate::try_with_rational_matrix!(@arm_mut 6, $matrix, $ret, $body),
1349            7 => $crate::try_with_rational_matrix!(@arm_mut 7, $matrix, $ret, $body),
1350            8 => $crate::try_with_rational_matrix!(@arm_mut 8, $matrix, $ret, $body),
1351            requested => Err(::core::convert::From::from(
1352                $crate::LaError::unsupported_dimension(
1353                    requested,
1354                    $crate::MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1355                ),
1356            )),
1357        }
1358    }};
1359    (@arm $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1360        let mut __la_stack_body = |$matrix: $crate::RationalMatrix<$d>| -> $ret { $body };
1361        __la_stack_body($crate::RationalMatrix::<$d>::zero())
1362    }};
1363    (@arm_mut $d:literal, $matrix:ident, $ret:ty, $body:block) => {{
1364        let mut __la_stack_body = |mut $matrix: $crate::RationalMatrix<$d>| -> $ret { $body };
1365        __la_stack_body($crate::RationalMatrix::<$d>::zero())
1366    }};
1367}
1368
1369/// Common imports for ergonomic usage.
1370///
1371/// This prelude re-exports the primary types and common constants: [`Matrix`],
1372/// [`DeterminantWithErrorBound`], [`Interval`], [`IntervalMatrix`],
1373/// [`IntervalDeterminantSign`], [`ScalarWithErrorBound`], [`Vector`], [`Lu`],
1374/// [`Ldlt`], [`Tolerance`],
1375/// and [`LaError`]. It also includes [`gram_matrix`] for constructing a symmetric
1376/// matrix of pairwise vector inner products. Its typed
1377/// error categories include [`ArithmeticOperation`], [`FactorizationKind`],
1378/// [`IntervalBound`], [`IntervalOperand`], [`InvalidToleranceReason`],
1379/// [`NonFiniteLocation`], [`NonFiniteOrigin`], [`PositiveSemidefiniteViolation`],
1380/// [`SingularityReason`], and [`UnrepresentableReason`]. It also re-exports
1381/// [`DEFAULT_SINGULAR_TOL`],
1382/// [`MAX_STACK_MATRIX_DISPATCH_DIM`], [`MAX_INTERVAL_MATRIX_DIM`],
1383/// [`try_with_stack_matrix!`], and [`try_with_interval_matrix!`] for
1384/// runtime-to-const matrix dispatch. Advanced custom-filter code should import
1385/// [`ERR_COEFF_2`], [`ERR_COEFF_3`], and [`ERR_COEFF_4`] explicitly from the
1386/// crate root; those raw coefficients intentionally stay out of the prelude.
1387#[cfg_attr(feature = "exact", doc = "")]
1388#[cfg_attr(
1389    feature = "exact",
1390    doc = "When the `exact` feature is enabled, [`RationalMatrix`], [`RationalVector`],"
1391)]
1392#[cfg_attr(
1393    feature = "exact",
1394    doc = "[`DeterminantSign`], [`ExactF64Conversion`], [`BigInt`], and [`BigRational`]"
1395)]
1396#[cfg_attr(
1397    feature = "exact",
1398    doc = "are also re-exported, together with [`MAX_RATIONAL_MATRIX_DISPATCH_DIM`] and"
1399)]
1400#[cfg_attr(
1401    feature = "exact",
1402    doc = "[`try_with_rational_matrix!`] for runtime-to-const exact-matrix dispatch."
1403)]
1404#[cfg_attr(
1405    feature = "exact",
1406    doc = "[`ExactF64Conversion`] converts an already-computed exact determinant or solution"
1407)]
1408#[cfg_attr(
1409    feature = "exact",
1410    doc = "under either the strict or explicitly rounded binary64 contract, without repeating"
1411)]
1412#[cfg_attr(
1413    feature = "exact",
1414    doc = "exact elimination. The number types let callers construct expected exact values"
1415)]
1416#[cfg_attr(
1417    feature = "exact",
1418    doc = "without adding `num-bigint` / `num-rational` to their own dependencies. The most"
1419)]
1420#[cfg_attr(
1421    feature = "exact",
1422    doc = "commonly needed `num-traits` items are re-exported alongside them: [`FromPrimitive`]"
1423)]
1424#[cfg_attr(
1425    feature = "exact",
1426    doc = "for `BigRational::from_f64` / `from_i64`, [`ToPrimitive`] for"
1427)]
1428#[cfg_attr(
1429    feature = "exact",
1430    doc = "`BigRational::to_f64` / `to_i64`, and [`Signed`] for `is_positive` / `is_negative` /"
1431)]
1432#[cfg_attr(feature = "exact", doc = "`abs`.")]
1433pub mod prelude {
1434    pub use crate::{
1435        ArithmeticOperation, DEFAULT_SINGULAR_TOL, DeterminantWithErrorBound, FactorizationKind,
1436        Interval, IntervalBound, IntervalDeterminantSign, IntervalMatrix, IntervalOperand,
1437        InvalidToleranceReason, LaError, Ldlt, Lu, MAX_INTERVAL_MATRIX_DIM,
1438        MAX_STACK_MATRIX_DISPATCH_DIM, Matrix, NonFiniteLocation, NonFiniteOrigin,
1439        PositiveSemidefiniteViolation, ScalarWithErrorBound, SingularityReason, Tolerance,
1440        UnrepresentableReason, Vector, gram_matrix, try_with_interval_matrix,
1441        try_with_stack_matrix,
1442    };
1443
1444    #[cfg(feature = "exact")]
1445    #[cfg_attr(docsrs, doc(cfg(feature = "exact")))]
1446    pub use crate::{
1447        BigInt, BigRational, DeterminantSign, ExactF64Conversion, FromPrimitive,
1448        MAX_RATIONAL_MATRIX_DISPATCH_DIM, RationalMatrix, RationalVector, Signed, ToPrimitive,
1449        try_with_rational_matrix,
1450    };
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455    use approx::assert_abs_diff_eq;
1456    use pastey::paste;
1457
1458    use super::*;
1459
1460    macro_rules! gen_stack_matrix_dispatch_tests {
1461        ($d:literal) => {
1462            paste! {
1463                #[test]
1464                fn [<try_with_stack_matrix_dispatches_ $d d>]() {
1465                    let requested = $d;
1466                    let got = try_with_stack_matrix!(requested, |mut m| -> Result<usize, LaError> {
1467                        if $d > 0 {
1468                            m.set($d - 1, $d - 1, f64::from($d))?;
1469                            assert_abs_diff_eq!(
1470                                m.try_get($d - 1, $d - 1)?,
1471                                f64::from($d),
1472                                epsilon = 0.0
1473                            );
1474                        }
1475                        Ok($d)
1476                    });
1477
1478                    assert_eq!(got, Ok($d));
1479                }
1480            }
1481        };
1482    }
1483
1484    gen_stack_matrix_dispatch_tests!(1);
1485    gen_stack_matrix_dispatch_tests!(2);
1486    gen_stack_matrix_dispatch_tests!(3);
1487    gen_stack_matrix_dispatch_tests!(4);
1488    gen_stack_matrix_dispatch_tests!(5);
1489    gen_stack_matrix_dispatch_tests!(6);
1490    gen_stack_matrix_dispatch_tests!(7);
1491
1492    macro_rules! gen_interval_matrix_dispatch_tests {
1493        ($d:literal) => {
1494            paste! {
1495                #[test]
1496                fn [<try_with_interval_matrix_dispatches_ $d d>]() {
1497                    let requested = $d;
1498                    let got = try_with_interval_matrix!(
1499                        requested,
1500                        |mut matrix| -> Result<IntervalDeterminantSign, LaError> {
1501                            let mut index = 0;
1502                            while index < $d {
1503                                matrix.set(index, index, Interval::ONE)?;
1504                                index += 1;
1505                            }
1506                            matrix.det_sign()
1507                        },
1508                    );
1509
1510                    assert_eq!(got, Ok(IntervalDeterminantSign::Positive));
1511                }
1512            }
1513        };
1514    }
1515
1516    gen_interval_matrix_dispatch_tests!(1);
1517    gen_interval_matrix_dispatch_tests!(2);
1518    gen_interval_matrix_dispatch_tests!(3);
1519    gen_interval_matrix_dispatch_tests!(4);
1520    gen_interval_matrix_dispatch_tests!(5);
1521    gen_interval_matrix_dispatch_tests!(6);
1522    gen_interval_matrix_dispatch_tests!(7);
1523
1524    #[cfg(feature = "exact")]
1525    macro_rules! gen_rational_matrix_dispatch_tests {
1526        ($d:literal) => {
1527            paste! {
1528                #[test]
1529                fn [<try_with_rational_matrix_dispatches_ $d d>]() {
1530                    let requested = $d;
1531                    let got = try_with_rational_matrix!(
1532                        requested,
1533                        |mut matrix| -> Result<DeterminantSign, LaError> {
1534                            let mut index = 0;
1535                            while index < $d {
1536                                matrix.set(
1537                                    index,
1538                                    index,
1539                                    BigRational::from_integer(BigInt::from(1)),
1540                                )?;
1541                                index += 1;
1542                            }
1543                            Ok(matrix.det_sign())
1544                        },
1545                    );
1546
1547                    assert_eq!(got, Ok(DeterminantSign::Positive));
1548                }
1549            }
1550        };
1551    }
1552
1553    #[cfg(feature = "exact")]
1554    gen_rational_matrix_dispatch_tests!(1);
1555    #[cfg(feature = "exact")]
1556    gen_rational_matrix_dispatch_tests!(2);
1557    #[cfg(feature = "exact")]
1558    gen_rational_matrix_dispatch_tests!(3);
1559    #[cfg(feature = "exact")]
1560    gen_rational_matrix_dispatch_tests!(4);
1561    #[cfg(feature = "exact")]
1562    gen_rational_matrix_dispatch_tests!(5);
1563    #[cfg(feature = "exact")]
1564    gen_rational_matrix_dispatch_tests!(6);
1565    #[cfg(feature = "exact")]
1566    gen_rational_matrix_dispatch_tests!(7);
1567    #[cfg(feature = "exact")]
1568    gen_rational_matrix_dispatch_tests!(8);
1569
1570    #[cfg(feature = "exact")]
1571    #[test]
1572    fn try_with_rational_matrix_dispatches_zero_dimension() {
1573        let got = try_with_rational_matrix!(0usize, |matrix| -> Result<DeterminantSign, LaError> {
1574            Ok(matrix.det_sign())
1575        });
1576
1577        assert_eq!(got, Ok(DeterminantSign::Positive));
1578    }
1579
1580    #[test]
1581    fn try_with_stack_matrix_supports_zero_dimension() {
1582        let got = try_with_stack_matrix!(0usize, |m| -> Result<Option<f64>, LaError> {
1583            m.det_direct()
1584        });
1585
1586        assert_eq!(got, Ok(Some(1.0)));
1587    }
1588
1589    #[test]
1590    fn try_with_interval_matrix_supports_zero_dimension() {
1591        let got = try_with_interval_matrix!(0usize, |matrix| -> Result<
1592            IntervalDeterminantSign,
1593            LaError,
1594        > { matrix.det_sign() },);
1595
1596        assert_eq!(got, Ok(IntervalDeterminantSign::Positive));
1597    }
1598
1599    #[test]
1600    fn try_with_stack_matrix_evaluates_dimension_once() {
1601        let mut evaluations = 0;
1602        let got = try_with_stack_matrix!(
1603            {
1604                evaluations += 1;
1605                2usize
1606            },
1607            |matrix| -> Result<f64, LaError> { matrix.try_get(1, 1) },
1608        );
1609
1610        assert_eq!(evaluations, 1);
1611        assert_eq!(got, Ok(0.0));
1612    }
1613
1614    #[test]
1615    fn try_with_interval_matrix_evaluates_dimension_once() {
1616        let mut evaluations = 0;
1617        let got = try_with_interval_matrix!(
1618            {
1619                evaluations += 1;
1620                2usize
1621            },
1622            |matrix| -> Result<Interval, LaError> { matrix.try_get(1, 1) },
1623        );
1624
1625        assert_eq!(evaluations, 1);
1626        assert_eq!(got, Ok(Interval::ZERO));
1627    }
1628
1629    #[test]
1630    fn try_with_stack_matrix_reports_unsupported_dimension() {
1631        let got = try_with_stack_matrix!(8usize, |m| -> Result<f64, LaError> { m.det() });
1632
1633        assert_eq!(
1634            got,
1635            Err(LaError::UnsupportedDimension {
1636                requested: 8,
1637                max: MAX_STACK_MATRIX_DISPATCH_DIM,
1638            })
1639        );
1640    }
1641
1642    #[test]
1643    fn try_with_interval_matrix_reports_unsupported_dimension() {
1644        let got = try_with_interval_matrix!(8usize, |matrix| -> Result<
1645            IntervalDeterminantSign,
1646            LaError,
1647        > { matrix.det_sign() },);
1648
1649        assert_eq!(
1650            got,
1651            Err(LaError::UnsupportedDimension {
1652                requested: 8,
1653                max: MAX_INTERVAL_MATRIX_DIM,
1654            })
1655        );
1656    }
1657
1658    #[derive(Debug, PartialEq)]
1659    struct DownstreamError(LaError);
1660
1661    impl From<LaError> for DownstreamError {
1662        fn from(err: LaError) -> Self {
1663            Self(err)
1664        }
1665    }
1666
1667    #[test]
1668    fn try_with_stack_matrix_converts_unsupported_dimension_error() {
1669        let got = try_with_stack_matrix!(9usize, |m| -> Result<usize, DownstreamError> {
1670            assert_abs_diff_eq!(m.norm_inf()?, 0.0, epsilon = 0.0);
1671            Ok(0)
1672        });
1673
1674        assert_eq!(
1675            got,
1676            Err(DownstreamError(LaError::UnsupportedDimension {
1677                requested: 9,
1678                max: MAX_STACK_MATRIX_DISPATCH_DIM,
1679            }))
1680        );
1681    }
1682
1683    #[test]
1684    fn try_with_interval_matrix_converts_unsupported_dimension_error() {
1685        let got = try_with_interval_matrix!(8usize, |matrix| -> Result<
1686            IntervalDeterminantSign,
1687            DownstreamError,
1688        > { Ok(matrix.det_sign()?) },);
1689
1690        assert_eq!(
1691            got,
1692            Err(DownstreamError(LaError::UnsupportedDimension {
1693                requested: 8,
1694                max: MAX_INTERVAL_MATRIX_DIM,
1695            }))
1696        );
1697    }
1698
1699    #[cfg(feature = "exact")]
1700    #[test]
1701    fn try_with_rational_matrix_reports_unsupported_dimension() {
1702        let got = try_with_rational_matrix!(9usize, |matrix| -> Result<BigRational, LaError> {
1703            Ok(matrix.det())
1704        });
1705
1706        assert_eq!(
1707            got,
1708            Err(LaError::UnsupportedDimension {
1709                requested: 9,
1710                max: MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1711            })
1712        );
1713    }
1714
1715    #[cfg(feature = "exact")]
1716    #[test]
1717    fn try_with_rational_matrix_converts_unsupported_dimension_error() {
1718        let got =
1719            try_with_rational_matrix!(9usize, |matrix| -> Result<BigRational, DownstreamError> {
1720                Ok(matrix.det())
1721            });
1722
1723        assert_eq!(
1724            got,
1725            Err(DownstreamError(LaError::UnsupportedDimension {
1726                requested: 9,
1727                max: MAX_RATIONAL_MATRIX_DISPATCH_DIM,
1728            }))
1729        );
1730    }
1731}