use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizerError {
InvalidParameter,
OutOfRange,
}
pub struct UniformQuantizer<S> {
bits: u8,
levels: usize,
u_min: S,
u_max: S,
delta: S,
}
impl<S: ControlScalar> UniformQuantizer<S> {
pub fn new(bits: u8, u_min: S, u_max: S) -> Result<Self, QuantizerError> {
if bits == 0 || bits > 63 {
return Err(QuantizerError::InvalidParameter);
}
if u_min >= u_max {
return Err(QuantizerError::InvalidParameter);
}
let levels: usize = 1usize << (bits as usize);
let delta = (u_max - u_min) / S::from_f64((levels - 1) as f64);
if delta <= S::ZERO {
return Err(QuantizerError::InvalidParameter);
}
Ok(Self {
bits,
levels,
u_min,
u_max,
delta,
})
}
pub fn quantize(&self, u: S) -> S {
let u_clamped = u.clamp_val(self.u_min, self.u_max);
let normalised = (u_clamped - self.u_min) / self.delta;
let rounded = S::from_f64(libm::round(normalised.to_f64()));
let max_idx = S::from_f64((self.levels - 1) as f64);
let idx = rounded.clamp_val(S::ZERO, max_idx);
self.u_min + self.delta * idx
}
pub fn error(&self, u: S) -> S {
let u_clamped = u.clamp_val(self.u_min, self.u_max);
u_clamped - self.quantize(u)
}
pub fn snr_db(&self, _signal_rms: S) -> S {
S::from_f64(6.02 * self.bits as f64 + 1.76)
}
pub fn levels(&self) -> usize {
self.levels
}
pub fn delta(&self) -> S {
self.delta
}
}
pub struct LogQuantizer<S> {
rho: S,
delta_min: S,
n_levels: usize,
}
impl<S: ControlScalar> LogQuantizer<S> {
pub fn new(rho: S, delta_min: S, n_levels: usize) -> Result<Self, QuantizerError> {
if rho <= S::ZERO || rho >= S::ONE {
return Err(QuantizerError::InvalidParameter);
}
if delta_min <= S::ZERO {
return Err(QuantizerError::InvalidParameter);
}
if n_levels == 0 {
return Err(QuantizerError::InvalidParameter);
}
Ok(Self {
rho,
delta_min,
n_levels,
})
}
fn boundary(&self, k: usize) -> S {
let pow_val = libm::pow(self.rho.to_f64(), k as f64);
self.delta_min / S::from_f64(pow_val)
}
pub fn quantize(&self, u: S) -> S {
let abs_u = S::from_f64(libm::fabs(u.to_f64()));
let sign = if u < S::ZERO { -S::ONE } else { S::ONE };
if abs_u < self.delta_min {
return S::ZERO;
}
let mut best_k: usize = 0;
for k in 0..self.n_levels {
if abs_u >= self.boundary(k) {
best_k = k;
} else {
break;
}
}
let lower = self.boundary(best_k);
let quantized = if best_k + 1 < self.n_levels {
let upper = self.boundary(best_k + 1);
let mid = (lower + upper) * S::HALF;
if abs_u < mid {
lower
} else {
upper
}
} else {
lower
};
sign * quantized
}
pub fn n_levels(&self) -> usize {
self.n_levels
}
}
pub struct DynamicQuantizer<S> {
inner: UniformQuantizer<S>,
zoom: S,
rho: S,
zoom_min: S,
zoom_max: S,
}
impl<S: ControlScalar> DynamicQuantizer<S> {
pub fn new(bits: u8, u_min: S, u_max: S, rho: S) -> Result<Self, QuantizerError> {
if rho <= S::ONE {
return Err(QuantizerError::InvalidParameter);
}
let inner = UniformQuantizer::new(bits, u_min, u_max)?;
Ok(Self {
inner,
zoom: S::ONE,
rho,
zoom_min: S::from_f64(1e-6),
zoom_max: S::from_f64(1e6),
})
}
pub fn quantize(&mut self, u: S) -> Result<S, QuantizerError> {
let ratio = u / self.zoom;
let abs_ratio = S::from_f64(libm::fabs(ratio.to_f64()));
if abs_ratio > S::ONE {
self.zoom = (self.zoom * self.rho).clamp_val(self.zoom_min, self.zoom_max);
} else if abs_ratio < S::HALF {
let new_zoom = self.zoom / self.rho;
self.zoom = new_zoom.clamp_val(self.zoom_min, self.zoom_max);
}
let ratio_new = u / self.zoom;
let q = self.inner.quantize(ratio_new);
Ok(self.zoom * q)
}
pub fn zoom_factor(&self) -> S {
self.zoom
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uniform_integer_aligned_signal_quantizes_exactly() {
let q = UniformQuantizer::new(8, 0.0_f64, 255.0_f64).unwrap();
assert!((q.quantize(0.0) - 0.0).abs() < 1e-9);
assert!((q.quantize(1.0) - 1.0).abs() < 1e-9);
assert!((q.quantize(128.0) - 128.0).abs() < 1e-9);
assert!((q.quantize(255.0) - 255.0).abs() < 1e-9);
}
#[test]
fn uniform_midpoint_rounds_to_nearest_level() {
let q = UniformQuantizer::new(2, 0.0_f64, 3.0_f64).unwrap();
let v = q.quantize(0.4);
assert!((v - 0.0).abs() < 1e-9, "0.4 → 0, got {v}");
let v = q.quantize(0.6);
assert!((v - 1.0).abs() < 1e-9, "0.6 → 1, got {v}");
}
#[test]
fn uniform_snr_formula_correct() {
let q = UniformQuantizer::new(8, -1.0_f64, 1.0_f64).unwrap();
let snr = q.snr_db(0.707);
let expected = 6.02 * 8.0 + 1.76; assert!(
(snr - expected).abs() < 0.01,
"SNR got {snr}, expected {expected}"
);
}
#[test]
fn uniform_out_of_range_clamps() {
let q = UniformQuantizer::new(4, 0.0_f64, 1.0_f64).unwrap();
let v = q.quantize(-5.0);
assert!((v - 0.0).abs() < 1e-9, "Below min → 0.0, got {v}");
let v = q.quantize(99.0);
assert!((v - 1.0).abs() < 1e-9, "Above max → 1.0, got {v}");
}
#[test]
fn uniform_error_is_small() {
let q = UniformQuantizer::new(8, -1.0_f64, 1.0_f64).unwrap();
let delta = q.delta();
for i in 0..20 {
let u = -1.0 + (i as f64) * 0.1;
let e = q.error(u).abs();
assert!(
e <= delta / 2.0 + 1e-9,
"Error {e} exceeds delta/2 at u={u}"
);
}
}
#[test]
fn uniform_invalid_params_rejected() {
assert!(UniformQuantizer::<f64>::new(0, 0.0, 1.0).is_err());
assert!(UniformQuantizer::<f64>::new(8, 1.0, 0.0).is_err());
assert!(UniformQuantizer::<f64>::new(8, 1.0, 1.0).is_err());
}
#[test]
fn log_sign_symmetry() {
let q = LogQuantizer::new(0.5_f64, 0.1, 5).unwrap();
for i in 1..10 {
let u = i as f64 * 0.15;
let pos = q.quantize(u);
let neg = q.quantize(-u);
assert!(
(pos + neg).abs() < 1e-9,
"Sign symmetry violated at u={u}: Q(u)={pos}, Q(-u)={neg}"
);
}
}
#[test]
fn log_dead_zone_maps_to_zero() {
let q = LogQuantizer::new(0.5_f64, 1.0, 4).unwrap();
assert!((q.quantize(0.0)).abs() < 1e-9);
assert!((q.quantize(0.5)).abs() < 1e-9);
assert!((q.quantize(-0.5)).abs() < 1e-9);
}
#[test]
fn log_monotone_levels_positive() {
let q = LogQuantizer::new(0.5_f64, 1.0, 5).unwrap();
let mut prev = q.quantize(1.1_f64);
for i in 2..6 {
let u = i as f64 * 1.5;
let v = q.quantize(u);
assert!(v >= prev - 1e-9, "Not monotone: u={u}, v={v}, prev={prev}");
prev = v;
}
}
#[test]
fn log_invalid_params_rejected() {
assert!(LogQuantizer::<f64>::new(1.5, 0.1, 4).is_err()); assert!(LogQuantizer::<f64>::new(0.5, -1.0, 4).is_err()); assert!(LogQuantizer::<f64>::new(0.5, 0.1, 0).is_err()); }
#[test]
fn dynamic_zoom_grows_with_large_signal() {
let mut dq = DynamicQuantizer::new(4, -1.0_f64, 1.0_f64, 2.0).unwrap();
let initial_zoom = dq.zoom_factor();
for _ in 0..10 {
let _ = dq.quantize(100.0);
}
assert!(
dq.zoom_factor() > initial_zoom,
"Zoom should grow with large signal, got {}",
dq.zoom_factor()
);
}
#[test]
fn dynamic_zoom_shrinks_with_small_signal() {
let mut dq = DynamicQuantizer::new(4, -1.0_f64, 1.0_f64, 2.0).unwrap();
for _ in 0..20 {
let _ = dq.quantize(50.0);
}
let large_zoom = dq.zoom_factor();
for _ in 0..30 {
let _ = dq.quantize(0.0);
}
assert!(
dq.zoom_factor() < large_zoom,
"Zoom should shrink with small signal"
);
}
#[test]
fn dynamic_zoom_bounded() {
let mut dq = DynamicQuantizer::new(4, -1.0_f64, 1.0_f64, 2.0).unwrap();
for _ in 0..100 {
let _ = dq.quantize(1e9);
}
assert!(dq.zoom_factor() <= 1e6 + 1e-6, "Zoom exceeded max");
let mut dq2 = DynamicQuantizer::new(4, -1.0_f64, 1.0_f64, 2.0).unwrap();
for _ in 0..200 {
let _ = dq2.quantize(0.0);
}
assert!(dq2.zoom_factor() >= 1e-6 - 1e-12, "Zoom went below min");
}
#[test]
fn dynamic_invalid_rho_rejected() {
assert!(DynamicQuantizer::<f64>::new(8, -1.0, 1.0, 0.5).is_err()); assert!(DynamicQuantizer::<f64>::new(8, -1.0, 1.0, 1.0).is_err()); }
}