use std::fmt;
pub const MIN_SIGNIFICANT_BITS: u8 = 1;
pub const MAX_SIGNIFICANT_BITS: u8 = 52;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SignificantBitsError {
requested: u8,
}
impl SignificantBitsError {
pub const fn requested(&self) -> u8 {
self.requested
}
}
impl fmt::Display for SignificantBitsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"significant bits must be in {MIN_SIGNIFICANT_BITS}..={MAX_SIGNIFICANT_BITS}, got {}",
self.requested
)
}
}
impl std::error::Error for SignificantBitsError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SignificantBits(u8);
impl Default for SignificantBits {
fn default() -> Self {
Self::DEFAULT
}
}
impl SignificantBits {
pub const DEFAULT: Self = Self(8);
pub const fn new(bits: u8) -> Result<Self, SignificantBitsError> {
if bits < MIN_SIGNIFICANT_BITS || bits > MAX_SIGNIFICANT_BITS {
Err(SignificantBitsError { requested: bits })
} else {
Ok(Self(bits))
}
}
pub const fn get(self) -> u8 {
self.0
}
pub const fn exact_below(self) -> u64 {
1u64 << self.0
}
pub fn max_relative_error(self, rounding: Rounding) -> f64 {
let exponent = match rounding {
Rounding::Floor | Rounding::Ceil => 1 - i32::from(self.0),
Rounding::Midpoint => -i32::from(self.0),
};
f64::from(exponent).exp2()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[non_exhaustive]
pub enum Rounding {
Floor,
Ceil,
#[default]
Midpoint,
}
impl Rounding {
pub const fn as_str(self) -> &'static str {
match self {
Rounding::Floor => "floor",
Rounding::Ceil => "ceil",
Rounding::Midpoint => "midpoint",
}
}
}
impl fmt::Display for Rounding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub mod rounding {
use super::Rounding;
pub trait RoundingTag {
const ROUNDING: Rounding;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Floor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Ceil;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Midpoint;
impl RoundingTag for Floor {
const ROUNDING: Rounding = Rounding::Floor;
}
impl RoundingTag for Ceil {
const ROUNDING: Rounding = Rounding::Ceil;
}
impl RoundingTag for Midpoint {
const ROUNDING: Rounding = Rounding::Midpoint;
}
}
use rounding::RoundingTag;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Bits<const N: u8, R: RoundingTag = rounding::Midpoint>(std::marker::PhantomData<R>);
impl<const N: u8, R: RoundingTag> Bits<N, R> {
pub const BITS: SignificantBits = match SignificantBits::new(N) {
Ok(bits) => bits,
Err(_) => panic!("significant bits must be in 1..=52"),
};
pub const ROUNDING: Rounding = R::ROUNDING;
pub const QUANTIZER: Quantizer = Quantizer::new(Self::BITS, Self::ROUNDING);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Quantizer {
bits: SignificantBits,
rounding: Rounding,
}
impl Default for Quantizer {
fn default() -> Self {
Self::new(SignificantBits::DEFAULT, Rounding::Midpoint)
}
}
impl Quantizer {
pub const fn new(bits: SignificantBits, rounding: Rounding) -> Self {
Self { bits, rounding }
}
pub const fn significant_bits(self) -> SignificantBits {
self.bits
}
pub const fn rounding(self) -> Rounding {
self.rounding
}
pub fn max_relative_error(self) -> f64 {
self.bits.max_relative_error(self.rounding)
}
pub const fn quantize_u64(self, value: u64) -> u64 {
let bits = self.bits.get() as u32;
let width = u64::BITS - value.leading_zeros();
if width <= bits {
return value;
}
let shift = width - bits;
let low = (value >> shift) << shift;
match self.rounding {
Rounding::Floor => low,
Rounding::Ceil => {
if low == value {
value
} else {
low.saturating_add(1u64 << shift)
}
}
Rounding::Midpoint => low.saturating_add(1u64 << (shift - 1)),
}
}
pub fn quantize_f64(self, value: f64) -> f64 {
if !value.is_finite() || value == 0.0 {
return value;
}
let magnitude = value.abs();
if magnitude < f64::MIN_POSITIVE {
return value;
}
let bits = u32::from(self.bits.get());
let drop = 53 - bits;
let mask = u64::MAX << drop;
let raw = magnitude.to_bits();
let low_bits = raw & mask;
let already_on_lattice = low_bits == raw;
let rounding = if value.is_sign_negative() {
match self.rounding {
Rounding::Floor => Rounding::Ceil,
Rounding::Ceil => Rounding::Floor,
Rounding::Midpoint => Rounding::Midpoint,
}
} else {
self.rounding
};
let quantized = match rounding {
Rounding::Floor => low_bits,
Rounding::Ceil => {
if already_on_lattice {
raw
} else {
low_bits + (1u64 << drop)
}
}
Rounding::Midpoint => low_bits + (1u64 << (drop - 1)),
};
let quantized = f64::from_bits(quantized);
let quantized = if quantized.is_finite() {
quantized
} else {
f64::MAX
};
if value.is_sign_negative() {
-quantized
} else {
quantized
}
}
}
pub trait QuantizerSource {
fn quantizer(&self) -> Quantizer;
}
impl QuantizerSource for Quantizer {
fn quantizer(&self) -> Quantizer {
*self
}
}
impl<const N: u8, R: RoundingTag> QuantizerSource for Bits<N, R> {
fn quantizer(&self) -> Quantizer {
Self::QUANTIZER
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reference_quantize_u64(value: u64, bits: u8, rounding: Rounding) -> u64 {
if value == 0 {
return 0;
}
let mut width = 0u32;
let mut remaining = value;
while remaining > 0 {
width += 1;
remaining /= 2;
}
if width <= u32::from(bits) {
return value;
}
let shift = width - u32::from(bits);
let step = 2u128.pow(shift);
let value = u128::from(value);
let low = (value / step) * step;
let result = match rounding {
Rounding::Floor => low,
Rounding::Ceil => {
if low == value {
low
} else {
low + step
}
}
Rounding::Midpoint => low + step / 2,
};
if result > u128::from(u64::MAX) {
u64::MAX
} else {
result as u64
}
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
}
const ALL_MODES: [Rounding; 3] = [Rounding::Floor, Rounding::Ceil, Rounding::Midpoint];
fn interesting_values() -> Vec<u64> {
let mut values = vec![0, u64::MAX, u64::MAX - 1];
values.extend(0..512u64);
for exponent in 0..64u32 {
let power = 1u64 << exponent;
values.push(power);
values.push(power.saturating_sub(1));
values.push(power.saturating_add(1));
}
for exponent in 1..64u32 {
values.push((1u64 << exponent) - 1);
}
values.sort_unstable();
values.dedup();
values
}
fn quantizer(bits: u8, rounding: Rounding) -> Quantizer {
Quantizer::new(SignificantBits::new(bits).unwrap(), rounding)
}
#[test]
fn matches_reference_on_interesting_values() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for value in interesting_values() {
assert_eq!(
quantizer.quantize_u64(value),
reference_quantize_u64(value, bits, rounding),
"bits={bits} rounding={rounding} value={value}"
);
}
}
}
}
#[test]
fn matches_reference_on_random_values() {
let mut rng = Rng::new(0x5EED_1234_ABCD_0001);
for _ in 0..20_000 {
let value = rng.next_u64();
let shifted = value >> (rng.next_u64() % 64);
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for candidate in [value, shifted] {
assert_eq!(
quantizer.quantize_u64(candidate),
reference_quantize_u64(candidate, bits, rounding),
"bits={bits} rounding={rounding} value={candidate}"
);
}
}
}
}
}
#[test]
fn floor_never_overstates_and_ceil_never_understates() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for value in interesting_values() {
assert!(
quantizer(bits, Rounding::Floor).quantize_u64(value) <= value,
"floor overstated: bits={bits} value={value}"
);
assert!(
quantizer(bits, Rounding::Ceil).quantize_u64(value) >= value,
"ceil understated: bits={bits} value={value}"
);
}
}
}
#[test]
fn relative_error_stays_within_documented_bound() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let significant = SignificantBits::new(bits).unwrap();
for rounding in ALL_MODES {
let quantizer = Quantizer::new(significant, rounding);
let bound = significant.max_relative_error(rounding);
for value in interesting_values() {
if value == 0 {
continue;
}
let quantized = quantizer.quantize_u64(value);
let error = (quantized as f64 - value as f64).abs() / value as f64;
match rounding {
Rounding::Floor | Rounding::Ceil => assert!(
error <= bound,
"bits={bits} rounding={rounding} value={value} error={error} bound={bound}"
),
Rounding::Midpoint => assert!(
error <= bound,
"bits={bits} rounding={rounding} value={value} error={error} bound={bound}"
),
}
}
}
}
}
#[test]
fn all_modes_are_idempotent() {
let mut rng = Rng::new(0xD1CE_0000_0000_0007);
let mut values = interesting_values();
for _ in 0..5_000 {
values.push(rng.next_u64() >> (rng.next_u64() % 64));
}
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for &value in &values {
let once = quantizer.quantize_u64(value);
let twice = quantizer.quantize_u64(once);
assert_eq!(
once, twice,
"not idempotent: bits={bits} rounding={rounding} value={value}"
);
}
}
}
}
#[test]
fn values_below_exact_below_are_untouched() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let significant = SignificantBits::new(bits).unwrap();
let threshold = significant.exact_below();
for rounding in ALL_MODES {
let quantizer = Quantizer::new(significant, rounding);
let limit = threshold.min(4096);
for value in 0..limit {
assert_eq!(
quantizer.quantize_u64(value),
value,
"bits={bits} rounding={rounding} value={value}"
);
}
assert_eq!(quantizer.quantize_u64(threshold - 1), threshold - 1);
}
}
}
#[test]
fn ceil_saturates_instead_of_overflowing() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let quantizer = quantizer(bits, Rounding::Ceil);
assert_eq!(quantizer.quantize_u64(u64::MAX), u64::MAX);
assert_eq!(quantizer.quantize_u64(u64::MAX - 1), u64::MAX);
}
}
#[test]
fn zero_is_a_fixed_point() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
assert_eq!(quantizer(bits, rounding).quantize_u64(0), 0);
}
}
}
#[test]
fn quantizing_is_monotonic() {
let mut rng = Rng::new(0xA5A5_0000_1111_2222);
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for _ in 0..2_000 {
let a = rng.next_u64() >> (rng.next_u64() % 64);
let b = rng.next_u64() >> (rng.next_u64() % 64);
let (low, high) = if a <= b { (a, b) } else { (b, a) };
assert!(
quantizer.quantize_u64(low) <= quantizer.quantize_u64(high),
"not monotonic: bits={bits} rounding={rounding} {low} vs {high}"
);
}
}
}
}
#[test]
fn floor_and_midpoint_produce_one_representative_per_bucket() {
use std::collections::BTreeSet;
for bits in [1u8, 2, 3, 4, 8] {
let mut floor_outputs = BTreeSet::new();
let mut midpoint_outputs = BTreeSet::new();
let mut ceil_outputs = BTreeSet::new();
for value in 0..100_000u64 {
floor_outputs.insert(quantizer(bits, Rounding::Floor).quantize_u64(value));
midpoint_outputs.insert(quantizer(bits, Rounding::Midpoint).quantize_u64(value));
ceil_outputs.insert(quantizer(bits, Rounding::Ceil).quantize_u64(value));
}
assert_eq!(
floor_outputs.len(),
midpoint_outputs.len(),
"floor and midpoint should have equal alphabet size at {bits} bits"
);
assert!(
ceil_outputs.len() >= floor_outputs.len(),
"ceil alphabet should not be smaller at {bits} bits"
);
}
}
#[test]
fn quantizer_accessors_round_trip() {
let bits = SignificantBits::new(11).unwrap();
let quantizer = Quantizer::new(bits, Rounding::Ceil);
assert_eq!(quantizer.significant_bits(), bits);
assert_eq!(quantizer.rounding(), Rounding::Ceil);
assert_eq!(
quantizer.max_relative_error(),
bits.max_relative_error(Rounding::Ceil)
);
}
#[test]
fn quantizer_default_is_eight_bit_midpoint() {
let quantizer = Quantizer::default();
assert_eq!(quantizer.significant_bits(), SignificantBits::DEFAULT);
assert_eq!(quantizer.rounding(), Rounding::Midpoint);
}
#[test]
fn bits_tag_produces_matching_quantizer() {
assert_eq!(
Bits::<8>::QUANTIZER,
Quantizer::new(SignificantBits::new(8).unwrap(), Rounding::Midpoint)
);
assert_eq!(
Bits::<11, rounding::Floor>::QUANTIZER,
Quantizer::new(SignificantBits::new(11).unwrap(), Rounding::Floor)
);
}
#[test]
fn worked_example_from_module_docs() {
let bits = SignificantBits::new(4).unwrap();
assert_eq!(
Quantizer::new(bits, Rounding::Floor).quantize_u64(1000),
960
);
assert_eq!(
Quantizer::new(bits, Rounding::Midpoint).quantize_u64(1000),
992
);
assert_eq!(
Quantizer::new(bits, Rounding::Ceil).quantize_u64(1000),
1024
);
}
#[test]
fn binade_spacing_doubles() {
let quantizer = quantizer(3, Rounding::Floor);
let representatives = |range: std::ops::Range<u64>| {
range
.map(|v| quantizer.quantize_u64(v))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
};
assert_eq!(representatives(8..16), vec![8, 10, 12, 14]);
assert_eq!(representatives(16..32), vec![16, 20, 24, 28]);
assert_eq!(representatives(32..64), vec![32, 40, 48, 56]);
}
#[test]
fn each_binade_holds_two_to_the_bits_minus_one_values() {
for bits in 1u8..=8 {
let quantizer = quantizer(bits, Rounding::Floor);
let expected = 1usize << (bits - 1);
for exponent in u32::from(bits)..u32::from(bits) + 6 {
let start = 1u64 << exponent;
let end = start * 2;
let distinct = (start..end)
.map(|v| quantizer.quantize_u64(v))
.collect::<std::collections::BTreeSet<_>>()
.len();
assert_eq!(
distinct, expected,
"bits={bits} binade=[{start}, {end}) should hold {expected} values"
);
}
}
}
#[test]
fn one_bit_collapses_each_binade_to_its_power_of_two() {
let quantizer = quantizer(1, Rounding::Floor);
for exponent in 1..20u32 {
let start = 1u64 << exponent;
for value in start..start * 2 {
assert_eq!(quantizer.quantize_u64(value), start, "value={value}");
}
}
}
fn reference_quantize_f64(value: f64, bits: u8, rounding: Rounding) -> f64 {
if !value.is_finite() || value == 0.0 {
return value;
}
let magnitude = value.abs();
if magnitude < f64::MIN_POSITIVE {
return value;
}
let rounding = if value.is_sign_negative() {
match rounding {
Rounding::Floor => Rounding::Ceil,
Rounding::Ceil => Rounding::Floor,
Rounding::Midpoint => Rounding::Midpoint,
}
} else {
rounding
};
let mut exponent = magnitude.log2().floor() as i32;
while 2f64.powi(exponent) > magnitude {
exponent -= 1;
}
while exponent < 1024 && 2f64.powi(exponent + 1) <= magnitude {
exponent += 1;
}
let step = 2f64.powi(exponent - i32::from(bits) + 1);
let low = (magnitude / step).floor() * step;
let quantized = match rounding {
Rounding::Floor => low,
Rounding::Ceil => {
if low == magnitude {
magnitude
} else {
low + step
}
}
Rounding::Midpoint => low + step / 2.0,
};
let quantized = if quantized.is_finite() {
quantized
} else {
f64::MAX
};
if value.is_sign_negative() {
-quantized
} else {
quantized
}
}
const REFERENCE_EXPONENT_RANGE: std::ops::RangeInclusive<i32> = -500..=500;
fn reference_is_valid_for(value: f64) -> bool {
if !value.is_finite() || value == 0.0 {
return false;
}
let magnitude = value.abs();
if magnitude < f64::MIN_POSITIVE {
return false;
}
let exponent = magnitude.log2().floor() as i32;
REFERENCE_EXPONENT_RANGE.contains(&exponent)
}
fn interesting_floats() -> Vec<f64> {
let mut values = vec![
0.1,
0.5,
1.0,
1.5,
2.0,
3.0,
100.0,
1000.0,
1e6,
1e15,
1e100,
1e-100,
f64::MIN_POSITIVE,
f64::MAX,
std::f64::consts::PI,
std::f64::consts::E,
];
for exponent in -60..60i32 {
let power = 2f64.powi(exponent);
values.push(power);
values.push(power * 1.9999999999999998);
values.push(power * 1.5);
}
for v in 1..2048u64 {
values.push(v as f64);
}
values.retain(|v| v.is_finite() && *v != 0.0);
values
}
#[test]
fn f64_matches_reference() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for value in interesting_floats() {
for signed in [value, -value] {
if !reference_is_valid_for(signed) {
continue;
}
let actual = quantizer.quantize_f64(signed);
let expected = reference_quantize_f64(signed, bits, rounding);
assert_eq!(
actual.to_bits(),
expected.to_bits(),
"bits={bits} rounding={rounding} value={signed:e}: {actual:e} != {expected:e}"
);
}
}
}
}
}
#[test]
fn f64_matches_reference_on_random_values() {
let mut rng = Rng::new(0xBEEF_0F64_0000_0001);
let mut checked = 0u32;
while checked < 20_000 {
let candidate = f64::from_bits(rng.next_u64());
if !reference_is_valid_for(candidate) {
continue;
}
checked += 1;
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
let actual = quantizer.quantize_f64(candidate);
let expected = reference_quantize_f64(candidate, bits, rounding);
assert_eq!(
actual.to_bits(),
expected.to_bits(),
"bits={bits} rounding={rounding} value={candidate:e}"
);
}
}
}
}
#[test]
fn f64_agrees_with_u64_at_or_above_exact_below() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let significant = SignificantBits::new(bits).unwrap();
let threshold = significant.exact_below();
for rounding in ALL_MODES {
let quantizer = Quantizer::new(significant, rounding);
let mut candidates: Vec<u64> = (threshold..threshold.saturating_mul(4))
.take(4096)
.collect();
for exponent in 20..53u32 {
candidates.push(1u64 << exponent);
candidates.push((1u64 << exponent) + 1);
candidates.push((1u64 << exponent) - 1);
}
candidates.retain(|v| *v < (1u64 << 53) && *v >= threshold);
for value in candidates {
let via_u64 = quantizer.quantize_u64(value);
let via_f64 = quantizer.quantize_f64(value as f64);
assert_eq!(
via_u64 as f64, via_f64,
"bits={bits} rounding={rounding} value={value}"
);
}
}
}
}
#[test]
fn f64_passes_through_special_values() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
assert!(quantizer.quantize_f64(f64::NAN).is_nan());
assert_eq!(quantizer.quantize_f64(f64::INFINITY), f64::INFINITY);
assert_eq!(quantizer.quantize_f64(f64::NEG_INFINITY), f64::NEG_INFINITY);
assert_eq!(quantizer.quantize_f64(0.0).to_bits(), 0.0f64.to_bits());
assert_eq!(quantizer.quantize_f64(-0.0).to_bits(), (-0.0f64).to_bits());
}
}
}
#[test]
fn f64_passes_through_subnormals() {
let subnormals = [
f64::from_bits(1),
f64::from_bits(2),
f64::from_bits(0x000F_FFFF_FFFF_FFFF),
-f64::from_bits(1),
];
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for value in subnormals {
assert_eq!(
quantizer.quantize_f64(value).to_bits(),
value.to_bits(),
"bits={bits} rounding={rounding} subnormal={value:e}"
);
}
}
}
}
#[test]
fn f64_one_sided_modes_bracket_the_true_value() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for value in interesting_floats() {
for signed in [value, -value] {
let floor = quantizer(bits, Rounding::Floor).quantize_f64(signed);
let ceil = quantizer(bits, Rounding::Ceil).quantize_f64(signed);
assert!(
floor <= signed,
"floor overstated: bits={bits} value={signed:e} -> {floor:e}"
);
if ceil != f64::MAX && ceil != -f64::MAX {
assert!(
ceil >= signed,
"ceil understated: bits={bits} value={signed:e} -> {ceil:e}"
);
}
}
}
}
}
#[test]
fn f64_relative_error_stays_within_documented_bound() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let significant = SignificantBits::new(bits).unwrap();
for rounding in ALL_MODES {
let quantizer = Quantizer::new(significant, rounding);
let bound = significant.max_relative_error(rounding);
for value in interesting_floats() {
for signed in [value, -value] {
let quantized = quantizer.quantize_f64(signed);
if !quantized.is_finite() {
continue;
}
let error = (quantized - signed).abs() / signed.abs();
assert!(
error <= bound * (1.0 + 1e-12),
"bits={bits} rounding={rounding} value={signed:e} error={error:e} bound={bound:e}"
);
}
}
}
}
}
#[test]
fn f64_is_idempotent() {
let mut rng = Rng::new(0xF10A_7000_0000_0001);
let mut values = interesting_floats();
for _ in 0..5_000 {
let candidate = f64::from_bits(rng.next_u64());
if candidate.is_finite() && candidate != 0.0 {
values.push(candidate);
}
}
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for &value in &values {
let once = quantizer.quantize_f64(value);
let twice = quantizer.quantize_f64(once);
assert_eq!(
once.to_bits(),
twice.to_bits(),
"not idempotent: bits={bits} rounding={rounding} value={value:e}"
);
}
}
}
}
#[test]
fn f64_is_monotonic() {
let mut rng = Rng::new(0x0FEE_1111_2222_3333);
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
for rounding in ALL_MODES {
let quantizer = quantizer(bits, rounding);
for _ in 0..2_000 {
let a = f64::from_bits(rng.next_u64());
let b = f64::from_bits(rng.next_u64());
if !a.is_finite() || !b.is_finite() {
continue;
}
let (low, high) = if a <= b { (a, b) } else { (b, a) };
assert!(
quantizer.quantize_f64(low) <= quantizer.quantize_f64(high),
"not monotonic: bits={bits} rounding={rounding} {low:e} vs {high:e}"
);
}
}
}
}
#[test]
fn f64_ceil_saturates_at_max_instead_of_going_infinite() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let quantizer = quantizer(bits, Rounding::Ceil);
let quantized = quantizer.quantize_f64(f64::MAX);
assert!(quantized.is_finite(), "bits={bits} produced {quantized:e}");
assert_eq!(quantized, f64::MAX);
}
}
#[test]
fn f64_binade_crossing_is_exact() {
let quantizer = quantizer(4, Rounding::Ceil);
for exponent in -20..20i32 {
let power = 2f64.powi(exponent);
let just_below = power * 1.9999999999999998;
let quantized = quantizer.quantize_f64(just_below);
assert_eq!(
quantized,
power * 2.0,
"exponent={exponent} value={just_below:e}"
);
}
}
#[test]
fn f64_one_bit_collapses_to_powers_of_two() {
let quantizer = quantizer(1, Rounding::Floor);
for exponent in -30..30i32 {
let power = 2f64.powi(exponent);
assert_eq!(quantizer.quantize_f64(power), power);
assert_eq!(quantizer.quantize_f64(power * 1.5), power);
assert_eq!(quantizer.quantize_f64(power * 1.99), power);
}
}
#[test]
fn f64_worked_examples_from_docs() {
let bits = SignificantBits::new(4).unwrap();
assert_eq!(
Quantizer::new(bits, Rounding::Floor).quantize_f64(1000.0),
960.0
);
assert_eq!(
Quantizer::new(bits, Rounding::Midpoint).quantize_f64(1000.0),
992.0
);
assert_eq!(
Quantizer::new(bits, Rounding::Ceil).quantize_f64(1000.0),
1024.0
);
assert_eq!(
Quantizer::new(bits, Rounding::Floor).quantize_f64(0.1),
0.09375
);
assert_eq!(
Quantizer::new(bits, Rounding::Floor).quantize_f64(-1000.0),
-1024.0
);
assert_eq!(
Quantizer::new(bits, Rounding::Ceil).quantize_f64(-1000.0),
-960.0
);
}
#[test]
fn rejects_out_of_range_bits() {
assert_eq!(SignificantBits::new(0).unwrap_err().requested(), 0);
assert_eq!(SignificantBits::new(53).unwrap_err().requested(), 53);
assert_eq!(SignificantBits::new(255).unwrap_err().requested(), 255);
}
#[test]
fn accepts_full_valid_range() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let parsed = SignificantBits::new(bits).expect("in range");
assert_eq!(parsed.get(), bits);
}
}
#[test]
fn default_is_eight_bits() {
assert_eq!(SignificantBits::DEFAULT.get(), 8);
assert_eq!(SignificantBits::default(), SignificantBits::DEFAULT);
}
#[test]
fn default_rounding_is_midpoint() {
assert_eq!(Rounding::default(), Rounding::Midpoint);
}
#[test]
fn exact_below_is_two_to_the_bits() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let parsed = SignificantBits::new(bits).unwrap();
assert_eq!(parsed.exact_below(), 1u64 << bits, "bits={bits}");
}
}
#[test]
fn max_relative_error_matches_powers_of_two() {
for bits in MIN_SIGNIFICANT_BITS..=MAX_SIGNIFICANT_BITS {
let parsed = SignificantBits::new(bits).unwrap();
let one_sided = f64::from(1 - i32::from(bits)).exp2();
let midpoint = f64::from(-i32::from(bits)).exp2();
assert_eq!(parsed.max_relative_error(Rounding::Floor), one_sided);
assert_eq!(parsed.max_relative_error(Rounding::Ceil), one_sided);
assert_eq!(parsed.max_relative_error(Rounding::Midpoint), midpoint);
assert_eq!(midpoint * 2.0, one_sided);
}
}
#[test]
fn documented_error_table_is_exact() {
let cases: &[(u8, f64, f64)] = &[
(1, 1.0, 0.5),
(2, 0.5, 0.25),
(3, 0.25, 0.125),
(4, 0.125, 0.0625),
(5, 0.0625, 0.03125),
(6, 0.03125, 0.015625),
(7, 0.015625, 0.0078125),
(8, 0.0078125, 0.00390625),
(10, 0.001953125, 0.0009765625),
(11, 0.0009765625, 0.00048828125),
(12, 0.00048828125, 0.000244140625),
(16, 0.000030517578125, 0.0000152587890625),
];
for &(bits, one_sided, midpoint) in cases {
let parsed = SignificantBits::new(bits).unwrap();
assert_eq!(
parsed.max_relative_error(Rounding::Floor),
one_sided,
"one-sided bound for {bits} bits"
);
assert_eq!(
parsed.max_relative_error(Rounding::Midpoint),
midpoint,
"midpoint bound for {bits} bits"
);
}
}
#[test]
fn decimal_digit_equivalence_table_is_correct() {
for (digits, expected_bits) in [(1u32, 5u8), (2, 8), (3, 11), (4, 15)] {
let derived = (2.0 * 10f64.powi(digits as i32)).log2().ceil() as u8;
assert_eq!(derived, expected_bits, "digits={digits}");
let bits = SignificantBits::new(expected_bits).unwrap();
let target = 10f64.powi(-(digits as i32));
assert!(
bits.max_relative_error(Rounding::Floor) < target,
"{expected_bits} bits should beat 10^-{digits}"
);
let weaker = SignificantBits::new(expected_bits - 1).unwrap();
assert!(
weaker.max_relative_error(Rounding::Floor) >= target,
"{} bits should not beat 10^-{digits}",
expected_bits - 1
);
}
}
#[test]
fn bits_tag_carries_settings() {
assert_eq!(Bits::<8>::BITS.get(), 8);
assert_eq!(Bits::<8>::ROUNDING, Rounding::Midpoint);
assert_eq!(Bits::<1, rounding::Floor>::BITS.get(), 1);
assert_eq!(Bits::<1, rounding::Floor>::ROUNDING, Rounding::Floor);
assert_eq!(Bits::<52, rounding::Ceil>::BITS.get(), 52);
assert_eq!(Bits::<52, rounding::Ceil>::ROUNDING, Rounding::Ceil);
}
#[test]
fn rounding_display_is_stable() {
assert_eq!(Rounding::Floor.to_string(), "floor");
assert_eq!(Rounding::Ceil.to_string(), "ceil");
assert_eq!(Rounding::Midpoint.to_string(), "midpoint");
}
#[test]
fn error_message_names_the_valid_range() {
let message = SignificantBits::new(53).unwrap_err().to_string();
assert_eq!(message, "significant bits must be in 1..=52, got 53");
}
}