diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4e9463..12aa440 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Added
+
+- Guard README dependency snippets [`7137fee`](https://github.com/acgetchell/la-stack/commit/7137fee16ab33e08f4dc6a60e02417e3e7c4e020)
+
+ - Add a generic docs-version sync check that compares Markdown dependency
+ snippets against the Cargo package name and version
+
+ - Run the docs-version check from the repository Semgrep policy lane
+ - Refresh README determinant examples with explicit fallible handling and
+ hidden doctest mirrors
+
+ - Update CI uv pins to 0.11.19
+
+### Documentation
+
+- Sync citation metadata for v0.4.2 [`f473ec5`](https://github.com/acgetchell/la-stack/commit/f473ec50946e5f668e9ad9a2d978e499dcb10f04)
+
+ - Update CITATION.cff with the v0.4.2 version and release date.
+ - Align the Python utility package metadata and lockfile with the crate release.
+ - Add citation metadata validation to the release checklist and config lint flow.
+ - Include CITATION.cff in YAML/CFF formatting checks.
+
## [0.4.2] - 2026-06-04
### Added
@@ -78,6 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- State that LU and LDLT solve_vec use floating-point substitution without a certified absolute rounding-error bound.
- Clarify that inf_norm reports NonFinite for unchecked stored NaN/∞ as well as row-sum overflow.
- Exercise the unchecked finite-proof fixture path directly in exact tests.
+- Update v0.4.2 release notes [`7e11f93`](https://github.com/acgetchell/la-stack/commit/7e11f930b94bbba99c1c426e68a515bbefb8c489)
### Fixed
@@ -626,6 +651,7 @@ Older releases are archived by minor series:
- [0.2.x](docs/archive/changelog/0.2.md)
- [0.1.x](docs/archive/changelog/0.1.md)
+[Unreleased]: https://github.com/acgetchell/la-stack/compare/v0.4.2...HEAD
[0.4.2]: https://github.com/acgetchell/la-stack/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/acgetchell/la-stack/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/acgetchell/la-stack/compare/v0.3.0...v0.4.0
diff --git a/scripts/check_docs_version_sync.py b/scripts/check_docs_version_sync.py
index 16ace05..b459269 100644
--- a/scripts/check_docs_version_sync.py
+++ b/scripts/check_docs_version_sync.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import os
import re
import sys
import tomllib
@@ -68,12 +69,16 @@ def _read_cargo_package_info(cargo_toml: Path) -> PackageInfo:
def _iter_markdown_files(root: Path) -> list[Path]:
- return sorted(path for path in root.rglob("*.md") if path.is_file() and not (set(path.relative_to(root).parts) & SKIP_DIRS))
+ markdown_files: list[Path] = []
+ for dirpath, dirnames, filenames in os.walk(root):
+ dirnames[:] = [dirname for dirname in dirnames if not (set((Path(dirpath) / dirname).relative_to(root).parts) & SKIP_DIRS)]
+ markdown_files.extend(Path(dirpath) / filename for filename in filenames if filename.endswith(".md"))
+ return sorted(markdown_files)
def _dependency_regex(package_name: str) -> re.Pattern[str]:
escaped_name = re.escape(package_name)
- return re.compile(rf'(?[^"]+)"|\{{\s*version\s*=\s*"(?P
[^"]+)")')
+ return re.compile(rf'(?[^"]+)"|\{{[^}}]*version\s*=\s*"(?P[^"]+)"[^}}]*\}})')
def _dependency_snippets(path: Path, package_name: str) -> list[DependencySnippet]:
diff --git a/scripts/tests/test_check_docs_version_sync.py b/scripts/tests/test_check_docs_version_sync.py
index e4e3c48..cfbe071 100644
--- a/scripts/tests/test_check_docs_version_sync.py
+++ b/scripts/tests/test_check_docs_version_sync.py
@@ -1,9 +1,14 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
import check_docs_version_sync
+if TYPE_CHECKING:
+ from pathlib import Path
+
-def test_find_version_mismatches_accepts_matching_dependency_snippets(tmp_path) -> None:
+def test_find_version_mismatches_accepts_matching_dependency_snippets(tmp_path: Path) -> None:
(tmp_path / "Cargo.toml").write_text(
"\n".join(
[
@@ -28,7 +33,7 @@ def test_find_version_mismatches_accepts_matching_dependency_snippets(tmp_path)
assert check_docs_version_sync.find_version_mismatches(tmp_path) == []
-def test_find_version_mismatches_reports_stale_dependency_snippets(tmp_path) -> None:
+def test_find_version_mismatches_reports_stale_dependency_snippets(tmp_path: Path) -> None:
(tmp_path / "Cargo.toml").write_text(
"\n".join(
[
@@ -54,3 +59,32 @@ def test_find_version_mismatches_reports_stale_dependency_snippets(tmp_path) ->
assert mismatches[0].snippet.version == "1.2.2"
assert mismatches[0].package.name == "other-crate"
assert mismatches[0].package.version == "1.2.3"
+
+
+def test_find_version_mismatches_handles_reordered_inline_table_keys(tmp_path: Path) -> None:
+ (tmp_path / "Cargo.toml").write_text(
+ "\n".join(
+ [
+ "[package]",
+ 'name = "other-crate"',
+ 'version = "1.2.3"',
+ ]
+ ),
+ encoding="utf-8",
+ )
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ install_doc = docs / "install.md"
+ install_doc.write_text(
+ 'other-crate = { features = ["exact"], version = "1.2.2" }\n',
+ encoding="utf-8",
+ )
+
+ mismatches = check_docs_version_sync.find_version_mismatches(tmp_path)
+
+ assert len(mismatches) == 1
+ assert mismatches[0].snippet.path == install_doc
+ assert mismatches[0].snippet.line == 1
+ assert mismatches[0].snippet.version == "1.2.2"
+ assert mismatches[0].package.name == "other-crate"
+ assert mismatches[0].package.version == "1.2.3"
diff --git a/src/exact.rs b/src/exact.rs
index 0334c10..8d978de 100644
--- a/src/exact.rs
+++ b/src/exact.rs
@@ -13,9 +13,10 @@
//! `e - e_min`), and Bareiss elimination runs entirely in `BigInt`
//! arithmetic — no `BigRational`, no GCD, no denominator tracking.
//! The result is `(det_int, total_exp)` where `det = det_int × 2^(D × e_min)`.
-//! `bareiss_det` wraps this with `bigint_exp_to_bigrational` to reconstruct
-//! a reduced `BigRational`; `det_sign_exact` reads the sign directly from
-//! `det_int` (the scale factor is always positive).
+//! `det_exact` wraps this with `bigint_exp_to_bigrational` to reconstruct a
+//! reduced `BigRational`; `det_exact_f64` converts the same pair directly to
+//! `f64`; and `det_sign_exact` reads the sign directly from `det_int` (the
+//! scale factor is always positive).
//!
//! `det_sign_exact` adds a two-stage adaptive-precision optimisation inspired
//! by Shewchuk's robust geometric predicates:
@@ -142,7 +143,8 @@ fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
let exp_abs = exp.unsigned_abs();
let reduce = tz.min(u64::from(exp_abs));
value >>= reduce;
- let reduce = u32::try_from(reduce).unwrap_or(u32::MAX);
+ #[allow(clippy::cast_possible_truncation)]
+ let reduce = reduce as u32;
let remaining_abs = exp_abs - reduce;
exp = match remaining_abs {
0 => 0,
@@ -158,6 +160,31 @@ fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
}
}
+/// Convert a `BigInt × 2^exp` determinant pair to finite `f64` without first
+/// reducing a public `BigRational` determinant value.
+fn bigint_exp_to_finite_f64(value: BigInt, exp: i32) -> Result {
+ if value == BigInt::from(0) {
+ return Ok(0.0);
+ }
+
+ let exact = if exp >= 0 {
+ BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
+ } else {
+ BigRational::new_raw(value, BigInt::from(1u32) << exp.unsigned_abs())
+ };
+
+ let Some(val) = exact.to_f64() else {
+ cold_path();
+ return Err(LaError::Overflow { index: None });
+ };
+ if val.is_finite() {
+ Ok(val)
+ } else {
+ cold_path();
+ Err(LaError::Overflow { index: None })
+ }
+}
+
// -----------------------------------------------------------------------
// Shared integer-Bareiss primitives
// -----------------------------------------------------------------------
@@ -353,6 +380,28 @@ fn bareiss_forward_eliminate(
BareissResult::Upper { sign }
}
+/// Compute the determinant scale exponent `D × e_min`.
+///
+/// This centralizes the scale-overflow classification used by public exact
+/// determinant APIs: [`Matrix::det_exact`], [`Matrix::det_exact_f64`], and
+/// [`Matrix::det_sign_exact`] all surface failures from this helper as
+/// [`LaError::DeterminantScaleOverflow`].
+///
+/// # Errors
+/// Returns [`LaError::DeterminantScaleOverflow`] if `D` cannot fit in the
+/// internal `i32` exponent multiplier or if `D × e_min` overflows `i32`.
+fn determinant_scale_exp(e_min: i32) -> Result {
+ let Ok(d_i32) = i32::try_from(D) else {
+ cold_path();
+ return Err(LaError::determinant_scale_overflow(D, e_min));
+ };
+ let Some(total_exp) = e_min.checked_mul(d_i32) else {
+ cold_path();
+ return Err(LaError::determinant_scale_overflow(D, e_min));
+ };
+ Ok(total_exp)
+}
+
/// Compute the exact determinant using integer-only Bareiss elimination.
///
/// Returns `(det_int, scale_exp)` where the true determinant is
@@ -393,16 +442,7 @@ fn bareiss_det_int_finite(m: &FiniteMatrix) -> Result<(BigInt
a[D - 1][D - 1].clone()
};
- // det(original) = det_int × 2^(D × e_min)
- let Ok(d_i32) = i32::try_from(D) else {
- cold_path();
- return Err(LaError::unsupported_dimension(D, i32::MAX as usize));
- };
- let Some(total_exp) = e_min.checked_mul(d_i32) else {
- cold_path();
- return Err(LaError::Overflow { index: None });
- };
-
+ let total_exp = determinant_scale_exp::(e_min)?;
Ok((det_int, total_exp))
}
@@ -489,21 +529,15 @@ impl FiniteMatrix {
/// Exact determinant converted to a finite `f64`.
///
/// # Errors
+ /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
+ /// overflows the internal exponent representation.
+ ///
/// Returns [`LaError::Overflow`] if the exact determinant cannot be
/// represented as a finite `f64`.
#[inline]
fn det_exact_f64(&self) -> Result {
- let exact = self.det_exact()?;
- let Some(val) = exact.to_f64() else {
- cold_path();
- return Err(LaError::Overflow { index: None });
- };
- if val.is_finite() {
- Ok(val)
- } else {
- cold_path();
- Err(LaError::Overflow { index: None })
- }
+ let (det_int, total_exp) = bareiss_det_int_finite(self)?;
+ bigint_exp_to_finite_f64(det_int, total_exp)
}
/// Exact linear solve for finite inputs.
@@ -548,6 +582,9 @@ impl FiniteMatrix {
/// Returns [`LaError::NonFinite`] if a direct determinant or error-bound
/// computation detects a non-finite condition that is not an inconclusive
/// scalar overflow.
+ ///
+ /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
+ /// overflows the internal exponent representation.
#[inline]
fn det_sign_exact(&self) -> Result {
match (self.det_direct(), self.det_errbound()) {
@@ -607,11 +644,8 @@ impl Matrix {
/// # Errors
/// Returns [`LaError::NonFinite`] if stored matrix entries are NaN or infinity.
///
- /// Returns [`LaError::Overflow`] if determinant scaling overflows the internal
- /// exponent representation.
- ///
- /// Returns [`LaError::UnsupportedDimension`] if `D` cannot be represented in
- /// the internal determinant exponent calculation.
+ /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
+ /// overflows the internal exponent representation.
#[inline]
pub fn det_exact(&self) -> Result {
FiniteMatrix::new(*self)?.det_exact()
@@ -621,10 +655,12 @@ impl Matrix {
///
/// Requires the `exact` Cargo feature.
///
- /// Computes the exact [`BigRational`] determinant via [`det_exact`](Self::det_exact)
- /// and converts it to the nearest `f64`. This is useful when you want the
- /// most accurate f64 determinant possible without committing to `BigRational`
- /// in your downstream code.
+ /// Computes the exact determinant with the same integer Bareiss core used by
+ /// [`det_exact`](Self::det_exact), then converts the exact scaled integer
+ /// result to the nearest `f64` without first materializing the public
+ /// [`BigRational`] determinant. This is useful when you want the most accurate
+ /// f64 determinant possible without committing to `BigRational` in your
+ /// downstream code.
///
/// # Examples
/// ```
@@ -641,8 +677,10 @@ impl Matrix {
/// # Errors
/// Returns [`LaError::NonFinite`] if stored matrix entries are NaN or infinity.
///
- /// Returns [`LaError::Overflow`] if determinant scaling overflows the internal
- /// exponent representation or if the exact determinant is too large to
+ /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
+ /// overflows the internal exponent representation.
+ ///
+ /// Returns [`LaError::Overflow`] if the exact determinant is too large to
/// represent as a finite `f64`.
#[inline]
pub fn det_exact_f64(&self) -> Result {
@@ -778,7 +816,9 @@ impl Matrix {
///
/// # Errors
/// Returns [`LaError::NonFinite`] if stored matrix entries are NaN or infinity.
- /// This exact sign path has no additional runtime errors for finite matrices.
+ ///
+ /// Returns [`LaError::DeterminantScaleOverflow`] if determinant scaling
+ /// overflows the internal exponent representation.
#[inline]
pub fn det_sign_exact(&self) -> Result {
FiniteMatrix::new(*self)?.det_sign_exact()
@@ -1340,6 +1380,33 @@ mod tests {
assert_eq!(component_to_bigint(negative, 1), BigInt::from(-20));
}
+ #[test]
+ fn determinant_scale_exp_multiplies_dimension_and_min_exponent() {
+ assert_eq!(determinant_scale_exp::<4>(-1074), Ok(-4296));
+ }
+
+ #[test]
+ fn determinant_scale_exp_rejects_dimension_too_large_for_i32() {
+ assert_eq!(
+ determinant_scale_exp::<{ i32::MAX as usize + 1 }>(-1074),
+ Err(LaError::DeterminantScaleOverflow {
+ dim: i32::MAX as usize + 1,
+ min_exponent: -1074,
+ })
+ );
+ }
+
+ #[test]
+ fn determinant_scale_exp_rejects_exponent_product_overflow() {
+ assert_eq!(
+ determinant_scale_exp::<3_000_000>(-1074),
+ Err(LaError::DeterminantScaleOverflow {
+ dim: 3_000_000,
+ min_exponent: -1074,
+ })
+ );
+ }
+
// -----------------------------------------------------------------------
// bareiss_det_int tests
// -----------------------------------------------------------------------
diff --git a/src/ldlt.rs b/src/ldlt.rs
index 0761091..02940d7 100644
--- a/src/ldlt.rs
+++ b/src/ldlt.rs
@@ -282,7 +282,6 @@ mod tests {
use crate::DEFAULT_SINGULAR_TOL;
use crate::matrix::FiniteMatrix;
- use core::assert_matches;
use core::hint::black_box;
use approx::assert_abs_diff_eq;
@@ -571,25 +570,6 @@ mod tests {
);
}
- #[test]
- fn invalid_tolerance_rejected() {
- assert_eq!(
- Tolerance::new(-1.0),
- Err(LaError::InvalidTolerance { value: -1.0 })
- );
-
- assert_matches!(
- Tolerance::new(f64::NAN),
- Err(LaError::InvalidTolerance { value }) if value.is_nan()
- );
- assert_eq!(
- Tolerance::new(f64::INFINITY),
- Err(LaError::InvalidTolerance {
- value: f64::INFINITY,
- })
- );
- }
-
macro_rules! gen_solve_vec_boundary_tests {
($d:literal) => {
paste! {
diff --git a/src/lib.rs b/src/lib.rs
index b7b80d7..d0fee4a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -403,6 +403,13 @@ pub enum LaError {
/// component that overflowed. `None` for scalar results.
index: Option,
},
+ /// Exact determinant scaling overflowed the internal exponent representation.
+ DeterminantScaleOverflow {
+ /// Matrix dimension `D`.
+ dim: usize,
+ /// Minimum decomposed f64 exponent among non-zero matrix entries.
+ min_exponent: i32,
+ },
/// A requested runtime matrix dimension has no stack-dispatch arm.
UnsupportedDimension {
/// Runtime dimension requested by the caller.
@@ -500,6 +507,26 @@ impl LaError {
Self::NonFinite { row: None, col }
}
+ /// Construct a [`LaError::DeterminantScaleOverflow`] for exact determinant scaling.
+ ///
+ /// # Examples
+ /// ```
+ /// use la_stack::prelude::*;
+ ///
+ /// assert_eq!(
+ /// LaError::determinant_scale_overflow(3, -1074),
+ /// LaError::DeterminantScaleOverflow {
+ /// dim: 3,
+ /// min_exponent: -1074,
+ /// }
+ /// );
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn determinant_scale_overflow(dim: usize, min_exponent: i32) -> Self {
+ Self::DeterminantScaleOverflow { dim, min_exponent }
+ }
+
/// Construct a [`LaError::UnsupportedDimension`] for runtime stack dispatch.
///
/// # Examples
@@ -648,6 +675,12 @@ impl fmt::Display for LaError {
Self::Overflow { index: None } => {
write!(f, "exact result overflows the target representation")
}
+ Self::DeterminantScaleOverflow { dim, min_exponent } => {
+ write!(
+ f,
+ "exact determinant scale exponent overflows for dimension {dim} with minimum entry exponent {min_exponent}"
+ )
+ }
Self::UnsupportedDimension { requested, max } => {
write!(
f,
@@ -918,6 +951,18 @@ mod tests {
);
}
+ #[test]
+ fn laerror_display_formats_determinant_scale_overflow() {
+ let err = LaError::DeterminantScaleOverflow {
+ dim: 3,
+ min_exponent: -1074,
+ };
+ assert_eq!(
+ err.to_string(),
+ "exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
+ );
+ }
+
#[test]
fn laerror_display_formats_unsupported_dimension() {
let err = LaError::UnsupportedDimension {
diff --git a/src/lu.rs b/src/lu.rs
index ac92ed8..21a6eea 100644
--- a/src/lu.rs
+++ b/src/lu.rs
@@ -266,7 +266,6 @@ mod tests {
use super::*;
use crate::DEFAULT_PIVOT_TOL;
- use core::assert_matches;
use core::hint::black_box;
use approx::assert_abs_diff_eq;
@@ -501,25 +500,6 @@ mod tests {
assert_eq!(err, LaError::Singular { pivot_col: 0 });
}
- #[test]
- fn invalid_tolerance_rejected() {
- assert_eq!(
- Tolerance::new(-1.0),
- Err(LaError::InvalidTolerance { value: -1.0 })
- );
-
- assert_matches!(
- Tolerance::new(f64::NAN),
- Err(LaError::InvalidTolerance { value }) if value.is_nan()
- );
- assert_eq!(
- Tolerance::new(f64::INFINITY),
- Err(LaError::InvalidTolerance {
- value: f64::INFINITY,
- })
- );
- }
-
#[test]
fn matrix_constructor_rejects_nonfinite_pivot_entry() {
let err = Matrix::<2>::try_from_rows([[f64::NAN, 0.0], [0.0, 1.0]]).unwrap_err();
diff --git a/src/matrix.rs b/src/matrix.rs
index ac57cda..32965a9 100644
--- a/src/matrix.rs
+++ b/src/matrix.rs
@@ -7,6 +7,12 @@ use crate::lu::Lu;
use crate::{ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4, LDLT_SYMMETRY_REL_TOL, LaError, Tolerance};
/// Fixed-size square matrix `D×D`, stored inline.
+///
+/// `Matrix` is designed for small, robustness-sensitive systems where stack
+/// allocation and const-generic dimensions are useful. For large, dynamic, sparse,
+/// or parallel workloads, prefer a broader linear-algebra crate such as
+/// [`nalgebra`](https://crates.io/crates/nalgebra) or
+/// [`faer`](https://crates.io/crates/faer).
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Matrix {
diff --git a/src/vector.rs b/src/vector.rs
index b2ce6a9..8314b91 100644
--- a/src/vector.rs
+++ b/src/vector.rs
@@ -39,14 +39,11 @@ impl FiniteVector {
/// entry index when `vector` contains NaN or infinity.
#[inline]
pub(crate) const fn new(vector: Vector) -> Result {
- let mut i = 0;
- while i < D {
- if !vector.data[i].is_finite() {
- return Err(LaError::non_finite_at(i));
- }
- i += 1;
+ if let Some(index) = Vector::::first_non_finite_entry_in(&vector.data) {
+ Err(LaError::non_finite_at(index))
+ } else {
+ Ok(Self::new_unchecked(vector))
}
- Ok(Self::new_unchecked(vector))
}
/// Validate raw vector storage and construct a finite vector.
@@ -199,14 +196,11 @@ impl Vector {
/// `data` contains NaN or infinity.
#[inline]
pub const fn try_new(data: [f64; D]) -> Result {
- let mut i = 0;
- while i < D {
- if !data[i].is_finite() {
- return Err(LaError::non_finite_at(i));
- }
- i += 1;
+ if let Some(index) = Self::first_non_finite_entry_in(&data) {
+ Err(LaError::non_finite_at(index))
+ } else {
+ Ok(Self::new_unchecked(data))
}
- Ok(Self::new_unchecked(data))
}
/// Construct a vector without checking that entries are finite.
@@ -218,6 +212,22 @@ impl Vector {
Self { data }
}
+ /// Return the first non-finite stored entry in index order.
+ ///
+ /// Shared by the public raw-storage boundary and the internal finite-vector
+ /// proof wrapper so both paths report the same first offending index with
+ /// [`LaError::NonFinite`].
+ const fn first_non_finite_entry_in(data: &[f64; D]) -> Option {
+ let mut i = 0;
+ while i < D {
+ if !data[i].is_finite() {
+ return Some(i);
+ }
+ i += 1;
+ }
+ None
+ }
+
/// All-zeros vector.
///
/// # Examples
diff --git a/tests/proptest_exact.rs b/tests/proptest_exact.rs
index c67afda..9ad5c76 100644
--- a/tests/proptest_exact.rs
+++ b/tests/proptest_exact.rs
@@ -35,6 +35,23 @@ fn small_int_f64() -> impl Strategy {
(-10i32..=10i32).prop_map(f64::from)
}
+fn mixed_scale_finite_f64() -> impl Strategy {
+ prop_oneof![
+ Just(0.0),
+ Just(-0.0),
+ Just(f64::from_bits(1)),
+ Just(-f64::from_bits(1)),
+ Just(f64::MIN_POSITIVE),
+ Just(-f64::MIN_POSITIVE),
+ Just(1.0),
+ Just(-1.0),
+ Just(3.5),
+ Just(-7.25),
+ Just(f64::MAX / 4.0),
+ Just(-f64::MAX / 4.0),
+ ]
+}
+
/// Multiply `A · x` entirely in `BigRational`, lifting each f64 matrix
/// entry via `BigRational::from_f64`. Used by residual assertions.
///
@@ -333,3 +350,63 @@ macro_rules! gen_det_sign_fast_filter_boundary_proptests {
gen_det_sign_fast_filter_boundary_proptests!(2);
gen_det_sign_fast_filter_boundary_proptests!(3);
gen_det_sign_fast_filter_boundary_proptests!(4);
+
+/// Mixed-scale diagonal matrices stress the shared-exponent conversion path:
+/// zeros, subnormals, tiny normal values, ordinary values, and very large
+/// finite values can all appear in the same determinant. The independent
+/// expectation uses `BigRational::from_f64` on each diagonal value.
+macro_rules! gen_mixed_scale_diagonal_exact_det_proptests {
+ ($d:literal) => {
+ paste! {
+ proptest! {
+ #![proptest_config(ProptestConfig::with_cases(32))]
+
+ #[test]
+ fn [](
+ diag in proptest::array::[](mixed_scale_finite_f64()),
+ ) {
+ let mut rows = [[0.0f64; $d]; $d];
+ let mut expected = BigRational::from_integer(BigInt::from(1));
+ for i in 0..$d {
+ rows[i][i] = diag[i];
+ expected *= BigRational::from_f64(diag[i])
+ .expect("strategy only emits finite f64 values");
+ }
+ let m = Matrix::<$d>::try_from_rows(rows).unwrap();
+
+ let expected_sign = if expected.is_positive() {
+ 1
+ } else if expected.is_negative() {
+ -1
+ } else {
+ 0
+ };
+ let expected_f64 = expected.to_f64();
+
+ prop_assert_eq!(m.det_exact().unwrap(), expected);
+ prop_assert_eq!(m.det_sign_exact().unwrap(), expected_sign);
+
+ match expected_f64 {
+ Some(expected_f64) if expected_f64.is_finite() => {
+ prop_assert_eq!(
+ m.det_exact_f64().unwrap().to_bits(),
+ expected_f64.to_bits()
+ );
+ }
+ _ => {
+ prop_assert_eq!(
+ m.det_exact_f64(),
+ Err(LaError::Overflow { index: None })
+ );
+ }
+ }
+ }
+ }
+ }
+ };
+}
+
+gen_mixed_scale_diagonal_exact_det_proptests!(2);
+gen_mixed_scale_diagonal_exact_det_proptests!(3);
+gen_mixed_scale_diagonal_exact_det_proptests!(4);
+gen_mixed_scale_diagonal_exact_det_proptests!(5);