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}