1use crate::store::VecqIndex;
35
36pub(crate) const MAGIC: u32 = u32::from_le_bytes(*b"VECQ");
37const V1: u16 = 1;
38const V1_1: u16 = 257;
39pub const V1_2: u16 = 258;
40pub(crate) const V1_3: u16 = 259;
41pub(crate) const V1_4: u16 = 260;
42pub(crate) const V1_5: u16 = 261;
43
44#[derive(Debug)]
45pub enum Error {
46 NotAStableFile,
47 UnsupportedVersion(u16),
48 Truncated,
49 DimMismatch { expected: usize, got: usize },
50 InvalidWorkingDim { dim: usize, working_dim: usize },
51 InvalidKeyTable,
52 InvalidWidth { width: u8 },
53}
54
55impl std::fmt::Display for Error {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Error::NotAStableFile => write!(f, "not a vecq file"),
59 Error::UnsupportedVersion(v) => write!(f, "unsupported version {v}"),
60 Error::Truncated => write!(f, "file truncated"),
61 Error::DimMismatch { expected, got } => {
62 write!(f, "dim mismatch: expected {expected}, got {got}")
63 }
64 Error::InvalidWorkingDim { dim, working_dim } => {
65 write!(f, "invalid working_dim {working_dim} for dim {dim}")
66 }
67 Error::InvalidKeyTable => write!(f, "invalid keyed-slot table"),
68 Error::InvalidWidth { width } => {
69 write!(f, "invalid code width {width} (supported: 4, 5, 6)")
70 }
71 }
72 }
73}
74
75impl std::error::Error for Error {}
76
77fn f32_to_f16_bits(x: f32) -> u16 {
79 let bits = x.to_bits();
80 let sign = ((bits >> 16) & 0x8000) as u16;
81 let exp = ((bits >> 23) & 0xFF) as i32;
82 let mant = bits & 0x7F_FFFF;
83 if exp == 0xFF {
84 return sign | 0x7C00 | if mant != 0 { 0x0200 } else { 0 };
86 }
87 let unbiased = exp - 127;
88 if unbiased > 15 {
89 return sign | 0x7C00; }
91 if unbiased >= -14 {
92 let half_exp = (unbiased + 15) as u32;
94 let half_mant = mant >> 13;
95 let mut out = sign | ((half_exp << 10) as u16) | half_mant as u16;
96 let round = mant & 0x1FFF;
98 if round > 0x1000 || (round == 0x1000 && (half_mant & 1 == 1)) {
99 out = out.wrapping_add(1);
100 }
101 out
102 } else {
103 let m = mant | 0x80_0000; let shift = (-unbiased - 14 + 13) as u32;
106 if shift >= 32 {
107 return sign;
108 }
109 let half_mant = m >> shift;
110 let mut out = sign | half_mant as u16;
111 let round_bits = m & ((1u32 << shift) - 1);
112 let halfway = 1u32 << (shift - 1);
113 if round_bits > halfway || (round_bits == halfway && (half_mant & 1 == 1)) {
114 out = out.wrapping_add(1);
115 }
116 out
117 }
118}
119
120pub(crate) fn f16_bits_to_f32(h: u16) -> f32 {
122 let sign = ((h & 0x8000) as u32) << 16;
123 let exp = ((h >> 10) & 0x1F) as u32;
124 let mant = (h & 0x03FF) as u32;
125 let bits = if exp == 0 {
126 if mant == 0 {
127 sign
128 } else {
129 let mut e = -1i32;
131 let mut m = mant;
132 while m & 0x0400 == 0 {
133 m <<= 1;
134 e -= 1;
135 }
136 m &= 0x03FF;
137 sign | (((113 + e) as u32) << 23) | (m << 13)
138 }
139 } else if exp == 0x1F {
140 sign | 0x7F80_0000 | (mant << 13)
141 } else {
142 sign | ((exp + 112) << 23) | (mant << 13)
143 };
144 f32::from_bits(bits)
145}
146
147impl VecqIndex {
148 pub fn to_bytes(&self) -> Vec<u8> {
157 let bits = self.bits();
158 let bpv = self.bytes_per_vector();
159 let reserved: u16 = if self.working_dim() == self.dim() {
160 0
161 } else {
162 self.working_dim() as u16
163 };
164 let wide = !self.is_residual() && bits != 4;
167 let version = if self.is_residual() {
168 V1_4
169 } else if wide {
170 V1_5
171 } else {
172 V1_3
173 };
174 let extra = if self.is_residual() {
175 self.live_slots() * (2 + bpv)
176 } else {
177 0
178 };
179 let width_byte = usize::from(wide);
180 let mut out = Vec::with_capacity(
181 24 + width_byte + self.live_slots() * bpv + self.live_slots() * 2 + extra,
182 );
183 out.extend_from_slice(&MAGIC.to_le_bytes());
184 out.extend_from_slice(&version.to_le_bytes());
185 out.extend_from_slice(&reserved.to_le_bytes());
186 out.extend_from_slice(&(self.dim() as u32).to_le_bytes());
187 out.extend_from_slice(&self.seed().to_le_bytes());
188 out.extend_from_slice(&(self.len() as u32).to_le_bytes());
189 if wide {
190 out.push(bits);
191 }
192 for slot in 0..self.slots() {
193 if !self.slot_alive(slot) {
194 continue;
195 }
196 out.extend_from_slice(&f32_to_f16_bits(self.slot_scale(slot)).to_le_bytes());
197 }
198 for slot in 0..self.slots() {
199 if !self.slot_alive(slot) {
200 continue;
201 }
202 out.extend_from_slice(self.slot_codes(slot, bpv));
203 }
204 if self.is_residual() {
205 for slot in 0..self.slots() {
206 if !self.slot_alive(slot) {
207 continue;
208 }
209 out.extend_from_slice(&f32_to_f16_bits(self.slot_scale2(slot)).to_le_bytes());
210 }
211 for slot in 0..self.slots() {
212 if !self.slot_alive(slot) {
213 continue;
214 }
215 out.extend_from_slice(self.slot_codes2(slot, bpv));
216 }
217 }
218 let keyed: Vec<(u32, u64)> = (0..self.slots())
223 .filter(|&s| self.slot_alive(s))
224 .enumerate()
225 .filter_map(|(dense, s)| self.key_of(s).map(|k| (dense as u32, k)))
226 .collect();
227 out.extend_from_slice(&(keyed.len() as u32).to_le_bytes());
228 for (slot, key) in keyed {
229 out.extend_from_slice(&slot.to_le_bytes());
230 out.extend_from_slice(&key.to_le_bytes());
231 }
232 out
233 }
234
235 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
238 let rd_u32 = |b: &[u8]| u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
239 let rd_u16 = |b: &[u8]| u16::from_le_bytes([b[0], b[1]]);
240 if bytes.len() < 24 {
241 return Err(Error::Truncated);
242 }
243 if rd_u32(&bytes[0..4]) != MAGIC {
244 return Err(Error::NotAStableFile);
245 }
246 let version = rd_u16(&bytes[4..6]);
247 if version != V1
248 && version != V1_1
249 && version != V1_2
250 && version != V1_3
251 && version != V1_4
252 && version != V1_5
253 {
254 return Err(Error::UnsupportedVersion(version));
255 }
256 let dim = rd_u32(&bytes[8..12]) as usize;
257 let seed = u64::from_le_bytes(bytes[12..20].try_into().unwrap());
258 let count = rd_u32(&bytes[20..24]) as usize;
259
260 let working_dim = match version {
262 V1_2 | V1_3 | V1_4 | V1_5 => match rd_u16(&bytes[6..8]) as usize {
263 0 => dim,
264 w if w <= dim => w,
265 w => {
266 return Err(Error::InvalidWorkingDim {
267 dim,
268 working_dim: w,
269 })
270 }
271 },
272 _ => dim,
273 };
274
275 let padded = crate::rhdh::padded_dim(working_dim);
276 let (index_bits, mut off) = if version == V1_5 {
279 if bytes.len() < 25 {
280 return Err(Error::Truncated);
281 }
282 let w = bytes[24];
283 if !matches!(w, 4..=6) {
284 return Err(Error::InvalidWidth { width: w });
285 }
286 (w, 25usize)
287 } else {
288 (4u8, 24usize)
289 };
290 let codes_bytes = (padded * index_bits as usize).div_ceil(8);
291 let scale_bytes = if version == V1 { 4 } else { 2 };
292 let expected = off + count * (scale_bytes + codes_bytes);
293 if bytes.len() < expected {
294 return Err(Error::Truncated);
295 }
296
297 let mut scales = Vec::with_capacity(count);
298 for _ in 0..count {
299 let s = if version == V1 {
300 f32::from_le_bytes(bytes[off..off + 4].try_into().unwrap())
301 } else {
302 f16_bits_to_f32(rd_u16(&bytes[off..off + 2]))
303 };
304 scales.push(s);
305 off += scale_bytes;
306 }
307 let code_end = off + count * codes_bytes;
308 let mut index = VecqIndex::with_working_dim(dim, working_dim, seed);
309 index.codes = bytes[off..code_end].to_vec();
310 index.scales = scales;
311 index.n = count;
312 index.init_dense(count);
313 index.bits = index_bits;
314 if version == V1_4 {
315 index.residual = true;
317 if bytes.len() < code_end + count * (2 + codes_bytes) {
318 return Err(Error::Truncated);
319 }
320 let mut off2 = code_end;
321 for _ in 0..count {
322 index
323 .scales2
324 .push(f16_bits_to_f32(rd_u16(&bytes[off2..off2 + 2])));
325 off2 += 2;
326 }
327 index.codes2 = bytes[off2..off2 + count * codes_bytes].to_vec();
328 }
329 if version == V1_3 || version == V1_4 || version == V1_5 {
330 if bytes.len() < code_end + 4 {
332 return Err(Error::Truncated);
333 }
334 let key_base = if version == V1_4 {
335 code_end + count * (2 + codes_bytes)
336 } else {
337 code_end
338 };
339 let entries = rd_u32(&bytes[key_base..key_base + 4]) as usize;
340 let table_end = key_base + 4 + entries * 12;
341 if bytes.len() < table_end {
342 return Err(Error::Truncated);
343 }
344 let mut key_table: Vec<Option<u64>> = vec![None; count];
345 let mut e = key_base + 4;
346 for _ in 0..entries {
347 let slot = rd_u32(&bytes[e..e + 4]) as usize;
348 let key = u64::from_le_bytes(bytes[e + 4..e + 12].try_into().unwrap());
349 e += 12;
350 if slot >= count || key_table[slot].is_some() {
351 return Err(Error::InvalidKeyTable);
352 }
353 key_table[slot] = Some(key);
354 }
355 index.restore_keys(key_table);
356 }
357 Ok(index)
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn unit(dim: usize, salt: u64) -> Vec<f32> {
366 let mut x = salt | 1;
367 let mut v = Vec::with_capacity(dim);
368 for _ in 0..dim {
369 x ^= x << 13;
370 x ^= x >> 7;
371 x ^= x << 17;
372 v.push((x as f32 / u32::MAX as f32 - 0.5) * 2.0);
373 }
374 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
375 v.into_iter().map(|a| a / norm).collect()
376 }
377
378 #[test]
379 fn round_trip_is_identical() {
380 let mut idx = VecqIndex::new(384, 99);
381 for i in 0..50 {
382 idx.add(&unit(384, i * 7 + 1));
383 }
384 let bytes = idx.to_bytes();
385 let back = VecqIndex::from_bytes(&bytes).expect("parse");
386 assert_eq!(back.len(), 50);
387 assert_eq!(back.dim(), 384);
388 assert_eq!(back.seed(), 99);
389 let q = unit(384, 4242);
391 let r1 = back.search(&q, 10);
392 let bytes2 = back.to_bytes();
393 assert_eq!(bytes, bytes2, "re-serialize must be byte-identical");
394 let back2 = VecqIndex::from_bytes(&bytes2).unwrap();
395 assert_eq!(r1, back2.search(&q, 10));
396 let r0 = idx.search(&q, 10);
399 assert_eq!(r0[0].0, r1[0].0);
400 let overlap = r0
401 .iter()
402 .filter(|(i, _)| r1.iter().any(|(j, _)| i == j))
403 .count();
404 assert!(overlap >= 9, "top-10 overlap {overlap}");
405 }
406
407 #[test]
408 fn v11_file_smaller_and_v1_readable() {
409 let mut idx = VecqIndex::new(384, 7);
410 idx.set_bits(4); for i in 0..30 {
412 idx.add(&unit(384, i + 3));
413 }
414 let v11 = idx.to_bytes();
415 let mut v1 = Vec::new();
417 v1.extend_from_slice(&MAGIC.to_le_bytes());
418 v1.extend_from_slice(&V1.to_le_bytes());
419 v1.extend_from_slice(&0u16.to_le_bytes());
420 v1.extend_from_slice(&(384u32).to_le_bytes());
421 v1.extend_from_slice(&7u64.to_le_bytes());
422 v1.extend_from_slice(&(30u32).to_le_bytes());
423 for s in &idx.scales {
424 v1.extend_from_slice(&s.to_le_bytes());
425 }
426 let codes = &v11[24 + 30 * 2..];
428 v1.extend_from_slice(codes);
429 let from_v1 = VecqIndex::from_bytes(&v1).expect("v1 readable");
430 assert_eq!(from_v1.len(), 30);
431 let q = unit(384, 55);
433 assert_eq!(idx.search(&q, 5), from_v1.search(&q, 5));
434 assert_eq!(v1.len() - v11.len(), 30 * 2);
436 }
437
438 #[test]
439 fn deterministic_across_instances() {
440 let mut a = VecqIndex::new(64, 5);
441 let mut b = VecqIndex::new(64, 5);
442 for i in 0..10 {
443 let v = unit(64, i + 100);
444 a.add(&v);
445 b.add(&v);
446 }
447 assert_eq!(a.to_bytes(), b.to_bytes());
448 }
449
450 #[test]
451 fn rejects_garbage() {
452 assert!(matches!(
453 VecqIndex::from_bytes(b"nonsense-not-a-file-at-all"),
454 Err(Error::NotAStableFile)
455 ));
456 assert!(matches!(VecqIndex::from_bytes(&[]), Err(Error::Truncated)));
457 }
458
459 #[test]
460 fn f16_round_trip_accuracy() {
461 for &x in &[0.001f32, 0.03, 0.5, 1.0, 3.7, 100.0] {
463 let back = f16_bits_to_f32(f32_to_f16_bits(x));
464 assert!((back - x).abs() / x < 0.001, "{x} -> {back}");
465 }
466 }
467}