Skip to main content

la_stack/
exact.rs

1//! Exact determinant sign via arbitrary-precision rational arithmetic.
2//!
3//! This module is only compiled when the `"exact"` Cargo feature is enabled.
4//!
5//! # Architecture
6//!
7//! `det_sign_exact` uses a two-stage adaptive-precision approach inspired by
8//! Shewchuk's robust geometric predicates:
9//!
10//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
11//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
12//!    immediately without allocating.
13//! 2. **Exact fallback**: convert entries to `BigRational` and run the Bareiss
14//!    algorithm (fraction-free Gaussian elimination) for a guaranteed-correct
15//!    sign.
16
17use num_bigint::{BigInt, Sign};
18use num_rational::BigRational;
19
20use crate::LaError;
21use crate::matrix::Matrix;
22
23/// Convert an `f64` to an exact `BigRational`.
24///
25/// Every finite `f64` is exactly representable as a rational number (`m × 2^e`),
26/// so this conversion is lossless.
27///
28/// # Panics
29/// Panics if `x` is NaN or infinite.
30fn f64_to_bigrational(x: f64) -> BigRational {
31    BigRational::from_float(x).expect("non-finite matrix entry in det_sign_exact")
32}
33
34/// Compute the exact determinant of a `D×D` matrix using the Bareiss algorithm
35/// (fraction-free Gaussian elimination) in `BigRational` arithmetic.
36///
37/// Returns the determinant as an exact `BigRational`.
38fn bareiss_det<const D: usize>(m: &Matrix<D>) -> BigRational {
39    if D == 0 {
40        return BigRational::from_integer(BigInt::from(1));
41    }
42    if D == 1 {
43        return f64_to_bigrational(m.rows[0][0]);
44    }
45
46    // Convert f64 entries to exact BigRational.
47    let mut a: Vec<Vec<BigRational>> = Vec::with_capacity(D);
48    for r in 0..D {
49        let mut row = Vec::with_capacity(D);
50        for c in 0..D {
51            row.push(f64_to_bigrational(m.rows[r][c]));
52        }
53        a.push(row);
54    }
55
56    let zero = BigRational::from_integer(BigInt::from(0));
57    let mut prev_pivot = BigRational::from_integer(BigInt::from(1));
58    let mut sign: i8 = 1;
59
60    for k in 0..D {
61        // Partial pivoting: find non-zero entry in column k at or below row k.
62        if a[k][k] == zero {
63            let mut found = false;
64            for i in (k + 1)..D {
65                if a[i][k] != zero {
66                    a.swap(k, i);
67                    sign = -sign;
68                    found = true;
69                    break;
70                }
71            }
72            if !found {
73                // Entire column below (and including) diagonal is zero → singular.
74                return zero;
75            }
76        }
77
78        // Bareiss elimination for rows below k.
79        for i in (k + 1)..D {
80            for j in (k + 1)..D {
81                // a[i][j] = (a[k][k] * a[i][j] - a[i][k] * a[k][j]) / prev_pivot
82                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
83            }
84            // Zero out the eliminated column entry (not needed for det, but keeps
85            // the matrix consistent for potential debugging).
86            a[i][k] = zero.clone();
87        }
88
89        prev_pivot = a[k][k].clone();
90    }
91
92    let det = &a[D - 1][D - 1];
93    if sign < 0 { -det } else { det.clone() }
94}
95
96impl<const D: usize> Matrix<D> {
97    /// Exact determinant sign using adaptive-precision arithmetic.
98    ///
99    /// Returns `1` if `det > 0`, `-1` if `det < 0`, and `0` if `det == 0` (singular).
100    ///
101    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
102    /// against a conservative error bound derived from the matrix permanent.
103    /// If the f64 result clearly exceeds the bound, the sign is returned
104    /// immediately without allocating.  Otherwise (and always for D ≥ 5), the
105    /// Bareiss algorithm runs in exact [`BigRational`] arithmetic.
106    ///
107    /// # When to use
108    ///
109    /// Use this when the sign of the determinant must be correct regardless of
110    /// floating-point conditioning (e.g. geometric predicates on near-degenerate
111    /// configurations).  For well-conditioned matrices the fast filter resolves
112    /// the sign without touching `BigRational`, so the overhead is minimal.
113    ///
114    /// # Examples
115    /// ```
116    /// use la_stack::prelude::*;
117    ///
118    /// let m = Matrix::<3>::from_rows([
119    ///     [1.0, 2.0, 3.0],
120    ///     [4.0, 5.0, 6.0],
121    ///     [7.0, 8.0, 9.0],
122    /// ]);
123    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
124    /// assert_eq!(m.det_sign_exact().unwrap(), 0);
125    ///
126    /// assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
127    /// ```
128    ///
129    /// # Errors
130    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
131    #[inline]
132    pub fn det_sign_exact(&self) -> Result<i8, LaError> {
133        // Validate all entries are finite before any arithmetic.
134        for r in 0..D {
135            for c in 0..D {
136                if !self.rows[r][c].is_finite() {
137                    return Err(LaError::NonFinite { col: c });
138                }
139            }
140        }
141
142        // Stage 1: f64 fast filter for D ≤ 4.
143        if let (Some(det_f64), Some(err)) = (self.det_direct(), self.det_errbound()) {
144            // When entries are large (e.g. near f64::MAX) the determinant can
145            // overflow to infinity even though every individual entry is finite.
146            // In that case the fast filter is inconclusive; fall through to the
147            // exact Bareiss path.
148            if det_f64.is_finite() {
149                if det_f64 > err {
150                    return Ok(1);
151                }
152                if det_f64 < -err {
153                    return Ok(-1);
154                }
155            }
156        }
157
158        // Stage 2: exact Bareiss fallback.
159        let det = bareiss_det(self);
160        Ok(match det.numer().sign() {
161            Sign::Plus => 1,
162            Sign::Minus => -1,
163            Sign::NoSign => 0,
164        })
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::DEFAULT_PIVOT_TOL;
172
173    #[test]
174    fn det_sign_exact_d0_is_positive() {
175        assert_eq!(Matrix::<0>::zero().det_sign_exact().unwrap(), 1);
176    }
177
178    #[test]
179    fn det_sign_exact_d1_positive() {
180        let m = Matrix::<1>::from_rows([[42.0]]);
181        assert_eq!(m.det_sign_exact().unwrap(), 1);
182    }
183
184    #[test]
185    fn det_sign_exact_d1_negative() {
186        let m = Matrix::<1>::from_rows([[-3.5]]);
187        assert_eq!(m.det_sign_exact().unwrap(), -1);
188    }
189
190    #[test]
191    fn det_sign_exact_d1_zero() {
192        let m = Matrix::<1>::from_rows([[0.0]]);
193        assert_eq!(m.det_sign_exact().unwrap(), 0);
194    }
195
196    #[test]
197    fn det_sign_exact_identity_2d() {
198        assert_eq!(Matrix::<2>::identity().det_sign_exact().unwrap(), 1);
199    }
200
201    #[test]
202    fn det_sign_exact_identity_3d() {
203        assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
204    }
205
206    #[test]
207    fn det_sign_exact_identity_4d() {
208        assert_eq!(Matrix::<4>::identity().det_sign_exact().unwrap(), 1);
209    }
210
211    #[test]
212    fn det_sign_exact_identity_5d() {
213        assert_eq!(Matrix::<5>::identity().det_sign_exact().unwrap(), 1);
214    }
215
216    #[test]
217    fn det_sign_exact_singular_duplicate_rows() {
218        let m = Matrix::<3>::from_rows([
219            [1.0, 2.0, 3.0],
220            [4.0, 5.0, 6.0],
221            [1.0, 2.0, 3.0], // duplicate of row 0
222        ]);
223        assert_eq!(m.det_sign_exact().unwrap(), 0);
224    }
225
226    #[test]
227    fn det_sign_exact_singular_linear_combination() {
228        // Row 2 = row 0 + row 1 in exact arithmetic.
229        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
230        assert_eq!(m.det_sign_exact().unwrap(), 0);
231    }
232
233    #[test]
234    fn det_sign_exact_negative_det_row_swap() {
235        // Swapping two rows of the identity negates the determinant.
236        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
237        assert_eq!(m.det_sign_exact().unwrap(), -1);
238    }
239
240    #[test]
241    fn det_sign_exact_negative_det_known() {
242        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
243        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
244        assert_eq!(m.det_sign_exact().unwrap(), -1);
245    }
246
247    #[test]
248    fn det_sign_exact_agrees_with_det_for_spd() {
249        // SPD matrix → positive determinant.
250        let m = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
251        assert_eq!(m.det_sign_exact().unwrap(), 1);
252        assert!(m.det(DEFAULT_PIVOT_TOL).unwrap() > 0.0);
253    }
254
255    /// Near-singular matrix with an exact perturbation.
256    ///
257    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
258    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
259    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
260    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
261    #[test]
262    fn det_sign_exact_near_singular_perturbation() {
263        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
264        let m = Matrix::<3>::from_rows([
265            [1.0 + perturbation, 2.0, 3.0],
266            [4.0, 5.0, 6.0],
267            [7.0, 8.0, 9.0],
268        ]);
269        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
270        assert_eq!(m.det_sign_exact().unwrap(), -1);
271    }
272
273    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
274    /// and never allocate `BigRational`.  We can't directly observe this,
275    /// but we verify correctness for a range of known signs.
276    #[test]
277    fn det_sign_exact_fast_filter_positive_4x4() {
278        let m = Matrix::<4>::from_rows([
279            [2.0, 1.0, 0.0, 0.0],
280            [1.0, 3.0, 1.0, 0.0],
281            [0.0, 1.0, 4.0, 1.0],
282            [0.0, 0.0, 1.0, 5.0],
283        ]);
284        // SPD tridiagonal → positive det.
285        assert_eq!(m.det_sign_exact().unwrap(), 1);
286    }
287
288    #[test]
289    fn det_sign_exact_fast_filter_negative_4x4() {
290        // Swap rows 0 and 1 of the above → negate det.
291        let m = Matrix::<4>::from_rows([
292            [1.0, 3.0, 1.0, 0.0],
293            [2.0, 1.0, 0.0, 0.0],
294            [0.0, 1.0, 4.0, 1.0],
295            [0.0, 0.0, 1.0, 5.0],
296        ]);
297        assert_eq!(m.det_sign_exact().unwrap(), -1);
298    }
299
300    #[test]
301    fn det_sign_exact_subnormal_entries() {
302        // Subnormal f64 values should convert losslessly.
303        let tiny = 5e-324_f64; // smallest positive subnormal
304        assert!(tiny.is_subnormal());
305
306        let m = Matrix::<2>::from_rows([[tiny, 0.0], [0.0, tiny]]);
307        // det = tiny^2 > 0
308        assert_eq!(m.det_sign_exact().unwrap(), 1);
309    }
310
311    #[test]
312    fn det_sign_exact_returns_err_on_nan() {
313        let m = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
314        assert_eq!(m.det_sign_exact(), Err(LaError::NonFinite { col: 0 }));
315    }
316
317    #[test]
318    fn det_sign_exact_returns_err_on_infinity() {
319        let m = Matrix::<2>::from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]);
320        assert_eq!(m.det_sign_exact(), Err(LaError::NonFinite { col: 0 }));
321    }
322
323    #[test]
324    fn det_sign_exact_returns_err_on_nan_5x5() {
325        // D ≥ 5 bypasses the fast filter, exercising the bareiss_det path.
326        let mut m = Matrix::<5>::identity();
327        m.set(2, 3, f64::NAN);
328        assert_eq!(m.det_sign_exact(), Err(LaError::NonFinite { col: 3 }));
329    }
330
331    #[test]
332    fn det_sign_exact_returns_err_on_infinity_5x5() {
333        let mut m = Matrix::<5>::identity();
334        m.set(0, 0, f64::INFINITY);
335        assert_eq!(m.det_sign_exact(), Err(LaError::NonFinite { col: 0 }));
336    }
337
338    #[test]
339    fn det_sign_exact_pivot_needed_5x5() {
340        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
341        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
342        let m = Matrix::<5>::from_rows([
343            [0.0, 1.0, 0.0, 0.0, 0.0],
344            [1.0, 0.0, 0.0, 0.0, 0.0],
345            [0.0, 0.0, 1.0, 0.0, 0.0],
346            [0.0, 0.0, 0.0, 1.0, 0.0],
347            [0.0, 0.0, 0.0, 0.0, 1.0],
348        ]);
349        assert_eq!(m.det_sign_exact().unwrap(), -1);
350    }
351
352    #[test]
353    fn det_sign_exact_5x5_known() {
354        // det of a permutation matrix with two swaps = +1 (even permutation).
355        let m = Matrix::<5>::from_rows([
356            [0.0, 1.0, 0.0, 0.0, 0.0],
357            [1.0, 0.0, 0.0, 0.0, 0.0],
358            [0.0, 0.0, 0.0, 1.0, 0.0],
359            [0.0, 0.0, 1.0, 0.0, 0.0],
360            [0.0, 0.0, 0.0, 0.0, 1.0],
361        ]);
362        // Two transpositions → even permutation → det = +1
363        assert_eq!(m.det_sign_exact().unwrap(), 1);
364    }
365
366    // -----------------------------------------------------------------------
367    // Direct tests for internal helpers (coverage of private functions)
368    // -----------------------------------------------------------------------
369
370    #[test]
371    fn det_errbound_d0_is_zero() {
372        assert_eq!(Matrix::<0>::zero().det_errbound(), Some(0.0));
373    }
374
375    #[test]
376    fn det_errbound_d1_is_zero() {
377        assert_eq!(Matrix::<1>::from_rows([[42.0]]).det_errbound(), Some(0.0));
378    }
379
380    #[test]
381    fn det_errbound_d2_positive() {
382        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
383        let bound = m.det_errbound().unwrap();
384        assert!(bound > 0.0);
385        // bound = ERR_COEFF_2 * (|1*4| + |2*3|) = ERR_COEFF_2 * 10
386        assert!(crate::ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
387    }
388
389    #[test]
390    fn det_errbound_d3_positive() {
391        let m = Matrix::<3>::identity();
392        let bound = m.det_errbound().unwrap();
393        assert!(bound > 0.0);
394    }
395
396    #[test]
397    fn det_errbound_d3_non_identity() {
398        // Non-identity matrix to exercise all code paths in D=3 case
399        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]]);
400        let bound = m.det_errbound().unwrap();
401        assert!(bound > 0.0);
402    }
403
404    #[test]
405    fn det_errbound_d4_positive() {
406        let m = Matrix::<4>::identity();
407        let bound = m.det_errbound().unwrap();
408        assert!(bound > 0.0);
409    }
410
411    #[test]
412    fn det_errbound_d4_non_identity() {
413        // Non-identity matrix to exercise all code paths in D=4 case
414        let m = Matrix::<4>::from_rows([
415            [1.0, 0.0, 0.0, 0.0],
416            [0.0, 2.0, 0.0, 0.0],
417            [0.0, 0.0, 3.0, 0.0],
418            [0.0, 0.0, 0.0, 4.0],
419        ]);
420        let bound = m.det_errbound().unwrap();
421        assert!(bound > 0.0);
422    }
423
424    #[test]
425    fn det_errbound_d5_is_none() {
426        assert_eq!(Matrix::<5>::identity().det_errbound(), None);
427    }
428
429    #[test]
430    fn bareiss_det_d0_is_one() {
431        let det = bareiss_det(&Matrix::<0>::zero());
432        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
433    }
434
435    #[test]
436    fn bareiss_det_d1_returns_entry() {
437        let det = bareiss_det(&Matrix::<1>::from_rows([[7.0]]));
438        assert_eq!(det, f64_to_bigrational(7.0));
439    }
440
441    #[test]
442    fn bareiss_det_d3_with_pivoting() {
443        // First column has zero on diagonal → exercises pivot swap + break.
444        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
445        let det = bareiss_det(&m);
446        // det of this permutation matrix = -1
447        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
448    }
449
450    #[test]
451    fn bareiss_det_singular_all_zeros_in_column() {
452        // Column 1 is all zeros below diagonal after elimination → singular.
453        let m = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
454        let det = bareiss_det(&m);
455        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
456    }
457
458    #[test]
459    fn det_sign_exact_overflow_determinant_finite_entries() {
460        // Entries near f64::MAX are finite, but the f64 determinant overflows
461        // to infinity.  The fast filter should be skipped and Bareiss should
462        // compute the correct positive sign.
463        let big = f64::MAX / 2.0;
464        assert!(big.is_finite());
465        let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
466        // det = big^2 > 0
467        assert_eq!(m.det_sign_exact().unwrap(), 1);
468    }
469}