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}