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