#[inline]
pub fn quantize_q16_16(x: f64) -> i32 {
const SCALE: f64 = 65536.0; let scaled = x * SCALE;
let rounded = if scaled >= 0.0 {
scaled + 0.5
} else {
scaled - 0.5
};
if rounded >= i32::MAX as f64 {
i32::MAX
} else if rounded <= i32::MIN as f64 {
i32::MIN
} else {
rounded as i32
}
}
#[inline]
pub fn dequantize_q16_16(raw: i32) -> f64 {
raw as f64 / 65536.0
}
#[inline]
pub fn q16_16_to_f32(raw: i32) -> f32 {
raw as f32 / 65536.0_f32
}
#[inline]
pub fn quantize_f32(x: f32) -> i32 {
quantize_q16_16(x as f64)
}
#[inline]
pub fn mul_q16_16(a: i32, b: i32) -> i32 {
let prod = a as i64 * b as i64;
let shifted = prod >> 16;
if shifted > i32::MAX as i64 {
i32::MAX
} else if shifted < i32::MIN as i64 {
i32::MIN
} else {
shifted as i32
}
}
#[inline]
pub fn add_q16_16(a: i32, b: i32) -> i32 {
a.saturating_add(b)
}
pub const MODE_LABEL: &str = "fixed_q16_16";
pub const FRAC_BITS: u32 = 16;
pub const SCALE: i32 = 1 << FRAC_BITS;
pub const MAX_VALUE: f64 = i32::MAX as f64 / 65536.0;
pub const MIN_VALUE: f64 = i32::MIN as f64 / 65536.0;
pub const RESOLUTION: f64 = 1.0 / 65536.0;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_zero() {
let raw = quantize_q16_16(0.0);
let back = dequantize_q16_16(raw);
assert_eq!(raw, 0);
assert!((back - 0.0).abs() < 1e-10);
}
#[test]
fn round_trip_one() {
let raw = quantize_q16_16(1.0);
assert_eq!(raw, 65536);
let back = dequantize_q16_16(raw);
assert!((back - 1.0).abs() < 1e-10, "back={}", back);
}
#[test]
fn round_trip_negative() {
let raw = quantize_q16_16(-1.0);
assert_eq!(raw, -65536);
let back = dequantize_q16_16(raw);
assert!((back - (-1.0)).abs() < 1e-10);
}
#[test]
fn round_trip_fractional() {
let x = 0.12345_f64;
let raw = quantize_q16_16(x);
let back = dequantize_q16_16(raw);
assert!(
(back - x).abs() < RESOLUTION + 1e-14,
"round-trip error {} (expected < {})", (back - x).abs(), RESOLUTION
);
}
#[test]
fn saturation_on_overflow() {
let raw = quantize_q16_16(1_000_000.0);
assert_eq!(raw, i32::MAX, "must saturate at i32::MAX");
let raw_neg = quantize_q16_16(-1_000_000.0);
assert_eq!(raw_neg, i32::MIN, "must saturate at i32::MIN");
}
#[test]
fn multiply_integers() {
let a = quantize_q16_16(2.0);
let b = quantize_q16_16(3.0);
let c = mul_q16_16(a, b);
let back = dequantize_q16_16(c);
assert!((back - 6.0).abs() < 1e-4, "2×3={}", back);
}
#[test]
fn multiply_fractions() {
let a = quantize_q16_16(0.5);
let b = quantize_q16_16(0.5);
let c = mul_q16_16(a, b);
let back = dequantize_q16_16(c);
assert!((back - 0.25).abs() < 1e-4, "0.5×0.5={}", back);
}
#[test]
fn add_saturates() {
let big = i32::MAX / 2 + 10;
let sum = add_q16_16(big, big);
assert_eq!(sum, i32::MAX, "overflow must saturate");
}
#[test]
fn f32_conversion_matches() {
let x = 0.0456_f32;
let raw = quantize_f32(x);
let back = q16_16_to_f32(raw);
assert!((back - x).abs() < 2.0 * RESOLUTION as f32, "f32 round-trip error");
}
#[test]
fn constants_consistent() {
assert_eq!(SCALE, 65536);
assert_eq!(FRAC_BITS, 16);
assert!((MAX_VALUE - (i32::MAX as f64 / 65536.0)).abs() < 1e-10);
assert!((MIN_VALUE - (i32::MIN as f64 / 65536.0)).abs() < 1e-10);
assert!((RESOLUTION - 1.0 / 65536.0).abs() < 1e-20);
}
#[test]
fn mode_label_is_canonical() {
assert_eq!(MODE_LABEL, "fixed_q16_16");
}
#[test]
fn periodic_resync_triggers_at_period() {
let cfg = PeriodicResyncConfig { period: 1000, max_drift_ulps: 100 };
assert!(cfg.should_resync(999));
assert!(!cfg.should_resync(998));
}
#[test]
fn apply_resync_clamps_to_envelope() {
let rho_q = quantize_f32(0.10);
let drifted = rho_q + 200; let (clamped, corrected) = apply_periodic_resync(drifted, rho_q, 100);
assert!(corrected, "value outside tolerance should be corrected");
assert!(clamped <= rho_q, "clamped value must not exceed rho_q");
}
#[test]
fn apply_resync_within_tolerance_unchanged() {
let rho_q = quantize_f32(0.10);
let close = rho_q + 50; let (val, corrected) = apply_periodic_resync(close, rho_q, 100);
assert!(!corrected, "within-tolerance value must not be corrected");
assert_eq!(val, close, "within-tolerance value must be unchanged");
}
}
#[derive(Debug, Clone, Copy)]
pub struct PeriodicResyncConfig {
pub period: u32,
pub max_drift_ulps: i32,
}
impl PeriodicResyncConfig {
pub const DEFAULT: Self = Self { period: 65536, max_drift_ulps: 32 };
#[inline]
pub fn should_resync(&self, obs_mod_period: u32) -> bool {
obs_mod_period == self.period.saturating_sub(1)
}
}
#[inline]
pub fn apply_periodic_resync(
accumulator: i32,
reference_q: i32,
max_drift_ulps: i32,
) -> (i32, bool) {
let drift = accumulator.wrapping_sub(reference_q);
let abs_drift = drift.unsigned_abs() as i32;
if abs_drift > max_drift_ulps {
(reference_q, true)
} else {
(accumulator, false)
}
}