#![forbid(unsafe_code)]
use core::hint::cold_path;
use core::mem::take;
use core::num::NonZeroU64;
use std::array::from_fn;
use num_bigint::{BigInt, Sign};
use num_rational::BigRational;
use num_traits::ToPrimitive;
use crate::matrix::Matrix;
use crate::vector::Vector;
use crate::{LaError, UnrepresentableReason};
#[must_use]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeterminantSign {
Negative,
Zero,
Positive,
}
impl DeterminantSign {
#[inline]
#[must_use]
pub const fn as_i8(self) -> i8 {
match self {
Self::Negative => -1,
Self::Zero => 0,
Self::Positive => 1,
}
}
}
pub trait ExactF64Conversion {
type Output;
fn try_to_f64(&self) -> Result<Self::Output, LaError>;
fn to_rounded_f64(&self) -> Result<Self::Output, LaError>;
}
const F64_SIGNIFICAND_BITS: i64 = 53;
const F64_FRACTION_BITS: i64 = 52;
const F64_MIN_BINARY_EXPONENT: i64 = -1074;
const F64_MIN_NORMAL_EXPONENT: i64 = -1022;
const F64_MAX_BINARY_EXPONENT: i64 = 1023;
const F64_EXPONENT_BIAS: i64 = 1023;
const F64_FRACTION_MASK: u64 = (1u64 << 52) - 1;
const fn decompose_proven_finite_f64(x: f64) -> Component {
let bits = x.to_bits();
let biased_exp = ((bits >> 52) & 0x7FF) as i32;
let fraction = bits & 0x000F_FFFF_FFFF_FFFF;
if biased_exp == 0 && fraction == 0 {
return Component::Zero;
}
let (mantissa, raw_exp) = if biased_exp == 0 {
(fraction, -1074_i32)
} else {
((1u64 << 52) | fraction, biased_exp - 1075)
};
let tz = mantissa.trailing_zeros();
let Some(mantissa) = NonZeroU64::new(mantissa >> tz) else {
return Component::Zero;
};
Component::NonZero {
mantissa,
exponent: raw_exp + tz.cast_signed(),
is_negative: bits >> 63 != 0,
}
}
#[cfg(test)]
const fn decompose_f64(x: f64) -> Result<Component, LaError> {
let bits = x.to_bits();
let biased_exp = ((bits >> 52) & 0x7FF) as i32;
if biased_exp == 0x7FF {
cold_path();
return Err(LaError::non_finite_input_scalar());
}
Ok(decompose_proven_finite_f64(x))
}
fn big_int_exp_to_big_rational(mut value: BigInt, mut exp: i32) -> BigRational {
if value == BigInt::from(0) {
return BigRational::from_integer(BigInt::from(0));
}
if exp < 0
&& let Some(tz) = value.trailing_zeros()
{
let exp_abs = exp.unsigned_abs();
let reduce = tz.min(u64::from(exp_abs));
value >>= reduce;
let remaining_abs = u64::from(exp_abs) - reduce;
exp = negative_exponent_from_magnitude(remaining_abs);
}
if exp >= 0 {
BigRational::new_raw(value << exp.cast_unsigned(), BigInt::from(1u32))
} else {
BigRational::new_raw(value, BigInt::from(1u32) << exp.unsigned_abs())
}
}
#[inline]
fn negative_exponent_from_magnitude(magnitude: u64) -> i32 {
if magnitude == u64::from(i32::MIN.unsigned_abs()) {
return i32::MIN;
}
let Ok(value) = i32::try_from(magnitude) else {
cold_path();
unreachable!("negative exponent magnitude exceeds the i32 domain");
};
-value
}
fn exact_rational_to_finite_f64(exact: &BigRational, index: Option<usize>) -> Result<f64, LaError> {
if exact.denom().sign() == Sign::NoSign {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
}
if exact.numer().sign() == Sign::NoSign {
return Ok(0.0);
}
let denominator = exact.denom();
if denominator.sign() == Sign::Plus
&& let Some(denominator_exp) = positive_power_of_two_exponent(denominator)
&& let Ok(denominator_exp) = i32::try_from(denominator_exp)
{
return big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
rounded_rational_unrepresentable_reason(exact)
});
}
let reduced = exact.reduced();
reduced_rational_to_finite_f64(&reduced, index)
}
fn positive_power_of_two_exponent(value: &BigInt) -> Option<u64> {
if value.sign() != Sign::Plus {
return None;
}
let exponent = value.trailing_zeros()?;
(value.bits().checked_sub(1) == Some(exponent)).then_some(exponent)
}
fn reduced_rational_to_finite_f64(
exact: &BigRational,
index: Option<usize>,
) -> Result<f64, LaError> {
let Some(denominator_exp) = positive_power_of_two_exponent(exact.denom()) else {
cold_path();
return Err(LaError::unrepresentable(
index,
rounded_rational_unrepresentable_reason(exact),
));
};
let Ok(denominator_exp) = i32::try_from(denominator_exp) else {
cold_path();
return Err(LaError::unrepresentable(
index,
rounded_rational_unrepresentable_reason(exact),
));
};
big_int_exp_ref_to_finite_f64(exact.numer(), -denominator_exp, index, || {
rounded_rational_unrepresentable_reason(exact)
})
}
fn rounded_rational_unrepresentable_reason(exact: &BigRational) -> UnrepresentableReason {
match exact.to_f64() {
Some(value) if value.is_finite() => UnrepresentableReason::RequiresRounding,
_ => UnrepresentableReason::NotFinite,
}
}
fn exact_rational_to_rounded_f64(
exact: &BigRational,
index: Option<usize>,
) -> Result<f64, LaError> {
if exact.denom().sign() == Sign::NoSign {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
}
if exact.numer().sign() == Sign::NoSign {
return Ok(0.0);
}
let Some(value) = exact.to_f64() else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
if value.is_finite() {
Ok(value)
} else {
cold_path();
Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
))
}
}
impl ExactF64Conversion for BigRational {
type Output = f64;
#[inline]
fn try_to_f64(&self) -> Result<Self::Output, LaError> {
exact_rational_to_finite_f64(self, None)
}
#[inline]
fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
exact_rational_to_rounded_f64(self, None)
}
}
impl<const D: usize> ExactF64Conversion for [BigRational; D] {
type Output = Vector<D>;
#[inline]
fn try_to_f64(&self) -> Result<Self::Output, LaError> {
let mut result = [0.0; D];
for (index, value) in self.iter().enumerate() {
result[index] = exact_rational_to_finite_f64(value, Some(index))?;
}
Vector::try_new(result)
}
#[inline]
fn to_rounded_f64(&self) -> Result<Self::Output, LaError> {
let mut result = [0.0; D];
for (index, value) in self.iter().enumerate() {
result[index] = exact_rational_to_rounded_f64(value, Some(index))?;
}
Vector::try_new(result)
}
}
fn shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
let word_bits = u64::from(u64::BITS);
let word_index = usize::try_from(shift / word_bits).ok()?;
let bit_shift = u32::try_from(shift % word_bits).ok()?;
let mut digits = value.iter_u64_digits().skip(word_index);
let low = digits.next()? >> bit_shift;
if bit_shift == 0 {
Some(low)
} else {
let high = digits.next().unwrap_or(0) << (u64::BITS - bit_shift);
Some(low | high)
}
}
fn magnitude_bit_is_set(value: &BigInt, bit: u64) -> bool {
let word_bits = u64::from(u64::BITS);
let Ok(word_index) = usize::try_from(bit / word_bits) else {
return false;
};
let bit_index = u32::try_from(bit % word_bits).unwrap_or(0);
value
.iter_u64_digits()
.nth(word_index)
.is_some_and(|word| word & (1_u64 << bit_index) != 0)
}
fn magnitude_has_lower_bits(value: &BigInt, exclusive_end: u64) -> bool {
let word_bits = u64::from(u64::BITS);
let Ok(full_words) = usize::try_from(exclusive_end / word_bits) else {
return value.sign() != Sign::NoSign;
};
let partial_bits = u32::try_from(exclusive_end % word_bits).unwrap_or(0);
let mut digits = value.iter_u64_digits();
for _ in 0..full_words {
if digits.next().unwrap_or(0) != 0 {
return true;
}
}
if partial_bits == 0 {
false
} else {
let mask = (1_u64 << partial_bits) - 1;
digits.next().is_some_and(|word| word & mask != 0)
}
}
fn rounded_shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
if shift > value.bits() {
return Some(0);
}
let retained = shifted_magnitude_to_u64(value, shift).unwrap_or(0);
if shift == 0 {
return Some(retained);
}
let guard_bit = shift - 1;
let increment = magnitude_bit_is_set(value, guard_bit)
&& (magnitude_has_lower_bits(value, guard_bit) || retained & 1 != 0);
retained.checked_add(u64::from(increment))
}
#[inline]
fn inexact_big_int_reason(
top_bit_exp: i64,
rounded_reason: impl FnOnce() -> UnrepresentableReason,
) -> UnrepresentableReason {
if top_bit_exp < F64_MAX_BINARY_EXPONENT {
UnrepresentableReason::RequiresRounding
} else {
rounded_reason()
}
}
fn big_int_exp_ref_to_rounded_f64(
value: &BigInt,
exp: i32,
index: Option<usize>,
) -> Result<f64, LaError> {
if value.sign() == Sign::NoSign {
return Ok(0.0);
}
let sign = if value.sign() == Sign::Minus {
1_u64 << 63
} else {
0
};
let Ok(bit_len) = i64::try_from(value.bits()) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
let Some(mut top_bit_exp) = i64::from(exp).checked_add(bit_len - 1) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
if top_bit_exp > F64_MAX_BINARY_EXPONENT {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
}
if top_bit_exp >= F64_MIN_NORMAL_EXPONENT {
let mut significand = if bit_len <= F64_SIGNIFICAND_BITS {
let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
cold_path();
unreachable!("nonzero integer must expose magnitude digits");
};
let shift = u32::try_from(F64_SIGNIFICAND_BITS - bit_len)
.unwrap_or_else(|_| unreachable!("normal significand shift must fit u32"));
magnitude
.checked_shl(shift)
.unwrap_or_else(|| unreachable!("normal significand must fit u64"))
} else {
let shift = u64::try_from(bit_len - F64_SIGNIFICAND_BITS)
.unwrap_or_else(|_| unreachable!("positive significand shift must fit u64"));
rounded_shifted_magnitude_to_u64(value, shift)
.unwrap_or_else(|| unreachable!("rounded binary64 significand must fit u64"))
};
if significand == 1_u64 << F64_SIGNIFICAND_BITS {
significand >>= 1;
top_bit_exp += 1;
}
if top_bit_exp > F64_MAX_BINARY_EXPONENT {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
}
let biased_exp = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS)
.unwrap_or_else(|_| unreachable!("normal exponent must be positive"));
return Ok(f64::from_bits(
sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
));
}
let subnormal_shift = i64::from(exp) - F64_MIN_BINARY_EXPONENT;
let significand = if subnormal_shift >= 0 {
let Some(magnitude) = shifted_magnitude_to_u64(value, 0) else {
cold_path();
unreachable!("nonzero integer must expose magnitude digits");
};
let shift = u32::try_from(subnormal_shift)
.unwrap_or_else(|_| unreachable!("subnormal left shift must fit u32"));
magnitude
.checked_shl(shift)
.unwrap_or_else(|| unreachable!("subnormal significand must fit u64"))
} else {
let shift = u64::try_from(-subnormal_shift)
.unwrap_or_else(|_| unreachable!("subnormal right shift must fit u64"));
rounded_shifted_magnitude_to_u64(value, shift)
.unwrap_or_else(|| unreachable!("rounded subnormal significand must fit u64"))
};
if significand == 1_u64 << F64_FRACTION_BITS {
return Ok(f64::from_bits(sign | (1_u64 << F64_FRACTION_BITS)));
}
Ok(f64::from_bits(sign | significand))
}
fn big_int_exp_ref_to_finite_f64(
value: &BigInt,
exp: i32,
index: Option<usize>,
rounded_reason: impl FnOnce() -> UnrepresentableReason,
) -> Result<f64, LaError> {
if value.sign() == Sign::NoSign {
return Ok(0.0);
}
let is_negative = value.sign() == Sign::Minus;
let mut exp = i64::from(exp);
let Some(trailing_zeros) = value.trailing_zeros() else {
cold_path();
unreachable!("nonzero integer must have a least-significant set bit");
};
let Ok(trailing_zeros_i64) = i64::try_from(trailing_zeros) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
let Some(updated_exp) = exp.checked_add(trailing_zeros_i64) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
exp = updated_exp;
let Some(bit_len) = value.bits().checked_sub(trailing_zeros) else {
cold_path();
unreachable!("trailing-zero count cannot exceed integer bit length");
};
let Ok(bit_len) = i64::try_from(bit_len) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
let Some(top_bit_exp) = exp.checked_add(bit_len - 1) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
if top_bit_exp > F64_MAX_BINARY_EXPONENT {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
}
if exp < F64_MIN_BINARY_EXPONENT {
cold_path();
let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
return Err(LaError::unrepresentable(index, reason));
}
if bit_len > F64_SIGNIFICAND_BITS {
cold_path();
let reason = inexact_big_int_reason(top_bit_exp, rounded_reason);
return Err(LaError::unrepresentable(index, reason));
}
let Some(mantissa) = shifted_magnitude_to_u64(value, trailing_zeros) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
let sign = if is_negative { 1u64 << 63 } else { 0 };
if top_bit_exp < F64_MIN_NORMAL_EXPONENT {
let Ok(shift) = u32::try_from(exp - F64_MIN_BINARY_EXPONENT) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::RequiresRounding,
));
};
Ok(f64::from_bits(sign | (mantissa << shift)))
} else {
let Ok(biased_exp) = u64::try_from(top_bit_exp + F64_EXPONENT_BIAS) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::NotFinite,
));
};
let Ok(shift) = u32::try_from(F64_FRACTION_BITS - (bit_len - 1)) else {
cold_path();
return Err(LaError::unrepresentable(
index,
UnrepresentableReason::RequiresRounding,
));
};
let significand = mantissa << shift;
Ok(f64::from_bits(
sign | (biased_exp << F64_FRACTION_BITS) | (significand & F64_FRACTION_MASK),
))
}
}
fn big_int_exp_to_finite_f64(
value: &BigInt,
exp: i32,
index: Option<usize>,
) -> Result<f64, LaError> {
big_int_exp_ref_to_finite_f64(value, exp, index, || {
match big_int_exp_ref_to_rounded_f64(value, exp, index) {
Ok(_) => UnrepresentableReason::RequiresRounding,
Err(_) => UnrepresentableReason::NotFinite,
}
})
}
fn big_int_exp_to_rounded_f64(value: &BigInt, exp: i32) -> Result<f64, LaError> {
big_int_exp_ref_to_rounded_f64(value, exp, None)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum Component {
#[default]
Zero,
NonZero {
mantissa: NonZeroU64,
exponent: i32,
is_negative: bool,
},
}
impl Component {
const fn exponent(self) -> Option<i32> {
match self {
Self::Zero => None,
Self::NonZero { exponent, .. } => Some(exponent),
}
}
}
mod decomposition {
use super::Component;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct Decomposed<T> {
components: T,
min_exponent: Option<i32>,
}
impl<T> Decomposed<T> {
pub(super) const fn components(&self) -> &T {
&self.components
}
pub(super) const fn min_exponent(&self) -> Option<i32> {
self.min_exponent
}
}
impl<const D: usize> Decomposed<[Component; D]> {
pub(super) fn from_vector_components(components: [Component; D]) -> Self {
let min_exponent = components
.iter()
.filter_map(|component| component.exponent())
.min();
Self {
components,
min_exponent,
}
}
}
impl<const D: usize> Decomposed<[[Component; D]; D]> {
pub(super) fn from_matrix_components(components: [[Component; D]; D]) -> Self {
let min_exponent = components
.iter()
.flatten()
.filter_map(|component| component.exponent())
.min();
Self {
components,
min_exponent,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct ScaleExponent {
value: i32,
}
impl ScaleExponent {
pub(super) const ZERO: Self = Self { value: 0 };
pub(super) const fn for_decomposed<T>(decomposed: &Decomposed<T>) -> Self {
let value = match decomposed.min_exponent() {
Some(exponent) => exponent,
None => 0,
};
Self { value }
}
pub(super) const fn min(self, other: Self) -> Self {
if self.value < other.value {
self
} else {
other
}
}
pub(super) const fn get(self) -> i32 {
self.value
}
pub(super) fn shift_for(self, exponent: i32) -> u32 {
let Some(shift) = exponent.checked_sub(self.value) else {
unreachable!("finite f64 exponent difference cannot overflow");
};
let Ok(shift) = u32::try_from(shift) else {
unreachable!("scale exponent cannot exceed a component exponent");
};
shift
}
}
}
use decomposition::{Decomposed, ScaleExponent};
fn decompose_proven_finite_matrix<const D: usize>(
m: &Matrix<D>,
) -> Decomposed<[[Component; D]; D]> {
let components =
from_fn(|row| from_fn(|col| decompose_proven_finite_f64(m.as_rows()[row][col])));
Decomposed::from_matrix_components(components)
}
fn decompose_proven_finite_vector<const D: usize>(v: &Vector<D>) -> Decomposed<[Component; D]> {
let components = from_fn(|index| decompose_proven_finite_f64(v.as_array()[index]));
Decomposed::from_vector_components(components)
}
#[inline]
fn component_to_big_int(component: Component, scale: ScaleExponent) -> BigInt {
match component {
Component::Zero => BigInt::from(0),
Component::NonZero {
mantissa,
exponent,
is_negative,
} => {
let value = BigInt::from(mantissa.get()) << scale.shift_for(exponent);
if is_negative { -value } else { value }
}
}
}
fn build_big_int_matrix<const D: usize>(
components: &[[Component; D]; D],
scale: ScaleExponent,
) -> [[BigInt; D]; D] {
from_fn(|row| from_fn(|col| component_to_big_int(components[row][col], scale)))
}
fn build_big_int_vec<const D: usize>(
components: &[Component; D],
scale: ScaleExponent,
) -> [BigInt; D] {
from_fn(|index| component_to_big_int(components[index], scale))
}
#[inline]
fn det2_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
&a[0][0] * &a[1][1] - &a[0][1] * &a[1][0]
}
#[inline]
fn det3_big_int_entries(a: [[&BigInt; 3]; 3]) -> BigInt {
let m00 = a[1][1] * a[2][2] - a[1][2] * a[2][1];
let m01 = a[1][0] * a[2][2] - a[1][2] * a[2][0];
let m02 = a[1][0] * a[2][1] - a[1][1] * a[2][0];
a[0][0] * m00 - a[0][1] * m01 + a[0][2] * m02
}
#[inline]
fn det3_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
det3_big_int_entries([
[&a[0][0], &a[0][1], &a[0][2]],
[&a[1][0], &a[1][1], &a[1][2]],
[&a[2][0], &a[2][1], &a[2][2]],
])
}
#[inline]
fn det4_big_int<const D: usize>(a: &[[BigInt; D]; D]) -> BigInt {
let mut det = BigInt::from(0);
if a[0][0].sign() != Sign::NoSign {
let c00 = det3_big_int_entries([
[&a[1][1], &a[1][2], &a[1][3]],
[&a[2][1], &a[2][2], &a[2][3]],
[&a[3][1], &a[3][2], &a[3][3]],
]);
det += &a[0][0] * c00;
}
if a[0][1].sign() != Sign::NoSign {
let c01 = det3_big_int_entries([
[&a[1][0], &a[1][2], &a[1][3]],
[&a[2][0], &a[2][2], &a[2][3]],
[&a[3][0], &a[3][2], &a[3][3]],
]);
det -= &a[0][1] * c01;
}
if a[0][2].sign() != Sign::NoSign {
let c02 = det3_big_int_entries([
[&a[1][0], &a[1][1], &a[1][3]],
[&a[2][0], &a[2][1], &a[2][3]],
[&a[3][0], &a[3][1], &a[3][3]],
]);
det += &a[0][2] * c02;
}
if a[0][3].sign() != Sign::NoSign {
let c03 = det3_big_int_entries([
[&a[1][0], &a[1][1], &a[1][2]],
[&a[2][0], &a[2][1], &a[2][2]],
[&a[3][0], &a[3][1], &a[3][2]],
]);
det -= &a[0][3] * c03;
}
det
}
#[derive(Debug)]
enum BareissResult {
Upper { odd_swaps: bool },
Singular { pivot_col: usize },
}
fn bareiss_forward_eliminate<const D: usize>(
a: &mut [[BigInt; D]; D],
mut rhs: Option<&mut [BigInt; D]>,
) -> BareissResult {
let zero = BigInt::from(0);
let mut prev_pivot = BigInt::from(1);
let mut odd_swaps = false;
for k in 0..D {
if a[k][k] == zero {
let mut found = false;
for i in (k + 1)..D {
if a[i][k] != zero {
a.swap(k, i);
if let Some(r) = &mut rhs {
r.swap(k, i);
}
odd_swaps = !odd_swaps;
found = true;
break;
}
}
if !found {
cold_path();
return BareissResult::Singular { pivot_col: k };
}
}
if k + 1 == D {
break;
}
for i in (k + 1)..D {
for j in (k + 1)..D {
a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
}
if let Some(r) = &mut rhs {
r[i] = (&a[k][k] * &r[i] - &a[i][k] * &r[k]) / &prev_pivot;
}
a[i][k].clone_from(&zero);
}
prev_pivot.clone_from(&a[k][k]);
}
#[cfg(debug_assertions)]
for (k, row) in a.iter().enumerate() {
assert_ne!(row[k], zero, "pivot at ({k}, {k}) must be non-zero");
for (i, lower_row) in a.iter().enumerate().skip(k + 1) {
assert_eq!(
lower_row[k], zero,
"sub-diagonal at ({i}, {k}) must be zero"
);
}
}
BareissResult::Upper { odd_swaps }
}
fn determinant_scale_exp<const D: usize>(e_min: i32) -> Result<i32, LaError> {
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)
}
fn scaled_det_int_finite<const D: usize>(m: &Matrix<D>) -> (BigInt, ScaleExponent) {
let decomposed = decompose_proven_finite_matrix(m);
scaled_det_int_decomposed(&decomposed)
}
fn scaled_det_int_decomposed<const D: usize>(
decomposed: &Decomposed<[[Component; D]; D]>,
) -> (BigInt, ScaleExponent) {
if D == 0 {
return (BigInt::from(1), ScaleExponent::ZERO);
}
if decomposed.min_exponent().is_none() {
return (BigInt::from(0), ScaleExponent::ZERO);
}
let scale = ScaleExponent::for_decomposed(decomposed);
let mut a = build_big_int_matrix(decomposed.components(), scale);
let det_int = match D {
1 => take(&mut a[0][0]),
2 => det2_big_int(&a),
3 => det3_big_int(&a),
4 => det4_big_int(&a),
_ => {
let odd_swaps = match bareiss_forward_eliminate(&mut a, None) {
BareissResult::Upper { odd_swaps } => odd_swaps,
BareissResult::Singular { .. } => {
cold_path();
return (BigInt::from(0), ScaleExponent::ZERO);
}
};
let det = take(&mut a[D - 1][D - 1]);
if odd_swaps { -det } else { det }
}
};
(det_int, scale)
}
fn exact_det_int_finite<const D: usize>(m: &Matrix<D>) -> Result<(BigInt, i32), LaError> {
let (det_int, scale) = scaled_det_int_finite(m);
if det_int.sign() == Sign::NoSign {
return Ok((det_int, 0));
}
let total_exp = determinant_scale_exp::<D>(scale.get())?;
Ok((det_int, total_exp))
}
fn exact_det_finite<const D: usize>(m: &Matrix<D>) -> Result<BigRational, LaError> {
let (det_int, total_exp) = exact_det_int_finite(m)?;
Ok(big_int_exp_to_big_rational(det_int, total_exp))
}
fn bareiss_solve_finite<const D: usize>(
m: &Matrix<D>,
b: &Vector<D>,
) -> Result<[BigRational; D], LaError> {
let matrix = decompose_proven_finite_matrix(m);
let rhs = decompose_proven_finite_vector(b);
bareiss_solve_components(&matrix, &rhs)
}
fn bareiss_solve_components<const D: usize>(
matrix: &Decomposed<[[Component; D]; D]>,
rhs: &Decomposed<[Component; D]>,
) -> Result<[BigRational; D], LaError> {
const MAX_SHARED_SCALE_GAP_BITS: u32 = 64;
let independent_matrix_scale = ScaleExponent::for_decomposed(matrix);
let independent_rhs_scale = ScaleExponent::for_decomposed(rhs);
let scale_gap = independent_matrix_scale
.get()
.abs_diff(independent_rhs_scale.get());
let (matrix_scale, rhs_scale) = if scale_gap <= MAX_SHARED_SCALE_GAP_BITS {
let shared = independent_matrix_scale.min(independent_rhs_scale);
(shared, shared)
} else {
(independent_matrix_scale, independent_rhs_scale)
};
let mut a = build_big_int_matrix(matrix.components(), matrix_scale);
let mut rhs = build_big_int_vec(rhs.components(), rhs_scale);
match bareiss_forward_eliminate(&mut a, Some(&mut rhs)) {
BareissResult::Upper { .. } => {}
BareissResult::Singular { pivot_col } => {
cold_path();
return Err(LaError::singular_exact(pivot_col));
}
}
let mut x: [BigRational; D] = from_fn(|_| BigRational::from_integer(BigInt::from(0)));
for i in (0..D).rev() {
let mut sum = BigRational::from_integer(take(&mut rhs[i]));
for j in (i + 1)..D {
let a_ij = BigRational::from_integer(take(&mut a[i][j]));
sum -= &a_ij * &x[j];
}
let a_ii = BigRational::from_integer(take(&mut a[i][i]));
x[i] = sum / &a_ii;
}
let solution_scale_exp = rhs_scale
.get()
.checked_sub(matrix_scale.get())
.unwrap_or_else(|| unreachable!("finite f64 scale difference cannot overflow i32"));
if solution_scale_exp != 0 {
let solution_scale = big_int_exp_to_big_rational(BigInt::from(1_u8), solution_scale_exp);
for component in &mut x {
*component *= &solution_scale;
}
}
Ok(x)
}
#[inline]
fn det_exact_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
let (det_int, total_exp) = exact_det_int_finite(m)?;
big_int_exp_to_finite_f64(&det_int, total_exp, None)
}
#[inline]
fn det_exact_rounded_f64_finite<const D: usize>(m: &Matrix<D>) -> Result<f64, LaError> {
let (det_int, total_exp) = exact_det_int_finite(m)?;
big_int_exp_to_rounded_f64(&det_int, total_exp)
}
#[inline]
fn det_sign_exact_finite<const D: usize>(m: &Matrix<D>) -> DeterminantSign {
if let Ok(Some(estimate)) = m.det_direct_with_errbound() {
let det_f64 = estimate.determinant();
let error_bound = estimate.absolute_error_bound();
if det_f64 > error_bound {
return DeterminantSign::Positive;
}
if det_f64 < -error_bound {
return DeterminantSign::Negative;
}
}
cold_path();
let decomposed = decompose_proven_finite_matrix(m);
let (det_int, _) = scaled_det_int_decomposed(&decomposed);
match det_int.sign() {
Sign::Plus => DeterminantSign::Positive,
Sign::Minus => DeterminantSign::Negative,
Sign::NoSign => DeterminantSign::Zero,
}
}
impl<const D: usize> Matrix<D> {
#[inline]
pub fn det_exact(&self) -> Result<BigRational, LaError> {
exact_det_finite(self)
}
#[inline]
pub fn det_exact_f64(&self) -> Result<f64, LaError> {
det_exact_f64_finite(self)
}
#[inline]
pub fn det_exact_rounded_f64(&self) -> Result<f64, LaError> {
det_exact_rounded_f64_finite(self)
}
#[inline]
pub fn solve_exact(&self, b: Vector<D>) -> Result<[BigRational; D], LaError> {
bareiss_solve_finite(self, &b)
}
#[inline]
pub fn solve_exact_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
self.solve_exact(b)?.try_to_f64()
}
#[inline]
pub fn solve_exact_rounded_f64(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
self.solve_exact(b)?.to_rounded_f64()
}
#[inline]
pub fn det_sign_exact(&self) -> DeterminantSign {
det_sign_exact_finite(self)
}
}
#[cfg(test)]
mod tests {
use core::assert_matches;
use std::array::from_fn;
use num_traits::Signed;
use pastey::paste;
use proptest::prelude::*;
use super::*;
use crate::{
ArithmeticOperation, DEFAULT_SINGULAR_TOL, NonFiniteLocation, NonFiniteOrigin,
SingularityReason,
};
fn f64_to_big_rational(x: f64) -> BigRational {
let component = decompose_f64(x).expect("test helper requires finite f64 input");
let Component::NonZero {
mantissa,
exponent,
is_negative,
} = component
else {
return BigRational::from_integer(BigInt::from(0));
};
let numer = if is_negative {
-BigInt::from(mantissa.get())
} else {
BigInt::from(mantissa.get())
};
if exponent >= 0 {
BigRational::new_raw(numer << exponent.cast_unsigned(), BigInt::from(1u32))
} else {
BigRational::new_raw(numer, BigInt::from(1u32) << (-exponent).cast_unsigned())
}
}
fn assert_non_finite_input_scalar<T>(result: &Result<T, LaError>) {
let Err(error) = result else {
panic!("expected a non-finite scalar-input error");
};
assert!(matches!(
*error,
LaError::NonFinite {
location: NonFiniteLocation::Scalar,
origin: NonFiniteOrigin::Input,
..
}
));
}
fn assert_unrepresentable<T>(
result: &Result<T, LaError>,
expected_index: Option<usize>,
expected_reason: UnrepresentableReason,
) {
let Err(error) = result else {
panic!("expected an exact-to-f64 conversion error");
};
assert!(matches!(
*error,
LaError::Unrepresentable { index, reason, .. }
if index == expected_index && reason == expected_reason
));
}
macro_rules! gen_exact_identity_tests {
($d:literal) => {
paste! {
#[test]
fn [<exact_identity_paths_ $d d>]() {
let matrix = Matrix::<$d>::identity();
let one = BigRational::from_integer(BigInt::from(1));
assert_eq!(matrix.det_exact().unwrap(), one);
assert_eq!(matrix.det_exact_f64().unwrap().to_bits(), 1.0_f64.to_bits());
assert_eq!(
matrix.det_exact_rounded_f64().unwrap().to_bits(),
1.0_f64.to_bits()
);
assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
}
}
};
}
gen_exact_identity_tests!(2);
gen_exact_identity_tests!(3);
gen_exact_identity_tests!(4);
gen_exact_identity_tests!(5);
macro_rules! gen_det_exact_f64_agrees_with_det_direct {
($d:literal) => {
paste! {
#[test]
fn [<det_exact_f64_agrees_with_det_direct_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
let mut value = 2.0;
for (i, row) in rows.iter_mut().enumerate() {
row[i] = value;
value *= 2.0;
}
let m = Matrix::<$d>::try_from_rows(rows).unwrap();
let exact = m.det_exact_f64().unwrap();
let direct = m.det_direct().unwrap().unwrap();
assert_eq!(exact.to_bits(), direct.to_bits());
}
}
};
}
gen_det_exact_f64_agrees_with_det_direct!(2);
gen_det_exact_f64_agrees_with_det_direct!(3);
gen_det_exact_f64_agrees_with_det_direct!(4);
#[test]
fn det_sign_exact_d0_is_positive() {
assert_eq!(
Matrix::<0>::zero().det_sign_exact(),
DeterminantSign::Positive
);
}
#[test]
fn det_sign_exact_d1_positive() {
let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_sign_exact_d1_negative() {
let m = Matrix::<1>::try_from_rows([[-3.5]]).unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_d1_zero() {
let m = Matrix::<1>::try_from_rows([[0.0]]).unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
}
#[test]
fn det_sign_exact_singular_duplicate_rows() {
let m = Matrix::<3>::try_from_rows([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[1.0, 2.0, 3.0], ])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
}
#[test]
fn det_sign_exact_singular_linear_combination() {
let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Zero);
}
#[test]
fn det_sign_exact_negative_det_row_swap() {
let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_negative_det_known() {
let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_agrees_with_det_for_spd() {
let m = Matrix::<3>::try_from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
assert!(m.det().unwrap() > 0.0);
}
#[test]
fn det_sign_exact_near_singular_perturbation() {
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); let m = Matrix::<3>::try_from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_fast_filter_positive_4x4() {
let m = Matrix::<4>::try_from_rows([
[2.0, 1.0, 0.0, 0.0],
[1.0, 3.0, 1.0, 0.0],
[0.0, 1.0, 4.0, 1.0],
[0.0, 0.0, 1.0, 5.0],
])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_sign_exact_fast_filter_negative_4x4() {
let m = Matrix::<4>::try_from_rows([
[1.0, 3.0, 1.0, 0.0],
[2.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 4.0, 1.0],
[0.0, 0.0, 1.0, 5.0],
])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_subnormal_entries() {
let tiny = 5e-324_f64; assert!(tiny.is_subnormal());
let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_sign_exact_falls_back_when_subnormal_rounding_reverses_direct_sign() {
let scale = 2.0_f64.powi(-360);
let matrix = Matrix::<3>::try_from_rows([
[-5.0 * scale, 3.0 * scale, 6.0 * scale],
[0.0, -7.0 * scale, -7.0 * scale],
[2.0 * scale, -3.0 * scale, -4.0 * scale],
])
.unwrap();
assert_eq!(
matrix.det_direct().unwrap().unwrap().to_bits(),
(-f64::from_bits(1)).to_bits()
);
assert_eq!(matrix.det_errbound(), Ok(None));
assert!(matrix.det_exact().unwrap().is_positive());
assert_eq!(matrix.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_sign_exact_falls_back_for_bit_exact_underflow_counterexample() {
let matrix = Matrix::<3>::try_from_rows([
[
f64::from_bits(9_218_868_437_227_405_311),
f64::from_bits(13_830_554_455_654_793_216),
0.0,
],
[
f64::from_bits(6_790_500_848_393_242_208),
f64::from_bits(2_184_621_143_747_520_227),
f64::from_bits(2_187_555_472_467_513_745),
],
[
0.0,
f64::from_bits(2_184_859_204_554_904_434),
f64::from_bits(2_184_762_736_385_916_910),
],
])
.unwrap();
assert!(matrix.det_direct().unwrap().unwrap().is_sign_positive());
assert_eq!(matrix.det_errbound(), Ok(None));
assert!(matrix.det_exact().unwrap().is_negative());
assert_eq!(matrix.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_pivot_needed_5x5() {
let m = Matrix::<5>::try_from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Negative);
}
#[test]
fn det_sign_exact_5x5_known() {
let m = Matrix::<5>::try_from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_errbound_d0_is_zero() {
assert_eq!(Matrix::<0>::zero().det_errbound(), Ok(Some(0.0)));
}
#[test]
fn det_errbound_d1_is_zero() {
assert_eq!(
Matrix::<1>::try_from_rows([[42.0]]).unwrap().det_errbound(),
Ok(Some(0.0))
);
}
#[test]
fn det_errbound_d3_non_identity() {
let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])
.unwrap();
let bound = m.det_errbound().unwrap().unwrap();
assert!(bound > 0.0);
}
#[test]
fn det_errbound_d4_non_identity() {
let m = Matrix::<4>::try_from_rows([
[1.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, 3.0, 0.0],
[0.0, 0.0, 0.0, 4.0],
])
.unwrap();
let bound = m.det_errbound().unwrap().unwrap();
assert!(bound > 0.0);
}
#[test]
fn decompose_f64_zero() {
assert_eq!(decompose_f64(0.0), Ok(Component::Zero));
assert_eq!(decompose_f64(-0.0), Ok(Component::Zero));
}
#[test]
fn decompose_f64_one() {
assert_eq!(
decompose_f64(1.0),
Ok(Component::NonZero {
mantissa: NonZeroU64::new(1).unwrap(),
exponent: 0,
is_negative: false,
})
);
}
#[test]
fn decompose_f64_negative() {
assert_eq!(
decompose_f64(-3.5),
Ok(Component::NonZero {
mantissa: NonZeroU64::new(7).unwrap(),
exponent: -1,
is_negative: true,
})
);
}
#[test]
fn decompose_f64_subnormal() {
let tiny = f64::from_bits(1);
assert!(tiny.is_subnormal());
assert_eq!(
decompose_f64(tiny),
Ok(Component::NonZero {
mantissa: NonZeroU64::new(1).unwrap(),
exponent: -1074,
is_negative: false,
})
);
}
#[test]
fn decompose_f64_normalizes_mixed_subnormal_mantissa() {
let value = f64::from_bits(0x000C_0000_0000_0000);
assert!(value.is_subnormal());
assert_eq!(
decompose_f64(value),
Ok(Component::NonZero {
mantissa: NonZeroU64::new(3).unwrap(),
exponent: -1024,
is_negative: false,
})
);
}
#[test]
fn decompose_f64_power_of_two() {
assert_eq!(
decompose_f64(1024.0),
Ok(Component::NonZero {
mantissa: NonZeroU64::new(1).unwrap(),
exponent: 10,
is_negative: false,
})
);
}
#[test]
fn decompose_f64_rejects_nan() {
assert_non_finite_input_scalar(&decompose_f64(f64::NAN));
}
proptest! {
#[test]
fn finite_f64_round_trips_through_exact_decomposition(bits in any::<u64>()) {
let value = f64::from_bits(bits);
prop_assume!(value.is_finite());
if let Ok(Component::NonZero { mantissa, .. }) = decompose_f64(value) {
prop_assert_eq!(mantissa.get() & 1, 1);
}
let exact = f64_to_big_rational(value);
let reconstructed = exact_rational_to_finite_f64(&exact, None);
prop_assert_eq!(reconstructed, Ok(value));
}
}
#[test]
fn wide_low_exponent_value_reports_non_finite_rounded_result() {
let value = (BigInt::from(1_u8) << 2099_u32) - BigInt::from(1_u8);
let result = big_int_exp_to_finite_f64(&value, -1075, None);
assert!(!result.as_ref().unwrap_err().requires_rounding());
assert_unrepresentable(&result, None, UnrepresentableReason::NotFinite);
}
#[test]
fn direct_big_int_rounding_handles_extreme_negative_exponent_without_large_denominator() {
let positive = big_int_exp_ref_to_rounded_f64(&BigInt::from(1_u8), i32::MIN, None).unwrap();
let negative =
big_int_exp_ref_to_rounded_f64(&BigInt::from(-1_i8), i32::MIN, None).unwrap();
assert_eq!(positive.to_bits(), 0.0_f64.to_bits());
assert_eq!(negative.to_bits(), (-0.0_f64).to_bits());
}
proptest! {
#[test]
fn direct_big_int_rounding_matches_rational_oracle(
value in any::<i128>(),
exp in -1200_i32..=1200_i32,
) {
let value = BigInt::from(value);
let direct = big_int_exp_ref_to_rounded_f64(&value, exp, None);
let exact = big_int_exp_to_big_rational(value, exp);
let oracle = exact_rational_to_rounded_f64(&exact, None);
match (direct, oracle) {
(Ok(actual), Ok(expected)) => {
prop_assert_eq!(actual.to_bits(), expected.to_bits());
}
(Err(actual), Err(expected)) => {
prop_assert_eq!(actual, expected);
}
(actual, expected) => {
prop_assert_eq!(actual, expected);
}
}
}
}
#[test]
fn component_to_big_int_distinguishes_zero_from_nonzero_mantissa() {
let baseline = Component::NonZero {
mantissa: NonZeroU64::new(1).unwrap(),
exponent: 1,
is_negative: false,
};
let positive = Component::NonZero {
mantissa: NonZeroU64::new(3).unwrap(),
exponent: 4,
is_negative: false,
};
let negative = Component::NonZero {
mantissa: NonZeroU64::new(5).unwrap(),
exponent: 3,
is_negative: true,
};
let decomposed =
Decomposed::from_vector_components([Component::Zero, baseline, positive, negative]);
let scale = ScaleExponent::for_decomposed(&decomposed);
assert_eq!(
component_to_big_int(Component::Zero, scale),
BigInt::from(0)
);
assert_eq!(component_to_big_int(positive, scale), BigInt::from(24));
assert_eq!(component_to_big_int(negative, scale), BigInt::from(-20));
}
#[test]
fn decomposed_all_zero_uses_no_sentinel_exponent() {
let decomposed = decompose_proven_finite_matrix(&Matrix::<2>::zero());
assert_eq!(decomposed.min_exponent(), None);
let scale = ScaleExponent::for_decomposed(&decomposed);
assert_eq!(scale, ScaleExponent::ZERO);
assert_eq!(scale.get(), 0);
assert_eq!(
build_big_int_matrix(decomposed.components(), scale),
[
[BigInt::from(0), BigInt::from(0)],
[BigInt::from(0), BigInt::from(0)]
]
);
}
#[test]
fn matrix_and_rhs_scales_are_derived_independently() {
let tiny = f64::from_bits(1);
let matrix = Matrix::<2>::try_from_rows([[f64::MAX, 0.0], [0.0, 1.0]]).unwrap();
let rhs = Vector::<2>::try_new([tiny, 0.0]).unwrap();
let matrix = decompose_proven_finite_matrix(&matrix);
let rhs = decompose_proven_finite_vector(&rhs);
assert_eq!(matrix.min_exponent(), Some(0));
assert_eq!(rhs.min_exponent(), Some(-1074));
let matrix_scale = ScaleExponent::for_decomposed(&matrix);
let rhs_scale = ScaleExponent::for_decomposed(&rhs);
assert_eq!(matrix_scale.get(), 0);
assert_eq!(matrix_scale.shift_for(0), 0);
assert_eq!(rhs_scale.get(), -1074);
assert_eq!(rhs_scale.shift_for(-1074), 0);
}
proptest! {
#[test]
fn derived_scale_yields_nonnegative_shifts(bits in any::<[u64; 4]>()) {
let values = bits.map(f64::from_bits);
prop_assume!(values.iter().all(|value| value.is_finite()));
let matrix = Matrix::<2>::try_from_rows([
[values[0], values[1]],
[values[2], values[3]],
]).unwrap();
let decomposed = decompose_proven_finite_matrix(&matrix);
let scale = ScaleExponent::for_decomposed(&decomposed);
for component in decomposed.components().iter().flatten() {
if let Some(exponent) = component.exponent() {
prop_assert!(exponent >= scale.get());
prop_assert_eq!(
scale.shift_for(exponent),
u32::try_from(exponent - scale.get()).unwrap(),
);
}
}
}
}
#[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,
})
);
}
#[test]
fn negative_exponent_from_magnitude_covers_i32_domain_boundaries() {
assert_eq!(negative_exponent_from_magnitude(0), 0);
assert_eq!(negative_exponent_from_magnitude(1), -1);
assert_eq!(
negative_exponent_from_magnitude(i32::MAX.cast_unsigned().into()),
-i32::MAX
);
assert_eq!(
negative_exponent_from_magnitude(i32::MIN.unsigned_abs().into()),
i32::MIN
);
}
#[test]
#[should_panic(expected = "negative exponent magnitude exceeds the i32 domain")]
fn negative_exponent_from_magnitude_rejects_values_above_i32_domain() {
let _ = negative_exponent_from_magnitude(u64::from(i32::MIN.unsigned_abs()) + 1);
}
#[test]
fn exact_det_int_d0() {
let m = Matrix::<0>::zero();
let (det, exp) = exact_det_int_finite(&m).unwrap();
assert_eq!(det, BigInt::from(1));
assert_eq!(exp, 0);
}
#[test]
fn exact_det_int_d1_cases() {
let cases: &[(f64, i64, i32)] = &[
(7.0, 7, 0), (0.0, 0, 0), (-3.5, -7, -1), (0.5, 1, -1), ];
for &(input, expected_det_int, expected_exp) in cases {
let m = Matrix::<1>::try_from_rows([[input]]).unwrap();
let (det, exp) = exact_det_int_finite(&m).unwrap();
assert_eq!(
det,
BigInt::from(expected_det_int),
"det_int for input={input}"
);
assert_eq!(exp, expected_exp, "exp for input={input}");
}
}
#[test]
fn exact_det_int_d2_known() {
let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
let det = big_int_exp_to_big_rational(det_int, total_exp);
assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
}
#[test]
fn exact_det_int_all_zeros() {
let m = Matrix::<3>::zero();
let (det, _) = exact_det_int_finite(&m).unwrap();
assert_eq!(det, BigInt::from(0));
}
#[test]
fn exact_det_int_fractional_entries() {
let m = Matrix::<2>::try_from_rows([[0.5, 0.25], [1.0, 1.0]]).unwrap();
let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
let det = big_int_exp_to_big_rational(det_int, total_exp);
assert_eq!(det, BigRational::new(BigInt::from(1), BigInt::from(4)));
}
#[test]
fn exact_det_int_d3_direct_expansion_handles_zero_diagonal() {
let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
let (det_int, total_exp) = exact_det_int_finite(&m).unwrap();
let det = big_int_exp_to_big_rational(det_int, total_exp);
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn big_int_exp_to_big_rational_zero() {
let r = big_int_exp_to_big_rational(BigInt::from(0), -50);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn big_int_exp_to_big_rational_positive_exp() {
let r = big_int_exp_to_big_rational(BigInt::from(3), 2);
assert_eq!(r, BigRational::from_integer(BigInt::from(12)));
}
#[test]
fn big_int_exp_to_big_rational_negative_exp_reduced() {
let r = big_int_exp_to_big_rational(BigInt::from(6), -2);
assert_eq!(*r.numer(), BigInt::from(3));
assert_eq!(*r.denom(), BigInt::from(2));
}
#[test]
fn big_int_exp_to_big_rational_negative_exp_reduces_to_integer() {
let r = big_int_exp_to_big_rational(BigInt::from(8), -3);
assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn big_int_exp_to_big_rational_negative_exp_already_odd() {
let r = big_int_exp_to_big_rational(BigInt::from(3), -2);
assert_eq!(*r.numer(), BigInt::from(3));
assert_eq!(*r.denom(), BigInt::from(4));
}
#[test]
fn big_int_exp_to_big_rational_negative_value() {
let r = big_int_exp_to_big_rational(BigInt::from(-5), 1);
assert_eq!(r, BigRational::from_integer(BigInt::from(-10)));
}
#[test]
fn big_int_exp_to_big_rational_negative_value_with_denominator() {
let r = big_int_exp_to_big_rational(BigInt::from(-3), -2);
assert_eq!(*r.numer(), BigInt::from(-3));
assert_eq!(*r.denom(), BigInt::from(4));
}
#[test]
fn det_exact_d1_returns_entry() {
let det = Matrix::<1>::try_from_rows([[7.0]])
.unwrap()
.det_exact()
.unwrap();
assert_eq!(det, f64_to_big_rational(7.0));
}
#[test]
fn det_exact_d3_direct_expansion_handles_zero_diagonal() {
let m = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn det_exact_d3_singular_zero_column_returns_zero() {
let m = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn det_sign_exact_overflow_determinant_finite_entries() {
let big = f64::MAX / 2.0;
assert!(big.is_finite());
let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
.unwrap();
assert_eq!(m.det_sign_exact(), DeterminantSign::Positive);
}
#[test]
fn det_exact_d0_is_one() {
let det = Matrix::<0>::zero().det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn det_exact_known_2x2() {
let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(-2)));
}
#[test]
fn det_exact_known_dense_4x4() {
let m = Matrix::<4>::try_from_rows([
[4.0, 1.0, 3.0, 2.0],
[0.0, 5.0, 2.0, 1.0],
[7.0, 2.0, 6.0, 3.0],
[1.0, 8.0, 4.0, 9.0],
])
.unwrap();
assert_eq!(
m.det_exact(),
Ok(BigRational::from_integer(BigInt::from(92)))
);
}
#[test]
fn det_exact_singular_returns_zero() {
let m = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
.unwrap();
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn det_exact_near_singular_perturbation() {
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); let m = Matrix::<3>::try_from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
])
.unwrap();
let det = m.det_exact().unwrap();
let expected = BigRational::new(BigInt::from(-3), BigInt::from(1u64 << 50));
assert_eq!(det, expected);
}
#[test]
fn det_exact_5x5_permutation() {
let m = Matrix::<5>::try_from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
])
.unwrap();
let det = m.det_exact().unwrap();
assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn det_exact_f64_known_2x2() {
let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
let det = m.det_exact_f64().unwrap();
assert!((det - (-2.0)).abs() <= f64::EPSILON);
}
#[test]
fn det_exact_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
.unwrap();
assert_unrepresentable(&m.det_exact_f64(), None, UnrepresentableReason::NotFinite);
}
#[test]
fn det_exact_rounded_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let m = Matrix::<3>::try_from_rows([[0.0, 0.0, 1.0], [big, 0.0, 1.0], [0.0, big, 1.0]])
.unwrap();
assert_unrepresentable(
&m.det_exact_rounded_f64(),
None,
UnrepresentableReason::NotFinite,
);
}
#[test]
fn det_exact_f64_underflow_returns_err_for_nonzero_exact_result() {
let tiny = f64::from_bits(1);
let m = Matrix::<2>::try_from_rows([[tiny, 0.0], [0.0, tiny]]).unwrap();
assert!(m.det_exact().unwrap().is_positive());
assert_unrepresentable(
&m.det_exact_f64(),
None,
UnrepresentableReason::RequiresRounding,
);
}
#[test]
fn det_exact_f64_rejects_inexact_rounding() {
let m = Matrix::<2>::try_from_rows([[1.0 + f64::EPSILON, 0.0], [0.0, 1.0 - f64::EPSILON]])
.unwrap();
assert_eq!(
m.det_exact(),
Ok(BigRational::new(
(BigInt::from(1_u128) << 104_u32) - BigInt::from(1),
BigInt::from(1_u128 << 104),
))
);
assert_unrepresentable(
&m.det_exact_f64(),
None,
UnrepresentableReason::RequiresRounding,
);
}
#[test]
fn det_exact_f64_accepts_max_finite_binary64() {
let m = Matrix::<1>::try_from_rows([[f64::MAX]]).unwrap();
assert_eq!(m.det_exact_f64().unwrap().to_bits(), f64::MAX.to_bits());
}
fn arbitrary_rhs<const D: usize>() -> Vector<D> {
let values = [1.0, -2.5, 3.0, 0.25, -4.0];
let mut arr = [0.0f64; D];
for (dst, src) in arr.iter_mut().zip(values.iter()) {
*dst = *src;
}
Vector::<D>::new(arr)
}
macro_rules! gen_solve_exact_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_identity_paths_ $d d>]() {
let a = Matrix::<$d>::identity();
let b = arbitrary_rhs::<$d>();
let exact = a.solve_exact(b).unwrap();
let strict_f64 = a.solve_exact_f64(b).unwrap().into_array();
for i in 0..$d {
assert_eq!(exact[i], f64_to_big_rational(b.as_array()[i]));
assert_eq!(strict_f64[i].to_bits(), b.as_array()[i].to_bits());
}
}
#[test]
fn [<solve_exact_singular_ $d d>]() {
let a = Matrix::<$d>::zero();
let b = arbitrary_rhs::<$d>();
assert_matches!(
a.solve_exact(b),
Err(LaError::Singular {
pivot_col: 0,
reason: SingularityReason::Exact,
..
})
);
}
}
};
}
gen_solve_exact_tests!(2);
gen_solve_exact_tests!(3);
gen_solve_exact_tests!(4);
gen_solve_exact_tests!(5);
macro_rules! gen_solve_exact_f64_agrees_with_lu {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_f64_agrees_with_lu_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
for r in 0..$d {
for c in 0..$d {
rows[r][c] = if r == c {
f64::from($d) + 1.0
} else {
1.0
};
}
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let x_true = {
let mut arr = [0.0f64; $d];
for (dst, src) in arr.iter_mut().zip([1.0, -2.0, 3.0, -4.0, 5.0]) {
*dst = src;
}
arr
};
let mut b_arr = [0.0f64; $d];
for i in 0..$d {
let mut sum = 0.0;
for j in 0..$d {
sum = rows[i][j].mul_add(x_true[j], sum);
}
b_arr[i] = sum;
}
let b = Vector::<$d>::new(b_arr);
let exact = a.solve_exact_f64(b).unwrap().into_array();
let lu_sol = a.lu(DEFAULT_SINGULAR_TOL).unwrap()
.solve(b).unwrap().into_array();
for i in 0..$d {
assert_eq!(exact[i].to_bits(), x_true[i].to_bits());
let eps = lu_sol[i].abs().mul_add(1e-12, 1e-12);
assert!((exact[i] - lu_sol[i]).abs() <= eps);
}
}
}
};
}
gen_solve_exact_f64_agrees_with_lu!(2);
gen_solve_exact_f64_agrees_with_lu!(3);
gen_solve_exact_f64_agrees_with_lu!(4);
gen_solve_exact_f64_agrees_with_lu!(5);
macro_rules! gen_solve_exact_roundtrip_tests {
($d:literal) => {
paste! {
#[test]
#[expect(
clippy::cast_precision_loss,
reason = "dimensions and indices are at most five and exactly representable as f64"
)]
fn [<solve_exact_roundtrip_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
for r in 0..$d {
for c in 0..$d {
rows[r][c] = if r == c {
f64::from($d) + 1.0
} else {
1.0
};
}
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let mut x0 = [0.0f64; $d];
for i in 0..$d {
x0[i] = (i + 1) as f64;
}
let mut b_arr = [0.0f64; $d];
for r in 0..$d {
let mut sum = 0.0_f64;
for c in 0..$d {
sum = rows[r][c].mul_add(x0[c], sum);
}
b_arr[r] = sum;
}
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
for i in 0..$d {
assert_eq!(x[i], f64_to_big_rational(x0[i]));
}
}
}
};
}
gen_solve_exact_roundtrip_tests!(2);
gen_solve_exact_roundtrip_tests!(3);
gen_solve_exact_roundtrip_tests!(4);
gen_solve_exact_roundtrip_tests!(5);
#[test]
fn solve_exact_d0_returns_empty() {
let a = Matrix::<0>::zero();
let b = Vector::<0>::zero();
let x = a.solve_exact(b).unwrap();
assert!(x.is_empty());
}
#[test]
fn solve_exact_known_2x2() {
let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
let b = Vector::<2>::new([5.0, 11.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::from_integer(BigInt::from(1)));
assert_eq!(x[1], BigRational::from_integer(BigInt::from(2)));
}
#[test]
fn solve_exact_pivoting_needed() {
let a = Matrix::<3>::try_from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
let b = Vector::<3>::new([2.0, 3.0, 4.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], f64_to_big_rational(3.0));
assert_eq!(x[1], f64_to_big_rational(2.0));
assert_eq!(x[2], f64_to_big_rational(4.0));
}
#[test]
fn solve_exact_fractional_result() {
let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
let b = Vector::<2>::new([1.0, 1.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::new(BigInt::from(2), BigInt::from(5)));
assert_eq!(x[1], BigRational::new(BigInt::from(1), BigInt::from(5)));
}
#[test]
fn solve_exact_singular_duplicate_rows() {
let a = Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [1.0, 2.0, 3.0]])
.unwrap();
let b = Vector::<3>::new([1.0, 2.0, 3.0]);
assert_matches!(
a.solve_exact(b),
Err(LaError::Singular {
reason: SingularityReason::Exact,
..
})
);
}
#[test]
fn solve_exact_5x5_permutation() {
let a = Matrix::<5>::try_from_rows([
[0.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
])
.unwrap();
let b = Vector::<5>::new([10.0, 20.0, 30.0, 40.0, 50.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], f64_to_big_rational(20.0));
assert_eq!(x[1], f64_to_big_rational(10.0));
assert_eq!(x[2], f64_to_big_rational(30.0));
assert_eq!(x[3], f64_to_big_rational(40.0));
assert_eq!(x[4], f64_to_big_rational(50.0));
}
macro_rules! gen_solve_exact_large_finite_entries_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_large_finite_entries_ $d d>]() {
let big = f64::MAX / 2.0;
assert!(big.is_finite());
let mut rows = [[0.0f64; $d]; $d];
for i in 0..$d {
rows[i][i] = big;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let mut b_arr = [big; $d];
b_arr[$d - 1] = 0.0;
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
for i in 0..($d - 1) {
assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
}
assert_eq!(x[$d - 1], BigRational::from_integer(BigInt::from(0)));
}
}
};
}
gen_solve_exact_large_finite_entries_tests!(2);
gen_solve_exact_large_finite_entries_tests!(3);
gen_solve_exact_large_finite_entries_tests!(4);
gen_solve_exact_large_finite_entries_tests!(5);
macro_rules! gen_solve_exact_mixed_magnitude_entries_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_mixed_magnitude_entries_ $d d>]() {
let tiny = f64::MIN_POSITIVE; let huge = 1.0e100_f64;
let mut rows = [[0.0f64; $d]; $d];
let mut b_arr = [0.0f64; $d];
for i in 0..$d {
let val = if i % 2 == 0 { huge } else { tiny };
rows[i][i] = val;
b_arr[i] = val;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
for i in 0..$d {
assert_eq!(x[i], BigRational::from_integer(BigInt::from(1)));
}
}
}
};
}
gen_solve_exact_mixed_magnitude_entries_tests!(2);
gen_solve_exact_mixed_magnitude_entries_tests!(3);
gen_solve_exact_mixed_magnitude_entries_tests!(4);
gen_solve_exact_mixed_magnitude_entries_tests!(5);
#[test]
fn solve_exact_restores_independent_matrix_and_rhs_scales() {
let large = 2.0_f64.powi(500);
let tiny = 2.0_f64.powi(-1000);
let large_matrix = Matrix::<2>::try_from_rows([[large, 0.0], [0.0, large]]).unwrap();
let tiny_rhs = Vector::<2>::new([tiny, -2.0 * tiny]);
let small_solution = large_matrix.solve_exact(tiny_rhs).unwrap();
assert_eq!(
small_solution[0],
BigRational::new(BigInt::from(1_u8), BigInt::from(1_u8) << 1500_u32)
);
assert_eq!(
small_solution[1],
BigRational::new(BigInt::from(-1_i8), BigInt::from(1_u8) << 1499_u32)
);
let tiny_matrix = Matrix::<1>::try_from_rows([[tiny]]).unwrap();
let large_rhs = Vector::<1>::new([large]);
let large_solution = tiny_matrix.solve_exact(large_rhs).unwrap();
assert_eq!(
large_solution[0],
BigRational::from_integer(BigInt::from(1_u8) << 1500_u32)
);
}
macro_rules! gen_solve_exact_subnormal_rhs_tests {
($d:literal) => {
paste! {
#[test]
#[expect(
clippy::cast_precision_loss,
reason = "indices are at most five and exactly representable as f64"
)]
fn [<solve_exact_subnormal_rhs_ $d d>]() {
let tiny = 5e-324_f64; assert!(tiny.is_subnormal());
let a = Matrix::<$d>::identity();
let mut b_arr = [0.0f64; $d];
for i in 0..$d {
b_arr[i] = (i + 1) as f64 * tiny;
assert!(b_arr[i].is_subnormal());
}
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
for i in 0..$d {
assert_eq!(x[i], f64_to_big_rational((i + 1) as f64 * tiny));
}
}
}
};
}
gen_solve_exact_subnormal_rhs_tests!(2);
gen_solve_exact_subnormal_rhs_tests!(3);
gen_solve_exact_subnormal_rhs_tests!(4);
gen_solve_exact_subnormal_rhs_tests!(5);
macro_rules! gen_solve_exact_pivot_swap_fractional_tests {
($d:literal) => {
paste! {
#[test]
#[expect(
clippy::cast_precision_loss,
reason = "indices and test offsets are small integers exactly representable as f64"
)]
fn [<solve_exact_pivot_swap_with_fractional_result_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
rows[0][1] = 1.0;
rows[1][0] = 2.0;
rows[1][1] = 1.0;
for (i, row) in rows.iter_mut().enumerate().skip(2) {
row[i] = 1.0;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let mut b_arr = [0.0f64; $d];
b_arr[0] = 3.0;
b_arr[1] = 4.0;
for (i, value) in b_arr.iter_mut().enumerate().skip(2) {
*value = (i + 10) as f64;
}
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::new(BigInt::from(1), BigInt::from(2)));
assert_eq!(x[1], BigRational::from_integer(BigInt::from(3)));
for (i, value) in x.iter().enumerate().skip(2) {
assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
}
}
}
};
}
gen_solve_exact_pivot_swap_fractional_tests!(2);
gen_solve_exact_pivot_swap_fractional_tests!(3);
gen_solve_exact_pivot_swap_fractional_tests!(4);
gen_solve_exact_pivot_swap_fractional_tests!(5);
macro_rules! gen_solve_exact_mid_pivot_swap_tests {
($d:literal) => {
paste! {
#[test]
#[expect(
clippy::cast_precision_loss,
reason = "indices and test offsets are small integers exactly representable as f64"
)]
fn [<solve_exact_mid_pivot_swap_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
rows[0][0] = 1.0; rows[0][1] = 2.0; rows[0][2] = 3.0;
rows[1][2] = 4.0;
rows[2][1] = 5.0; rows[2][2] = 6.0;
for (i, row) in rows.iter_mut().enumerate().skip(3) {
row[i] = 1.0;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let mut b_arr = [0.0f64; $d];
b_arr[0] = 6.0;
b_arr[1] = 7.0;
b_arr[2] = 8.0;
for (i, value) in b_arr.iter_mut().enumerate().skip(3) {
*value = (i + 10) as f64;
}
let b = Vector::<$d>::new(b_arr);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], BigRational::new(BigInt::from(7), BigInt::from(4)));
assert_eq!(x[1], BigRational::new(BigInt::from(-1), BigInt::from(2)));
assert_eq!(x[2], BigRational::new(BigInt::from(7), BigInt::from(4)));
for (i, value) in x.iter().enumerate().skip(3) {
assert_eq!(value, &f64_to_big_rational((i + 10) as f64));
}
}
}
};
}
gen_solve_exact_mid_pivot_swap_tests!(3);
gen_solve_exact_mid_pivot_swap_tests!(4);
gen_solve_exact_mid_pivot_swap_tests!(5);
macro_rules! gen_solve_exact_singular_rank_deficient_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_singular_rank_deficient_ $d d>]() {
let mut rows = [[0.0f64; $d]; $d];
for i in 0..($d - 1) {
rows[i][i] = 1.0;
rows[$d - 1][i] = 1.0;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let b = Vector::<$d>::new([1.0; $d]);
assert_matches!(
a.solve_exact(b),
Err(LaError::Singular {
pivot_col,
reason: SingularityReason::Exact,
..
}) if pivot_col == $d - 1
);
}
}
};
}
gen_solve_exact_singular_rank_deficient_tests!(2);
gen_solve_exact_singular_rank_deficient_tests!(3);
gen_solve_exact_singular_rank_deficient_tests!(4);
gen_solve_exact_singular_rank_deficient_tests!(5);
macro_rules! gen_solve_exact_zero_rhs_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_zero_rhs_ $d d>]() {
let mut rows = [[1.0f64; $d]; $d];
for i in 0..$d {
rows[i][i] = f64::from($d) + 1.0;
}
let a = Matrix::<$d>::try_from_rows(rows).unwrap();
let b = Vector::<$d>::zero();
let x = a.solve_exact(b).unwrap();
for xi in &x {
assert_eq!(*xi, BigRational::from_integer(BigInt::from(0)));
}
}
}
};
}
gen_solve_exact_zero_rhs_tests!(2);
gen_solve_exact_zero_rhs_tests!(3);
gen_solve_exact_zero_rhs_tests!(4);
gen_solve_exact_zero_rhs_tests!(5);
fn big_rational_matvec<const D: usize>(
a: &Matrix<D>,
x: &[BigRational; D],
) -> [BigRational; D] {
from_fn(|i| {
let mut sum = BigRational::from_integer(BigInt::from(0));
for (aij, xj) in a.as_rows()[i].iter().zip(x.iter()) {
sum += f64_to_big_rational(*aij) * xj;
}
sum
})
}
fn hilbert<const D: usize>() -> Matrix<D> {
let rows = from_fn(|r| from_fn(|c| 1.0 / f64::from(u32::try_from(r + c + 1).unwrap())));
Matrix::<D>::try_from_rows(rows).unwrap()
}
#[test]
fn solve_exact_near_singular_3x3_integer_x0() {
let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); let a = Matrix::<3>::try_from_rows([
[1.0 + perturbation, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
])
.unwrap();
let b = Vector::<3>::new([6.0 + perturbation, 15.0, 24.0]);
let x = a.solve_exact(b).unwrap();
let one = BigRational::from_integer(BigInt::from(1));
assert_eq!(x[0], one);
assert_eq!(x[1], one);
assert_eq!(x[2], one);
}
#[test]
fn solve_exact_large_entries_3x3_unit_vector() {
let big = f64::MAX / 2.0;
assert!(big.is_finite());
let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
.unwrap();
let b = Vector::<3>::new([big, 1.0, 1.0]);
let x = a.solve_exact(b).unwrap();
let zero = BigRational::from_integer(BigInt::from(0));
let one = BigRational::from_integer(BigInt::from(1));
assert_eq!(x[0], one);
assert_eq!(x[1], zero);
assert_eq!(x[2], zero);
}
#[test]
fn det_sign_exact_large_entries_3x3_positive() {
let big = f64::MAX / 2.0;
let a = Matrix::<3>::try_from_rows([[big, 1.0, 1.0], [1.0, big, 1.0], [1.0, 1.0, big]])
.unwrap();
assert_matches!(
a.det_direct(),
Err(LaError::NonFinite {
location: NonFiniteLocation::Scalar,
origin: NonFiniteOrigin::Computation {
operation: ArithmeticOperation::Determinant,
..
},
..
})
);
assert_eq!(a.det_sign_exact(), DeterminantSign::Positive);
assert!(a.det_exact().unwrap().is_positive());
assert_unrepresentable(&a.det_exact_f64(), None, UnrepresentableReason::NotFinite);
}
macro_rules! gen_det_sign_exact_hilbert_positive_tests {
($d:literal) => {
paste! {
#[test]
fn [<det_sign_exact_hilbert_positive_ $d d>]() {
let h = hilbert::<$d>();
assert_eq!(h.det_sign_exact(), DeterminantSign::Positive);
}
}
};
}
gen_det_sign_exact_hilbert_positive_tests!(2);
gen_det_sign_exact_hilbert_positive_tests!(3);
gen_det_sign_exact_hilbert_positive_tests!(4);
gen_det_sign_exact_hilbert_positive_tests!(5);
macro_rules! gen_solve_exact_hilbert_residual_tests {
($d:literal) => {
paste! {
#[test]
fn [<solve_exact_hilbert_residual_ $d d>]() {
let h = hilbert::<$d>();
let mut b_arr = [0.0f64; $d];
for i in 0usize..$d {
let sign = if i.is_multiple_of(2) { 1.0 } else { -1.0 };
b_arr[i] = sign * f64::from(u32::try_from(i + 1).unwrap());
}
let b = Vector::<$d>::new(b_arr);
let x = h.solve_exact(b).unwrap();
let ax = big_rational_matvec(&h, &x);
for i in 0..$d {
assert_eq!(ax[i], f64_to_big_rational(b_arr[i]));
}
}
}
};
}
gen_solve_exact_hilbert_residual_tests!(2);
gen_solve_exact_hilbert_residual_tests!(3);
gen_solve_exact_hilbert_residual_tests!(4);
gen_solve_exact_hilbert_residual_tests!(5);
#[test]
fn solve_exact_f64_known_2x2() {
let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
let b = Vector::<2>::new([5.0, 11.0]);
let x = a.solve_exact_f64(b).unwrap().into_array();
assert!((x[0] - 1.0).abs() <= f64::EPSILON);
assert!((x[1] - 2.0).abs() <= f64::EPSILON);
}
#[test]
fn solve_exact_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
let b = Vector::<2>::new([big, big]);
assert_unrepresentable(
&a.solve_exact_f64(b),
Some(0),
UnrepresentableReason::NotFinite,
);
}
#[test]
fn solve_exact_f64_huge_non_dyadic_component_returns_not_finite() {
let a = Matrix::<1>::try_from_rows([[3.0 * f64::MIN_POSITIVE]]).unwrap();
let b = Vector::<1>::new([f64::MAX]);
assert_unrepresentable(
&a.solve_exact_f64(b),
Some(0),
UnrepresentableReason::NotFinite,
);
}
#[test]
fn solve_exact_rounded_f64_overflow_returns_err() {
let big = f64::MAX / 2.0;
let a = Matrix::<2>::try_from_rows([[1.0 / big, 0.0], [0.0, 1.0 / big]]).unwrap();
let b = Vector::<2>::new([big, big]);
assert_unrepresentable(
&a.solve_exact_rounded_f64(b),
Some(0),
UnrepresentableReason::NotFinite,
);
}
#[test]
fn solve_exact_f64_underflow_returns_err_for_nonzero_exact_component() {
let tiny = f64::from_bits(1);
let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
let b = Vector::<1>::new([tiny]);
assert_unrepresentable(
&a.solve_exact_f64(b),
Some(0),
UnrepresentableReason::RequiresRounding,
);
}
#[test]
fn solve_exact_f64_accepts_smallest_subnormal_result() {
let tiny = f64::from_bits(1);
let a = Matrix::<1>::identity();
let b = Vector::<1>::new([tiny]);
assert_eq!(
a.solve_exact_f64(b).unwrap().into_array()[0].to_bits(),
tiny.to_bits()
);
}
#[test]
fn bareiss_solve_d1() {
let a = Matrix::<1>::try_from_rows([[2.0]]).unwrap();
let b = Vector::<1>::new([6.0]);
let x = a.solve_exact(b).unwrap();
assert_eq!(x[0], f64_to_big_rational(3.0));
}
#[test]
fn bareiss_solve_singular_column_all_zero() {
let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]])
.unwrap();
let b = Vector::<3>::new([1.0, 2.0, 3.0]);
assert_matches!(
a.solve_exact(b),
Err(LaError::Singular {
pivot_col: 1,
reason: SingularityReason::Exact,
..
})
);
}
#[test]
fn f64_to_big_rational_positive_zero() {
let r = f64_to_big_rational(0.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn f64_to_big_rational_negative_zero() {
let r = f64_to_big_rational(-0.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(0)));
}
#[test]
fn f64_to_big_rational_one() {
let r = f64_to_big_rational(1.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(1)));
}
#[test]
fn f64_to_big_rational_negative_one() {
let r = f64_to_big_rational(-1.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(-1)));
}
#[test]
fn f64_to_big_rational_half() {
let r = f64_to_big_rational(0.5);
assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(2)));
}
#[test]
fn f64_to_big_rational_quarter() {
let r = f64_to_big_rational(0.25);
assert_eq!(r, BigRational::new(BigInt::from(1), BigInt::from(4)));
}
#[test]
fn f64_to_big_rational_negative_three_and_a_half() {
let r = f64_to_big_rational(-3.5);
assert_eq!(r, BigRational::new(BigInt::from(-7), BigInt::from(2)));
}
#[test]
fn f64_to_big_rational_integer() {
let r = f64_to_big_rational(42.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(42)));
}
#[test]
fn f64_to_big_rational_power_of_two() {
let r = f64_to_big_rational(1024.0);
assert_eq!(r, BigRational::from_integer(BigInt::from(1024)));
}
#[test]
fn f64_to_big_rational_subnormal() {
let tiny = 5e-324_f64; assert!(tiny.is_subnormal());
let r = f64_to_big_rational(tiny);
assert_eq!(
r,
BigRational::new(BigInt::from(1), BigInt::from(1u32) << 1074u32)
);
}
#[test]
fn f64_to_big_rational_already_lowest_terms() {
let r = f64_to_big_rational(0.5);
assert_eq!(*r.numer(), BigInt::from(1));
assert_eq!(*r.denom(), BigInt::from(2));
}
#[test]
fn f64_to_big_rational_round_trip() {
let values = [
0.0,
1.0,
-1.0,
0.5,
0.25,
0.1,
42.0,
-3.5,
1e10,
1e-10,
f64::MAX / 2.0,
f64::MIN_POSITIVE,
5e-324,
];
for &v in &values {
let r = f64_to_big_rational(v);
let back = r.to_f64().expect("round-trip to_f64 failed");
assert_eq!(
v.to_bits(),
back.to_bits(),
"round-trip failed for {v}: got {back}"
);
}
}
#[test]
fn decompose_f64_rejects_non_finite_inputs() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_non_finite_input_scalar(&decompose_f64(value));
}
}
}