1#[cfg(target_arch = "aarch64")]
19use std::arch::aarch64::*;
20
21use super::simd_config;
22
23#[derive(Debug, Clone)]
27pub struct BinaryVector {
28 pub data: Vec<u8>,
30 pub dims: usize,
32 pub norm: f32,
34}
35
36impl BinaryVector {
37 pub fn from_f32(vector: &[f32]) -> Self {
42 Self::from_f32_with_threshold(vector, 0.0)
43 }
44
45 pub fn from_f32_with_threshold(vector: &[f32], threshold: f32) -> Self {
47 let dims = vector.len();
48
49 let mut norm_sq = 0.0f32;
51 for &v in vector {
52 if v.is_finite() {
53 norm_sq += v * v;
54 }
55 }
56 let norm = norm_sq.sqrt();
57
58 let packed_len = dims.div_ceil(8);
59 let mut data = vec![0u8; packed_len];
60
61 for (i, &v) in vector.iter().enumerate() {
62 let val = if v.is_finite() { v } else { 0.0 };
63 if val >= threshold {
64 let byte_idx = i / 8;
65 let bit_idx = 7 - (i % 8); data[byte_idx] |= 1 << bit_idx;
67 }
68 }
69
70 Self { data, dims, norm }
71 }
72
73 pub fn to_f32(&self) -> Vec<f32> {
80 let required_bytes = self.dims.div_ceil(8);
81 if self.data.len() < required_bytes {
82 return Vec::new();
83 }
84 let mut result = Vec::with_capacity(self.dims);
85 for i in 0..self.dims {
86 let byte_idx = i / 8;
87 let bit_idx = 7 - (i % 8);
88 let bit = (self.data[byte_idx] >> bit_idx) & 1;
89 result.push(if bit == 1 { 1.0 } else { -1.0 });
90 }
91 result
92 }
93
94 #[inline]
98 pub fn hamming_distance(&self, other: &BinaryVector) -> u32 {
99 hamming_distance_binary(self, other)
100 }
101
102 #[inline]
108 pub fn cosine_distance_approx(&self, other: &BinaryVector) -> f32 {
109 if self.dims == 0 {
110 return 0.0;
111 }
112 let hamming = self.hamming_distance(other) as f32;
113 2.0 * hamming / self.dims as f32
114 }
115
116 #[inline]
118 pub fn cosine_similarity_approx(&self, other: &BinaryVector) -> f32 {
119 1.0 - self.cosine_distance_approx(other)
120 }
121}
122
123#[inline]
130pub fn hamming_distance_binary(a: &BinaryVector, b: &BinaryVector) -> u32 {
131 if a.dims != b.dims {
132 return u32::MAX;
133 }
134
135 let required_bytes = a.dims.div_ceil(8);
136 if a.data.len() < required_bytes || b.data.len() < required_bytes {
137 return u32::MAX;
138 }
139
140 let config = simd_config();
141
142 #[cfg(target_arch = "aarch64")]
143 {
144 if config.neon_enabled {
145 return unsafe {
150 hamming_distance_neon(&a.data[..required_bytes], &b.data[..required_bytes], a.dims)
151 };
152 }
153 }
154
155 #[cfg(not(target_arch = "aarch64"))]
156 {
157 let _ = config;
158 }
159
160 hamming_distance_scalar(&a.data[..required_bytes], &b.data[..required_bytes], a.dims)
161}
162
163fn hamming_distance_scalar(a: &[u8], b: &[u8], dims: usize) -> u32 {
171 let mut total: u32 = 0;
172
173 let full_bytes = dims / 8; let chunks = full_bytes / 8;
176
177 for c in 0..chunks {
179 let offset = c * 8;
180 let a_u64 = u64::from_ne_bytes([
181 a[offset],
182 a[offset + 1],
183 a[offset + 2],
184 a[offset + 3],
185 a[offset + 4],
186 a[offset + 5],
187 a[offset + 6],
188 a[offset + 7],
189 ]);
190 let b_u64 = u64::from_ne_bytes([
191 b[offset],
192 b[offset + 1],
193 b[offset + 2],
194 b[offset + 3],
195 b[offset + 4],
196 b[offset + 5],
197 b[offset + 6],
198 b[offset + 7],
199 ]);
200 total += (a_u64 ^ b_u64).count_ones();
201 }
202
203 let remainder_start = chunks * 8;
205 for i in remainder_start..full_bytes {
206 total += (a[i] ^ b[i]).count_ones();
207 }
208
209 let r = dims % 8;
212 if r != 0 {
213 let mask = 0xFFu8 << (8 - r); total += ((a[full_bytes] ^ b[full_bytes]) & mask).count_ones();
215 }
216
217 total
218}
219
220#[cfg(target_arch = "aarch64")]
234#[inline]
235unsafe fn hamming_distance_neon(a: &[u8], b: &[u8], dims: usize) -> u32 {
236 debug_assert_eq!(
238 a.len(),
239 b.len(),
240 "hamming_distance_neon: slice lengths differ ({} vs {})",
241 a.len(),
242 b.len()
243 );
244
245 let full_bytes = dims / 8;
247 const SIMD_WIDTH: usize = 16;
248 let chunks = full_bytes / SIMD_WIDTH;
249
250 let mut sum_u64 = vdupq_n_u64(0);
254
255 for c in 0..chunks {
256 let base = c * SIMD_WIDTH;
257 let va = vld1q_u8(a.as_ptr().add(base));
258 let vb = vld1q_u8(b.as_ptr().add(base));
259
260 let xor = veorq_u8(va, vb);
262
263 let popcnt = vcntq_u8(xor);
265
266 let sum_u16 = vpaddlq_u8(popcnt);
268 let sum_u32 = vpaddlq_u16(sum_u16);
269 sum_u64 = vaddq_u64(sum_u64, vpaddlq_u32(sum_u32));
270 }
271
272 let total = vgetq_lane_u64(sum_u64, 0) + vgetq_lane_u64(sum_u64, 1);
274 let mut result = total as u32;
275
276 let remainder_start = chunks * SIMD_WIDTH;
278 for i in remainder_start..full_bytes {
279 result += (a[i] ^ b[i]).count_ones();
280 }
281
282 let r = dims % 8;
285 if r != 0 {
286 let mask = 0xFFu8 << (8 - r); result += ((a[full_bytes] ^ b[full_bytes]) & mask).count_ones();
288 }
289
290 result
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn generate_vector(dim: usize, seed: u64) -> Vec<f32> {
298 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
299 (0..dim)
300 .map(|i| {
301 state = state
302 .wrapping_mul(6364136223846793005)
303 .wrapping_add(1442695040888963407)
304 .wrapping_add(i as u64);
305 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
306 unit * 2.0 - 1.0
307 })
308 .collect()
309 }
310
311 #[test]
312 fn test_binary_quantize_basic() {
313 let v = vec![0.5, -0.3, 0.0, -1.0, 1.0, 0.1, -0.1, 0.9];
314 let bv = BinaryVector::from_f32(&v);
315 assert_eq!(bv.data.len(), 1); assert_eq!(bv.dims, 8);
317
318 assert_eq!(bv.data[0], 0xAD, "packed bits: {:08b}", bv.data[0]);
320 }
321
322 #[test]
323 fn test_binary_roundtrip() {
324 let v = vec![0.5, -0.3, 0.0, -1.0, 1.0, 0.1, -0.1, 0.9];
325 let bv = BinaryVector::from_f32(&v);
326 let deq = bv.to_f32();
327
328 assert_eq!(deq, vec![1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0]);
330 }
331
332 #[test]
333 fn test_binary_hamming_distance() {
334 let v = generate_vector(384, 42);
336 let bv = BinaryVector::from_f32(&v);
337 assert_eq!(bv.hamming_distance(&bv), 0);
338
339 let neg_v: Vec<f32> = v.iter().map(|x| -x).collect();
341 let neg_bv = BinaryVector::from_f32(&neg_v);
342 let hamming = bv.hamming_distance(&neg_bv);
345 assert!(hamming > 350, "hamming={hamming}, expected close to 384");
347 }
348
349 #[test]
350 fn test_binary_cosine_approx_identical() {
351 let v = generate_vector(384, 55);
352 let bv = BinaryVector::from_f32(&v);
353 let cos_dist = bv.cosine_distance_approx(&bv);
354 assert!(
355 cos_dist.abs() < 1e-5,
356 "Identical binary vectors should have 0 cosine distance, got {cos_dist}"
357 );
358 }
359
360 #[test]
361 fn test_binary_cosine_approx_quality() {
362 let a = generate_vector(384, 101);
363 let b = generate_vector(384, 202);
364
365 let dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
367 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
368 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
369 let f32_cos = dot / (norm_a * norm_b);
370
371 let ba = BinaryVector::from_f32(&a);
372 let bb = BinaryVector::from_f32(&b);
373 let bin_cos = ba.cosine_similarity_approx(&bb);
374
375 assert!(
377 (f32_cos - bin_cos).abs() < 0.35,
378 "Binary cosine too far from f32: f32={f32_cos}, binary={bin_cos}"
379 );
380 }
381
382 #[test]
383 fn test_binary_memory_savings() {
384 let v = generate_vector(384, 999);
385 let bv = BinaryVector::from_f32(&v);
386
387 assert_eq!(bv.data.len(), 48);
390 }
391
392 #[test]
393 fn test_binary_non_multiple_of_8_dims() {
394 let v = generate_vector(385, 77);
396 let bv = BinaryVector::from_f32(&v);
397 assert_eq!(bv.data.len(), 49);
398 assert_eq!(bv.dims, 385);
399
400 let deq = bv.to_f32();
402 assert_eq!(deq.len(), 385);
403 }
404
405 #[test]
406 fn test_binary_with_threshold() {
407 let v = vec![0.5, 0.3, 0.1, -0.1, -0.3, -0.5, 0.7, 0.2];
408 let bv = BinaryVector::from_f32_with_threshold(&v, 0.25);
410 let deq = bv.to_f32();
411 assert_eq!(deq, vec![1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0]);
413 }
414
415 #[test]
416 fn test_binary_nan_inf_handling() {
417 let v = vec![
418 f32::NAN,
419 f32::INFINITY,
420 f32::NEG_INFINITY,
421 1.0,
422 -1.0,
423 0.0,
424 0.5,
425 -0.5,
426 ];
427 let bv = BinaryVector::from_f32(&v);
428 let deq = bv.to_f32();
429 assert_eq!(deq.len(), 8);
430 for &val in &deq {
431 assert!(val == 1.0 || val == -1.0, "Binary should produce +/-1.0");
432 }
433 }
434
435 #[test]
436 fn test_hamming_scalar_vs_neon_parity() {
437 let a = generate_vector(384, 111);
439 let b = generate_vector(384, 222);
440 let ba = BinaryVector::from_f32(&a);
441 let bb = BinaryVector::from_f32(&b);
442
443 let scalar_result = hamming_distance_scalar(&ba.data, &bb.data, ba.dims);
444 let dispatch_result = ba.hamming_distance(&bb);
445
446 assert_eq!(
447 scalar_result, dispatch_result,
448 "Scalar and dispatched Hamming should match"
449 );
450 }
451
452 #[test]
455 fn test_hamming_short_data_returns_max() {
456 let a = BinaryVector {
458 dims: 128,
459 data: vec![0xFFu8; 4],
460 norm: 1.0,
461 };
462 let b = BinaryVector {
463 dims: 128,
464 data: vec![0x00u8; 4],
465 norm: 1.0,
466 };
467 assert_eq!(
468 hamming_distance_binary(&a, &b),
469 u32::MAX,
470 "Short data must yield u32::MAX, not an OOB read"
471 );
472 }
473
474 #[test]
475 fn test_hamming_one_side_short_returns_max() {
476 let a = BinaryVector {
478 dims: 128,
479 data: vec![0xFFu8; 16],
480 norm: 1.0,
481 };
482 let b = BinaryVector {
483 dims: 128,
484 data: vec![0x00u8; 8],
485 norm: 1.0,
486 };
487 assert_eq!(hamming_distance_binary(&a, &b), u32::MAX);
488 }
489
490 #[test]
491 fn test_hamming_correct_data_still_works() {
492 let v = generate_vector(128, 42);
494 let bv = BinaryVector::from_f32(&v);
495 assert_eq!(bv.hamming_distance(&bv), 0);
496 }
497
498 #[test]
499 fn test_binary_to_f32_short_data_returns_empty() {
500 let bv = BinaryVector {
502 dims: 128,
503 data: vec![0xFFu8; 4],
504 norm: 1.0,
505 };
506 let result = bv.to_f32();
507 assert!(
508 result.is_empty(),
509 "to_f32 on malformed BinaryVector must return empty Vec"
510 );
511 }
512
513 #[test]
514 fn test_binary_to_f32_exact_length_works() {
515 let v = generate_vector(128, 7);
517 let bv = BinaryVector::from_f32(&v);
518 let deq = bv.to_f32();
519 assert_eq!(deq.len(), 128);
520 }
521
522 #[test]
532 fn test_hamming_ignores_padding_bits() {
533 let clean = BinaryVector {
536 dims: 12,
537 data: vec![0b10101010u8, 0b11110000u8],
539 norm: 1.0,
540 };
541 let dirty = BinaryVector {
542 dims: 12,
543 data: vec![0b10101010u8, 0b11111111u8],
545 norm: 1.0,
546 };
547
548 assert_eq!(
550 hamming_distance_scalar(&clean.data, &dirty.data, 12),
551 0,
552 "scalar: padding bits must not be counted"
553 );
554 assert_eq!(
555 clean.hamming_distance(&dirty),
556 0,
557 "dispatch: padding bits must not be counted"
558 );
559
560 assert_eq!(
562 clean.cosine_distance_approx(&dirty),
563 0.0,
564 "cosine_distance_approx: padding bits must not be counted"
565 );
566 }
567
568 #[test]
574 fn test_hamming_partial_byte_count() {
575 let a = BinaryVector {
576 dims: 12,
577 data: vec![0b10101010u8, 0b11110000u8],
578 norm: 1.0,
579 };
580 let b = BinaryVector {
581 dims: 12,
582 data: vec![0b01010101u8, 0b00000000u8],
583 norm: 1.0,
584 };
585
586 assert_eq!(hamming_distance_scalar(&a.data, &b.data, 12), 12);
587 assert_eq!(a.hamming_distance(&b), 12);
588 }
589}