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