Skip to main content

vecq_core/
lloyd.rs

1//! Lloyd-Max optimal scalar quantizer tables for N(0,1), precomputed offline.
2//!
3//! Centroids and decision boundaries are computed via Lloyd's algorithm
4//! (2000 iterations, convergence tolerance 1e-12) on the standard normal
5//! distribution, then embedded as constants. No runtime training pass.
6//!
7//! 4-bit: 16 centroids / 15 boundaries. 2-bit: 4 centroids / 3 boundaries.
8
9/// 4-bit Lloyd-Max centroids for N(0,1).
10pub const CENTROIDS_4BIT: [f32; 16] = [
11    -1.996_112, -1.512_225, -1.172_563, -0.887_568, -0.632_509, -0.395_530, -0.169_891, 0.049_892,
12    0.269_673, 0.495_312, 0.732_291, 0.987_350, 1.272_345, 1.612_007, 2.095_894, 2.724_265,
13];
14
15/// 4-bit decision boundaries (index i separates centroid i-1 and i).
16pub const BOUNDARIES_4BIT: [f32; 15] = [
17    -1.751_289, -1.340_438, -1.026_996, -0.756_128, -0.509_062, -0.276_322, -0.056_279, 0.164_289,
18    0.391_029, 0.626_095, 0.868_960, 1.125_026, 1.413_956, 1.764_827, 2.238_297,
19];
20
21/// Quantize one N(0,1)-distributed value to a 4-bit index via binary search
22/// over the decision boundaries.
23#[inline]
24pub fn quantize_4bit(x: f32) -> u8 {
25    let mut lo = 0usize;
26    let mut hi = BOUNDARIES_4BIT.len(); // 15
27    while lo < hi {
28        let mid = (lo + hi) / 2;
29        if x > BOUNDARIES_4BIT[mid] {
30            lo = mid + 1;
31        } else {
32            hi = mid;
33        }
34    }
35    lo as u8
36}
37
38/// Dequantize a 4-bit index back to its centroid value.
39#[inline]
40pub fn dequantize_4bit(i: u8) -> f32 {
41    CENTROIDS_4BIT[i as usize]
42}
43
44/// 5-bit Lloyd-Max centroids for N(0,1).
45pub const CENTROIDS_5BIT: [f32; 32] = [
46    -3.2641168,
47    -2.6949832,
48    -2.321901,
49    -2.0330853,
50    -1.7916883,
51    -1.5806495,
52    -1.3905898,
53    -1.2158026,
54    -1.0524858,
55    -0.89793956,
56    -0.7501299,
57    -0.6074978,
58    -0.4687885,
59    -0.33291993,
60    -0.19900484,
61    -0.06620281,
62    0.06625264,
63    0.19905461,
64    0.33296943,
65    0.46883777,
66    0.60757166,
67    0.75022924,
68    0.8980622,
69    1.052632,
70    1.2159506,
71    1.3907394,
72    1.5808226,
73    1.7919079,
74    2.0333521,
75    2.3222392,
76    2.6954327,
77    3.2648566,
78];
79
80/// 5-bit decision boundaries.
81pub const BOUNDARIES_5BIT: [f32; 31] = [
82    -2.9795978,
83    -2.5084915,
84    -2.177543,
85    -1.9124366,
86    -1.6862187,
87    -1.4856696,
88    -1.3032461,
89    -1.1341941,
90    -0.97524965,
91    -0.8240596,
92    -0.67883897,
93    -0.5381553,
94    -0.40085423,
95    -0.2659624,
96    -0.13260382,
97    0.000024914742,
98    0.13265362,
99    0.266012,
100    0.40090358,
101    0.5382047,
102    0.6789125,
103    0.82418275,
104    0.975397,
105    1.1343412,
106    1.3033949,
107    1.4858308,
108    1.6864152,
109    1.9126797,
110    2.177845,
111    2.508878,
112    2.980162,
113];
114
115/// 6-bit Lloyd-Max centroids for N(0,1).
116pub const CENTROIDS_6BIT: [f32; 64] = [
117    -3.8273482,
118    -3.333197,
119    -3.0166795,
120    -2.7765143,
121    -2.5793982,
122    -2.41001,
123    -2.260009,
124    -2.124298,
125    -1.9995066,
126    -1.8833042,
127    -1.7739713,
128    -1.6702707,
129    -1.571215,
130    -1.4760238,
131    -1.3840784,
132    -1.2948383,
133    -1.207906,
134    -1.1230142,
135    -1.0398592,
136    -0.9581757,
137    -0.87773234,
138    -0.7983605,
139    -0.7199343,
140    -0.6422799,
141    -0.5652983,
142    -0.48888877,
143    -0.41292742,
144    -0.33736518,
145    -0.26210162,
146    -0.1870626,
147    -0.11217268,
148    -0.037357662,
149    0.037407417,
150    0.11222233,
151    0.1871122,
152    0.26215115,
153    0.33741492,
154    0.412977,
155    0.48893812,
156    0.5653473,
157    0.64232975,
158    0.71998423,
159    0.7984106,
160    0.8777821,
161    0.9582232,
162    1.0399102,
163    1.1230648,
164    1.2079563,
165    1.2948877,
166    1.384128,
167    1.4760972,
168    1.5713139,
169    1.6703697,
170    1.7740716,
171    1.8834057,
172    1.9996046,
173    2.1243992,
174    2.2601078,
175    2.4101071,
176    2.5794992,
177    2.7766082,
178    3.016757,
179    3.3332598,
180    3.827421,
181];
182
183/// 6-bit decision boundaries.
184pub const BOUNDARIES_6BIT: [f32; 63] = [
185    -3.580496,
186    -3.1751587,
187    -2.8968368,
188    -2.6782057,
189    -2.4949536,
190    -2.335259,
191    -2.192403,
192    -2.0621655,
193    -1.9416802,
194    -1.8289125,
195    -1.7223959,
196    -1.6210177,
197    -1.5238943,
198    -1.4303128,
199    -1.3397081,
200    -1.2516222,
201    -1.1656973,
202    -1.0816617,
203    -0.9992423,
204    -0.91816604,
205    -0.8382337,
206    -0.75930965,
207    -0.6812571,
208    -0.6039391,
209    -0.5272308,
210    -0.4510203,
211    -0.37523368,
212    -0.29980838,
213    -0.22464456,
214    -0.14965503,
215    -0.07479018,
216    0.000024879351,
217    0.07483988,
218    0.14970465,
219    0.2246941,
220    0.29985797,
221    0.37528333,
222    0.4510699,
223    0.52728003,
224    0.6039884,
225    0.68130684,
226    0.75935954,
227    0.8382834,
228    0.9182147,
229    0.9992916,
230    1.0817122,
231    1.1657475,
232    1.2516719,
233    1.3397577,
234    1.4303625,
235    1.5239555,
236    1.6211035,
237    1.7224953,
238    1.8290132,
239    1.9417915,
240    2.06229,
241    2.1925168,
242    2.3353572,
243    2.4950526,
244    2.678303,
245    2.8969312,
246    3.1752489,
247    3.580552,
248];
249
250/// Number of codebook levels for a supported bit width (4 -> 16, 5 -> 32, 6 -> 64).
251#[inline]
252pub fn levels(bits: u8) -> usize {
253    match bits {
254        4 => 16,
255        5 => 32,
256        6 => 64,
257        w => panic!("unsupported bit width {w} (supported: 4, 5, 6)"),
258    }
259}
260
261/// Codebook centroids for a bit width (alias of the per-width constants).
262#[inline]
263pub fn centroids(bits: u8) -> &'static [f32] {
264    match bits {
265        4 => &CENTROIDS_4BIT,
266        5 => &CENTROIDS_5BIT,
267        6 => &CENTROIDS_6BIT,
268        w => panic!("unsupported bit width {w} (supported: 4, 5, 6)"),
269    }
270}
271
272/// Decision boundaries for a bit width.
273#[inline]
274pub fn boundaries(bits: u8) -> &'static [f32] {
275    match bits {
276        4 => &BOUNDARIES_4BIT,
277        5 => &BOUNDARIES_5BIT,
278        6 => &BOUNDARIES_6BIT,
279        w => panic!("unsupported bit width {w} (supported: 4, 5, 6)"),
280    }
281}
282
283/// Quantize one N(0,1) value at the given bit width via binary search.
284#[inline]
285pub fn quantize(x: f32, bits: u8) -> u8 {
286    let bnd = boundaries(bits);
287    let mut lo = 0usize;
288    let mut hi = bnd.len();
289    while lo < hi {
290        let mid = (lo + hi) / 2;
291        if x > bnd[mid] {
292            lo = mid + 1;
293        } else {
294            hi = mid;
295        }
296    }
297    lo as u8
298}
299
300/// Dequantize a code at the given bit width (direct centroid index).
301#[inline]
302pub fn dequantize(code: u8, bits: u8) -> f32 {
303    centroids(bits)[code as usize]
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn centroids_are_sorted() {
312        for w in CENTROIDS_4BIT.windows(2) {
313            assert!(w[0] < w[1], "centroids must be strictly increasing");
314        }
315    }
316
317    #[test]
318    fn boundaries_are_sorted_and_interleaved() {
319        for w in BOUNDARIES_4BIT.windows(2) {
320            assert!(w[0] < w[1]);
321        }
322        // boundaries sit between adjacent centroids
323        for i in 0..15 {
324            assert!(CENTROIDS_4BIT[i] < BOUNDARIES_4BIT[i]);
325            assert!(BOUNDARIES_4BIT[i] < CENTROIDS_4BIT[i + 1]);
326        }
327    }
328
329    #[test]
330    fn extreme_values_map_to_endpoints() {
331        assert_eq!(quantize_4bit(-10.0), 0);
332        assert_eq!(quantize_4bit(10.0), 15);
333    }
334
335    #[test]
336    fn zero_maps_near_center() {
337        let i = quantize_4bit(0.0);
338        assert!(
339            (7..=8).contains(&i),
340            "0.0 should map to centroid 7 or 8, got {i}"
341        );
342    }
343
344    #[test]
345    fn round_trip_error_is_bounded() {
346        // Worst-case quantization error for N(0,1) Lloyd-Max 4-bit is half
347        // the largest inter-centroid gap in the tails.
348        let mut max_gap = 0f32;
349        for i in 0..15 {
350            max_gap = max_gap.max(CENTROIDS_4BIT[i + 1] - CENTROIDS_4BIT[i]);
351        }
352        // Lloyd-Max MSE for 4-bit on N(0,1) is ~0.0115; per-dim worst error
353        // must be below the max tail gap.
354        assert!(max_gap < 0.75, "max centroid gap {max_gap} too large");
355    }
356}