1use crate::store::VecqIndex;
19
20const MAGIC: u32 = u32::from_le_bytes(*b"VECQ");
21const V1: u16 = 1;
22const V1_1: u16 = 257;
23
24#[derive(Debug)]
25pub enum Error {
26 NotAStableFile,
27 UnsupportedVersion(u16),
28 Truncated,
29 DimMismatch { expected: usize, got: usize },
30}
31
32impl std::fmt::Display for Error {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Error::NotAStableFile => write!(f, "not a vecq file"),
36 Error::UnsupportedVersion(v) => write!(f, "unsupported version {v}"),
37 Error::Truncated => write!(f, "file truncated"),
38 Error::DimMismatch { expected, got } => {
39 write!(f, "dim mismatch: expected {expected}, got {got}")
40 }
41 }
42 }
43}
44
45impl std::error::Error for Error {}
46
47fn f32_to_f16_bits(x: f32) -> u16 {
49 let bits = x.to_bits();
50 let sign = ((bits >> 16) & 0x8000) as u16;
51 let exp = ((bits >> 23) & 0xFF) as i32;
52 let mant = bits & 0x7F_FFFF;
53 if exp == 0xFF {
54 return sign | 0x7C00 | if mant != 0 { 0x0200 } else { 0 };
56 }
57 let unbiased = exp - 127;
58 if unbiased > 15 {
59 return sign | 0x7C00; }
61 if unbiased >= -14 {
62 let half_exp = (unbiased + 15) as u32;
64 let half_mant = mant >> 13;
65 let mut out = sign | ((half_exp << 10) as u16) | half_mant as u16;
66 let round = mant & 0x1FFF;
68 if round > 0x1000 || (round == 0x1000 && (half_mant & 1 == 1)) {
69 out = out.wrapping_add(1);
70 }
71 out
72 } else {
73 let m = mant | 0x80_0000; let shift = (-unbiased - 14 + 13) as u32;
76 if shift >= 32 {
77 return sign;
78 }
79 let half_mant = m >> shift;
80 let mut out = sign | half_mant as u16;
81 let round_bits = m & ((1u32 << shift) - 1);
82 let halfway = 1u32 << (shift - 1);
83 if round_bits > halfway || (round_bits == halfway && (half_mant & 1 == 1)) {
84 out = out.wrapping_add(1);
85 }
86 out
87 }
88}
89
90fn f16_bits_to_f32(h: u16) -> f32 {
92 let sign = ((h & 0x8000) as u32) << 16;
93 let exp = ((h >> 10) & 0x1F) as u32;
94 let mant = (h & 0x03FF) as u32;
95 let bits = if exp == 0 {
96 if mant == 0 {
97 sign
98 } else {
99 let mut e = -1i32;
101 let mut m = mant;
102 while m & 0x0400 == 0 {
103 m <<= 1;
104 e -= 1;
105 }
106 m &= 0x03FF;
107 sign | (((113 + e) as u32) << 23) | (m << 13)
108 }
109 } else if exp == 0x1F {
110 sign | 0x7F80_0000 | (mant << 13)
111 } else {
112 sign | ((exp + 112) << 23) | (mant << 13)
113 };
114 f32::from_bits(bits)
115}
116
117impl VecqIndex {
118 pub fn to_bytes(&self) -> Vec<u8> {
120 let mut out = Vec::with_capacity(24 + self.codes.len() + self.scales.len() * 2);
121 out.extend_from_slice(&MAGIC.to_le_bytes());
122 out.extend_from_slice(&V1_1.to_le_bytes());
123 out.extend_from_slice(&0u16.to_le_bytes());
124 out.extend_from_slice(&(self.dim as u32).to_le_bytes());
125 out.extend_from_slice(&self.seed.to_le_bytes());
126 out.extend_from_slice(&(self.n as u32).to_le_bytes());
127 for s in &self.scales {
128 out.extend_from_slice(&f32_to_f16_bits(*s).to_le_bytes());
129 }
130 out.extend_from_slice(&self.codes);
131 out
132 }
133
134 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
136 let rd_u32 = |b: &[u8]| u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
137 let rd_u16 = |b: &[u8]| u16::from_le_bytes([b[0], b[1]]);
138 if bytes.len() < 24 {
139 return Err(Error::Truncated);
140 }
141 if rd_u32(&bytes[0..4]) != MAGIC {
142 return Err(Error::NotAStableFile);
143 }
144 let version = rd_u16(&bytes[4..6]);
145 if version != V1 && version != V1_1 {
146 return Err(Error::UnsupportedVersion(version));
147 }
148 let dim = rd_u32(&bytes[8..12]) as usize;
149 let seed = u64::from_le_bytes(bytes[12..20].try_into().unwrap());
150 let count = rd_u32(&bytes[20..24]) as usize;
151
152 let padded = crate::rhdh::padded_dim(dim);
153 let codes_bytes = padded / 2;
154 let scale_bytes = if version == V1 { 4 } else { 2 };
155 let expected = 24 + count * (scale_bytes + codes_bytes);
156 if bytes.len() < expected {
157 return Err(Error::Truncated);
158 }
159
160 let mut scales = Vec::with_capacity(count);
161 let mut off = 24;
162 for _ in 0..count {
163 let s = if version == V1 {
164 f32::from_le_bytes(bytes[off..off + 4].try_into().unwrap())
165 } else {
166 f16_bits_to_f32(rd_u16(&bytes[off..off + 2]))
167 };
168 scales.push(s);
169 off += scale_bytes;
170 }
171 let mut index = VecqIndex::new(dim, seed);
172 index.codes = bytes[off..off + count * codes_bytes].to_vec();
173 index.scales = scales;
174 index.n = count;
175 Ok(index)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn unit(dim: usize, salt: u64) -> Vec<f32> {
184 let mut x = salt | 1;
185 let mut v = Vec::with_capacity(dim);
186 for _ in 0..dim {
187 x ^= x << 13;
188 x ^= x >> 7;
189 x ^= x << 17;
190 v.push((x as f32 / u32::MAX as f32 - 0.5) * 2.0);
191 }
192 let norm: f32 = v.iter().map(|a| a * a).sum::<f32>().sqrt();
193 v.into_iter().map(|a| a / norm).collect()
194 }
195
196 #[test]
197 fn round_trip_is_identical() {
198 let mut idx = VecqIndex::new(384, 99);
199 for i in 0..50 {
200 idx.add(&unit(384, i * 7 + 1));
201 }
202 let bytes = idx.to_bytes();
203 let back = VecqIndex::from_bytes(&bytes).expect("parse");
204 assert_eq!(back.len(), 50);
205 assert_eq!(back.dim(), 384);
206 assert_eq!(back.seed(), 99);
207 let q = unit(384, 4242);
209 let r1 = back.search(&q, 10);
210 let bytes2 = back.to_bytes();
211 assert_eq!(bytes, bytes2, "re-serialize must be byte-identical");
212 let back2 = VecqIndex::from_bytes(&bytes2).unwrap();
213 assert_eq!(r1, back2.search(&q, 10));
214 let r0 = idx.search(&q, 10);
217 assert_eq!(r0[0].0, r1[0].0);
218 let overlap = r0
219 .iter()
220 .filter(|(i, _)| r1.iter().any(|(j, _)| i == j))
221 .count();
222 assert!(overlap >= 9, "top-10 overlap {overlap}");
223 }
224
225 #[test]
226 fn v11_file_smaller_and_v1_readable() {
227 let mut idx = VecqIndex::new(384, 7);
228 for i in 0..30 {
229 idx.add(&unit(384, i + 3));
230 }
231 let v11 = idx.to_bytes();
232 let mut v1 = Vec::new();
234 v1.extend_from_slice(&MAGIC.to_le_bytes());
235 v1.extend_from_slice(&V1.to_le_bytes());
236 v1.extend_from_slice(&0u16.to_le_bytes());
237 v1.extend_from_slice(&(384u32).to_le_bytes());
238 v1.extend_from_slice(&7u64.to_le_bytes());
239 v1.extend_from_slice(&(30u32).to_le_bytes());
240 for s in &idx.scales {
241 v1.extend_from_slice(&s.to_le_bytes());
242 }
243 let codes = &v11[24 + 30 * 2..];
245 v1.extend_from_slice(codes);
246 let from_v1 = VecqIndex::from_bytes(&v1).expect("v1 readable");
247 assert_eq!(from_v1.len(), 30);
248 let q = unit(384, 55);
250 assert_eq!(idx.search(&q, 5), from_v1.search(&q, 5));
251 assert_eq!(v1.len() - v11.len(), 30 * 2);
253 }
254
255 #[test]
256 fn deterministic_across_instances() {
257 let mut a = VecqIndex::new(64, 5);
258 let mut b = VecqIndex::new(64, 5);
259 for i in 0..10 {
260 let v = unit(64, i + 100);
261 a.add(&v);
262 b.add(&v);
263 }
264 assert_eq!(a.to_bytes(), b.to_bytes());
265 }
266
267 #[test]
268 fn rejects_garbage() {
269 assert!(matches!(
270 VecqIndex::from_bytes(b"nonsense-not-a-file-at-all"),
271 Err(Error::NotAStableFile)
272 ));
273 assert!(matches!(VecqIndex::from_bytes(&[]), Err(Error::Truncated)));
274 }
275
276 #[test]
277 fn f16_round_trip_accuracy() {
278 for &x in &[0.001f32, 0.03, 0.5, 1.0, 3.7, 100.0] {
280 let back = f16_bits_to_f32(f32_to_f16_bits(x));
281 assert!((back - x).abs() / x < 0.001, "{x} -> {back}");
282 }
283 }
284}