1use crate::codec::META_BYTES;
34use crate::score::Metric;
35use crate::TurboQuantError;
36
37#[inline]
40pub fn bits_len(dim: usize) -> usize {
41 dim.div_ceil(64) * 8
42}
43
44#[inline]
46pub fn code1_len(dim: usize) -> usize {
47 bits_len(dim) + META_BYTES
48}
49
50pub fn encode_bits(rotated: &[f32], alpha: f32) -> Vec<u8> {
54 let dim = rotated.len();
55 let n_words = dim.div_ceil(64);
56 let mut words = vec![0u64; n_words];
57 let mut abs_sum = 0.0f32;
58 let inv = if alpha > 0.0 { 1.0 / alpha } else { 0.0 };
59 for (i, &r) in rotated.iter().enumerate() {
60 let z = r * inv;
61 abs_sum += z.abs();
62 if z >= 0.0 {
63 words[i / 64] |= 1u64 << (i % 64);
64 }
65 }
66 let c = abs_sum / dim as f32;
67 let mut blob = Vec::with_capacity(code1_len(dim));
68 for w in &words {
69 blob.extend_from_slice(&w.to_le_bytes());
70 }
71 blob.extend_from_slice(&alpha.to_le_bytes());
72 blob.extend_from_slice(&c.to_le_bytes());
73 blob
74}
75
76pub struct Bits1Query {
78 planes: [Vec<u64>; 8],
81 plane_pops: [u32; 8],
83 qscale: f32,
85 pub norm_sq: f32,
87 dim: usize,
88}
89
90impl Bits1Query {
91 pub fn new(rotated: &[f32]) -> Result<Self, TurboQuantError> {
94 let dim = rotated.len();
95 if dim < 2 {
96 return Err(TurboQuantError::InvalidDimension(dim));
97 }
98 let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
99 let qmax = rotated.iter().fold(0.0f32, |m, x| m.max(x.abs()));
100 let qscale = if qmax > 0.0 { qmax / 127.0 } else { 0.0 };
101 let inv = if qscale > 0.0 { 1.0 / qscale } else { 0.0 };
102
103 let n_words = dim.div_ceil(64);
104 let mut planes: [Vec<u64>; 8] = std::array::from_fn(|_| vec![0u64; n_words]);
105 let mut plane_pops = [0u32; 8];
106 for (i, &x) in rotated.iter().enumerate() {
107 let q_u8 = ((x * inv).round() as i8 as i16 + 128) as u16 as u8;
108 for (p, plane) in planes.iter_mut().enumerate() {
109 if q_u8 >> p & 1 != 0 {
110 plane[i / 64] |= 1u64 << (i % 64);
111 plane_pops[p] += 1;
112 }
113 }
114 }
115 Ok(Self {
116 planes,
117 plane_pops,
118 qscale,
119 norm_sq,
120 dim,
121 })
122 }
123
124 pub fn distance_to(&self, metric: Metric, blob: &[u8]) -> f32 {
127 let qblob = self.to_blob();
128 query_blob_distance(metric, &qblob, blob, self.dim)
129 }
130
131 pub fn to_blob(&self) -> Vec<u8> {
137 let bl = bits_len(self.dim);
138 let mut out = Vec::with_capacity(query1_len(self.dim));
139 for plane in &self.planes {
140 for w in plane {
141 out.extend_from_slice(&w.to_le_bytes());
142 }
143 }
144 for p in &self.plane_pops {
145 out.extend_from_slice(&p.to_le_bytes());
146 }
147 out.extend_from_slice(&self.qscale.to_le_bytes());
148 out.extend_from_slice(&self.norm_sq.to_le_bytes());
149 debug_assert_eq!(out.len(), 8 * bl + 40);
150 out
151 }
152}
153
154#[inline]
156pub fn query1_len(dim: usize) -> usize {
157 8 * bits_len(dim) + 40
158}
159
160pub fn query_blob_distance(metric: Metric, qblob: &[u8], code: &[u8], dim: usize) -> f32 {
164 let n_words = dim.div_ceil(64);
165 let bl = n_words * 8;
166 assert!(dim >= 2, "1-bit query dimensions must be at least 2");
167 assert_eq!(qblob.len(), 8 * bl + 40, "invalid 1-bit query length");
168 assert_eq!(code.len(), bl + META_BYTES, "invalid 1-bit code length");
169
170 let alpha = f32::from_le_bytes(code[bl..bl + 4].try_into().unwrap());
171 let c = f32::from_le_bytes(code[bl + 4..bl + 8].try_into().unwrap());
172 let qscale = f32::from_le_bytes(qblob[8 * bl + 32..8 * bl + 36].try_into().unwrap());
173 let q_norm_sq = f32::from_le_bytes(qblob[8 * bl + 36..8 * bl + 40].try_into().unwrap());
174
175 let word = |bytes: &[u8], k: usize| -> u64 {
176 u64::from_le_bytes(bytes[k * 8..k * 8 + 8].try_into().unwrap())
177 };
178
179 let mut bits_pop = 0u32;
180 for k in 0..n_words {
181 bits_pop += word(code, k).count_ones();
182 }
183 let mut dot_u8 = 0i64;
184 for p in 0..8 {
185 let plane = &qblob[p * bl..(p + 1) * bl];
186 let pop_p = u32::from_le_bytes(
187 qblob[8 * bl + p * 4..8 * bl + p * 4 + 4]
188 .try_into()
189 .unwrap(),
190 );
191 let mut agree = 0u32;
192 for k in 0..n_words {
193 agree += (word(plane, k) & word(code, k)).count_ones();
194 }
195 dot_u8 += (1i64 << p) * (2 * agree as i64 - pop_p as i64);
196 }
197 let sum_s = 2 * bits_pop as i64 - dim as i64;
198 let dot_i8 = dot_u8 - 128 * sum_s;
199 let dot = qscale * alpha * c * dot_i8 as f32;
200
201 let norm_sq_v = alpha * alpha * dim as f32; match metric {
203 Metric::Euclidean => (q_norm_sq + norm_sq_v - 2.0 * dot).max(0.0).sqrt(),
204 Metric::Cosine => {
205 let denom = (q_norm_sq * norm_sq_v).sqrt();
206 if denom > 0.0 {
207 (1.0 - dot / denom).max(0.0)
208 } else {
209 1.0
210 }
211 }
212 Metric::DotProduct => (-dot).max(0.0),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::codec::Turbo4Codec;
220 use crate::rotation::{Rotation, SplitMix64};
221
222 fn gauss_vec(dim: usize, seed: u64) -> Vec<f32> {
223 let mut rng = SplitMix64(seed);
224 let mut out = Vec::with_capacity(dim);
225 while out.len() < dim {
226 let u1 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
227 let u2 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
228 let r = (-2.0 * u1.max(1e-12).ln()).sqrt();
229 let (s, c) = (2.0 * std::f64::consts::PI * u2).sin_cos();
230 out.push((r * c) as f32);
231 if out.len() < dim {
232 out.push((r * s) as f32);
233 }
234 }
235 out
236 }
237
238 #[test]
240 fn bitplane_dot_matches_naive_sign_sum() {
241 for dim in [64usize, 100, 384, 1536] {
242 let rot = Rotation::new(dim, 42);
243 for seed in 0..4u64 {
244 let v = gauss_vec(dim, seed + 1);
245 let q = gauss_vec(dim, seed + 100);
246 let rv = rot.apply(&v);
247 let rq = rot.apply(&q);
248 let norm_sq: f32 = rv.iter().map(|x| x * x).sum();
249 let alpha = (norm_sq / dim as f32).sqrt();
250 let blob = encode_bits(&rv, alpha);
251 let query = Bits1Query::new(&rq).unwrap();
252
253 let qmax = rq.iter().fold(0.0f32, |m, x| m.max(x.abs()));
255 let qscale = if qmax > 0.0 { qmax / 127.0 } else { 0.0 };
256 let inv_a = if alpha > 0.0 { 1.0 / alpha } else { 0.0 };
257 let mut naive = 0i64;
258 for i in 0..dim {
259 let qi = (rq[i] / qscale).round() as i8 as i64;
260 let s = if rv[i] * inv_a >= 0.0 { 1 } else { -1 };
261 naive += qi * s;
262 }
263 let n_words = dim.div_ceil(64);
264 let alpha_read =
265 f32::from_le_bytes(blob[n_words * 8..n_words * 8 + 4].try_into().unwrap());
266 let c =
267 f32::from_le_bytes(blob[n_words * 8 + 4..n_words * 8 + 8].try_into().unwrap());
268 let expected_dot = qscale * alpha_read * c * naive as f32;
269 let d = query.distance_to(Metric::Euclidean, &blob);
271 let norm_sq_v = alpha_read * alpha_read * dim as f32;
272 let kernel_dot = (query.norm_sq + norm_sq_v - d * d) / 2.0;
273 assert!(
274 (kernel_dot - expected_dot).abs() <= 1e-2 * expected_dot.abs().max(1.0),
275 "dim {dim} seed {seed}: kernel dot {kernel_dot} vs naive {expected_dot}"
276 );
277 }
278 }
279 }
280
281 #[test]
284 fn candidate_generation_recall_with_oversampling() {
285 let dim = 128;
286 let n = 300;
287 let codec = Turbo4Codec::new(dim, 42).unwrap();
288 let rot = Rotation::new(dim, 42);
289 let base: Vec<Vec<f32>> = (0..n as u64).map(|i| gauss_vec(dim, 500 + i)).collect();
290 let blobs: Vec<Vec<u8>> = base
291 .iter()
292 .map(|v| {
293 let rv = rot.apply(v);
294 let norm_sq: f32 = rv.iter().map(|x| x * x).sum();
295 encode_bits(&rv, (norm_sq / dim as f32).sqrt())
296 })
297 .collect();
298 let t4codes: Vec<Vec<u8>> = base.iter().map(|v| codec.encode(v).unwrap()).collect();
299
300 let mut total_hits = 0usize;
301 for qs in 0..10u64 {
302 let q = gauss_vec(dim, 9000 + qs);
303 let rq = rot.apply(&q);
304 let bq = Bits1Query::new(&rq).unwrap();
305 let tq = codec.encode_query(&q).unwrap();
306
307 let l2 =
308 |a: &[f32], b: &[f32]| a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f32>();
309 let mut truth: Vec<(usize, f32)> = base
310 .iter()
311 .enumerate()
312 .map(|(i, v)| (i, l2(&q, v)))
313 .collect();
314 truth.sort_by(|a, b| a.1.total_cmp(&b.1));
315 let top10: std::collections::HashSet<usize> =
316 truth[..10].iter().map(|(i, _)| *i).collect();
317
318 let mut stage1: Vec<(usize, f32)> = blobs
320 .iter()
321 .enumerate()
322 .map(|(i, b)| (i, bq.distance_to(Metric::Euclidean, b)))
323 .collect();
324 stage1.sort_by(|a, b| a.1.total_cmp(&b.1));
325 let mut stage2: Vec<(usize, f32)> = stage1[..40]
327 .iter()
328 .map(|&(i, _)| {
329 (
330 i,
331 crate::score::rescore(Metric::Euclidean, &tq, &t4codes[i], dim),
332 )
333 })
334 .collect();
335 stage2.sort_by(|a, b| a.1.total_cmp(&b.1));
336 total_hits += stage2[..10]
337 .iter()
338 .filter(|(i, _)| top10.contains(i))
339 .count();
340 }
341 let recall = total_hits as f32 / 100.0;
342 assert!(
343 recall >= 0.70,
344 "1-bit cascade recall@10 {recall} below floor on Gaussian worst case"
345 );
346 }
347
348 #[test]
349 fn zero_vector_is_safe() {
350 let dim = 64;
351 let blob = encode_bits(&vec![0.0; dim], 0.0);
352 let q = Bits1Query::new(&vec![0.0; dim]).unwrap();
353 let d = q.distance_to(Metric::Euclidean, &blob);
354 assert_eq!(d, 0.0);
355 assert_eq!(q.distance_to(Metric::Cosine, &blob), 1.0);
356 }
357
358 #[test]
359 fn blob_length_is_word_padded() {
360 assert_eq!(code1_len(64), 8 + 8);
361 assert_eq!(code1_len(100), 16 + 8);
362 assert_eq!(code1_len(1536), 192 + 8);
363 }
364}