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` with a hybrid
32//! algorithm that reuses the integer-only Bareiss core used for
33//! determinants.  Matrix and RHS entries are decomposed via
34//! `f64_decompose` into `mantissa × 2^exponent`, scaled to a shared
35//! base `2^e_min`, and assembled into a `BigInt` augmented system
36//! `(A | b)`.  Forward elimination runs entirely in `BigInt` with
37//! fraction-free Bareiss updates — no `BigRational`, no GCD
38//! normalisation in the `O(D³)` phase.  Once the system is upper
39//! triangular, back-substitution is performed in `BigRational`, where
40//! fractions are inherent; this phase is only `O(D²)` so the rational
41//! overhead is modest.  First-non-zero pivoting is used throughout;
42//! since all arithmetic is exact, any non-zero pivot gives the correct
43//! result (no numerical stability concern).  Every finite `f64` is
44//! exactly representable as a rational, so the result is provably
45//! correct.
46//!
47//! ## f64 → integer decomposition
48//!
49//! Both the determinant and solve paths share a single conversion
50//! primitive, `f64_decompose`, which extracts `(mantissa, exponent,
51//! sign)` from the IEEE 754 binary64 bit representation (\[9\]).  The
52//! determinant path combines those components into a `BigInt` matrix
53//! (for Bareiss) and a `2^(D × e_min)` scale factor, while the solve
54//! path builds a `BigInt` augmented system and lifts the
55//! upper-triangular result into `BigRational` for back-substitution.
56//! See Goldberg \[10\] for background on floating-point representation
57//! and exact rational reconstruction.  Reference numbers refer to
58//! `REFERENCES.md`.
59//!
60//! ## Validation
61//!
62//! `decompose_matrix` / `decompose_vec` fold an `is_finite()` check
63//! into the same pass that decomposes each entry, returning
64//! `Err(LaError::NonFinite { row, col })` on the first NaN or ±∞
65//! encountered.  This error is propagated through `bareiss_det_int`,
66//! `bareiss_det`, and `gauss_solve` via the `?` operator, so every
67//! public entry point that reaches the integer-Bareiss core is
68//! automatically validated — `f64_decompose` itself is therefore
69//! never called with non-finite input from the public API.
70
71use core::hint::cold_path;
72use core::mem::take;
73use std::array::from_fn;
74
75use num_bigint::{BigInt, Sign};
76use num_rational::BigRational;
77use num_traits::ToPrimitive;
78
79use crate::LaError;
80use crate::matrix::Matrix;
81use crate::vector::Vector;
82
83/// Decompose a finite `f64` into its IEEE 754 components.
84///
85/// Returns `None` for ±0.0, or `Some((mantissa, exponent, is_negative))` where
86/// the value is exactly `(-1)^is_negative × mantissa × 2^exponent` and
87/// `mantissa` is odd (trailing zeros stripped).  See `REFERENCES.md` \[9-10\].
88///
89/// # Panics
90/// Panics if `x` is NaN or infinite.
91fn f64_decompose(x: f64) -> Option<(u64, i32, bool)> {
92    let bits = x.to_bits();
93    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
94    let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
95
96    // ±0.0
97    if biased_exp == 0 && fraction == 0 {
98        return None;
99    }
100
101    // NaN / Inf — callers must validate finiteness before reaching here.
102    assert!(biased_exp != 0x7FF, "non-finite f64 in exact conversion");
103
104    let (mantissa, raw_exp) = if biased_exp == 0 {
105        // Subnormal: (-1)^s × 0.fraction × 2^(-1022)
106        //          = (-1)^s × fraction × 2^(-1074)
107        (fraction, -1074_i32)
108    } else {
109        // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023)
110        //       = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075)
111        ((1u64 << 52) | fraction, biased_exp - 1075)
112    };
113
114    // Strip trailing zeros so the mantissa is odd.
115    let tz = mantissa.trailing_zeros();
116    let mantissa = mantissa >> tz;
117    let exponent = raw_exp + tz.cast_signed();
118    let is_negative = bits >> 63 != 0;
119
120    Some((mantissa, exponent, is_negative))
121}
122
123/// Convert a `BigInt × 2^exp` pair to a reduced `BigRational`.
124///
125/// When `exp < 0` (denominator is `2^(-exp)`), shared factors of 2 are
126/// stripped from `value` to keep the fraction in lowest terms without a
127/// full GCD computation.
128fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
129    if value == BigInt::from(0) {
130        return BigRational::from_integer(BigInt::from(0));
131    }
132
133    // Strip shared powers of 2 between value and the 2^(-exp) denominator.
134    if exp < 0
135        && let Some(tz) = value.trailing_zeros()
136    {
137        let reduce = tz.min(u64::from((-exp).cast_unsigned()));
138        value >>= reduce;
139        exp += i32::try_from(reduce).expect("reduce ≤ -exp which fits in i32");
140    }
141
142    if exp >= 0 {
143        BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
144    } else {
145        BigRational::new_raw(value, BigInt::from(1u32) << (-exp).cast_unsigned())
146    }
147}
148
149// -----------------------------------------------------------------------
150// Shared integer-Bareiss primitives
151// -----------------------------------------------------------------------
152//
153// Both `bareiss_det_int` (determinants) and `gauss_solve` (linear system
154// solve) follow the same pipeline: decompose every f64 entry into
155// `(mantissa, exponent, is_negative)`, track the minimum exponent across
156// non-zero entries, scale each entry by `2^(exp − e_min)` to build a
157// fully-integer `BigInt` matrix, and run Bareiss fraction-free forward
158// elimination.  The helpers below factor out each stage so the two
159// callers differ only in post-processing (± sign for det, back-sub for
160// solve) and in whether they carry a RHS through the elimination.
161
162/// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`.
163///
164/// Zero entries have `mantissa == 0`; the other fields are unused in that
165/// case.  `Default` yields such a zero component, which is what the
166/// per-entry initialiser in `decompose_matrix` / `decompose_vec` produces
167/// for ±0.0 cells.
168#[derive(Clone, Copy, Default)]
169struct Component {
170    mantissa: u64,
171    exponent: i32,
172    is_negative: bool,
173}
174
175/// Decompose every entry of a `D×D` matrix via `f64_decompose`,
176/// validating finiteness in the same pass.  Returns the per-entry
177/// components and the minimum exponent across non-zero entries.  If
178/// every entry is zero, the exponent is `i32::MAX`.
179///
180/// # Errors
181/// Returns [`LaError::NonFinite`] with `row: Some(r), col: c` pointing
182/// at the first non-finite entry encountered (row-major order).
183fn decompose_matrix<const D: usize>(m: &Matrix<D>) -> Result<([[Component; D]; D], i32), LaError> {
184    let mut components = [[Component::default(); D]; D];
185    let mut e_min = i32::MAX;
186    for (r, row) in m.rows.iter().enumerate() {
187        for (c, &entry) in row.iter().enumerate() {
188            if !entry.is_finite() {
189                cold_path();
190                return Err(LaError::non_finite_cell(r, c));
191            }
192            if let Some((mantissa, exponent, is_negative)) = f64_decompose(entry) {
193                components[r][c] = Component {
194                    mantissa,
195                    exponent,
196                    is_negative,
197                };
198                e_min = e_min.min(exponent);
199            }
200        }
201    }
202    Ok((components, e_min))
203}
204
205/// Decompose every entry of a length-`D` vector via `f64_decompose`,
206/// validating finiteness in the same pass.  Returns the per-entry
207/// components and the minimum exponent across non-zero entries.  If
208/// every entry is zero, the exponent is `i32::MAX`.
209///
210/// # Errors
211/// Returns [`LaError::NonFinite`] with `row: None, col: i` pointing at
212/// the first non-finite entry encountered.
213fn decompose_vec<const D: usize>(v: &Vector<D>) -> Result<([Component; D], i32), LaError> {
214    let mut components = [Component::default(); D];
215    let mut e_min = i32::MAX;
216    for (i, &entry) in v.data.iter().enumerate() {
217        if !entry.is_finite() {
218            cold_path();
219            return Err(LaError::non_finite_at(i));
220        }
221        if let Some((mantissa, exponent, is_negative)) = f64_decompose(entry) {
222            components[i] = Component {
223                mantissa,
224                exponent,
225                is_negative,
226            };
227            e_min = e_min.min(exponent);
228        }
229    }
230    Ok((components, e_min))
231}
232
233/// Convert a single decomposed component to its scaled `BigInt`
234/// representation: `(±mantissa) << (exp − e_min)`.  Zero components map
235/// to `BigInt::from(0)`.
236#[inline]
237fn component_to_bigint(c: Component, e_min: i32) -> BigInt {
238    if c.mantissa == 0 {
239        BigInt::from(0)
240    } else {
241        let v = BigInt::from(c.mantissa) << (c.exponent - e_min).cast_unsigned();
242        if c.is_negative { -v } else { v }
243    }
244}
245
246/// Build a `D×D` integer matrix from a component table, scaled to the
247/// shared base `2^e_min`.
248fn build_bigint_matrix<const D: usize>(
249    components: &[[Component; D]; D],
250    e_min: i32,
251) -> [[BigInt; D]; D] {
252    from_fn(|r| from_fn(|c| component_to_bigint(components[r][c], e_min)))
253}
254
255/// Build a length-`D` integer vector from a component array, scaled to
256/// the shared base `2^e_min`.
257fn build_bigint_vec<const D: usize>(components: &[Component; D], e_min: i32) -> [BigInt; D] {
258    from_fn(|i| component_to_bigint(components[i], e_min))
259}
260
261/// Outcome of a Bareiss forward-elimination pass.
262#[derive(Debug)]
263enum BareissResult {
264    /// Elimination completed; `sign` is `±1` based on the parity of row
265    /// swaps (relevant for determinants; solves discard it).
266    Upper { sign: i8 },
267    /// Column `pivot_col` has no non-zero pivot at or below its diagonal.
268    Singular { pivot_col: usize },
269}
270
271/// Run Bareiss fraction-free forward elimination on the `D×D` integer
272/// matrix `a`, optionally augmented with a length-`D` RHS vector.
273///
274/// When `rhs` is `Some`, row swaps and the inner-loop Bareiss update are
275/// mirrored on the RHS (treating it as column `D+1` of an augmented
276/// system).  On return, `a` is upper triangular and the last pivot lives
277/// in `a[D-1][D-1]`.
278///
279/// First-non-zero pivoting is used: since all arithmetic is exact, any
280/// non-zero pivot is valid — no tolerance is required.
281fn bareiss_forward_eliminate<const D: usize>(
282    a: &mut [[BigInt; D]; D],
283    mut rhs: Option<&mut [BigInt; D]>,
284) -> BareissResult {
285    let zero = BigInt::from(0);
286    let mut prev_pivot = BigInt::from(1);
287    let mut sign: i8 = 1;
288
289    for k in 0..D {
290        // First-non-zero pivot search.
291        if a[k][k] == zero {
292            let mut found = false;
293            for i in (k + 1)..D {
294                if a[i][k] != zero {
295                    a.swap(k, i);
296                    if let Some(r) = &mut rhs {
297                        r.swap(k, i);
298                    }
299                    sign = -sign;
300                    found = true;
301                    break;
302                }
303            }
304            if !found {
305                cold_path();
306                return BareissResult::Singular { pivot_col: k };
307            }
308        }
309
310        // Elimination.  The Bareiss update reads the current `a[i][k]`
311        // in both the inner `j`-loop and the RHS update, so zero it only
312        // *after* those reads.
313        for i in (k + 1)..D {
314            for j in (k + 1)..D {
315                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
316            }
317            if let Some(r) = &mut rhs {
318                r[i] = (&a[k][k] * &r[i] - &a[i][k] * &r[k]) / &prev_pivot;
319            }
320            a[i][k].clone_from(&zero);
321        }
322
323        prev_pivot.clone_from(&a[k][k]);
324    }
325
326    // Post-conditions (debug builds only): `a` is upper triangular with
327    // non-zero pivots.  These catch future regressions in the inner-loop
328    // update or pivot-search logic without runtime cost in release.
329    // Indexed iteration is clearer than iterator chains here because the
330    // checks read disjoint cells across rows and columns at each step.
331    #[cfg(debug_assertions)]
332    #[allow(clippy::needless_range_loop)]
333    for k in 0..D {
334        assert_ne!(a[k][k], zero, "pivot at ({k}, {k}) must be non-zero");
335        for i in (k + 1)..D {
336            assert_eq!(a[i][k], zero, "sub-diagonal at ({i}, {k}) must be zero");
337        }
338    }
339
340    BareissResult::Upper { sign }
341}
342
343/// Compute the exact determinant using integer-only Bareiss elimination.
344///
345/// Returns `(det_int, scale_exp)` where the true determinant is
346/// `det_int × 2^scale_exp`.  Since the scale factor `2^scale_exp` is always
347/// positive, `det_int.sign()` gives the sign of the determinant directly.
348///
349/// All arithmetic is in `BigInt` — no `BigRational`, no GCD, no denominator
350/// tracking.  Each f64 entry is decomposed into `mantissa × 2^exponent` and
351/// scaled to a common base `2^e_min` so every entry becomes an integer.
352/// The Bareiss inner-loop division is exact (guaranteed by the algorithm).
353///
354/// # Errors
355/// Returns [`LaError::NonFinite`] (propagated from `decompose_matrix`) if
356/// any matrix entry is NaN or infinite.
357fn bareiss_det_int<const D: usize>(m: &Matrix<D>) -> Result<(BigInt, i32), LaError> {
358    // D == 0 has no `a[D-1][D-1]` to read; shortcut to the empty-product
359    // determinant.
360    if D == 0 {
361        return Ok((BigInt::from(1), 0));
362    }
363
364    let (components, e_min) = decompose_matrix(m)?;
365
366    // All entries are zero → singular (det = 0).
367    if e_min == i32::MAX {
368        return Ok((BigInt::from(0), 0));
369    }
370
371    let mut a = build_bigint_matrix(&components, e_min);
372    let sign = match bareiss_forward_eliminate(&mut a, None) {
373        BareissResult::Upper { sign } => sign,
374        BareissResult::Singular { .. } => {
375            cold_path();
376            return Ok((BigInt::from(0), 0));
377        }
378    };
379
380    let det_int = if sign < 0 {
381        -&a[D - 1][D - 1]
382    } else {
383        a[D - 1][D - 1].clone()
384    };
385
386    // det(original) = det_int × 2^(D × e_min)
387    let d_i32 = i32::try_from(D).expect("dimension exceeds i32");
388    let total_exp = e_min
389        .checked_mul(d_i32)
390        .expect("exponent overflow in bareiss_det_int");
391
392    Ok((det_int, total_exp))
393}
394
395/// Compute the exact determinant of a `D×D` matrix using integer-only Bareiss
396/// elimination and return the result as a `BigRational`.
397///
398/// # Errors
399/// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
400fn bareiss_det<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
401    let (det_int, total_exp) = bareiss_det_int(m)?;
402    Ok(bigint_exp_to_bigrational(det_int, total_exp))
403}
404
405/// Solve `A x = b` using a hybrid BigInt/BigRational algorithm.
406///
407/// Forward elimination runs entirely in `BigInt` using fraction-free
408/// Bareiss updates on the augmented system `(A | b)`: every f64 entry
409/// is decomposed into `mantissa × 2^exponent` and scaled to a shared
410/// base `2^e_min` so both the matrix and the RHS become integer.
411/// Because the same power-of-two scaling is applied to both sides of
412/// `A x = b`, the solution is unchanged.  Row swaps also swap the RHS
413/// row; no sign tracking is needed (pivot permutations do not affect
414/// the solution of a linear system).
415///
416/// After forward elimination, the upper-triangular `BigInt` system and
417/// its RHS are lifted into `BigRational` for back-substitution, where
418/// fractions are inherent.  This keeps the expensive `O(D³)` phase
419/// GCD-free and limits `BigRational` work to the cheaper `O(D²)` phase.
420///
421/// Returns the exact solution as `[BigRational; D]`.
422///
423/// # Errors
424/// Returns [`LaError::NonFinite`] (propagated from `decompose_matrix` /
425/// `decompose_vec`) if any matrix or vector entry is NaN or infinite.
426/// The matrix is validated before the vector, matching public-API order.
427/// Returns [`LaError::Singular`] if the matrix is exactly singular.
428fn gauss_solve<const D: usize>(m: &Matrix<D>, b: &Vector<D>) -> Result<[BigRational; D], LaError> {
429    // Decompose both matrix and RHS (validating finiteness in one pass);
430    // the shared minimum exponent makes every entry of the augmented
431    // system an integer after scaling.
432    let (m_components, m_e_min) = decompose_matrix(m)?;
433    let (b_components, b_e_min) = decompose_vec(b)?;
434    let mut e_min = m_e_min.min(b_e_min);
435
436    // All matrix + RHS entries are zero.  For `D > 0` this surfaces as
437    // singular inside forward elimination; for `D == 0` the elimination
438    // loop body is empty and we return `Ok([])` without touching e_min.
439    // Pick any finite value so the shift computation is well-defined (the
440    // resulting BigInts are all zero either way).
441    if e_min == i32::MAX {
442        e_min = 0;
443    }
444
445    let mut a = build_bigint_matrix(&m_components, e_min);
446    let mut rhs = build_bigint_vec(&b_components, e_min);
447
448    if let BareissResult::Singular { pivot_col } = bareiss_forward_eliminate(&mut a, Some(&mut rhs))
449    {
450        cold_path();
451        return Err(LaError::Singular { pivot_col });
452    }
453
454    // Back-substitution in `BigRational`.  Only the upper triangle of `a`
455    // and the transformed `rhs` are read, each exactly once — so we
456    // `mem::take` instead of `clone` to avoid a per-entry allocation.
457    // `BigInt::default()` is the zero value and does not allocate.
458    let mut x: [BigRational; D] = from_fn(|_| BigRational::from_integer(BigInt::from(0)));
459    for i in (0..D).rev() {
460        let mut sum = BigRational::from_integer(take(&mut rhs[i]));
461        for j in (i + 1)..D {
462            let a_ij = BigRational::from_integer(take(&mut a[i][j]));
463            sum -= &a_ij * &x[j];
464        }
465        let a_ii = BigRational::from_integer(take(&mut a[i][i]));
466        x[i] = sum / &a_ii;
467    }
468
469    Ok(x)
470}
471
472impl<const D: usize> Matrix<D> {
473    /// Exact determinant using arbitrary-precision rational arithmetic.
474    ///
475    /// Returns the determinant as an exact [`BigRational`] value. Every finite
476    /// `f64` is exactly representable as a rational, so the conversion is
477    /// lossless and the result is provably correct.
478    ///
479    /// # When to use
480    ///
481    /// Use this when you need the exact determinant *value* — for example,
482    /// exact volume computation or distinguishing truly-degenerate simplices
483    /// from near-degenerate ones.  If you only need the *sign*, prefer
484    /// [`det_sign_exact`](Self::det_sign_exact) which has a fast f64 filter.
485    ///
486    /// # Examples
487    /// ```
488    /// use la_stack::prelude::*;
489    ///
490    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
491    /// let det = m.det_exact().unwrap();
492    /// // det = 1*4 - 2*3 = -2  (exact)
493    /// assert_eq!(det, BigRational::from_integer((-2).into()));
494    /// ```
495    ///
496    /// # Errors
497    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
498    #[inline]
499    pub fn det_exact(&self) -> Result<BigRational, LaError> {
500        bareiss_det(self)
501    }
502
503    /// Exact determinant converted to `f64`.
504    ///
505    /// Computes the exact [`BigRational`] determinant via [`det_exact`](Self::det_exact)
506    /// and converts it to the nearest `f64`.  This is useful when you want the
507    /// most accurate f64 determinant possible without committing to `BigRational`
508    /// in your downstream code.
509    ///
510    /// # Examples
511    /// ```
512    /// use la_stack::prelude::*;
513    ///
514    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
515    /// let det = m.det_exact_f64().unwrap();
516    /// assert!((det - (-2.0)).abs() <= f64::EPSILON);
517    /// ```
518    ///
519    /// # Errors
520    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
521    /// Returns [`LaError::Overflow`] if the exact determinant is too large to
522    /// represent as a finite `f64`.
523    #[inline]
524    pub fn det_exact_f64(&self) -> Result<f64, LaError> {
525        let exact = self.det_exact()?;
526        let val = exact.to_f64().unwrap_or(f64::INFINITY);
527        if val.is_finite() {
528            Ok(val)
529        } else {
530            cold_path();
531            Err(LaError::Overflow { index: None })
532        }
533    }
534
535    /// Exact linear system solve using hybrid integer/rational arithmetic.
536    ///
537    /// Solves `A x = b` where `A` is `self` and `b` is the given vector.
538    /// Returns the exact solution as `[BigRational; D]`.  Every finite `f64`
539    /// is exactly representable as a rational, so the conversion is lossless
540    /// and the result is provably correct.
541    ///
542    /// # When to use
543    ///
544    /// Use this when you need a provably correct solution — for example,
545    /// exact circumcenter computation for near-degenerate simplices where
546    /// f64 arithmetic may produce wildly wrong results.
547    ///
548    /// # Algorithm
549    ///
550    /// Matrix and RHS entries are decomposed via IEEE 754 bit extraction and
551    /// scaled to a shared power-of-two base so the augmented system `(A | b)`
552    /// becomes integer-valued.  Forward elimination runs entirely in `BigInt`
553    /// with fraction-free Bareiss updates — no `BigRational`, no GCD, no
554    /// denominator tracking in the `O(D³)` phase.  Only the upper-triangular
555    /// result is lifted into `BigRational` for back-substitution (the `O(D²)`
556    /// phase where fractions are inherent).  First-non-zero pivoting is used
557    /// throughout; since all arithmetic is exact, any non-zero pivot yields
558    /// the correct answer (no numerical-stability concerns).
559    ///
560    /// # Examples
561    /// ```
562    /// use la_stack::prelude::*;
563    ///
564    /// // A x = b  where A = [[1,2],[3,4]], b = [5, 11]  →  x = [1, 2]
565    /// let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
566    /// let b = Vector::<2>::new([5.0, 11.0]);
567    /// let x = a.solve_exact(b).unwrap();
568    /// assert_eq!(x[0], BigRational::from_integer(1.into()));
569    /// assert_eq!(x[1], BigRational::from_integer(2.into()));
570    /// ```
571    ///
572    /// # Errors
573    /// Returns [`LaError::NonFinite`] if any matrix or vector entry is NaN or
574    /// infinite.
575    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
576    #[inline]
577    pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
578        gauss_solve(self, &b)
579    }
580
581    /// Exact linear system solve converted to `f64`.
582    ///
583    /// Computes the exact [`BigRational`] solution via
584    /// [`solve_exact`](Self::solve_exact) and converts each component to the
585    /// nearest `f64`.  This is useful when you want the most accurate f64
586    /// solution possible without committing to `BigRational` downstream.
587    ///
588    /// # Examples
589    /// ```
590    /// use la_stack::prelude::*;
591    ///
592    /// let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
593    /// let b = Vector::<2>::new([5.0, 11.0]);
594    /// let x = a.solve_exact_f64(b).unwrap().into_array();
595    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
596    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
597    /// ```
598    ///
599    /// # Errors
600    /// Returns [`LaError::NonFinite`] if any matrix or vector entry is NaN or
601    /// infinite.
602    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
603    /// Returns [`LaError::Overflow`] if any component of the exact solution is
604    /// too large to represent as a finite `f64`.
605    #[inline]
606    pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
607        let exact = self.solve_exact(b)?;
608        let mut result = [0.0f64; D];
609        for (i, val) in exact.iter().enumerate() {
610            let f = val.to_f64().unwrap_or(f64::INFINITY);
611            if !f.is_finite() {
612                cold_path();
613                return Err(LaError::Overflow { index: Some(i) });
614            }
615            result[i] = f;
616        }
617        Ok(Vector::new(result))
618    }
619
620    /// Exact determinant sign using adaptive-precision arithmetic.
621    ///
622    /// Returns `1` if `det > 0`, `-1` if `det < 0`, and `0` if `det == 0` (singular).
623    ///
624    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
625    /// against a conservative error bound derived from the matrix permanent.
626    /// If the f64 result clearly exceeds the bound, the sign is returned
627    /// immediately without allocating.  Otherwise (and always for D ≥ 5),
628    /// integer-only Bareiss elimination (`bareiss_det_int`) computes the exact
629    /// sign without constructing any `BigRational` values.
630    ///
631    /// # When to use
632    ///
633    /// Use this when the sign of the determinant must be correct regardless of
634    /// floating-point conditioning (e.g. geometric predicates on near-degenerate
635    /// configurations).  For well-conditioned matrices the fast filter resolves
636    /// the sign without touching `BigRational`, so the overhead is minimal.
637    ///
638    /// # Examples
639    /// ```
640    /// use la_stack::prelude::*;
641    ///
642    /// let m = Matrix::<3>::from_rows([
643    ///     [1.0, 2.0, 3.0],
644    ///     [4.0, 5.0, 6.0],
645    ///     [7.0, 8.0, 9.0],
646    /// ]);
647    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
648    /// assert_eq!(m.det_sign_exact().unwrap(), 0);
649    ///
650    /// assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
651    /// ```
652    ///
653    /// # Errors
654    /// Returns [`LaError::NonFinite`] if any matrix entry is NaN or infinite.
655    #[inline]
656    pub fn det_sign_exact(&self) -> Result<i8, LaError> {
657        // Stage 1: f64 fast filter for D ≤ 4.
658        //
659        // When entries are large (e.g. near f64::MAX) the determinant can
660        // overflow to infinity even though every individual entry is finite.
661        // In that case the fast filter is inconclusive; fall through to the
662        // exact Bareiss path.  For NaN/±∞ entries IEEE 754 propagates
663        // non-finite through `det_direct()`, the `det_f64.is_finite()`
664        // guard fails, and we also fall through — validation then happens
665        // inside `bareiss_det_int` via `decompose_matrix`.
666        match self.det_direct() {
667            Some(det_f64)
668                if let Some(err) = self.det_errbound()
669                    && det_f64.is_finite() =>
670            {
671                if det_f64 > err {
672                    return Ok(1);
673                }
674                if det_f64 < -err {
675                    return Ok(-1);
676                }
677            }
678            _ => {}
679        }
680
681        // Stage 2: integer Bareiss fallback — the 2^(D×e_min) scale factor
682        // is always positive, so det_int.sign() == det(A).sign().  This is
683        // the cold path: the fast filter resolves the vast majority of
684        // well-conditioned calls without allocating.  `bareiss_det_int`
685        // validates finiteness via `decompose_matrix`, so NaN/±∞ inputs
686        // surface here as `Err(LaError::NonFinite)`.
687        cold_path();
688        let (det_int, _) = bareiss_det_int(self)?;
689        Ok(match det_int.sign() {
690            Sign::Plus => 1,
691            Sign::Minus => -1,
692            Sign::NoSign => 0,
693        })
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use crate::DEFAULT_PIVOT_TOL;
701
702    use num_traits::Signed;
703    use pastey::paste;
704    use std::array::from_fn;
705
706    // -----------------------------------------------------------------------
707    // Test helpers
708    // -----------------------------------------------------------------------
709
710    /// Build an exact `BigRational` from an `f64` via IEEE 754 bit decomposition.
711    ///
712    /// Thin wrapper over [`f64_decompose`] that packs the mantissa/exponent
713    /// pair into a fully-formed `BigRational` of the form `±m · 2^e`.  The
714    /// production code paths (`bareiss_det_int`, `gauss_solve`) instead
715    /// decompose every entry into a shared-scale `BigInt` matrix, which
716    /// avoids per-entry GCD work in the elimination loops — so this helper
717    /// is not used by them and lives here to keep test assertions concise
718    /// (e.g. `assert_eq!(x[0], f64_to_bigrational(3.0))`).
719    ///
720    /// See `REFERENCES.md` \[9-10\] for the IEEE 754 standard and Goldberg's
721    /// survey of floating-point representation.
722    ///
723    /// # Panics
724    /// Panics if `x` is NaN or infinite.
725    fn f64_to_bigrational(x: f64) -> BigRational {
726        let Some((mantissa, exponent, is_negative)) = f64_decompose(x) else {
727            return BigRational::from_integer(BigInt::from(0));
728        };
729
730        let numer = if is_negative {
731            -BigInt::from(mantissa)
732        } else {
733            BigInt::from(mantissa)
734        };
735
736        if exponent >= 0 {
737            BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
738        } else {
739            BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
740        }
741    }
742
743    // -----------------------------------------------------------------------
744    // Macro-generated per-dimension tests (D=2..5)
745    // -----------------------------------------------------------------------
746
747    macro_rules! gen_det_exact_tests {
748        ($d:literal) => {
749            paste! {
750                #[test]
751                fn [<det_exact_identity_ $d d>]() {
752                    let det = Matrix::<$d>::identity().det_exact().unwrap();
753                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
754                }
755
756                #[test]
757                fn [<det_exact_err_on_nan_ $d d>]() {
758                    let mut m = Matrix::<$d>::identity();
759                    m.set(0, 0, f64::NAN);
760                    assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
761                }
762
763                #[test]
764                fn [<det_exact_err_on_inf_ $d d>]() {
765                    let mut m = Matrix::<$d>::identity();
766                    m.set(0, 0, f64::INFINITY);
767                    assert_eq!(m.det_exact(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
768                }
769            }
770        };
771    }
772
773    gen_det_exact_tests!(2);
774    gen_det_exact_tests!(3);
775    gen_det_exact_tests!(4);
776    gen_det_exact_tests!(5);
777
778    macro_rules! gen_det_exact_f64_tests {
779        ($d:literal) => {
780            paste! {
781                #[test]
782                fn [<det_exact_f64_identity_ $d d>]() {
783                    let det = Matrix::<$d>::identity().det_exact_f64().unwrap();
784                    assert!((det - 1.0).abs() <= f64::EPSILON);
785                }
786
787                #[test]
788                fn [<det_exact_f64_err_on_nan_ $d d>]() {
789                    let mut m = Matrix::<$d>::identity();
790                    m.set(0, 0, f64::NAN);
791                    assert_eq!(m.det_exact_f64(), Err(LaError::NonFinite { row: Some(0), col: 0 }));
792                }
793            }
794        };
795    }
796
797    gen_det_exact_f64_tests!(2);
798    gen_det_exact_f64_tests!(3);
799    gen_det_exact_f64_tests!(4);
800    gen_det_exact_f64_tests!(5);
801
802    /// For D ≤ 4, `det_exact_f64` should agree with `det_direct` on
803    /// well-conditioned matrices.
804    macro_rules! gen_det_exact_f64_agrees_with_det_direct {
805        ($d:literal) => {
806            paste! {
807                #[test]
808                #[allow(clippy::cast_precision_loss)]
809                fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
810                    // Diagonally dominant → well-conditioned.
811                    let mut rows = [[0.0f64; $d]; $d];
812                    for r in 0..$d {
813                        for c in 0..$d {
814                            rows[r][c] = if r == c {
815                                (r as f64) + f64::from($d) + 1.0
816                            } else {
817                                0.1 / ((r + c + 1) as f64)
818                            };
819                        }
820                    }
821                    let m = Matrix::<$d>::from_rows(rows);
822                    let exact = m.det_exact_f64().unwrap();
823                    let direct = m.det_direct().unwrap();
824                    let eps = direct.abs().mul_add(1e-12, 1e-12);
825                    assert!((exact - direct).abs() <= eps);
826                }
827            }
828        };
829    }
830
831    gen_det_exact_f64_agrees_with_det_direct!(2);
832    gen_det_exact_f64_agrees_with_det_direct!(3);
833    gen_det_exact_f64_agrees_with_det_direct!(4);
834
835    #[test]
836    fn det_sign_exact_d0_is_positive() {
837        assert_eq!(Matrix::<0>::zero().det_sign_exact().unwrap(), 1);
838    }
839
840    #[test]
841    fn det_sign_exact_d1_positive() {
842        let m = Matrix::<1>::from_rows([[42.0]]);
843        assert_eq!(m.det_sign_exact().unwrap(), 1);
844    }
845
846    #[test]
847    fn det_sign_exact_d1_negative() {
848        let m = Matrix::<1>::from_rows([[-3.5]]);
849        assert_eq!(m.det_sign_exact().unwrap(), -1);
850    }
851
852    #[test]
853    fn det_sign_exact_d1_zero() {
854        let m = Matrix::<1>::from_rows([[0.0]]);
855        assert_eq!(m.det_sign_exact().unwrap(), 0);
856    }
857
858    #[test]
859    fn det_sign_exact_identity_2d() {
860        assert_eq!(Matrix::<2>::identity().det_sign_exact().unwrap(), 1);
861    }
862
863    #[test]
864    fn det_sign_exact_identity_3d() {
865        assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
866    }
867
868    #[test]
869    fn det_sign_exact_identity_4d() {
870        assert_eq!(Matrix::<4>::identity().det_sign_exact().unwrap(), 1);
871    }
872
873    #[test]
874    fn det_sign_exact_identity_5d() {
875        assert_eq!(Matrix::<5>::identity().det_sign_exact().unwrap(), 1);
876    }
877
878    #[test]
879    fn det_sign_exact_singular_duplicate_rows() {
880        let m = Matrix::<3>::from_rows([
881            [1.0, 2.0, 3.0],
882            [4.0, 5.0, 6.0],
883            [1.0, 2.0, 3.0], // duplicate of row 0
884        ]);
885        assert_eq!(m.det_sign_exact().unwrap(), 0);
886    }
887
888    #[test]
889    fn det_sign_exact_singular_linear_combination() {
890        // Row 2 = row 0 + row 1 in exact arithmetic.
891        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
892        assert_eq!(m.det_sign_exact().unwrap(), 0);
893    }
894
895    #[test]
896    fn det_sign_exact_negative_det_row_swap() {
897        // Swapping two rows of the identity negates the determinant.
898        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
899        assert_eq!(m.det_sign_exact().unwrap(), -1);
900    }
901
902    #[test]
903    fn det_sign_exact_negative_det_known() {
904        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
905        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
906        assert_eq!(m.det_sign_exact().unwrap(), -1);
907    }
908
909    #[test]
910    fn det_sign_exact_agrees_with_det_for_spd() {
911        // SPD matrix → positive determinant.
912        let m = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
913        assert_eq!(m.det_sign_exact().unwrap(), 1);
914        assert!(m.det(DEFAULT_PIVOT_TOL).unwrap() > 0.0);
915    }
916
917    /// Near-singular matrix with an exact perturbation.
918    ///
919    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
920    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
921    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
922    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
923    #[test]
924    fn det_sign_exact_near_singular_perturbation() {
925        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
926        let m = Matrix::<3>::from_rows([
927            [1.0 + perturbation, 2.0, 3.0],
928            [4.0, 5.0, 6.0],
929            [7.0, 8.0, 9.0],
930        ]);
931        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
932        assert_eq!(m.det_sign_exact().unwrap(), -1);
933    }
934
935    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
936    /// and never allocate `BigRational`.  We can't directly observe this,
937    /// but we verify correctness for a range of known signs.
938    #[test]
939    fn det_sign_exact_fast_filter_positive_4x4() {
940        let m = Matrix::<4>::from_rows([
941            [2.0, 1.0, 0.0, 0.0],
942            [1.0, 3.0, 1.0, 0.0],
943            [0.0, 1.0, 4.0, 1.0],
944            [0.0, 0.0, 1.0, 5.0],
945        ]);
946        // SPD tridiagonal → positive det.
947        assert_eq!(m.det_sign_exact().unwrap(), 1);
948    }
949
950    #[test]
951    fn det_sign_exact_fast_filter_negative_4x4() {
952        // Swap rows 0 and 1 of the above → negate det.
953        let m = Matrix::<4>::from_rows([
954            [1.0, 3.0, 1.0, 0.0],
955            [2.0, 1.0, 0.0, 0.0],
956            [0.0, 1.0, 4.0, 1.0],
957            [0.0, 0.0, 1.0, 5.0],
958        ]);
959        assert_eq!(m.det_sign_exact().unwrap(), -1);
960    }
961
962    #[test]
963    fn det_sign_exact_subnormal_entries() {
964        // Subnormal f64 values should convert losslessly.
965        let tiny = 5e-324_f64; // smallest positive subnormal
966        assert!(tiny.is_subnormal());
967
968        let m = Matrix::<2>::from_rows([[tiny, 0.0], [0.0, tiny]]);
969        // det = tiny^2 > 0
970        assert_eq!(m.det_sign_exact().unwrap(), 1);
971    }
972
973    #[test]
974    fn det_sign_exact_returns_err_on_nan() {
975        let m = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
976        assert_eq!(
977            m.det_sign_exact(),
978            Err(LaError::NonFinite {
979                row: Some(0),
980                col: 0
981            })
982        );
983    }
984
985    #[test]
986    fn det_sign_exact_returns_err_on_infinity() {
987        let m = Matrix::<2>::from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]);
988        assert_eq!(
989            m.det_sign_exact(),
990            Err(LaError::NonFinite {
991                row: Some(0),
992                col: 0
993            })
994        );
995    }
996
997    #[test]
998    fn det_sign_exact_returns_err_on_nan_5x5() {
999        // D ≥ 5 bypasses the fast filter, exercising the bareiss_det path.
1000        let mut m = Matrix::<5>::identity();
1001        m.set(2, 3, f64::NAN);
1002        assert_eq!(
1003            m.det_sign_exact(),
1004            Err(LaError::NonFinite {
1005                row: Some(2),
1006                col: 3
1007            })
1008        );
1009    }
1010
1011    #[test]
1012    fn det_sign_exact_returns_err_on_infinity_5x5() {
1013        let mut m = Matrix::<5>::identity();
1014        m.set(0, 0, f64::INFINITY);
1015        assert_eq!(
1016            m.det_sign_exact(),
1017            Err(LaError::NonFinite {
1018                row: Some(0),
1019                col: 0
1020            })
1021        );
1022    }
1023
1024    #[test]
1025    fn det_sign_exact_pivot_needed_5x5() {
1026        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
1027        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
1028        let m = Matrix::<5>::from_rows([
1029            [0.0, 1.0, 0.0, 0.0, 0.0],
1030            [1.0, 0.0, 0.0, 0.0, 0.0],
1031            [0.0, 0.0, 1.0, 0.0, 0.0],
1032            [0.0, 0.0, 0.0, 1.0, 0.0],
1033            [0.0, 0.0, 0.0, 0.0, 1.0],
1034        ]);
1035        assert_eq!(m.det_sign_exact().unwrap(), -1);
1036    }
1037
1038    #[test]
1039    fn det_sign_exact_5x5_known() {
1040        // det of a permutation matrix with two swaps = +1 (even permutation).
1041        let m = Matrix::<5>::from_rows([
1042            [0.0, 1.0, 0.0, 0.0, 0.0],
1043            [1.0, 0.0, 0.0, 0.0, 0.0],
1044            [0.0, 0.0, 0.0, 1.0, 0.0],
1045            [0.0, 0.0, 1.0, 0.0, 0.0],
1046            [0.0, 0.0, 0.0, 0.0, 1.0],
1047        ]);
1048        // Two transpositions → even permutation → det = +1
1049        assert_eq!(m.det_sign_exact().unwrap(), 1);
1050    }
1051
1052    // -----------------------------------------------------------------------
1053    // Direct tests for internal helpers (coverage of private functions)
1054    // -----------------------------------------------------------------------
1055
1056    #[test]
1057    fn det_errbound_d0_is_zero() {
1058        assert_eq!(Matrix::<0>::zero().det_errbound(), Some(0.0));
1059    }
1060
1061    #[test]
1062    fn det_errbound_d1_is_zero() {
1063        assert_eq!(Matrix::<1>::from_rows([[42.0]]).det_errbound(), Some(0.0));
1064    }
1065
1066    #[test]
1067    fn det_errbound_d2_positive() {
1068        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1069        let bound = m.det_errbound().unwrap();
1070        assert!(bound > 0.0);
1071        // bound = ERR_COEFF_2 * (|1*4| + |2*3|) = ERR_COEFF_2 * 10
1072        assert!(crate::ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
1073    }
1074
1075    #[test]
1076    fn det_errbound_d3_positive() {
1077        let m = Matrix::<3>::identity();
1078        let bound = m.det_errbound().unwrap();
1079        assert!(bound > 0.0);
1080    }
1081
1082    #[test]
1083    fn det_errbound_d3_non_identity() {
1084        // Non-identity matrix to exercise all code paths in D=3 case
1085        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]]);
1086        let bound = m.det_errbound().unwrap();
1087        assert!(bound > 0.0);
1088    }
1089
1090    #[test]
1091    fn det_errbound_d4_positive() {
1092        let m = Matrix::<4>::identity();
1093        let bound = m.det_errbound().unwrap();
1094        assert!(bound > 0.0);
1095    }
1096
1097    #[test]
1098    fn det_errbound_d4_non_identity() {
1099        // Non-identity matrix to exercise all code paths in D=4 case
1100        let m = Matrix::<4>::from_rows([
1101            [1.0, 0.0, 0.0, 0.0],
1102            [0.0, 2.0, 0.0, 0.0],
1103            [0.0, 0.0, 3.0, 0.0],
1104            [0.0, 0.0, 0.0, 4.0],
1105        ]);
1106        let bound = m.det_errbound().unwrap();
1107        assert!(bound > 0.0);
1108    }
1109
1110    #[test]
1111    fn det_errbound_d5_is_none() {
1112        assert_eq!(Matrix::<5>::identity().det_errbound(), None);
1113    }
1114
1115    // -----------------------------------------------------------------------
1116    // f64_decompose tests
1117    // -----------------------------------------------------------------------
1118
1119    #[test]
1120    fn f64_decompose_zero() {
1121        assert!(f64_decompose(0.0).is_none());
1122        assert!(f64_decompose(-0.0).is_none());
1123    }
1124
1125    #[test]
1126    fn f64_decompose_one() {
1127        let (mant, exp, neg) = f64_decompose(1.0).unwrap();
1128        assert_eq!(mant, 1);
1129        assert_eq!(exp, 0);
1130        assert!(!neg);
1131    }
1132
1133    #[test]
1134    fn f64_decompose_negative() {
1135        let (mant, exp, neg) = f64_decompose(-3.5).unwrap();
1136        // -3.5 = -7 × 2^(-1), mantissa is 7 (odd after stripping)
1137        assert_eq!(mant, 7);
1138        assert_eq!(exp, -1);
1139        assert!(neg);
1140    }
1141
1142    #[test]
1143    fn f64_decompose_subnormal() {
1144        let tiny = 5e-324_f64;
1145        assert!(tiny.is_subnormal());
1146        let (mant, exp, neg) = f64_decompose(tiny).unwrap();
1147        assert_eq!(mant, 1);
1148        assert_eq!(exp, -1074);
1149        assert!(!neg);
1150    }
1151
1152    #[test]
1153    fn f64_decompose_power_of_two() {
1154        let (mant, exp, neg) = f64_decompose(1024.0).unwrap();
1155        assert_eq!(mant, 1);
1156        assert_eq!(exp, 10); // 1024 = 2^10
1157        assert!(!neg);
1158    }
1159
1160    #[test]
1161    #[should_panic(expected = "non-finite f64 in exact conversion")]
1162    fn f64_decompose_panics_on_nan() {
1163        f64_decompose(f64::NAN);
1164    }
1165
1166    // -----------------------------------------------------------------------
1167    // bareiss_det_int tests
1168    // -----------------------------------------------------------------------
1169
1170    #[test]
1171    fn bareiss_det_int_d0() {
1172        let (det, exp) = bareiss_det_int(&Matrix::<0>::zero()).unwrap();
1173        assert_eq!(det, BigInt::from(1));
1174        assert_eq!(exp, 0);
1175    }
1176
1177    /// Table-driven coverage of the D=1 fast-path: each 1×1 matrix
1178    /// decomposes to `(±mant, exp)` directly.  Includes an integer, zero,
1179    /// a negative fractional, and a positive fractional case — the
1180    /// combinations that exercise the sign handling, the all-zero early
1181    /// return, trailing-zero stripping, and negative exponent scaling.
1182    #[test]
1183    fn bareiss_det_int_d1_cases() {
1184        let cases: &[(f64, i64, i32)] = &[
1185            // (input, expected_det_int, expected_exp)
1186            (7.0, 7, 0),    // integer → (7, 0)
1187            (0.0, 0, 0),    // all-zero early return → (0, 0)
1188            (-3.5, -7, -1), // -3.5 = -7 × 2^(-1)
1189            (0.5, 1, -1),   // 0.5  =  1 × 2^(-1)
1190        ];
1191        for &(input, expected_det_int, expected_exp) in cases {
1192            let (det, exp) = bareiss_det_int(&Matrix::<1>::from_rows([[input]])).unwrap();
1193            assert_eq!(
1194                det,
1195                BigInt::from(expected_det_int),
1196                "det_int for input={input}"
1197            );
1198            assert_eq!(exp, expected_exp, "exp for input={input}");
1199        }
1200    }
1201
1202    #[test]
1203    fn bareiss_det_int_d2_known() {
1204        // det([[1,2],[3,4]]) = -2
1205        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1206        let (det_int, total_exp) = bareiss_det_int(&m).unwrap();
1207        // Reconstruct and verify.
1208        let det = bigint_exp_to_bigrational(det_int, total_exp);
1209        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
1210    }
1211
1212    #[test]
1213    fn bareiss_det_int_all_zeros() {
1214        let (det, _) = bareiss_det_int(&Matrix::<3>::zero()).unwrap();
1215        assert_eq!(det, BigInt::from(0));
1216    }
1217
1218    #[test]
1219    fn bareiss_det_int_sign_matches_det_sign_exact() {
1220        // The sign of det_int should match det_sign_exact for various matrices.
1221        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1222        let (det_int, _) = bareiss_det_int(&m).unwrap();
1223        assert_eq!(det_int.sign(), Sign::Minus); // det = -1
1224    }
1225
1226    #[test]
1227    fn bareiss_det_int_fractional_entries() {
1228        // Entries with negative exponents: 0.5 = 1×2^(-1), 0.25 = 1×2^(-2).
1229        // det([[0.5, 0.25], [1.0, 1.0]]) = 0.5×1.0 − 0.25×1.0 = 0.25
1230        let m = Matrix::<2>::from_rows([[0.5, 0.25], [1.0, 1.0]]);
1231        let (det_int, total_exp) = bareiss_det_int(&m).unwrap();
1232        let det = bigint_exp_to_bigrational(det_int, total_exp);
1233        assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
1234    }
1235
1236    #[test]
1237    fn bareiss_det_int_d3_with_pivoting() {
1238        // Zero on diagonal → exercises pivot swap inside bareiss_det_int.
1239        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1240        let (det_int, total_exp) = bareiss_det_int(&m).unwrap();
1241        let det = bigint_exp_to_bigrational(det_int, total_exp);
1242        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1243    }
1244
1245    /// Non-finite matrix entries surface as `LaError::NonFinite` with the
1246    /// row/col of the first offending entry.
1247    #[test]
1248    fn bareiss_det_int_errs_on_nan() {
1249        let mut m = Matrix::<3>::identity();
1250        m.set(1, 2, f64::NAN);
1251        assert_eq!(
1252            bareiss_det_int(&m),
1253            Err(LaError::NonFinite {
1254                row: Some(1),
1255                col: 2
1256            })
1257        );
1258    }
1259
1260    #[test]
1261    fn bareiss_det_int_errs_on_inf() {
1262        let mut m = Matrix::<2>::identity();
1263        m.set(0, 0, f64::INFINITY);
1264        assert_eq!(
1265            bareiss_det_int(&m),
1266            Err(LaError::NonFinite {
1267                row: Some(0),
1268                col: 0
1269            })
1270        );
1271    }
1272
1273    /// Per AGENTS.md: dimension-generic tests must cover D=2–5.
1274    macro_rules! gen_bareiss_det_int_identity_tests {
1275        ($d:literal) => {
1276            paste! {
1277                #[test]
1278                fn [<bareiss_det_int_identity_ $d d>]() {
1279                    let (det_int, total_exp) =
1280                        bareiss_det_int(&Matrix::<$d>::identity()).unwrap();
1281                    let det = bigint_exp_to_bigrational(det_int, total_exp);
1282                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1283                }
1284            }
1285        };
1286    }
1287
1288    gen_bareiss_det_int_identity_tests!(2);
1289    gen_bareiss_det_int_identity_tests!(3);
1290    gen_bareiss_det_int_identity_tests!(4);
1291    gen_bareiss_det_int_identity_tests!(5);
1292
1293    // -----------------------------------------------------------------------
1294    // bigint_exp_to_bigrational tests
1295    // -----------------------------------------------------------------------
1296
1297    #[test]
1298    fn bigint_exp_to_bigrational_zero() {
1299        let r = bigint_exp_to_bigrational(BigInt::from(0), -50);
1300        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
1301    }
1302
1303    #[test]
1304    fn bigint_exp_to_bigrational_positive_exp() {
1305        // 3 × 2^2 = 12
1306        let r = bigint_exp_to_bigrational(BigInt::from(3), 2);
1307        assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
1308    }
1309
1310    #[test]
1311    fn bigint_exp_to_bigrational_negative_exp_reduced() {
1312        // 6 × 2^(-2) = 6/4 → reduced to 3/2 (strip one shared factor of 2)
1313        let r = bigint_exp_to_bigrational(BigInt::from(6), -2);
1314        assert_eq!(*r.numer(), BigInt::from(3));
1315        assert_eq!(*r.denom(), BigInt::from(2));
1316    }
1317
1318    #[test]
1319    fn bigint_exp_to_bigrational_negative_exp_already_odd() {
1320        // 3 × 2^(-2) = 3/4 (already in lowest terms since 3 is odd)
1321        let r = bigint_exp_to_bigrational(BigInt::from(3), -2);
1322        assert_eq!(*r.numer(), BigInt::from(3));
1323        assert_eq!(*r.denom(), BigInt::from(4));
1324    }
1325
1326    #[test]
1327    fn bigint_exp_to_bigrational_negative_value() {
1328        // -5 × 2^1 = -10
1329        let r = bigint_exp_to_bigrational(BigInt::from(-5), 1);
1330        assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
1331    }
1332
1333    #[test]
1334    fn bigint_exp_to_bigrational_negative_value_with_denominator() {
1335        // -3 × 2^(-2) = -3/4
1336        let r = bigint_exp_to_bigrational(BigInt::from(-3), -2);
1337        assert_eq!(*r.numer(), BigInt::from(-3));
1338        assert_eq!(*r.denom(), BigInt::from(4));
1339    }
1340
1341    // -----------------------------------------------------------------------
1342    // bareiss_det (wrapper) tests
1343    // -----------------------------------------------------------------------
1344
1345    #[test]
1346    fn bareiss_det_d0_is_one() {
1347        let det = bareiss_det(&Matrix::<0>::zero()).unwrap();
1348        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1349    }
1350
1351    #[test]
1352    fn bareiss_det_d1_returns_entry() {
1353        let det = bareiss_det(&Matrix::<1>::from_rows([[7.0]])).unwrap();
1354        assert_eq!(det, f64_to_bigrational(7.0));
1355    }
1356
1357    #[test]
1358    fn bareiss_det_d3_with_pivoting() {
1359        // First column has zero on diagonal → exercises pivot swap + break.
1360        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1361        let det = bareiss_det(&m).unwrap();
1362        // det of this permutation matrix = -1
1363        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1364    }
1365
1366    #[test]
1367    fn bareiss_det_singular_all_zeros_in_column() {
1368        // Column 1 is all zeros below diagonal after elimination → singular.
1369        let m = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1370        let det = bareiss_det(&m).unwrap();
1371        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
1372    }
1373
1374    #[test]
1375    fn det_sign_exact_overflow_determinant_finite_entries() {
1376        // Entries near f64::MAX are finite, but the f64 determinant overflows
1377        // to infinity.  The fast filter should be skipped and Bareiss should
1378        // compute the correct positive sign.
1379        let big = f64::MAX / 2.0;
1380        assert!(big.is_finite());
1381        let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
1382        // det = big^2 > 0
1383        assert_eq!(m.det_sign_exact().unwrap(), 1);
1384    }
1385
1386    // -----------------------------------------------------------------------
1387    // det_exact: dimension-specific tests
1388    // -----------------------------------------------------------------------
1389
1390    #[test]
1391    fn det_exact_d0_is_one() {
1392        let det = Matrix::<0>::zero().det_exact().unwrap();
1393        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1394    }
1395
1396    #[test]
1397    fn det_exact_known_2x2() {
1398        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
1399        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1400        let det = m.det_exact().unwrap();
1401        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
1402    }
1403
1404    #[test]
1405    fn det_exact_singular_returns_zero() {
1406        // Rows in arithmetic progression → exactly singular.
1407        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]);
1408        let det = m.det_exact().unwrap();
1409        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
1410    }
1411
1412    #[test]
1413    fn det_exact_near_singular_perturbation() {
1414        // Same 2^-50 perturbation case: exact det = -3 × 2^-50.
1415        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
1416        let m = Matrix::<3>::from_rows([
1417            [1.0 + perturbation, 2.0, 3.0],
1418            [4.0, 5.0, 6.0],
1419            [7.0, 8.0, 9.0],
1420        ]);
1421        let det = m.det_exact().unwrap();
1422        // det should be exactly -3 × 2^-50.
1423        let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
1424        assert_eq!(det, expected);
1425    }
1426
1427    #[test]
1428    fn det_exact_5x5_permutation() {
1429        // Single swap (rows 0↔1) → det = -1.
1430        let m = Matrix::<5>::from_rows([
1431            [0.0, 1.0, 0.0, 0.0, 0.0],
1432            [1.0, 0.0, 0.0, 0.0, 0.0],
1433            [0.0, 0.0, 1.0, 0.0, 0.0],
1434            [0.0, 0.0, 0.0, 1.0, 0.0],
1435            [0.0, 0.0, 0.0, 0.0, 1.0],
1436        ]);
1437        let det = m.det_exact().unwrap();
1438        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1439    }
1440
1441    // -----------------------------------------------------------------------
1442    // det_exact_f64: dimension-specific tests
1443    // -----------------------------------------------------------------------
1444
1445    #[test]
1446    fn det_exact_f64_known_2x2() {
1447        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1448        let det = m.det_exact_f64().unwrap();
1449        assert!((det - (-2.0)).abs() <= f64::EPSILON);
1450    }
1451
1452    #[test]
1453    fn det_exact_f64_overflow_returns_err() {
1454        // Entries near f64::MAX produce a determinant too large for f64.
1455        let big = f64::MAX / 2.0;
1456        let m = Matrix::<3>::from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]]);
1457        // det = big^2, which overflows f64.
1458        assert_eq!(m.det_exact_f64(), Err(LaError::Overflow { index: None }));
1459    }
1460
1461    // -----------------------------------------------------------------------
1462    // solve_exact: macro-generated per-dimension tests (D=2..5)
1463    // -----------------------------------------------------------------------
1464
1465    /// Helper: build an arbitrary RHS vector for dimension `$d`.
1466    fn arbitrary_rhs<const D: usize>() -> Vector<D> {
1467        let values = [1.0, -2.5, 3.0, 0.25, -4.0];
1468        let mut arr = [0.0f64; D];
1469        for (dst, src) in arr.iter_mut().zip(values.iter()) {
1470            *dst = *src;
1471        }
1472        Vector::<D>::new(arr)
1473    }
1474
1475    macro_rules! gen_solve_exact_tests {
1476        ($d:literal) => {
1477            paste! {
1478                #[test]
1479                fn [<solve_exact_identity_ $d d>]() {
1480                    let a = Matrix::<$d>::identity();
1481                    let b = arbitrary_rhs::<$d>();
1482                    let x = a.solve_exact(b).unwrap();
1483                    for (i, xi) in x.iter().enumerate() {
1484                        assert_eq!(*xi, f64_to_bigrational(b.data[i]));
1485                    }
1486                }
1487
1488                #[test]
1489                fn [<solve_exact_err_on_nan_matrix_ $d d>]() {
1490                    let mut a = Matrix::<$d>::identity();
1491                    a.set(0, 0, f64::NAN);
1492                    let b = arbitrary_rhs::<$d>();
1493                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1494                }
1495
1496                #[test]
1497                fn [<solve_exact_err_on_inf_matrix_ $d d>]() {
1498                    let mut a = Matrix::<$d>::identity();
1499                    a.set(0, 0, f64::INFINITY);
1500                    let b = arbitrary_rhs::<$d>();
1501                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1502                }
1503
1504                #[test]
1505                fn [<solve_exact_err_on_nan_vector_ $d d>]() {
1506                    let a = Matrix::<$d>::identity();
1507                    let mut b_arr = [1.0f64; $d];
1508                    b_arr[0] = f64::NAN;
1509                    let b = Vector::<$d>::new(b_arr);
1510                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: 0 }));
1511                }
1512
1513                #[test]
1514                fn [<solve_exact_err_on_inf_vector_ $d d>]() {
1515                    let a = Matrix::<$d>::identity();
1516                    let mut b_arr = [1.0f64; $d];
1517                    b_arr[$d - 1] = f64::INFINITY;
1518                    let b = Vector::<$d>::new(b_arr);
1519                    assert_eq!(a.solve_exact(b), Err(LaError::NonFinite { row: None, col: $d - 1 }));
1520                }
1521
1522                #[test]
1523                fn [<solve_exact_singular_ $d d>]() {
1524                    // Zero matrix is singular.
1525                    let a = Matrix::<$d>::zero();
1526                    let b = arbitrary_rhs::<$d>();
1527                    assert_eq!(a.solve_exact(b), Err(LaError::Singular { pivot_col: 0 }));
1528                }
1529            }
1530        };
1531    }
1532
1533    gen_solve_exact_tests!(2);
1534    gen_solve_exact_tests!(3);
1535    gen_solve_exact_tests!(4);
1536    gen_solve_exact_tests!(5);
1537
1538    macro_rules! gen_solve_exact_f64_tests {
1539        ($d:literal) => {
1540            paste! {
1541                #[test]
1542                fn [<solve_exact_f64_identity_ $d d>]() {
1543                    let a = Matrix::<$d>::identity();
1544                    let b = arbitrary_rhs::<$d>();
1545                    let x = a.solve_exact_f64(b).unwrap().into_array();
1546                    for i in 0..$d {
1547                        assert!((x[i] - b.data[i]).abs() <= f64::EPSILON);
1548                    }
1549                }
1550
1551                #[test]
1552                fn [<solve_exact_f64_err_on_nan_ $d d>]() {
1553                    let mut a = Matrix::<$d>::identity();
1554                    a.set(0, 0, f64::NAN);
1555                    let b = arbitrary_rhs::<$d>();
1556                    assert_eq!(a.solve_exact_f64(b), Err(LaError::NonFinite { row: Some(0), col: 0 }));
1557                }
1558            }
1559        };
1560    }
1561
1562    gen_solve_exact_f64_tests!(2);
1563    gen_solve_exact_f64_tests!(3);
1564    gen_solve_exact_f64_tests!(4);
1565    gen_solve_exact_f64_tests!(5);
1566
1567    /// For D ≤ 4, `solve_exact_f64` should agree with `Lu::solve_vec` on
1568    /// well-conditioned matrices.
1569    macro_rules! gen_solve_exact_f64_agrees_with_lu {
1570        ($d:literal) => {
1571            paste! {
1572                #[test]
1573                #[allow(clippy::cast_precision_loss)]
1574                fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
1575                    // Diagonally dominant → well-conditioned.
1576                    let mut rows = [[0.0f64; $d]; $d];
1577                    for r in 0..$d {
1578                        for c in 0..$d {
1579                            rows[r][c] = if r == c {
1580                                (r as f64) + f64::from($d) + 1.0
1581                            } else {
1582                                0.1 / ((r + c + 1) as f64)
1583                            };
1584                        }
1585                    }
1586                    let a = Matrix::<$d>::from_rows(rows);
1587                    let b = arbitrary_rhs::<$d>();
1588                    let exact = a.solve_exact_f64(b).unwrap().into_array();
1589                    let lu_sol = a.lu(DEFAULT_PIVOT_TOL).unwrap()
1590                        .solve_vec(b).unwrap().into_array();
1591                    for i in 0..$d {
1592                        let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
1593                        assert!((exact[i] - lu_sol[i]).abs() <= eps);
1594                    }
1595                }
1596            }
1597        };
1598    }
1599
1600    gen_solve_exact_f64_agrees_with_lu!(2);
1601    gen_solve_exact_f64_agrees_with_lu!(3);
1602    gen_solve_exact_f64_agrees_with_lu!(4);
1603    gen_solve_exact_f64_agrees_with_lu!(5);
1604
1605    /// Round-trip: for a well-conditioned integer matrix `A` and integer
1606    /// target `x0`, solving `A x = A x0` must return `x0` exactly.  All
1607    /// intermediate values stay small enough that `A * x0` is exactly
1608    /// representable in `f64`, so the round-trip is a precise equality
1609    /// check on the hybrid BigInt/BigRational path.
1610    macro_rules! gen_solve_exact_roundtrip_tests {
1611        ($d:literal) => {
1612            paste! {
1613                #[test]
1614                #[allow(clippy::cast_precision_loss)]
1615                fn [<solve_exact_roundtrip_ $d d>]() {
1616                    // A = D * I + J (diag = D+1, off-diag = 1).  Invertible
1617                    // for any D >= 1 and cheap to multiply by hand.
1618                    let mut rows = [[0.0f64; $d]; $d];
1619                    for r in 0..$d {
1620                        for c in 0..$d {
1621                            rows[r][c] = if r == c {
1622                                f64::from($d) + 1.0
1623                            } else {
1624                                1.0
1625                            };
1626                        }
1627                    }
1628                    let a = Matrix::<$d>::from_rows(rows);
1629
1630                    // x0 = [1, 2, ..., D].
1631                    let mut x0 = [0.0f64; $d];
1632                    for i in 0..$d {
1633                        x0[i] = (i + 1) as f64;
1634                    }
1635
1636                    // b = A * x0 computed in f64.  With small integers the
1637                    // multiply-add sequence is exact.
1638                    let mut b_arr = [0.0f64; $d];
1639                    for r in 0..$d {
1640                        let mut sum = 0.0_f64;
1641                        for c in 0..$d {
1642                            sum += rows[r][c] * x0[c];
1643                        }
1644                        b_arr[r] = sum;
1645                    }
1646                    let b = Vector::<$d>::new(b_arr);
1647
1648                    let x = a.solve_exact(b).unwrap();
1649                    for i in 0..$d {
1650                        assert_eq!(x[i], f64_to_bigrational(x0[i]));
1651                    }
1652                }
1653            }
1654        };
1655    }
1656
1657    gen_solve_exact_roundtrip_tests!(2);
1658    gen_solve_exact_roundtrip_tests!(3);
1659    gen_solve_exact_roundtrip_tests!(4);
1660    gen_solve_exact_roundtrip_tests!(5);
1661
1662    // -----------------------------------------------------------------------
1663    // solve_exact: dimension-specific tests
1664    // -----------------------------------------------------------------------
1665
1666    #[test]
1667    fn solve_exact_d0_returns_empty() {
1668        let a = Matrix::<0>::zero();
1669        let b = Vector::<0>::zero();
1670        let x = a.solve_exact(b).unwrap();
1671        assert!(x.is_empty());
1672    }
1673
1674    #[test]
1675    fn solve_exact_known_2x2() {
1676        // [[1,2],[3,4]] x = [5, 11] → x = [1, 2]
1677        let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
1678        let b = Vector::<2>::new([5.0, 11.0]);
1679        let x = a.solve_exact(b).unwrap();
1680        assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
1681        assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
1682    }
1683
1684    #[test]
1685    fn solve_exact_pivoting_needed() {
1686        // First column has zero on diagonal → pivot swap required.
1687        let a = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
1688        let b = Vector::<3>::new([2.0, 3.0, 4.0]);
1689        let x = a.solve_exact(b).unwrap();
1690        // x = [3, 2, 4]
1691        assert_eq!(x[0], f64_to_bigrational(3.0));
1692        assert_eq!(x[1], f64_to_bigrational(2.0));
1693        assert_eq!(x[2], f64_to_bigrational(4.0));
1694    }
1695
1696    #[test]
1697    fn solve_exact_fractional_result() {
1698        // [[2, 1], [1, 3]] x = [1, 1] → x = [2/5, 1/5]
1699        let a = Matrix::<2>::from_rows([[2.0, 1.0], [1.0, 3.0]]);
1700        let b = Vector::<2>::new([1.0, 1.0]);
1701        let x = a.solve_exact(b).unwrap();
1702        assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
1703        assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
1704    }
1705
1706    #[test]
1707    fn solve_exact_singular_duplicate_rows() {
1708        let a = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]]);
1709        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
1710        assert!(matches!(a.solve_exact(b), Err(LaError::Singular { .. })));
1711    }
1712
1713    #[test]
1714    fn solve_exact_5x5_permutation() {
1715        // Permutation matrix (swap rows 0↔1): P x = b → x = P^T b.
1716        let a = Matrix::<5>::from_rows([
1717            [0.0, 1.0, 0.0, 0.0, 0.0],
1718            [1.0, 0.0, 0.0, 0.0, 0.0],
1719            [0.0, 0.0, 1.0, 0.0, 0.0],
1720            [0.0, 0.0, 0.0, 1.0, 0.0],
1721            [0.0, 0.0, 0.0, 0.0, 1.0],
1722        ]);
1723        let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
1724        let x = a.solve_exact(b).unwrap();
1725        assert_eq!(x[0], f64_to_bigrational(20.0));
1726        assert_eq!(x[1], f64_to_bigrational(10.0));
1727        assert_eq!(x[2], f64_to_bigrational(30.0));
1728        assert_eq!(x[3], f64_to_bigrational(40.0));
1729        assert_eq!(x[4], f64_to_bigrational(50.0));
1730    }
1731
1732    /// Entries near `f64::MAX / 2` are finite but their product would
1733    /// overflow to ±∞ in pure f64 arithmetic.  The `BigInt` augmented-system
1734    /// path computes the correct solution without any overflow.  The D×D
1735    /// case uses a diagonal matrix with `big` on every diagonal and a RHS
1736    /// of `[big, …, big, 0]`, giving the known solution `[1, …, 1, 0]`.
1737    macro_rules! gen_solve_exact_large_finite_entries_tests {
1738        ($d:literal) => {
1739            paste! {
1740                #[test]
1741                fn [<solve_exact_large_finite_entries_ $d d>]() {
1742                    let big = f64::MAX / 2.0;
1743                    assert!(big.is_finite());
1744                    // D×D diagonal matrix with `big` on the diagonal.
1745                    let mut rows = [[0.0f64; $d]; $d];
1746                    for i in 0..$d {
1747                        rows[i][i] = big;
1748                    }
1749                    let a = Matrix::<$d>::from_rows(rows);
1750                    // RHS = [big, …, big, 0] → x = [1, …, 1, 0].
1751                    let mut b_arr = [big; $d];
1752                    b_arr[$d - 1] = 0.0;
1753                    let b = Vector::<$d>::new(b_arr);
1754                    let x = a.solve_exact(b).unwrap();
1755                    for i in 0..($d - 1) {
1756                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
1757                    }
1758                    assert_eq!(x[$d - 1], BigRational::from_integer(BigInt::from(0)));
1759                }
1760            }
1761        };
1762    }
1763
1764    gen_solve_exact_large_finite_entries_tests!(2);
1765    gen_solve_exact_large_finite_entries_tests!(3);
1766    gen_solve_exact_large_finite_entries_tests!(4);
1767    gen_solve_exact_large_finite_entries_tests!(5);
1768
1769    /// Matrix and RHS entries span many orders of magnitude (from
1770    /// `f64::MIN_POSITIVE` up through `1e100`).  This exercises the
1771    /// shared `e_min` scaling: even the largest shift keeps every entry a
1772    /// representable `BigInt`.  The D×D case alternates `huge`/`tiny`
1773    /// along the diagonal with a matching RHS, giving `x = [1, …, 1]`.
1774    macro_rules! gen_solve_exact_mixed_magnitude_entries_tests {
1775        ($d:literal) => {
1776            paste! {
1777                #[test]
1778                fn [<solve_exact_mixed_magnitude_entries_ $d d>]() {
1779                    let tiny = f64::MIN_POSITIVE; // 2^-1022, smallest normal
1780                    let huge = 1.0e100_f64;
1781                    // Alternate huge/tiny along the diagonal.
1782                    let mut rows = [[0.0f64; $d]; $d];
1783                    let mut b_arr = [0.0f64; $d];
1784                    for i in 0..$d {
1785                        let val = if i % 2 == 0 { huge } else { tiny };
1786                        rows[i][i] = val;
1787                        b_arr[i] = val;
1788                    }
1789                    let a = Matrix::<$d>::from_rows(rows);
1790                    let b = Vector::<$d>::new(b_arr);
1791                    let x = a.solve_exact(b).unwrap();
1792                    for i in 0..$d {
1793                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
1794                    }
1795                }
1796            }
1797        };
1798    }
1799
1800    gen_solve_exact_mixed_magnitude_entries_tests!(2);
1801    gen_solve_exact_mixed_magnitude_entries_tests!(3);
1802    gen_solve_exact_mixed_magnitude_entries_tests!(4);
1803    gen_solve_exact_mixed_magnitude_entries_tests!(5);
1804
1805    /// Subnormal RHS entries must survive the decomposition and
1806    /// back-substitution paths unchanged.  The D×D case uses the identity
1807    /// matrix and RHS `[1·tiny, 2·tiny, …, D·tiny]`; each entry remains a
1808    /// valid subnormal f64 (integer multiples of `2^-1074` fit in the
1809    /// 52-bit subnormal mantissa for the small integers used here).
1810    macro_rules! gen_solve_exact_subnormal_rhs_tests {
1811        ($d:literal) => {
1812            paste! {
1813                #[test]
1814                #[allow(clippy::cast_precision_loss)]
1815                fn [<solve_exact_subnormal_rhs_ $d d>]() {
1816                    let tiny = 5e-324_f64; // smallest positive subnormal
1817                    assert!(tiny.is_subnormal());
1818                    let a = Matrix::<$d>::identity();
1819                    // b[i] = (i+1) · tiny — each entry remains a valid subnormal.
1820                    let mut b_arr = [0.0f64; $d];
1821                    for i in 0..$d {
1822                        b_arr[i] = (i + 1) as f64 * tiny;
1823                        assert!(b_arr[i].is_subnormal());
1824                    }
1825                    let b = Vector::<$d>::new(b_arr);
1826                    let x = a.solve_exact(b).unwrap();
1827                    for i in 0..$d {
1828                        assert_eq!(x[i], f64_to_bigrational((i + 1) as f64 * tiny));
1829                    }
1830                }
1831            }
1832        };
1833    }
1834
1835    gen_solve_exact_subnormal_rhs_tests!(2);
1836    gen_solve_exact_subnormal_rhs_tests!(3);
1837    gen_solve_exact_subnormal_rhs_tests!(4);
1838    gen_solve_exact_subnormal_rhs_tests!(5);
1839
1840    /// Pivoting path with a zero top-left entry forces a row swap in the
1841    /// `BigInt` forward-elimination loop and propagates it to the RHS.
1842    /// Combined with a fractional solution, this exercises the
1843    /// `BigRational` back-substitution after integer forward elimination.
1844    ///
1845    /// The 2×2 block `[[0, 1], [2, 1]]` with rhs `[3, 4]` (→ `x = [1/2, 3]`)
1846    /// is embedded into the top-left of a D×D identity matrix.  Remaining
1847    /// rows contribute pass-through equalities `x[i] = b[i]`, so the same
1848    /// fractional solution appears at indices 0 and 1 regardless of D.
1849    macro_rules! gen_solve_exact_pivot_swap_fractional_tests {
1850        ($d:literal) => {
1851            paste! {
1852                #[test]
1853                #[allow(clippy::cast_precision_loss)]
1854                // `2..$d` is empty when D=2 (no padded rows); that is the
1855                // intended behaviour of the macro, not a bug.
1856                #[allow(clippy::reversed_empty_ranges)]
1857                fn [<solve_exact_pivot_swap_with_fractional_result_ $d d>]() {
1858                    // Top-left 2×2: A = [[0, 1], [2, 1]].  After swap:
1859                    // [[2, 1], [0, 1]], rhs = [4, 3] → x[1] = 3, x[0] = 1/2.
1860                    let mut rows = [[0.0f64; $d]; $d];
1861                    rows[0][1] = 1.0;
1862                    rows[1][0] = 2.0;
1863                    rows[1][1] = 1.0;
1864                    // Identity padding for the remaining rows.
1865                    for i in 2..$d {
1866                        rows[i][i] = 1.0;
1867                    }
1868                    let a = Matrix::<$d>::from_rows(rows);
1869                    // b = [3, 4, 12, 13, …]; padded entries are arbitrary
1870                    // finite integers so the identity block gives x[i] = b[i].
1871                    let mut b_arr = [0.0f64; $d];
1872                    b_arr[0] = 3.0;
1873                    b_arr[1] = 4.0;
1874                    for i in 2..$d {
1875                        b_arr[i] = (i + 10) as f64;
1876                    }
1877                    let b = Vector::<$d>::new(b_arr);
1878                    let x = a.solve_exact(b).unwrap();
1879                    assert_eq!(x[0], BigRational::new(BigInt::from(1), BigInt::from(2)));
1880                    assert_eq!(x[1], BigRational::from_integer(BigInt::from(3)));
1881                    for i in 2..$d {
1882                        assert_eq!(x[i], f64_to_bigrational((i + 10) as f64));
1883                    }
1884                }
1885            }
1886        };
1887    }
1888
1889    gen_solve_exact_pivot_swap_fractional_tests!(2);
1890    gen_solve_exact_pivot_swap_fractional_tests!(3);
1891    gen_solve_exact_pivot_swap_fractional_tests!(4);
1892    gen_solve_exact_pivot_swap_fractional_tests!(5);
1893
1894    /// Mid-elimination pivot swap: the 3×3 block
1895    /// `[[1, 2, 3], [0, 0, 4], [0, 5, 6]]` has a non-zero pivot at k=0 but
1896    /// a zero pivot at k=1, so the swap happens *during* forward
1897    /// elimination rather than at the start.  With rhs `[6, 7, 8]` the
1898    /// exact solution is `[7/4, -1/2, 7/4]`.  For D > 3 the block is
1899    /// embedded into the top-left of a D×D identity matrix so the same
1900    /// fractional solution appears in `x[0..3]` and `x[i] = b[i]` for
1901    /// `i >= 3`.
1902    macro_rules! gen_solve_exact_mid_pivot_swap_tests {
1903        ($d:literal) => {
1904            paste! {
1905                #[test]
1906                #[allow(clippy::cast_precision_loss)]
1907                // `3..$d` is empty when D=3 (no padded rows); that is the
1908                // intended behaviour of the macro, not a bug.
1909                #[allow(clippy::reversed_empty_ranges)]
1910                fn [<solve_exact_mid_pivot_swap_ $d d>]() {
1911                    let mut rows = [[0.0f64; $d]; $d];
1912                    rows[0][0] = 1.0; rows[0][1] = 2.0; rows[0][2] = 3.0;
1913                    // rows[1][0..2] are zero; rows[1][2] = 4.
1914                    rows[1][2] = 4.0;
1915                    rows[2][1] = 5.0; rows[2][2] = 6.0;
1916                    // Identity padding for the remaining rows.
1917                    for i in 3..$d {
1918                        rows[i][i] = 1.0;
1919                    }
1920                    let a = Matrix::<$d>::from_rows(rows);
1921                    let mut b_arr = [0.0f64; $d];
1922                    b_arr[0] = 6.0;
1923                    b_arr[1] = 7.0;
1924                    b_arr[2] = 8.0;
1925                    for i in 3..$d {
1926                        b_arr[i] = (i + 10) as f64;
1927                    }
1928                    let b = Vector::<$d>::new(b_arr);
1929                    let x = a.solve_exact(b).unwrap();
1930                    // x[0..3] = [7/4, -1/2, 7/4].
1931                    assert_eq!(x[0], BigRational::new(BigInt::from(7), BigInt::from(4)));
1932                    assert_eq!(x[1], BigRational::new(BigInt::from(-1), BigInt::from(2)));
1933                    assert_eq!(x[2], BigRational::new(BigInt::from(7), BigInt::from(4)));
1934                    for i in 3..$d {
1935                        assert_eq!(x[i], f64_to_bigrational((i + 10) as f64));
1936                    }
1937                }
1938            }
1939        };
1940    }
1941
1942    gen_solve_exact_mid_pivot_swap_tests!(3);
1943    gen_solve_exact_mid_pivot_swap_tests!(4);
1944    gen_solve_exact_mid_pivot_swap_tests!(5);
1945
1946    /// Rank-deficient singular: the last column is identically zero and the
1947    /// leading `(D-1)×(D-1)` block is full rank, so every intermediate
1948    /// pivot is non-zero and the singularity surfaces only at the final
1949    /// column.  The matrix is identity in the top-left `(D-1)×(D-1)` with
1950    /// a row of ones as the last row (and an all-zero last column), so the
1951    /// rank is exactly `D-1`.  `solve_exact` must return
1952    /// `LaError::Singular { pivot_col: D - 1 }`.
1953    macro_rules! gen_solve_exact_singular_rank_deficient_tests {
1954        ($d:literal) => {
1955            paste! {
1956                #[test]
1957                fn [<solve_exact_singular_rank_deficient_ $d d>]() {
1958                    let mut rows = [[0.0f64; $d]; $d];
1959                    for i in 0..($d - 1) {
1960                        rows[i][i] = 1.0;
1961                        rows[$d - 1][i] = 1.0;
1962                    }
1963                    // Last column is left all-zero → rank exactly D-1.
1964                    let a = Matrix::<$d>::from_rows(rows);
1965                    let b = Vector::<$d>::new([1.0; $d]);
1966                    assert_eq!(
1967                        a.solve_exact(b),
1968                        Err(LaError::Singular { pivot_col: $d - 1 })
1969                    );
1970                }
1971            }
1972        };
1973    }
1974
1975    gen_solve_exact_singular_rank_deficient_tests!(2);
1976    gen_solve_exact_singular_rank_deficient_tests!(3);
1977    gen_solve_exact_singular_rank_deficient_tests!(4);
1978    gen_solve_exact_singular_rank_deficient_tests!(5);
1979
1980    /// Zero RHS with a non-singular matrix.  Every Bareiss update reads
1981    /// `rhs[k]` and `rhs[i]`, both initialised to zero; every update
1982    /// produces zero; back-substitution therefore yields `x = 0`
1983    /// regardless of the matrix entries.  This exercises the
1984    /// back-substitution `mem::take` path against an all-zero `rhs`.
1985    macro_rules! gen_solve_exact_zero_rhs_tests {
1986        ($d:literal) => {
1987            paste! {
1988                #[test]
1989                fn [<solve_exact_zero_rhs_ $d d>]() {
1990                    // A = D*I + J (diagonally dominant, invertible).
1991                    let mut rows = [[1.0f64; $d]; $d];
1992                    for i in 0..$d {
1993                        rows[i][i] = f64::from($d) + 1.0;
1994                    }
1995                    let a = Matrix::<$d>::from_rows(rows);
1996                    let b = Vector::<$d>::zero();
1997                    let x = a.solve_exact(b).unwrap();
1998                    for xi in &x {
1999                        assert_eq!(*xi, BigRational::from_integer(BigInt::from(0)));
2000                    }
2001                }
2002            }
2003        };
2004    }
2005
2006    gen_solve_exact_zero_rhs_tests!(2);
2007    gen_solve_exact_zero_rhs_tests!(3);
2008    gen_solve_exact_zero_rhs_tests!(4);
2009    gen_solve_exact_zero_rhs_tests!(5);
2010
2011    // -----------------------------------------------------------------------
2012    // Adversarial-input coverage mirroring `benches/exact.rs`
2013    // -----------------------------------------------------------------------
2014    //
2015    // These tests pin the behaviour of the extreme-input benchmark groups
2016    // (`exact_near_singular_3x3`, `exact_large_entries_3x3`,
2017    // `exact_hilbert_{4x4,5x5}`) so a regression would be caught even
2018    // when benchmarks are not running.
2019
2020    /// Multiply `A · x` entirely in `BigRational`, using `f64_to_bigrational`
2021    /// to lift each matrix entry.  Used by residual assertions for inputs
2022    /// whose exact solution has no closed form we can easily type out.
2023    fn bigrational_matvec<const D: usize>(a: &Matrix<D>, x: &[BigRational; D]) -> [BigRational; D] {
2024        from_fn(|i| {
2025            let mut sum = BigRational::from_integer(BigInt::from(0));
2026            for (aij, xj) in a.rows[i].iter().zip(x.iter()) {
2027                sum += f64_to_bigrational(*aij) * xj;
2028            }
2029            sum
2030        })
2031    }
2032
2033    /// Near-singular 3×3 solve (matches the `exact_near_singular_3x3`
2034    /// bench).  With `A = [[1+2^-50, 2, 3], [4, 5, 6], [7, 8, 9]]` and
2035    /// `x0 = [1, 1, 1]`, `A · x0 = [6 + 2^-50, 15, 24]`; every component is
2036    /// exactly representable in `f64` (`6` has ulp `2^-50` at its exponent).
2037    /// `solve_exact` must recover `x0` exactly — the fractional denominator
2038    /// introduced by `det(A) = -3 × 2^-50` cancels cleanly against the
2039    /// augmented RHS.
2040    #[test]
2041    fn solve_exact_near_singular_3x3_integer_x0() {
2042        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2043        let a = Matrix::<3>::from_rows([
2044            [1.0 + perturbation, 2.0, 3.0],
2045            [4.0, 5.0, 6.0],
2046            [7.0, 8.0, 9.0],
2047        ]);
2048        let b = Vector::<3>::new([6.0 + perturbation, 15.0, 24.0]);
2049        let x = a.solve_exact(b).unwrap();
2050        let one = BigRational::from_integer(BigInt::from(1));
2051        assert_eq!(x[0], one);
2052        assert_eq!(x[1], one);
2053        assert_eq!(x[2], one);
2054    }
2055
2056    /// Large-entry 3×3 solve (matches the `exact_large_entries_3x3`
2057    /// bench).  `A = big · I + (1 - I)` with `big = f64::MAX / 2` and
2058    /// `b = [big, 1, 1] = A · [1, 0, 0]`.  The `BigInt` augmented system
2059    /// sees entries of ~1023 bits on the diagonal and unit entries
2060    /// elsewhere; Bareiss elimination still produces the exact integer
2061    /// solution `[1, 0, 0]`.
2062    #[test]
2063    fn solve_exact_large_entries_3x3_unit_vector() {
2064        let big = f64::MAX / 2.0;
2065        assert!(big.is_finite());
2066        let a = Matrix::<3>::from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]]);
2067        let b = Vector::<3>::new([big, 1.0, 1.0]);
2068        let x = a.solve_exact(b).unwrap();
2069        let zero = BigRational::from_integer(BigInt::from(0));
2070        let one = BigRational::from_integer(BigInt::from(1));
2071        assert_eq!(x[0], one);
2072        assert_eq!(x[1], zero);
2073        assert_eq!(x[2], zero);
2074    }
2075
2076    /// Determinant of the large-entry 3×3 is roughly `big^3`, which
2077    /// overflows `f64`.  `det_direct()` therefore returns `±∞`, the fast
2078    /// filter inside `det_sign_exact` falls through on the `is_finite()`
2079    /// guard, and the Bareiss fallback resolves the positive sign
2080    /// correctly.  `det_exact_f64` must report `Overflow`.
2081    #[test]
2082    fn det_sign_exact_large_entries_3x3_positive() {
2083        let big = f64::MAX / 2.0;
2084        let a = Matrix::<3>::from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]]);
2085        // Fast filter is inconclusive (big^3 overflows f64 to +∞), so
2086        // this exercises the Bareiss cold path.
2087        assert!(!a.det_direct().is_some_and(f64::is_finite));
2088        assert_eq!(a.det_sign_exact().unwrap(), 1);
2089        // Cross-validate: the exact `BigRational` determinant must agree
2090        // on sign with `det_sign_exact`, and `det_exact_f64` must overflow
2091        // (the value is representable in BigRational but far exceeds f64).
2092        assert!(a.det_exact().unwrap().is_positive());
2093        assert_eq!(a.det_exact_f64(), Err(LaError::Overflow { index: None }));
2094    }
2095
2096    /// Hilbert matrices are symmetric positive-definite, so
2097    /// `det_sign_exact` must return `1` for every D.  For D=2..=4 the
2098    /// fast f64 filter resolves the positive sign without falling
2099    /// through (Hilbert's determinant is tiny but still well above the
2100    /// `det_errbound` cushion); for D=5 the filter is skipped entirely
2101    /// and the Bareiss path handles inputs whose `(mantissa, exponent)`
2102    /// pairs all differ.
2103    macro_rules! gen_det_sign_exact_hilbert_positive_tests {
2104        ($d:literal) => {
2105            paste! {
2106                #[test]
2107                #[allow(clippy::cast_precision_loss)]
2108                fn [<det_sign_exact_hilbert_positive_ $d d>]() {
2109                    let mut rows = [[0.0f64; $d]; $d];
2110                    for r in 0..$d {
2111                        for c in 0..$d {
2112                            rows[r][c] = 1.0 / ((r + c + 1) as f64);
2113                        }
2114                    }
2115                    let h = Matrix::<$d>::from_rows(rows);
2116                    assert_eq!(h.det_sign_exact().unwrap(), 1);
2117                }
2118            }
2119        };
2120    }
2121
2122    gen_det_sign_exact_hilbert_positive_tests!(2);
2123    gen_det_sign_exact_hilbert_positive_tests!(3);
2124    gen_det_sign_exact_hilbert_positive_tests!(4);
2125    gen_det_sign_exact_hilbert_positive_tests!(5);
2126
2127    /// `solve_exact` on a Hilbert matrix must produce a solution whose
2128    /// residual `A · x - b` is *exactly* zero in `BigRational` arithmetic.
2129    /// Hilbert entries (`1/3`, `1/5`, `1/6`, `1/7`, …) are non-terminating
2130    /// in binary, so this is a stronger test than the
2131    /// `gen_solve_exact_roundtrip_tests` construction (which requires the
2132    /// RHS to be representable as an exact `f64` product).
2133    macro_rules! gen_solve_exact_hilbert_residual_tests {
2134        ($d:literal) => {
2135            paste! {
2136                #[test]
2137                #[allow(clippy::cast_precision_loss)]
2138                fn [<solve_exact_hilbert_residual_ $d d>]() {
2139                    let mut rows = [[0.0f64; $d]; $d];
2140                    for r in 0..$d {
2141                        for c in 0..$d {
2142                            rows[r][c] = 1.0 / ((r + c + 1) as f64);
2143                        }
2144                    }
2145                    let h = Matrix::<$d>::from_rows(rows);
2146                    // Use a non-trivial RHS with both positive and negative
2147                    // entries to avoid accidental structural cancellation.
2148                    let mut b_arr = [0.0f64; $d];
2149                    for i in 0usize..$d {
2150                        let sign = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
2151                        b_arr[i] = sign * ((i + 1) as f64);
2152                    }
2153                    let b = Vector::<$d>::new(b_arr);
2154                    let x = h.solve_exact(b).unwrap();
2155                    let ax = bigrational_matvec(&h, &x);
2156                    for i in 0..$d {
2157                        assert_eq!(ax[i], f64_to_bigrational(b_arr[i]));
2158                    }
2159                }
2160            }
2161        };
2162    }
2163
2164    gen_solve_exact_hilbert_residual_tests!(2);
2165    gen_solve_exact_hilbert_residual_tests!(3);
2166    gen_solve_exact_hilbert_residual_tests!(4);
2167    gen_solve_exact_hilbert_residual_tests!(5);
2168
2169    // -----------------------------------------------------------------------
2170    // solve_exact_f64: dimension-specific tests
2171    // -----------------------------------------------------------------------
2172
2173    #[test]
2174    fn solve_exact_f64_known_2x2() {
2175        let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
2176        let b = Vector::<2>::new([5.0, 11.0]);
2177        let x = a.solve_exact_f64(b).unwrap().into_array();
2178        assert!((x[0] - 1.0).abs() <= f64::EPSILON);
2179        assert!((x[1] - 2.0).abs() <= f64::EPSILON);
2180    }
2181
2182    #[test]
2183    fn solve_exact_f64_overflow_returns_err() {
2184        // [[1/big, 0], [0, 1/big]] x = [big, big] → x = [big², big²],
2185        // which overflows f64.
2186        let big = f64::MAX / 2.0;
2187        let a = Matrix::<2>::from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]);
2188        let b = Vector::<2>::new([big, big]);
2189        assert_eq!(
2190            a.solve_exact_f64(b),
2191            Err(LaError::Overflow { index: Some(0) })
2192        );
2193    }
2194
2195    // -----------------------------------------------------------------------
2196    // gauss_solve: internal helper tests
2197    // -----------------------------------------------------------------------
2198
2199    #[test]
2200    fn gauss_solve_d0_returns_empty() {
2201        let a = Matrix::<0>::zero();
2202        let b = Vector::<0>::zero();
2203        assert_eq!(gauss_solve(&a, &b).unwrap().len(), 0);
2204    }
2205
2206    #[test]
2207    fn gauss_solve_d1() {
2208        let a = Matrix::<1>::from_rows([[2.0]]);
2209        let b = Vector::<1>::new([6.0]);
2210        let x = gauss_solve(&a, &b).unwrap();
2211        assert_eq!(x[0], f64_to_bigrational(3.0));
2212    }
2213
2214    #[test]
2215    fn gauss_solve_singular_column_all_zero() {
2216        let a = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
2217        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
2218        assert_eq!(gauss_solve(&a, &b), Err(LaError::Singular { pivot_col: 1 }));
2219    }
2220
2221    // -----------------------------------------------------------------------
2222    // f64_to_bigrational tests
2223    // -----------------------------------------------------------------------
2224
2225    #[test]
2226    fn f64_to_bigrational_positive_zero() {
2227        let r = f64_to_bigrational(0.0);
2228        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
2229    }
2230
2231    #[test]
2232    fn f64_to_bigrational_negative_zero() {
2233        let r = f64_to_bigrational(-0.0);
2234        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
2235    }
2236
2237    #[test]
2238    fn f64_to_bigrational_one() {
2239        let r = f64_to_bigrational(1.0);
2240        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
2241    }
2242
2243    #[test]
2244    fn f64_to_bigrational_negative_one() {
2245        let r = f64_to_bigrational(-1.0);
2246        assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
2247    }
2248
2249    #[test]
2250    fn f64_to_bigrational_half() {
2251        let r = f64_to_bigrational(0.5);
2252        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
2253    }
2254
2255    #[test]
2256    fn f64_to_bigrational_quarter() {
2257        let r = f64_to_bigrational(0.25);
2258        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
2259    }
2260
2261    #[test]
2262    fn f64_to_bigrational_negative_three_and_a_half() {
2263        // -3.5 = -7/2
2264        let r = f64_to_bigrational(-3.5);
2265        assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
2266    }
2267
2268    #[test]
2269    fn f64_to_bigrational_integer() {
2270        let r = f64_to_bigrational(42.0);
2271        assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
2272    }
2273
2274    #[test]
2275    fn f64_to_bigrational_power_of_two() {
2276        let r = f64_to_bigrational(1024.0);
2277        assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
2278    }
2279
2280    #[test]
2281    fn f64_to_bigrational_subnormal() {
2282        let tiny = 5e-324_f64; // smallest positive subnormal
2283        assert!(tiny.is_subnormal());
2284        let r = f64_to_bigrational(tiny);
2285        // 5e-324 = 1 × 2^(-1074)
2286        assert_eq!(
2287            r,
2288            BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
2289        );
2290    }
2291
2292    #[test]
2293    fn f64_to_bigrational_already_lowest_terms() {
2294        // 0.5 should produce numer=1, denom=2 (already reduced).
2295        let r = f64_to_bigrational(0.5);
2296        assert_eq!(*r.numer(), BigInt::from(1));
2297        assert_eq!(*r.denom(), BigInt::from(2));
2298    }
2299
2300    #[test]
2301    fn f64_to_bigrational_round_trip() {
2302        // -0.0 is excluded: it maps to BigRational(0) which round-trips
2303        // to +0.0 (correct; tested separately in f64_to_bigrational_negative_zero).
2304        let values = [
2305            0.0,
2306            1.0,
2307            -1.0,
2308            0.5,
2309            0.25,
2310            0.1,
2311            42.0,
2312            -3.5,
2313            1e10,
2314            1e-10,
2315            f64::MAX / 2.0,
2316            f64::MIN_POSITIVE,
2317            5e-324,
2318        ];
2319        for &v in &values {
2320            let r = f64_to_bigrational(v);
2321            let back = r.to_f64().expect("round-trip to_f64 failed");
2322            assert!(
2323                v.to_bits() == back.to_bits(),
2324                "round-trip failed for {v}: got {back}"
2325            );
2326        }
2327    }
2328
2329    #[test]
2330    #[should_panic(expected = "non-finite f64 in exact conversion")]
2331    fn f64_to_bigrational_panics_on_nan() {
2332        f64_to_bigrational(f64::NAN);
2333    }
2334
2335    #[test]
2336    #[should_panic(expected = "non-finite f64 in exact conversion")]
2337    fn f64_to_bigrational_panics_on_inf() {
2338        f64_to_bigrational(f64::INFINITY);
2339    }
2340
2341    #[test]
2342    #[should_panic(expected = "non-finite f64 in exact conversion")]
2343    fn f64_to_bigrational_panics_on_neg_inf() {
2344        f64_to_bigrational(f64::NEG_INFINITY);
2345    }
2346}