1use crate::rotation::Rotation;
33use crate::tables::{level, quantize_coord, LEVELS_F32};
34use crate::TurboQuantError;
35
36pub const META_BYTES: usize = 8;
38
39pub struct Turbo4Query {
42 pub blob: Vec<u8>,
44 pub rotated: Vec<f32>,
46 pub norm_sq: f32,
48}
49
50pub struct Turbo4Codec {
52 dim: usize,
53 rotation: Rotation,
54}
55
56impl Turbo4Codec {
57 pub fn new(dim: usize, rotation_seed: u64) -> Result<Self, TurboQuantError> {
60 if dim < 2 || dim % 2 != 0 {
61 return Err(TurboQuantError::InvalidDimension(dim));
62 }
63 Ok(Self {
64 dim,
65 rotation: Rotation::new(dim, rotation_seed),
66 })
67 }
68
69 #[inline]
70 pub fn dim(&self) -> usize {
71 self.dim
72 }
73
74 #[inline]
76 pub fn code_len(&self) -> usize {
77 self.dim / 2 + META_BYTES
78 }
79
80 #[inline]
82 pub fn query_len(&self) -> usize {
83 self.dim + META_BYTES
84 }
85
86 pub fn encode(&self, v: &[f32]) -> Result<Vec<u8>, TurboQuantError> {
88 if v.len() != self.dim {
89 return Err(TurboQuantError::DimensionMismatch {
90 expected: self.dim,
91 actual: v.len(),
92 });
93 }
94 let rotated = self.rotation.apply(v);
95 let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
96 let alpha = (norm_sq / self.dim as f32).sqrt();
97 Ok(self.encode_rotated(&rotated, alpha))
98 }
99
100 pub fn encode_dual(&self, v: &[f32]) -> Result<(Vec<u8>, Vec<u8>), TurboQuantError> {
104 if v.len() != self.dim {
105 return Err(TurboQuantError::DimensionMismatch {
106 expected: self.dim,
107 actual: v.len(),
108 });
109 }
110 let rotated = self.rotation.apply(v);
111 let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
112 let alpha = (norm_sq / self.dim as f32).sqrt();
113 let bits = crate::bits1::encode_bits(&rotated, alpha);
114 let turbo4 = self.encode_rotated(&rotated, alpha);
115 Ok((turbo4, bits))
116 }
117
118 fn encode_rotated(&self, rotated: &[f32], alpha: f32) -> Vec<u8> {
121 let inv = if alpha > 0.0 { 1.0 / alpha } else { 0.0 };
122 let half = self.dim / 2;
123 let mut blob = vec![0u8; self.code_len()];
124 let mut s = 0.0f32;
125 for i in 0..half {
126 let c_lo = quantize_coord(rotated[i] * inv);
127 let c_hi = quantize_coord(rotated[i + half] * inv);
128 s += level(c_lo) * level(c_lo) + level(c_hi) * level(c_hi);
129 blob[i] = c_lo | (c_hi << 4);
130 }
131 blob[half..half + 4].copy_from_slice(&alpha.to_le_bytes());
132 blob[half + 4..half + 8].copy_from_slice(&s.to_le_bytes());
133 blob
134 }
135
136 pub fn encode_query(&self, q: &[f32]) -> Result<Turbo4Query, TurboQuantError> {
138 if q.len() != self.dim {
139 return Err(TurboQuantError::DimensionMismatch {
140 expected: self.dim,
141 actual: q.len(),
142 });
143 }
144 let rotated = self.rotation.apply(q);
145 let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
146 let qmax = rotated.iter().fold(0.0f32, |m, x| m.max(x.abs()));
147 let qscale = if qmax > 0.0 { qmax / 127.0 } else { 0.0 };
148 let inv = if qscale > 0.0 { 1.0 / qscale } else { 0.0 };
149
150 let mut blob = vec![0u8; self.query_len()];
151 for (i, &x) in rotated.iter().enumerate() {
152 blob[i] = ((x * inv).round() as i8) as u8;
153 }
154 blob[self.dim..self.dim + 4].copy_from_slice(&qscale.to_le_bytes());
155 blob[self.dim + 4..self.dim + 8].copy_from_slice(&norm_sq.to_le_bytes());
156 Ok(Turbo4Query {
157 blob,
158 rotated,
159 norm_sq,
160 })
161 }
162
163 pub fn decode_rotated(&self, blob: &[u8]) -> Vec<f32> {
166 let (nibbles, alpha, _) = split_code(blob, self.dim);
167 let half = self.dim / 2;
168 let mut out = vec![0.0f32; self.dim];
169 for i in 0..half {
170 out[i] = LEVELS_F32[(nibbles[i] & 0x0F) as usize] * alpha;
171 out[i + half] = LEVELS_F32[(nibbles[i] >> 4) as usize] * alpha;
172 }
173 out
174 }
175
176 pub fn decode(&self, blob: &[u8]) -> Vec<f32> {
178 self.rotation.apply_inverse(&self.decode_rotated(blob))
179 }
180}
181
182#[inline]
185pub fn split_code(blob: &[u8], dim: usize) -> (&[u8], f32, f32) {
186 let half = dim / 2;
187 assert_eq!(dim % 2, 0, "Turbo4 dimensions must be even");
188 assert_eq!(blob.len(), half + META_BYTES, "invalid Turbo4 code length");
189 let alpha = f32::from_le_bytes(blob[half..half + 4].try_into().unwrap());
190 let s = f32::from_le_bytes(blob[half + 4..half + 8].try_into().unwrap());
191 (&blob[..half], alpha, s)
192}
193
194#[inline]
196pub fn split_query(blob: &[u8], dim: usize) -> (&[u8], f32, f32) {
197 assert_eq!(dim % 2, 0, "Turbo4 dimensions must be even");
198 assert_eq!(blob.len(), dim + META_BYTES, "invalid Turbo4 query length");
199 let qscale = f32::from_le_bytes(blob[dim..dim + 4].try_into().unwrap());
200 let norm_sq = f32::from_le_bytes(blob[dim + 4..dim + 8].try_into().unwrap());
201 (&blob[..dim], qscale, norm_sq)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::rotation::SplitMix64;
208
209 fn gauss_vec(dim: usize, seed: u64) -> Vec<f32> {
210 let mut rng = SplitMix64(seed);
211 let mut out = Vec::with_capacity(dim);
212 while out.len() < dim {
213 let u1 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
214 let u2 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
215 let r = (-2.0 * u1.max(1e-12).ln()).sqrt();
216 let (s, c) = (2.0 * std::f64::consts::PI * u2).sin_cos();
217 out.push((r * c) as f32);
218 if out.len() < dim {
219 out.push((r * s) as f32);
220 }
221 }
222 out
223 }
224
225 #[test]
226 fn code_len_is_8x_compression() {
227 let codec = Turbo4Codec::new(1536, 42).unwrap();
228 assert_eq!(codec.code_len(), 768 + 8);
229 assert!(1536.0 * 4.0 / codec.code_len() as f32 > 7.5);
231 }
232
233 #[test]
234 fn rejects_odd_or_tiny_dims() {
235 assert!(Turbo4Codec::new(3, 42).is_err());
236 assert!(Turbo4Codec::new(0, 42).is_err());
237 assert!(Turbo4Codec::new(128, 42).is_ok());
238 }
239
240 #[test]
241 fn roundtrip_error_is_bounded() {
242 let dim = 256;
243 let codec = Turbo4Codec::new(dim, 42).unwrap();
244 let v = gauss_vec(dim, 3);
245 let blob = codec.encode(&v).unwrap();
246 let back = codec.decode(&blob);
247 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
250 let err_sq: f32 = v.iter().zip(&back).map(|(a, b)| (a - b) * (a - b)).sum();
251 assert!(
252 err_sq / norm_sq < 0.05,
253 "relative sq error {}",
254 err_sq / norm_sq
255 );
256 }
257
258 #[test]
259 fn zero_vector_is_safe() {
260 let codec = Turbo4Codec::new(64, 42).unwrap();
261 let blob = codec.encode(&vec![0.0; 64]).unwrap();
262 let (_, alpha, _) = split_code(&blob, 64);
263 assert_eq!(alpha, 0.0);
264 assert!(codec.decode(&blob).iter().all(|&x| x == 0.0));
265 let q = codec.encode_query(&vec![0.0; 64]).unwrap();
266 assert_eq!(q.norm_sq, 0.0);
267 }
268
269 #[test]
270 fn blob_lengths_are_disjoint() {
271 for dim in [2usize, 64, 384, 1536] {
272 let codec = Turbo4Codec::new(dim, 1).unwrap();
273 assert_ne!(codec.code_len(), codec.query_len());
274 }
275 }
276
277 #[test]
278 fn encode_dual_matches_single_encoders() {
279 let dim = 128;
280 let codec = Turbo4Codec::new(dim, 42).unwrap();
281 let v = gauss_vec(dim, 17);
282 let (t4, bits) = codec.encode_dual(&v).unwrap();
283 assert_eq!(t4, codec.encode(&v).unwrap());
284 assert_eq!(bits.len(), crate::bits1::code1_len(dim));
285 }
286
287 #[test]
288 fn encoding_is_deterministic() {
289 let dim = 384;
290 let v = gauss_vec(dim, 9);
291 let c1 = Turbo4Codec::new(dim, 42).unwrap();
292 let c2 = Turbo4Codec::new(dim, 42).unwrap();
293 assert_eq!(c1.encode(&v).unwrap(), c2.encode(&v).unwrap());
294 }
295}