Skip to content

Commit 7a664ed

Browse files
committed
refactor(exact): encode nonzero mantissas in exact decomposition
- Replace the exact-arithmetic zero mantissa sentinel with `Option<NonZeroU64>`. - Carry nonzero mantissa proof through matrix/vector decomposition and BigInt scaling. - Clarify determinant documentation around uncertified `det()` bounds. - Keep SPD determinant proptests on the tolerance-aware LU path. Closes #120
1 parent 18a4e44 commit 7a664ed

4 files changed

Lines changed: 62 additions & 30 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[![Audit dependencies](https://github.com/acgetchell/la-stack/actions/workflows/audit.yml/badge.svg)](https://github.com/acgetchell/la-stack/actions/workflows/audit.yml)
1212
[![Codacy Security Scan](https://github.com/acgetchell/la-stack/actions/workflows/codacy.yml/badge.svg)](https://github.com/acgetchell/la-stack/actions/workflows/codacy.yml)
1313

14-
![la-stack](docs/assets/la-stack.jpg)
14+
![la-stack](https://raw.githubusercontent.com/acgetchell/la-stack/main/docs/assets/la-stack.jpg)
1515

1616
Fast, stack-allocated linear algebra for fixed dimensions in Rust.
1717

src/exact.rs

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
7171
use core::hint::cold_path;
7272
use core::mem::take;
73+
use core::num::NonZeroU64;
7374
use std::array::from_fn;
7475

7576
use num_bigint::{BigInt, Sign};
@@ -82,13 +83,14 @@ use crate::vector::Vector;
8283

8384
/// Decompose a finite `f64` into its IEEE 754 components.
8485
///
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\].
86+
/// Returns `None` for ±0.0, or `Some((mantissa, exponent, is_negative))` with a
87+
/// non-zero mantissa where the value is exactly
88+
/// `(-1)^is_negative × mantissa × 2^exponent` and `mantissa` is odd (trailing
89+
/// zeros stripped). See `REFERENCES.md` \[9-10\].
8890
///
8991
/// # Panics
9092
/// Panics if `x` is NaN or infinite.
91-
fn f64_decompose(x: f64) -> Option<(u64, i32, bool)> {
93+
fn f64_decompose(x: f64) -> Option<(NonZeroU64, i32, bool)> {
9294
let bits = x.to_bits();
9395
let biased_exp = ((bits >> 52) & 0x7FF) as i32;
9496
let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
@@ -114,6 +116,7 @@ fn f64_decompose(x: f64) -> Option<(u64, i32, bool)> {
114116
// Strip trailing zeros so the mantissa is odd.
115117
let tz = mantissa.trailing_zeros();
116118
let mantissa = mantissa >> tz;
119+
let mantissa = NonZeroU64::new(mantissa)?;
117120
let exponent = raw_exp + tz.cast_signed();
118121
let is_negative = bits >> 63 != 0;
119122

@@ -161,13 +164,15 @@ fn bigint_exp_to_bigrational(mut value: BigInt, mut exp: i32) -> BigRational {
161164

162165
/// Decomposed finite f64 in the form `(-1)^is_negative · mantissa · 2^exponent`.
163166
///
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
167+
/// Zero entries have `mantissa == None`; the other fields are unused in that
168+
/// case. `Default` yields such a zero component, which is what the
166169
/// per-entry initialiser in `decompose_matrix` / `decompose_vec` produces
167-
/// for ±0.0 cells.
170+
/// for ±0.0 cells. Non-zero entries carry a [`NonZeroU64`] mantissa, so the
171+
/// exact-arithmetic paths cannot accidentally store a raw zero sentinel after
172+
/// decomposition.
168173
#[derive(Clone, Copy, Default)]
169174
struct Component {
170-
mantissa: u64,
175+
mantissa: Option<NonZeroU64>,
171176
exponent: i32,
172177
is_negative: bool,
173178
}
@@ -191,7 +196,7 @@ fn decompose_matrix<const D: usize>(m: &Matrix<D>) -> Result<([[Component; D]; D
191196
}
192197
if let Some((mantissa, exponent, is_negative)) = f64_decompose(entry) {
193198
components[r][c] = Component {
194-
mantissa,
199+
mantissa: Some(mantissa),
195200
exponent,
196201
is_negative,
197202
};
@@ -220,7 +225,7 @@ fn decompose_vec<const D: usize>(v: &Vector<D>) -> Result<([Component; D], i32),
220225
}
221226
if let Some((mantissa, exponent, is_negative)) = f64_decompose(entry) {
222227
components[i] = Component {
223-
mantissa,
228+
mantissa: Some(mantissa),
224229
exponent,
225230
is_negative,
226231
};
@@ -232,15 +237,17 @@ fn decompose_vec<const D: usize>(v: &Vector<D>) -> Result<([Component; D], i32),
232237

233238
/// Convert a single decomposed component to its scaled `BigInt`
234239
/// representation: `(±mantissa) << (exp − e_min)`. Zero components map
235-
/// to `BigInt::from(0)`.
240+
/// to `BigInt::from(0)` through the `None` case; non-zero components reuse
241+
/// their carried [`NonZeroU64`] proof without revalidating the mantissa.
236242
#[inline]
237243
fn 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+
c.mantissa.map_or_else(
245+
|| BigInt::from(0),
246+
|mantissa| {
247+
let v = BigInt::from(mantissa.get()) << (c.exponent - e_min).cast_unsigned();
248+
if c.is_negative { -v } else { v }
249+
},
250+
)
244251
}
245252

246253
/// Build a `D×D` integer matrix from a component table, scaled to the
@@ -758,9 +765,9 @@ mod tests {
758765
};
759766

760767
let numer = if is_negative {
761-
-BigInt::from(mantissa)
768+
-BigInt::from(mantissa.get())
762769
} else {
763-
BigInt::from(mantissa)
770+
BigInt::from(mantissa.get())
764771
};
765772

766773
if exponent >= 0 {
@@ -1158,7 +1165,7 @@ mod tests {
11581165
#[test]
11591166
fn f64_decompose_one() {
11601167
let (mant, exp, neg) = f64_decompose(1.0).unwrap();
1161-
assert_eq!(mant, 1);
1168+
assert_eq!(mant.get(), 1);
11621169
assert_eq!(exp, 0);
11631170
assert!(!neg);
11641171
}
@@ -1167,7 +1174,7 @@ mod tests {
11671174
fn f64_decompose_negative() {
11681175
let (mant, exp, neg) = f64_decompose(-3.5).unwrap();
11691176
// -3.5 = -7 × 2^(-1), mantissa is 7 (odd after stripping)
1170-
assert_eq!(mant, 7);
1177+
assert_eq!(mant.get(), 7);
11711178
assert_eq!(exp, -1);
11721179
assert!(neg);
11731180
}
@@ -1177,15 +1184,15 @@ mod tests {
11771184
let tiny = 5e-324_f64;
11781185
assert!(tiny.is_subnormal());
11791186
let (mant, exp, neg) = f64_decompose(tiny).unwrap();
1180-
assert_eq!(mant, 1);
1187+
assert_eq!(mant.get(), 1);
11811188
assert_eq!(exp, -1074);
11821189
assert!(!neg);
11831190
}
11841191

11851192
#[test]
11861193
fn f64_decompose_power_of_two() {
11871194
let (mant, exp, neg) = f64_decompose(1024.0).unwrap();
1188-
assert_eq!(mant, 1);
1195+
assert_eq!(mant.get(), 1);
11891196
assert_eq!(exp, 10); // 1024 = 2^10
11901197
assert!(!neg);
11911198
}
@@ -1196,6 +1203,28 @@ mod tests {
11961203
f64_decompose(f64::NAN);
11971204
}
11981205

1206+
#[test]
1207+
fn component_to_bigint_distinguishes_zero_from_nonzero_mantissa() {
1208+
assert_eq!(
1209+
component_to_bigint(Component::default(), -10),
1210+
BigInt::from(0)
1211+
);
1212+
1213+
let positive = Component {
1214+
mantissa: NonZeroU64::new(3),
1215+
exponent: 4,
1216+
is_negative: false,
1217+
};
1218+
assert_eq!(component_to_bigint(positive, 1), BigInt::from(24));
1219+
1220+
let negative = Component {
1221+
mantissa: NonZeroU64::new(5),
1222+
exponent: 3,
1223+
is_negative: true,
1224+
};
1225+
assert_eq!(component_to_bigint(negative, 1), BigInt::from(-20));
1226+
}
1227+
11991228
// -----------------------------------------------------------------------
12001229
// bareiss_det_int tests
12011230
// -----------------------------------------------------------------------

src/matrix.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -751,11 +751,14 @@ impl<const D: usize> Matrix<D> {
751751
/// speedup (see [`det_direct`](Self::det_direct)).
752752
///
753753
/// Finite inputs return a floating-point determinant estimate in every dimension;
754-
/// this method does not surface [`LaError::Singular`]. For D ≥ 5, the LU
755-
/// fallback only maps an exactly zero pivot to `Ok(0.0)`. Use [`lu`](Self::lu)
756-
/// directly when you need tolerance-aware singularity detection or the pivot
757-
/// column, and use the exact determinant APIs when exact singularity
758-
/// classification matters.
754+
/// this method does not surface [`LaError::Singular`]. Because it mixes
755+
/// closed-form paths from [`det_direct`](Self::det_direct) with an LU fallback,
756+
/// the returned value has no certified absolute error bound. Use
757+
/// [`det_errbound`](Self::det_errbound) for D ≤ 4 bounds, or the exact
758+
/// determinant APIs when exact singularity classification or certified values
759+
/// matter. For D ≥ 5, the LU fallback only maps an exactly zero pivot to
760+
/// `Ok(0.0)`. Use [`lu`](Self::lu) directly when you need tolerance-aware
761+
/// singularity detection or the pivot column.
759762
///
760763
/// # Examples
761764
/// ```

tests/proptest_matrix.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ macro_rules! gen_public_api_matrix_proptests {
212212
let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
213213

214214
let det_ldlt = ldlt.det().unwrap();
215-
let det_lu = a.det().unwrap();
215+
let det_lu = a.lu(DEFAULT_PIVOT_TOL).unwrap().det().unwrap();
216216
assert_abs_diff_eq!(det_ldlt, det_lu, epsilon = 1e-8);
217217

218218
let b = Vector::<$d>::new(b_arr);

0 commit comments

Comments
 (0)