Skip to main content

la_stack/
exact.rs

1//! Exact arithmetic operations via arbitrary-precision rational numbers.
2//!
3//! This module is only compiled when the `"exact"` Cargo feature is enabled.
4//!
5//! # Architecture
6//!
7//! ## Determinants
8//!
9//! All three determinant methods (`det_exact`, `det_exact_f64`, `det_sign_exact`)
10//! share the same integer-only Bareiss core (`bareiss_det_int`).  Each f64
11//! entry is decomposed via `f64_decompose` into `mantissa × 2^exponent`,
12//! all entries are scaled to a common `BigInt` matrix (shifting by
13//! `e - e_min`), and Bareiss elimination runs entirely in `BigInt`
14//! arithmetic — no `BigRational`, no GCD, no denominator tracking.
15//! The result is `(det_int, total_exp)` where `det = det_int × 2^(D × e_min)`.
16//! `bareiss_det` wraps this with `bigint_exp_to_bigrational` to reconstruct
17//! a reduced `BigRational`; `det_sign_exact` reads the sign directly from
18//! `det_int` (the scale factor is always positive).
19//!
20//! `det_sign_exact` adds a two-stage adaptive-precision optimisation inspired
21//! by Shewchuk's robust geometric predicates:
22//!
23//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
24//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
25//!    immediately without allocating.
26//! 2. **Exact fallback**: run integer-only Bareiss for a guaranteed-correct
27//!    sign.
28//!
29//! ## Linear system solve
30//!
31//! `solve_exact` and `solve_exact_f64` solve `A x = b` using Gaussian
32//! elimination with first-non-zero pivoting in `BigRational` arithmetic.
33//! Since all arithmetic is exact, any non-zero pivot gives the correct result
34//! (there is no numerical stability concern).  Every finite `f64` is exactly
35//! representable as a rational, so the result is provably correct.
36//!
37//! ## f64 → `BigRational` conversion
38//!
39//! All entry conversions use `f64_to_bigrational`, which decomposes the
40//! IEEE 754 binary64 bit representation (\[9\]) into sign, exponent, and
41//! significand and constructs a `BigRational` directly — avoiding the GCD
42//! normalization that `BigRational::from_float` performs.  See Goldberg
43//! \[10\] for background on floating-point representation and exact
44//! rational reconstruction.  Reference numbers refer to `REFERENCES.md`.
45
46use std::array::from_fn;
47
48use num_bigint::{BigInt, Sign};
49use num_rational::BigRational;
50use num_traits::ToPrimitive;
51
52use crate::LaError;
53use crate::matrix::Matrix;
54use crate::vector::Vector;
55
56/// Validate that all entries in a `D×D` matrix are finite (not NaN or infinite).
57///
58/// Returns `Ok(())` if all entries are finite, or `Err(LaError::NonFinite)` with
59/// the column of the first non-finite entry found.
60fn validate_finite<const D: usize>(m: &Matrix<D>) -> Result<(), LaError> {
61    for r in 0..D {
62        for c in 0..D {
63            if !m.rows[r][c].is_finite() {
64                return Err(LaError::NonFinite {
65                    row: Some(r),
66                    col: c,
67                });
68            }
69        }
70    }
71    Ok(())
72}
73
74/// Validate that all entries in a length-`D` vector are finite.
75///
76/// Returns `Ok(())` if all entries are finite, or `Err(LaError::NonFinite)` with
77/// the index of the first non-finite entry found.
78fn validate_finite_vec<const D: usize>(v: &Vector<D>) -> Result<(), LaError> {
79    for (i, &x) in v.data.iter().enumerate() {
80        if !x.is_finite() {
81            return Err(LaError::NonFinite { row: None, col: i });
82        }
83    }
84    Ok(())
85}
86
87/// Decompose a finite `f64` into its IEEE 754 components.
88///
89/// Returns `None` for ±0.0, or `Some((mantissa, exponent, is_negative))` where
90/// the value is exactly `(-1)^is_negative × mantissa × 2^exponent` and
91/// `mantissa` is odd (trailing zeros stripped).  See `REFERENCES.md` \[9-10\].
92///
93/// # Panics
94/// Panics if `x` is NaN or infinite.
95fn f64_decompose(x: f64) -> Option<(u64, i32, bool)> {
96    let bits = x.to_bits();
97    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
98    let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
99
100    // ±0.0
101    if biased_exp == 0 && fraction == 0 {
102        return None;
103    }
104
105    // NaN / Inf — callers must validate finiteness before reaching here.
106    assert!(biased_exp != 0x7FF, "non-finite f64 in exact conversion");
107
108    let (mantissa, raw_exp) = if biased_exp == 0 {
109        // Subnormal: (-1)^s × 0.fraction × 2^(-1022)
110        //          = (-1)^s × fraction × 2^(-1074)
111        (fraction, -1074_i32)
112    } else {
113        // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023)
114        //       = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075)
115        ((1u64 << 52) | fraction, biased_exp - 1075)
116    };
117
118    // Strip trailing zeros so the mantissa is odd.
119    let tz = mantissa.trailing_zeros();
120    let mantissa = mantissa >> tz;
121    let exponent = raw_exp + tz.cast_signed();
122    let is_negative = bits >> 63 != 0;
123
124    Some((mantissa, exponent, is_negative))
125}
126
127/// Convert an `f64` to an exact `BigRational` via IEEE 754 bit decomposition.
128///
129/// Every finite `f64` is exactly representable as `±m × 2^e` where `m` is a
130/// non-negative integer and `e` is an integer.  This function extracts `(m, e)`
131/// directly from the IEEE 754 binary64 bit layout \[9\], strips trailing zeros
132/// from `m` so the resulting fraction is already in lowest terms, then
133/// constructs a `BigRational` via `new_raw` — bypassing the GCD reduction
134/// that `BigRational::from_float` performs internally.
135///
136/// See `REFERENCES.md` \[9-10\] for the IEEE 754 standard and Goldberg's
137/// survey of floating-point representation.
138///
139/// # Panics
140/// Panics if `x` is NaN or infinite.
141fn f64_to_bigrational(x: f64) -> BigRational {
142    let Some((mantissa, exponent, is_negative)) = f64_decompose(x) else {
143        return BigRational::from_integer(BigInt::from(0));
144    };
145
146    let numer = if is_negative {
147        -BigInt::from(mantissa)
148    } else {
149        BigInt::from(mantissa)
150    };
151
152    if exponent >= 0 {
153        BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
154    } else {
155        BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
156    }
157}
158
159/// Convert a `BigInt × 2^exp` pair to a reduced `BigRational`.
160///
161/// When `exp < 0` (denominator is `2^(-exp)`), shared factors of 2 are
162/// stripped from `value` to keep the fraction in lowest terms without a
163/// full GCD computation.
164fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
165    if value == BigInt::from(0) {
166        return BigRational::from_integer(BigInt::from(0));
167    }
168
169    // Strip shared powers of 2 between value and the 2^(-exp) denominator.
170    if exp < 0
171        && let Some(tz) = value.trailing_zeros()
172    {
173        let reduce = tz.min(u64::from((-exp).cast_unsigned()));
174        value >>= reduce;
175        exp += i32::try_from(reduce).expect("reduce ≤ -exp which fits in i32");
176    }
177
178    if exp >= 0 {
179        BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
180    } else {
181        BigRational::new_raw(value, BigInt::from(1u32) << (-exp).cast_unsigned())
182    }
183}
184
185/// Compute the exact determinant using integer-only Bareiss elimination.
186///
187/// Returns `(det_int, scale_exp)` where the true determinant is
188/// `det_int × 2^scale_exp`.  Since the scale factor `2^scale_exp` is always
189/// positive, `det_int.sign()` gives the sign of the determinant directly.
190///
191/// All arithmetic is in `BigInt` — no `BigRational`, no GCD, no denominator
192/// tracking.  Each f64 entry is decomposed into `mantissa × 2^exponent` and
193/// scaled to a common base `2^e_min` so every entry becomes an integer.
194/// The Bareiss inner-loop division is exact (guaranteed by the algorithm).
195fn bareiss_det_int<const D: usize>(m: &Matrix<D>) -> (BigInt, i32) {
196    if D == 0 {
197        return (BigInt::from(1), 0);
198    }
199    if D == 1 {
200        return match f64_decompose(m.rows[0][0]) {
201            None => (BigInt::from(0), 0),
202            Some((mant, exp, neg)) => {
203                let v = if neg {
204                    -BigInt::from(mant)
205                } else {
206                    BigInt::from(mant)
207                };
208                (v, exp)
209            }
210        };
211    }
212
213    // Decompose all entries and find the minimum exponent.
214    let mut components = [[(0u64, 0i32, false); D]; D];
215    let mut e_min = i32::MAX;
216
217    for (r, row) in m.rows.iter().enumerate() {
218        for (c, &entry) in row.iter().enumerate() {
219            if let Some((mant, exp, neg)) = f64_decompose(entry) {
220                components[r][c] = (mant, exp, neg);
221                e_min = e_min.min(exp);
222            }
223            // Zero entries keep the default (0, 0, false); their exponent is
224            // excluded from e_min.
225        }
226    }
227
228    // All entries are zero → singular.
229    if e_min == i32::MAX {
230        return (BigInt::from(0), 0);
231    }
232
233    // Build the integer matrix: a[r][c] = (±mantissa) × 2^(exp − e_min).
234    let mut a: [[BigInt; D]; D] = from_fn(|r| {
235        from_fn(|c| {
236            let (mant, exp, neg) = components[r][c];
237            if mant == 0 {
238                BigInt::from(0)
239            } else {
240                let shift = (exp - e_min).cast_unsigned();
241                let v = BigInt::from(mant) << shift;
242                if neg { -v } else { v }
243            }
244        })
245    });
246
247    // Bareiss elimination in BigInt.
248    let zero = BigInt::from(0);
249    let mut prev_pivot = BigInt::from(1);
250    let mut sign: i8 = 1;
251
252    for k in 0..D {
253        // Pivot search.
254        if a[k][k] == zero {
255            let mut found = false;
256            for i in (k + 1)..D {
257                if a[i][k] != zero {
258                    a.swap(k, i);
259                    sign = -sign;
260                    found = true;
261                    break;
262                }
263            }
264            if !found {
265                return (BigInt::from(0), 0);
266            }
267        }
268
269        // Elimination.
270        for i in (k + 1)..D {
271            for j in (k + 1)..D {
272                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
273            }
274            a[i][k].clone_from(&zero);
275        }
276
277        prev_pivot.clone_from(&a[k][k]);
278    }
279
280    let det_int = if sign < 0 {
281        -&a[D - 1][D - 1]
282    } else {
283        a[D - 1][D - 1].clone()
284    };
285
286    // det(original) = det_int × 2^(D × e_min)
287    let d_i32 = i32::try_from(D).expect("dimension exceeds i32");
288    let total_exp = e_min
289        .checked_mul(d_i32)
290        .expect("exponent overflow in bareiss_det_int");
291
292    (det_int, total_exp)
293}
294
295/// Compute the exact determinant of a `D×D` matrix using integer-only Bareiss
296/// elimination and return the result as a `BigRational`.
297fn bareiss_det<const D: usize>(m: &Matrix<D>) -> BigRational {
298    let (det_int, total_exp) = bareiss_det_int(m);
299    bigint_exp_to_bigrational(det_int, total_exp)
300}
301
302/// Solve `A x = b` using Gaussian elimination with first-non-zero pivoting
303/// in `BigRational` arithmetic.
304///
305/// Since all arithmetic is exact, any non-zero pivot gives the correct result
306/// (no numerical stability concern).  This matches the pivoting strategy used
307/// by `bareiss_det`.
308///
309/// Returns the exact solution as `[BigRational; D]`.
310/// Returns `Err(LaError::Singular)` if the matrix is exactly singular.
311fn gauss_solve<const D: usize>(m: &Matrix<D>, b: &Vector<D>) -> Result<[BigRational; D], LaError> {
312    let zero = BigRational::from_integer(BigInt::from(0));
313
314    // Build matrix and RHS separately (cannot use [BigRational; D+1] augmented
315    // columns because const-generic expressions are unstable).
316    let mut mat: [[BigRational; D]; D] = from_fn(|r| from_fn(|c| f64_to_bigrational(m.rows[r][c])));
317    let mut rhs: [BigRational; D] = from_fn(|r| f64_to_bigrational(b.data[r]));
318
319    // Forward elimination with first-non-zero pivoting.
320    for k in 0..D {
321        // Find first non-zero pivot in column k at or below row k.
322        if mat[k][k] == zero {
323            if let Some(swap_row) = ((k + 1)..D).find(|&i| mat[i][k] != zero) {
324                mat.swap(k, swap_row);
325                rhs.swap(k, swap_row);
326            } else {
327                return Err(LaError::Singular { pivot_col: k });
328            }
329        }
330
331        // Eliminate below pivot.
332        let pivot = mat[k][k].clone();
333        for i in (k + 1)..D {
334            if mat[i][k] != zero {
335                let factor = &mat[i][k] / &pivot;
336                // We need index `j` to read mat[k][j] and write mat[i][j]
337                // (two distinct rows) — iterators can't borrow both.
338                #[allow(clippy::needless_range_loop)]
339                for j in (k + 1)..D {
340                    let term = &factor * &mat[k][j];
341                    mat[i][j] -= term;
342                }
343                let rhs_term = &factor * &rhs[k];
344                rhs[i] -= rhs_term;
345                mat[i][k] = zero.clone();
346            }
347        }
348    }
349
350    // Back-substitution.
351    let mut x: [BigRational; D] = from_fn(|_| zero.clone());
352    for i in (0..D).rev() {
353        let mut sum = rhs[i].clone();
354        for j in (i + 1)..D {
355            sum -= &mat[i][j] * &x[j];
356        }
357        x[i] = sum / &mat[i][i];
358    }
359
360    Ok(x)
361}
362
363impl<const D: usize> Matrix<D> {
364    /// Exact determinant using arbitrary-precision rational arithmetic.
365    ///
366    /// Returns the determinant as an exact [`BigRational`] value. Every finite
367    /// `f64` is exactly representable as a rational, so the conversion is
368    /// lossless and the result is provably correct.
369    ///
370    /// # When to use
371    ///
372    /// Use this when you need the exact determinant *value* — for example,
373    /// exact volume computation or distinguishing truly-degenerate simplices
374    /// from near-degenerate ones.  If you only need the *sign*, prefer
375    /// [`det_sign_exact`](Self::det_sign_exact) which has a fast f64 filter.
376    ///
377    /// # Examples
378    /// ```
379    /// use la_stack::prelude::*;
380    ///
381    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
382    /// let det = m.det_exact().unwrap();
383    /// // det = 1*4 - 2*3 = -2  (exact)
384    /// assert_eq!(det, BigRational::from_integer((-2).into()));
385    /// ```
386    ///
387    /// # Errors
388    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
389    #[inline]
390    pub fn det_exact(&self) -> Result<BigRational, LaError> {
391        validate_finite(self)?;
392        Ok(bareiss_det(self))
393    }
394
395    /// Exact determinant converted to `f64`.
396    ///
397    /// Computes the exact [`BigRational`] determinant via [`det_exact`](Self::det_exact)
398    /// and converts it to the nearest `f64`.  This is useful when you want the
399    /// most accurate f64 determinant possible without committing to `BigRational`
400    /// in your downstream code.
401    ///
402    /// # Examples
403    /// ```
404    /// use la_stack::prelude::*;
405    ///
406    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
407    /// let det = m.det_exact_f64().unwrap();
408    /// assert!((det - (-2.0)).abs() <= f64::EPSILON);
409    /// ```
410    ///
411    /// # Errors
412    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
413    /// Returns [`LaError::Overflow`] if the exact determinant is too large to
414    /// represent as a finite `f64`.
415    #[inline]
416    pub fn det_exact_f64(&self) -> Result<f64, LaError> {
417        let exact = self.det_exact()?;
418        let val = exact.to_f64().unwrap_or(f64::INFINITY);
419        if val.is_finite() {
420            Ok(val)
421        } else {
422            Err(LaError::Overflow { index: None })
423        }
424    }
425
426    /// Exact linear system solve using arbitrary-precision rational arithmetic.
427    ///
428    /// Solves `A x = b` where `A` is `self` and `b` is the given vector.
429    /// Returns the exact solution as `[BigRational; D]`.  Every finite `f64`
430    /// is exactly representable as a rational, so the conversion is lossless
431    /// and the result is provably correct.
432    ///
433    /// # When to use
434    ///
435    /// Use this when you need a provably correct solution — for example,
436    /// exact circumcenter computation for near-degenerate simplices where
437    /// f64 arithmetic may produce wildly wrong results.
438    ///
439    /// # Examples
440    /// ```
441    /// use la_stack::prelude::*;
442    ///
443    /// // A x = b  where A = [[1,2],[3,4]], b = [5, 11]  →  x = [1, 2]
444    /// let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
445    /// let b = Vector::<2>::new([5.0, 11.0]);
446    /// let x = a.solve_exact(b).unwrap();
447    /// assert_eq!(x[0], BigRational::from_integer(1.into()));
448    /// assert_eq!(x[1], BigRational::from_integer(2.into()));
449    /// ```
450    ///
451    /// # Errors
452    /// Returns [`LaError::NonFinite`] if any matrix or vector entry is NaN or
453    /// infinite.
454    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
455    #[inline]
456    pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
457        validate_finite(self)?;
458        validate_finite_vec(&b)?;
459        gauss_solve(self, &b)
460    }
461
462    /// Exact linear system solve converted to `f64`.
463    ///
464    /// Computes the exact [`BigRational`] solution via
465    /// [`solve_exact`](Self::solve_exact) and converts each component to the
466    /// nearest `f64`.  This is useful when you want the most accurate f64
467    /// solution possible without committing to `BigRational` downstream.
468    ///
469    /// # Examples
470    /// ```
471    /// use la_stack::prelude::*;
472    ///
473    /// let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
474    /// let b = Vector::<2>::new([5.0, 11.0]);
475    /// let x = a.solve_exact_f64(b).unwrap().into_array();
476    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
477    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
478    /// ```
479    ///
480    /// # Errors
481    /// Returns [`LaError::NonFinite`] if any matrix or vector entry is NaN or
482    /// infinite.
483    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
484    /// Returns [`LaError::Overflow`] if any component of the exact solution is
485    /// too large to represent as a finite `f64`.
486    #[inline]
487    pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
488        let exact = self.solve_exact(b)?;
489        let mut result = [0.0f64; D];
490        for (i, val) in exact.iter().enumerate() {
491            let f = val.to_f64().unwrap_or(f64::INFINITY);
492            if !f.is_finite() {
493                return Err(LaError::Overflow { index: Some(i) });
494            }
495            result[i] = f;
496        }
497        Ok(Vector::new(result))
498    }
499
500    /// Exact determinant sign using adaptive-precision arithmetic.
501    ///
502    /// Returns `1` if `det > 0`, `-1` if `det < 0`, and `0` if `det == 0` (singular).
503    ///
504    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
505    /// against a conservative error bound derived from the matrix permanent.
506    /// If the f64 result clearly exceeds the bound, the sign is returned
507    /// immediately without allocating.  Otherwise (and always for D ≥ 5),
508    /// integer-only Bareiss elimination (`bareiss_det_int`) computes the exact
509    /// sign without constructing any `BigRational` values.
510    ///
511    /// # When to use
512    ///
513    /// Use this when the sign of the determinant must be correct regardless of
514    /// floating-point conditioning (e.g. geometric predicates on near-degenerate
515    /// configurations).  For well-conditioned matrices the fast filter resolves
516    /// the sign without touching `BigRational`, so the overhead is minimal.
517    ///
518    /// # Examples
519    /// ```
520    /// use la_stack::prelude::*;
521    ///
522    /// let m = Matrix::<3>::from_rows([
523    ///     [1.0, 2.0, 3.0],
524    ///     [4.0, 5.0, 6.0],
525    ///     [7.0, 8.0, 9.0],
526    /// ]);
527    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
528    /// assert_eq!(m.det_sign_exact().unwrap(), 0);
529    ///
530    /// assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
531    /// ```
532    ///
533    /// # Errors
534    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
535    #[inline]
536    pub fn det_sign_exact(&self) -> Result<i8, LaError> {
537        validate_finite(self)?;
538
539        // Stage 1: f64 fast filter for D ≤ 4.
540        if let (Some(det_f64), Some(err)) = (self.det_direct(), self.det_errbound()) {
541            // When entries are large (e.g. near f64::MAX) the determinant can
542            // overflow to infinity even though every individual entry is finite.
543            // In that case the fast filter is inconclusive; fall through to the
544            // exact Bareiss path.
545            if det_f64.is_finite() {
546                if det_f64 > err {
547                    return Ok(1);
548                }
549                if det_f64 < -err {
550                    return Ok(-1);
551                }
552            }
553        }
554
555        // Stage 2: integer Bareiss fallback — the 2^(D×e_min) scale factor
556        // is always positive, so det_int.sign() == det(A).sign().
557        let (det_int, _) = bareiss_det_int(self);
558        Ok(match det_int.sign() {
559            Sign::Plus => 1,
560            Sign::Minus => -1,
561            Sign::NoSign => 0,
562        })
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::DEFAULT_PIVOT_TOL;
570
571    use pastey::paste;
572
573    // -----------------------------------------------------------------------
574    // Macro-generated per-dimension tests (D=2..5)
575    // -----------------------------------------------------------------------
576
577    macro_rules! gen_det_exact_tests {
578        ($d:literal) => {
579            paste! {
580                #[test]
581                fn [<det_exact_identity_ $d d>]() {
582                    let det = Matrix::<$d>::identity().det_exact().unwrap();
583                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
584                }
585
586                #[test]
587                fn [<det_exact_err_on_nan_ $d d>]() {
588                    let mut m = Matrix::<$d>::identity();
589                    m.set(0, 0, f64::NAN);
590                    assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
591                }
592
593                #[test]
594                fn [<det_exact_err_on_inf_ $d d>]() {
595                    let mut m = Matrix::<$d>::identity();
596                    m.set(0, 0, f64::INFINITY);
597                    assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
598                }
599            }
600        };
601    }
602
603    gen_det_exact_tests!(2);
604    gen_det_exact_tests!(3);
605    gen_det_exact_tests!(4);
606    gen_det_exact_tests!(5);
607
608    macro_rules! gen_det_exact_f64_tests {
609        ($d:literal) => {
610            paste! {
611                #[test]
612                fn [<det_exact_f64_identity_ $d d>]() {
613                    let det = Matrix::<$d>::identity().det_exact_f64().unwrap();
614                    assert!((det - 1.0).abs() <= f64::EPSILON);
615                }
616
617                #[test]
618                fn [<det_exact_f64_err_on_nan_ $d d>]() {
619                    let mut m = Matrix::<$d>::identity();
620                    m.set(0, 0, f64::NAN);
621                    assert_eq!(m.det_exact_f64(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
622                }
623            }
624        };
625    }
626
627    gen_det_exact_f64_tests!(2);
628    gen_det_exact_f64_tests!(3);
629    gen_det_exact_f64_tests!(4);
630    gen_det_exact_f64_tests!(5);
631
632    /// For D ≤ 4, `det_exact_f64` should agree with `det_direct` on
633    /// well-conditioned matrices.
634    macro_rules! gen_det_exact_f64_agrees_with_det_direct {
635        ($d:literal) => {
636            paste! {
637                #[test]
638                #[allow(clippy::cast_precision_loss)]
639                fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
640                    // Diagonally dominant → well-conditioned.
641                    let mut rows = [[0.0f64; $d]; $d];
642                    for r in 0..$d {
643                        for c in 0..$d {
644                            rows[r][c] = if r == c {
645                                (r as f64) + f64::from($d) + 1.0
646                            } else {
647                                0.1 / ((r + c + 1) as f64)
648                            };
649                        }
650                    }
651                    let m = Matrix::<$d>::from_rows(rows);
652                    let exact = m.det_exact_f64().unwrap();
653                    let direct = m.det_direct().unwrap();
654                    let eps = direct.abs().mul_add(1e-12, 1e-12);
655                    assert!((exact - direct).abs() <= eps);
656                }
657            }
658        };
659    }
660
661    gen_det_exact_f64_agrees_with_det_direct!(2);
662    gen_det_exact_f64_agrees_with_det_direct!(3);
663    gen_det_exact_f64_agrees_with_det_direct!(4);
664
665    #[test]
666    fn det_sign_exact_d0_is_positive() {
667        assert_eq!(Matrix::<0>::zero().det_sign_exact().unwrap(), 1);
668    }
669
670    #[test]
671    fn det_sign_exact_d1_positive() {
672        let m = Matrix::<1>::from_rows([[42.0]]);
673        assert_eq!(m.det_sign_exact().unwrap(), 1);
674    }
675
676    #[test]
677    fn det_sign_exact_d1_negative() {
678        let m = Matrix::<1>::from_rows([[-3.5]]);
679        assert_eq!(m.det_sign_exact().unwrap(), -1);
680    }
681
682    #[test]
683    fn det_sign_exact_d1_zero() {
684        let m = Matrix::<1>::from_rows([[0.0]]);
685        assert_eq!(m.det_sign_exact().unwrap(), 0);
686    }
687
688    #[test]
689    fn det_sign_exact_identity_2d() {
690        assert_eq!(Matrix::<2>::identity().det_sign_exact().unwrap(), 1);
691    }
692
693    #[test]
694    fn det_sign_exact_identity_3d() {
695        assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
696    }
697
698    #[test]
699    fn det_sign_exact_identity_4d() {
700        assert_eq!(Matrix::<4>::identity().det_sign_exact().unwrap(), 1);
701    }
702
703    #[test]
704    fn det_sign_exact_identity_5d() {
705        assert_eq!(Matrix::<5>::identity().det_sign_exact().unwrap(), 1);
706    }
707
708    #[test]
709    fn det_sign_exact_singular_duplicate_rows() {
710        let m = Matrix::<3>::from_rows([
711            [1.0, 2.0, 3.0],
712            [4.0, 5.0, 6.0],
713            [1.0, 2.0, 3.0], // duplicate of row 0
714        ]);
715        assert_eq!(m.det_sign_exact().unwrap(), 0);
716    }
717
718    #[test]
719    fn det_sign_exact_singular_linear_combination() {
720        // Row 2 = row 0 + row 1 in exact arithmetic.
721        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
722        assert_eq!(m.det_sign_exact().unwrap(), 0);
723    }
724
725    #[test]
726    fn det_sign_exact_negative_det_row_swap() {
727        // Swapping two rows of the identity negates the determinant.
728        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
729        assert_eq!(m.det_sign_exact().unwrap(), -1);
730    }
731
732    #[test]
733    fn det_sign_exact_negative_det_known() {
734        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
735        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
736        assert_eq!(m.det_sign_exact().unwrap(), -1);
737    }
738
739    #[test]
740    fn det_sign_exact_agrees_with_det_for_spd() {
741        // SPD matrix → positive determinant.
742        let m = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
743        assert_eq!(m.det_sign_exact().unwrap(), 1);
744        assert!(m.det(DEFAULT_PIVOT_TOL).unwrap() > 0.0);
745    }
746
747    /// Near-singular matrix with an exact perturbation.
748    ///
749    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
750    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
751    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
752    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
753    #[test]
754    fn det_sign_exact_near_singular_perturbation() {
755        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
756        let m = Matrix::<3>::from_rows([
757            [1.0 + perturbation, 2.0, 3.0],
758            [4.0, 5.0, 6.0],
759            [7.0, 8.0, 9.0],
760        ]);
761        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
762        assert_eq!(m.det_sign_exact().unwrap(), -1);
763    }
764
765    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
766    /// and never allocate `BigRational`.  We can't directly observe this,
767    /// but we verify correctness for a range of known signs.
768    #[test]
769    fn det_sign_exact_fast_filter_positive_4x4() {
770        let m = Matrix::<4>::from_rows([
771            [2.0, 1.0, 0.0, 0.0],
772            [1.0, 3.0, 1.0, 0.0],
773            [0.0, 1.0, 4.0, 1.0],
774            [0.0, 0.0, 1.0, 5.0],
775        ]);
776        // SPD tridiagonal → positive det.
777        assert_eq!(m.det_sign_exact().unwrap(), 1);
778    }
779
780    #[test]
781    fn det_sign_exact_fast_filter_negative_4x4() {
782        // Swap rows 0 and 1 of the above → negate det.
783        let m = Matrix::<4>::from_rows([
784            [1.0, 3.0, 1.0, 0.0],
785            [2.0, 1.0, 0.0, 0.0],
786            [0.0, 1.0, 4.0, 1.0],
787            [0.0, 0.0, 1.0, 5.0],
788        ]);
789        assert_eq!(m.det_sign_exact().unwrap(), -1);
790    }
791
792    #[test]
793    fn det_sign_exact_subnormal_entries() {
794        // Subnormal f64 values should convert losslessly.
795        let tiny = 5e-324_f64; // smallest positive subnormal
796        assert!(tiny.is_subnormal());
797
798        let m = Matrix::<2>::from_rows([[tiny, 0.0], [0.0, tiny]]);
799        // det = tiny^2 > 0
800        assert_eq!(m.det_sign_exact().unwrap(), 1);
801    }
802
803    #[test]
804    fn det_sign_exact_returns_err_on_nan() {
805        let m = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
806        assert_eq!(
807            m.det_sign_exact(),
808            Err(LaError::NonFinite {
809                row: Some(0),
810                col: 0
811            })
812        );
813    }
814
815    #[test]
816    fn det_sign_exact_returns_err_on_infinity() {
817        let m = Matrix::<2>::from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]);
818        assert_eq!(
819            m.det_sign_exact(),
820            Err(LaError::NonFinite {
821                row: Some(0),
822                col: 0
823            })
824        );
825    }
826
827    #[test]
828    fn det_sign_exact_returns_err_on_nan_5x5() {
829        // D ≥ 5 bypasses the fast filter, exercising the bareiss_det path.
830        let mut m = Matrix::<5>::identity();
831        m.set(2, 3, f64::NAN);
832        assert_eq!(
833            m.det_sign_exact(),
834            Err(LaError::NonFinite {
835                row: Some(2),
836                col: 3
837            })
838        );
839    }
840
841    #[test]
842    fn det_sign_exact_returns_err_on_infinity_5x5() {
843        let mut m = Matrix::<5>::identity();
844        m.set(0, 0, f64::INFINITY);
845        assert_eq!(
846            m.det_sign_exact(),
847            Err(LaError::NonFinite {
848                row: Some(0),
849                col: 0
850            })
851        );
852    }
853
854    #[test]
855    fn det_sign_exact_pivot_needed_5x5() {
856        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
857        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
858        let m = Matrix::<5>::from_rows([
859            [0.0, 1.0, 0.0, 0.0, 0.0],
860            [1.0, 0.0, 0.0, 0.0, 0.0],
861            [0.0, 0.0, 1.0, 0.0, 0.0],
862            [0.0, 0.0, 0.0, 1.0, 0.0],
863            [0.0, 0.0, 0.0, 0.0, 1.0],
864        ]);
865        assert_eq!(m.det_sign_exact().unwrap(), -1);
866    }
867
868    #[test]
869    fn det_sign_exact_5x5_known() {
870        // det of a permutation matrix with two swaps = +1 (even permutation).
871        let m = Matrix::<5>::from_rows([
872            [0.0, 1.0, 0.0, 0.0, 0.0],
873            [1.0, 0.0, 0.0, 0.0, 0.0],
874            [0.0, 0.0, 0.0, 1.0, 0.0],
875            [0.0, 0.0, 1.0, 0.0, 0.0],
876            [0.0, 0.0, 0.0, 0.0, 1.0],
877        ]);
878        // Two transpositions → even permutation → det = +1
879        assert_eq!(m.det_sign_exact().unwrap(), 1);
880    }
881
882    // -----------------------------------------------------------------------
883    // Direct tests for internal helpers (coverage of private functions)
884    // -----------------------------------------------------------------------
885
886    #[test]
887    fn det_errbound_d0_is_zero() {
888        assert_eq!(Matrix::<0>::zero().det_errbound(), Some(0.0));
889    }
890
891    #[test]
892    fn det_errbound_d1_is_zero() {
893        assert_eq!(Matrix::<1>::from_rows([[42.0]]).det_errbound(), Some(0.0));
894    }
895
896    #[test]
897    fn det_errbound_d2_positive() {
898        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
899        let bound = m.det_errbound().unwrap();
900        assert!(bound > 0.0);
901        // bound = ERR_COEFF_2 * (|1*4| + |2*3|) = ERR_COEFF_2 * 10
902        assert!(crate::ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
903    }
904
905    #[test]
906    fn det_errbound_d3_positive() {
907        let m = Matrix::<3>::identity();
908        let bound = m.det_errbound().unwrap();
909        assert!(bound > 0.0);
910    }
911
912    #[test]
913    fn det_errbound_d3_non_identity() {
914        // Non-identity matrix to exercise all code paths in D=3 case
915        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]]);
916        let bound = m.det_errbound().unwrap();
917        assert!(bound > 0.0);
918    }
919
920    #[test]
921    fn det_errbound_d4_positive() {
922        let m = Matrix::<4>::identity();
923        let bound = m.det_errbound().unwrap();
924        assert!(bound > 0.0);
925    }
926
927    #[test]
928    fn det_errbound_d4_non_identity() {
929        // Non-identity matrix to exercise all code paths in D=4 case
930        let m = Matrix::<4>::from_rows([
931            [1.0, 0.0, 0.0, 0.0],
932            [0.0, 2.0, 0.0, 0.0],
933            [0.0, 0.0, 3.0, 0.0],
934            [0.0, 0.0, 0.0, 4.0],
935        ]);
936        let bound = m.det_errbound().unwrap();
937        assert!(bound > 0.0);
938    }
939
940    #[test]
941    fn det_errbound_d5_is_none() {
942        assert_eq!(Matrix::<5>::identity().det_errbound(), None);
943    }
944
945    // -----------------------------------------------------------------------
946    // f64_decompose tests
947    // -----------------------------------------------------------------------
948
949    #[test]
950    fn f64_decompose_zero() {
951        assert!(f64_decompose(0.0).is_none());
952        assert!(f64_decompose(-0.0).is_none());
953    }
954
955    #[test]
956    fn f64_decompose_one() {
957        let (mant, exp, neg) = f64_decompose(1.0).unwrap();
958        assert_eq!(mant, 1);
959        assert_eq!(exp, 0);
960        assert!(!neg);
961    }
962
963    #[test]
964    fn f64_decompose_negative() {
965        let (mant, exp, neg) = f64_decompose(-3.5).unwrap();
966        // -3.5 = -7 × 2^(-1), mantissa is 7 (odd after stripping)
967        assert_eq!(mant, 7);
968        assert_eq!(exp, -1);
969        assert!(neg);
970    }
971
972    #[test]
973    fn f64_decompose_subnormal() {
974        let tiny = 5e-324_f64;
975        assert!(tiny.is_subnormal());
976        let (mant, exp, neg) = f64_decompose(tiny).unwrap();
977        assert_eq!(mant, 1);
978        assert_eq!(exp, -1074);
979        assert!(!neg);
980    }
981
982    #[test]
983    fn f64_decompose_power_of_two() {
984        let (mant, exp, neg) = f64_decompose(1024.0).unwrap();
985        assert_eq!(mant, 1);
986        assert_eq!(exp, 10); // 1024 = 2^10
987        assert!(!neg);
988    }
989
990    #[test]
991    #[should_panic(expected = "non-finite f64 in exact conversion")]
992    fn f64_decompose_panics_on_nan() {
993        f64_decompose(f64::NAN);
994    }
995
996    // -----------------------------------------------------------------------
997    // bareiss_det_int tests
998    // -----------------------------------------------------------------------
999
1000    #[test]
1001    fn bareiss_det_int_d0() {
1002        let (det, exp) = bareiss_det_int(&Matrix::<0>::zero());
1003        assert_eq!(det, BigInt::from(1));
1004        assert_eq!(exp, 0);
1005    }
1006
1007    #[test]
1008    fn bareiss_det_int_d1_value() {
1009        // 7.0 = 7 × 2^0
1010        let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[7.0]]));
1011        assert_eq!(det, BigInt::from(7));
1012        assert_eq!(exp, 0);
1013    }
1014
1015    #[test]
1016    fn bareiss_det_int_d1_zero() {
1017        let (det, _) = bareiss_det_int(&Matrix::<1>::from_rows([[0.0]]));
1018        assert_eq!(det, BigInt::from(0));
1019    }
1020
1021    #[test]
1022    fn bareiss_det_int_d2_known() {
1023        // det([[1,2],[3,4]]) = -2
1024        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1025        let (det_int, total_exp) = bareiss_det_int(&m);
1026        // Reconstruct and verify.
1027        let det = bigint_exp_to_bigrational(det_int, total_exp);
1028        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
1029    }
1030
1031    #[test]
1032    fn bareiss_det_int_all_zeros() {
1033        let (det, _) = bareiss_det_int(&Matrix::<3>::zero());
1034        assert_eq!(det, BigInt::from(0));
1035    }
1036
1037    #[test]
1038    fn bareiss_det_int_sign_matches_det_sign_exact() {
1039        // The sign of det_int should match det_sign_exact for various matrices.
1040        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1041        let (det_int, _) = bareiss_det_int(&m);
1042        assert_eq!(det_int.sign(), Sign::Minus); // det = -1
1043    }
1044
1045    #[test]
1046    fn bareiss_det_int_fractional_entries() {
1047        // Entries with negative exponents: 0.5 = 1×2^(-1), 0.25 = 1×2^(-2).
1048        // det([[0.5, 0.25], [1.0, 1.0]]) = 0.5×1.0 − 0.25×1.0 = 0.25
1049        let m = Matrix::<2>::from_rows([[0.5, 0.25], [1.0, 1.0]]);
1050        let (det_int, total_exp) = bareiss_det_int(&m);
1051        let det = bigint_exp_to_bigrational(det_int, total_exp);
1052        assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
1053    }
1054
1055    #[test]
1056    fn bareiss_det_int_d1_negative() {
1057        // -3.5 = -7 × 2^(-1)
1058        let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[-3.5]]));
1059        assert_eq!(det, BigInt::from(-7));
1060        assert_eq!(exp, -1);
1061    }
1062
1063    #[test]
1064    fn bareiss_det_int_d1_fractional() {
1065        // 0.5 = 1 × 2^(-1)
1066        let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[0.5]]));
1067        assert_eq!(det, BigInt::from(1));
1068        assert_eq!(exp, -1);
1069    }
1070
1071    #[test]
1072    fn bareiss_det_int_d3_with_pivoting() {
1073        // Zero on diagonal → exercises pivot swap inside bareiss_det_int.
1074        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1075        let (det_int, total_exp) = bareiss_det_int(&m);
1076        let det = bigint_exp_to_bigrational(det_int, total_exp);
1077        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1078    }
1079
1080    /// Per AGENTS.md: dimension-generic tests must cover D=2–5.
1081    macro_rules! gen_bareiss_det_int_identity_tests {
1082        ($d:literal) => {
1083            paste! {
1084                #[test]
1085                fn [<bareiss_det_int_identity_ $d d>]() {
1086                    let (det_int, total_exp) = bareiss_det_int(&Matrix::<$d>::identity());
1087                    let det = bigint_exp_to_bigrational(det_int, total_exp);
1088                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1089                }
1090            }
1091        };
1092    }
1093
1094    gen_bareiss_det_int_identity_tests!(2);
1095    gen_bareiss_det_int_identity_tests!(3);
1096    gen_bareiss_det_int_identity_tests!(4);
1097    gen_bareiss_det_int_identity_tests!(5);
1098
1099    // -----------------------------------------------------------------------
1100    // bigint_exp_to_bigrational tests
1101    // -----------------------------------------------------------------------
1102
1103    #[test]
1104    fn bigint_exp_to_bigrational_zero() {
1105        let r = bigint_exp_to_bigrational(BigInt::from(0), -50);
1106        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
1107    }
1108
1109    #[test]
1110    fn bigint_exp_to_bigrational_positive_exp() {
1111        // 3 × 2^2 = 12
1112        let r = bigint_exp_to_bigrational(BigInt::from(3), 2);
1113        assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
1114    }
1115
1116    #[test]
1117    fn bigint_exp_to_bigrational_negative_exp_reduced() {
1118        // 6 × 2^(-2) = 6/4 → reduced to 3/2 (strip one shared factor of 2)
1119        let r = bigint_exp_to_bigrational(BigInt::from(6), -2);
1120        assert_eq!(*r.numer(), BigInt::from(3));
1121        assert_eq!(*r.denom(), BigInt::from(2));
1122    }
1123
1124    #[test]
1125    fn bigint_exp_to_bigrational_negative_exp_already_odd() {
1126        // 3 × 2^(-2) = 3/4 (already in lowest terms since 3 is odd)
1127        let r = bigint_exp_to_bigrational(BigInt::from(3), -2);
1128        assert_eq!(*r.numer(), BigInt::from(3));
1129        assert_eq!(*r.denom(), BigInt::from(4));
1130    }
1131
1132    #[test]
1133    fn bigint_exp_to_bigrational_negative_value() {
1134        // -5 × 2^1 = -10
1135        let r = bigint_exp_to_bigrational(BigInt::from(-5), 1);
1136        assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
1137    }
1138
1139    #[test]
1140    fn bigint_exp_to_bigrational_negative_value_with_denominator() {
1141        // -3 × 2^(-2) = -3/4
1142        let r = bigint_exp_to_bigrational(BigInt::from(-3), -2);
1143        assert_eq!(*r.numer(), BigInt::from(-3));
1144        assert_eq!(*r.denom(), BigInt::from(4));
1145    }
1146
1147    // -----------------------------------------------------------------------
1148    // bareiss_det (wrapper) tests
1149    // -----------------------------------------------------------------------
1150
1151    #[test]
1152    fn bareiss_det_d0_is_one() {
1153        let det = bareiss_det(&Matrix::<0>::zero());
1154        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1155    }
1156
1157    #[test]
1158    fn bareiss_det_d1_returns_entry() {
1159        let det = bareiss_det(&Matrix::<1>::from_rows([[7.0]]));
1160        assert_eq!(det, f64_to_bigrational(7.0));
1161    }
1162
1163    #[test]
1164    fn bareiss_det_d3_with_pivoting() {
1165        // First column has zero on diagonal → exercises pivot swap + break.
1166        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1167        let det = bareiss_det(&m);
1168        // det of this permutation matrix = -1
1169        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1170    }
1171
1172    #[test]
1173    fn bareiss_det_singular_all_zeros_in_column() {
1174        // Column 1 is all zeros below diagonal after elimination → singular.
1175        let m = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1176        let det = bareiss_det(&m);
1177        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
1178    }
1179
1180    #[test]
1181    fn det_sign_exact_overflow_determinant_finite_entries() {
1182        // Entries near f64::MAX are finite, but the f64 determinant overflows
1183        // to infinity.  The fast filter should be skipped and Bareiss should
1184        // compute the correct positive sign.
1185        let big = f64::MAX / 2.0;
1186        assert!(big.is_finite());
1187        let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
1188        // det = big^2 > 0
1189        assert_eq!(m.det_sign_exact().unwrap(), 1);
1190    }
1191
1192    // -----------------------------------------------------------------------
1193    // det_exact: dimension-specific tests
1194    // -----------------------------------------------------------------------
1195
1196    #[test]
1197    fn det_exact_d0_is_one() {
1198        let det = Matrix::<0>::zero().det_exact().unwrap();
1199        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1200    }
1201
1202    #[test]
1203    fn det_exact_known_2x2() {
1204        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
1205        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1206        let det = m.det_exact().unwrap();
1207        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
1208    }
1209
1210    #[test]
1211    fn det_exact_singular_returns_zero() {
1212        // Rows in arithmetic progression → exactly singular.
1213        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]);
1214        let det = m.det_exact().unwrap();
1215        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
1216    }
1217
1218    #[test]
1219    fn det_exact_near_singular_perturbation() {
1220        // Same 2^-50 perturbation case: exact det = -3 × 2^-50.
1221        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
1222        let m = Matrix::<3>::from_rows([
1223            [1.0 + perturbation, 2.0, 3.0],
1224            [4.0, 5.0, 6.0],
1225            [7.0, 8.0, 9.0],
1226        ]);
1227        let det = m.det_exact().unwrap();
1228        // det should be exactly -3 × 2^-50.
1229        let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
1230        assert_eq!(det, expected);
1231    }
1232
1233    #[test]
1234    fn det_exact_5x5_permutation() {
1235        // Single swap (rows 0↔1) → det = -1.
1236        let m = Matrix::<5>::from_rows([
1237            [0.0, 1.0, 0.0, 0.0, 0.0],
1238            [1.0, 0.0, 0.0, 0.0, 0.0],
1239            [0.0, 0.0, 1.0, 0.0, 0.0],
1240            [0.0, 0.0, 0.0, 1.0, 0.0],
1241            [0.0, 0.0, 0.0, 0.0, 1.0],
1242        ]);
1243        let det = m.det_exact().unwrap();
1244        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1245    }
1246
1247    // -----------------------------------------------------------------------
1248    // det_exact_f64: dimension-specific tests
1249    // -----------------------------------------------------------------------
1250
1251    #[test]
1252    fn det_exact_f64_known_2x2() {
1253        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1254        let det = m.det_exact_f64().unwrap();
1255        assert!((det - (-2.0)).abs() <= f64::EPSILON);
1256    }
1257
1258    #[test]
1259    fn det_exact_f64_overflow_returns_err() {
1260        // Entries near f64::MAX produce a determinant too large for f64.
1261        let big = f64::MAX / 2.0;
1262        let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
1263        // det = big^2, which overflows f64.
1264        assert_eq!(m.det_exact_f64(), Err(LaError::Overflow { index: None }));
1265    }
1266
1267    // -----------------------------------------------------------------------
1268    // solve_exact: macro-generated per-dimension tests (D=2..5)
1269    // -----------------------------------------------------------------------
1270
1271    /// Helper: build an arbitrary RHS vector for dimension `$d`.
1272    fn arbitrary_rhs<const D: usize>() -> Vector<D> {
1273        let values = [1.0, -2.5, 3.0, 0.25, -4.0];
1274        let mut arr = [0.0f64; D];
1275        for (dst, src) in arr.iter_mut().zip(values.iter()) {
1276            *dst = *src;
1277        }
1278        Vector::<D>::new(arr)
1279    }
1280
1281    macro_rules! gen_solve_exact_tests {
1282        ($d:literal) => {
1283            paste! {
1284                #[test]
1285                fn [<solve_exact_identity_ $d d>]() {
1286                    let a = Matrix::<$d>::identity();
1287                    let b = arbitrary_rhs::<$d>();
1288                    let x = a.solve_exact(b).unwrap();
1289                    for (i, xi) in x.iter().enumerate() {
1290                        assert_eq!(*xi, f64_to_bigrational(b.data[i]));
1291                    }
1292                }
1293
1294                #[test]
1295                fn [<solve_exact_err_on_nan_matrix_ $d d>]() {
1296                    let mut a = Matrix::<$d>::identity();
1297                    a.set(0, 0, f64::NAN);
1298                    let b = arbitrary_rhs::<$d>();
1299                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1300                }
1301
1302                #[test]
1303                fn [<solve_exact_err_on_inf_matrix_ $d d>]() {
1304                    let mut a = Matrix::<$d>::identity();
1305                    a.set(0, 0, f64::INFINITY);
1306                    let b = arbitrary_rhs::<$d>();
1307                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1308                }
1309
1310                #[test]
1311                fn [<solve_exact_err_on_nan_vector_ $d d>]() {
1312                    let a = Matrix::<$d>::identity();
1313                    let mut b_arr = [1.0f64; $d];
1314                    b_arr[0] = f64::NAN;
1315                    let b = Vector::<$d>::new(b_arr);
1316                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: 0 }));
1317                }
1318
1319                #[test]
1320                fn [<solve_exact_err_on_inf_vector_ $d d>]() {
1321                    let a = Matrix::<$d>::identity();
1322                    let mut b_arr = [1.0f64; $d];
1323                    b_arr[$d - 1] = f64::INFINITY;
1324                    let b = Vector::<$d>::new(b_arr);
1325                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: $d - 1 }));
1326                }
1327
1328                #[test]
1329                fn [<solve_exact_singular_ $d d>]() {
1330                    // Zero matrix is singular.
1331                    let a = Matrix::<$d>::zero();
1332                    let b = arbitrary_rhs::<$d>();
1333                    assert_eq!(a.solve_exact(b), Err(LaError::Singular { pivot_col: 0 }));
1334                }
1335            }
1336        };
1337    }
1338
1339    gen_solve_exact_tests!(2);
1340    gen_solve_exact_tests!(3);
1341    gen_solve_exact_tests!(4);
1342    gen_solve_exact_tests!(5);
1343
1344    macro_rules! gen_solve_exact_f64_tests {
1345        ($d:literal) => {
1346            paste! {
1347                #[test]
1348                fn [<solve_exact_f64_identity_ $d d>]() {
1349                    let a = Matrix::<$d>::identity();
1350                    let b = arbitrary_rhs::<$d>();
1351                    let x = a.solve_exact_f64(b).unwrap().into_array();
1352                    for i in 0..$d {
1353                        assert!((x[i] - b.data[i]).abs() <= f64::EPSILON);
1354                    }
1355                }
1356
1357                #[test]
1358                fn [<solve_exact_f64_err_on_nan_ $d d>]() {
1359                    let mut a = Matrix::<$d>::identity();
1360                    a.set(0, 0, f64::NAN);
1361                    let b = arbitrary_rhs::<$d>();
1362                    assert_eq!(a.solve_exact_f64(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1363                }
1364            }
1365        };
1366    }
1367
1368    gen_solve_exact_f64_tests!(2);
1369    gen_solve_exact_f64_tests!(3);
1370    gen_solve_exact_f64_tests!(4);
1371    gen_solve_exact_f64_tests!(5);
1372
1373    /// For D ≤ 4, `solve_exact_f64` should agree with `Lu::solve_vec` on
1374    /// well-conditioned matrices.
1375    macro_rules! gen_solve_exact_f64_agrees_with_lu {
1376        ($d:literal) => {
1377            paste! {
1378                #[test]
1379                #[allow(clippy::cast_precision_loss)]
1380                fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
1381                    // Diagonally dominant → well-conditioned.
1382                    let mut rows = [[0.0f64; $d]; $d];
1383                    for r in 0..$d {
1384                        for c in 0..$d {
1385                            rows[r][c] = if r == c {
1386                                (r as f64) + f64::from($d) + 1.0
1387                            } else {
1388                                0.1 / ((r + c + 1) as f64)
1389                            };
1390                        }
1391                    }
1392                    let a = Matrix::<$d>::from_rows(rows);
1393                    let b = arbitrary_rhs::<$d>();
1394                    let exact = a.solve_exact_f64(b).unwrap().into_array();
1395                    let lu_sol = a.lu(DEFAULT_PIVOT_TOL).unwrap()
1396                        .solve_vec(b).unwrap().into_array();
1397                    for i in 0..$d {
1398                        let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
1399                        assert!((exact[i] - lu_sol[i]).abs() <= eps);
1400                    }
1401                }
1402            }
1403        };
1404    }
1405
1406    gen_solve_exact_f64_agrees_with_lu!(2);
1407    gen_solve_exact_f64_agrees_with_lu!(3);
1408    gen_solve_exact_f64_agrees_with_lu!(4);
1409    gen_solve_exact_f64_agrees_with_lu!(5);
1410
1411    // -----------------------------------------------------------------------
1412    // solve_exact: dimension-specific tests
1413    // -----------------------------------------------------------------------
1414
1415    #[test]
1416    fn solve_exact_d0_returns_empty() {
1417        let a = Matrix::<0>::zero();
1418        let b = Vector::<0>::zero();
1419        let x = a.solve_exact(b).unwrap();
1420        assert!(x.is_empty());
1421    }
1422
1423    #[test]
1424    fn solve_exact_known_2x2() {
1425        // [[1,2],[3,4]] x = [5, 11] → x = [1, 2]
1426        let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1427        let b = Vector::<2>::new([5.0, 11.0]);
1428        let x = a.solve_exact(b).unwrap();
1429        assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
1430        assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
1431    }
1432
1433    #[test]
1434    fn solve_exact_pivoting_needed() {
1435        // First column has zero on diagonal → pivot swap required.
1436        let a = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1437        let b = Vector::<3>::new([2.0, 3.0, 4.0]);
1438        let x = a.solve_exact(b).unwrap();
1439        // x = [3, 2, 4]
1440        assert_eq!(x[0], f64_to_bigrational(3.0));
1441        assert_eq!(x[1], f64_to_bigrational(2.0));
1442        assert_eq!(x[2], f64_to_bigrational(4.0));
1443    }
1444
1445    #[test]
1446    fn solve_exact_fractional_result() {
1447        // [[2, 1], [1, 3]] x = [1, 1] → x = [2/5, 1/5]
1448        let a = Matrix::<2>::from_rows([[2.0, 1.0], [1.0, 3.0]]);
1449        let b = Vector::<2>::new([1.0, 1.0]);
1450        let x = a.solve_exact(b).unwrap();
1451        assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
1452        assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
1453    }
1454
1455    #[test]
1456    fn solve_exact_singular_duplicate_rows() {
1457        let a = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]]);
1458        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
1459        assert!(matches!(a.solve_exact(b), Err(LaError::Singular { .. })));
1460    }
1461
1462    #[test]
1463    fn solve_exact_5x5_permutation() {
1464        // Permutation matrix (swap rows 0↔1): P x = b → x = P^T b.
1465        let a = Matrix::<5>::from_rows([
1466            [0.0, 1.0, 0.0, 0.0, 0.0],
1467            [1.0, 0.0, 0.0, 0.0, 0.0],
1468            [0.0, 0.0, 1.0, 0.0, 0.0],
1469            [0.0, 0.0, 0.0, 1.0, 0.0],
1470            [0.0, 0.0, 0.0, 0.0, 1.0],
1471        ]);
1472        let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
1473        let x = a.solve_exact(b).unwrap();
1474        assert_eq!(x[0], f64_to_bigrational(20.0));
1475        assert_eq!(x[1], f64_to_bigrational(10.0));
1476        assert_eq!(x[2], f64_to_bigrational(30.0));
1477        assert_eq!(x[3], f64_to_bigrational(40.0));
1478        assert_eq!(x[4], f64_to_bigrational(50.0));
1479    }
1480
1481    // -----------------------------------------------------------------------
1482    // solve_exact_f64: dimension-specific tests
1483    // -----------------------------------------------------------------------
1484
1485    #[test]
1486    fn solve_exact_f64_known_2x2() {
1487        let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1488        let b = Vector::<2>::new([5.0, 11.0]);
1489        let x = a.solve_exact_f64(b).unwrap().into_array();
1490        assert!((x[0] - 1.0).abs() <= f64::EPSILON);
1491        assert!((x[1] - 2.0).abs() <= f64::EPSILON);
1492    }
1493
1494    #[test]
1495    fn solve_exact_f64_overflow_returns_err() {
1496        // [[1/big, 0], [0, 1/big]] x = [big, big] → x = [big², big²],
1497        // which overflows f64.
1498        let big = f64::MAX / 2.0;
1499        let a = Matrix::<2>::from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]);
1500        let b = Vector::<2>::new([big, big]);
1501        assert_eq!(
1502            a.solve_exact_f64(b),
1503            Err(LaError::Overflow { index: Some(0) })
1504        );
1505    }
1506
1507    // -----------------------------------------------------------------------
1508    // gauss_solve: internal helper tests
1509    // -----------------------------------------------------------------------
1510
1511    #[test]
1512    fn gauss_solve_d0_returns_empty() {
1513        let a = Matrix::<0>::zero();
1514        let b = Vector::<0>::zero();
1515        assert_eq!(gauss_solve(&a, &b).unwrap().len(), 0);
1516    }
1517
1518    #[test]
1519    fn gauss_solve_d1() {
1520        let a = Matrix::<1>::from_rows([[2.0]]);
1521        let b = Vector::<1>::new([6.0]);
1522        let x = gauss_solve(&a, &b).unwrap();
1523        assert_eq!(x[0], f64_to_bigrational(3.0));
1524    }
1525
1526    #[test]
1527    fn gauss_solve_singular_column_all_zero() {
1528        let a = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1529        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
1530        assert_eq!(gauss_solve(&a, &b), Err(LaError::Singular { pivot_col: 1 }));
1531    }
1532
1533    // -----------------------------------------------------------------------
1534    // f64_to_bigrational tests
1535    // -----------------------------------------------------------------------
1536
1537    #[test]
1538    fn f64_to_bigrational_positive_zero() {
1539        let r = f64_to_bigrational(0.0);
1540        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
1541    }
1542
1543    #[test]
1544    fn f64_to_bigrational_negative_zero() {
1545        let r = f64_to_bigrational(-0.0);
1546        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
1547    }
1548
1549    #[test]
1550    fn f64_to_bigrational_one() {
1551        let r = f64_to_bigrational(1.0);
1552        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
1553    }
1554
1555    #[test]
1556    fn f64_to_bigrational_negative_one() {
1557        let r = f64_to_bigrational(-1.0);
1558        assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
1559    }
1560
1561    #[test]
1562    fn f64_to_bigrational_half() {
1563        let r = f64_to_bigrational(0.5);
1564        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
1565    }
1566
1567    #[test]
1568    fn f64_to_bigrational_quarter() {
1569        let r = f64_to_bigrational(0.25);
1570        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
1571    }
1572
1573    #[test]
1574    fn f64_to_bigrational_negative_three_and_a_half() {
1575        // -3.5 = -7/2
1576        let r = f64_to_bigrational(-3.5);
1577        assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
1578    }
1579
1580    #[test]
1581    fn f64_to_bigrational_integer() {
1582        let r = f64_to_bigrational(42.0);
1583        assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
1584    }
1585
1586    #[test]
1587    fn f64_to_bigrational_power_of_two() {
1588        let r = f64_to_bigrational(1024.0);
1589        assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
1590    }
1591
1592    #[test]
1593    fn f64_to_bigrational_subnormal() {
1594        let tiny = 5e-324_f64; // smallest positive subnormal
1595        assert!(tiny.is_subnormal());
1596        let r = f64_to_bigrational(tiny);
1597        // 5e-324 = 1 × 2^(-1074)
1598        assert_eq!(
1599            r,
1600            BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
1601        );
1602    }
1603
1604    #[test]
1605    fn f64_to_bigrational_already_lowest_terms() {
1606        // 0.5 should produce numer=1, denom=2 (already reduced).
1607        let r = f64_to_bigrational(0.5);
1608        assert_eq!(*r.numer(), BigInt::from(1));
1609        assert_eq!(*r.denom(), BigInt::from(2));
1610    }
1611
1612    #[test]
1613    fn f64_to_bigrational_round_trip() {
1614        // -0.0 is excluded: it maps to BigRational(0) which round-trips
1615        // to +0.0 (correct; tested separately in f64_to_bigrational_negative_zero).
1616        let values = [
1617            0.0,
1618            1.0,
1619            -1.0,
1620            0.5,
1621            0.25,
1622            0.1,
1623            42.0,
1624            -3.5,
1625            1e10,
1626            1e-10,
1627            f64::MAX / 2.0,
1628            f64::MIN_POSITIVE,
1629            5e-324,
1630        ];
1631        for &v in &values {
1632            let r = f64_to_bigrational(v);
1633            let back = r.to_f64().expect("round-trip to_f64 failed");
1634            assert!(
1635                v.to_bits() == back.to_bits(),
1636                "round-trip failed for {v}: got {back}"
1637            );
1638        }
1639    }
1640
1641    #[test]
1642    #[should_panic(expected = "non-finite f64 in exact conversion")]
1643    fn f64_to_bigrational_panics_on_nan() {
1644        f64_to_bigrational(f64::NAN);
1645    }
1646
1647    #[test]
1648    #[should_panic(expected = "non-finite f64 in exact conversion")]
1649    fn f64_to_bigrational_panics_on_inf() {
1650        f64_to_bigrational(f64::INFINITY);
1651    }
1652
1653    #[test]
1654    #[should_panic(expected = "non-finite f64 in exact conversion")]
1655    fn f64_to_bigrational_panics_on_neg_inf() {
1656        f64_to_bigrational(f64::NEG_INFINITY);
1657    }
1658
1659    // -----------------------------------------------------------------------
1660    // validate_finite_vec tests
1661    // -----------------------------------------------------------------------
1662
1663    #[test]
1664    fn validate_finite_vec_ok() {
1665        assert!(validate_finite_vec(&Vector::<3>::new([1.0, 2.0, 3.0])).is_ok());
1666    }
1667
1668    #[test]
1669    fn validate_finite_vec_err_on_nan() {
1670        assert_eq!(
1671            validate_finite_vec(&Vector::<2>::new([f64::NAN, 1.0])),
1672            Err(LaError::NonFinite { row: None, col: 0 })
1673        );
1674    }
1675
1676    #[test]
1677    fn validate_finite_vec_err_on_inf() {
1678        assert_eq!(
1679            validate_finite_vec(&Vector::<2>::new([1.0, f64::NEG_INFINITY])),
1680            Err(LaError::NonFinite { row: None, col: 1 })
1681        );
1682    }
1683
1684    // -----------------------------------------------------------------------
1685    // validate_finite tests
1686    // -----------------------------------------------------------------------
1687
1688    #[test]
1689    fn validate_finite_ok_for_finite() {
1690        assert!(validate_finite(&Matrix::<3>::identity()).is_ok());
1691    }
1692
1693    #[test]
1694    fn validate_finite_err_on_nan() {
1695        let mut m = Matrix::<2>::identity();
1696        m.set(1, 0, f64::NAN);
1697        assert_eq!(
1698            validate_finite(&m),
1699            Err(LaError::NonFinite {
1700                row: Some(1),
1701                col: 0
1702            })
1703        );
1704    }
1705
1706    #[test]
1707    fn validate_finite_err_on_inf() {
1708        let mut m = Matrix::<2>::identity();
1709        m.set(0, 1, f64::NEG_INFINITY);
1710        assert_eq!(
1711            validate_finite(&m),
1712            Err(LaError::NonFinite {
1713                row: Some(0),
1714                col: 1
1715            })
1716        );
1717    }
1718}