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