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//! Exactness begins with the finite binary64 values already stored in
7//! [`Matrix`] and [`Vector`]: each value is lifted losslessly to a rational.
8//! These APIs cannot recover information rounded away before construction.
9//!
10//! # Architecture
11//!
12//! ## Determinants
13//!
14//! All determinant methods (`det_exact`, `det_exact_f64`,
15//! `det_exact_rounded_f64`, and `det_sign_exact`) share the same integer-scaled
16//! determinant core. Each proven-finite f64 entry is decomposed via
17//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`, then each row
18//! is scaled to a `BigInt` row (shifting by `e - e_row`, where `e_row` is
19//! that row's minimum non-zero exponent). D≤4 uses direct integer expansions; larger
20//! matrices use fraction-free Bareiss elimination \[7\] entirely in `BigInt`
21//! arithmetic — no `BigRational`, no GCD, no denominator tracking. The result
22//! is `(det_int, total_exp)` where `det = det_int × 2^Σ e_row`. `det_exact`
23//! wraps this with `big_int_exp_to_big_rational` to reconstruct a reduced
24//! `BigRational`; `det_exact_f64` converts the same pair only when the exact
25//! value is representable as finite binary64; `det_exact_rounded_f64` rounds
26//! the same exact value to finite binary64; and `det_sign_exact` reads the sign
27//! directly from `det_int` (the scale factor is always positive).
28//!
29//! `det_sign_exact` adds a two-stage adaptive-precision optimisation inspired
30//! by Shewchuk's robust geometric predicates \[8\]:
31//!
32//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
33//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
34//!    immediately without allocating.
35//! 2. **Exact fallback**: evaluate the scaled `BigInt` matrix directly for
36//!    D ≤ 4 or with Bareiss elimination for D ≥ 5, yielding a
37//!    guaranteed-correct sign.
38//!
39//! ## Linear system solve
40//!
41//! `solve_exact`, `solve_exact_f64`, and `solve_exact_rounded_f64` solve
42//! `A x = b` with a hybrid algorithm that shares the determinant path's exact
43//! integer scaling and then applies Bareiss elimination to the augmented
44//! system. Matrix and RHS entries are decomposed via
45//! `decompose_proven_finite_f64` into `mantissa × 2^exponent`. Each side first
46//! derives an independent scale from its minimum exponent. When those scales
47//! differ by at most `MAX_SHARED_SCALE_GAP_BITS` (64), both sides use the lower
48//! scale; larger gaps retain the independent scales. The resulting solution is
49//! adjusted by the exact power-of-two ratio between the selected scales. This
50//! shares common factors when inexpensive without inflating one side's integers
51//! across a large exponent gap.
52//! Forward elimination runs entirely in `BigInt` with
53//! fraction-free Bareiss updates \[7\] — no `BigRational`, no GCD
54//! normalisation in the `O(D³)` phase. The `O(D²)` back-substitution also
55//! accumulates in `BigInt` over a shared denominator. One reduction per solved
56//! row grows that denominator only as needed; final components are then
57//! canonicalized into `BigRational`.
58//! First-non-zero pivoting is used throughout;
59//! since all arithmetic is exact, any non-zero pivot gives the correct
60//! result (no numerical stability concern). Every finite `f64` is exactly
61//! representable as a rational, so the result is exact for the stored inputs.
62//! `solve_exact_f64` returns `Vector<D>` only when every exact component is
63//! exactly representable as finite binary64; `solve_exact_rounded_f64` returns
64//! the exact components rounded to finite binary64.
65//!
66//! ## f64 → integer decomposition
67//!
68//! Both the determinant and solve paths share a single conversion
69//! primitive, `decompose_proven_finite_f64`, which parses the IEEE 754 binary64 bit
70//! representation into a proof-bearing component (\[9\]). The
71//! determinant path combines those components into a `BigInt` matrix for
72//! direct expansion or Bareiss elimination and a `2^Σ e_row` scale factor,
73//! while the solve
74//! path builds a `BigInt` augmented system, solves for integer numerators
75//! over a common denominator, and constructs canonical `BigRational` outputs.
76//! See Goldberg \[10\] for background on floating-point representation
77//! and conversion. Reference numbers refer to
78//! `REFERENCES.md`.
79//!
80//! ## Exact-to-binary64 conversion
81//!
82//! Strict conversion checks dyadic representability, significand width, and
83//! exponent range. Integer-and-exponent rounding reads retained, guard, and
84//! sticky bits directly; rational-value rounding uses `num-rational`'s
85//! `ToPrimitive::to_f64`. Both implement the nearest-even output policy in
86//! `REFERENCES.md` \[9-10\]. See the
87//! [conversion criteria](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#exact-to-binary64-conversion)
88//! for subnormal, tie, and overflow cases.
89//!
90//! ## Validation
91//!
92//! Public `Matrix` / `Vector` values are finite by construction before exact
93//! methods reach the integer-scaled exact core. The decomposition helpers consume
94//! that proof without repeating stored-entry validation. Tests verify rejection
95//! at the owning constructors and check decomposition against independent
96//! `BigRational::from_f64` conversions.
97
98use core::hint::cold_path;
99use core::mem::take;
100use core::num::NonZeroU64;
101use std::array::from_fn;
102
103use num_bigint::{BigInt, Sign};
104use num_rational::BigRational;
105use num_traits::ToPrimitive;
106
107use crate::matrix::Matrix;
108use crate::rational::RationalVector;
109use crate::vector::Vector;
110use crate::{LaError, UnrepresentableReason};
111
112/// The exact sign of a determinant.
113///
114/// Available with the `exact` Cargo feature.
115///
116/// This type makes the three possible outcomes explicit instead of exposing a
117/// raw integer that could contain values other than −1, 0, or +1.
118///
119/// # Examples
120/// ```
121/// use la_stack::prelude::*;
122///
123/// let sign = Matrix::<2>::identity().det_sign_exact();
124/// assert_eq!(sign, DeterminantSign::Positive);
125/// assert_eq!(sign.as_i8(), 1);
126/// ```
127#[must_use]
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum DeterminantSign {
130    /// The determinant is strictly negative.
131    Negative,
132    /// The determinant is exactly zero.
133    Zero,
134    /// The determinant is strictly positive.
135    Positive,
136}
137
138impl DeterminantSign {
139    /// Return the conventional numeric sign −1, 0, or +1.
140    #[inline]
141    #[must_use]
142    pub const fn as_i8(self) -> i8 {
143        match self {
144            Self::Negative => -1,
145            Self::Zero => 0,
146            Self::Positive => 1,
147        }
148    }
149}
150
151/// Convert an already-computed exact result to finite binary64 output.
152///
153/// This extension trait is implemented for [`BigRational`] determinants,
154/// `[BigRational; D]` exact arrays, and [`crate::RationalVector`] solutions.
155/// It lets callers retain the exact value, try the strict no-rounding contract,
156/// and recover with explicit rounding without repeating determinant evaluation
157/// or linear-system elimination.
158/// [`BigRational::new_raw`] values are interpreted by their mathematical
159/// quotient: denominator signs and common factors do not change the result. A
160/// zero denominator is rejected as [`UnrepresentableReason::NotFinite`].
161///
162/// # Examples
163/// ```
164/// use la_stack::prelude::*;
165///
166/// # fn main() -> Result<(), LaError> {
167/// let matrix = Matrix::<2>::try_from_rows([
168///     [1.0 + f64::EPSILON, 0.0],
169///     [0.0, 1.0 - f64::EPSILON],
170/// ])?;
171/// let exact = matrix.det_exact()?;
172/// let rounded = match exact.try_to_f64() {
173///     Ok(value) => value,
174///     Err(error) if error.requires_rounding() => exact.to_rounded_f64()?,
175///     Err(error) => return Err(error),
176/// };
177/// assert_eq!(rounded.to_bits(), 1.0_f64.to_bits());
178///
179/// let system = Matrix::<1>::try_from_rows([[3.0]])?;
180/// let rhs = Vector::<1>::try_new([3.0])?;
181/// let exact_solution = system.solve_exact(rhs)?;
182/// assert_eq!(exact_solution.try_to_f64()?.into_array(), [1.0]);
183/// # Ok(())
184/// # }
185/// ```
186pub trait ExactF64Conversion {
187    /// Finite binary64 output produced by the conversion.
188    type Output;
189
190    /// Convert only when every exact value already has an exact finite
191    /// binary64 representation.
192    ///
193    /// The candidate conversion follows IEEE 754 round-to-nearest,
194    /// ties-to-even, but this strict method returns it only when no rounding is
195    /// required.
196    ///
197    /// # Errors
198    /// Returns [`LaError::Unrepresentable`] with
199    /// [`UnrepresentableReason::RequiresRounding`] when finite binary64 output
200    /// would require rounding, or [`UnrepresentableReason::NotFinite`] when
201    /// rounding cannot produce finite output. Exact solution errors include the
202    /// first failing component index.
203    fn try_to_f64(&self) -> Result<Self::Output, LaError>;
204
205    /// Round the exact value to finite binary64 output.
206    ///
207    /// Rounding follows IEEE 754 round-to-nearest, ties-to-even.
208    ///
209    /// # Errors
210    /// Returns [`LaError::Unrepresentable`] with
211    /// [`UnrepresentableReason::NotFinite`] when rounding cannot produce finite
212    /// output. Exact solution errors include the first failing component index.
213    fn to_rounded_f64(&self) -> Result<Self::Output, LaError>;
214}
215
216const F64_SIGNIFICAND_BITS: i64 = 53;
217const F64_FRACTION_BITS: i64 = 52;
218const F64_MIN_BINARY_EXPONENT: i64 = -1074;
219const F64_MIN_NORMAL_EXPONENT: i64 = -1022;
220const F64_MAX_BINARY_EXPONENT: i64 = 1023;
221const F64_EXPONENT_BIAS: i64 = 1023;
222const F64_FRACTION_MASK: u64 = (1u64 << 52) - 1;
223
224/// Decompose an `f64` whose finiteness has already been proven into its IEEE
225/// 754 components.
226///
227/// This helper is total for every bit pattern so proof-bearing callers never
228/// need to recover from a second finiteness check. Its result is meaningful as
229/// an exact real value only when `x` is finite.
230const fn decompose_proven_finite_f64(x: f64) -> Component {
231    let bits = x.to_bits();
232    let biased_exp = ((bits >> 52) & 0x7FF) as i32;
233    let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
234
235    // ±0.0
236    if biased_exp == 0 && fraction == 0 {
237        return Component::Zero;
238    }
239
240    let (mantissa, raw_exp) = if biased_exp == 0 {
241        // Subnormal: (-1)^s × 0.fraction × 2^(-1022)
242        //          = (-1)^s × fraction × 2^(-1074)
243        (fraction, -1074_i32)
244    } else {
245        // Normal: (-1)^s × 1.fraction × 2^(biased_exp - 1023)
246        //       = (-1)^s × (2^52 | fraction) × 2^(biased_exp - 1075)
247        ((1u64 << 52) | fraction, biased_exp - 1075)
248    };
249
250    // Strip trailing zeros so the mantissa is odd. The zero bit patterns
251    // returned above are the only finite values with a zero mantissa.
252    let tz = mantissa.trailing_zeros();
253    let Some(mantissa) = NonZeroU64::new(mantissa >> tz) else {
254        return Component::Zero;
255    };
256
257    Component::NonZero {
258        mantissa,
259        exponent: raw_exp + tz.cast_signed(),
260        is_negative: bits >> 63 != 0,
261    }
262}
263
264/// Convert a [`BigInt`] × `2^exp` pair to a reduced [`BigRational`].
265///
266/// When `exp < 0` (denominator is `2^(-exp)`), shared factors of 2 are
267/// stripped from `value` to keep the fraction in lowest terms without a
268/// full GCD computation.
269fn big_int_exp_to_big_rational(mut value: BigInt, mut exp: i32) -> BigRational {
270    if value == BigInt::from(0) {
271        return BigRational::from_integer(BigInt::from(0));
272    }
273
274    // Strip shared powers of 2 between value and the 2^(-exp) denominator.
275    if exp < 0
276        && let Some(tz) = value.trailing_zeros()
277    {
278        let exp_abs = exp.unsigned_abs();
279        let reduce = tz.min(u64::from(exp_abs));
280        value >>= reduce;
281        let remaining_abs = u64::from(exp_abs) - reduce;
282        exp = negative_exponent_from_magnitude(remaining_abs);
283    }
284
285    if exp >= 0 {
286        BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
287    } else {
288        BigRational::new_raw(value, BigInt::from(1u32) << exp.unsigned_abs())
289    }
290}
291
292/// Reconstruct a non-positive `i32` exponent from its unsigned magnitude.
293///
294/// The exact-conversion path can produce magnitude 2^31 for `i32::MIN`, which
295/// is one greater than `i32::MAX` and therefore cannot be converted before
296/// negation. Magnitudes derived from an `i32` never exceed that boundary.
297#[inline]
298fn negative_exponent_from_magnitude(magnitude: u64) -> i32 {
299    if magnitude == u64::from(i32::MIN.unsigned_abs()) {
300        return i32::MIN;
301    }
302
303    let Ok(value) = i32::try_from(magnitude) else {
304        cold_path();
305        unreachable!("negative exponent magnitude exceeds the i32 domain");
306    };
307    -value
308}
309
310/// Convert an exact rational result to `f64` only when the conversion is exact.
311///
312/// This supports the strict `*_exact_f64` public APIs by accepting only dyadic
313/// rational values that fit in finite binary64. The optional `index` is attached
314/// to [`LaError::Unrepresentable`] for vector-valued solve components.
315///
316/// # Errors
317/// Returns [`LaError::Unrepresentable`] with
318/// [`UnrepresentableReason::RequiresRounding`] when the value needs rounding
319/// but the rounded result would be finite, including non-dyadic values and
320/// dyadic values outside binary64's exact precision or exponent range.
321/// Returns [`UnrepresentableReason::NotFinite`] for a raw zero denominator or
322/// when rounding cannot produce a finite result.
323fn exact_rational_to_finite_f64(exact: &BigRational, index: Option<usize>) -> Result<f64, LaError> {
324    if exact.denom().sign() == Sign::NoSign {
325        cold_path();
326        return Err(LaError::unrepresentable(
327            index,
328            UnrepresentableReason::NotFinite,
329        ));
330    }
331
332    if exact.numer().sign() == Sign::NoSign {
333        return Ok(0.0);
334    }
335
336    let denominator = exact.denom();
337    if denominator.sign() == Sign::Plus
338        && let Some(denominator_exp) = positive_power_of_two_exponent(denominator)
339        && let Ok(denominator_exp) = i32::try_from(denominator_exp)
340    {
341        return big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
342            rounded_rational_unrepresentable_reason(exact)
343        });
344    }
345
346    // `BigRational::new_raw` can expose a negative denominator or uncancelled
347    // common factors. Normalization is necessary before deciding whether the
348    // mathematical quotient is dyadic. Canonical dyadic values take the
349    // borrowed fast path above and do not clone.
350    let reduced = exact.reduced();
351    reduced_rational_to_finite_f64(&reduced, index)
352}
353
354/// Return `k` exactly when `value` is the positive integer `2^k`.
355fn positive_power_of_two_exponent(value: &BigInt) -> Option<u64> {
356    if value.sign() != Sign::Plus {
357        return None;
358    }
359
360    let exponent = value.trailing_zeros()?;
361    (value.bits().checked_sub(1) == Some(exponent)).then_some(exponent)
362}
363
364/// Convert a canonical rational to finite binary64 without rounding.
365///
366/// Callers must provide a reduced value with a positive denominator, as
367/// guaranteed by [`RationalVector`] storage or by normalization in
368/// [`exact_rational_to_finite_f64`]. This lets canonical vectors avoid another
369/// reduction while raw rational inputs still pass through normalization when
370/// needed. The optional `index` identifies the failing solution component.
371///
372/// # Errors
373/// Returns [`LaError::Unrepresentable`] with
374/// [`UnrepresentableReason::RequiresRounding`] if only a rounded finite result
375/// is available, or [`UnrepresentableReason::NotFinite`] if rounding cannot
376/// produce a finite result.
377fn reduced_rational_to_finite_f64(
378    exact: &BigRational,
379    index: Option<usize>,
380) -> Result<f64, LaError> {
381    let Some(denominator_exp) = positive_power_of_two_exponent(exact.denom()) else {
382        cold_path();
383        return Err(LaError::unrepresentable(
384            index,
385            rounded_rational_unrepresentable_reason(exact),
386        ));
387    };
388    let Ok(denominator_exp) = i32::try_from(denominator_exp) else {
389        cold_path();
390        return Err(LaError::unrepresentable(
391            index,
392            rounded_rational_unrepresentable_reason(exact),
393        ));
394    };
395
396    big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
397        rounded_rational_unrepresentable_reason(exact)
398    })
399}
400
401/// Classify a failed exact-rational-to-`f64` conversion by the rounded result.
402///
403/// Strict exact conversion has already failed when this helper is called. It
404/// preserves the [`UnrepresentableReason`] recovery contract: callers may retry
405/// with a rounded API only when that rounded result would still be finite.
406fn rounded_rational_unrepresentable_reason(exact: &BigRational) -> UnrepresentableReason {
407    match exact.to_f64() {
408        Some(value) if value.is_finite() => UnrepresentableReason::RequiresRounding,
409        _ => UnrepresentableReason::NotFinite,
410    }
411}
412
413/// Convert an exact rational result to a rounded finite `f64` using IEEE 754
414/// round-to-nearest, ties-to-even.
415fn exact_rational_to_rounded_f64(
416    exact: &BigRational,
417    index: Option<usize>,
418) -> Result<f64, LaError> {
419    if exact.denom().sign() == Sign::NoSign {
420        cold_path();
421        return Err(LaError::unrepresentable(
422            index,
423            UnrepresentableReason::NotFinite,
424        ));
425    }
426    if exact.numer().sign() == Sign::NoSign {
427        return Ok(0.0);
428    }
429
430    let Some(value) = exact.to_f64() else {
431        cold_path();
432        return Err(LaError::unrepresentable(
433            index,
434            UnrepresentableReason::NotFinite,
435        ));
436    };
437    if value.is_finite() {
438        Ok(value)
439    } else {
440        cold_path();
441        Err(LaError::unrepresentable(
442            index,
443            UnrepresentableReason::NotFinite,
444        ))
445    }
446}
447
448impl ExactF64Conversion for BigRational {
449    type Output = f64;
450
451    #[inline]
452    fn try_to_f64(&self) -> Result<Self::Output, LaError> {
453        exact_rational_to_finite_f64(self, None)
454    }
455
456    #[inline]
457    fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
458        exact_rational_to_rounded_f64(self, None)
459    }
460}
461
462impl<const D: usize> ExactF64Conversion for [BigRational; D] {
463    type Output = Vector<D>;
464
465    #[inline]
466    fn try_to_f64(&self) -> Result<Self::Output, LaError> {
467        let mut result = [0.0; D];
468        for (index, value) in self.iter().enumerate() {
469            result[index] = exact_rational_to_finite_f64(value, Some(index))?;
470        }
471        Vector::try_new(result)
472    }
473
474    #[inline]
475    fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
476        let mut result = [0.0; D];
477        for (index, value) in self.iter().enumerate() {
478            result[index] = exact_rational_to_rounded_f64(value, Some(index))?;
479        }
480        Vector::try_new(result)
481    }
482}
483
484impl<const D: usize> ExactF64Conversion for RationalVector<D> {
485    type Output = Vector<D>;
486
487    #[inline]
488    fn try_to_f64(&self) -> Result<Self::Output, LaError> {
489        let mut result = [0.0; D];
490        for (index, value) in self.as_array().iter().enumerate() {
491            // Canonical storage already proves reduction and a positive denominator.
492            result[index] = reduced_rational_to_finite_f64(value, Some(index))?;
493        }
494        Vector::try_new(result)
495    }
496
497    #[inline]
498    fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
499        self.as_array().to_rounded_f64()
500    }
501}
502
503/// Extract the low 64 bits of `|value| >> shift` without allocating.
504///
505/// Discards the sign and bits outside this window; callers must establish that
506/// the shifted magnitude fits when they need the whole value. Returns `None`
507/// when the starting word is absent or its index does not fit in `usize`.
508fn shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
509    let word_bits = u64::from(u64::BITS);
510    let word_index = usize::try_from(shift / word_bits).ok()?;
511    let bit_shift = u32::try_from(shift % word_bits).ok()?;
512    let mut digits = value.iter_u64_digits().skip(word_index);
513    let low = digits.next()? >> bit_shift;
514    if bit_shift == 0 {
515        Some(low)
516    } else {
517        let high = digits.next().unwrap_or(0) << (u64::BITS - bit_shift);
518        Some(low | high)
519    }
520}
521
522/// Return whether a bit is set in the magnitude of `value`.
523fn magnitude_bit_is_set(value: &BigInt, bit: u64) -> bool {
524    let word_bits = u64::from(u64::BITS);
525    let Ok(word_index) = usize::try_from(bit / word_bits) else {
526        return false;
527    };
528    let bit_index = u32::try_from(bit % word_bits).unwrap_or(0);
529    value
530        .iter_u64_digits()
531        .nth(word_index)
532        .is_some_and(|word| word & (1_u64 << bit_index) != 0)
533}
534
535/// Return whether any magnitude bit below `exclusive_end` is set.
536fn magnitude_has_lower_bits(value: &BigInt, exclusive_end: u64) -> bool {
537    let word_bits = u64::from(u64::BITS);
538    let Ok(full_words) = usize::try_from(exclusive_end / word_bits) else {
539        return value.sign() != Sign::NoSign;
540    };
541    let partial_bits = u32::try_from(exclusive_end % word_bits).unwrap_or(0);
542    let mut digits = value.iter_u64_digits();
543
544    for _ in 0..full_words {
545        if digits.next().unwrap_or(0) != 0 {
546            return true;
547        }
548    }
549
550    if partial_bits == 0 {
551        false
552    } else {
553        let mask = (1_u64 << partial_bits) - 1;
554        digits.next().is_some_and(|word| word & mask != 0)
555    }
556}
557
558/// Right-shift a magnitude and round the retained integer to nearest-even.
559///
560/// The guard bit rounds upward exactly when lower discarded bits are nonzero
561/// or the retained integer is odd. This is the IEEE 754 tie rule from
562/// `REFERENCES.md` \[9-10\], applied before binary64 exponent assembly.
563fn rounded_shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
564    if shift > value.bits() {
565        return Some(0);
566    }
567    let retained = shifted_magnitude_to_u64(value, shift).unwrap_or(0);
568    if shift == 0 {
569        return Some(retained);
570    }
571
572    let guard_bit = shift - 1;
573    let increment = magnitude_bit_is_set(value, guard_bit)
574        && (magnitude_has_lower_bits(value, guard_bit) || retained & 1 != 0);
575    retained.checked_add(u64::from(increment))
576}
577
578/// Classify an inexact integer conversion, evaluating rounding only in the
579/// maximum exponent bin where it can overflow to infinity.
580#[inline]
581fn inexact_big_int_reason(
582    top_bit_exp: i64,
583    rounded_reason: impl FnOnce() -> UnrepresentableReason,
584) -> UnrepresentableReason {
585    if top_bit_exp < F64_MAX_BINARY_EXPONENT {
586        UnrepresentableReason::RequiresRounding
587    } else {
588        rounded_reason()
589    }
590}
591
592/// Round a `BigInt × 2^exp` pair directly to finite binary64.
593///
594/// The implementation reads only the magnitude bits needed for the binary64
595/// significand and rounding decision. It therefore avoids constructing a
596/// potentially enormous [`BigRational`] denominator for very negative
597/// exponents.
598fn big_int_exp_ref_to_rounded_f64(
599    value: &BigInt,
600    exp: i32,
601    index: Option<usize>,
602) -> Result<f64, LaError> {
603    if value.sign() == Sign::NoSign {
604        return Ok(0.0);
605    }
606
607    let sign = if value.sign() == Sign::Minus {
608        1_u64 << 63
609    } else {
610        0
611    };
612    let Ok(bit_len) = i64::try_from(value.bits()) else {
613        cold_path();
614        return Err(LaError::unrepresentable(
615            index,
616            UnrepresentableReason::NotFinite,
617        ));
618    };
619    let Some(mut top_bit_exp) = i64::from(exp).checked_add(bit_len - 1) else {
620        cold_path();
621        return Err(LaError::unrepresentable(
622            index,
623            UnrepresentableReason::NotFinite,
624        ));
625    };
626    if top_bit_exp > F64_MAX_BINARY_EXPONENT {
627        cold_path();
628        return Err(LaError::unrepresentable(
629            index,
630            UnrepresentableReason::NotFinite,
631        ));
632    }
633
634    if top_bit_exp >= F64_MIN_NORMAL_EXPONENT {
635        let mut significand = if bit_len <= F64_SIGNIFICAND_BITS {
636            let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
637                cold_path();
638                unreachable!("nonzero integer must expose magnitude digits");
639            };
640            let shift = u32::try_from(F64_SIGNIFICAND_BITS - bit_len)
641                .unwrap_or_else(|_| unreachable!("normal significand shift must fit u32"));
642            magnitude
643                .checked_shl(shift)
644                .unwrap_or_else(|| unreachable!("normal significand must fit u64"))
645        } else {
646            let shift = u64::try_from(bit_len - F64_SIGNIFICAND_BITS)
647                .unwrap_or_else(|_| unreachable!("positive significand shift must fit u64"));
648            rounded_shifted_magnitude_to_u64(value, shift)
649                .unwrap_or_else(|| unreachable!("rounded binary64 significand must fit u64"))
650        };
651
652        if significand == 1_u64 << F64_SIGNIFICAND_BITS {
653            significand >>= 1;
654            top_bit_exp += 1;
655        }
656        if top_bit_exp > F64_MAX_BINARY_EXPONENT {
657            cold_path();
658            return Err(LaError::unrepresentable(
659                index,
660                UnrepresentableReason::NotFinite,
661            ));
662        }
663
664        let biased_exp = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS)
665            .unwrap_or_else(|_| unreachable!("normal exponent must be positive"));
666        return Ok(f64::from_bits(
667            sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
668        ));
669    }
670
671    let subnormal_shift = i64::from(exp) - F64_MIN_BINARY_EXPONENT;
672    let significand = if subnormal_shift >= 0 {
673        let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
674            cold_path();
675            unreachable!("nonzero integer must expose magnitude digits");
676        };
677        let shift = u32::try_from(subnormal_shift)
678            .unwrap_or_else(|_| unreachable!("subnormal left shift must fit u32"));
679        magnitude
680            .checked_shl(shift)
681            .unwrap_or_else(|| unreachable!("subnormal significand must fit u64"))
682    } else {
683        let shift = u64::try_from(-subnormal_shift)
684            .unwrap_or_else(|_| unreachable!("subnormal right shift must fit u64"));
685        rounded_shifted_magnitude_to_u64(value, shift)
686            .unwrap_or_else(|| unreachable!("rounded subnormal significand must fit u64"))
687    };
688
689    // Rounding up to 2^52 naturally sets the smallest normal exponent bit.
690    Ok(f64::from_bits(sign | significand))
691}
692
693/// Borrowed core for exact integer-and-exponent conversion.
694///
695/// The normalized significand is read directly from the [`BigInt`] digits, so
696/// successful strict conversion does not clone an already-computed exact
697/// result. `rounded_reason` is evaluated only when finite output would require
698/// rounding.
699/// After stripping trailing zeros, an odd magnitude with bit length `b` is
700/// representable exactly when `b ≤ 53`, `exp ≥ -1074`, and
701/// `exp + b - 1 ≤ 1023`; see `REFERENCES.md` \[9-10\].
702fn big_int_exp_ref_to_finite_f64(
703    value: &BigInt,
704    exp: i32,
705    index: Option<usize>,
706    rounded_reason: impl FnOnce() -> UnrepresentableReason,
707) -> Result<f64, LaError> {
708    if value.sign() == Sign::NoSign {
709        return Ok(0.0);
710    }
711
712    let is_negative = value.sign() == Sign::Minus;
713    let mut exp = i64::from(exp);
714    let Some(trailing_zeros) = value.trailing_zeros() else {
715        cold_path();
716        unreachable!("nonzero integer must have a least-significant set bit");
717    };
718    let Ok(trailing_zeros_i64) = i64::try_from(trailing_zeros) else {
719        cold_path();
720        return Err(LaError::unrepresentable(
721            index,
722            UnrepresentableReason::NotFinite,
723        ));
724    };
725    let Some(updated_exp) = exp.checked_add(trailing_zeros_i64) else {
726        cold_path();
727        return Err(LaError::unrepresentable(
728            index,
729            UnrepresentableReason::NotFinite,
730        ));
731    };
732    exp = updated_exp;
733
734    let Some(bit_len) = value.bits().checked_sub(trailing_zeros) else {
735        cold_path();
736        unreachable!("trailing-zero count cannot exceed integer bit length");
737    };
738    let Ok(bit_len) = i64::try_from(bit_len) else {
739        cold_path();
740        return Err(LaError::unrepresentable(
741            index,
742            UnrepresentableReason::NotFinite,
743        ));
744    };
745    let Some(top_bit_exp) = exp.checked_add(bit_len - 1) else {
746        cold_path();
747        return Err(LaError::unrepresentable(
748            index,
749            UnrepresentableReason::NotFinite,
750        ));
751    };
752    if top_bit_exp > F64_MAX_BINARY_EXPONENT {
753        cold_path();
754        return Err(LaError::unrepresentable(
755            index,
756            UnrepresentableReason::NotFinite,
757        ));
758    }
759    if exp < F64_MIN_BINARY_EXPONENT {
760        cold_path();
761        // A low least-significant exponent normally rounds to a finite value,
762        // but a very wide integer in the maximum exponent bin can round up to
763        // infinity.
764        let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
765        return Err(LaError::unrepresentable(index, reason));
766    }
767    if bit_len > F64_SIGNIFICAND_BITS {
768        cold_path();
769        // Rounding can overflow only when the exact value already occupies the
770        // maximum binary64 exponent bin. Avoid the full rounding calculation
771        // for every ordinary inexact conversion.
772        let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
773        return Err(LaError::unrepresentable(index, reason));
774    }
775
776    let Some(mantissa) = shifted_magnitude_to_u64(value, trailing_zeros) else {
777        cold_path();
778        return Err(LaError::unrepresentable(
779            index,
780            UnrepresentableReason::NotFinite,
781        ));
782    };
783    let sign = if is_negative { 1u64 << 63 } else { 0 };
784
785    if top_bit_exp < F64_MIN_NORMAL_EXPONENT {
786        let Ok(shift) = u32::try_from(exp - F64_MIN_BINARY_EXPONENT) else {
787            cold_path();
788            return Err(LaError::unrepresentable(
789                index,
790                UnrepresentableReason::RequiresRounding,
791            ));
792        };
793        Ok(f64::from_bits(sign | (mantissa << shift)))
794    } else {
795        let Ok(biased_exp) = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS) else {
796            cold_path();
797            return Err(LaError::unrepresentable(
798                index,
799                UnrepresentableReason::NotFinite,
800            ));
801        };
802        let Ok(shift) = u32::try_from(F64_FRACTION_BITS - (bit_len - 1)) else {
803            cold_path();
804            return Err(LaError::unrepresentable(
805                index,
806                UnrepresentableReason::RequiresRounding,
807            ));
808        };
809        let significand = mantissa << shift;
810        Ok(f64::from_bits(
811            sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
812        ))
813    }
814}
815
816fn big_int_exp_to_finite_f64(
817    value: &BigInt,
818    exp: i32,
819    index: Option<usize>,
820) -> Result<f64, LaError> {
821    big_int_exp_ref_to_finite_f64(value, exp, index, || {
822        match big_int_exp_ref_to_rounded_f64(value, exp, index) {
823            Ok(_) => UnrepresentableReason::RequiresRounding,
824            Err(_) => UnrepresentableReason::NotFinite,
825        }
826    })
827}
828
829/// Convert a `BigInt × 2^exp` determinant pair to a rounded finite `f64`.
830fn big_int_exp_to_rounded_f64(value: &BigInt, exp: i32) -> Result<f64, LaError> {
831    big_int_exp_ref_to_rounded_f64(value, exp, None)
832}
833
834// -----------------------------------------------------------------------
835// Shared integer-scaling and Bareiss primitives
836// -----------------------------------------------------------------------
837//
838// Both `exact_det_int_finite` (determinants) and `bareiss_solve_finite` (linear
839// systems) parse every f64 entry into a proof-bearing component, track the
840// minimum exponent across non-zero entries in each scaling domain, and scale
841// each entry by `2^(exp − e_min)`. Determinants scale each row independently,
842// then use direct expansions for D≤4 and
843// fraction-free Bareiss elimination for D≥5. Solves derive matrix and RHS scales
844// independently, share the lower scale when their gap is at most 64 bits, use
845// Bareiss elimination on the augmented system, and restore the selected scales'
846// exact power-of-two ratio after integer back-substitution and reduction.
847
848/// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`.
849///
850/// `Zero` represents ±0.0. Non-zero entries carry a [`NonZeroU64`] mantissa, so
851/// the exact-arithmetic paths cannot accidentally combine an absent mantissa
852/// with active exponent/sign fields after decomposition.
853#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
854enum Component {
855    #[default]
856    Zero,
857    NonZero {
858        mantissa: NonZeroU64,
859        exponent: i32,
860        is_negative: bool,
861    },
862}
863
864impl Component {
865    /// Return the exponent carried by a non-zero component.
866    const fn exponent(self) -> Option<i32> {
867        match self {
868            Self::Zero => None,
869            Self::NonZero { exponent, .. } => Some(exponent),
870        }
871    }
872}
873
874mod decomposition {
875    use super::Component;
876
877    /// A component collection paired with its proven minimum non-zero exponent.
878    ///
879    /// `None` represents an all-zero collection; no sentinel exponent is stored.
880    /// Private fields prevent callers from supplying components and their proof
881    /// independently.
882    #[derive(Clone, Debug, Eq, PartialEq)]
883    pub(super) struct Decomposed<T> {
884        components: T,
885        min_exponent: Option<i32>,
886    }
887
888    impl<T> Decomposed<T> {
889        /// Borrow the parsed components.
890        pub(super) const fn components(&self) -> &T {
891            &self.components
892        }
893
894        /// Return the minimum exponent, or `None` when every component is zero.
895        pub(super) const fn min_exponent(&self) -> Option<i32> {
896            self.min_exponent
897        }
898    }
899
900    impl<const D: usize> Decomposed<[Component; D]> {
901        /// Derive a vector decomposition and its proof together.
902        pub(super) fn from_vector_components(components: [Component; D]) -> Self {
903            let min_exponent = components
904                .iter()
905                .filter_map(|component| component.exponent())
906                .min();
907            Self {
908                components,
909                min_exponent,
910            }
911        }
912    }
913
914    impl<const D: usize> Decomposed<[[Component; D]; D]> {
915        /// Derive a matrix decomposition and its proof together.
916        pub(super) fn from_matrix_components(components: [[Component; D]; D]) -> Self {
917            let min_exponent = components
918                .iter()
919                .flatten()
920                .filter_map(|component| component.exponent())
921                .min();
922            Self {
923                components,
924                min_exponent,
925            }
926        }
927    }
928
929    /// A scaling exponent derived from a component collection's minimum.
930    ///
931    /// The private field prevents raw construction outside this proof-owning
932    /// module.
933    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
934    pub(super) struct ScaleExponent {
935        value: i32,
936    }
937
938    impl ScaleExponent {
939        /// Canonical scale for an empty or all-zero component collection.
940        pub(super) const ZERO: Self = Self { value: 0 };
941
942        /// Select a scale for one decomposed collection.
943        pub(super) const fn for_decomposed<T>(decomposed: &Decomposed<T>) -> Self {
944            let value = match decomposed.min_exponent() {
945                Some(exponent) => exponent,
946                None => 0,
947            };
948            Self { value }
949        }
950
951        /// Select the lower of two already-derived collection scales.
952        pub(super) const fn min(self, other: Self) -> Self {
953            if self.value < other.value {
954                self
955            } else {
956                other
957            }
958        }
959
960        /// Return the proven collection exponent.
961        pub(super) const fn get(self) -> i32 {
962            self.value
963        }
964
965        /// Compute a non-negative shift from this proven collection exponent.
966        ///
967        /// # Panics
968        /// Panics only if a private decomposition invariant is broken and an entry
969        /// exponent is lower than the common minimum.
970        pub(super) fn shift_for(self, exponent: i32) -> u32 {
971            let Some(shift) = exponent.checked_sub(self.value) else {
972                unreachable!("finite f64 exponent difference cannot overflow");
973            };
974            let Ok(shift) = u32::try_from(shift) else {
975                unreachable!("scale exponent cannot exceed a component exponent");
976            };
977            shift
978        }
979    }
980}
981
982use decomposition::{Decomposed, ScaleExponent};
983
984/// Decompose a matrix whose finite-storage invariant has already been proven.
985fn decompose_proven_finite_matrix<const D: usize>(
986    m: &Matrix<D>,
987) -> Decomposed<[[Component; D]; D]> {
988    let components =
989        from_fn(|row| from_fn(|col| decompose_proven_finite_f64(m.as_rows()[row][col])));
990    Decomposed::from_matrix_components(components)
991}
992
993/// Decompose a vector whose finite-storage invariant has already been proven.
994fn decompose_proven_finite_vector<const D: usize>(v: &Vector<D>) -> Decomposed<[Component; D]> {
995    let components = from_fn(|index| decompose_proven_finite_f64(v.as_array()[index]));
996    Decomposed::from_vector_components(components)
997}
998
999/// Convert a single decomposed component to its scaled `BigInt`
1000/// representation: `(±mantissa) << (exp − e_min)`.
1001#[inline]
1002fn component_to_big_int(component: Component, scale: ScaleExponent) -> BigInt {
1003    match component {
1004        Component::Zero => BigInt::from(0),
1005        Component::NonZero {
1006            mantissa,
1007            exponent,
1008            is_negative,
1009        } => {
1010            let value = BigInt::from(mantissa.get()) << scale.shift_for(exponent);
1011            if is_negative { -value } else { value }
1012        }
1013    }
1014}
1015
1016/// Build a `D×D` integer matrix from components, scaled to a shared base.
1017fn build_big_int_matrix<const D: usize>(
1018    components: &[[Component; D]; D],
1019    scale: ScaleExponent,
1020) -> [[BigInt; D]; D] {
1021    from_fn(|row| from_fn(|col| component_to_big_int(components[row][col], scale)))
1022}
1023
1024/// Build a length-`D` integer vector from components, scaled to a shared base.
1025fn build_big_int_vec<const D: usize>(
1026    components: &[Component; D],
1027    scale: ScaleExponent,
1028) -> [BigInt; D] {
1029    from_fn(|index| component_to_big_int(components[index], scale))
1030}
1031
1032/// Compute a 2×2 determinant from a scaled integer matrix.
1033#[inline]
1034fn det2_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1035    &a[0][0] * &a[1][1] - &a[0][1] * &a[1][0]
1036}
1037
1038/// Compute an exact 3×3 determinant from borrowed scaled-integer entries.
1039///
1040/// This fixed-shape kernel serves both the direct D=3 determinant path and the
1041/// 3×3 minors used by the D=4 expansion. Borrowing entries avoids cloning
1042/// [`BigInt`] values while keeping every operation in exact integer arithmetic.
1043#[inline]
1044fn det3_big_int_entries(a: [[&BigInt; 3]; 3]) -> BigInt {
1045    let m00 = a[1][1] * a[2][2] - a[1][2] * a[2][1];
1046    let m01 = a[1][0] * a[2][2] - a[1][2] * a[2][0];
1047    let m02 = a[1][0] * a[2][1] - a[1][1] * a[2][0];
1048    a[0][0] * m00 - a[0][1] * m01 + a[0][2] * m02
1049}
1050
1051/// Compute a 3×3 determinant from a scaled integer matrix.
1052#[inline]
1053fn det3_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1054    det3_big_int_entries([
1055        [&a[0][0], &a[0][1], &a[0][2]],
1056        [&a[1][0], &a[1][1], &a[1][2]],
1057        [&a[2][0], &a[2][1], &a[2][2]],
1058    ])
1059}
1060
1061/// Compute a 4×4 determinant from a scaled integer matrix.
1062///
1063/// [`det_big_int`] dispatches here only for D=4. When every first-row entry is
1064/// non-zero, sharing six lower-row minors avoids repeating their products;
1065/// otherwise, separate cofactors skip work for zero entries. Both expansions
1066/// use exact [`BigInt`] arithmetic, preserving the determinant value and sign
1067/// required by the public exact APIs.
1068#[inline]
1069fn det4_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
1070    if a[0][..4].iter().all(|value| value.sign() != Sign::NoSign) {
1071        // Six lower-row minors serve all four cofactors. Consume each
1072        // temporary on its last use so BigInt can reuse its storage.
1073        let m01 = &a[2][0] * &a[3][1] - &a[2][1] * &a[3][0];
1074        let m02 = &a[2][0] * &a[3][2] - &a[2][2] * &a[3][0];
1075        let m03 = &a[2][0] * &a[3][3] - &a[2][3] * &a[3][0];
1076        let m12 = &a[2][1] * &a[3][2] - &a[2][2] * &a[3][1];
1077        let m13 = &a[2][1] * &a[3][3] - &a[2][3] * &a[3][1];
1078        let m23 = &a[2][2] * &a[3][3] - &a[2][3] * &a[3][2];
1079        let c00 = &a[1][1] * &m23 - &a[1][2] * &m13 + &a[1][3] * &m12;
1080        let mut det = &a[0][0] * c00;
1081        let c01 = &a[1][0] * m23 - &a[1][2] * &m03 + &a[1][3] * &m02;
1082        det -= &a[0][1] * c01;
1083        let c02 = &a[1][0] * m13 - &a[1][1] * m03 + &a[1][3] * &m01;
1084        det += &a[0][2] * c02;
1085        let c03 = &a[1][0] * m12 - &a[1][1] * m02 + &a[1][2] * m01;
1086        return det - &a[0][3] * c03;
1087    }
1088
1089    let mut det = BigInt::from(0);
1090
1091    if a[0][0].sign() != Sign::NoSign {
1092        let c00 = det3_big_int_entries([
1093            [&a[1][1], &a[1][2], &a[1][3]],
1094            [&a[2][1], &a[2][2], &a[2][3]],
1095            [&a[3][1], &a[3][2], &a[3][3]],
1096        ]);
1097        det += &a[0][0] * c00;
1098    }
1099    if a[0][1].sign() != Sign::NoSign {
1100        let c01 = det3_big_int_entries([
1101            [&a[1][0], &a[1][2], &a[1][3]],
1102            [&a[2][0], &a[2][2], &a[2][3]],
1103            [&a[3][0], &a[3][2], &a[3][3]],
1104        ]);
1105        det -= &a[0][1] * c01;
1106    }
1107    if a[0][2].sign() != Sign::NoSign {
1108        let c02 = det3_big_int_entries([
1109            [&a[1][0], &a[1][1], &a[1][3]],
1110            [&a[2][0], &a[2][1], &a[2][3]],
1111            [&a[3][0], &a[3][1], &a[3][3]],
1112        ]);
1113        det += &a[0][2] * c02;
1114    }
1115    if a[0][3].sign() != Sign::NoSign {
1116        let c03 = det3_big_int_entries([
1117            [&a[1][0], &a[1][1], &a[1][2]],
1118            [&a[2][0], &a[2][1], &a[2][2]],
1119            [&a[3][0], &a[3][1], &a[3][2]],
1120        ]);
1121        det -= &a[0][3] * c03;
1122    }
1123
1124    det
1125}
1126
1127/// Compute the determinant of an integer matrix with direct expansions for
1128/// D≤4 and fraction-free Bareiss elimination otherwise.
1129pub(crate) fn det_big_int<const D: usize>(mut a: [[BigInt; D]; D]) -> BigInt {
1130    if D == 0 {
1131        return BigInt::from(1);
1132    }
1133
1134    match D {
1135        1 => take(&mut a[0][0]),
1136        2 => det2_big_int(&a),
1137        3 => det3_big_int(&a),
1138        4 => det4_big_int(&a),
1139        _ => {
1140            let odd_swaps = match bareiss_forward_eliminate(&mut a, None) {
1141                BareissResult::Upper { odd_swaps } => odd_swaps,
1142                BareissResult::Singular { .. } => {
1143                    cold_path();
1144                    return BigInt::from(0);
1145                }
1146            };
1147
1148            let determinant = take(&mut a[D - 1][D - 1]);
1149            if odd_swaps { -determinant } else { determinant }
1150        }
1151    }
1152}
1153
1154/// Outcome of a Bareiss forward-elimination pass.
1155#[derive(Debug)]
1156enum BareissResult {
1157    /// Elimination completed; `odd_swaps` records the parity of row
1158    /// swaps (relevant for determinants; solves discard it).
1159    Upper { odd_swaps: bool },
1160    /// Column `pivot_col` has no non-zero pivot at or below its diagonal.
1161    Singular { pivot_col: usize },
1162}
1163
1164/// Run Bareiss fraction-free forward elimination on the `D×D` integer
1165/// matrix `a`, optionally augmented with a length-`D` RHS vector.
1166///
1167/// When `rhs` is `Some`, row swaps and the inner-loop Bareiss update are
1168/// mirrored on the RHS (treating it as column `D+1` of an augmented
1169/// system). On [`BareissResult::Upper`], `a` is upper triangular with non-zero
1170/// diagonal entries; for D>0, the last pivot lives in `a[D-1][D-1]`.
1171/// [`BareissResult::Singular`] may leave `a` and `rhs` partially eliminated.
1172/// Callers own these scratch arrays and discard them on failure; no rollback
1173/// is performed.
1174///
1175/// First-non-zero pivoting is used: since all arithmetic is exact, any
1176/// non-zero pivot is valid — no tolerance is required.
1177fn bareiss_forward_eliminate<const D: usize>(
1178    a: &mut [[BigInt; D]; D],
1179    mut rhs: Option<&mut [BigInt; D]>,
1180) -> BareissResult {
1181    let zero = BigInt::from(0);
1182    let mut prev_pivot = BigInt::from(1);
1183    let mut odd_swaps = false;
1184
1185    for k in 0..D {
1186        // First-non-zero pivot search.
1187        if a[k][k] == zero {
1188            let mut found = false;
1189            for i in (k + 1)..D {
1190                if a[i][k] != zero {
1191                    a.swap(k, i);
1192                    if let Some(r) = &mut rhs {
1193                        r.swap(k, i);
1194                    }
1195                    odd_swaps = !odd_swaps;
1196                    found = true;
1197                    break;
1198                }
1199            }
1200            if !found {
1201                cold_path();
1202                return BareissResult::Singular { pivot_col: k };
1203            }
1204        }
1205
1206        // The final pivot has now been proven non-zero. There are no rows or
1207        // columns left to eliminate, and `prev_pivot` would never be read again.
1208        if k + 1 == D {
1209            break;
1210        }
1211
1212        // Elimination.  The Bareiss update reads the current `a[i][k]`
1213        // in both the inner `j`-loop and the RHS update, so zero it only
1214        // *after* those reads.
1215        for i in (k + 1)..D {
1216            for j in (k + 1)..D {
1217                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
1218            }
1219            if let Some(r) = &mut rhs {
1220                r[i] = (&a[k][k] * &r[i] - &a[i][k] * &r[k]) / &prev_pivot;
1221            }
1222            a[i][k].clone_from(&zero);
1223        }
1224
1225        prev_pivot.clone_from(&a[k][k]);
1226    }
1227
1228    // Post-conditions (debug builds only): `a` is upper triangular with
1229    // non-zero pivots.  These catch future regressions in the inner-loop
1230    // update or pivot-search logic without runtime cost in release.
1231    #[cfg(debug_assertions)]
1232    for (k, row) in a.iter().enumerate() {
1233        assert_ne!(row[k], zero, "pivot at ({k}, {k}) must be non-zero");
1234        for (i, lower_row) in a.iter().enumerate().skip(k + 1) {
1235            assert_eq!(
1236                lower_row[k], zero,
1237                "sub-diagonal at ({i}, {k}) must be zero"
1238            );
1239        }
1240    }
1241
1242    BareissResult::Upper { odd_swaps }
1243}
1244
1245/// Check the sum of the determinant's row scale exponents.
1246///
1247/// This centralizes the scale-overflow classification used by exact
1248/// determinant value APIs. Sign-only evaluation deliberately bypasses this
1249/// bookkeeping because a positive binary scale cannot change determinant sign.
1250///
1251/// # Errors
1252/// Returns [`LaError::DeterminantScaleOverflow`] if the final sum does not fit
1253/// the internal `i32` exponent representation. The error retains the minimum
1254/// non-zero entry exponent across the original matrix.
1255fn determinant_scale_exp<const D: usize>(total: i64, e_min: i32) -> Result<i32, LaError> {
1256    let Ok(total_exp) = i32::try_from(total) else {
1257        cold_path();
1258        return Err(LaError::determinant_scale_overflow(D, e_min));
1259    };
1260    Ok(total_exp)
1261}
1262
1263/// Compute the determinant integer and its independent row scales.
1264///
1265/// Returns `(det_int, scales)` where the true determinant is
1266/// `det_int × 2^Σ scales`. Since that scale factor is always positive,
1267/// callers interested only in the sign do not need to sum the scales.
1268///
1269/// All arithmetic is in `BigInt` — no `BigRational`, no GCD, no denominator
1270/// tracking.  Each f64 entry is decomposed into `mantissa × 2^exponent` and
1271/// scaled to its row's base `2^e_row` so every entry becomes an integer. D≤4
1272/// uses direct determinant expansions; larger matrices use Bareiss elimination
1273/// whose inner-loop division is exact (guaranteed by the algorithm).
1274///
1275#[expect(
1276    clippy::inline_always,
1277    reason = "keep the measured row-scaling prototype's fixed-dimension code generation"
1278)]
1279#[inline(always)]
1280fn scaled_det_int_finite<const D: usize>(m: &Matrix<D>) -> (BigInt, [ScaleExponent; D]) {
1281    let mut scales = [ScaleExponent::ZERO; D];
1282    let mut has_zero_row = false;
1283    let a: [[BigInt; D]; D] = from_fn(|row| {
1284        let decomposed = Decomposed::from_vector_components(from_fn(|column| {
1285            decompose_proven_finite_f64(m.as_rows()[row][column])
1286        }));
1287        has_zero_row |= decomposed.min_exponent().is_none();
1288        let scale = ScaleExponent::for_decomposed(&decomposed);
1289        scales[row] = scale;
1290        build_big_int_vec(decomposed.components(), scale)
1291    });
1292    if has_zero_row {
1293        return (BigInt::from(0), scales);
1294    }
1295    (det_big_int(a), scales)
1296}
1297
1298/// Compute the exact determinant as an integer plus one total binary scale.
1299///
1300/// Zero determinants use exponent zero because their value is independent of
1301/// scale. Non-zero determinants validate `Σ e_row` for the value-producing
1302/// exact APIs; sign-only callers use [`scaled_det_int_finite`] directly.
1303fn exact_det_int_finite<const D: usize>(m: &Matrix<D>) -> Result<(BigInt, i32), LaError> {
1304    let (det_int, scales) = scaled_det_int_finite(m);
1305    if det_int.sign() == Sign::NoSign {
1306        return Ok((det_int, 0));
1307    }
1308    // Sum in a wider type so cancellation cannot cause intermediate overflow.
1309    // On Rust's 16/32/64-bit targets, any addressable D×D binary64 matrix has
1310    // D < 2^31. Row exponents lie in [-1074, 1023], so every partial sum has
1311    // magnitude below 2^42 and fits in i64, even when the final i32 sum does not.
1312    let total = scales.iter().map(|scale| i64::from(scale.get())).sum();
1313    let minimum = scales.iter().map(|scale| scale.get()).min().unwrap_or(0);
1314    let total_exp = determinant_scale_exp::<D>(total, minimum)?;
1315    Ok((det_int, total_exp))
1316}
1317
1318/// Compute the exact determinant of a `D×D` matrix using direct `BigInt`
1319/// expansions for D≤4 or integer-only Bareiss elimination for D≥5, then return
1320/// the result as a `BigRational`.
1321fn exact_det_finite<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
1322    let (det_int, total_exp) = exact_det_int_finite(m)?;
1323    Ok(big_int_exp_to_big_rational(det_int, total_exp))
1324}
1325
1326/// Solve `A x = b` exactly after matrix and RHS finiteness has been proven.
1327///
1328/// Public [`Matrix`] / [`Vector`] values are finite by construction before
1329/// reaching this helper, so decomposition can proceed without rediscovering
1330/// stored NaN/∞ entries.
1331///
1332/// # Errors
1333/// Returns [`LaError::Singular`] if the matrix is exactly singular.
1334fn bareiss_solve_finite<const D: usize>(
1335    m: &Matrix<D>,
1336    b: &Vector<D>,
1337) -> Result<[BigRational; D], LaError> {
1338    let matrix = decompose_proven_finite_matrix(m);
1339    let rhs = decompose_proven_finite_vector(b);
1340    bareiss_solve_components(&matrix, &rhs)
1341}
1342
1343/// Solve an exact integer-scaled augmented system from decomposed components.
1344///
1345/// Forward elimination runs in [`BigInt`] using fraction-free Bareiss updates
1346/// \[7\]. This is exact arithmetic, so there is no floating-point conditioning or
1347/// roundoff error in the elimination itself; ill-conditioned inputs can still
1348/// produce large exact numerators and denominators in the final solution. The
1349/// elimination phase performs `O(D³)` integer operations and Bareiss exact
1350/// division controls intermediate integer growth compared with naive fraction
1351/// arithmetic. Back-substitution takes `O(D²)` integer operations with the
1352/// shared denominator. Reductions occur once per solved row and once per
1353/// final [`BigRational`] component, rather than after each product and sum.
1354///
1355/// # Errors
1356/// Returns [`LaError::Singular`] if the matrix component table represents an
1357/// exactly singular matrix.
1358fn bareiss_solve_components<const D: usize>(
1359    matrix: &Decomposed<[[Component; D]; D]>,
1360    rhs: &Decomposed<[Component; D]>,
1361) -> Result<[BigRational; D], LaError> {
1362    const MAX_SHARED_SCALE_GAP_BITS: u32 = 64;
1363
1364    let independent_matrix_scale = ScaleExponent::for_decomposed(matrix);
1365    let independent_rhs_scale = ScaleExponent::for_decomposed(rhs);
1366    let scale_gap = independent_matrix_scale
1367        .get()
1368        .abs_diff(independent_rhs_scale.get());
1369    let (matrix_scale, rhs_scale) = if scale_gap <= MAX_SHARED_SCALE_GAP_BITS {
1370        let shared = independent_matrix_scale.min(independent_rhs_scale);
1371        (shared, shared)
1372    } else {
1373        (independent_matrix_scale, independent_rhs_scale)
1374    };
1375    let a = build_big_int_matrix(matrix.components(), matrix_scale);
1376    let rhs = build_big_int_vec(rhs.components(), rhs_scale);
1377    let mut x = solve_big_int(a, rhs)?;
1378
1379    let solution_scale_exp = rhs_scale
1380        .get()
1381        .checked_sub(matrix_scale.get())
1382        .unwrap_or_else(|| unreachable!("finite f64 scale difference cannot overflow i32"));
1383    if solution_scale_exp != 0 {
1384        let solution_scale = big_int_exp_to_big_rational(BigInt::from(1_u8), solution_scale_exp);
1385        for component in &mut x {
1386            *component *= &solution_scale;
1387        }
1388    }
1389
1390    Ok(x)
1391}
1392
1393/// Solve an integer system with fraction-free elimination and shared-denominator
1394/// back-substitution.
1395///
1396/// The solved suffix stores integer numerators over one positive denominator
1397/// `q`, initially 1. For `U x = c`, the integer residual
1398/// `s = q*c[i] - sum(U[i][j]*n[j])` satisfies `q*x[i] = s/U[i][i]`.
1399/// Reduce this ratio once to `p/t`. Multiplying `q` and the solved numerators
1400/// by positive `t` makes the new component `p/(q*t)` without changing the
1401/// suffix values. Because `p` and `t` are coprime, `q*t` is the least common
1402/// denominator of the expanded suffix. This avoids retaining unnecessary
1403/// determinant factors. Reuse RHS storage for numerators and canonicalize
1404/// each output at the end; no rational arithmetic occurs in the inner sum.
1405///
1406/// # Errors
1407/// Returns [`LaError::Singular`] with exact-singularity metadata and the first
1408/// pivot column that contains no non-zero entry.
1409pub(crate) fn solve_big_int<const D: usize>(
1410    mut a: [[BigInt; D]; D],
1411    mut rhs: [BigInt; D],
1412) -> Result<[BigRational; D], LaError> {
1413    match bareiss_forward_eliminate(&mut a, Some(&mut rhs)) {
1414        BareissResult::Upper { .. } => {}
1415        BareissResult::Singular { pivot_col } => {
1416            cold_path();
1417            return Err(LaError::singular_exact(pivot_col));
1418        }
1419    }
1420
1421    let mut denominator = BigInt::from(1);
1422    for i in (0..D).rev() {
1423        let mut sum = &rhs[i] * &denominator;
1424        for j in (i + 1)..D {
1425            sum -= &a[i][j] * &rhs[j];
1426        }
1427        let (numerator, factor) = BigRational::new(sum, take(&mut a[i][i])).into_raw();
1428        if factor != BigInt::from(1) {
1429            denominator *= &factor;
1430            for solved in &mut rhs[i + 1..] {
1431                *solved *= &factor;
1432            }
1433        }
1434        rhs[i] = numerator;
1435    }
1436
1437    Ok(rhs.map(|numerator| BigRational::new(numerator, denominator.clone())))
1438}
1439
1440/// Exact determinant converted to finite `f64` without rounding.
1441///
1442/// This preserves the strict contract of [`Matrix::det_exact_f64`]: if the exact
1443/// determinant is not representable as a finite binary64 value, callers receive
1444/// a typed [`LaError::Unrepresentable`] instead of a rounded result.
1445///
1446/// # Errors
1447/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1448/// overflows the internal exponent representation.
1449///
1450/// Returns [`LaError::Unrepresentable`] with
1451/// [`UnrepresentableReason::RequiresRounding`] when the exact determinant is
1452/// finite but not exactly representable as binary64, or
1453/// [`UnrepresentableReason::NotFinite`] when no finite `f64` can represent it.
1454#[inline]
1455fn det_exact_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
1456    let (det_int, total_exp) = exact_det_int_finite(m)?;
1457    big_int_exp_to_finite_f64(&det_int, total_exp, None)
1458}
1459
1460/// Exact determinant rounded to finite `f64`.
1461///
1462/// This is the intentionally lossy counterpart to [`det_exact_f64_finite`] and
1463/// the private implementation target for [`Matrix::det_exact_rounded_f64`].
1464///
1465/// # Errors
1466/// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1467/// overflows the internal exponent representation.
1468///
1469/// Returns [`LaError::Unrepresentable`] with
1470/// [`UnrepresentableReason::NotFinite`] if rounding cannot produce a finite `f64`.
1471#[inline]
1472fn det_exact_rounded_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
1473    let (det_int, total_exp) = exact_det_int_finite(m)?;
1474    big_int_exp_to_rounded_f64(&det_int, total_exp)
1475}
1476
1477/// Exact determinant sign for an already finite matrix.
1478///
1479/// The fast `f64` filter treats overflowed or underflow-sensitive scalar
1480/// intermediates as inconclusive, then falls back to exact integer sign
1481/// computation: direct expansion for D≤4 or Bareiss elimination for D≥5.
1482///
1483#[inline]
1484fn det_sign_exact_finite<const D: usize>(m: &Matrix<D>) -> DeterminantSign {
1485    if let Ok(Some(estimate)) = m.det_direct_with_errbound() {
1486        let det_f64 = estimate.determinant();
1487        let error_bound = estimate.absolute_error_bound();
1488        if det_f64 > error_bound {
1489            return DeterminantSign::Positive;
1490        }
1491        if det_f64 < -error_bound {
1492            return DeterminantSign::Negative;
1493        }
1494    }
1495
1496    cold_path();
1497    let (det_int, _) = scaled_det_int_finite(m);
1498    match det_int.sign() {
1499        Sign::Plus => DeterminantSign::Positive,
1500        Sign::Minus => DeterminantSign::Negative,
1501        Sign::NoSign => DeterminantSign::Zero,
1502    }
1503}
1504
1505impl<const D: usize> Matrix<D> {
1506    /// Exact determinant using arbitrary-precision rational arithmetic.
1507    ///
1508    /// Requires the `exact` Cargo feature.
1509    ///
1510    /// Returns the determinant as an exact [`BigRational`] value. Every finite
1511    /// `f64` is exactly representable as a rational, so the conversion is
1512    /// lossless and the result is exact for the stored binary64 entries. It
1513    /// cannot recover precision lost before matrix construction.
1514    ///
1515    /// # When to use
1516    ///
1517    /// Use this when you need the exact determinant *value* — for example,
1518    /// volume computation over stored coordinates or distinguishing simplices
1519    /// that are exactly degenerate at those coordinates from near-degenerate
1520    /// ones. If you only need the *sign*, prefer
1521    /// [`det_sign_exact`](Self::det_sign_exact) which has a fast f64 filter.
1522    ///
1523    /// # Examples
1524    /// ```
1525    /// use la_stack::prelude::*;
1526    ///
1527    /// # fn main() -> Result<(), LaError> {
1528    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1529    /// let det = m.det_exact()?;
1530    /// // det = 1*4 - 2*3 = -2  (exact)
1531    /// assert_eq!(det, BigRational::from_integer((-2).into()));
1532    /// # Ok(())
1533    /// # }
1534    /// ```
1535    ///
1536    /// # Errors
1537    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1538    /// overflows the internal exponent representation.
1539    #[inline]
1540    pub fn det_exact(&self) -> Result<BigRational, LaError> {
1541        exact_det_finite(self)
1542    }
1543
1544    /// Exact determinant converted to `f64`.
1545    ///
1546    /// Requires the `exact` Cargo feature.
1547    ///
1548    /// Computes the exact determinant with the same integer-scaled core used by
1549    /// [`det_exact`](Self::det_exact), then converts the exact scaled integer
1550    /// result to `f64` only if the result is exactly representable as a finite
1551    /// binary64 value. The candidate conversion follows IEEE 754
1552    /// round-to-nearest, ties-to-even, but is returned only when no rounding is
1553    /// required.
1554    ///
1555    /// When callers also need the exact value or may recover with explicit
1556    /// rounding, compute [`det_exact`](Self::det_exact) once and use
1557    /// [`ExactF64Conversion`] on the returned [`BigRational`].
1558    ///
1559    /// # Examples
1560    /// ```
1561    /// use la_stack::prelude::*;
1562    ///
1563    /// # fn main() -> Result<(), LaError> {
1564    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1565    /// let det = m.det_exact_f64()?;
1566    /// assert!((det - (-2.0)).abs() <= f64::EPSILON);
1567    /// # Ok(())
1568    /// # }
1569    /// ```
1570    ///
1571    /// # Errors
1572    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1573    /// overflows the internal exponent representation.
1574    ///
1575    /// Returns [`LaError::Unrepresentable`] if the exact determinant cannot be
1576    /// represented exactly as a finite `f64`.
1577    #[inline]
1578    pub fn det_exact_f64(&self) -> Result<f64, LaError> {
1579        det_exact_f64_finite(self)
1580    }
1581
1582    /// Exact determinant rounded to `f64`.
1583    ///
1584    /// Requires the `exact` Cargo feature.
1585    ///
1586    /// Computes the exact determinant with the same integer-scaled core used by
1587    /// [`det_exact`](Self::det_exact), then rounds the exact value to a finite
1588    /// binary64 value using IEEE 754 round-to-nearest, ties-to-even. Unlike
1589    /// [`det_exact_f64`](Self::det_exact_f64), this method is intentionally lossy
1590    /// and may round non-dyadic or underflowing nonzero exact determinants.
1591    ///
1592    /// # Examples
1593    /// ```
1594    /// use core::assert_matches;
1595    /// use la_stack::prelude::*;
1596    ///
1597    /// # fn main() -> Result<(), LaError> {
1598    /// let m = Matrix::<2>::try_from_rows([
1599    ///     [1.0 + f64::EPSILON, 0.0],
1600    ///     [0.0, 1.0 - f64::EPSILON],
1601    /// ])?;
1602    ///
1603    /// assert_matches!(
1604    ///     m.det_exact_f64(),
1605    ///     Err(LaError::Unrepresentable {
1606    ///         index: None,
1607    ///         reason: UnrepresentableReason::RequiresRounding,
1608    ///         ..
1609    ///     })
1610    /// );
1611    /// assert_eq!(m.det_exact_rounded_f64()?.to_bits(), 1.0f64.to_bits());
1612    /// # Ok(())
1613    /// # }
1614    /// ```
1615    ///
1616    /// # Errors
1617    /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
1618    /// overflows the internal exponent representation.
1619    ///
1620    /// Returns [`LaError::Unrepresentable`] if rounding cannot produce a finite `f64`.
1621    #[inline]
1622    pub fn det_exact_rounded_f64(&self) -> Result<f64, LaError> {
1623        det_exact_rounded_f64_finite(self)
1624    }
1625
1626    /// Exact linear system solve using hybrid integer/rational arithmetic.
1627    ///
1628    /// Requires the `exact` Cargo feature.
1629    ///
1630    /// Solves `A x = b` where `A` is `self` and `b` is the given vector.
1631    /// Returns the exact solution as [`RationalVector<D>`]. Every finite `f64`
1632    /// is exactly representable as a rational, so the conversion is lossless
1633    /// and the result is exact for the stored binary64 entries. It cannot
1634    /// recover precision lost before matrix or vector construction.
1635    ///
1636    /// # When to use
1637    ///
1638    /// Use this when you need a solution exact for the stored inputs — for
1639    /// example, circumcenter computation over stored coordinates for
1640    /// near-degenerate simplices where f64 arithmetic may produce wildly wrong
1641    /// results.
1642    ///
1643    /// # Algorithm
1644    ///
1645    /// Matrix and RHS entries are decomposed via IEEE 754 bit extraction and
1646    /// initially assigned independent power-of-two bases. When the base
1647    /// exponents differ by at most 64, both sides use the lower shared base;
1648    /// larger gaps retain independent bases to avoid needless cross-side
1649    /// shifts. Both sides of the augmented system `(A | b)` are integer-valued.
1650    /// After solving that integer system, the exact
1651    /// power-of-two ratio between the RHS and matrix scales is restored.
1652    /// Forward elimination runs entirely in `BigInt`
1653    /// with fraction-free Bareiss updates — no `BigRational`, no GCD, no
1654    /// denominator tracking in the `O(D³)` phase. Back-substitution uses
1655    /// `O(D²)` integer operations over a shared denominator, with one reduction
1656    /// per solved row and final output canonicalization before scale restoration.
1657    /// First-non-zero pivoting is used
1658    /// throughout; since all arithmetic is exact, any non-zero pivot yields
1659    /// the correct answer (no numerical-stability concerns).
1660    ///
1661    /// # Examples
1662    /// ```
1663    /// use la_stack::prelude::*;
1664    ///
1665    /// # fn main() -> Result<(), LaError> {
1666    /// // A x = b  where A = [[1,2],[3,4]], b = [5, 11]  →  x = [1, 2]
1667    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1668    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1669    /// let x = a.solve_exact(b)?;
1670    /// assert_eq!(x.as_array()[0], BigRational::from_integer(1.into()));
1671    /// assert_eq!(x.as_array()[1], BigRational::from_integer(2.into()));
1672    /// # Ok(())
1673    /// # }
1674    /// ```
1675    ///
1676    /// # Errors
1677    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1678    #[inline]
1679    pub fn solve_exact(&self, b: Vector<D>) -> Result<RationalVector<D>, LaError> {
1680        bareiss_solve_finite(self, &b).map(RationalVector::from_canonical_array)
1681    }
1682
1683    /// Exact linear system solve converted to `f64`.
1684    ///
1685    /// Requires the `exact` Cargo feature.
1686    ///
1687    /// Computes the exact [`BigRational`] solution via
1688    /// [`solve_exact`](Self::solve_exact) and converts each component to `f64`
1689    /// only if that component is exactly representable as a finite binary64
1690    /// value. The candidate conversion follows IEEE 754 round-to-nearest,
1691    /// ties-to-even, but is returned only when no rounding is required.
1692    ///
1693    /// When callers also need the exact solution or may recover with explicit
1694    /// rounding, compute [`solve_exact`](Self::solve_exact) once and use
1695    /// [`ExactF64Conversion`] on the returned [`RationalVector`].
1696    ///
1697    /// # Examples
1698    /// ```
1699    /// use la_stack::prelude::*;
1700    ///
1701    /// # fn main() -> Result<(), LaError> {
1702    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1703    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
1704    /// let x = a.solve_exact_f64(b)?.into_array();
1705    /// assert!((x[0] - 1.0).abs() <= f64::EPSILON);
1706    /// assert!((x[1] - 2.0).abs() <= f64::EPSILON);
1707    /// # Ok(())
1708    /// # }
1709    /// ```
1710    ///
1711    /// # Errors
1712    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1713    /// Returns [`LaError::Unrepresentable`] if any component of the exact solution
1714    /// cannot be represented exactly as a finite `f64`.
1715    #[inline]
1716    pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1717        self.solve_exact(b)?.try_to_f64()
1718    }
1719
1720    /// Exact linear system solve rounded to `f64`.
1721    ///
1722    /// Requires the `exact` Cargo feature.
1723    ///
1724    /// Computes the exact [`BigRational`] solution via
1725    /// [`solve_exact`](Self::solve_exact) and rounds each component to a finite
1726    /// binary64 value using IEEE 754 round-to-nearest, ties-to-even. Unlike
1727    /// [`solve_exact_f64`](Self::solve_exact_f64), this method is intentionally
1728    /// lossy and may round non-dyadic or underflowing nonzero exact components.
1729    ///
1730    /// # Examples
1731    /// ```
1732    /// use core::assert_matches;
1733    /// use la_stack::prelude::*;
1734    ///
1735    /// # fn main() -> Result<(), LaError> {
1736    /// let a = Matrix::<1>::try_from_rows([[3.0]])?;
1737    /// let b = Vector::<1>::try_new([1.0])?;
1738    ///
1739    /// assert_matches!(
1740    ///     a.solve_exact_f64(b),
1741    ///     Err(LaError::Unrepresentable {
1742    ///         index: Some(0),
1743    ///         reason: UnrepresentableReason::RequiresRounding,
1744    ///         ..
1745    ///     })
1746    /// );
1747    /// assert_eq!(a.solve_exact_rounded_f64(b)?.into_array(), [1.0 / 3.0]);
1748    /// # Ok(())
1749    /// # }
1750    /// ```
1751    ///
1752    /// # Errors
1753    /// Returns [`LaError::Singular`] if the matrix is exactly singular.
1754    /// Returns [`LaError::Unrepresentable`] if rounding any component cannot
1755    /// produce a finite `f64`.
1756    #[inline]
1757    pub fn solve_exact_rounded_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
1758        self.solve_exact(b)?.to_rounded_f64()
1759    }
1760
1761    /// Exact determinant sign using adaptive-precision arithmetic.
1762    ///
1763    /// Requires the `exact` Cargo feature.
1764    ///
1765    /// Returns [`DeterminantSign::Positive`], [`DeterminantSign::Negative`], or
1766    /// [`DeterminantSign::Zero`] according to the determinant that is exact for
1767    /// the stored binary64 entries. This cannot recover precision lost before
1768    /// matrix construction.
1769    ///
1770    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
1771    /// against a conservative error bound derived from the matrix permanent.
1772    /// If the f64 result clearly exceeds the bound, the sign is returned
1773    /// immediately without allocating. Otherwise, exact integer arithmetic
1774    /// computes the sign without constructing any `BigRational` values: direct
1775    /// `BigInt` expansions for D ≤ 4 and Bareiss elimination for D ≥ 5.
1776    ///
1777    /// # When to use
1778    ///
1779    /// Use this when the sign of the determinant over the stored entries must be
1780    /// correct regardless of floating-point conditioning (e.g. geometric
1781    /// predicates on near-degenerate stored coordinates). For well-conditioned
1782    /// matrices the fast filter resolves the sign without touching
1783    /// `BigRational`, so the overhead is minimal.
1784    ///
1785    /// # Examples
1786    /// ```
1787    /// use la_stack::prelude::*;
1788    ///
1789    /// let m = Matrix::<3>::try_from_rows([
1790    ///     [1.0, 2.0, 3.0],
1791    ///     [4.0, 5.0, 6.0],
1792    ///     [7.0, 8.0, 9.0],
1793    /// ])?;
1794    /// // This matrix is singular (row 3 = 2 × row 2 − row 1 in exact arithmetic).
1795    /// assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1796    ///
1797    /// assert_eq!(Matrix::<3>::identity().det_sign_exact(), DeterminantSign::Positive);
1798    /// # Ok::<(), LaError>(())
1799    /// ```
1800    #[inline]
1801    pub fn det_sign_exact(&self) -> DeterminantSign {
1802        det_sign_exact_finite(self)
1803    }
1804}
1805
1806#[cfg(test)]
1807mod tests {
1808    use core::assert_matches;
1809    use std::array::from_fn;
1810
1811    use num_traits::{FromPrimitive, Signed};
1812    use pastey::paste;
1813    use proptest::prelude::*;
1814
1815    use super::*;
1816    use crate::{
1817        ArithmeticOperation, DEFAULT_SINGULAR_TOL, NonFiniteLocation, NonFiniteOrigin,
1818        SingularityReason,
1819    };
1820
1821    // -----------------------------------------------------------------------
1822    // D=4 determinant regression
1823    #[test]
1824    fn det4_matches_bareiss_across_sparse_wide_and_singular_inputs() {
1825        let coefficients = [
1826            [11_i32, 2, -3, 4],
1827            [2, 13, 5, -1],
1828            [3, -2, 17, 6],
1829            [-1, 4, 2, 19],
1830        ];
1831        for shift in [0_u32, 80, 256, 1024] {
1832            for mask in 0..16_u8 {
1833                let mut rows: [[BigInt; 4]; 4] = from_fn(|i| {
1834                    from_fn(|j| {
1835                        if i == 0 && mask & (1 << j) == 0 {
1836                            BigInt::from(0)
1837                        } else {
1838                            (BigInt::from(coefficients[i][j]) << shift) + BigInt::from(i + j)
1839                        }
1840                    })
1841                });
1842                for variant in 0..3 {
1843                    if variant == 1 {
1844                        rows.swap(0, 2);
1845                    } else if variant == 2 {
1846                        rows[1] = rows[0].clone();
1847                    }
1848                    let mut eliminated = rows.clone();
1849                    let expected = match bareiss_forward_eliminate(&mut eliminated, None) {
1850                        BareissResult::Upper { odd_swaps } => {
1851                            let det = take(&mut eliminated[3][3]);
1852                            if odd_swaps { -det } else { det }
1853                        }
1854                        BareissResult::Singular { .. } => BigInt::from(0),
1855                    };
1856                    assert_eq!(
1857                        det4_big_int(&rows),
1858                        expected,
1859                        "shift={shift}, mask={mask}, variant={variant}"
1860                    );
1861                }
1862            }
1863        }
1864    }
1865
1866    // -----------------------------------------------------------------------
1867
1868    // Test helpers
1869
1870    /// Lift binary64 inputs independently of production decomposition and scaling.
1871    ///
1872    /// Expected solutions and residuals must not inherit a decomposition bug
1873    /// from the implementation they check.
1874    ///
1875    /// # Panics
1876    /// Panics if `x` is NaN or infinite.
1877    fn f64_to_big_rational(x: f64) -> BigRational {
1878        BigRational::from_f64(x).expect("test oracle requires finite f64 input")
1879    }
1880
1881    fn assert_unrepresentable<T>(
1882        result: &Result<T, LaError>,
1883        expected_index: Option<usize>,
1884        expected_reason: UnrepresentableReason,
1885    ) {
1886        let Err(error) = result else {
1887            panic!("expected an exact-to-f64 conversion error");
1888        };
1889        assert!(matches!(
1890            *error,
1891            LaError::Unrepresentable { index, reason, .. }
1892                if index == expected_index && reason == expected_reason
1893        ));
1894    }
1895
1896    // -----------------------------------------------------------------------
1897    // Macro-generated per-dimension tests (D=2..5)
1898    // -----------------------------------------------------------------------
1899
1900    macro_rules! gen_exact_identity_tests {
1901        ($d:literal) => {
1902            paste! {
1903                #[test]
1904                fn [<exact_identity_paths_ $d d>]() {
1905                    let matrix = Matrix::<$d>::identity();
1906                    let one = BigRational::from_integer(BigInt::from(1));
1907
1908                    assert_eq!(matrix.det_exact().unwrap(), one);
1909                    assert_eq!(matrix.det_exact_f64().unwrap().to_bits(), 1.0_f64.to_bits());
1910                    assert_eq!(
1911                        matrix.det_exact_rounded_f64().unwrap().to_bits(),
1912                        1.0_f64.to_bits()
1913                    );
1914                    assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
1915                }
1916            }
1917        };
1918    }
1919
1920    gen_exact_identity_tests!(2);
1921    gen_exact_identity_tests!(3);
1922    gen_exact_identity_tests!(4);
1923    gen_exact_identity_tests!(5);
1924
1925    /// For D ≤ 4, `det_exact_f64` should agree with `det_direct` on matrices
1926    /// whose exact determinant is representable in f64.
1927    macro_rules! gen_det_exact_f64_agrees_with_det_direct {
1928        ($d:literal) => {
1929            paste! {
1930                #[test]
1931                fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
1932                    // Power-of-two diagonal entries make the determinant
1933                    // exactly representable in binary64.
1934                    let mut rows = [[0.0f64; $d]; $d];
1935                    let mut value = 2.0;
1936                    for (i, row) in rows.iter_mut().enumerate() {
1937                        row[i] = value;
1938                        value *= 2.0;
1939                    }
1940                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1941                    let exact = m.det_exact_f64().unwrap();
1942                    let direct = m.det_direct().unwrap().unwrap();
1943                    assert_eq!(exact.to_bits(), direct.to_bits());
1944                }
1945            }
1946        };
1947    }
1948
1949    gen_det_exact_f64_agrees_with_det_direct!(2);
1950    gen_det_exact_f64_agrees_with_det_direct!(3);
1951    gen_det_exact_f64_agrees_with_det_direct!(4);
1952
1953    #[test]
1954    fn det_sign_exact_d0_is_positive() {
1955        assert_eq!(
1956            Matrix::<0>::zero().det_sign_exact(),
1957            DeterminantSign::Positive
1958        );
1959    }
1960
1961    #[test]
1962    fn det_sign_exact_d1_positive() {
1963        let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
1964        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
1965    }
1966
1967    #[test]
1968    fn det_sign_exact_d1_negative() {
1969        let m = Matrix::<1>::try_from_rows([[-3.5]]).unwrap();
1970        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
1971    }
1972
1973    #[test]
1974    fn det_sign_exact_d1_zero() {
1975        let m = Matrix::<1>::try_from_rows([[0.0]]).unwrap();
1976        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1977    }
1978
1979    #[test]
1980    fn det_sign_exact_singular_duplicate_rows() {
1981        let m = Matrix::<3>::try_from_rows([
1982            [1.0, 2.0, 3.0],
1983            [4.0, 5.0, 6.0],
1984            [1.0, 2.0, 3.0], // duplicate of row 0
1985        ])
1986        .unwrap();
1987        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1988    }
1989
1990    #[test]
1991    fn det_sign_exact_singular_linear_combination() {
1992        // Row 2 = row 0 + row 1 in exact arithmetic.
1993        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]])
1994            .unwrap();
1995        assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
1996    }
1997
1998    #[test]
1999    fn det_sign_exact_negative_det_row_swap() {
2000        // Swapping two rows of the identity negates the determinant.
2001        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]])
2002            .unwrap();
2003        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2004    }
2005
2006    #[test]
2007    fn det_sign_exact_negative_det_known() {
2008        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
2009        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2010        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2011    }
2012
2013    #[test]
2014    fn det_sign_exact_agrees_with_det_for_spd() {
2015        // SPD matrix → positive determinant.
2016        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]])
2017            .unwrap();
2018        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2019        assert!(m.det().unwrap() > 0.0);
2020    }
2021
2022    /// Near-singular matrix with an exact perturbation.
2023    ///
2024    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
2025    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
2026    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
2027    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
2028    #[test]
2029    fn det_sign_exact_near_singular_perturbation() {
2030        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2031        let m = Matrix::<3>::try_from_rows([
2032            [1.0 + perturbation, 2.0, 3.0],
2033            [4.0, 5.0, 6.0],
2034            [7.0, 8.0, 9.0],
2035        ])
2036        .unwrap();
2037        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
2038        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2039    }
2040
2041    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
2042    /// and never allocate `BigRational`.  We can't directly observe this,
2043    /// but we verify correctness for a range of known signs.
2044    #[test]
2045    fn det_sign_exact_fast_filter_positive_4x4() {
2046        let m = Matrix::<4>::try_from_rows([
2047            [2.0, 1.0, 0.0, 0.0],
2048            [1.0, 3.0, 1.0, 0.0],
2049            [0.0, 1.0, 4.0, 1.0],
2050            [0.0, 0.0, 1.0, 5.0],
2051        ])
2052        .unwrap();
2053        // SPD tridiagonal → positive det.
2054        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2055    }
2056
2057    #[test]
2058    fn det_sign_exact_fast_filter_negative_4x4() {
2059        // Swap rows 0 and 1 of the above → negate det.
2060        let m = Matrix::<4>::try_from_rows([
2061            [1.0, 3.0, 1.0, 0.0],
2062            [2.0, 1.0, 0.0, 0.0],
2063            [0.0, 1.0, 4.0, 1.0],
2064            [0.0, 0.0, 1.0, 5.0],
2065        ])
2066        .unwrap();
2067        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2068    }
2069
2070    #[test]
2071    fn det_sign_exact_subnormal_entries() {
2072        // Subnormal f64 values should convert losslessly.
2073        let tiny = 5e-324_f64; // smallest positive subnormal
2074        assert!(tiny.is_subnormal());
2075
2076        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
2077        // det = tiny^2 > 0
2078        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2079    }
2080
2081    #[test]
2082    fn det_sign_exact_falls_back_when_subnormal_rounding_reverses_direct_sign() {
2083        let scale = 2.0_f64.powi(-360);
2084        let matrix = Matrix::<3>::try_from_rows([
2085            [-5.0 * scale, 3.0 * scale, 6.0 * scale],
2086            [0.0, -7.0 * scale, -7.0 * scale],
2087            [2.0 * scale, -3.0 * scale, -4.0 * scale],
2088        ])
2089        .unwrap();
2090
2091        assert_eq!(
2092            matrix.det_direct().unwrap().unwrap().to_bits(),
2093            (-f64::from_bits(1)).to_bits()
2094        );
2095        assert_eq!(matrix.det_errbound(), Ok(None));
2096        assert!(matrix.det_exact().unwrap().is_positive());
2097        assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
2098    }
2099
2100    #[test]
2101    fn det_sign_exact_falls_back_for_bit_exact_underflow_counterexample() {
2102        let matrix = Matrix::<3>::try_from_rows([
2103            [
2104                f64::from_bits(9_218_868_437_227_405_311),
2105                f64::from_bits(13_830_554_455_654_793_216),
2106                0.0,
2107            ],
2108            [
2109                f64::from_bits(6_790_500_848_393_242_208),
2110                f64::from_bits(2_184_621_143_747_520_227),
2111                f64::from_bits(2_187_555_472_467_513_745),
2112            ],
2113            [
2114                0.0,
2115                f64::from_bits(2_184_859_204_554_904_434),
2116                f64::from_bits(2_184_762_736_385_916_910),
2117            ],
2118        ])
2119        .unwrap();
2120
2121        assert!(matrix.det_direct().unwrap().unwrap().is_sign_positive());
2122        assert_eq!(matrix.det_errbound(), Ok(None));
2123        assert!(matrix.det_exact().unwrap().is_negative());
2124        assert_eq!(matrix.det_sign_exact(), DeterminantSign::Negative);
2125    }
2126
2127    #[test]
2128    fn det_sign_exact_pivot_needed_5x5() {
2129        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
2130        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
2131        let m = Matrix::<5>::try_from_rows([
2132            [0.0, 1.0, 0.0, 0.0, 0.0],
2133            [1.0, 0.0, 0.0, 0.0, 0.0],
2134            [0.0, 0.0, 1.0, 0.0, 0.0],
2135            [0.0, 0.0, 0.0, 1.0, 0.0],
2136            [0.0, 0.0, 0.0, 0.0, 1.0],
2137        ])
2138        .unwrap();
2139        assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
2140    }
2141
2142    #[test]
2143    fn det_sign_exact_5x5_known() {
2144        // det of a permutation matrix with two swaps = +1 (even permutation).
2145        let m = Matrix::<5>::try_from_rows([
2146            [0.0, 1.0, 0.0, 0.0, 0.0],
2147            [1.0, 0.0, 0.0, 0.0, 0.0],
2148            [0.0, 0.0, 0.0, 1.0, 0.0],
2149            [0.0, 0.0, 1.0, 0.0, 0.0],
2150            [0.0, 0.0, 0.0, 0.0, 1.0],
2151        ])
2152        .unwrap();
2153        // Two transpositions → even permutation → det = +1
2154        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2155    }
2156
2157    // -----------------------------------------------------------------------
2158    // Finite binary64 decomposition tests. Matrix/Vector constructor tests own
2159    // rejection of non-finite inputs before they can reach this helper.
2160    // -----------------------------------------------------------------------
2161
2162    #[test]
2163    fn decompose_f64_zero() {
2164        assert_eq!(decompose_proven_finite_f64(0.0), Component::Zero);
2165        assert_eq!(decompose_proven_finite_f64(-0.0), Component::Zero);
2166    }
2167
2168    #[test]
2169    fn decompose_f64_one() {
2170        assert_eq!(
2171            decompose_proven_finite_f64(1.0),
2172            Component::NonZero {
2173                mantissa: NonZeroU64::new(1).unwrap(),
2174                exponent: 0,
2175                is_negative: false,
2176            }
2177        );
2178    }
2179
2180    #[test]
2181    fn decompose_f64_negative() {
2182        assert_eq!(
2183            decompose_proven_finite_f64(-3.5),
2184            Component::NonZero {
2185                mantissa: NonZeroU64::new(7).unwrap(),
2186                exponent: -1,
2187                is_negative: true,
2188            }
2189        );
2190    }
2191
2192    #[test]
2193    fn decompose_f64_subnormal() {
2194        let tiny = f64::from_bits(1);
2195        assert!(tiny.is_subnormal());
2196        assert_eq!(
2197            decompose_proven_finite_f64(tiny),
2198            Component::NonZero {
2199                mantissa: NonZeroU64::new(1).unwrap(),
2200                exponent: -1074,
2201                is_negative: false,
2202            }
2203        );
2204    }
2205
2206    #[test]
2207    fn decompose_f64_normalizes_mixed_subnormal_mantissa() {
2208        let value = f64::from_bits(0x000C_0000_0000_0000);
2209        assert!(value.is_subnormal());
2210        assert_eq!(
2211            decompose_proven_finite_f64(value),
2212            Component::NonZero {
2213                mantissa: NonZeroU64::new(3).unwrap(),
2214                exponent: -1024,
2215                is_negative: false,
2216            }
2217        );
2218    }
2219
2220    #[test]
2221    fn decompose_f64_power_of_two() {
2222        assert_eq!(
2223            decompose_proven_finite_f64(1024.0),
2224            Component::NonZero {
2225                mantissa: NonZeroU64::new(1).unwrap(),
2226                exponent: 10,
2227                is_negative: false,
2228            }
2229        );
2230    }
2231
2232    proptest! {
2233        #[test]
2234        fn finite_f64_round_trips_through_exact_decomposition(bits in any::<u64>()) {
2235            let value = f64::from_bits(bits);
2236            prop_assume!(value.is_finite());
2237
2238            let exact = f64_to_big_rational(value);
2239            let decomposed = match decompose_proven_finite_f64(value) {
2240                Component::Zero => BigRational::from_integer(BigInt::from(0)),
2241                Component::NonZero { mantissa, exponent, is_negative } => {
2242                    prop_assert_eq!(mantissa.get() & 1, 1);
2243                    prop_assert!((-1074..=1023).contains(&exponent));
2244                    let numerator = if is_negative {
2245                        -BigInt::from(mantissa.get())
2246                    } else {
2247                        BigInt::from(mantissa.get())
2248                    };
2249                    if exponent >= 0 {
2250                        BigRational::from_integer(numerator << exponent.cast_unsigned())
2251                    } else {
2252                        BigRational::new(numerator, BigInt::from(1) << exponent.unsigned_abs())
2253                    }
2254                }
2255            };
2256            prop_assert_eq!(&decomposed, &exact);
2257            let reconstructed = exact_rational_to_finite_f64(&exact, None);
2258
2259            prop_assert_eq!(reconstructed, Ok(value));
2260        }
2261    }
2262
2263    #[test]
2264    fn wide_low_exponent_value_reports_non_finite_rounded_result() {
2265        // (2^2099 - 1) × 2^-1075 lies just below 2^1024 and rounds to +∞.
2266        let value = (BigInt::from(1_u8) << 2099_u32) - BigInt::from(1_u8);
2267        let result = big_int_exp_to_finite_f64(&value, -1075, None);
2268
2269        assert!(!result.as_ref().unwrap_err().requires_rounding());
2270        assert_unrepresentable(&result, None, UnrepresentableReason::NotFinite);
2271    }
2272
2273    #[test]
2274    fn direct_big_int_rounding_handles_extreme_negative_exponent_without_large_denominator() {
2275        let positive = big_int_exp_ref_to_rounded_f64(&BigInt::from(1_u8), i32::MIN, None).unwrap();
2276        let negative =
2277            big_int_exp_ref_to_rounded_f64(&BigInt::from(-1_i8), i32::MIN, None).unwrap();
2278
2279        assert_eq!(positive.to_bits(), 0.0_f64.to_bits());
2280        assert_eq!(negative.to_bits(), (-0.0_f64).to_bits());
2281    }
2282
2283    proptest! {
2284        #[test]
2285        fn direct_big_int_rounding_matches_rational_oracle(
2286            value in any::<i128>(),
2287            exp in -1200_i32..=1200_i32,
2288        ) {
2289            let value = BigInt::from(value);
2290            let direct = big_int_exp_ref_to_rounded_f64(&value, exp, None);
2291            let exact = big_int_exp_to_big_rational(value, exp);
2292            let oracle = exact_rational_to_rounded_f64(&exact, None);
2293
2294            prop_assert_eq!(direct.map(f64::to_bits), oracle.map(f64::to_bits));
2295        }
2296    }
2297
2298    #[test]
2299    fn component_to_big_int_distinguishes_zero_from_nonzero_mantissa() {
2300        let baseline = Component::NonZero {
2301            mantissa: NonZeroU64::new(1).unwrap(),
2302            exponent: 1,
2303            is_negative: false,
2304        };
2305        let positive = Component::NonZero {
2306            mantissa: NonZeroU64::new(3).unwrap(),
2307            exponent: 4,
2308            is_negative: false,
2309        };
2310        let negative = Component::NonZero {
2311            mantissa: NonZeroU64::new(5).unwrap(),
2312            exponent: 3,
2313            is_negative: true,
2314        };
2315
2316        let decomposed =
2317            Decomposed::from_vector_components([Component::Zero, baseline, positive, negative]);
2318        let scale = ScaleExponent::for_decomposed(&decomposed);
2319
2320        assert_eq!(
2321            component_to_big_int(Component::Zero, scale),
2322            BigInt::from(0)
2323        );
2324        assert_eq!(component_to_big_int(positive, scale), BigInt::from(24));
2325        assert_eq!(component_to_big_int(negative, scale), BigInt::from(-20));
2326    }
2327
2328    #[test]
2329    fn decomposed_all_zero_uses_no_sentinel_exponent() {
2330        let decomposed = decompose_proven_finite_matrix(&Matrix::<2>::zero());
2331        assert_eq!(decomposed.min_exponent(), None);
2332
2333        let scale = ScaleExponent::for_decomposed(&decomposed);
2334        assert_eq!(scale, ScaleExponent::ZERO);
2335        assert_eq!(scale.get(), 0);
2336        assert_eq!(
2337            build_big_int_matrix(decomposed.components(), scale),
2338            [
2339                [BigInt::from(0), BigInt::from(0)],
2340                [BigInt::from(0), BigInt::from(0)]
2341            ]
2342        );
2343    }
2344
2345    #[test]
2346    fn matrix_and_rhs_scales_are_derived_independently() {
2347        let tiny = f64::from_bits(1);
2348        let matrix = Matrix::<2>::try_from_rows([[f64::MAX, 0.0], [0.0, 1.0]]).unwrap();
2349        let rhs = Vector::<2>::try_new([tiny, 0.0]).unwrap();
2350        let matrix = decompose_proven_finite_matrix(&matrix);
2351        let rhs = decompose_proven_finite_vector(&rhs);
2352
2353        assert_eq!(matrix.min_exponent(), Some(0));
2354        assert_eq!(rhs.min_exponent(), Some(-1074));
2355
2356        let matrix_scale = ScaleExponent::for_decomposed(&matrix);
2357        let rhs_scale = ScaleExponent::for_decomposed(&rhs);
2358        assert_eq!(matrix_scale.get(), 0);
2359        assert_eq!(matrix_scale.shift_for(0), 0);
2360        assert_eq!(rhs_scale.get(), -1074);
2361        assert_eq!(rhs_scale.shift_for(-1074), 0);
2362    }
2363
2364    proptest! {
2365        #[test]
2366        fn derived_scale_yields_nonnegative_shifts(bits in any::<[u64; 4]>()) {
2367            let values = bits.map(f64::from_bits);
2368            prop_assume!(values.iter().all(|value| value.is_finite()));
2369            let matrix = Matrix::<2>::try_from_rows([
2370                [values[0], values[1]],
2371                [values[2], values[3]],
2372            ]).unwrap();
2373            let decomposed = decompose_proven_finite_matrix(&matrix);
2374            let scale = ScaleExponent::for_decomposed(&decomposed);
2375
2376            for component in decomposed.components().iter().flatten() {
2377                if let Some(exponent) = component.exponent() {
2378                    prop_assert!(exponent >= scale.get());
2379                    prop_assert_eq!(
2380                        scale.shift_for(exponent),
2381                        u32::try_from(exponent - scale.get()).unwrap(),
2382                    );
2383                }
2384            }
2385        }
2386    }
2387
2388    #[test]
2389    fn determinant_scale_exp_accepts_reconstructed_row_sum() {
2390        assert_eq!(determinant_scale_exp::<4>(-4296, -1074), Ok(-4296));
2391        assert_eq!(determinant_scale_exp::<4>(0, -1074), Ok(0));
2392        assert_eq!(determinant_scale_exp::<0>(0, 0), Ok(0));
2393    }
2394
2395    #[test]
2396    fn determinant_scale_exp_rejects_positive_overflow() {
2397        assert_eq!(
2398            determinant_scale_exp::<3_000_000>(i64::from(i32::MAX) + 1, 1023),
2399            Err(LaError::DeterminantScaleOverflow {
2400                dim: 3_000_000,
2401                min_exponent: 1023,
2402            })
2403        );
2404    }
2405
2406    #[test]
2407    fn determinant_scale_exp_rejects_negative_overflow() {
2408        assert_eq!(
2409            determinant_scale_exp::<3_000_000>(-1074 * 3_000_000_i64, -1074),
2410            Err(LaError::DeterminantScaleOverflow {
2411                dim: 3_000_000,
2412                min_exponent: -1074,
2413            })
2414        );
2415    }
2416
2417    #[test]
2418    fn negative_exponent_from_magnitude_covers_i32_domain_boundaries() {
2419        assert_eq!(negative_exponent_from_magnitude(0), 0);
2420        assert_eq!(negative_exponent_from_magnitude(1), -1);
2421        assert_eq!(
2422            negative_exponent_from_magnitude(i32::MAX.cast_unsigned().into()),
2423            -i32::MAX
2424        );
2425        assert_eq!(
2426            negative_exponent_from_magnitude(i32::MIN.unsigned_abs().into()),
2427            i32::MIN
2428        );
2429    }
2430
2431    #[test]
2432    #[should_panic(expected = "negative exponent magnitude exceeds the i32 domain")]
2433    fn negative_exponent_from_magnitude_rejects_values_above_i32_domain() {
2434        let _ = negative_exponent_from_magnitude(u64::from(i32::MIN.unsigned_abs()) + 1);
2435    }
2436
2437    // -----------------------------------------------------------------------
2438    // Exact scaled-integer determinant tests
2439    // -----------------------------------------------------------------------
2440
2441    #[test]
2442    fn exact_det_int_d0() {
2443        let m = Matrix::<0>::zero();
2444        let (det, exp) = exact_det_int_finite(&m).unwrap();
2445        assert_eq!(det, BigInt::from(1));
2446        assert_eq!(exp, 0);
2447    }
2448
2449    /// Table-driven coverage of the D=1 fast-path: each 1×1 matrix
2450    /// decomposes to `(±mant, exp)` directly.  Includes an integer, zero,
2451    /// a negative fractional, and a positive fractional case — the
2452    /// combinations that exercise the sign handling, the all-zero early
2453    /// return, trailing-zero stripping, and negative exponent scaling.
2454    #[test]
2455    fn exact_det_int_d1_cases() {
2456        let cases: &[(f64, i64, i32)] = &[
2457            // (input, expected_det_int, expected_exp)
2458            (7.0, 7, 0),    // integer → (7, 0)
2459            (0.0, 0, 0),    // all-zero early return → (0, 0)
2460            (-3.5, -7, -1), // -3.5 = -7 × 2^(-1)
2461            (0.5, 1, -1),   // 0.5  =  1 × 2^(-1)
2462        ];
2463        for &(input, expected_det_int, expected_exp) in cases {
2464            let m = Matrix::<1>::try_from_rows([[input]]).unwrap();
2465            let (det, exp) = exact_det_int_finite(&m).unwrap();
2466            assert_eq!(
2467                det,
2468                BigInt::from(expected_det_int),
2469                "det_int for input={input}"
2470            );
2471            assert_eq!(exp, expected_exp, "exp for input={input}");
2472        }
2473    }
2474
2475    #[test]
2476    fn exact_det_int_d2_known() {
2477        // det([[1,2],[3,4]]) = -2
2478        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2479        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2480        // Reconstruct and verify.
2481        let det = big_int_exp_to_big_rational(det_int, total_exp);
2482        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
2483    }
2484
2485    #[test]
2486    fn exact_det_int_all_zeros() {
2487        let m = Matrix::<3>::zero();
2488        let (det, _) = exact_det_int_finite(&m).unwrap();
2489        assert_eq!(det, BigInt::from(0));
2490    }
2491
2492    #[test]
2493    fn exact_det_int_fractional_entries() {
2494        // Entries with negative exponents: 0.5 = 1×2^(-1), 0.25 = 1×2^(-2).
2495        // det([[0.5, 0.25], [1.0, 1.0]]) = 0.5×1.0 − 0.25×1.0 = 0.25
2496        let m = Matrix::<2>::try_from_rows([[0.5, 0.25], [1.0, 1.0]]).unwrap();
2497        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2498        let det = big_int_exp_to_big_rational(det_int, total_exp);
2499        assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
2500    }
2501
2502    #[test]
2503    fn exact_det_int_d3_direct_expansion_handles_zero_diagonal() {
2504        // A zero diagonal entry does not require pivoting in the direct D=3 expansion.
2505        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]])
2506            .unwrap();
2507        let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
2508        let det = big_int_exp_to_big_rational(det_int, total_exp);
2509        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2510    }
2511
2512    // -----------------------------------------------------------------------
2513    // big_int_exp_to_big_rational tests
2514    // -----------------------------------------------------------------------
2515
2516    #[test]
2517    fn big_int_exp_to_big_rational_zero() {
2518        let r = big_int_exp_to_big_rational(BigInt::from(0), -50);
2519        assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
2520    }
2521
2522    #[test]
2523    fn big_int_exp_to_big_rational_positive_exp() {
2524        // 3 × 2^2 = 12
2525        let r = big_int_exp_to_big_rational(BigInt::from(3), 2);
2526        assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
2527    }
2528
2529    #[test]
2530    fn big_int_exp_to_big_rational_negative_exp_reduced() {
2531        // 6 × 2^(-2) = 6/4 → reduced to 3/2 (strip one shared factor of 2)
2532        let r = big_int_exp_to_big_rational(BigInt::from(6), -2);
2533        assert_eq!(*r.numer(), BigInt::from(3));
2534        assert_eq!(*r.denom(), BigInt::from(2));
2535    }
2536
2537    #[test]
2538    fn big_int_exp_to_big_rational_negative_exp_reduces_to_integer() {
2539        // 8 × 2^(-3) = 1 after stripping every denominator factor.
2540        let r = big_int_exp_to_big_rational(BigInt::from(8), -3);
2541        assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
2542    }
2543
2544    #[test]
2545    fn big_int_exp_to_big_rational_negative_exp_already_odd() {
2546        // 3 × 2^(-2) = 3/4 (already in lowest terms since 3 is odd)
2547        let r = big_int_exp_to_big_rational(BigInt::from(3), -2);
2548        assert_eq!(*r.numer(), BigInt::from(3));
2549        assert_eq!(*r.denom(), BigInt::from(4));
2550    }
2551
2552    #[test]
2553    fn big_int_exp_to_big_rational_negative_value() {
2554        // -5 × 2^1 = -10
2555        let r = big_int_exp_to_big_rational(BigInt::from(-5), 1);
2556        assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
2557    }
2558
2559    #[test]
2560    fn big_int_exp_to_big_rational_negative_value_with_denominator() {
2561        // -3 × 2^(-2) = -3/4
2562        let r = big_int_exp_to_big_rational(BigInt::from(-3), -2);
2563        assert_eq!(*r.numer(), BigInt::from(-3));
2564        assert_eq!(*r.denom(), BigInt::from(4));
2565    }
2566
2567    // -----------------------------------------------------------------------
2568    // Public exact determinant wrapper tests
2569    // -----------------------------------------------------------------------
2570
2571    #[test]
2572    fn det_exact_d1_returns_entry() {
2573        let det = Matrix::<1>::try_from_rows([[7.0]])
2574            .unwrap()
2575            .det_exact()
2576            .unwrap();
2577        assert_eq!(det, f64_to_big_rational(7.0));
2578    }
2579
2580    #[test]
2581    fn det_exact_d3_direct_expansion_handles_zero_diagonal() {
2582        // Direct D=3 expansion handles a zero diagonal entry without pivoting.
2583        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]])
2584            .unwrap();
2585        let det = m.det_exact().unwrap();
2586        // det of this permutation matrix = -1
2587        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2588    }
2589
2590    #[test]
2591    fn det_exact_d3_singular_zero_column_returns_zero() {
2592        // A zero column makes the direct D=3 determinant exactly zero.
2593        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]])
2594            .unwrap();
2595        let det = m.det_exact().unwrap();
2596        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
2597    }
2598
2599    #[test]
2600    fn det_sign_exact_overflow_determinant_finite_entries() {
2601        // Entries near f64::MAX are finite, but the f64 determinant overflows
2602        // to infinity. The fast filter is inconclusive and the direct `BigInt`
2603        // expansion computes the correct positive sign.
2604        let big = f64::MAX / 2.0;
2605        assert!(big.is_finite());
2606        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2607            .unwrap();
2608        // det = big^2 > 0
2609        assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
2610    }
2611
2612    // -----------------------------------------------------------------------
2613    // det_exact: dimension-specific tests
2614    // -----------------------------------------------------------------------
2615
2616    #[test]
2617    fn det_exact_d0_is_one() {
2618        let det = Matrix::<0>::zero().det_exact().unwrap();
2619        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
2620    }
2621
2622    #[test]
2623    fn det_exact_known_2x2() {
2624        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
2625        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2626        let det = m.det_exact().unwrap();
2627        assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
2628    }
2629
2630    #[test]
2631    fn det_exact_known_dense_4x4() {
2632        let m = Matrix::<4>::try_from_rows([
2633            [4.0, 1.0, 3.0, 2.0],
2634            [0.0, 5.0, 2.0, 1.0],
2635            [7.0, 2.0, 6.0, 3.0],
2636            [1.0, 8.0, 4.0, 9.0],
2637        ])
2638        .unwrap();
2639
2640        assert_eq!(
2641            m.det_exact(),
2642            Ok(BigRational::from_integer(BigInt::from(92)))
2643        );
2644    }
2645
2646    #[test]
2647    fn det_exact_singular_returns_zero() {
2648        // Rows in arithmetic progression → exactly singular.
2649        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]])
2650            .unwrap();
2651        let det = m.det_exact().unwrap();
2652        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
2653    }
2654
2655    #[test]
2656    fn det_exact_near_singular_perturbation() {
2657        // Same 2^-50 perturbation case: exact det = -3 × 2^-50.
2658        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
2659        let m = Matrix::<3>::try_from_rows([
2660            [1.0 + perturbation, 2.0, 3.0],
2661            [4.0, 5.0, 6.0],
2662            [7.0, 8.0, 9.0],
2663        ])
2664        .unwrap();
2665        let det = m.det_exact().unwrap();
2666        // det should be exactly -3 × 2^-50.
2667        let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
2668        assert_eq!(det, expected);
2669    }
2670
2671    #[test]
2672    fn det_exact_5x5_permutation() {
2673        // Single swap (rows 0↔1) → det = -1.
2674        let m = Matrix::<5>::try_from_rows([
2675            [0.0, 1.0, 0.0, 0.0, 0.0],
2676            [1.0, 0.0, 0.0, 0.0, 0.0],
2677            [0.0, 0.0, 1.0, 0.0, 0.0],
2678            [0.0, 0.0, 0.0, 1.0, 0.0],
2679            [0.0, 0.0, 0.0, 0.0, 1.0],
2680        ])
2681        .unwrap();
2682        let det = m.det_exact().unwrap();
2683        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
2684    }
2685
2686    // -----------------------------------------------------------------------
2687    // det_exact_f64: dimension-specific tests
2688    // -----------------------------------------------------------------------
2689
2690    #[test]
2691    fn det_exact_f64_known_2x2() {
2692        let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2693        let det = m.det_exact_f64().unwrap();
2694        assert!((det - (-2.0)).abs() <= f64::EPSILON);
2695    }
2696
2697    #[test]
2698    fn det_exact_f64_overflow_returns_err() {
2699        // Entries near f64::MAX produce a determinant too large for f64.
2700        let big = f64::MAX / 2.0;
2701        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2702            .unwrap();
2703        // det = big^2, which overflows f64.
2704        assert_unrepresentable(&m.det_exact_f64(), None, UnrepresentableReason::NotFinite);
2705    }
2706
2707    #[test]
2708    fn det_exact_rounded_f64_overflow_returns_err() {
2709        let big = f64::MAX / 2.0;
2710        let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
2711            .unwrap();
2712
2713        assert_unrepresentable(
2714            &m.det_exact_rounded_f64(),
2715            None,
2716            UnrepresentableReason::NotFinite,
2717        );
2718    }
2719
2720    #[test]
2721    fn det_exact_f64_underflow_returns_err_for_nonzero_exact_result() {
2722        let tiny = f64::from_bits(1);
2723        let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
2724
2725        assert!(m.det_exact().unwrap().is_positive());
2726        assert_unrepresentable(
2727            &m.det_exact_f64(),
2728            None,
2729            UnrepresentableReason::RequiresRounding,
2730        );
2731    }
2732
2733    #[test]
2734    fn det_exact_f64_rejects_inexact_rounding() {
2735        let m = Matrix::<2>::try_from_rows([[1.0 + f64::EPSILON, 0.0], [0.0, 1.0 - f64::EPSILON]])
2736            .unwrap();
2737
2738        assert_eq!(
2739            m.det_exact(),
2740            Ok(BigRational::new(
2741                (BigInt::from(1_u128) << 104_u32) - BigInt::from(1),
2742                BigInt::from(1_u128 << 104),
2743            ))
2744        );
2745        assert_unrepresentable(
2746            &m.det_exact_f64(),
2747            None,
2748            UnrepresentableReason::RequiresRounding,
2749        );
2750    }
2751
2752    #[test]
2753    fn det_exact_f64_accepts_max_finite_binary64() {
2754        let m = Matrix::<1>::try_from_rows([[f64::MAX]]).unwrap();
2755
2756        assert_eq!(m.det_exact_f64().unwrap().to_bits(), f64::MAX.to_bits());
2757    }
2758
2759    // -----------------------------------------------------------------------
2760    // solve_exact: macro-generated per-dimension tests (D=2..5)
2761    // -----------------------------------------------------------------------
2762
2763    /// Helper: build an arbitrary RHS vector for dimension `$d`.
2764    fn arbitrary_rhs<const D: usize>() -> Vector<D> {
2765        let values = [1.0, -2.5, 3.0, 0.25, -4.0];
2766        let mut arr = [0.0f64; D];
2767        for (dst, src) in arr.iter_mut().zip(values.iter()) {
2768            *dst = *src;
2769        }
2770        Vector::<D>::new(arr)
2771    }
2772
2773    macro_rules! gen_solve_exact_tests {
2774        ($d:literal) => {
2775            paste! {
2776                #[test]
2777                fn [<solve_exact_identity_paths_ $d d>]() {
2778                    let a = Matrix::<$d>::identity();
2779                    let b = arbitrary_rhs::<$d>();
2780                    let exact = a.solve_exact(b).unwrap();
2781                    let strict_f64 = a.solve_exact_f64(b).unwrap().into_array();
2782
2783                    for i in 0..$d {
2784                        assert_eq!(exact.as_array()[i], f64_to_big_rational(b.as_array()[i]));
2785                        assert_eq!(strict_f64[i].to_bits(), b.as_array()[i].to_bits());
2786                    }
2787                }
2788
2789                #[test]
2790                fn [<solve_exact_singular_ $d d>]() {
2791                    // Zero matrix is singular.
2792                    let a = Matrix::<$d>::zero();
2793                    let b = arbitrary_rhs::<$d>();
2794                    assert_matches!(
2795                        a.solve_exact(b),
2796                        Err(LaError::Singular {
2797                            pivot_col: 0,
2798                            reason: SingularityReason::Exact,
2799                            ..
2800                        })
2801                    );
2802                }
2803            }
2804        };
2805    }
2806
2807    gen_solve_exact_tests!(2);
2808    gen_solve_exact_tests!(3);
2809    gen_solve_exact_tests!(4);
2810    gen_solve_exact_tests!(5);
2811
2812    /// Manufacture integer systems with fractional solutions, zero components,
2813    /// signed row scales, and large common factors. Column denominators cancel
2814    /// analytically in A*x, independently of the elimination implementation.
2815    fn check_integer_solve_canonical_components<const D: usize>() {
2816        let denominators = [2, 3, 5, 7, 11, 13, 17, 19];
2817        let numerators: [BigInt; D] = from_fn(|col| {
2818            let value = BigInt::from(col);
2819            if col.is_multiple_of(2) { value } else { -value }
2820        });
2821        let expected: [BigRational; D] =
2822            from_fn(|col| BigRational::new(numerators[col].clone(), denominators[col].into()));
2823        for bits in [0, 137] {
2824            let row_scales: [BigInt; D] = from_fn(|row| {
2825                let magnitude = if bits == 0 {
2826                    BigInt::from(1)
2827                } else {
2828                    (BigInt::from(1) << (bits + 17 * row)) + 1
2829                };
2830                if row.is_multiple_of(2) {
2831                    -magnitude
2832                } else {
2833                    magnitude
2834                }
2835            });
2836            // D*I + ones is nonsingular for D>0; row scaling and column
2837            // scaling by nonzero integers preserve invertibility.
2838            let coefficients: [[BigInt; D]; D] =
2839                from_fn(|row| from_fn(|col| BigInt::from(if row == col { D + 1 } else { 1 })));
2840            let mut rows: [[BigInt; D]; D] = from_fn(|row| {
2841                from_fn(|col| &row_scales[row] * &coefficients[row][col] * denominators[col])
2842            });
2843            let mut rhs: [BigInt; D] = from_fn(|row| {
2844                &row_scales[row]
2845                    * coefficients[row]
2846                        .iter()
2847                        .zip(&numerators)
2848                        .map(|(a, n)| a * n)
2849                        .sum::<BigInt>()
2850            });
2851            for _ in 0..D.max(1) {
2852                let actual = solve_big_int(rows.clone(), rhs.clone()).unwrap();
2853                for (actual, expected) in actual.iter().zip(&expected) {
2854                    assert_eq!(actual.numer(), expected.numer());
2855                    assert_eq!(actual.denom(), expected.denom());
2856                }
2857                rows.rotate_left(usize::from(D > 0));
2858                rhs.rotate_left(usize::from(D > 0));
2859            }
2860        }
2861    }
2862
2863    macro_rules! gen_integer_solve_canonical_tests {
2864        ($d:literal) => {
2865            paste! {
2866                #[test]
2867                fn [<integer_solve_canonical_components_ $d d>]() {
2868                    check_integer_solve_canonical_components::<$d>();
2869                }
2870            }
2871        };
2872    }
2873
2874    gen_integer_solve_canonical_tests!(0);
2875    gen_integer_solve_canonical_tests!(1);
2876    gen_integer_solve_canonical_tests!(2);
2877    gen_integer_solve_canonical_tests!(3);
2878    gen_integer_solve_canonical_tests!(4);
2879    gen_integer_solve_canonical_tests!(5);
2880    gen_integer_solve_canonical_tests!(6);
2881    gen_integer_solve_canonical_tests!(7);
2882    gen_integer_solve_canonical_tests!(8);
2883
2884    /// For D ≤ 4, `solve_exact_f64` should agree with `Lu::solve` on
2885    /// well-conditioned matrices.
2886    macro_rules! gen_solve_exact_f64_agrees_with_lu {
2887        ($d:literal) => {
2888            paste! {
2889                #[test]
2890                fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
2891                    // Diagonally dominant integer matrix with an exactly
2892                    // representable target solution.  The exact result can
2893                    // therefore be parsed into f64 without rounding.
2894                    let mut rows = [[0.0f64; $d]; $d];
2895                    for r in 0..$d {
2896                        for c in 0..$d {
2897                            rows[r][c] = if r == c {
2898                                f64::from($d) + 1.0
2899                            } else {
2900                                1.0
2901                            };
2902                        }
2903                    }
2904                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2905                    let x_true = {
2906                        let mut arr = [0.0f64; $d];
2907                        for (dst, src) in arr.iter_mut().zip([1.0, -2.0, 3.0, -4.0, 5.0]) {
2908                            *dst = src;
2909                        }
2910                        arr
2911                    };
2912                    let mut b_arr = [0.0f64; $d];
2913                    for i in 0..$d {
2914                        let mut sum = 0.0;
2915                        for j in 0..$d {
2916                            sum = rows[i][j].mul_add(x_true[j], sum);
2917                        }
2918                        b_arr[i] = sum;
2919                    }
2920                    let b = Vector::<$d>::new(b_arr);
2921                    let exact = a.solve_exact_f64(b).unwrap().into_array();
2922                    let lu_sol = a.lu(DEFAULT_SINGULAR_TOL).unwrap()
2923                        .solve(b).unwrap().into_array();
2924                    for i in 0..$d {
2925                        assert_eq!(exact[i].to_bits(), x_true[i].to_bits());
2926                        let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
2927                        assert!((exact[i] - lu_sol[i]).abs() <= eps);
2928                    }
2929                }
2930            }
2931        };
2932    }
2933
2934    gen_solve_exact_f64_agrees_with_lu!(2);
2935    gen_solve_exact_f64_agrees_with_lu!(3);
2936    gen_solve_exact_f64_agrees_with_lu!(4);
2937    gen_solve_exact_f64_agrees_with_lu!(5);
2938
2939    /// Round-trip: for a well-conditioned integer matrix `A` and integer
2940    /// target `x0`, solving `A x = A x0` must return `x0` exactly.  All
2941    /// intermediate values stay small enough that `A * x0` is exactly
2942    /// representable in `f64`, so the round-trip is a precise equality
2943    /// check on the hybrid BigInt/BigRational path.
2944    macro_rules! gen_solve_exact_roundtrip_tests {
2945        ($d:literal) => {
2946            paste! {
2947                #[test]
2948                #[expect(
2949                    clippy::cast_precision_loss,
2950                    reason = "dimensions and indices are at most five and exactly representable as f64"
2951                )]
2952                fn [<solve_exact_roundtrip_ $d d>]() {
2953                    // A = D * I + J (diag = D+1, off-diag = 1).  Invertible
2954                    // for any D >= 1 and cheap to multiply by hand.
2955                    let mut rows = [[0.0f64; $d]; $d];
2956                    for r in 0..$d {
2957                        for c in 0..$d {
2958                            rows[r][c] = if r == c {
2959                                f64::from($d) + 1.0
2960                            } else {
2961                                1.0
2962                            };
2963                        }
2964                    }
2965                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2966
2967                    // x0 = [1, 2, ..., D].
2968                    let mut x0 = [0.0f64; $d];
2969                    for i in 0..$d {
2970                        x0[i] = (i + 1) as f64;
2971                    }
2972
2973                    // b = A * x0 computed in f64.  With small integers the
2974                    // multiply-add sequence is exact.
2975                    let mut b_arr = [0.0f64; $d];
2976                    for r in 0..$d {
2977                        let mut sum = 0.0_f64;
2978                        for c in 0..$d {
2979                            sum = rows[r][c].mul_add(x0[c], sum);
2980                        }
2981                        b_arr[r] = sum;
2982                    }
2983                    let b = Vector::<$d>::new(b_arr);
2984
2985                    let x = a.solve_exact(b).unwrap();
2986                    for i in 0..$d {
2987                        assert_eq!(x.as_array()[i], f64_to_big_rational(x0[i]));
2988                    }
2989                }
2990            }
2991        };
2992    }
2993
2994    gen_solve_exact_roundtrip_tests!(2);
2995    gen_solve_exact_roundtrip_tests!(3);
2996    gen_solve_exact_roundtrip_tests!(4);
2997    gen_solve_exact_roundtrip_tests!(5);
2998
2999    // -----------------------------------------------------------------------
3000    // solve_exact: dimension-specific tests
3001    // -----------------------------------------------------------------------
3002
3003    #[test]
3004    fn solve_exact_d0_returns_empty() {
3005        let a = Matrix::<0>::zero();
3006        let b = Vector::<0>::zero();
3007        let x = a.solve_exact(b).unwrap();
3008        assert_eq!(x.as_array().as_slice(), []);
3009    }
3010
3011    #[test]
3012    fn solve_exact_known_2x2() {
3013        // [[1,2],[3,4]] x = [5, 11] → x = [1, 2]
3014        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
3015        let b = Vector::<2>::new([5.0, 11.0]);
3016        let x = a.solve_exact(b).unwrap();
3017        assert_eq!(x.as_array()[0], BigRational::from_integer(BigInt::from(1)));
3018        assert_eq!(x.as_array()[1], BigRational::from_integer(BigInt::from(2)));
3019    }
3020
3021    #[test]
3022    fn solve_exact_pivoting_needed() {
3023        // First column has zero on diagonal → pivot swap required.
3024        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]])
3025            .unwrap();
3026        let b = Vector::<3>::new([2.0, 3.0, 4.0]);
3027        let x = a.solve_exact(b).unwrap();
3028        // x = [3, 2, 4]
3029        assert_eq!(x.as_array()[0], f64_to_big_rational(3.0));
3030        assert_eq!(x.as_array()[1], f64_to_big_rational(2.0));
3031        assert_eq!(x.as_array()[2], f64_to_big_rational(4.0));
3032    }
3033
3034    #[test]
3035    fn solve_exact_fractional_result() {
3036        // [[2, 1], [1, 3]] x = [1, 1] → x = [2/5, 1/5]
3037        let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
3038        let b = Vector::<2>::new([1.0, 1.0]);
3039        let x = a.solve_exact(b).unwrap();
3040        assert_eq!(
3041            x.as_array()[0],
3042            BigRational::new(BigInt::from(2), BigInt::from(5))
3043        );
3044        assert_eq!(
3045            x.as_array()[1],
3046            BigRational::new(BigInt::from(1), BigInt::from(5))
3047        );
3048    }
3049
3050    #[test]
3051    fn solve_exact_singular_duplicate_rows() {
3052        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]])
3053            .unwrap();
3054        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
3055        assert_matches!(
3056            a.solve_exact(b),
3057            Err(LaError::Singular {
3058                reason: SingularityReason::Exact,
3059                ..
3060            })
3061        );
3062    }
3063
3064    #[test]
3065    fn solve_exact_5x5_permutation() {
3066        // Permutation matrix (swap rows 0↔1): P x = b → x = P^T b.
3067        let a = Matrix::<5>::try_from_rows([
3068            [0.0, 1.0, 0.0, 0.0, 0.0],
3069            [1.0, 0.0, 0.0, 0.0, 0.0],
3070            [0.0, 0.0, 1.0, 0.0, 0.0],
3071            [0.0, 0.0, 0.0, 1.0, 0.0],
3072            [0.0, 0.0, 0.0, 0.0, 1.0],
3073        ])
3074        .unwrap();
3075        let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
3076        let x = a.solve_exact(b).unwrap();
3077        assert_eq!(x.as_array()[0], f64_to_big_rational(20.0));
3078        assert_eq!(x.as_array()[1], f64_to_big_rational(10.0));
3079        assert_eq!(x.as_array()[2], f64_to_big_rational(30.0));
3080        assert_eq!(x.as_array()[3], f64_to_big_rational(40.0));
3081        assert_eq!(x.as_array()[4], f64_to_big_rational(50.0));
3082    }
3083
3084    /// Entries near `f64::MAX / 2` are finite but their product would
3085    /// overflow to ±∞ in pure f64 arithmetic.  The `BigInt` augmented-system
3086    /// path computes the correct solution without any overflow.  The D×D
3087    /// case uses a diagonal matrix with `big` on every diagonal and a RHS
3088    /// of `[big, …, big, 0]`, giving the known solution `[1, …, 1, 0]`.
3089    macro_rules! gen_solve_exact_large_finite_entries_tests {
3090        ($d:literal) => {
3091            paste! {
3092                #[test]
3093                fn [<solve_exact_large_finite_entries_ $d d>]() {
3094                    let big = f64::MAX / 2.0;
3095                    assert!(big.is_finite());
3096                    // D×D diagonal matrix with `big` on the diagonal.
3097                    let mut rows = [[0.0f64; $d]; $d];
3098                    for i in 0..$d {
3099                        rows[i][i] = big;
3100                    }
3101                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3102                    // RHS = [big, …, big, 0] → x = [1, …, 1, 0].
3103                    let mut b_arr = [big; $d];
3104                    b_arr[$d - 1] = 0.0;
3105                    let b = Vector::<$d>::new(b_arr);
3106                    let x = a.solve_exact(b).unwrap();
3107                    for i in 0..($d - 1) {
3108                        assert_eq!(x.as_array()[i], BigRational::from_integer(BigInt::from(1)));
3109                    }
3110                    assert_eq!(x.as_array()[$d - 1], BigRational::from_integer(BigInt::from(0)));
3111                }
3112            }
3113        };
3114    }
3115
3116    gen_solve_exact_large_finite_entries_tests!(2);
3117    gen_solve_exact_large_finite_entries_tests!(3);
3118    gen_solve_exact_large_finite_entries_tests!(4);
3119    gen_solve_exact_large_finite_entries_tests!(5);
3120
3121    /// Matrix and RHS entries span many orders of magnitude (from
3122    /// `f64::MIN_POSITIVE` up through `1e100`). This exercises each
3123    /// collection's independently derived minimum exponent: even the largest
3124    /// within-collection shift remains a representable `BigInt`. The D×D case
3125    /// alternates `huge`/`tiny` along the diagonal with a matching RHS, giving
3126    /// `x = [1, …, 1]`.
3127    macro_rules! gen_solve_exact_mixed_magnitude_entries_tests {
3128        ($d:literal) => {
3129            paste! {
3130                #[test]
3131                fn [<solve_exact_mixed_magnitude_entries_ $d d>]() {
3132                    let tiny = f64::MIN_POSITIVE; // 2^-1022, smallest normal
3133                    let huge = 1.0e100_f64;
3134                    // Alternate huge/tiny along the diagonal.
3135                    let mut rows = [[0.0f64; $d]; $d];
3136                    let mut b_arr = [0.0f64; $d];
3137                    for i in 0..$d {
3138                        let val = if i % 2 == 0 { huge } else { tiny };
3139                        rows[i][i] = val;
3140                        b_arr[i] = val;
3141                    }
3142                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3143                    let b = Vector::<$d>::new(b_arr);
3144                    let x = a.solve_exact(b).unwrap();
3145                    for i in 0..$d {
3146                        assert_eq!(x.as_array()[i], BigRational::from_integer(BigInt::from(1)));
3147                    }
3148                }
3149            }
3150        };
3151    }
3152
3153    gen_solve_exact_mixed_magnitude_entries_tests!(2);
3154    gen_solve_exact_mixed_magnitude_entries_tests!(3);
3155    gen_solve_exact_mixed_magnitude_entries_tests!(4);
3156    gen_solve_exact_mixed_magnitude_entries_tests!(5);
3157
3158    #[test]
3159    fn solve_exact_restores_independent_matrix_and_rhs_scales() {
3160        let large = 2.0_f64.powi(500);
3161        let tiny = 2.0_f64.powi(-1000);
3162
3163        let large_matrix = Matrix::<2>::try_from_rows([[large, 0.0], [0.0, large]]).unwrap();
3164        let tiny_rhs = Vector::<2>::new([tiny, -2.0 * tiny]);
3165        let small_solution = large_matrix.solve_exact(tiny_rhs).unwrap();
3166        assert_eq!(
3167            small_solution.as_array()[0],
3168            BigRational::new(BigInt::from(1_u8), BigInt::from(1_u8) << 1500_u32)
3169        );
3170        assert_eq!(
3171            small_solution.as_array()[1],
3172            BigRational::new(BigInt::from(-1_i8), BigInt::from(1_u8) << 1499_u32)
3173        );
3174
3175        let tiny_matrix = Matrix::<1>::try_from_rows([[tiny]]).unwrap();
3176        let large_rhs = Vector::<1>::new([large]);
3177        let large_solution = tiny_matrix.solve_exact(large_rhs).unwrap();
3178        assert_eq!(
3179            large_solution.as_array()[0],
3180            BigRational::from_integer(BigInt::from(1_u8) << 1500_u32)
3181        );
3182    }
3183
3184    /// Subnormal RHS entries must survive the decomposition and
3185    /// back-substitution paths unchanged.  The D×D case uses the identity
3186    /// matrix and RHS `[1·tiny, 2·tiny, …, D·tiny]`; each entry remains a
3187    /// valid subnormal f64 (integer multiples of `2^-1074` fit in the
3188    /// 52-bit subnormal mantissa for the small integers used here).
3189    macro_rules! gen_solve_exact_subnormal_rhs_tests {
3190        ($d:literal) => {
3191            paste! {
3192                #[test]
3193                #[expect(
3194                    clippy::cast_precision_loss,
3195                    reason = "indices are at most five and exactly representable as f64"
3196                )]
3197                fn [<solve_exact_subnormal_rhs_ $d d>]() {
3198                    let tiny = 5e-324_f64; // smallest positive subnormal
3199                    assert!(tiny.is_subnormal());
3200                    let a = Matrix::<$d>::identity();
3201                    // b[i] = (i+1) · tiny — each entry remains a valid subnormal.
3202                    let mut b_arr = [0.0f64; $d];
3203                    for i in 0..$d {
3204                        b_arr[i] = (i + 1) as f64 * tiny;
3205                        assert!(b_arr[i].is_subnormal());
3206                    }
3207                    let b = Vector::<$d>::new(b_arr);
3208                    let x = a.solve_exact(b).unwrap();
3209                    for i in 0..$d {
3210                        assert_eq!(x.as_array()[i], f64_to_big_rational((i + 1) as f64 * tiny));
3211                    }
3212                }
3213            }
3214        };
3215    }
3216
3217    gen_solve_exact_subnormal_rhs_tests!(2);
3218    gen_solve_exact_subnormal_rhs_tests!(3);
3219    gen_solve_exact_subnormal_rhs_tests!(4);
3220    gen_solve_exact_subnormal_rhs_tests!(5);
3221
3222    /// Pivoting path with a zero top-left entry forces a row swap in the
3223    /// `BigInt` forward-elimination loop and propagates it to the RHS.
3224    /// Combined with a fractional solution, this exercises the
3225    /// common-denominator back-substitution after integer forward elimination.
3226    ///
3227    /// The 2×2 block `[[0, 1], [2, 1]]` with rhs `[3, 4]` (→ `x = [1/2, 3]`)
3228    /// is embedded into the top-left of a D×D identity matrix.  Remaining
3229    /// rows contribute pass-through equalities `x[i] = b[i]`, so the same
3230    /// fractional solution appears at indices 0 and 1 regardless of D.
3231    macro_rules! gen_solve_exact_pivot_swap_fractional_tests {
3232        ($d:literal) => {
3233            paste! {
3234                #[test]
3235                #[expect(
3236                    clippy::cast_precision_loss,
3237                    reason = "indices and test offsets are small integers exactly representable as f64"
3238                )]
3239                fn [<solve_exact_pivot_swap_with_fractional_result_ $d d>]() {
3240                    // Top-left 2×2: A = [[0, 1], [2, 1]].  After swap:
3241                    // [[2, 1], [0, 1]], rhs = [4, 3] → x[1] = 3, x[0] = 1/2.
3242                    let mut rows = [[0.0f64; $d]; $d];
3243                    rows[0][1] = 1.0;
3244                    rows[1][0] = 2.0;
3245                    rows[1][1] = 1.0;
3246                    // Identity padding for the remaining rows.
3247                    for (i, row) in rows.iter_mut().enumerate().skip(2) {
3248                        row[i] = 1.0;
3249                    }
3250                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3251                    // b = [3, 4, 12, 13, …]; padded entries are arbitrary
3252                    // finite integers so the identity block gives x[i] = b[i].
3253                    let mut b_arr = [0.0f64; $d];
3254                    b_arr[0] = 3.0;
3255                    b_arr[1] = 4.0;
3256                    for (i, value) in b_arr.iter_mut().enumerate().skip(2) {
3257                        *value = (i + 10) as f64;
3258                    }
3259                    let b = Vector::<$d>::new(b_arr);
3260                    let x = a.solve_exact(b).unwrap();
3261                    assert_eq!(x.as_array()[0], BigRational::new(BigInt::from(1), BigInt::from(2)));
3262                    assert_eq!(x.as_array()[1], BigRational::from_integer(BigInt::from(3)));
3263                    for (i, value) in x.as_array().iter().enumerate().skip(2) {
3264                        assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
3265                    }
3266                }
3267            }
3268        };
3269    }
3270
3271    gen_solve_exact_pivot_swap_fractional_tests!(2);
3272    gen_solve_exact_pivot_swap_fractional_tests!(3);
3273    gen_solve_exact_pivot_swap_fractional_tests!(4);
3274    gen_solve_exact_pivot_swap_fractional_tests!(5);
3275
3276    /// Mid-elimination pivot swap: the 3×3 block
3277    /// `[[1, 2, 3], [0, 0, 4], [0, 5, 6]]` has a non-zero pivot at k=0 but
3278    /// a zero pivot at k=1, so the swap happens *during* forward
3279    /// elimination rather than at the start.  With rhs `[6, 7, 8]` the
3280    /// exact solution is `[7/4, -1/2, 7/4]`.  For D > 3 the block is
3281    /// embedded into the top-left of a D×D identity matrix so the same
3282    /// fractional solution appears in `x[0..3]` and `x[i] = b[i]` for
3283    /// `i >= 3`.
3284    macro_rules! gen_solve_exact_mid_pivot_swap_tests {
3285        ($d:literal) => {
3286            paste! {
3287                #[test]
3288                #[expect(
3289                    clippy::cast_precision_loss,
3290                    reason = "indices and test offsets are small integers exactly representable as f64"
3291                )]
3292                fn [<solve_exact_mid_pivot_swap_ $d d>]() {
3293                    let mut rows = [[0.0f64; $d]; $d];
3294                    rows[0][0] = 1.0; rows[0][1] = 2.0; rows[0][2] = 3.0;
3295                    // rows[1][0..2] are zero; rows[1][2] = 4.
3296                    rows[1][2] = 4.0;
3297                    rows[2][1] = 5.0; rows[2][2] = 6.0;
3298                    // Identity padding for the remaining rows.
3299                    for (i, row) in rows.iter_mut().enumerate().skip(3) {
3300                        row[i] = 1.0;
3301                    }
3302                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3303                    let mut b_arr = [0.0f64; $d];
3304                    b_arr[0] = 6.0;
3305                    b_arr[1] = 7.0;
3306                    b_arr[2] = 8.0;
3307                    for (i, value) in b_arr.iter_mut().enumerate().skip(3) {
3308                        *value = (i + 10) as f64;
3309                    }
3310                    let b = Vector::<$d>::new(b_arr);
3311                    let x = a.solve_exact(b).unwrap();
3312                    // x[0..3] = [7/4, -1/2, 7/4].
3313                    assert_eq!(x.as_array()[0], BigRational::new(BigInt::from(7), BigInt::from(4)));
3314                    assert_eq!(x.as_array()[1], BigRational::new(BigInt::from(-1), BigInt::from(2)));
3315                    assert_eq!(x.as_array()[2], BigRational::new(BigInt::from(7), BigInt::from(4)));
3316                    for (i, value) in x.as_array().iter().enumerate().skip(3) {
3317                        assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
3318                    }
3319                }
3320            }
3321        };
3322    }
3323
3324    gen_solve_exact_mid_pivot_swap_tests!(3);
3325    gen_solve_exact_mid_pivot_swap_tests!(4);
3326    gen_solve_exact_mid_pivot_swap_tests!(5);
3327
3328    /// Rank-deficient singular: the last column is identically zero and the
3329    /// leading `(D-1)×(D-1)` block is full rank, so every intermediate
3330    /// pivot is non-zero and the singularity surfaces only at the final
3331    /// column.  The matrix is identity in the top-left `(D-1)×(D-1)` with
3332    /// a row of ones as the last row (and an all-zero last column), so the
3333    /// rank is exactly `D-1`.  `solve_exact` must return
3334    /// exact singularity at `pivot_col = D - 1`.
3335    macro_rules! gen_solve_exact_singular_rank_deficient_tests {
3336        ($d:literal) => {
3337            paste! {
3338                #[test]
3339                fn [<solve_exact_singular_rank_deficient_ $d d>]() {
3340                    let mut rows = [[0.0f64; $d]; $d];
3341                    for i in 0..($d - 1) {
3342                        rows[i][i] = 1.0;
3343                        rows[$d - 1][i] = 1.0;
3344                    }
3345                    // Last column is left all-zero → rank exactly D-1.
3346                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3347                    let b = Vector::<$d>::new([1.0; $d]);
3348                    assert_matches!(
3349                        a.solve_exact(b),
3350                        Err(LaError::Singular {
3351                            pivot_col,
3352                            reason: SingularityReason::Exact,
3353                            ..
3354                        }) if pivot_col == $d - 1
3355                    );
3356                }
3357            }
3358        };
3359    }
3360
3361    gen_solve_exact_singular_rank_deficient_tests!(2);
3362    gen_solve_exact_singular_rank_deficient_tests!(3);
3363    gen_solve_exact_singular_rank_deficient_tests!(4);
3364    gen_solve_exact_singular_rank_deficient_tests!(5);
3365
3366    /// Zero RHS with a non-singular matrix.  Every Bareiss update reads
3367    /// `rhs[k]` and `rhs[i]`, both initialised to zero; every update
3368    /// produces zero; back-substitution therefore yields `x = 0`
3369    /// regardless of the matrix entries.  This exercises the
3370    /// common-denominator back-substitution against an all-zero `rhs`.
3371    macro_rules! gen_solve_exact_zero_rhs_tests {
3372        ($d:literal) => {
3373            paste! {
3374                #[test]
3375                fn [<solve_exact_zero_rhs_ $d d>]() {
3376                    // A = D*I + J (diagonally dominant, invertible).
3377                    let mut rows = [[1.0f64; $d]; $d];
3378                    for i in 0..$d {
3379                        rows[i][i] = f64::from($d) + 1.0;
3380                    }
3381                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
3382                    let b = Vector::<$d>::zero();
3383                    let x = a.solve_exact(b).unwrap();
3384                    for xi in x.as_array() {
3385                        assert_eq!(*xi, BigRational::from_integer(BigInt::from(0)));
3386                    }
3387                }
3388            }
3389        };
3390    }
3391
3392    gen_solve_exact_zero_rhs_tests!(2);
3393    gen_solve_exact_zero_rhs_tests!(3);
3394    gen_solve_exact_zero_rhs_tests!(4);
3395    gen_solve_exact_zero_rhs_tests!(5);
3396
3397    // -----------------------------------------------------------------------
3398    // Adversarial-input coverage mirroring `benches/exact.rs`
3399    // -----------------------------------------------------------------------
3400    //
3401    // These tests pin the behaviour of the extreme-input benchmark groups
3402    // (`exact_near_singular_3x3`, `exact_large_entries_3x3`,
3403    // `exact_hilbert_{4x4,5x5}`) so a regression would be caught even
3404    // when benchmarks are not running.
3405
3406    /// Multiply `A · x` entirely in `BigRational`, using `f64_to_big_rational`
3407    /// to lift each matrix entry.  Used by residual assertions for inputs
3408    /// whose exact solution has no closed form we can easily type out.
3409    fn big_rational_matvec<const D: usize>(
3410        a: &Matrix<D>,
3411        x: &[BigRational; D],
3412    ) -> [BigRational; D] {
3413        from_fn(|i| {
3414            let mut sum = BigRational::from_integer(BigInt::from(0));
3415            for (aij, xj) in a.as_rows()[i].iter().zip(x.iter()) {
3416                sum += f64_to_big_rational(*aij) * xj;
3417            }
3418            sum
3419        })
3420    }
3421
3422    fn hilbert<const D: usize>() -> Matrix<D> {
3423        let rows = from_fn(|r| from_fn(|c| 1.0 / f64::from(u32::try_from(r + c + 1).unwrap())));
3424        Matrix::<D>::try_from_rows(rows).unwrap()
3425    }
3426
3427    /// Near-singular 3×3 solve (matches the `exact_near_singular_3x3`
3428    /// bench).  With `A = [[1+2^-50, 2, 3], [4, 5, 6], [7, 8, 9]]` and
3429    /// `x0 = [1, 1, 1]`, `A · x0 = [6 + 2^-50, 15, 24]`; every component is
3430    /// exactly representable in `f64` (`6` has ulp `2^-50` at its exponent).
3431    /// `solve_exact` must recover `x0` exactly — the fractional denominator
3432    /// introduced by `det(A) = -3 × 2^-50` cancels cleanly against the
3433    /// augmented RHS.
3434    #[test]
3435    fn solve_exact_near_singular_3x3_integer_x0() {
3436        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
3437        let a = Matrix::<3>::try_from_rows([
3438            [1.0 + perturbation, 2.0, 3.0],
3439            [4.0, 5.0, 6.0],
3440            [7.0, 8.0, 9.0],
3441        ])
3442        .unwrap();
3443        let b = Vector::<3>::new([6.0 + perturbation, 15.0, 24.0]);
3444        let x = a.solve_exact(b).unwrap();
3445        let one = BigRational::from_integer(BigInt::from(1));
3446        assert_eq!(x.as_array()[0], one);
3447        assert_eq!(x.as_array()[1], one);
3448        assert_eq!(x.as_array()[2], one);
3449    }
3450
3451    /// Large-entry 3×3 solve (matches the `exact_large_entries_3x3`
3452    /// bench).  `A = big · I + (1 - I)` with `big = f64::MAX / 2` and
3453    /// `b = [big, 1, 1] = A · [1, 0, 0]`.  The `BigInt` augmented system
3454    /// sees entries of ~1023 bits on the diagonal and unit entries
3455    /// elsewhere; Bareiss elimination still produces the exact integer
3456    /// solution `[1, 0, 0]`.
3457    #[test]
3458    fn solve_exact_large_entries_3x3_unit_vector() {
3459        let big = f64::MAX / 2.0;
3460        assert!(big.is_finite());
3461        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
3462            .unwrap();
3463        let b = Vector::<3>::new([big, 1.0, 1.0]);
3464        let x = a.solve_exact(b).unwrap();
3465        let zero = BigRational::from_integer(BigInt::from(0));
3466        let one = BigRational::from_integer(BigInt::from(1));
3467        assert_eq!(x.as_array()[0], one);
3468        assert_eq!(x.as_array()[1], zero);
3469        assert_eq!(x.as_array()[2], zero);
3470    }
3471
3472    /// Determinant of the large-entry 3×3 is roughly `big^3`, which
3473    /// overflows `f64`. `det_direct()` therefore reports a computed
3474    /// [`LaError::NonFinite`], the fast filter inside `det_sign_exact`
3475    /// treats that as inconclusive, and the direct `BigInt` fallback resolves
3476    /// the positive sign correctly. `det_exact_f64` must report `Unrepresentable`.
3477    #[test]
3478    fn det_sign_exact_large_entries_3x3_positive() {
3479        let big = f64::MAX / 2.0;
3480        let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
3481            .unwrap();
3482        // Fast filter is inconclusive (big^3 overflows f64 to +∞), so
3483        // this exercises the direct `BigInt` cold path.
3484        assert_matches!(
3485            a.det_direct(),
3486            Err(LaError::NonFinite {
3487                location: NonFiniteLocation::Scalar,
3488                origin: NonFiniteOrigin::Computation {
3489                    operation: ArithmeticOperation::Determinant,
3490                    ..
3491                },
3492                ..
3493            })
3494        );
3495        assert_eq!(a.det_sign_exact(), DeterminantSign::Positive);
3496        // Cross-validate: the exact `BigRational` determinant must agree
3497        // on sign with `det_sign_exact`, and `det_exact_f64` must reject the
3498        // conversion (the value is representable in BigRational but far exceeds f64).
3499        assert!(a.det_exact().unwrap().is_positive());
3500        assert_unrepresentable(&a.det_exact_f64(), None, UnrepresentableReason::NotFinite);
3501    }
3502
3503    /// Hilbert matrices are symmetric positive-definite, so
3504    /// `det_sign_exact` must return [`DeterminantSign::Positive`] for every D.
3505    /// For D=2..=4 the
3506    /// fast f64 filter resolves the positive sign without falling
3507    /// through (Hilbert's determinant is tiny but still well above the
3508    /// `det_errbound` cushion); for D=5 the filter is skipped entirely
3509    /// and the Bareiss path handles inputs whose `(mantissa, exponent)`
3510    /// pairs all differ.
3511    macro_rules! gen_det_sign_exact_hilbert_positive_tests {
3512        ($d:literal) => {
3513            paste! {
3514                #[test]
3515                fn [<det_sign_exact_hilbert_positive_ $d d>]() {
3516                    let h = hilbert::<$d>();
3517                    assert_eq!(h.det_sign_exact(), DeterminantSign::Positive);
3518                }
3519            }
3520        };
3521    }
3522
3523    gen_det_sign_exact_hilbert_positive_tests!(2);
3524    gen_det_sign_exact_hilbert_positive_tests!(3);
3525    gen_det_sign_exact_hilbert_positive_tests!(4);
3526    gen_det_sign_exact_hilbert_positive_tests!(5);
3527
3528    /// `solve_exact` on a Hilbert matrix must produce a solution whose
3529    /// residual `A · x - b` is *exactly* zero in `BigRational` arithmetic.
3530    /// Hilbert entries (`1/3`, `1/5`, `1/6`, `1/7`, …) are non-terminating
3531    /// in binary, so this is a stronger test than the
3532    /// `gen_solve_exact_roundtrip_tests` construction (which requires the
3533    /// RHS to be representable as an exact `f64` product).
3534    macro_rules! gen_solve_exact_hilbert_residual_tests {
3535        ($d:literal) => {
3536            paste! {
3537                #[test]
3538                fn [<solve_exact_hilbert_residual_ $d d>]() {
3539                    let h = hilbert::<$d>();
3540                    // Use a non-trivial RHS with both positive and negative
3541                    // entries to avoid accidental structural cancellation.
3542                    let mut b_arr = [0.0f64; $d];
3543                    for i in 0usize..$d {
3544                        let sign = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
3545                        b_arr[i] = sign * f64::from(u32::try_from(i + 1).unwrap());
3546                    }
3547                    let b = Vector::<$d>::new(b_arr);
3548                    let x = h.solve_exact(b).unwrap();
3549                    let ax = big_rational_matvec(&h, x.as_array());
3550                    for i in 0..$d {
3551                        assert_eq!(ax[i], f64_to_big_rational(b_arr[i]));
3552                    }
3553                }
3554            }
3555        };
3556    }
3557
3558    gen_solve_exact_hilbert_residual_tests!(2);
3559    gen_solve_exact_hilbert_residual_tests!(3);
3560    gen_solve_exact_hilbert_residual_tests!(4);
3561    gen_solve_exact_hilbert_residual_tests!(5);
3562
3563    // -----------------------------------------------------------------------
3564    // solve_exact_f64: dimension-specific tests
3565    // -----------------------------------------------------------------------
3566
3567    #[test]
3568    fn solve_exact_f64_known_2x2() {
3569        let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
3570        let b = Vector::<2>::new([5.0, 11.0]);
3571        let x = a.solve_exact_f64(b).unwrap().into_array();
3572        assert!((x[0] - 1.0).abs() <= f64::EPSILON);
3573        assert!((x[1] - 2.0).abs() <= f64::EPSILON);
3574    }
3575
3576    #[test]
3577    fn solve_exact_f64_overflow_returns_err() {
3578        // [[1/big, 0], [0, 1/big]] x = [big, big] → x = [big², big²],
3579        // which overflows f64.
3580        let big = f64::MAX / 2.0;
3581        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
3582        let b = Vector::<2>::new([big, big]);
3583        assert_unrepresentable(
3584            &a.solve_exact_f64(b),
3585            Some(0),
3586            UnrepresentableReason::NotFinite,
3587        );
3588    }
3589
3590    #[test]
3591    fn solve_exact_f64_huge_non_dyadic_component_returns_not_finite() {
3592        let a = Matrix::<1>::try_from_rows([[3.0 * f64::MIN_POSITIVE]]).unwrap();
3593        let b = Vector::<1>::new([f64::MAX]);
3594
3595        assert_unrepresentable(
3596            &a.solve_exact_f64(b),
3597            Some(0),
3598            UnrepresentableReason::NotFinite,
3599        );
3600    }
3601
3602    #[test]
3603    fn solve_exact_rounded_f64_overflow_returns_err() {
3604        let big = f64::MAX / 2.0;
3605        let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
3606        let b = Vector::<2>::new([big, big]);
3607
3608        assert_unrepresentable(
3609            &a.solve_exact_rounded_f64(b),
3610            Some(0),
3611            UnrepresentableReason::NotFinite,
3612        );
3613    }
3614
3615    #[test]
3616    fn solve_exact_f64_underflow_returns_err_for_nonzero_exact_component() {
3617        let tiny = f64::from_bits(1);
3618        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
3619        let b = Vector::<1>::new([tiny]);
3620
3621        assert_unrepresentable(
3622            &a.solve_exact_f64(b),
3623            Some(0),
3624            UnrepresentableReason::RequiresRounding,
3625        );
3626    }
3627
3628    #[test]
3629    fn solve_exact_f64_accepts_smallest_subnormal_result() {
3630        let tiny = f64::from_bits(1);
3631        let a = Matrix::<1>::identity();
3632        let b = Vector::<1>::new([tiny]);
3633
3634        assert_eq!(
3635            a.solve_exact_f64(b).unwrap().into_array()[0].to_bits(),
3636            tiny.to_bits()
3637        );
3638    }
3639
3640    // -----------------------------------------------------------------------
3641    // exact solve boundary tests
3642    // -----------------------------------------------------------------------
3643
3644    #[test]
3645    fn bareiss_solve_d1() {
3646        let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
3647        let b = Vector::<1>::new([6.0]);
3648        let x = a.solve_exact(b).unwrap();
3649        assert_eq!(x.as_array()[0], f64_to_big_rational(3.0));
3650    }
3651
3652    #[test]
3653    fn bareiss_solve_singular_column_all_zero() {
3654        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]])
3655            .unwrap();
3656        let b = Vector::<3>::new([1.0, 2.0, 3.0]);
3657        assert_matches!(
3658            a.solve_exact(b),
3659            Err(LaError::Singular {
3660                pivot_col: 1,
3661                reason: SingularityReason::Exact,
3662                ..
3663            })
3664        );
3665    }
3666
3667    // -----------------------------------------------------------------------
3668    // f64_to_big_rational tests
3669    // -----------------------------------------------------------------------
3670
3671    #[test]
3672    fn f64_to_big_rational_scalar_cases() {
3673        let cases = [
3674            ("positive zero", 0.0, 0, 1),
3675            ("negative zero", -0.0, 0, 1),
3676            ("one", 1.0, 1, 1),
3677            ("negative one", -1.0, -1, 1),
3678            ("half", 0.5, 1, 2),
3679            ("quarter", 0.25, 1, 4),
3680            ("negative three and a half", -3.5, -7, 2),
3681            ("integer", 42.0, 42, 1),
3682            ("power of two", 1024.0, 1024, 1),
3683        ];
3684
3685        for (label, value, numerator, denominator) in cases {
3686            assert_eq!(
3687                f64_to_big_rational(value),
3688                BigRational::new(BigInt::from(numerator), BigInt::from(denominator)),
3689                "{label}"
3690            );
3691        }
3692    }
3693
3694    #[test]
3695    fn f64_to_big_rational_subnormal() {
3696        let tiny = 5e-324_f64; // smallest positive subnormal
3697        assert!(tiny.is_subnormal());
3698        let r = f64_to_big_rational(tiny);
3699        // 5e-324 = 1 × 2^(-1074)
3700        assert_eq!(
3701            r,
3702            BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
3703        );
3704    }
3705
3706    #[test]
3707    fn f64_to_big_rational_already_lowest_terms() {
3708        // 0.5 should produce numer=1, denom=2 (already reduced).
3709        let r = f64_to_big_rational(0.5);
3710        assert_eq!(*r.numer(), BigInt::from(1));
3711        assert_eq!(*r.denom(), BigInt::from(2));
3712    }
3713
3714    #[test]
3715    fn f64_to_big_rational_round_trip() {
3716        // -0.0 is excluded: it maps to BigRational(0) which round-trips
3717        // to +0.0 (covered by the negative-zero case in f64_to_big_rational_scalar_cases).
3718        let values = [
3719            0.0,
3720            1.0,
3721            -1.0,
3722            0.5,
3723            0.25,
3724            0.1,
3725            42.0,
3726            -3.5,
3727            1e10,
3728            1e-10,
3729            f64::MAX / 2.0,
3730            f64::MIN_POSITIVE,
3731            5e-324,
3732        ];
3733        for &v in &values {
3734            let r = f64_to_big_rational(v);
3735            let back = r.to_f64().expect("round-trip to_f64 failed");
3736            assert_eq!(
3737                v.to_bits(),
3738                back.to_bits(),
3739                "round-trip failed for {v}: got {back}"
3740            );
3741        }
3742    }
3743}