use crate::fixed::Q0_16;
#[must_use]
pub const fn quantize<const BITS: u32>(v: Q0_16) -> (u16, i32) {
const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
let shift = 16u32.saturating_sub(BITS);
let q = match v.to_raw().checked_shr(shift) {
Some(code) => code,
None => 0,
};
let residual = (v.to_raw() as i32).saturating_sub(expand::<BITS>(q).to_raw() as i32);
(q, residual)
}
#[must_use]
pub const fn quantize_round<const BITS: u32>(v: Q0_16) -> u16 {
const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
let (q, residual) = quantize::<BITS>(v);
let max = match 1u16.checked_shl(BITS) {
Some(width) => width.saturating_sub(1),
None => u16::MAX,
};
let half = 1i32 << 15u32.saturating_sub(BITS);
if residual >= half && q < max {
q.saturating_add(1)
} else {
q
}
}
#[must_use]
pub const fn expand<const BITS: u32>(q: u16) -> Q0_16 {
const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
let shift = 16u32.saturating_sub(BITS);
let code = match 1u16.checked_shl(BITS) {
None => q,
Some(width) => q & width.saturating_sub(1),
};
match code.checked_shl(shift) {
Some(v) => Q0_16::from_raw(v),
None => Q0_16::ZERO,
}
}
#[cfg(test)]
mod tests {
use super::{Q0_16, expand, quantize, quantize_round};
fn max_code<const BITS: u32>() -> u16 {
match 1u16.checked_shl(BITS) {
Some(width) => width.saturating_sub(1),
None => u16::MAX,
}
}
fn expected_rounded<const BITS: u32>(v: u16) -> u16 {
let shift = 16u32.saturating_sub(BITS);
let bin = 1u32.checked_shl(shift).unwrap_or(1);
let half = bin.checked_shr(1).unwrap_or(0);
let numerator = u32::from(v).saturating_add(half);
let rounded = numerator.checked_div(bin).unwrap_or(0);
rounded.min(u32::from(max_code::<BITS>())) as u16
}
fn rounded_matches_reference<const BITS: u32>(v: u16) {
assert_eq!(
quantize_round::<BITS>(Q0_16::from_raw(v)),
expected_rounded::<BITS>(v),
"BITS={BITS} v={v}"
);
}
fn rounded_excursion_is_bounded<const BITS: u32>(v: u16) {
let q = quantize::<BITS>(Q0_16::from_raw(v)).0;
let rounded = quantize_round::<BITS>(Q0_16::from_raw(v));
assert!(
rounded == q || rounded == q.saturating_add(1),
"BITS={BITS} v={v} q={q} rounded={rounded}"
);
}
fn rounded_is_in_range<const BITS: u32>(v: u16) {
let rounded = quantize_round::<BITS>(Q0_16::from_raw(v));
let max = max_code::<BITS>();
assert!(
rounded <= max,
"BITS={BITS} v={v} rounded={rounded} max={max}"
);
}
fn full_scale_clamps<const BITS: u32>() {
assert_eq!(
quantize_round::<BITS>(Q0_16::ONE),
max_code::<BITS>(),
"BITS={BITS}"
);
}
fn tie_rounds_up<const BITS: u32>() {
let half = 1u16.checked_shl(15u32.saturating_sub(BITS)).unwrap_or(0);
assert_eq!(
quantize::<BITS>(Q0_16::from_raw(half)),
(0, i32::from(half)),
"BITS={BITS}"
);
assert_eq!(
quantize_round::<BITS>(Q0_16::from_raw(half)),
1,
"BITS={BITS}"
);
}
fn rounded_is_consistent_with_quantize<const BITS: u32>(v: u16) {
let (q, residual) = quantize::<BITS>(Q0_16::from_raw(v));
let half = 1i32 << 15u32.saturating_sub(BITS);
let increment = u16::from(residual >= half);
let expected = q.saturating_add(increment).min(max_code::<BITS>());
assert_eq!(
quantize_round::<BITS>(Q0_16::from_raw(v)),
expected,
"BITS={BITS} v={v} q={q} residual={residual}"
);
}
fn identity_holds<const BITS: u32>(v: u16) {
let (q, residual) = quantize::<BITS>(Q0_16::from_raw(v));
let reconstructed = (expand::<BITS>(q).to_raw() as i32).saturating_add(residual);
assert_eq!(reconstructed, i32::from(v), "BITS={BITS} v={v}");
}
#[test]
fn edges_identity() {
identity_holds::<1>(0);
identity_holds::<1>(1);
identity_holds::<1>(65535);
identity_holds::<8>(0);
identity_holds::<8>(1);
identity_holds::<8>(65535);
identity_holds::<16>(0);
identity_holds::<16>(1);
identity_holds::<16>(65535);
}
#[test]
fn bits16_is_identity_with_zero_residual() {
assert_eq!(quantize::<16>(Q0_16::from_raw(0)), (0, 0));
assert_eq!(quantize::<16>(Q0_16::from_raw(1)), (1, 0));
assert_eq!(quantize::<16>(Q0_16::from_raw(65535)), (65535, 0));
assert_eq!(expand::<16>(0xABCD), Q0_16::from_raw(0xABCD));
}
#[test]
fn truncates_toward_zero_without_rounding() {
let (q, residual) = quantize::<8>(Q0_16::from_raw(0x8080));
assert_eq!(q, 0x80);
assert_eq!(residual, 0x80);
assert_eq!(expand::<8>(q), Q0_16::from_raw(0x8000));
assert_ne!(q, 0x81);
}
#[test]
fn expand_uses_only_the_bits_wide_code() {
assert_eq!(expand::<8>(0x80), Q0_16::from_raw(0x8000));
assert_eq!(expand::<8>(0x80FF), Q0_16::from_raw(0xFF00));
}
#[test]
fn exhaustive_identity_all_valid_bits() {
for v in 0..=u16::MAX {
identity_holds::<1>(v);
identity_holds::<2>(v);
identity_holds::<3>(v);
identity_holds::<4>(v);
identity_holds::<5>(v);
identity_holds::<6>(v);
identity_holds::<7>(v);
identity_holds::<8>(v);
identity_holds::<9>(v);
identity_holds::<10>(v);
identity_holds::<11>(v);
identity_holds::<12>(v);
identity_holds::<13>(v);
identity_holds::<14>(v);
identity_holds::<15>(v);
identity_holds::<16>(v);
}
}
#[test]
fn quantize_round_matches_round_half_away_from_zero_exhaustively() {
for v in 0..=u16::MAX {
rounded_matches_reference::<1>(v);
rounded_matches_reference::<2>(v);
rounded_matches_reference::<3>(v);
rounded_matches_reference::<4>(v);
rounded_matches_reference::<5>(v);
rounded_matches_reference::<6>(v);
rounded_matches_reference::<7>(v);
rounded_matches_reference::<8>(v);
rounded_matches_reference::<9>(v);
rounded_matches_reference::<10>(v);
rounded_matches_reference::<11>(v);
rounded_matches_reference::<12>(v);
rounded_matches_reference::<13>(v);
rounded_matches_reference::<14>(v);
rounded_matches_reference::<15>(v);
rounded_matches_reference::<16>(v);
}
}
#[test]
fn quantize_round_moves_at_most_one_code_exhaustively() {
for v in 0..=u16::MAX {
rounded_excursion_is_bounded::<1>(v);
rounded_excursion_is_bounded::<2>(v);
rounded_excursion_is_bounded::<3>(v);
rounded_excursion_is_bounded::<4>(v);
rounded_excursion_is_bounded::<5>(v);
rounded_excursion_is_bounded::<6>(v);
rounded_excursion_is_bounded::<7>(v);
rounded_excursion_is_bounded::<8>(v);
rounded_excursion_is_bounded::<9>(v);
rounded_excursion_is_bounded::<10>(v);
rounded_excursion_is_bounded::<11>(v);
rounded_excursion_is_bounded::<12>(v);
rounded_excursion_is_bounded::<13>(v);
rounded_excursion_is_bounded::<14>(v);
rounded_excursion_is_bounded::<15>(v);
rounded_excursion_is_bounded::<16>(v);
}
}
#[test]
fn quantize_round_stays_in_range_exhaustively() {
for v in 0..=u16::MAX {
rounded_is_in_range::<1>(v);
rounded_is_in_range::<2>(v);
rounded_is_in_range::<3>(v);
rounded_is_in_range::<4>(v);
rounded_is_in_range::<5>(v);
rounded_is_in_range::<6>(v);
rounded_is_in_range::<7>(v);
rounded_is_in_range::<8>(v);
rounded_is_in_range::<9>(v);
rounded_is_in_range::<10>(v);
rounded_is_in_range::<11>(v);
rounded_is_in_range::<12>(v);
rounded_is_in_range::<13>(v);
rounded_is_in_range::<14>(v);
rounded_is_in_range::<15>(v);
rounded_is_in_range::<16>(v);
}
}
#[test]
fn quantize_round_clamps_full_scale_at_every_width() {
full_scale_clamps::<1>();
full_scale_clamps::<2>();
full_scale_clamps::<3>();
full_scale_clamps::<4>();
full_scale_clamps::<5>();
full_scale_clamps::<6>();
full_scale_clamps::<7>();
full_scale_clamps::<8>();
full_scale_clamps::<9>();
full_scale_clamps::<10>();
full_scale_clamps::<11>();
full_scale_clamps::<12>();
full_scale_clamps::<13>();
full_scale_clamps::<14>();
full_scale_clamps::<15>();
full_scale_clamps::<16>();
}
#[test]
fn quantize_round_is_identity_at_16_bits_exhaustively() {
for v in 0..=u16::MAX {
assert_eq!(quantize_round::<16>(Q0_16::from_raw(v)), v, "v={v}");
}
}
#[test]
fn quantize_round_ties_round_up_at_every_reduced_width() {
tie_rounds_up::<1>();
tie_rounds_up::<2>();
tie_rounds_up::<3>();
tie_rounds_up::<4>();
tie_rounds_up::<5>();
tie_rounds_up::<6>();
tie_rounds_up::<7>();
tie_rounds_up::<8>();
tie_rounds_up::<9>();
tie_rounds_up::<10>();
tie_rounds_up::<11>();
tie_rounds_up::<12>();
tie_rounds_up::<13>();
tie_rounds_up::<14>();
tie_rounds_up::<15>();
}
#[test]
fn quantize_round_is_consistent_with_quantize_exhaustively() {
for v in 0..=u16::MAX {
rounded_is_consistent_with_quantize::<1>(v);
rounded_is_consistent_with_quantize::<2>(v);
rounded_is_consistent_with_quantize::<3>(v);
rounded_is_consistent_with_quantize::<4>(v);
rounded_is_consistent_with_quantize::<5>(v);
rounded_is_consistent_with_quantize::<6>(v);
rounded_is_consistent_with_quantize::<7>(v);
rounded_is_consistent_with_quantize::<8>(v);
rounded_is_consistent_with_quantize::<9>(v);
rounded_is_consistent_with_quantize::<10>(v);
rounded_is_consistent_with_quantize::<11>(v);
rounded_is_consistent_with_quantize::<12>(v);
rounded_is_consistent_with_quantize::<13>(v);
rounded_is_consistent_with_quantize::<14>(v);
rounded_is_consistent_with_quantize::<15>(v);
rounded_is_consistent_with_quantize::<16>(v);
}
}
#[test]
fn quantize_round_matches_reference_values() {
assert_eq!(quantize_round::<1>(Q0_16::from_raw(0xFFFF)), 1);
assert_eq!(quantize_round::<1>(Q0_16::from_raw(0x8080)), 1);
assert_eq!(quantize_round::<1>(Q0_16::from_raw(0x007F)), 0);
assert_eq!(quantize_round::<4>(Q0_16::from_raw(0xFFFF)), 15);
assert_eq!(quantize_round::<4>(Q0_16::from_raw(0x8080)), 8);
assert_eq!(quantize_round::<4>(Q0_16::from_raw(0x007F)), 0);
assert_eq!(quantize_round::<8>(Q0_16::from_raw(0xFFFF)), 255);
assert_eq!(quantize_round::<8>(Q0_16::from_raw(0x8080)), 129);
assert_eq!(quantize_round::<8>(Q0_16::from_raw(0x007F)), 0);
assert_eq!(quantize_round::<15>(Q0_16::from_raw(0xFFFF)), 32767);
assert_eq!(quantize_round::<15>(Q0_16::from_raw(0x8080)), 16448);
assert_eq!(quantize_round::<15>(Q0_16::from_raw(0x007F)), 64);
assert_eq!(quantize_round::<16>(Q0_16::from_raw(0xFFFF)), 65535);
assert_eq!(quantize_round::<16>(Q0_16::from_raw(0x8080)), 32896);
assert_eq!(quantize_round::<16>(Q0_16::from_raw(0x007F)), 127);
}
}