1use crate::fixed::Q0_16;
11
12#[must_use]
23pub const fn quantize<const BITS: u32>(v: Q0_16) -> (u16, i32) {
24 const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
25 let shift = 16u32.saturating_sub(BITS);
26 let q = match v.to_raw().checked_shr(shift) {
27 Some(code) => code,
28 None => 0,
29 };
30 let residual = (v.to_raw() as i32).saturating_sub(expand::<BITS>(q).to_raw() as i32);
31 (q, residual)
32}
33
34#[must_use]
50pub const fn quantize_round<const BITS: u32>(v: Q0_16) -> u16 {
51 const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
52 let (q, residual) = quantize::<BITS>(v);
53 let max = match 1u16.checked_shl(BITS) {
54 Some(width) => width.saturating_sub(1),
55 None => u16::MAX,
56 };
57 let half = 1i32 << 15u32.saturating_sub(BITS);
58 if residual >= half && q < max {
59 q.saturating_add(1)
60 } else {
61 q
62 }
63}
64
65#[must_use]
72pub const fn expand<const BITS: u32>(q: u16) -> Q0_16 {
73 const { assert!(BITS >= 1 && BITS <= 16, "BITS must be in 1..=16") };
74 let shift = 16u32.saturating_sub(BITS);
75 let code = match 1u16.checked_shl(BITS) {
76 None => q,
77 Some(width) => q & width.saturating_sub(1),
78 };
79 match code.checked_shl(shift) {
80 Some(v) => Q0_16::from_raw(v),
81 None => Q0_16::ZERO,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::{Q0_16, expand, quantize, quantize_round};
88
89 fn max_code<const BITS: u32>() -> u16 {
90 match 1u16.checked_shl(BITS) {
91 Some(width) => width.saturating_sub(1),
92 None => u16::MAX,
93 }
94 }
95
96 fn expected_rounded<const BITS: u32>(v: u16) -> u16 {
97 let shift = 16u32.saturating_sub(BITS);
98 let bin = 1u32.checked_shl(shift).unwrap_or(1);
99 let half = bin.checked_shr(1).unwrap_or(0);
100 let numerator = u32::from(v).saturating_add(half);
101 let rounded = numerator.checked_div(bin).unwrap_or(0);
102 rounded.min(u32::from(max_code::<BITS>())) as u16
103 }
104
105 fn rounded_matches_reference<const BITS: u32>(v: u16) {
106 assert_eq!(
107 quantize_round::<BITS>(Q0_16::from_raw(v)),
108 expected_rounded::<BITS>(v),
109 "BITS={BITS} v={v}"
110 );
111 }
112
113 fn rounded_excursion_is_bounded<const BITS: u32>(v: u16) {
114 let q = quantize::<BITS>(Q0_16::from_raw(v)).0;
115 let rounded = quantize_round::<BITS>(Q0_16::from_raw(v));
116 assert!(
117 rounded == q || rounded == q.saturating_add(1),
118 "BITS={BITS} v={v} q={q} rounded={rounded}"
119 );
120 }
121
122 fn rounded_is_in_range<const BITS: u32>(v: u16) {
123 let rounded = quantize_round::<BITS>(Q0_16::from_raw(v));
124 let max = max_code::<BITS>();
125 assert!(
126 rounded <= max,
127 "BITS={BITS} v={v} rounded={rounded} max={max}"
128 );
129 }
130
131 fn full_scale_clamps<const BITS: u32>() {
132 assert_eq!(
133 quantize_round::<BITS>(Q0_16::ONE),
134 max_code::<BITS>(),
135 "BITS={BITS}"
136 );
137 }
138
139 fn tie_rounds_up<const BITS: u32>() {
140 let half = 1u16.checked_shl(15u32.saturating_sub(BITS)).unwrap_or(0);
141 assert_eq!(
142 quantize::<BITS>(Q0_16::from_raw(half)),
143 (0, i32::from(half)),
144 "BITS={BITS}"
145 );
146 assert_eq!(
147 quantize_round::<BITS>(Q0_16::from_raw(half)),
148 1,
149 "BITS={BITS}"
150 );
151 }
152
153 fn rounded_is_consistent_with_quantize<const BITS: u32>(v: u16) {
154 let (q, residual) = quantize::<BITS>(Q0_16::from_raw(v));
155 let half = 1i32 << 15u32.saturating_sub(BITS);
156 let increment = u16::from(residual >= half);
157 let expected = q.saturating_add(increment).min(max_code::<BITS>());
158 assert_eq!(
159 quantize_round::<BITS>(Q0_16::from_raw(v)),
160 expected,
161 "BITS={BITS} v={v} q={q} residual={residual}"
162 );
163 }
164
165 fn identity_holds<const BITS: u32>(v: u16) {
166 let (q, residual) = quantize::<BITS>(Q0_16::from_raw(v));
167 let reconstructed = (expand::<BITS>(q).to_raw() as i32).saturating_add(residual);
168 assert_eq!(reconstructed, i32::from(v), "BITS={BITS} v={v}");
169 }
170
171 #[test]
172 fn edges_identity() {
173 identity_holds::<1>(0);
174 identity_holds::<1>(1);
175 identity_holds::<1>(65535);
176 identity_holds::<8>(0);
177 identity_holds::<8>(1);
178 identity_holds::<8>(65535);
179 identity_holds::<16>(0);
180 identity_holds::<16>(1);
181 identity_holds::<16>(65535);
182 }
183
184 #[test]
185 fn bits16_is_identity_with_zero_residual() {
186 assert_eq!(quantize::<16>(Q0_16::from_raw(0)), (0, 0));
187 assert_eq!(quantize::<16>(Q0_16::from_raw(1)), (1, 0));
188 assert_eq!(quantize::<16>(Q0_16::from_raw(65535)), (65535, 0));
189 assert_eq!(expand::<16>(0xABCD), Q0_16::from_raw(0xABCD));
190 }
191
192 #[test]
193 fn truncates_toward_zero_without_rounding() {
194 let (q, residual) = quantize::<8>(Q0_16::from_raw(0x8080));
197 assert_eq!(q, 0x80);
198 assert_eq!(residual, 0x80);
199 assert_eq!(expand::<8>(q), Q0_16::from_raw(0x8000));
200 assert_ne!(q, 0x81);
201 }
202
203 #[test]
204 fn expand_uses_only_the_bits_wide_code() {
205 assert_eq!(expand::<8>(0x80), Q0_16::from_raw(0x8000));
206 assert_eq!(expand::<8>(0x80FF), Q0_16::from_raw(0xFF00));
207 }
208
209 #[test]
210 fn exhaustive_identity_all_valid_bits() {
211 for v in 0..=u16::MAX {
212 identity_holds::<1>(v);
213 identity_holds::<2>(v);
214 identity_holds::<3>(v);
215 identity_holds::<4>(v);
216 identity_holds::<5>(v);
217 identity_holds::<6>(v);
218 identity_holds::<7>(v);
219 identity_holds::<8>(v);
220 identity_holds::<9>(v);
221 identity_holds::<10>(v);
222 identity_holds::<11>(v);
223 identity_holds::<12>(v);
224 identity_holds::<13>(v);
225 identity_holds::<14>(v);
226 identity_holds::<15>(v);
227 identity_holds::<16>(v);
228 }
229 }
230
231 #[test]
232 fn quantize_round_matches_round_half_away_from_zero_exhaustively() {
233 for v in 0..=u16::MAX {
234 rounded_matches_reference::<1>(v);
235 rounded_matches_reference::<2>(v);
236 rounded_matches_reference::<3>(v);
237 rounded_matches_reference::<4>(v);
238 rounded_matches_reference::<5>(v);
239 rounded_matches_reference::<6>(v);
240 rounded_matches_reference::<7>(v);
241 rounded_matches_reference::<8>(v);
242 rounded_matches_reference::<9>(v);
243 rounded_matches_reference::<10>(v);
244 rounded_matches_reference::<11>(v);
245 rounded_matches_reference::<12>(v);
246 rounded_matches_reference::<13>(v);
247 rounded_matches_reference::<14>(v);
248 rounded_matches_reference::<15>(v);
249 rounded_matches_reference::<16>(v);
250 }
251 }
252
253 #[test]
254 fn quantize_round_moves_at_most_one_code_exhaustively() {
255 for v in 0..=u16::MAX {
256 rounded_excursion_is_bounded::<1>(v);
257 rounded_excursion_is_bounded::<2>(v);
258 rounded_excursion_is_bounded::<3>(v);
259 rounded_excursion_is_bounded::<4>(v);
260 rounded_excursion_is_bounded::<5>(v);
261 rounded_excursion_is_bounded::<6>(v);
262 rounded_excursion_is_bounded::<7>(v);
263 rounded_excursion_is_bounded::<8>(v);
264 rounded_excursion_is_bounded::<9>(v);
265 rounded_excursion_is_bounded::<10>(v);
266 rounded_excursion_is_bounded::<11>(v);
267 rounded_excursion_is_bounded::<12>(v);
268 rounded_excursion_is_bounded::<13>(v);
269 rounded_excursion_is_bounded::<14>(v);
270 rounded_excursion_is_bounded::<15>(v);
271 rounded_excursion_is_bounded::<16>(v);
272 }
273 }
274
275 #[test]
276 fn quantize_round_stays_in_range_exhaustively() {
277 for v in 0..=u16::MAX {
278 rounded_is_in_range::<1>(v);
279 rounded_is_in_range::<2>(v);
280 rounded_is_in_range::<3>(v);
281 rounded_is_in_range::<4>(v);
282 rounded_is_in_range::<5>(v);
283 rounded_is_in_range::<6>(v);
284 rounded_is_in_range::<7>(v);
285 rounded_is_in_range::<8>(v);
286 rounded_is_in_range::<9>(v);
287 rounded_is_in_range::<10>(v);
288 rounded_is_in_range::<11>(v);
289 rounded_is_in_range::<12>(v);
290 rounded_is_in_range::<13>(v);
291 rounded_is_in_range::<14>(v);
292 rounded_is_in_range::<15>(v);
293 rounded_is_in_range::<16>(v);
294 }
295 }
296
297 #[test]
298 fn quantize_round_clamps_full_scale_at_every_width() {
299 full_scale_clamps::<1>();
300 full_scale_clamps::<2>();
301 full_scale_clamps::<3>();
302 full_scale_clamps::<4>();
303 full_scale_clamps::<5>();
304 full_scale_clamps::<6>();
305 full_scale_clamps::<7>();
306 full_scale_clamps::<8>();
307 full_scale_clamps::<9>();
308 full_scale_clamps::<10>();
309 full_scale_clamps::<11>();
310 full_scale_clamps::<12>();
311 full_scale_clamps::<13>();
312 full_scale_clamps::<14>();
313 full_scale_clamps::<15>();
314 full_scale_clamps::<16>();
315 }
316
317 #[test]
318 fn quantize_round_is_identity_at_16_bits_exhaustively() {
319 for v in 0..=u16::MAX {
320 assert_eq!(quantize_round::<16>(Q0_16::from_raw(v)), v, "v={v}");
321 }
322 }
323
324 #[test]
325 fn quantize_round_ties_round_up_at_every_reduced_width() {
326 tie_rounds_up::<1>();
327 tie_rounds_up::<2>();
328 tie_rounds_up::<3>();
329 tie_rounds_up::<4>();
330 tie_rounds_up::<5>();
331 tie_rounds_up::<6>();
332 tie_rounds_up::<7>();
333 tie_rounds_up::<8>();
334 tie_rounds_up::<9>();
335 tie_rounds_up::<10>();
336 tie_rounds_up::<11>();
337 tie_rounds_up::<12>();
338 tie_rounds_up::<13>();
339 tie_rounds_up::<14>();
340 tie_rounds_up::<15>();
341 }
344
345 #[test]
346 fn quantize_round_is_consistent_with_quantize_exhaustively() {
347 for v in 0..=u16::MAX {
348 rounded_is_consistent_with_quantize::<1>(v);
349 rounded_is_consistent_with_quantize::<2>(v);
350 rounded_is_consistent_with_quantize::<3>(v);
351 rounded_is_consistent_with_quantize::<4>(v);
352 rounded_is_consistent_with_quantize::<5>(v);
353 rounded_is_consistent_with_quantize::<6>(v);
354 rounded_is_consistent_with_quantize::<7>(v);
355 rounded_is_consistent_with_quantize::<8>(v);
356 rounded_is_consistent_with_quantize::<9>(v);
357 rounded_is_consistent_with_quantize::<10>(v);
358 rounded_is_consistent_with_quantize::<11>(v);
359 rounded_is_consistent_with_quantize::<12>(v);
360 rounded_is_consistent_with_quantize::<13>(v);
361 rounded_is_consistent_with_quantize::<14>(v);
362 rounded_is_consistent_with_quantize::<15>(v);
363 rounded_is_consistent_with_quantize::<16>(v);
364 }
365 }
366
367 #[test]
368 fn quantize_round_matches_reference_values() {
369 assert_eq!(quantize_round::<1>(Q0_16::from_raw(0xFFFF)), 1);
370 assert_eq!(quantize_round::<1>(Q0_16::from_raw(0x8080)), 1);
371 assert_eq!(quantize_round::<1>(Q0_16::from_raw(0x007F)), 0);
372 assert_eq!(quantize_round::<4>(Q0_16::from_raw(0xFFFF)), 15);
373 assert_eq!(quantize_round::<4>(Q0_16::from_raw(0x8080)), 8);
374 assert_eq!(quantize_round::<4>(Q0_16::from_raw(0x007F)), 0);
375 assert_eq!(quantize_round::<8>(Q0_16::from_raw(0xFFFF)), 255);
376 assert_eq!(quantize_round::<8>(Q0_16::from_raw(0x8080)), 129);
377 assert_eq!(quantize_round::<8>(Q0_16::from_raw(0x007F)), 0);
378 assert_eq!(quantize_round::<15>(Q0_16::from_raw(0xFFFF)), 32767);
379 assert_eq!(quantize_round::<15>(Q0_16::from_raw(0x8080)), 16448);
380 assert_eq!(quantize_round::<15>(Q0_16::from_raw(0x007F)), 64);
381 assert_eq!(quantize_round::<16>(Q0_16::from_raw(0xFFFF)), 65535);
382 assert_eq!(quantize_round::<16>(Q0_16::from_raw(0x8080)), 32896);
383 assert_eq!(quantize_round::<16>(Q0_16::from_raw(0x007F)), 127);
384 }
385}