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);