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