Skip to main content

la_stack/
exact.rs

1#![forbid(unsafe_code)]
2
3//! Exact arithmetic operations via arbitrary-precision rational numbers.
4//!
5//! This module is only compiled when the `"exact"` Cargo feature is enabled.
6//!
7//! # Architecture
8//!
9//! ## Determinants
10//!
11//! All determinant methods (`det_exact`, `det_exact_f64`,
12//! `det_exact_rounded_f64`, and `det_sign_exact`) share the same integer-scaled
13//! determinant core. Each f64 entry is decomposed via `f64_decompose` into
14//! `mantissa × 2^exponent`, then all entries are scaled to a common `BigInt`
15//! matrix (shifting by `e - e_min`). D≤4 uses direct integer expansions; larger
16//! matrices use fraction-free Bareiss elimination entirely in `BigInt`
17//! arithmetic — no `BigRational`, no GCD, no denominator tracking. The result
18//! is `(det_int, total_exp)` where `det = det_int × 2^(D × e_min)`. `det_exact`
19//! wraps this with `bigint_exp_to_bigrational` to reconstruct a reduced
20//! `BigRational`; `det_exact_f64` converts the same pair only when the exact
21//! value is representable as finite binary64; `det_exact_rounded_f64` rounds
22//! the same exact value to finite binary64; and `det_sign_exact` reads the sign
23//! directly from `det_int` (the scale factor is always positive).
24//!
25//! `det_sign_exact` adds a two-stage adaptive-precision optimisation inspired
26//! by Shewchuk's robust geometric predicates:
27//!
28//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
29//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
30//!    immediately without allocating.
31//! 2. **Exact fallback**: run integer-only Bareiss for a guaranteed-correct
32//!    sign.
33//!
34//! ## Linear system solve
35//!
36//! `solve_exact`, `solve_exact_f64`, and `solve_exact_rounded_f64` solve
37//! `A x = b` with a hybrid algorithm that reuses the integer-only Bareiss core
38//! used for determinants.  Matrix and RHS entries are decomposed via
39//! `f64_decompose` into `mantissa × 2^exponent`, scaled to a shared
40//! base `2^e_min`, and assembled into a `BigInt` augmented system
41//! `(A | b)`.  Forward elimination runs entirely in `BigInt` with
42//! fraction-free Bareiss updates — no `BigRational`, no GCD
43//! normalisation in the `O(D³)` phase.  Once the system is upper
44//! triangular, back-substitution is performed in `BigRational`, where
45//! fractions are inherent; this phase is only `O(D²)` so the rational
46//! overhead is modest.  First-non-zero pivoting is used throughout;
47//! since all arithmetic is exact, any non-zero pivot gives the correct
48//! result (no numerical stability concern).  Every finite `f64` is
49//! exactly representable as a rational, so the result is provably correct.
50//! `solve_exact_f64` returns `Vector<D>` only when every exact component is
51//! exactly representable as finite binary64; `solve_exact_rounded_f64` returns
52//! the exact components rounded to finite binary64.
53//!
54//! ## f64 → integer decomposition
55//!
56//! Both the determinant and solve paths share a single conversion
57//! primitive, `f64_decompose`, which extracts `(mantissa, exponent,
58//! sign)` from the IEEE 754 binary64 bit representation (\[9\]).  The
59//! determinant path combines those components into a `BigInt` matrix
60//! (for Bareiss) and a `2^(D × e_min)` scale factor, while the solve
61//! path builds a `BigInt` augmented system and lifts the
62//! upper-triangular result into `BigRational` for back-substitution.
63//! See Goldberg \[10\] for background on floating-point representation
64//! and exact rational reconstruction.  Reference numbers refer to
65//! `REFERENCES.md`.
66//!
67//! ## Validation
68//!
69//! Public `Matrix` / `Vector` values are finite by construction before exact
70//! methods reach the integer-Bareiss core. The decomposition helpers for those
71//! domain types can then call `f64_decompose` without repeating stored-entry
72//! validation; `f64_decompose` itself is therefore never called with non-finite
73//! input from the public API.
74
75use core::hint::cold_path;
76use core::mem::take;
77use core::num::NonZeroU64;
78use std::array::from_fn;
79
80use num_bigint::{BigInt, Sign};
81use num_rational::BigRational;
82use num_traits::ToPrimitive;
83
84use crate::matrix::Matrix;
85use crate::vector::Vector;
86use crate::{LaError, UnrepresentableReason};
87
88const F64_SIGNIFICAND_BITS: i64 = 53;
89const F64_FRACTION_BITS: i64 = 52;
90const F64_MIN_BINARY_EXPONENT: i64 = -1074;
91const F64_MIN_NORMAL_EXPONENT: i64 = -1022;
92const F64_MAX_BINARY_EXPONENT: i64 = 1023;
93const F64_EXPONENT_BIAS: i64 = 1023;
94const F64_FRACTION_MASK: u64 = (1u64 << 52) - 1;
95
96/// Decompose a finite `f64` into its IEEE 754 components.
97///
98/// Returns `None` for ±0.0, or `Some((mantissa, exponent, is_negative))` with a
99/// non-zero mantissa where the value is exactly
100/// `(-1)^is_negative × mantissa × 2^exponent` and `mantissa` is odd (trailing
101/// zeros stripped).  See `REFERENCES.md` \[9-10\].
102///
103/// # Errors
104/// Returns [`LaError::NonFinite`] if `x` is NaN or infinite.
105const fn f64_decompose(x: f64) -> Result<Option<(NonZeroU64, i32, bool)>, LaError> {
106    let bits = x.to_bits();
107    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
108    let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
109
110    // ±0.0
111    if biased_exp == 0 && fraction == 0 {
112        return Ok(None);
113    }
114
115    if biased_exp == 0x7FF {
116        cold_path();
117        return Err(LaError::non_finite_at(0));
118    }
119
120    let (mantissa, raw_exp) = if biased_exp == 0 {
121        // Subnormal: (-1)^s × 0.fraction × 2^(-1022)
122        //          = (-1)^s × fraction × 2^(-1074)
123        (fraction, -1074_i32)
124    } else {
125        // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023)
126        //       = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075)
127        ((1u64 << 52) | fraction, biased_exp - 1075)
128    };
129
130    // Strip trailing zeros so the mantissa is odd.
131    let tz = mantissa.trailing_zeros();
132    let mantissa = mantissa >> tz;
133    let Some(mantissa) = NonZeroU64::new(mantissa) else {
134        cold_path();
135        return Ok(None);
136    };
137    let exponent = raw_exp + tz.cast_signed();
138    let is_negative = bits >> 63 != 0;
139
140    Ok(Some((mantissa, exponent, is_negative)))
141}
142
143/// Convert a `BigInt × 2^exp` pair to a reduced `BigRational`.
144///
145/// When `exp < 0` (denominator is `2^(-exp)`), shared factors of 2 are
146/// stripped from `value` to keep the fraction in lowest terms without a
147/// full GCD computation.
148fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
149    if value == BigInt::from(0) {
150        return BigRational::from_integer(BigInt::from(0));
151    }
152
153    // Strip shared powers of 2 between value and the 2^(-exp) denominator.
154    if exp < 0
155        && let Some(tz) = value.trailing_zeros()
156    {
157        let exp_abs = exp.unsigned_abs();
158        let reduce = tz.min(u64::from(exp_abs));
159        value >>= reduce;
160        #[allow(clippy::cast_possible_truncation)]
161        let reduce = reduce as u32;
162        let remaining_abs = exp_abs - reduce;
163        exp = match remaining_abs {
164            0 => 0,
165            2_147_483_648 => i32::MIN,
166            value => -value.cast_signed(),
167        };
168    }
169
170    if exp >= 0 {
171        BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
172    } else {
173        BigRational::new_raw(value, BigInt::from(1u32) << exp.unsigned_abs())
174    }
175}
176
177/// Convert an exact rational result to `f64` only when the conversion is exact.
178///
179/// This supports the strict `*_exact_f64` public APIs by accepting only dyadic
180/// rational values that fit in finite binary64. The optional `index` is attached
181/// to [`LaError::Unrepresentable`] for vector-valued solve components.
182///
183/// # Errors
184/// Returns [`LaError::Unrepresentable`] with
185/// [`UnrepresentableReason::RequiresRounding`] when the rational denominator is
186/// not a power of two and the rounded value would still be finite.
187fn exact_rational_to_finite_f64(exact: &BigRational, index: Option<usize>) -> Result<f64, LaError> {
188    let numerator = exact.numer();
189    if numerator.sign() == Sign::NoSign {
190        return Ok(0.0);
191    }
192
193    let denominator = exact.denom();
194    let Some(denominator_exp) = denominator.trailing_zeros() else {
195        cold_path();
196        return Err(LaError::unrepresentable(
197            index,
198            rounded_rational_unrepresentable_reason(exact),
199        ));
200    };
201
202    if denominator.bits().checked_sub(1) != Some(denominator_exp) {
203        cold_path();
204        return Err(LaError::unrepresentable(
205            index,
206            rounded_rational_unrepresentable_reason(exact),
207        ));
208    }
209
210    let Ok(denominator_exp) = i32::try_from(denominator_exp) else {
211        cold_path();
212        return Err(LaError::unrepresentable(
213            index,
214            rounded_rational_unrepresentable_reason(exact),
215        ));
216    };
217
218    bigint_exp_to_finite_f64(numerator.clone(), -denominator_exp, index)
219}
220
221/// Classify a failed exact-rational-to-`f64` conversion by the rounded result.
222///
223/// Strict exact conversion has already failed when this helper is called. It
224/// preserves the [`UnrepresentableReason`] recovery contract: callers may retry
225/// with a rounded API only when that rounded result would still be finite.
226fn rounded_rational_unrepresentable_reason(exact: &BigRational) -> UnrepresentableReason {
227    match exact.to_f64() {
228        Some(value) if value.is_finite() => UnrepresentableReason::RequiresRounding,
229        _ => UnrepresentableReason::NotFinite,
230    }
231}
232
233/// Convert an exact rational result to a rounded finite `f64`.
234fn exact_rational_to_rounded_f64(
235    exact: &BigRational,
236    index: Option<usize>,
237) -> Result<f64, LaError> {
238    let Some(value) = exact.to_f64() else {
239        cold_path();
240        return Err(LaError::unrepresentable(
241            index,
242            UnrepresentableReason::NotFinite,
243        ));
244    };
245    if value.is_finite() {
246        Ok(value)
247    } else {
248        cold_path();
249        Err(LaError::unrepresentable(
250            index,
251            UnrepresentableReason::NotFinite,
252        ))
253    }
254}
255
256/// Convert a `BigInt × 2^exp` pair to an exactly represented finite `f64`.
257///
258/// This avoids allocating a [`BigRational`] when determinant and solve paths
259/// already have an integer significand plus binary exponent. The optional
260/// `index` is forwarded to [`LaError::Unrepresentable`] for vector-valued solve
261/// components; determinant callers pass `None`.
262///
263/// # Errors
264/// Returns [`LaError::Unrepresentable`] with
265/// [`UnrepresentableReason::RequiresRounding`] when the exact nonzero value
266/// would need rounding or underflows below the smallest positive subnormal.
267///
268/// Returns [`LaError::Unrepresentable`] with
269/// [`UnrepresentableReason::NotFinite`] when the exact value cannot be
270/// represented by any finite `f64`.
271fn bigint_exp_to_finite_f64(
272    mut value: BigInt,
273    exp: i32,
274    index: Option<usize>,
275) -> Result<f64, LaError> {
276    if value == BigInt::from(0) {
277        return Ok(0.0);
278    }
279
280    let is_negative = value.sign() == Sign::Minus;
281    if is_negative {
282        value = -value;
283    }
284
285    let mut exp = i64::from(exp);
286    if let Some(tz) = value.trailing_zeros() {
287        value >>= tz;
288        let Ok(tz) = i64::try_from(tz) else {
289            cold_path();
290            return Err(LaError::unrepresentable(
291                index,
292                UnrepresentableReason::NotFinite,
293            ));
294        };
295        let Some(updated_exp) = exp.checked_add(tz) else {
296            cold_path();
297            return Err(LaError::unrepresentable(
298                index,
299                UnrepresentableReason::NotFinite,
300            ));
301        };
302        exp = updated_exp;
303    }
304
305    let bit_len = value.bits();
306    let Ok(bit_len) = i64::try_from(bit_len) else {
307        cold_path();
308        return Err(LaError::unrepresentable(
309            index,
310            UnrepresentableReason::NotFinite,
311        ));
312    };
313    let Some(top_bit_exp) = exp.checked_add(bit_len - 1) else {
314        cold_path();
315        return Err(LaError::unrepresentable(
316            index,
317            UnrepresentableReason::NotFinite,
318        ));
319    };
320    if exp < F64_MIN_BINARY_EXPONENT {
321        cold_path();
322        return Err(LaError::unrepresentable(
323            index,
324            UnrepresentableReason::RequiresRounding,
325        ));
326    }
327    if top_bit_exp > F64_MAX_BINARY_EXPONENT {
328        cold_path();
329        return Err(LaError::unrepresentable(
330            index,
331            UnrepresentableReason::NotFinite,
332        ));
333    }
334    if bit_len > F64_SIGNIFICAND_BITS {
335        cold_path();
336        return Err(LaError::unrepresentable(
337            index,
338            UnrepresentableReason::RequiresRounding,
339        ));
340    }
341
342    let Some(mantissa) = value.to_u64() else {
343        cold_path();
344        return Err(LaError::unrepresentable(
345            index,
346            UnrepresentableReason::NotFinite,
347        ));
348    };
349    let sign = if is_negative { 1u64 << 63 } else { 0 };
350
351    if top_bit_exp < F64_MIN_NORMAL_EXPONENT {
352        let Ok(shift) = u32::try_from(exp - F64_MIN_BINARY_EXPONENT) else {
353            cold_path();
354            return Err(LaError::unrepresentable(
355                index,
356                UnrepresentableReason::RequiresRounding,
357            ));
358        };
359        Ok(f64::from_bits(sign | (mantissa << shift)))
360    } else {
361        let Ok(biased_exp) = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS) else {
362            cold_path();
363            return Err(LaError::unrepresentable(
364                index,
365                UnrepresentableReason::NotFinite,
366            ));
367        };
368        let Ok(shift) = u32::try_from(F64_FRACTION_BITS - (bit_len - 1)) else {
369            cold_path();
370            return Err(LaError::unrepresentable(
371                index,
372                UnrepresentableReason::RequiresRounding,
373            ));
374        };
375        let significand = mantissa << shift;
376        Ok(f64::from_bits(
377            sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
378        ))
379    }
380}
381
382/// Convert a `BigInt × 2^exp` determinant pair to a rounded finite `f64`.
383fn bigint_exp_to_rounded_f64(value: BigInt, exp: i32) -> Result<f64, LaError> {
384    let exact = bigint_exp_to_bigrational(value, exp);
385    exact_rational_to_rounded_f64(&exact, None)
386}
387
388// -----------------------------------------------------------------------
389// Shared integer-Bareiss primitives
390// -----------------------------------------------------------------------
391//
392// Both `bareiss_det_int` (determinants) and `gauss_solve` (linear system
393// solve) follow the same pipeline: decompose every f64 entry into
394// `(mantissa, exponent, is_negative)`, track the minimum exponent across
395// non-zero entries, scale each entry by `2^(exp − e_min)` to build a
396// fully-integer `BigInt` matrix, and run Bareiss fraction-free forward
397// elimination.  The helpers below factor out each stage so the two
398// callers differ only in post-processing (± sign for det, back-sub for
399// solve) and in whether they carry a RHS through the elimination.
400
401/// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`.
402///
403/// `Zero` represents ±0.0. Non-zero entries carry a [`NonZeroU64`] mantissa, so
404/// the exact-arithmetic paths cannot accidentally combine an absent mantissa
405/// with active exponent/sign fields after decomposition.
406#[derive(Clone, Copy, Default)]
407enum Component {
408    #[default]
409    Zero,
410    NonZero {
411        mantissa: NonZeroU64,
412        exponent: i32,
413        is_negative: bool,
414    },
415}
416
417/// Decompose every entry of a finite `D×D` matrix via `f64_decompose`.
418///
419/// Returns the per-entry components and the minimum exponent across non-zero
420/// entries. If every entry is zero, the exponent is `i32::MAX`.
421fn decompose_finite_matrix<const D: usize>(
422    m: &Matrix<D>,
423) -> Result<([[Component; D]; D], i32), LaError> {
424    let mut components = [[Component::default(); D]; D];
425    let mut e_min = i32::MAX;
426    for (r, row) in m.rows().iter().enumerate() {
427        for (c, &entry) in row.iter().enumerate() {
428            if let Some((mantissa, exponent, is_negative)) =
429                f64_decompose(entry).map_err(|_| LaError::non_finite_cell(r, c))?
430            {
431                components[r][c] = Component::NonZero {
432                    mantissa,
433                    exponent,
434                    is_negative,
435                };
436                e_min = e_min.min(exponent);
437            }
438        }
439    }
440    Ok((components, e_min))
441}
442
443/// Decompose every entry of a finite length-`D` vector via `f64_decompose`.
444///
445/// Returns the per-entry components and the minimum exponent across non-zero
446/// entries. If every entry is zero, the exponent is `i32::MAX`.
447fn decompose_finite_vec<const D: usize>(v: &Vector<D>) -> Result<([Component; D], i32), LaError> {
448    let mut components = [Component::default(); D];
449    let mut e_min = i32::MAX;
450    let data = v.as_array();
451    for (i, &entry) in data.iter().enumerate() {
452        if let Some((mantissa, exponent, is_negative)) =
453            f64_decompose(entry).map_err(|_| LaError::non_finite_at(i))?
454        {
455            components[i] = Component::NonZero {
456                mantissa,
457                exponent,
458                is_negative,
459            };
460            e_min = e_min.min(exponent);
461        }
462    }
463    Ok((components, e_min))
464}
465
466/// Convert a single decomposed component to its scaled `BigInt`
467/// representation: `(±mantissa) << (exp − e_min)`.
468#[inline]
469fn component_to_bigint(c: Component, e_min: i32) -> BigInt {
470    match c {
471        Component::Zero => BigInt::from(0),
472        Component::NonZero {
473            mantissa,
474            exponent,
475            is_negative,
476        } => {
477            let v = BigInt::from(mantissa.get()) << (exponent - e_min).cast_unsigned();
478            if is_negative { -v } else { v }
479        }
480    }
481}
482
483/// Build a `D×D` integer matrix from a component table, scaled to the
484/// shared base `2^e_min`.
485fn build_bigint_matrix<const D: usize>(
486    components: &[[Component; D]; D],
487    e_min: i32,
488) -> [[BigInt; D]; D] {
489    from_fn(|r| from_fn(|c| component_to_bigint(components[r][c], e_min)))
490}
491
492/// Build a length-`D` integer vector from a component array, scaled to
493/// the shared base `2^e_min`.
494fn build_bigint_vec<const D: usize>(components: &[Component; D], e_min: i32) -> [BigInt; D] {
495    from_fn(|i| component_to_bigint(components[i], e_min))
496}
497
498/// Compute a 2×2 determinant from a scaled integer matrix.
499#[inline]
500fn det2_bigint<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
501    &a[0][0] * &a[1][1] - &a[0][1] * &a[1][0]
502}
503
504/// Compute a 3×3 determinant from scaled integer entries.
505#[inline]
506#[allow(clippy::too_many_arguments)]
507fn det3_bigint_entries(
508    a00: &BigInt,
509    a01: &BigInt,
510    a02: &BigInt,
511    a10: &BigInt,
512    a11: &BigInt,
513    a12: &BigInt,
514    a20: &BigInt,
515    a21: &BigInt,
516    a22: &BigInt,
517) -> BigInt {
518    let m00 = a11 * a22 - a12 * a21;
519    let m01 = a10 * a22 - a12 * a20;
520    let m02 = a10 * a21 - a11 * a20;
521    a00 * m00 - a01 * m01 + a02 * m02
522}
523
524/// Compute a 3×3 determinant from a scaled integer matrix.
525#[inline]
526fn det3_bigint<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
527    det3_bigint_entries(
528        &a[0][0], &a[0][1], &a[0][2], &a[1][0], &a[1][1], &a[1][2], &a[2][0], &a[2][1], &a[2][2],
529    )
530}
531
532/// Compute a 4×4 determinant from a scaled integer matrix.
533#[inline]
534fn det4_bigint<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
535    let mut det = BigInt::from(0);
536
537    if a[0][0].sign() != Sign::NoSign {
538        let c00 = det3_bigint_entries(
539            &a[1][1], &a[1][2], &a[1][3], &a[2][1], &a[2][2], &a[2][3], &a[3][1], &a[3][2],
540            &a[3][3],
541        );
542        det += &a[0][0] * c00;
543    }
544    if a[0][1].sign() != Sign::NoSign {
545        let c01 = det3_bigint_entries(
546            &a[1][0], &a[1][2], &a[1][3], &a[2][0], &a[2][2], &a[2][3], &a[3][0], &a[3][2],
547            &a[3][3],
548        );
549        det -= &a[0][1] * c01;
550    }
551    if a[0][2].sign() != Sign::NoSign {
552        let c02 = det3_bigint_entries(
553            &a[1][0], &a[1][1], &a[1][3], &a[2][0], &a[2][1], &a[2][3], &a[3][0], &a[3][1],
554            &a[3][3],
555        );
556        det += &a[0][2] * c02;
557    }
558    if a[0][3].sign() != Sign::NoSign {
559        let c03 = det3_bigint_entries(
560            &a[1][0], &a[1][1], &a[1][2], &a[2][0], &a[2][1], &a[2][2], &a[3][0], &a[3][1],
561            &a[3][2],
562        );
563        det -= &a[0][3] * c03;
564    }
565
566    det
567}
568
569/// Outcome of a Bareiss forward-elimination pass.
570#[derive(Debug)]
571enum BareissResult {
572    /// Elimination completed; `sign` is `±1` based on the parity of row
573    /// swaps (relevant for determinants; solves discard it).
574    Upper { sign: i8 },
575    /// Column `pivot_col` has no non-zero pivot at or below its diagonal.
576    Singular { pivot_col: usize },
577}
578
579/// Run Bareiss fraction-free forward elimination on the `D×D` integer
580/// matrix `a`, optionally augmented with a length-`D` RHS vector.
581///
582/// When `rhs` is `Some`, row swaps and the inner-loop Bareiss update are
583/// mirrored on the RHS (treating it as column `D+1` of an augmented
584/// system).  On return, `a` is upper triangular and the last pivot lives
585/// in `a[D-1][D-1]`.
586///
587/// First-non-zero pivoting is used: since all arithmetic is exact, any
588/// non-zero pivot is valid — no tolerance is required.
589fn bareiss_forward_eliminate<const D: usize>(
590    a: &mut [[BigInt; D]; D],
591    mut rhs: Option<&mut [BigInt; D]>,
592) -> BareissResult {
593    let zero = BigInt::from(0);
594    let mut prev_pivot = BigInt::from(1);
595    let mut sign: i8 = 1;
596
597    for k in 0..D {
598        // First-non-zero pivot search.
599        if a[k][k] == zero {
600            let mut found = false;
601            for i in (k + 1)..D {
602                if a[i][k] != zero {
603                    a.swap(k, i);
604                    if let Some(r) = &mut rhs {
605                        r.swap(k, i);
606                    }
607                    sign = -sign;
608                    found = true;
609                    break;
610                }
611            }
612            if !found {
613                cold_path();
614                return BareissResult::Singular { pivot_col: k };
615            }
616        }
617
618        // Elimination.  The Bareiss update reads the current `a[i][k]`
619        // in both the inner `j`-loop and the RHS update, so zero it only
620        // *after* those reads.
621        for i in (k + 1)..D {
622            for j in (k + 1)..D {
623                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
624            }
625            if let Some(r) = &mut rhs {
626                r[i] = (&a[k][k] * &r[i] - &a[i][k] * &r[k]) / &prev_pivot;
627            }
628            a[i][k].clone_from(&zero);
629        }
630
631        prev_pivot.clone_from(&a[k][k]);
632    }
633
634    // Post-conditions (debug builds only): `a` is upper triangular with
635    // non-zero pivots.  These catch future regressions in the inner-loop
636    // update or pivot-search logic without runtime cost in release.
637    // Indexed iteration is clearer than iterator chains here because the
638    // checks read disjoint cells across rows and columns at each step.
639    #[cfg(debug_assertions)]
640    #[allow(clippy::needless_range_loop)]
641    for k in 0..D {
642        assert_ne!(a[k][k], zero, "pivot at ({k}, {k}) must be non-zero");
643        for i in (k + 1)..D {
644            assert_eq!(a[i][k], zero, "sub-diagonal at ({i}, {k}) must be zero");
645        }
646    }
647
648    BareissResult::Upper { sign }
649}
650
651/// Compute the determinant scale exponent `D × e_min`.
652///
653/// This centralizes the scale-overflow classification used by public exact
654/// determinant APIs: [`Matrix::det_exact`], [`Matrix::det_exact_f64`], and
655/// [`Matrix::det_sign_exact`] all surface failures from this helper as
656/// [`LaError::DeterminantScaleOverflow`].
657///
658/// # Errors
659/// Returns [`LaError::DeterminantScaleOverflow`] if `D` cannot fit in the
660/// internal `i32` exponent multiplier or if `D × e_min` overflows `i32`.
661fn determinant_scale_exp<const D: usize>(e_min: i32) -> Result<i32, LaError> {
662    let Ok(d_i32) = i32::try_from(D) else {
663        cold_path();
664        return Err(LaError::determinant_scale_overflow(D, e_min));
665    };
666    let Some(total_exp) = e_min.checked_mul(d_i32) else {
667        cold_path();
668        return Err(LaError::determinant_scale_overflow(D, e_min));
669    };
670    Ok(total_exp)
671}
672
673/// Compute the exact determinant from integer-scaled entries.
674///
675/// Returns `(det_int, scale_exp)` where the true determinant is
676/// `det_int × 2^scale_exp`.  Since the scale factor `2^scale_exp` is always
677/// positive, `det_int.sign()` gives the sign of the determinant directly.
678///
679/// All arithmetic is in `BigInt` — no `BigRational`, no GCD, no denominator
680/// tracking.  Each f64 entry is decomposed into `mantissa × 2^exponent` and
681/// scaled to a common base `2^e_min` so every entry becomes an integer. D≤4
682/// uses direct determinant expansions; larger matrices use Bareiss elimination
683/// whose inner-loop division is exact (guaranteed by the algorithm).
684///
685fn bareiss_det_int_finite<const D: usize>(m: &Matrix<D>) -> Result<(BigInt, i32), LaError> {
686    // D == 0 has no `a[D-1][D-1]` to read; shortcut to the empty-product
687    // determinant.
688    if D == 0 {
689        return Ok((BigInt::from(1), 0));
690    }
691
692    let (components, e_min) = decompose_finite_matrix(m)?;
693
694    // All entries are zero → singular (det = 0).
695    if e_min == i32::MAX {
696        return Ok((BigInt::from(0), 0));
697    }
698
699    let mut a = build_bigint_matrix(&components, e_min);
700    let det_int = match D {
701        1 => a[0][0].clone(),
702        2 => det2_bigint(&a),
703        3 => det3_bigint(&a),
704        4 => det4_bigint(&a),
705        _ => {
706            let sign = match bareiss_forward_eliminate(&mut a, None) {
707                BareissResult::Upper { sign } => sign,
708                BareissResult::Singular { .. } => {
709                    cold_path();
710                    return Ok((BigInt::from(0), 0));
711                }
712            };
713
714            if sign < 0 {
715                -&a[D - 1][D - 1]
716            } else {
717                a[D - 1][D - 1].clone()
718            }
719        }
720    };
721
722    let total_exp = determinant_scale_exp::<D>(e_min)?;
723    Ok((det_int, total_exp))
724}
725
726/// Compute the exact determinant of a `D×D` matrix using integer-only Bareiss
727/// elimination and return the result as a `BigRational`.
728fn bareiss_det_finite<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
729    let (det_int, total_exp) = bareiss_det_int_finite(m)?;
730    Ok(bigint_exp_to_bigrational(det_int, total_exp))
731}
732
733/// Solve `A x = b` exactly after matrix and RHS finiteness has been proven.
734///
735/// Public [`Matrix`] / [`Vector`] values are finite by construction before
736/// reaching this helper, so decomposition can proceed without rediscovering
737/// stored NaN/∞ entries.
738///
739/// # Errors
740/// Returns [`LaError::Singular`] if the matrix is exactly singular.
741fn gauss_solve_finite<const D: usize>(
742    m: &Matrix<D>,
743    b: &Vector<D>,
744) -> Result<[BigRational; D], LaError> {
745    let (m_components, m_e_min) = decompose_finite_matrix(m)?;
746    let (b_components, b_e_min) = decompose_finite_vec(b)?;
747    gauss_solve_components(m_components, m_e_min, b_components, b_e_min)
748}
749
750/// Solve an exact integer-scaled augmented system from decomposed components.
751///
752/// Forward elimination runs in [`BigInt`] using fraction-free Bareiss updates
753/// \[7\]. This is exact arithmetic, so there is no floating-point conditioning or
754/// roundoff error in the elimination itself; ill-conditioned inputs can still
755/// produce large exact numerators and denominators in the final solution. The
756/// elimination phase performs `O(D³)` integer operations and Bareiss exact
757/// division controls intermediate integer growth compared with naive fraction
758/// arithmetic. The resulting upper-triangular system is then lifted into
759/// [`BigRational`] for back-substitution, limiting rational arithmetic to the
760/// `O(D²)` phase.
761///
762/// # Errors
763/// Returns [`LaError::Singular`] if the matrix component table represents an
764/// exactly singular matrix.
765fn gauss_solve_components<const D: usize>(
766    m_components: [[Component; D]; D],
767    m_e_min: i32,
768    b_components: [Component; D],
769    b_e_min: i32,
770) -> Result<[BigRational; D], LaError> {
771    let mut e_min = m_e_min.min(b_e_min);
772    if e_min == i32::MAX {
773        e_min = 0;
774    }
775
776    let mut a = build_bigint_matrix(&m_components, e_min);
777    let mut rhs = build_bigint_vec(&b_components, e_min);
778
779    match bareiss_forward_eliminate(&mut a, Some(&mut rhs)) {
780        BareissResult::Upper { .. } => {}
781        BareissResult::Singular { pivot_col } => {
782            cold_path();
783            return Err(LaError::Singular { pivot_col });
784        }
785    }
786
787    let mut x: [BigRational; D] = from_fn(|_| BigRational::from_integer(BigInt::from(0)));
788    for i in (0..D).rev() {
789        let mut sum = BigRational::from_integer(take(&mut rhs[i]));
790        for j in (i + 1)..D {
791            let a_ij = BigRational::from_integer(take(&mut a[i][j]));
792            sum -= &a_ij * &x[j];
793        }
794        let a_ii = BigRational::from_integer(take(&mut a[i][i]));
795        x[i] = sum / &a_ii;
796    }
797
798    Ok(x)
799}
800
801/// Exact determinant for a finite-by-construction matrix.
802///
803/// This is the private implementation target for [`Matrix::det_exact`]. Keeping
804/// the helper separate from the public method keeps the exact core focused on
805/// the Bareiss computation while relying on the public [`Matrix`] finite-storage
806/// invariant.
807///
808/// # Errors
809/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
810/// overflows the internal exponent representation.
811#[inline]
812fn det_exact_finite<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
813    bareiss_det_finite(m)
814}
815
816/// Exact determinant converted to finite `f64` without rounding.
817///
818/// This preserves the strict contract of [`Matrix::det_exact_f64`]: if the exact
819/// determinant is not representable as a finite binary64 value, callers receive
820/// a typed [`LaError::Unrepresentable`] instead of a rounded result.
821///
822/// # Errors
823/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
824/// overflows the internal exponent representation.
825///
826/// Returns [`LaError::Unrepresentable`] with
827/// [`UnrepresentableReason::RequiresRounding`] when the exact determinant is
828/// finite but not exactly representable as binary64, or
829/// [`UnrepresentableReason::NotFinite`] when no finite `f64` can represent it.
830#[inline]
831fn det_exact_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
832    let (det_int, total_exp) = bareiss_det_int_finite(m)?;
833    bigint_exp_to_finite_f64(det_int, total_exp, None)
834}
835
836/// Exact determinant rounded to finite `f64`.
837///
838/// This is the intentionally lossy counterpart to [`det_exact_f64_finite`] and
839/// the private implementation target for [`Matrix::det_exact_rounded_f64`].
840///
841/// # Errors
842/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
843/// overflows the internal exponent representation.
844///
845/// Returns [`LaError::Unrepresentable`] with
846/// [`UnrepresentableReason::NotFinite`] if the rounded result would be NaN or
847/// infinity.
848#[inline]
849fn det_exact_rounded_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
850    let (det_int, total_exp) = bareiss_det_int_finite(m)?;
851    bigint_exp_to_rounded_f64(det_int, total_exp)
852}
853
854/// Exact linear solve for finite inputs.
855///
856/// This is the private implementation target for [`Matrix::solve_exact`].
857/// Public [`Matrix`] and [`Vector`] values are finite by construction, so this
858/// helper can focus on the exact Bareiss/rational solve.
859///
860/// # Errors
861/// Returns [`LaError::Singular`] if the matrix is exactly singular.
862#[inline]
863fn solve_exact_finite<const D: usize>(
864    m: &Matrix<D>,
865    b: Vector<D>,
866) -> Result<[BigRational; D], LaError> {
867    gauss_solve_finite(m, &b)
868}
869
870/// Exact linear solve converted to finite `f64` components without rounding.
871///
872/// This preserves the strict contract of [`Matrix::solve_exact_f64`]: each exact
873/// component must already be representable as finite binary64.
874///
875/// # Errors
876/// Returns [`LaError::Singular`] if the matrix is exactly singular.
877///
878/// Returns [`LaError::Unrepresentable`] with the failing component index when an
879/// exact solution component requires rounding or cannot be represented as a
880/// finite `f64`.
881#[inline]
882fn solve_exact_f64_finite<const D: usize>(
883    m: &Matrix<D>,
884    b: Vector<D>,
885) -> Result<Vector<D>, LaError> {
886    let exact = solve_exact_finite(m, b)?;
887    let mut result = [0.0f64; D];
888    for (i, val) in exact.iter().enumerate() {
889        result[i] = exact_rational_to_finite_f64(val, Some(i))?;
890    }
891    Ok(Vector::new_unchecked(result))
892}
893
894/// Exact linear solve rounded to finite `f64` components.
895///
896/// This is the intentionally lossy counterpart to [`solve_exact_f64_finite`] and
897/// the private implementation target for [`Matrix::solve_exact_rounded_f64`].
898///
899/// # Errors
900/// Returns [`LaError::Singular`] if the matrix is exactly singular.
901///
902/// Returns [`LaError::Unrepresentable`] with
903/// [`UnrepresentableReason::NotFinite`] if any rounded component would be NaN or
904/// infinity.
905#[inline]
906fn solve_exact_rounded_f64_finite<const D: usize>(
907    m: &Matrix<D>,
908    b: Vector<D>,
909) -> Result<Vector<D>, LaError> {
910    let exact = solve_exact_finite(m, b)?;
911    let mut result = [0.0f64; D];
912    for (i, val) in exact.iter().enumerate() {
913        result[i] = exact_rational_to_rounded_f64(val, Some(i))?;
914    }
915    Ok(Vector::new_unchecked(result))
916}
917
918/// Exact determinant sign for an already finite matrix.
919///
920/// The fast `f64` filter may reject overflowed scalar intermediates as
921/// inconclusive, then fall back to integer Bareiss sign computation.
922///
923/// # Errors
924/// Returns [`LaError::NonFinite`] if a direct determinant or error-bound
925/// computation detects a non-finite condition that is not an inconclusive scalar
926/// overflow.
927///
928/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
929/// overflows the internal exponent representation.
930#[inline]
931fn det_sign_exact_finite<const D: usize>(m: &Matrix<D>) -> Result<i8, LaError> {
932    match (m.det_direct(), m.det_errbound()) {
933        (Ok(Some(det_f64)), Ok(Some(err))) => {
934            if det_f64 > err {
935                return Ok(1);
936            }
937            if det_f64 < -err {
938                return Ok(-1);
939            }
940        }
941        (Err(LaError::NonFinite { row: None, .. }), _)
942        | (_, Err(LaError::NonFinite { row: None, .. })) => {}
943        (Err(err), _) | (_, Err(err)) => return Err(err),
944        _ => {}
945    }
946
947    cold_path();
948    let (det_int, _) = bareiss_det_int_finite(m)?;
949    Ok(match det_int.sign() {
950        Sign::Plus => 1,
951        Sign::Minus => -1,
952        Sign::NoSign => 0,
953    })
954}
955
956impl<const D: usize> Matrix<D> {
957    /// Exact determinant using arbitrary-precision rational arithmetic.
958    ///
959    /// Requires the `exact` Cargo feature.
960    ///
961    /// Returns the determinant as an exact [`BigRational`] value. Every finite
962    /// `f64` is exactly representable as a rational, so the conversion is
963    /// lossless and the result is provably correct.
964    ///
965    /// # When to use
966    ///
967    /// Use this when you need the exact determinant *value* — for example,
968    /// exact volume computation or distinguishing truly-degenerate simplices
969    /// from near-degenerate ones.  If you only need the *sign*, prefer
970    /// [`det_sign_exact`](Self::det_sign_exact) which has a fast f64 filter.
971    ///
972    /// # Examples
973    /// ```
974    /// use la_stack::prelude::*;
975    ///
976    /// # fn main() -> Result<(), LaError> {
977    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
978    /// let det = m.det_exact()?;
979    /// // det = 1*4 - 2*3 = -2  (exact)
980    /// assert_eq!(det, BigRational::from_integer((-2).into()));
981    /// # Ok(())
982    /// # }
983    /// ```
984    ///
985    /// # Errors
986    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
987    /// overflows the internal exponent representation.
988    #[inline]
989    pub fn det_exact(&self) -> Result<BigRational, LaError> {
990        det_exact_finite(self)
991    }
992
993    /// Exact determinant converted to `f64`.
994    ///
995    /// Requires the `exact` Cargo feature.
996    ///
997    /// Computes the exact determinant with the same integer Bareiss core used by
998    /// [`det_exact`](Self::det_exact), then converts the exact scaled integer
999    /// result to `f64` only if the result is exactly representable as a finite
1000    /// binary64 value.
1001    ///
1002    /// # Examples
1003    /// ```
1004    /// use la_stack::prelude::*;
1005    ///
1006    /// # fn main() -> Result<(), LaError> {
1007    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1008    /// let det = m.det_exact_f64()?;
1009    /// assert!((det - (-2.0)).abs() <= f64::EPSILON);
1010    /// # Ok(())
1011    /// # }
1012    /// ```
1013    ///
1014    /// # Errors
1015    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1016    /// overflows the internal exponent representation.
1017    ///
1018    /// Returns [`LaError::Unrepresentable`] if the exact determinant cannot be
1019    /// represented exactly as a finite `f64`.
1020    #[inline]
1021    pub fn det_exact_f64(&self) -> Result<f64, LaError> {
1022        det_exact_f64_finite(self)
1023    }
1024
1025    /// Exact determinant rounded to `f64`.
1026    ///
1027    /// Requires the `exact` Cargo feature.
1028    ///
1029    /// Computes the exact determinant with the same integer Bareiss core used by
1030    /// [`det_exact`](Self::det_exact), then rounds the exact value to a finite
1031    /// binary64 value. Unlike [`det_exact_f64`](Self::det_exact_f64), this method
1032    /// is intentionally lossy and may round non-dyadic or underflowing nonzero
1033    /// exact determinants.
1034    ///
1035    /// # Examples
1036    /// ```
1037    /// use la_stack::prelude::*;
1038    ///
1039    /// # fn main() -> Result<(), LaError> {
1040    /// let m = Matrix::<2>::try_from_rows([
1041    ///     [1.0 + f64::EPSILON, 0.0],
1042    ///     [0.0, 1.0 - f64::EPSILON],
1043    /// ])?;
1044    ///
1045    /// assert_eq!(
1046    ///     m.det_exact_f64(),
1047    ///     Err(LaError::Unrepresentable {
1048    ///         index: None,
1049    ///         reason: UnrepresentableReason::RequiresRounding,
1050    ///     })
1051    /// );
1052    /// assert_eq!(m.det_exact_rounded_f64()?.to_bits(), 1.0f64.to_bits());
1053    /// # Ok(())
1054    /// # }
1055    /// ```
1056    ///
1057    /// # Errors
1058    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1059    /// overflows the internal exponent representation.
1060    ///
1061    /// Returns [`LaError::Unrepresentable`] if the rounded determinant would be
1062    /// NaN or infinite.
1063    #[inline]
1064    pub fn det_exact_rounded_f64(&self) -> Result<f64, LaError> {
1065        det_exact_rounded_f64_finite(self)
1066    }
1067
1068    /// Exact linear system solve using hybrid integer/rational arithmetic.
1069    ///
1070    /// Requires the `exact` Cargo feature.
1071    ///
1072    /// Solves `A x = b` where `A` is `self` and `b` is the given vector.
1073    /// Returns the exact solution as `[BigRational; D]`.  Every finite `f64`
1074    /// is exactly representable as a rational, so the conversion is lossless
1075    /// and the result is provably correct.
1076    ///
1077    /// # When to use
1078    ///
1079    /// Use this when you need a provably correct solution — for example,
1080    /// exact circumcenter computation for near-degenerate simplices where
1081    /// f64 arithmetic may produce wildly wrong results.
1082    ///
1083    /// # Algorithm
1084    ///
1085    /// Matrix and RHS entries are decomposed via IEEE 754 bit extraction and
1086    /// scaled to a shared power-of-two base so the augmented system `(A | b)`
1087    /// becomes integer-valued.  Forward elimination runs entirely in `BigInt`
1088    /// with fraction-free Bareiss updates — no `BigRational`, no GCD, no
1089    /// denominator tracking in the `O(D³)` phase.  Only the upper-triangular
1090    /// result is lifted into `BigRational` for back-substitution (the `O(D²)`
1091    /// phase where fractions are inherent).  First-non-zero pivoting is used
1092    /// throughout; since all arithmetic is exact, any non-zero pivot yields
1093    /// the correct answer (no numerical-stability concerns).
1094    ///
1095    /// # Examples
1096    /// ```
1097    /// use la_stack::prelude::*;
1098    ///
1099    /// # fn main() -> Result<(), LaError> {
1100    /// // A x = b  where A = [[1,2],[3,4]], b = [5, 11]  →  x = [1, 2]
1101    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1102    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1103    /// let x = a.solve_exact(b)?;
1104    /// assert_eq!(x[0], BigRational::from_integer(1.into()));
1105    /// assert_eq!(x[1], BigRational::from_integer(2.into()));
1106    /// # Ok(())
1107    /// # }
1108    /// ```
1109    ///
1110    /// # Errors
1111    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1112    #[inline]
1113    pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
1114        solve_exact_finite(self, b)
1115    }
1116
1117    /// Exact linear system solve converted to `f64`.
1118    ///
1119    /// Requires the `exact` Cargo feature.
1120    ///
1121    /// Computes the exact [`BigRational`] solution via
1122    /// [`solve_exact`](Self::solve_exact) and converts each component to `f64`
1123    /// only if that component is exactly representable as a finite binary64
1124    /// value.
1125    ///
1126    /// # Examples
1127    /// ```
1128    /// use la_stack::prelude::*;
1129    ///
1130    /// # fn main() -> Result<(), LaError> {
1131    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1132    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1133    /// let x = a.solve_exact_f64(b)?.into_array();
1134    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
1135    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
1136    /// # Ok(())
1137    /// # }
1138    /// ```
1139    ///
1140    /// # Errors
1141    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1142    /// Returns [`LaError::Unrepresentable`] if any component of the exact solution
1143    /// cannot be represented exactly as a finite `f64`.
1144    #[inline]
1145    pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1146        solve_exact_f64_finite(self, b)
1147    }
1148
1149    /// Exact linear system solve rounded to `f64`.
1150    ///
1151    /// Requires the `exact` Cargo feature.
1152    ///
1153    /// Computes the exact [`BigRational`] solution via
1154    /// [`solve_exact`](Self::solve_exact) and rounds each component to a finite
1155    /// binary64 value. Unlike [`solve_exact_f64`](Self::solve_exact_f64), this
1156    /// method is intentionally lossy and may round non-dyadic or underflowing
1157    /// nonzero exact components.
1158    ///
1159    /// # Examples
1160    /// ```
1161    /// use la_stack::prelude::*;
1162    ///
1163    /// # fn main() -> Result<(), LaError> {
1164    /// let a = Matrix::<1>::try_from_rows([[3.0]])?;
1165    /// let b = Vector::<1>::try_new([1.0])?;
1166    ///
1167    /// assert_eq!(
1168    ///     a.solve_exact_f64(b),
1169    ///     Err(LaError::Unrepresentable {
1170    ///         index: Some(0),
1171    ///         reason: UnrepresentableReason::RequiresRounding,
1172    ///     })
1173    /// );
1174    /// assert_eq!(a.solve_exact_rounded_f64(b)?.into_array(), [1.0 / 3.0]);
1175    /// # Ok(())
1176    /// # }
1177    /// ```
1178    ///
1179    /// # Errors
1180    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1181    /// Returns [`LaError::Unrepresentable`] if any rounded component would be
1182    /// NaN or infinite.
1183    #[inline]
1184    pub fn solve_exact_rounded_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1185        solve_exact_rounded_f64_finite(self, b)
1186    }
1187
1188    /// Exact determinant sign using adaptive-precision arithmetic.
1189    ///
1190    /// Requires the `exact` Cargo feature.
1191    ///
1192    /// Returns `1` if `det > 0`, `-1` if `det < 0`, and `0` if `det == 0` (singular).
1193    ///
1194    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
1195    /// against a conservative error bound derived from the matrix permanent.
1196    /// If the f64 result clearly exceeds the bound, the sign is returned
1197    /// immediately without allocating.  Otherwise (and always for D ≥ 5),
1198    /// integer-only Bareiss elimination (`bareiss_det_int`) computes the exact
1199    /// sign without constructing any `BigRational` values.
1200    ///
1201    /// # When to use
1202    ///
1203    /// Use this when the sign of the determinant must be correct regardless of
1204    /// floating-point conditioning (e.g. geometric predicates on near-degenerate
1205    /// configurations).  For well-conditioned matrices the fast filter resolves
1206    /// the sign without touching `BigRational`, so the overhead is minimal.
1207    ///
1208    /// # Examples
1209    /// ```
1210    /// use la_stack::prelude::*;
1211    ///
1212    /// let m = Matrix::<3>::try_from_rows([
1213    ///     [1.0, 2.0, 3.0],
1214    ///     [4.0, 5.0, 6.0],
1215    ///     [7.0, 8.0, 9.0],
1216    /// ])?;
1217    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
1218    /// assert_eq!(m.det_sign_exact()?, 0);
1219    ///
1220    /// assert_eq!(Matrix::<3>::identity().det_sign_exact()?, 1);
1221    /// # Ok::<(), LaError>(())
1222    /// ```
1223    ///
1224    /// # Errors
1225    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1226    /// overflows the internal exponent representation.
1227    #[inline]
1228    pub fn det_sign_exact(&self) -> Result<i8, LaError> {
1229        det_sign_exact_finite(self)
1230    }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236    use crate::DEFAULT_SINGULAR_TOL;
1237
1238    use core::assert_matches;
1239    use num_traits::Signed;
1240    use pastey::paste;
1241    use std::array::from_fn;
1242
1243    // -----------------------------------------------------------------------
1244    // Test helpers
1245    // -----------------------------------------------------------------------
1246
1247    /// Build an exact `BigRational` from an `f64` via IEEE 754 bit decomposition.
1248    ///
1249    /// Thin wrapper over [`f64_decompose`] that packs the mantissa/exponent
1250    /// pair into a fully-formed `BigRational` of the form `±m · 2^e`.  The
1251    /// production code paths (`bareiss_det_int`, `gauss_solve`) instead
1252    /// decompose every entry into a shared-scale `BigInt` matrix, which
1253    /// avoids per-entry GCD work in the elimination loops — so this helper
1254    /// is not used by them and lives here to keep test assertions concise
1255    /// (e.g. `assert_eq!(x[0], f64_to_bigrational(3.0))`).
1256    ///
1257    /// See `REFERENCES.md` \[9-10\] for the IEEE 754 standard and Goldberg's
1258    /// survey of floating-point representation.
1259    ///
1260    /// # Panics
1261    /// Panics if `x` is NaN or infinite.
1262    fn f64_to_bigrational(x: f64) -> BigRational {
1263        let Some((mantissa, exponent, is_negative)) =
1264            f64_decompose(x).expect("test helper requires finite f64 input")
1265        else {
1266            return BigRational::from_integer(BigInt::from(0));
1267        };
1268
1269        let numer = if is_negative {
1270            -BigInt::from(mantissa.get())
1271        } else {
1272            BigInt::from(mantissa.get())
1273        };
1274
1275        if exponent >= 0 {
1276            BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
1277        } else {
1278            BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
1279        }
1280    }
1281
1282    // -----------------------------------------------------------------------
1283    // Macro-generated per-dimension tests (D=2..5)
1284    // -----------------------------------------------------------------------
1285
1286    macro_rules! gen_internal_matrix_exact_tests {
1287        ($d:literal) => {
1288            paste! {
1289                #[test]
1290                fn [<internal_matrix_exact_paths_reuse_validation_ $d d>]() {
1291                    let a = Matrix::<$d>::identity();
1292                    let b = Vector::<$d>::new([1.0; $d]);
1293
1294                    assert_eq!(
1295                        det_exact_finite(&a).unwrap(),
1296                        BigRational::from_integer(BigInt::from(1))
1297                    );
1298                    assert!((det_exact_f64_finite(&a).unwrap() - 1.0).abs() <= f64::EPSILON);
1299                    assert_eq!(det_sign_exact_finite(&a).unwrap(), 1);
1300
1301                    let exact = solve_exact_finite(&a, b).unwrap();
1302                    for value in exact {
1303                        assert_eq!(value, BigRational::from_integer(BigInt::from(1)));
1304                    }
1305
1306                    let exact_f64 = a.solve_exact_f64(b).unwrap();
1307                    for value in exact_f64.into_array() {
1308                        assert!((value - 1.0).abs() <= f64::EPSILON);
1309                    }
1310                }
1311            }
1312        };
1313    }
1314
1315    gen_internal_matrix_exact_tests!(2);
1316    gen_internal_matrix_exact_tests!(3);
1317    gen_internal_matrix_exact_tests!(4);
1318    gen_internal_matrix_exact_tests!(5);
1319
1320    macro_rules! gen_det_exact_tests {
1321        ($d:literal) => {
1322            paste! {
1323                #[test]
1324                fn [<det_exact_identity_ $d d>]() {
1325                    let det = Matrix::<$d>::identity().det_exact().unwrap();
1326                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1327                }
1328            }
1329        };
1330    }
1331
1332    gen_det_exact_tests!(2);
1333    gen_det_exact_tests!(3);
1334    gen_det_exact_tests!(4);
1335    gen_det_exact_tests!(5);
1336
1337    macro_rules! gen_det_exact_f64_tests {
1338        ($d:literal) => {
1339            paste! {
1340                #[test]
1341                fn [<det_exact_f64_identity_ $d d>]() {
1342                    let det = Matrix::<$d>::identity().det_exact_f64().unwrap();
1343                    assert!((det - 1.0).abs() <= f64::EPSILON);
1344                }
1345            }
1346        };
1347    }
1348
1349    gen_det_exact_f64_tests!(2);
1350    gen_det_exact_f64_tests!(3);
1351    gen_det_exact_f64_tests!(4);
1352    gen_det_exact_f64_tests!(5);
1353
1354    /// For D ≤ 4, `det_exact_f64` should agree with `det_direct` on matrices
1355    /// whose exact determinant is representable in f64.
1356    macro_rules! gen_det_exact_f64_agrees_with_det_direct {
1357        ($d:literal) => {
1358            paste! {
1359                #[test]
1360                fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
1361                    // Power-of-two diagonal entries make the determinant
1362                    // exactly representable in binary64.
1363                    let mut rows = [[0.0f64; $d]; $d];
1364                    let mut value = 2.0;
1365                    for (i, row) in rows.iter_mut().enumerate() {
1366                        row[i] = value;
1367                        value *= 2.0;
1368                    }
1369                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1370                    let exact = m.det_exact_f64().unwrap();
1371                    let direct = m.det_direct().unwrap().unwrap();
1372                    assert_eq!(exact.to_bits(), direct.to_bits());
1373                }
1374            }
1375        };
1376    }
1377
1378    gen_det_exact_f64_agrees_with_det_direct!(2);
1379    gen_det_exact_f64_agrees_with_det_direct!(3);
1380    gen_det_exact_f64_agrees_with_det_direct!(4);
1381
1382    #[test]
1383    fn det_sign_exact_d0_is_positive() {
1384        assert_eq!(Matrix::<0>::zero().det_sign_exact().unwrap(), 1);
1385    }
1386
1387    #[test]
1388    fn det_sign_exact_d1_positive() {
1389        let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
1390        assert_eq!(m.det_sign_exact().unwrap(), 1);
1391    }
1392
1393    #[test]
1394    fn det_sign_exact_d1_negative() {
1395        let m = Matrix::<1>::try_from_rows([[-3.5]]).unwrap();
1396        assert_eq!(m.det_sign_exact().unwrap(), -1);
1397    }
1398
1399    #[test]
1400    fn det_sign_exact_d1_zero() {
1401        let m = Matrix::<1>::try_from_rows([[0.0]]).unwrap();
1402        assert_eq!(m.det_sign_exact().unwrap(), 0);
1403    }
1404
1405    #[test]
1406    fn det_sign_exact_identity_2d() {
1407        assert_eq!(Matrix::<2>::identity().det_sign_exact().unwrap(), 1);
1408    }
1409
1410    #[test]
1411    fn det_sign_exact_identity_3d() {
1412        assert_eq!(Matrix::<3>::identity().det_sign_exact().unwrap(), 1);
1413    }
1414
1415    #[test]
1416    fn det_sign_exact_identity_4d() {
1417        assert_eq!(Matrix::<4>::identity().det_sign_exact().unwrap(), 1);
1418    }
1419
1420    #[test]
1421    fn det_sign_exact_identity_5d() {
1422        assert_eq!(Matrix::<5>::identity().det_sign_exact().unwrap(), 1);
1423    }
1424
1425    #[test]
1426    fn det_sign_exact_singular_duplicate_rows() {
1427        let m = Matrix::<3>::try_from_rows([
1428            [1.0, 2.0, 3.0],
1429            [4.0, 5.0, 6.0],
1430            [1.0, 2.0, 3.0], // duplicate of row 0
1431        ])
1432        .unwrap();
1433        assert_eq!(m.det_sign_exact().unwrap(), 0);
1434    }
1435
1436    #[test]
1437    fn det_sign_exact_singular_linear_combination() {
1438        // Row 2 = row 0 + row 1 in exact arithmetic.
1439        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]])
1440            .unwrap();
1441        assert_eq!(m.det_sign_exact().unwrap(), 0);
1442    }
1443
1444    #[test]
1445    fn det_sign_exact_negative_det_row_swap() {
1446        // Swapping two rows of the identity negates the determinant.
1447        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1448            .unwrap();
1449        assert_eq!(m.det_sign_exact().unwrap(), -1);
1450    }
1451
1452    #[test]
1453    fn det_sign_exact_negative_det_known() {
1454        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
1455        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
1456        assert_eq!(m.det_sign_exact().unwrap(), -1);
1457    }
1458
1459    #[test]
1460    fn det_sign_exact_agrees_with_det_for_spd() {
1461        // SPD matrix → positive determinant.
1462        let m = Matrix::<3>::try_from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]])
1463            .unwrap();
1464        assert_eq!(m.det_sign_exact().unwrap(), 1);
1465        assert!(m.det().unwrap() > 0.0);
1466    }
1467
1468    /// Near-singular matrix with an exact perturbation.
1469    ///
1470    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
1471    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
1472    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
1473    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
1474    #[test]
1475    fn det_sign_exact_near_singular_perturbation() {
1476        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
1477        let m = Matrix::<3>::try_from_rows([
1478            [1.0 + perturbation, 2.0, 3.0],
1479            [4.0, 5.0, 6.0],
1480            [7.0, 8.0, 9.0],
1481        ])
1482        .unwrap();
1483        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
1484        assert_eq!(m.det_sign_exact().unwrap(), -1);
1485    }
1486
1487    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
1488    /// and never allocate `BigRational`.  We can't directly observe this,
1489    /// but we verify correctness for a range of known signs.
1490    #[test]
1491    fn det_sign_exact_fast_filter_positive_4x4() {
1492        let m = Matrix::<4>::try_from_rows([
1493            [2.0, 1.0, 0.0, 0.0],
1494            [1.0, 3.0, 1.0, 0.0],
1495            [0.0, 1.0, 4.0, 1.0],
1496            [0.0, 0.0, 1.0, 5.0],
1497        ])
1498        .unwrap();
1499        // SPD tridiagonal → positive det.
1500        assert_eq!(m.det_sign_exact().unwrap(), 1);
1501    }
1502
1503    #[test]
1504    fn det_sign_exact_fast_filter_negative_4x4() {
1505        // Swap rows 0 and 1 of the above → negate det.
1506        let m = Matrix::<4>::try_from_rows([
1507            [1.0, 3.0, 1.0, 0.0],
1508            [2.0, 1.0, 0.0, 0.0],
1509            [0.0, 1.0, 4.0, 1.0],
1510            [0.0, 0.0, 1.0, 5.0],
1511        ])
1512        .unwrap();
1513        assert_eq!(m.det_sign_exact().unwrap(), -1);
1514    }
1515
1516    #[test]
1517    fn det_sign_exact_subnormal_entries() {
1518        // Subnormal f64 values should convert losslessly.
1519        let tiny = 5e-324_f64; // smallest positive subnormal
1520        assert!(tiny.is_subnormal());
1521
1522        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
1523        // det = tiny^2 > 0
1524        assert_eq!(m.det_sign_exact().unwrap(), 1);
1525    }
1526
1527    #[test]
1528    fn det_sign_exact_returns_err_on_nan() {
1529        assert_eq!(
1530            Matrix::<2>::try_from_rows([[f64::NAN, 0.0], [0.0, 1.0]]),
1531            Err(LaError::NonFinite {
1532                row: Some(0),
1533                col: 0
1534            })
1535        );
1536    }
1537
1538    #[test]
1539    fn det_sign_exact_returns_err_on_infinity() {
1540        assert_eq!(
1541            Matrix::<2>::try_from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]),
1542            Err(LaError::NonFinite {
1543                row: Some(0),
1544                col: 0
1545            })
1546        );
1547    }
1548
1549    #[test]
1550    fn exact_public_methods_reject_unchecked_nonfinite_matrix_before_computation() {
1551        let m = Matrix::<2>::from_rows_unchecked([[1.0, 0.0], [f64::NAN, 1.0]]);
1552        let b = Vector::<2>::new([1.0, 1.0]);
1553        let expected = Err(LaError::NonFinite {
1554            row: Some(1),
1555            col: 0,
1556        });
1557
1558        assert_eq!(m.det_exact().map(|_| ()), expected);
1559        assert_eq!(m.det_exact_f64().map(|_| ()), expected);
1560        assert_eq!(m.det_exact_rounded_f64().map(|_| ()), expected);
1561        assert_eq!(m.det_sign_exact().map(|_| ()), expected);
1562        assert_eq!(m.solve_exact(b).map(|_| ()), expected);
1563        assert_eq!(m.solve_exact_f64(b).map(|_| ()), expected);
1564        assert_eq!(m.solve_exact_rounded_f64(b).map(|_| ()), expected);
1565    }
1566
1567    #[test]
1568    fn exact_solve_public_methods_reject_unchecked_nonfinite_rhs_before_computation() {
1569        let a = Matrix::<2>::identity();
1570        let b = Vector::<2>::new_unchecked([1.0, f64::INFINITY]);
1571        let expected = Err(LaError::NonFinite { row: None, col: 1 });
1572
1573        assert_eq!(a.solve_exact(b).map(|_| ()), expected);
1574        assert_eq!(a.solve_exact_f64(b).map(|_| ()), expected);
1575        assert_eq!(a.solve_exact_rounded_f64(b).map(|_| ()), expected);
1576    }
1577
1578    #[test]
1579    fn det_sign_exact_returns_err_on_nan_5x5() {
1580        // D ≥ 5 bypasses the fast filter, exercising the bareiss_det path.
1581        let mut m = Matrix::<5>::identity();
1582        assert_eq!(
1583            m.set(2, 3, f64::NAN),
1584            Err(LaError::NonFinite {
1585                row: Some(2),
1586                col: 3
1587            })
1588        );
1589    }
1590
1591    #[test]
1592    fn det_sign_exact_returns_err_on_infinity_5x5() {
1593        let mut m = Matrix::<5>::identity();
1594        assert_eq!(
1595            m.set(0, 0, f64::INFINITY),
1596            Err(LaError::NonFinite {
1597                row: Some(0),
1598                col: 0
1599            })
1600        );
1601    }
1602
1603    #[test]
1604    fn det_sign_exact_pivot_needed_5x5() {
1605        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
1606        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
1607        let m = Matrix::<5>::try_from_rows([
1608            [0.0, 1.0, 0.0, 0.0, 0.0],
1609            [1.0, 0.0, 0.0, 0.0, 0.0],
1610            [0.0, 0.0, 1.0, 0.0, 0.0],
1611            [0.0, 0.0, 0.0, 1.0, 0.0],
1612            [0.0, 0.0, 0.0, 0.0, 1.0],
1613        ])
1614        .unwrap();
1615        assert_eq!(m.det_sign_exact().unwrap(), -1);
1616    }
1617
1618    #[test]
1619    fn det_sign_exact_5x5_known() {
1620        // det of a permutation matrix with two swaps = +1 (even permutation).
1621        let m = Matrix::<5>::try_from_rows([
1622            [0.0, 1.0, 0.0, 0.0, 0.0],
1623            [1.0, 0.0, 0.0, 0.0, 0.0],
1624            [0.0, 0.0, 0.0, 1.0, 0.0],
1625            [0.0, 0.0, 1.0, 0.0, 0.0],
1626            [0.0, 0.0, 0.0, 0.0, 1.0],
1627        ])
1628        .unwrap();
1629        // Two transpositions → even permutation → det = +1
1630        assert_eq!(m.det_sign_exact().unwrap(), 1);
1631    }
1632
1633    // -----------------------------------------------------------------------
1634    // Direct tests for internal helpers (coverage of private functions)
1635    // -----------------------------------------------------------------------
1636
1637    #[test]
1638    fn det_errbound_d0_is_zero() {
1639        assert_eq!(Matrix::<0>::zero().det_errbound(), Ok(Some(0.0)));
1640    }
1641
1642    #[test]
1643    fn det_errbound_d1_is_zero() {
1644        assert_eq!(
1645            Matrix::<1>::try_from_rows([[42.0]]).unwrap().det_errbound(),
1646            Ok(Some(0.0))
1647        );
1648    }
1649
1650    #[test]
1651    fn det_errbound_d2_positive() {
1652        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
1653        let bound = m.det_errbound().unwrap().unwrap();
1654        assert!(bound > 0.0);
1655        // bound = ERR_COEFF_2 * (|1*4| + |2*3|) = ERR_COEFF_2 * 10
1656        assert!(crate::ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
1657    }
1658
1659    #[test]
1660    fn det_errbound_d3_positive() {
1661        let m = Matrix::<3>::identity();
1662        let bound = m.det_errbound().unwrap().unwrap();
1663        assert!(bound > 0.0);
1664    }
1665
1666    #[test]
1667    fn det_errbound_d3_non_identity() {
1668        // Non-identity matrix to exercise all code paths in D=3 case
1669        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])
1670            .unwrap();
1671        let bound = m.det_errbound().unwrap().unwrap();
1672        assert!(bound > 0.0);
1673    }
1674
1675    #[test]
1676    fn det_errbound_d4_positive() {
1677        let m = Matrix::<4>::identity();
1678        let bound = m.det_errbound().unwrap().unwrap();
1679        assert!(bound > 0.0);
1680    }
1681
1682    #[test]
1683    fn det_errbound_d4_non_identity() {
1684        // Non-identity matrix to exercise all code paths in D=4 case
1685        let m = Matrix::<4>::try_from_rows([
1686            [1.0, 0.0, 0.0, 0.0],
1687            [0.0, 2.0, 0.0, 0.0],
1688            [0.0, 0.0, 3.0, 0.0],
1689            [0.0, 0.0, 0.0, 4.0],
1690        ])
1691        .unwrap();
1692        let bound = m.det_errbound().unwrap().unwrap();
1693        assert!(bound > 0.0);
1694    }
1695
1696    #[test]
1697    fn det_errbound_d5_is_none() {
1698        assert_eq!(Matrix::<5>::identity().det_errbound(), Ok(None));
1699    }
1700
1701    // -----------------------------------------------------------------------
1702    // f64_decompose tests
1703    // -----------------------------------------------------------------------
1704
1705    #[test]
1706    fn f64_decompose_zero() {
1707        assert!(f64_decompose(0.0).unwrap().is_none());
1708        assert!(f64_decompose(-0.0).unwrap().is_none());
1709    }
1710
1711    #[test]
1712    fn f64_decompose_one() {
1713        let (mant, exp, neg) = f64_decompose(1.0).unwrap().unwrap();
1714        assert_eq!(mant.get(), 1);
1715        assert_eq!(exp, 0);
1716        assert!(!neg);
1717    }
1718
1719    #[test]
1720    fn f64_decompose_negative() {
1721        let (mant, exp, neg) = f64_decompose(-3.5).unwrap().unwrap();
1722        // -3.5 = -7 × 2^(-1), mantissa is 7 (odd after stripping)
1723        assert_eq!(mant.get(), 7);
1724        assert_eq!(exp, -1);
1725        assert!(neg);
1726    }
1727
1728    #[test]
1729    fn f64_decompose_subnormal() {
1730        let tiny = 5e-324_f64;
1731        assert!(tiny.is_subnormal());
1732        let (mant, exp, neg) = f64_decompose(tiny).unwrap().unwrap();
1733        assert_eq!(mant.get(), 1);
1734        assert_eq!(exp, -1074);
1735        assert!(!neg);
1736    }
1737
1738    #[test]
1739    fn f64_decompose_power_of_two() {
1740        let (mant, exp, neg) = f64_decompose(1024.0).unwrap().unwrap();
1741        assert_eq!(mant.get(), 1);
1742        assert_eq!(exp, 10); // 1024 = 2^10
1743        assert!(!neg);
1744    }
1745
1746    #[test]
1747    fn f64_decompose_rejects_nan() {
1748        assert_eq!(
1749            f64_decompose(f64::NAN),
1750            Err(LaError::NonFinite { row: None, col: 0 })
1751        );
1752    }
1753
1754    #[test]
1755    fn component_to_bigint_distinguishes_zero_from_nonzero_mantissa() {
1756        assert_eq!(
1757            component_to_bigint(Component::default(), -10),
1758            BigInt::from(0)
1759        );
1760
1761        let positive = Component::NonZero {
1762            mantissa: NonZeroU64::new(3).unwrap(),
1763            exponent: 4,
1764            is_negative: false,
1765        };
1766        assert_eq!(component_to_bigint(positive, 1), BigInt::from(24));
1767
1768        let negative = Component::NonZero {
1769            mantissa: NonZeroU64::new(5).unwrap(),
1770            exponent: 3,
1771            is_negative: true,
1772        };
1773        assert_eq!(component_to_bigint(negative, 1), BigInt::from(-20));
1774    }
1775
1776    #[test]
1777    fn determinant_scale_exp_multiplies_dimension_and_min_exponent() {
1778        assert_eq!(determinant_scale_exp::<4>(-1074), Ok(-4296));
1779    }
1780
1781    #[test]
1782    fn determinant_scale_exp_rejects_dimension_too_large_for_i32() {
1783        assert_eq!(
1784            determinant_scale_exp::<{ i32::MAX as usize + 1 }>(-1074),
1785            Err(LaError::DeterminantScaleOverflow {
1786                dim: i32::MAX as usize + 1,
1787                min_exponent: -1074,
1788            })
1789        );
1790    }
1791
1792    #[test]
1793    fn determinant_scale_exp_rejects_exponent_product_overflow() {
1794        assert_eq!(
1795            determinant_scale_exp::<3_000_000>(-1074),
1796            Err(LaError::DeterminantScaleOverflow {
1797                dim: 3_000_000,
1798                min_exponent: -1074,
1799            })
1800        );
1801    }
1802
1803    // -----------------------------------------------------------------------
1804    // bareiss_det_int tests
1805    // -----------------------------------------------------------------------
1806
1807    #[test]
1808    fn bareiss_det_int_d0() {
1809        let m = Matrix::<0>::zero();
1810        let (det, exp) = bareiss_det_int_finite(&m).unwrap();
1811        assert_eq!(det, BigInt::from(1));
1812        assert_eq!(exp, 0);
1813    }
1814
1815    /// Table-driven coverage of the D=1 fast-path: each 1×1 matrix
1816    /// decomposes to `(±mant, exp)` directly.  Includes an integer, zero,
1817    /// a negative fractional, and a positive fractional case — the
1818    /// combinations that exercise the sign handling, the all-zero early
1819    /// return, trailing-zero stripping, and negative exponent scaling.
1820    #[test]
1821    fn bareiss_det_int_d1_cases() {
1822        let cases: &[(f64, i64, i32)] = &[
1823            // (input, expected_det_int, expected_exp)
1824            (7.0, 7, 0),    // integer → (7, 0)
1825            (0.0, 0, 0),    // all-zero early return → (0, 0)
1826            (-3.5, -7, -1), // -3.5 = -7 × 2^(-1)
1827            (0.5, 1, -1),   // 0.5  =  1 × 2^(-1)
1828        ];
1829        for &(input, expected_det_int, expected_exp) in cases {
1830            let m = Matrix::<1>::try_from_rows([[input]]).unwrap();
1831            let (det, exp) = bareiss_det_int_finite(&m).unwrap();
1832            assert_eq!(
1833                det,
1834                BigInt::from(expected_det_int),
1835                "det_int for input={input}"
1836            );
1837            assert_eq!(exp, expected_exp, "exp for input={input}");
1838        }
1839    }
1840
1841    #[test]
1842    fn bareiss_det_int_d2_known() {
1843        // det([[1,2],[3,4]]) = -2
1844        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
1845        let (det_int, total_exp) = bareiss_det_int_finite(&m).unwrap();
1846        // Reconstruct and verify.
1847        let det = bigint_exp_to_bigrational(det_int, total_exp);
1848        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
1849    }
1850
1851    #[test]
1852    fn bareiss_det_int_all_zeros() {
1853        let m = Matrix::<3>::zero();
1854        let (det, _) = bareiss_det_int_finite(&m).unwrap();
1855        assert_eq!(det, BigInt::from(0));
1856    }
1857
1858    #[test]
1859    fn bareiss_det_int_sign_matches_det_sign_exact() {
1860        // The sign of det_int should match det_sign_exact for various matrices.
1861        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1862            .unwrap();
1863        let (det_int, _) = bareiss_det_int_finite(&m).unwrap();
1864        assert_eq!(det_int.sign(), Sign::Minus); // det = -1
1865    }
1866
1867    #[test]
1868    fn bareiss_det_int_fractional_entries() {
1869        // Entries with negative exponents: 0.5 = 1×2^(-1), 0.25 = 1×2^(-2).
1870        // det([[0.5, 0.25], [1.0, 1.0]]) = 0.5×1.0 − 0.25×1.0 = 0.25
1871        let m = Matrix::<2>::try_from_rows([[0.5, 0.25], [1.0, 1.0]]).unwrap();
1872        let (det_int, total_exp) = bareiss_det_int_finite(&m).unwrap();
1873        let det = bigint_exp_to_bigrational(det_int, total_exp);
1874        assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
1875    }
1876
1877    #[test]
1878    fn bareiss_det_int_d3_with_pivoting() {
1879        // Zero on diagonal → exercises pivot swap inside bareiss_det_int.
1880        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1881            .unwrap();
1882        let (det_int, total_exp) = bareiss_det_int_finite(&m).unwrap();
1883        let det = bigint_exp_to_bigrational(det_int, total_exp);
1884        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1885    }
1886
1887    /// Per AGENTS.md: dimension-generic tests must cover D=2–5.
1888    macro_rules! gen_bareiss_det_int_identity_tests {
1889        ($d:literal) => {
1890            paste! {
1891                #[test]
1892                fn [<bareiss_det_int_identity_ $d d>]() {
1893                    let m = Matrix::<$d>::identity();
1894                    let (det_int, total_exp) = bareiss_det_int_finite(&m).unwrap();
1895                    let det = bigint_exp_to_bigrational(det_int, total_exp);
1896                    assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
1897                }
1898            }
1899        };
1900    }
1901
1902    gen_bareiss_det_int_identity_tests!(2);
1903    gen_bareiss_det_int_identity_tests!(3);
1904    gen_bareiss_det_int_identity_tests!(4);
1905    gen_bareiss_det_int_identity_tests!(5);
1906
1907    // -----------------------------------------------------------------------
1908    // bigint_exp_to_bigrational tests
1909    // -----------------------------------------------------------------------
1910
1911    #[test]
1912    fn bigint_exp_to_bigrational_zero() {
1913        let r = bigint_exp_to_bigrational(BigInt::from(0), -50);
1914        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
1915    }
1916
1917    #[test]
1918    fn bigint_exp_to_bigrational_positive_exp() {
1919        // 3 × 2^2 = 12
1920        let r = bigint_exp_to_bigrational(BigInt::from(3), 2);
1921        assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
1922    }
1923
1924    #[test]
1925    fn bigint_exp_to_bigrational_negative_exp_reduced() {
1926        // 6 × 2^(-2) = 6/4 → reduced to 3/2 (strip one shared factor of 2)
1927        let r = bigint_exp_to_bigrational(BigInt::from(6), -2);
1928        assert_eq!(*r.numer(), BigInt::from(3));
1929        assert_eq!(*r.denom(), BigInt::from(2));
1930    }
1931
1932    #[test]
1933    fn bigint_exp_to_bigrational_negative_exp_reduces_to_integer() {
1934        // 8 × 2^(-3) = 1 after stripping every denominator factor.
1935        let r = bigint_exp_to_bigrational(BigInt::from(8), -3);
1936        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
1937    }
1938
1939    #[test]
1940    fn bigint_exp_to_bigrational_negative_exp_already_odd() {
1941        // 3 × 2^(-2) = 3/4 (already in lowest terms since 3 is odd)
1942        let r = bigint_exp_to_bigrational(BigInt::from(3), -2);
1943        assert_eq!(*r.numer(), BigInt::from(3));
1944        assert_eq!(*r.denom(), BigInt::from(4));
1945    }
1946
1947    #[test]
1948    fn bigint_exp_to_bigrational_negative_value() {
1949        // -5 × 2^1 = -10
1950        let r = bigint_exp_to_bigrational(BigInt::from(-5), 1);
1951        assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
1952    }
1953
1954    #[test]
1955    fn bigint_exp_to_bigrational_negative_value_with_denominator() {
1956        // -3 × 2^(-2) = -3/4
1957        let r = bigint_exp_to_bigrational(BigInt::from(-3), -2);
1958        assert_eq!(*r.numer(), BigInt::from(-3));
1959        assert_eq!(*r.denom(), BigInt::from(4));
1960    }
1961
1962    // -----------------------------------------------------------------------
1963    // bareiss_det (wrapper) tests
1964    // -----------------------------------------------------------------------
1965
1966    #[test]
1967    fn bareiss_det_d1_returns_entry() {
1968        let det = Matrix::<1>::try_from_rows([[7.0]])
1969            .unwrap()
1970            .det_exact()
1971            .unwrap();
1972        assert_eq!(det, f64_to_bigrational(7.0));
1973    }
1974
1975    #[test]
1976    fn bareiss_det_d3_with_pivoting() {
1977        // First column has zero on diagonal → exercises pivot swap + break.
1978        let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1979            .unwrap();
1980        let det = m.det_exact().unwrap();
1981        // det of this permutation matrix = -1
1982        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
1983    }
1984
1985    #[test]
1986    fn bareiss_det_singular_all_zeros_in_column() {
1987        // Column 1 is all zeros below diagonal after elimination → singular.
1988        let m = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
1989            .unwrap();
1990        let det = m.det_exact().unwrap();
1991        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
1992    }
1993
1994    #[test]
1995    fn det_sign_exact_overflow_determinant_finite_entries() {
1996        // Entries near f64::MAX are finite, but the f64 determinant overflows
1997        // to infinity.  The fast filter should be skipped and Bareiss should
1998        // compute the correct positive sign.
1999        let big = f64::MAX / 2.0;
2000        assert!(big.is_finite());
2001        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2002            .unwrap();
2003        // det = big^2 > 0
2004        assert_eq!(m.det_sign_exact().unwrap(), 1);
2005    }
2006
2007    // -----------------------------------------------------------------------
2008    // det_exact: dimension-specific tests
2009    // -----------------------------------------------------------------------
2010
2011    #[test]
2012    fn det_exact_d0_is_one() {
2013        let det = Matrix::<0>::zero().det_exact().unwrap();
2014        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
2015    }
2016
2017    #[test]
2018    fn det_exact_known_2x2() {
2019        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
2020        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2021        let det = m.det_exact().unwrap();
2022        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
2023    }
2024
2025    #[test]
2026    fn det_exact_known_dense_4x4() {
2027        let m = Matrix::<4>::try_from_rows([
2028            [4.0, 1.0, 3.0, 2.0],
2029            [0.0, 5.0, 2.0, 1.0],
2030            [7.0, 2.0, 6.0, 3.0],
2031            [1.0, 8.0, 4.0, 9.0],
2032        ])
2033        .unwrap();
2034
2035        assert_eq!(
2036            m.det_exact(),
2037            Ok(BigRational::from_integer(BigInt::from(92)))
2038        );
2039    }
2040
2041    #[test]
2042    fn det_exact_singular_returns_zero() {
2043        // Rows in arithmetic progression → exactly singular.
2044        let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
2045            .unwrap();
2046        let det = m.det_exact().unwrap();
2047        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
2048    }
2049
2050    #[test]
2051    fn det_exact_near_singular_perturbation() {
2052        // Same 2^-50 perturbation case: exact det = -3 × 2^-50.
2053        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2054        let m = Matrix::<3>::try_from_rows([
2055            [1.0 + perturbation, 2.0, 3.0],
2056            [4.0, 5.0, 6.0],
2057            [7.0, 8.0, 9.0],
2058        ])
2059        .unwrap();
2060        let det = m.det_exact().unwrap();
2061        // det should be exactly -3 × 2^-50.
2062        let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
2063        assert_eq!(det, expected);
2064    }
2065
2066    #[test]
2067    fn det_exact_5x5_permutation() {
2068        // Single swap (rows 0↔1) → det = -1.
2069        let m = Matrix::<5>::try_from_rows([
2070            [0.0, 1.0, 0.0, 0.0, 0.0],
2071            [1.0, 0.0, 0.0, 0.0, 0.0],
2072            [0.0, 0.0, 1.0, 0.0, 0.0],
2073            [0.0, 0.0, 0.0, 1.0, 0.0],
2074            [0.0, 0.0, 0.0, 0.0, 1.0],
2075        ])
2076        .unwrap();
2077        let det = m.det_exact().unwrap();
2078        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2079    }
2080
2081    // -----------------------------------------------------------------------
2082    // det_exact_f64: dimension-specific tests
2083    // -----------------------------------------------------------------------
2084
2085    #[test]
2086    fn det_exact_f64_known_2x2() {
2087        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2088        let det = m.det_exact_f64().unwrap();
2089        assert!((det - (-2.0)).abs() <= f64::EPSILON);
2090    }
2091
2092    #[test]
2093    fn det_exact_f64_overflow_returns_err() {
2094        // Entries near f64::MAX produce a determinant too large for f64.
2095        let big = f64::MAX / 2.0;
2096        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2097            .unwrap();
2098        // det = big^2, which overflows f64.
2099        assert_eq!(
2100            m.det_exact_f64(),
2101            Err(LaError::Unrepresentable {
2102                index: None,
2103                reason: UnrepresentableReason::NotFinite,
2104            })
2105        );
2106    }
2107
2108    #[test]
2109    fn det_exact_rounded_f64_overflow_returns_err() {
2110        let big = f64::MAX / 2.0;
2111        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2112            .unwrap();
2113
2114        assert_eq!(
2115            m.det_exact_rounded_f64(),
2116            Err(LaError::Unrepresentable {
2117                index: None,
2118                reason: UnrepresentableReason::NotFinite,
2119            })
2120        );
2121    }
2122
2123    #[test]
2124    fn det_exact_f64_underflow_returns_err_for_nonzero_exact_result() {
2125        let tiny = f64::from_bits(1);
2126        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
2127
2128        assert!(m.det_exact().unwrap().is_positive());
2129        assert_eq!(
2130            m.det_exact_f64(),
2131            Err(LaError::Unrepresentable {
2132                index: None,
2133                reason: UnrepresentableReason::RequiresRounding,
2134            })
2135        );
2136    }
2137
2138    #[test]
2139    fn det_exact_f64_rejects_inexact_rounding() {
2140        let m = Matrix::<2>::try_from_rows([[1.0 + f64::EPSILON, 0.0], [0.0, 1.0 - f64::EPSILON]])
2141            .unwrap();
2142
2143        assert_eq!(
2144            m.det_exact(),
2145            Ok(BigRational::new(
2146                (BigInt::from(1_u128) << 104_u32) - BigInt::from(1),
2147                BigInt::from(1_u128 << 104),
2148            ))
2149        );
2150        assert_eq!(
2151            m.det_exact_f64(),
2152            Err(LaError::Unrepresentable {
2153                index: None,
2154                reason: UnrepresentableReason::RequiresRounding,
2155            })
2156        );
2157    }
2158
2159    #[test]
2160    fn det_exact_f64_accepts_min_positive_subnormal() {
2161        let tiny = f64::from_bits(1);
2162        let m = Matrix::<1>::try_from_rows([[tiny]]).unwrap();
2163
2164        assert_eq!(m.det_exact_f64().unwrap().to_bits(), tiny.to_bits());
2165    }
2166
2167    #[test]
2168    fn det_exact_f64_accepts_max_finite_binary64() {
2169        let m = Matrix::<1>::try_from_rows([[f64::MAX]]).unwrap();
2170
2171        assert_eq!(m.det_exact_f64().unwrap().to_bits(), f64::MAX.to_bits());
2172    }
2173
2174    #[test]
2175    fn det_exact_rounded_f64_rounds_inexact_result() {
2176        let m = Matrix::<2>::try_from_rows([[1.0 + f64::EPSILON, 0.0], [0.0, 1.0 - f64::EPSILON]])
2177            .unwrap();
2178
2179        assert_eq!(
2180            m.det_exact_rounded_f64().unwrap().to_bits(),
2181            1.0f64.to_bits()
2182        );
2183    }
2184
2185    // -----------------------------------------------------------------------
2186    // solve_exact: macro-generated per-dimension tests (D=2..5)
2187    // -----------------------------------------------------------------------
2188
2189    /// Helper: build an arbitrary RHS vector for dimension `$d`.
2190    fn arbitrary_rhs<const D: usize>() -> Vector<D> {
2191        let values = [1.0, -2.5, 3.0, 0.25, -4.0];
2192        let mut arr = [0.0f64; D];
2193        for (dst, src) in arr.iter_mut().zip(values.iter()) {
2194            *dst = *src;
2195        }
2196        Vector::<D>::new(arr)
2197    }
2198
2199    macro_rules! gen_solve_exact_tests {
2200        ($d:literal) => {
2201            paste! {
2202                #[test]
2203                fn [<solve_exact_identity_ $d d>]() {
2204                    let a = Matrix::<$d>::identity();
2205                    let b = arbitrary_rhs::<$d>();
2206                    let x = a.solve_exact(b).unwrap();
2207                    for (i, xi) in x.iter().enumerate() {
2208                        assert_eq!(*xi, f64_to_bigrational(b.as_array()[i]));
2209                    }
2210                }
2211
2212                #[test]
2213                fn [<solve_exact_singular_ $d d>]() {
2214                    // Zero matrix is singular.
2215                    let a = Matrix::<$d>::zero();
2216                    let b = arbitrary_rhs::<$d>();
2217                    assert_eq!(a.solve_exact(b), Err(LaError::Singular { pivot_col: 0 }));
2218                }
2219            }
2220        };
2221    }
2222
2223    gen_solve_exact_tests!(2);
2224    gen_solve_exact_tests!(3);
2225    gen_solve_exact_tests!(4);
2226    gen_solve_exact_tests!(5);
2227
2228    macro_rules! gen_solve_exact_f64_tests {
2229        ($d:literal) => {
2230            paste! {
2231                #[test]
2232                fn [<solve_exact_f64_identity_ $d d>]() {
2233                    let a = Matrix::<$d>::identity();
2234                    let b = arbitrary_rhs::<$d>();
2235                    let x = a.solve_exact_f64(b).unwrap().into_array();
2236                    for i in 0..$d {
2237                        assert!((x[i] - b.as_array()[i]).abs() <= f64::EPSILON);
2238                    }
2239                }
2240            }
2241        };
2242    }
2243
2244    gen_solve_exact_f64_tests!(2);
2245    gen_solve_exact_f64_tests!(3);
2246    gen_solve_exact_f64_tests!(4);
2247    gen_solve_exact_f64_tests!(5);
2248
2249    /// For D ≤ 4, `solve_exact_f64` should agree with `Lu::solve` on
2250    /// well-conditioned matrices.
2251    macro_rules! gen_solve_exact_f64_agrees_with_lu {
2252        ($d:literal) => {
2253            paste! {
2254                #[test]
2255                fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
2256                    // Diagonally dominant integer matrix with an exactly
2257                    // representable target solution.  The exact result can
2258                    // therefore be parsed into f64 without rounding.
2259                    let mut rows = [[0.0f64; $d]; $d];
2260                    for r in 0..$d {
2261                        for c in 0..$d {
2262                            rows[r][c] = if r == c {
2263                                f64::from($d) + 1.0
2264                            } else {
2265                                1.0
2266                            };
2267                        }
2268                    }
2269                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2270                    let x_true = {
2271                        let mut arr = [0.0f64; $d];
2272                        for (dst, src) in arr.iter_mut().zip([1.0, -2.0, 3.0, -4.0, 5.0]) {
2273                            *dst = src;
2274                        }
2275                        arr
2276                    };
2277                    let mut b_arr = [0.0f64; $d];
2278                    for i in 0..$d {
2279                        let mut sum = 0.0;
2280                        for j in 0..$d {
2281                            sum = rows[i][j].mul_add(x_true[j], sum);
2282                        }
2283                        b_arr[i] = sum;
2284                    }
2285                    let b = Vector::<$d>::new(b_arr);
2286                    let exact = a.solve_exact_f64(b).unwrap().into_array();
2287                    let lu_sol = a.lu(DEFAULT_SINGULAR_TOL).unwrap()
2288                        .solve(b).unwrap().into_array();
2289                    for i in 0..$d {
2290                        assert_eq!(exact[i].to_bits(), x_true[i].to_bits());
2291                        let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
2292                        assert!((exact[i] - lu_sol[i]).abs() <= eps);
2293                    }
2294                }
2295            }
2296        };
2297    }
2298
2299    gen_solve_exact_f64_agrees_with_lu!(2);
2300    gen_solve_exact_f64_agrees_with_lu!(3);
2301    gen_solve_exact_f64_agrees_with_lu!(4);
2302    gen_solve_exact_f64_agrees_with_lu!(5);
2303
2304    /// Round-trip: for a well-conditioned integer matrix `A` and integer
2305    /// target `x0`, solving `A x = A x0` must return `x0` exactly.  All
2306    /// intermediate values stay small enough that `A * x0` is exactly
2307    /// representable in `f64`, so the round-trip is a precise equality
2308    /// check on the hybrid BigInt/BigRational path.
2309    macro_rules! gen_solve_exact_roundtrip_tests {
2310        ($d:literal) => {
2311            paste! {
2312                #[test]
2313                #[allow(clippy::cast_precision_loss)]
2314                fn [<solve_exact_roundtrip_ $d d>]() {
2315                    // A = D * I + J (diag = D+1, off-diag = 1).  Invertible
2316                    // for any D >= 1 and cheap to multiply by hand.
2317                    let mut rows = [[0.0f64; $d]; $d];
2318                    for r in 0..$d {
2319                        for c in 0..$d {
2320                            rows[r][c] = if r == c {
2321                                f64::from($d) + 1.0
2322                            } else {
2323                                1.0
2324                            };
2325                        }
2326                    }
2327                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2328
2329                    // x0 = [1, 2, ..., D].
2330                    let mut x0 = [0.0f64; $d];
2331                    for i in 0..$d {
2332                        x0[i] = (i + 1) as f64;
2333                    }
2334
2335                    // b = A * x0 computed in f64.  With small integers the
2336                    // multiply-add sequence is exact.
2337                    let mut b_arr = [0.0f64; $d];
2338                    for r in 0..$d {
2339                        let mut sum = 0.0_f64;
2340                        for c in 0..$d {
2341                            sum = rows[r][c].mul_add(x0[c], sum);
2342                        }
2343                        b_arr[r] = sum;
2344                    }
2345                    let b = Vector::<$d>::new(b_arr);
2346
2347                    let x = a.solve_exact(b).unwrap();
2348                    for i in 0..$d {
2349                        assert_eq!(x[i], f64_to_bigrational(x0[i]));
2350                    }
2351                }
2352            }
2353        };
2354    }
2355
2356    gen_solve_exact_roundtrip_tests!(2);
2357    gen_solve_exact_roundtrip_tests!(3);
2358    gen_solve_exact_roundtrip_tests!(4);
2359    gen_solve_exact_roundtrip_tests!(5);
2360
2361    // -----------------------------------------------------------------------
2362    // solve_exact: dimension-specific tests
2363    // -----------------------------------------------------------------------
2364
2365    #[test]
2366    fn solve_exact_d0_returns_empty() {
2367        let a = Matrix::<0>::zero();
2368        let b = Vector::<0>::zero();
2369        let x = a.solve_exact(b).unwrap();
2370        assert!(x.is_empty());
2371    }
2372
2373    #[test]
2374    fn solve_exact_known_2x2() {
2375        // [[1,2],[3,4]] x = [5, 11] → x = [1, 2]
2376        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2377        let b = Vector::<2>::new([5.0, 11.0]);
2378        let x = a.solve_exact(b).unwrap();
2379        assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
2380        assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
2381    }
2382
2383    #[test]
2384    fn solve_exact_pivoting_needed() {
2385        // First column has zero on diagonal → pivot swap required.
2386        let a = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
2387            .unwrap();
2388        let b = Vector::<3>::new([2.0, 3.0, 4.0]);
2389        let x = a.solve_exact(b).unwrap();
2390        // x = [3, 2, 4]
2391        assert_eq!(x[0], f64_to_bigrational(3.0));
2392        assert_eq!(x[1], f64_to_bigrational(2.0));
2393        assert_eq!(x[2], f64_to_bigrational(4.0));
2394    }
2395
2396    #[test]
2397    fn solve_exact_fractional_result() {
2398        // [[2, 1], [1, 3]] x = [1, 1] → x = [2/5, 1/5]
2399        let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
2400        let b = Vector::<2>::new([1.0, 1.0]);
2401        let x = a.solve_exact(b).unwrap();
2402        assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
2403        assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
2404    }
2405
2406    #[test]
2407    fn solve_exact_singular_duplicate_rows() {
2408        let a = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]])
2409            .unwrap();
2410        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
2411        assert_matches!(a.solve_exact(b), Err(LaError::Singular { .. }));
2412    }
2413
2414    #[test]
2415    fn solve_exact_5x5_permutation() {
2416        // Permutation matrix (swap rows 0↔1): P x = b → x = P^T b.
2417        let a = Matrix::<5>::try_from_rows([
2418            [0.0, 1.0, 0.0, 0.0, 0.0],
2419            [1.0, 0.0, 0.0, 0.0, 0.0],
2420            [0.0, 0.0, 1.0, 0.0, 0.0],
2421            [0.0, 0.0, 0.0, 1.0, 0.0],
2422            [0.0, 0.0, 0.0, 0.0, 1.0],
2423        ])
2424        .unwrap();
2425        let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
2426        let x = a.solve_exact(b).unwrap();
2427        assert_eq!(x[0], f64_to_bigrational(20.0));
2428        assert_eq!(x[1], f64_to_bigrational(10.0));
2429        assert_eq!(x[2], f64_to_bigrational(30.0));
2430        assert_eq!(x[3], f64_to_bigrational(40.0));
2431        assert_eq!(x[4], f64_to_bigrational(50.0));
2432    }
2433
2434    /// Entries near `f64::MAX / 2` are finite but their product would
2435    /// overflow to ±∞ in pure f64 arithmetic.  The `BigInt` augmented-system
2436    /// path computes the correct solution without any overflow.  The D×D
2437    /// case uses a diagonal matrix with `big` on every diagonal and a RHS
2438    /// of `[big, …, big, 0]`, giving the known solution `[1, …, 1, 0]`.
2439    macro_rules! gen_solve_exact_large_finite_entries_tests {
2440        ($d:literal) => {
2441            paste! {
2442                #[test]
2443                fn [<solve_exact_large_finite_entries_ $d d>]() {
2444                    let big = f64::MAX / 2.0;
2445                    assert!(big.is_finite());
2446                    // D×D diagonal matrix with `big` on the diagonal.
2447                    let mut rows = [[0.0f64; $d]; $d];
2448                    for i in 0..$d {
2449                        rows[i][i] = big;
2450                    }
2451                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2452                    // RHS = [big, …, big, 0] → x = [1, …, 1, 0].
2453                    let mut b_arr = [big; $d];
2454                    b_arr[$d - 1] = 0.0;
2455                    let b = Vector::<$d>::new(b_arr);
2456                    let x = a.solve_exact(b).unwrap();
2457                    for i in 0..($d - 1) {
2458                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
2459                    }
2460                    assert_eq!(x[$d - 1], BigRational::from_integer(BigInt::from(0)));
2461                }
2462            }
2463        };
2464    }
2465
2466    gen_solve_exact_large_finite_entries_tests!(2);
2467    gen_solve_exact_large_finite_entries_tests!(3);
2468    gen_solve_exact_large_finite_entries_tests!(4);
2469    gen_solve_exact_large_finite_entries_tests!(5);
2470
2471    /// Matrix and RHS entries span many orders of magnitude (from
2472    /// `f64::MIN_POSITIVE` up through `1e100`).  This exercises the
2473    /// shared `e_min` scaling: even the largest shift keeps every entry a
2474    /// representable `BigInt`.  The D×D case alternates `huge`/`tiny`
2475    /// along the diagonal with a matching RHS, giving `x = [1, …, 1]`.
2476    macro_rules! gen_solve_exact_mixed_magnitude_entries_tests {
2477        ($d:literal) => {
2478            paste! {
2479                #[test]
2480                fn [<solve_exact_mixed_magnitude_entries_ $d d>]() {
2481                    let tiny = f64::MIN_POSITIVE; // 2^-1022, smallest normal
2482                    let huge = 1.0e100_f64;
2483                    // Alternate huge/tiny along the diagonal.
2484                    let mut rows = [[0.0f64; $d]; $d];
2485                    let mut b_arr = [0.0f64; $d];
2486                    for i in 0..$d {
2487                        let val = if i % 2 == 0 { huge } else { tiny };
2488                        rows[i][i] = val;
2489                        b_arr[i] = val;
2490                    }
2491                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2492                    let b = Vector::<$d>::new(b_arr);
2493                    let x = a.solve_exact(b).unwrap();
2494                    for i in 0..$d {
2495                        assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
2496                    }
2497                }
2498            }
2499        };
2500    }
2501
2502    gen_solve_exact_mixed_magnitude_entries_tests!(2);
2503    gen_solve_exact_mixed_magnitude_entries_tests!(3);
2504    gen_solve_exact_mixed_magnitude_entries_tests!(4);
2505    gen_solve_exact_mixed_magnitude_entries_tests!(5);
2506
2507    /// Subnormal RHS entries must survive the decomposition and
2508    /// back-substitution paths unchanged.  The D×D case uses the identity
2509    /// matrix and RHS `[1·tiny, 2·tiny, …, D·tiny]`; each entry remains a
2510    /// valid subnormal f64 (integer multiples of `2^-1074` fit in the
2511    /// 52-bit subnormal mantissa for the small integers used here).
2512    macro_rules! gen_solve_exact_subnormal_rhs_tests {
2513        ($d:literal) => {
2514            paste! {
2515                #[test]
2516                #[allow(clippy::cast_precision_loss)]
2517                fn [<solve_exact_subnormal_rhs_ $d d>]() {
2518                    let tiny = 5e-324_f64; // smallest positive subnormal
2519                    assert!(tiny.is_subnormal());
2520                    let a = Matrix::<$d>::identity();
2521                    // b[i] = (i+1) · tiny — each entry remains a valid subnormal.
2522                    let mut b_arr = [0.0f64; $d];
2523                    for i in 0..$d {
2524                        b_arr[i] = (i + 1) as f64 * tiny;
2525                        assert!(b_arr[i].is_subnormal());
2526                    }
2527                    let b = Vector::<$d>::new(b_arr);
2528                    let x = a.solve_exact(b).unwrap();
2529                    for i in 0..$d {
2530                        assert_eq!(x[i], f64_to_bigrational((i + 1) as f64 * tiny));
2531                    }
2532                }
2533            }
2534        };
2535    }
2536
2537    gen_solve_exact_subnormal_rhs_tests!(2);
2538    gen_solve_exact_subnormal_rhs_tests!(3);
2539    gen_solve_exact_subnormal_rhs_tests!(4);
2540    gen_solve_exact_subnormal_rhs_tests!(5);
2541
2542    /// Pivoting path with a zero top-left entry forces a row swap in the
2543    /// `BigInt` forward-elimination loop and propagates it to the RHS.
2544    /// Combined with a fractional solution, this exercises the
2545    /// `BigRational` back-substitution after integer forward elimination.
2546    ///
2547    /// The 2×2 block `[[0, 1], [2, 1]]` with rhs `[3, 4]` (→ `x = [1/2, 3]`)
2548    /// is embedded into the top-left of a D×D identity matrix.  Remaining
2549    /// rows contribute pass-through equalities `x[i] = b[i]`, so the same
2550    /// fractional solution appears at indices 0 and 1 regardless of D.
2551    macro_rules! gen_solve_exact_pivot_swap_fractional_tests {
2552        ($d:literal) => {
2553            paste! {
2554                #[test]
2555                #[allow(clippy::cast_precision_loss)]
2556                // `2..$d` is empty when D=2 (no padded rows); that is the
2557                // intended behaviour of the macro, not a bug.
2558                #[allow(clippy::reversed_empty_ranges)]
2559                fn [<solve_exact_pivot_swap_with_fractional_result_ $d d>]() {
2560                    // Top-left 2×2: A = [[0, 1], [2, 1]].  After swap:
2561                    // [[2, 1], [0, 1]], rhs = [4, 3] → x[1] = 3, x[0] = 1/2.
2562                    let mut rows = [[0.0f64; $d]; $d];
2563                    rows[0][1] = 1.0;
2564                    rows[1][0] = 2.0;
2565                    rows[1][1] = 1.0;
2566                    // Identity padding for the remaining rows.
2567                    for i in 2..$d {
2568                        rows[i][i] = 1.0;
2569                    }
2570                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2571                    // b = [3, 4, 12, 13, …]; padded entries are arbitrary
2572                    // finite integers so the identity block gives x[i] = b[i].
2573                    let mut b_arr = [0.0f64; $d];
2574                    b_arr[0] = 3.0;
2575                    b_arr[1] = 4.0;
2576                    for i in 2..$d {
2577                        b_arr[i] = (i + 10) as f64;
2578                    }
2579                    let b = Vector::<$d>::new(b_arr);
2580                    let x = a.solve_exact(b).unwrap();
2581                    assert_eq!(x[0], BigRational::new(BigInt::from(1), BigInt::from(2)));
2582                    assert_eq!(x[1], BigRational::from_integer(BigInt::from(3)));
2583                    for i in 2..$d {
2584                        assert_eq!(x[i], f64_to_bigrational((i + 10) as f64));
2585                    }
2586                }
2587            }
2588        };
2589    }
2590
2591    gen_solve_exact_pivot_swap_fractional_tests!(2);
2592    gen_solve_exact_pivot_swap_fractional_tests!(3);
2593    gen_solve_exact_pivot_swap_fractional_tests!(4);
2594    gen_solve_exact_pivot_swap_fractional_tests!(5);
2595
2596    /// Mid-elimination pivot swap: the 3×3 block
2597    /// `[[1, 2, 3], [0, 0, 4], [0, 5, 6]]` has a non-zero pivot at k=0 but
2598    /// a zero pivot at k=1, so the swap happens *during* forward
2599    /// elimination rather than at the start.  With rhs `[6, 7, 8]` the
2600    /// exact solution is `[7/4, -1/2, 7/4]`.  For D > 3 the block is
2601    /// embedded into the top-left of a D×D identity matrix so the same
2602    /// fractional solution appears in `x[0..3]` and `x[i] = b[i]` for
2603    /// `i >= 3`.
2604    macro_rules! gen_solve_exact_mid_pivot_swap_tests {
2605        ($d:literal) => {
2606            paste! {
2607                #[test]
2608                #[allow(clippy::cast_precision_loss)]
2609                // `3..$d` is empty when D=3 (no padded rows); that is the
2610                // intended behaviour of the macro, not a bug.
2611                #[allow(clippy::reversed_empty_ranges)]
2612                fn [<solve_exact_mid_pivot_swap_ $d d>]() {
2613                    let mut rows = [[0.0f64; $d]; $d];
2614                    rows[0][0] = 1.0; rows[0][1] = 2.0; rows[0][2] = 3.0;
2615                    // rows[1][0..2] are zero; rows[1][2] = 4.
2616                    rows[1][2] = 4.0;
2617                    rows[2][1] = 5.0; rows[2][2] = 6.0;
2618                    // Identity padding for the remaining rows.
2619                    for i in 3..$d {
2620                        rows[i][i] = 1.0;
2621                    }
2622                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2623                    let mut b_arr = [0.0f64; $d];
2624                    b_arr[0] = 6.0;
2625                    b_arr[1] = 7.0;
2626                    b_arr[2] = 8.0;
2627                    for i in 3..$d {
2628                        b_arr[i] = (i + 10) as f64;
2629                    }
2630                    let b = Vector::<$d>::new(b_arr);
2631                    let x = a.solve_exact(b).unwrap();
2632                    // x[0..3] = [7/4, -1/2, 7/4].
2633                    assert_eq!(x[0], BigRational::new(BigInt::from(7), BigInt::from(4)));
2634                    assert_eq!(x[1], BigRational::new(BigInt::from(-1), BigInt::from(2)));
2635                    assert_eq!(x[2], BigRational::new(BigInt::from(7), BigInt::from(4)));
2636                    for i in 3..$d {
2637                        assert_eq!(x[i], f64_to_bigrational((i + 10) as f64));
2638                    }
2639                }
2640            }
2641        };
2642    }
2643
2644    gen_solve_exact_mid_pivot_swap_tests!(3);
2645    gen_solve_exact_mid_pivot_swap_tests!(4);
2646    gen_solve_exact_mid_pivot_swap_tests!(5);
2647
2648    /// Rank-deficient singular: the last column is identically zero and the
2649    /// leading `(D-1)×(D-1)` block is full rank, so every intermediate
2650    /// pivot is non-zero and the singularity surfaces only at the final
2651    /// column.  The matrix is identity in the top-left `(D-1)×(D-1)` with
2652    /// a row of ones as the last row (and an all-zero last column), so the
2653    /// rank is exactly `D-1`.  `solve_exact` must return
2654    /// `LaError::Singular { pivot_col: D - 1 }`.
2655    macro_rules! gen_solve_exact_singular_rank_deficient_tests {
2656        ($d:literal) => {
2657            paste! {
2658                #[test]
2659                fn [<solve_exact_singular_rank_deficient_ $d d>]() {
2660                    let mut rows = [[0.0f64; $d]; $d];
2661                    for i in 0..($d - 1) {
2662                        rows[i][i] = 1.0;
2663                        rows[$d - 1][i] = 1.0;
2664                    }
2665                    // Last column is left all-zero → rank exactly D-1.
2666                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2667                    let b = Vector::<$d>::new([1.0; $d]);
2668                    assert_eq!(
2669                        a.solve_exact(b),
2670                        Err(LaError::Singular { pivot_col: $d - 1 })
2671                    );
2672                }
2673            }
2674        };
2675    }
2676
2677    gen_solve_exact_singular_rank_deficient_tests!(2);
2678    gen_solve_exact_singular_rank_deficient_tests!(3);
2679    gen_solve_exact_singular_rank_deficient_tests!(4);
2680    gen_solve_exact_singular_rank_deficient_tests!(5);
2681
2682    /// Zero RHS with a non-singular matrix.  Every Bareiss update reads
2683    /// `rhs[k]` and `rhs[i]`, both initialised to zero; every update
2684    /// produces zero; back-substitution therefore yields `x = 0`
2685    /// regardless of the matrix entries.  This exercises the
2686    /// back-substitution `mem::take` path against an all-zero `rhs`.
2687    macro_rules! gen_solve_exact_zero_rhs_tests {
2688        ($d:literal) => {
2689            paste! {
2690                #[test]
2691                fn [<solve_exact_zero_rhs_ $d d>]() {
2692                    // A = D*I + J (diagonally dominant, invertible).
2693                    let mut rows = [[1.0f64; $d]; $d];
2694                    for i in 0..$d {
2695                        rows[i][i] = f64::from($d) + 1.0;
2696                    }
2697                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2698                    let b = Vector::<$d>::zero();
2699                    let x = a.solve_exact(b).unwrap();
2700                    for xi in &x {
2701                        assert_eq!(*xi, BigRational::from_integer(BigInt::from(0)));
2702                    }
2703                }
2704            }
2705        };
2706    }
2707
2708    gen_solve_exact_zero_rhs_tests!(2);
2709    gen_solve_exact_zero_rhs_tests!(3);
2710    gen_solve_exact_zero_rhs_tests!(4);
2711    gen_solve_exact_zero_rhs_tests!(5);
2712
2713    // -----------------------------------------------------------------------
2714    // Adversarial-input coverage mirroring `benches/exact.rs`
2715    // -----------------------------------------------------------------------
2716    //
2717    // These tests pin the behaviour of the extreme-input benchmark groups
2718    // (`exact_near_singular_3x3`, `exact_large_entries_3x3`,
2719    // `exact_hilbert_{4x4,5x5}`) so a regression would be caught even
2720    // when benchmarks are not running.
2721
2722    /// Multiply `A · x` entirely in `BigRational`, using `f64_to_bigrational`
2723    /// to lift each matrix entry.  Used by residual assertions for inputs
2724    /// whose exact solution has no closed form we can easily type out.
2725    fn bigrational_matvec<const D: usize>(a: &Matrix<D>, x: &[BigRational; D]) -> [BigRational; D] {
2726        from_fn(|i| {
2727            let mut sum = BigRational::from_integer(BigInt::from(0));
2728            for (aij, xj) in a.rows()[i].iter().zip(x.iter()) {
2729                sum += f64_to_bigrational(*aij) * xj;
2730            }
2731            sum
2732        })
2733    }
2734
2735    fn hilbert<const D: usize>() -> Matrix<D> {
2736        let rows = from_fn(|r| from_fn(|c| 1.0 / f64::from(u32::try_from(r + c + 1).unwrap())));
2737        Matrix::<D>::try_from_rows(rows).unwrap()
2738    }
2739
2740    /// Near-singular 3×3 solve (matches the `exact_near_singular_3x3`
2741    /// bench).  With `A = [[1+2^-50, 2, 3], [4, 5, 6], [7, 8, 9]]` and
2742    /// `x0 = [1, 1, 1]`, `A · x0 = [6 + 2^-50, 15, 24]`; every component is
2743    /// exactly representable in `f64` (`6` has ulp `2^-50` at its exponent).
2744    /// `solve_exact` must recover `x0` exactly — the fractional denominator
2745    /// introduced by `det(A) = -3 × 2^-50` cancels cleanly against the
2746    /// augmented RHS.
2747    #[test]
2748    fn solve_exact_near_singular_3x3_integer_x0() {
2749        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2750        let a = Matrix::<3>::try_from_rows([
2751            [1.0 + perturbation, 2.0, 3.0],
2752            [4.0, 5.0, 6.0],
2753            [7.0, 8.0, 9.0],
2754        ])
2755        .unwrap();
2756        let b = Vector::<3>::new([6.0 + perturbation, 15.0, 24.0]);
2757        let x = a.solve_exact(b).unwrap();
2758        let one = BigRational::from_integer(BigInt::from(1));
2759        assert_eq!(x[0], one);
2760        assert_eq!(x[1], one);
2761        assert_eq!(x[2], one);
2762    }
2763
2764    #[test]
2765    fn solve_exact_f64_near_singular_benchmark_rhs_is_rejection_path() {
2766        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2767        let a = Matrix::<3>::try_from_rows([
2768            [1.0 + perturbation, 2.0, 3.0],
2769            [4.0, 5.0, 6.0],
2770            [7.0, 8.0, 9.0],
2771        ])
2772        .unwrap();
2773        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
2774
2775        assert_eq!(
2776            a.solve_exact_f64(b),
2777            Err(LaError::Unrepresentable {
2778                index: Some(2),
2779                reason: UnrepresentableReason::RequiresRounding,
2780            })
2781        );
2782        assert_eq!(
2783            a.solve_exact_rounded_f64(b).unwrap().into_array()[2].to_bits(),
2784            (1.0f64 / 3.0).to_bits()
2785        );
2786    }
2787
2788    /// Large-entry 3×3 solve (matches the `exact_large_entries_3x3`
2789    /// bench).  `A = big · I + (1 - I)` with `big = f64::MAX / 2` and
2790    /// `b = [big, 1, 1] = A · [1, 0, 0]`.  The `BigInt` augmented system
2791    /// sees entries of ~1023 bits on the diagonal and unit entries
2792    /// elsewhere; Bareiss elimination still produces the exact integer
2793    /// solution `[1, 0, 0]`.
2794    #[test]
2795    fn solve_exact_large_entries_3x3_unit_vector() {
2796        let big = f64::MAX / 2.0;
2797        assert!(big.is_finite());
2798        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
2799            .unwrap();
2800        let b = Vector::<3>::new([big, 1.0, 1.0]);
2801        let x = a.solve_exact(b).unwrap();
2802        let zero = BigRational::from_integer(BigInt::from(0));
2803        let one = BigRational::from_integer(BigInt::from(1));
2804        assert_eq!(x[0], one);
2805        assert_eq!(x[1], zero);
2806        assert_eq!(x[2], zero);
2807    }
2808
2809    /// Determinant of the large-entry 3×3 is roughly `big^3`, which
2810    /// overflows `f64`. `det_direct()` therefore reports a computed
2811    /// [`LaError::NonFinite`], the fast filter inside `det_sign_exact`
2812    /// treats that as inconclusive, and the Bareiss fallback resolves the
2813    /// positive sign correctly. `det_exact_f64` must report `Unrepresentable`.
2814    #[test]
2815    fn det_sign_exact_large_entries_3x3_positive() {
2816        let big = f64::MAX / 2.0;
2817        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
2818            .unwrap();
2819        // Fast filter is inconclusive (big^3 overflows f64 to +∞), so
2820        // this exercises the Bareiss cold path.
2821        assert_matches!(a.det_direct(), Err(LaError::NonFinite { row: None, .. }));
2822        assert_eq!(a.det_sign_exact().unwrap(), 1);
2823        // Cross-validate: the exact `BigRational` determinant must agree
2824        // on sign with `det_sign_exact`, and `det_exact_f64` must reject the
2825        // conversion (the value is representable in BigRational but far exceeds f64).
2826        assert!(a.det_exact().unwrap().is_positive());
2827        assert_eq!(
2828            a.det_exact_f64(),
2829            Err(LaError::Unrepresentable {
2830                index: None,
2831                reason: UnrepresentableReason::NotFinite,
2832            })
2833        );
2834    }
2835
2836    /// Hilbert matrices are symmetric positive-definite, so
2837    /// `det_sign_exact` must return `1` for every D.  For D=2..=4 the
2838    /// fast f64 filter resolves the positive sign without falling
2839    /// through (Hilbert's determinant is tiny but still well above the
2840    /// `det_errbound` cushion); for D=5 the filter is skipped entirely
2841    /// and the Bareiss path handles inputs whose `(mantissa, exponent)`
2842    /// pairs all differ.
2843    macro_rules! gen_det_sign_exact_hilbert_positive_tests {
2844        ($d:literal) => {
2845            paste! {
2846                #[test]
2847                fn [<det_sign_exact_hilbert_positive_ $d d>]() {
2848                    let h = hilbert::<$d>();
2849                    assert_eq!(h.det_sign_exact().unwrap(), 1);
2850                }
2851            }
2852        };
2853    }
2854
2855    gen_det_sign_exact_hilbert_positive_tests!(2);
2856    gen_det_sign_exact_hilbert_positive_tests!(3);
2857    gen_det_sign_exact_hilbert_positive_tests!(4);
2858    gen_det_sign_exact_hilbert_positive_tests!(5);
2859
2860    macro_rules! gen_solve_exact_f64_hilbert_benchmark_rhs_tests {
2861        ($d:literal) => {
2862            paste! {
2863                #[test]
2864                fn [<solve_exact_f64_hilbert_benchmark_rhs_is_rejection_path_ $d d>]() {
2865                    let h = hilbert::<$d>();
2866                    let b = Vector::<$d>::new([1.0; $d]);
2867
2868                    assert_matches!(
2869                        h.solve_exact_f64(b),
2870                        Err(LaError::Unrepresentable {
2871                            reason: UnrepresentableReason::RequiresRounding,
2872                            ..
2873                        })
2874                    );
2875                    assert_matches!(h.solve_exact_rounded_f64(b), Ok(_));
2876                }
2877            }
2878        };
2879    }
2880
2881    gen_solve_exact_f64_hilbert_benchmark_rhs_tests!(4);
2882    gen_solve_exact_f64_hilbert_benchmark_rhs_tests!(5);
2883
2884    /// `solve_exact` on a Hilbert matrix must produce a solution whose
2885    /// residual `A · x - b` is *exactly* zero in `BigRational` arithmetic.
2886    /// Hilbert entries (`1/3`, `1/5`, `1/6`, `1/7`, …) are non-terminating
2887    /// in binary, so this is a stronger test than the
2888    /// `gen_solve_exact_roundtrip_tests` construction (which requires the
2889    /// RHS to be representable as an exact `f64` product).
2890    macro_rules! gen_solve_exact_hilbert_residual_tests {
2891        ($d:literal) => {
2892            paste! {
2893                #[test]
2894                fn [<solve_exact_hilbert_residual_ $d d>]() {
2895                    let h = hilbert::<$d>();
2896                    // Use a non-trivial RHS with both positive and negative
2897                    // entries to avoid accidental structural cancellation.
2898                    let mut b_arr = [0.0f64; $d];
2899                    for i in 0usize..$d {
2900                        let sign = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
2901                        b_arr[i] = sign * f64::from(u32::try_from(i + 1).unwrap());
2902                    }
2903                    let b = Vector::<$d>::new(b_arr);
2904                    let x = h.solve_exact(b).unwrap();
2905                    let ax = bigrational_matvec(&h, &x);
2906                    for i in 0..$d {
2907                        assert_eq!(ax[i], f64_to_bigrational(b_arr[i]));
2908                    }
2909                }
2910            }
2911        };
2912    }
2913
2914    gen_solve_exact_hilbert_residual_tests!(2);
2915    gen_solve_exact_hilbert_residual_tests!(3);
2916    gen_solve_exact_hilbert_residual_tests!(4);
2917    gen_solve_exact_hilbert_residual_tests!(5);
2918
2919    // -----------------------------------------------------------------------
2920    // solve_exact_f64: dimension-specific tests
2921    // -----------------------------------------------------------------------
2922
2923    #[test]
2924    fn solve_exact_f64_known_2x2() {
2925        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2926        let b = Vector::<2>::new([5.0, 11.0]);
2927        let x = a.solve_exact_f64(b).unwrap().into_array();
2928        assert!((x[0] - 1.0).abs() <= f64::EPSILON);
2929        assert!((x[1] - 2.0).abs() <= f64::EPSILON);
2930    }
2931
2932    #[test]
2933    fn solve_exact_f64_overflow_returns_err() {
2934        // [[1/big, 0], [0, 1/big]] x = [big, big] → x = [big², big²],
2935        // which overflows f64.
2936        let big = f64::MAX / 2.0;
2937        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
2938        let b = Vector::<2>::new([big, big]);
2939        assert_eq!(
2940            a.solve_exact_f64(b),
2941            Err(LaError::Unrepresentable {
2942                index: Some(0),
2943                reason: UnrepresentableReason::NotFinite,
2944            })
2945        );
2946    }
2947
2948    #[test]
2949    fn solve_exact_f64_huge_non_dyadic_component_returns_not_finite() {
2950        let a = Matrix::<1>::try_from_rows([[3.0 * f64::MIN_POSITIVE]]).unwrap();
2951        let b = Vector::<1>::new([f64::MAX]);
2952
2953        assert_eq!(
2954            a.solve_exact_f64(b),
2955            Err(LaError::Unrepresentable {
2956                index: Some(0),
2957                reason: UnrepresentableReason::NotFinite,
2958            })
2959        );
2960    }
2961
2962    #[test]
2963    fn solve_exact_rounded_f64_overflow_returns_err() {
2964        let big = f64::MAX / 2.0;
2965        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
2966        let b = Vector::<2>::new([big, big]);
2967
2968        assert_eq!(
2969            a.solve_exact_rounded_f64(b),
2970            Err(LaError::Unrepresentable {
2971                index: Some(0),
2972                reason: UnrepresentableReason::NotFinite,
2973            })
2974        );
2975    }
2976
2977    #[test]
2978    fn solve_exact_f64_underflow_returns_err_for_nonzero_exact_component() {
2979        let tiny = f64::from_bits(1);
2980        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
2981        let b = Vector::<1>::new([tiny]);
2982
2983        assert_eq!(
2984            a.solve_exact_f64(b),
2985            Err(LaError::Unrepresentable {
2986                index: Some(0),
2987                reason: UnrepresentableReason::RequiresRounding,
2988            })
2989        );
2990    }
2991
2992    #[test]
2993    fn solve_exact_f64_accepts_smallest_subnormal_result() {
2994        let tiny = f64::from_bits(1);
2995        let a = Matrix::<1>::identity();
2996        let b = Vector::<1>::new([tiny]);
2997
2998        assert_eq!(
2999            a.solve_exact_f64(b).unwrap().into_array()[0].to_bits(),
3000            tiny.to_bits()
3001        );
3002    }
3003
3004    #[test]
3005    fn solve_exact_f64_rejects_non_dyadic_component() {
3006        let a = Matrix::<1>::try_from_rows([[3.0]]).unwrap();
3007        let b = Vector::<1>::new([1.0]);
3008
3009        assert_eq!(
3010            a.solve_exact_f64(b),
3011            Err(LaError::Unrepresentable {
3012                index: Some(0),
3013                reason: UnrepresentableReason::RequiresRounding,
3014            })
3015        );
3016    }
3017
3018    #[test]
3019    fn solve_exact_rounded_f64_rounds_non_dyadic_component() {
3020        let a = Matrix::<1>::try_from_rows([[3.0]]).unwrap();
3021        let b = Vector::<1>::new([1.0]);
3022
3023        assert_eq!(
3024            a.solve_exact_rounded_f64(b).unwrap().into_array()[0].to_bits(),
3025            (1.0f64 / 3.0).to_bits()
3026        );
3027    }
3028
3029    // -----------------------------------------------------------------------
3030    // exact solve boundary tests
3031    // -----------------------------------------------------------------------
3032
3033    #[test]
3034    fn gauss_solve_d1() {
3035        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
3036        let b = Vector::<1>::new([6.0]);
3037        let x = a.solve_exact(b).unwrap();
3038        assert_eq!(x[0], f64_to_bigrational(3.0));
3039    }
3040
3041    #[test]
3042    fn gauss_solve_singular_column_all_zero() {
3043        let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
3044            .unwrap();
3045        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
3046        assert_eq!(a.solve_exact(b), Err(LaError::Singular { pivot_col: 1 }));
3047    }
3048
3049    // -----------------------------------------------------------------------
3050    // f64_to_bigrational tests
3051    // -----------------------------------------------------------------------
3052
3053    #[test]
3054    fn f64_to_bigrational_positive_zero() {
3055        let r = f64_to_bigrational(0.0);
3056        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
3057    }
3058
3059    #[test]
3060    fn f64_to_bigrational_negative_zero() {
3061        let r = f64_to_bigrational(-0.0);
3062        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
3063    }
3064
3065    #[test]
3066    fn f64_to_bigrational_one() {
3067        let r = f64_to_bigrational(1.0);
3068        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
3069    }
3070
3071    #[test]
3072    fn f64_to_bigrational_negative_one() {
3073        let r = f64_to_bigrational(-1.0);
3074        assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
3075    }
3076
3077    #[test]
3078    fn f64_to_bigrational_half() {
3079        let r = f64_to_bigrational(0.5);
3080        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
3081    }
3082
3083    #[test]
3084    fn f64_to_bigrational_quarter() {
3085        let r = f64_to_bigrational(0.25);
3086        assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
3087    }
3088
3089    #[test]
3090    fn f64_to_bigrational_negative_three_and_a_half() {
3091        // -3.5 = -7/2
3092        let r = f64_to_bigrational(-3.5);
3093        assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
3094    }
3095
3096    #[test]
3097    fn f64_to_bigrational_integer() {
3098        let r = f64_to_bigrational(42.0);
3099        assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
3100    }
3101
3102    #[test]
3103    fn f64_to_bigrational_power_of_two() {
3104        let r = f64_to_bigrational(1024.0);
3105        assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
3106    }
3107
3108    #[test]
3109    fn f64_to_bigrational_subnormal() {
3110        let tiny = 5e-324_f64; // smallest positive subnormal
3111        assert!(tiny.is_subnormal());
3112        let r = f64_to_bigrational(tiny);
3113        // 5e-324 = 1 × 2^(-1074)
3114        assert_eq!(
3115            r,
3116            BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
3117        );
3118    }
3119
3120    #[test]
3121    fn f64_to_bigrational_already_lowest_terms() {
3122        // 0.5 should produce numer=1, denom=2 (already reduced).
3123        let r = f64_to_bigrational(0.5);
3124        assert_eq!(*r.numer(), BigInt::from(1));
3125        assert_eq!(*r.denom(), BigInt::from(2));
3126    }
3127
3128    #[test]
3129    fn f64_to_bigrational_round_trip() {
3130        // -0.0 is excluded: it maps to BigRational(0) which round-trips
3131        // to +0.0 (correct; tested separately in f64_to_bigrational_negative_zero).
3132        let values = [
3133            0.0,
3134            1.0,
3135            -1.0,
3136            0.5,
3137            0.25,
3138            0.1,
3139            42.0,
3140            -3.5,
3141            1e10,
3142            1e-10,
3143            f64::MAX / 2.0,
3144            f64::MIN_POSITIVE,
3145            5e-324,
3146        ];
3147        for &v in &values {
3148            let r = f64_to_bigrational(v);
3149            let back = r.to_f64().expect("round-trip to_f64 failed");
3150            assert!(
3151                v.to_bits() == back.to_bits(),
3152                "round-trip failed for {v}: got {back}"
3153            );
3154        }
3155    }
3156
3157    #[test]
3158    fn f64_decompose_rejects_nonfinite_inputs() {
3159        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3160            assert_eq!(
3161                f64_decompose(value),
3162                Err(LaError::NonFinite { row: None, col: 0 })
3163            );
3164        }
3165    }
3166}