1#[cfg(target_arch = "aarch64")]
8use std::arch::aarch64::*;
9
10#[cfg(target_arch = "x86_64")]
11use std::arch::x86_64::*;
12
13use super::simd_config;
14
15#[derive(Debug, Clone)]
19pub struct BinaryVector {
20 pub data: Vec<u8>,
22 pub dims: usize,
24 pub norm: f32,
26}
27
28impl BinaryVector {
29 pub fn from_f32(vector: &[f32]) -> Self {
34 Self::from_f32_with_threshold(vector, 0.0)
35 }
36
37 pub fn from_f32_with_threshold(vector: &[f32], threshold: f32) -> Self {
39 let dims = vector.len();
40
41 let mut norm_sq = 0.0f32;
43 for &v in vector {
44 if v.is_finite() {
45 norm_sq += v * v;
46 }
47 }
48 let norm = norm_sq.sqrt();
49
50 let packed_len = dims.div_ceil(8);
51 let data = quantize_binary(vector, threshold, packed_len);
52
53 Self { data, dims, norm }
54 }
55
56 pub fn to_f32(&self) -> Vec<f32> {
63 let required_bytes = self.dims.div_ceil(8);
64 if self.data.len() < required_bytes {
65 return Vec::new();
66 }
67 let mut result = Vec::with_capacity(self.dims);
68 for i in 0..self.dims {
69 let byte_idx = i / 8;
70 let bit_idx = 7 - (i % 8);
71 let bit = (self.data[byte_idx] >> bit_idx) & 1;
72 result.push(if bit == 1 { 1.0 } else { -1.0 });
73 }
74 result
75 }
76
77 #[inline]
81 pub fn hamming_distance(&self, other: &BinaryVector) -> u32 {
82 hamming_distance_binary(self, other)
83 }
84
85 #[inline]
91 pub fn cosine_distance_approx(&self, other: &BinaryVector) -> f32 {
92 if self.dims == 0 {
93 return 0.0;
94 }
95 let hamming = self.hamming_distance(other) as f32;
96 2.0 * hamming / self.dims as f32
97 }
98
99 #[inline]
101 pub fn cosine_similarity_approx(&self, other: &BinaryVector) -> f32 {
102 1.0 - self.cosine_distance_approx(other)
103 }
104}
105
106fn quantize_binary(vector: &[f32], threshold: f32, packed_len: usize) -> Vec<u8> {
107 #[cfg(target_arch = "x86_64")]
108 {
109 if simd_config().avx2_enabled {
110 return unsafe { quantize_binary_avx2(vector, threshold, packed_len) };
112 }
113 }
114 #[cfg(target_arch = "aarch64")]
115 {
116 if simd_config().neon_enabled {
117 return unsafe { quantize_binary_neon(vector, threshold, packed_len) };
119 }
120 }
121 quantize_binary_scalar(vector, threshold, packed_len)
122}
123
124fn quantize_binary_scalar(vector: &[f32], threshold: f32, packed_len: usize) -> Vec<u8> {
125 let mut data = vec![0u8; packed_len];
126 quantize_binary_scalar_tail(vector, threshold, &mut data, 0);
127 data
128}
129
130fn quantize_binary_scalar_tail(vector: &[f32], threshold: f32, data: &mut [u8], start: usize) {
131 for (i, &value) in vector.iter().enumerate().skip(start) {
132 let finite_value = if value.is_finite() { value } else { 0.0 };
133 if finite_value >= threshold {
134 data[i / 8] |= 1 << (7 - i % 8);
135 }
136 }
137}
138
139#[cfg(test)]
140thread_local! {
141 static BINARY_QUANTIZE_SIMD_HITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
142}
143
144#[cfg(target_arch = "x86_64")]
145#[target_feature(enable = "avx2")]
146unsafe fn quantize_binary_avx2(vector: &[f32], threshold: f32, packed_len: usize) -> Vec<u8> {
147 #[cfg(test)]
148 BINARY_QUANTIZE_SIMD_HITS.with(|hits| hits.set(hits.get() + 1));
149
150 let mut data = vec![0u8; packed_len];
151 let chunks = vector.len() / 8;
152 let threshold_scalar = threshold;
153 let sign = _mm256_set1_ps(-0.0);
154 let inf = _mm256_set1_ps(f32::INFINITY);
155 let threshold = _mm256_set1_ps(threshold_scalar);
156
157 for i in 0..chunks {
158 let input = _mm256_loadu_ps(vector.as_ptr().add(i * 8));
159 let abs = _mm256_andnot_ps(sign, input);
160 let finite = _mm256_cmp_ps(abs, inf, _CMP_LT_OQ);
161 let values = _mm256_and_ps(input, finite);
162 let above_threshold = _mm256_cmp_ps(values, threshold, _CMP_GE_OQ);
163 data[i] = (_mm256_movemask_ps(above_threshold) as u8).reverse_bits();
164 }
165
166 quantize_binary_scalar_tail(vector, threshold_scalar, &mut data, chunks * 8);
167 data
168}
169
170#[cfg(target_arch = "aarch64")]
171#[target_feature(enable = "neon")]
172unsafe fn quantize_binary_neon(vector: &[f32], threshold: f32, packed_len: usize) -> Vec<u8> {
173 #[cfg(test)]
174 BINARY_QUANTIZE_SIMD_HITS.with(|hits| hits.set(hits.get() + 1));
175
176 let mut data = vec![0u8; packed_len];
177 let chunks = vector.len() / 8;
178 let inf = vdupq_n_f32(f32::INFINITY);
179 let zero = vdupq_n_f32(0.0);
180 let threshold_vector = vdupq_n_f32(threshold);
181
182 for i in 0..chunks {
183 let base = i * 8;
184 let first = vld1q_f32(vector.as_ptr().add(base));
185 let second = vld1q_f32(vector.as_ptr().add(base + 4));
186 let first_values = vbslq_f32(vcaltq_f32(first, inf), first, zero);
187 let second_values = vbslq_f32(vcaltq_f32(second, inf), second, zero);
188 let first_mask = vcgeq_f32(first_values, threshold_vector);
189 let second_mask = vcgeq_f32(second_values, threshold_vector);
190 let mut first_lanes = [0u32; 4];
191 let mut second_lanes = [0u32; 4];
192 vst1q_u32(first_lanes.as_mut_ptr(), first_mask);
193 vst1q_u32(second_lanes.as_mut_ptr(), second_mask);
194
195 let mut packed = 0u8;
196 for (lane, mask) in first_lanes.into_iter().chain(second_lanes).enumerate() {
197 if mask != 0 {
198 packed |= 1 << (7 - lane);
199 }
200 }
201 data[i] = packed;
202 }
203
204 quantize_binary_scalar_tail(vector, threshold, &mut data, chunks * 8);
205 data
206}
207
208#[inline]
212pub fn hamming_distance_binary(a: &BinaryVector, b: &BinaryVector) -> u32 {
213 if a.dims != b.dims {
214 return u32::MAX;
215 }
216
217 let required_bytes = a.dims.div_ceil(8);
218 if a.data.len() < required_bytes || b.data.len() < required_bytes {
219 return u32::MAX;
220 }
221
222 let config = simd_config();
223
224 #[cfg(target_arch = "aarch64")]
225 {
226 if config.neon_enabled {
227 return unsafe {
232 hamming_distance_neon(&a.data[..required_bytes], &b.data[..required_bytes], a.dims)
233 };
234 }
235 }
236
237 #[cfg(not(target_arch = "aarch64"))]
238 {
239 let _ = config;
240 }
241
242 hamming_distance_scalar(&a.data[..required_bytes], &b.data[..required_bytes], a.dims)
243}
244
245fn hamming_distance_scalar(a: &[u8], b: &[u8], dims: usize) -> u32 {
249 let mut total: u32 = 0;
250
251 let full_bytes = dims / 8; let chunks = full_bytes / 8;
254
255 for c in 0..chunks {
257 let offset = c * 8;
258 let a_u64 = u64::from_ne_bytes([
259 a[offset],
260 a[offset + 1],
261 a[offset + 2],
262 a[offset + 3],
263 a[offset + 4],
264 a[offset + 5],
265 a[offset + 6],
266 a[offset + 7],
267 ]);
268 let b_u64 = u64::from_ne_bytes([
269 b[offset],
270 b[offset + 1],
271 b[offset + 2],
272 b[offset + 3],
273 b[offset + 4],
274 b[offset + 5],
275 b[offset + 6],
276 b[offset + 7],
277 ]);
278 total += (a_u64 ^ b_u64).count_ones();
279 }
280
281 let remainder_start = chunks * 8;
283 for i in remainder_start..full_bytes {
284 total += (a[i] ^ b[i]).count_ones();
285 }
286
287 let r = dims % 8;
290 if r != 0 {
291 let mask = 0xFFu8 << (8 - r); total += ((a[full_bytes] ^ b[full_bytes]) & mask).count_ones();
293 }
294
295 total
296}
297
298#[cfg(target_arch = "aarch64")]
304#[inline]
305unsafe fn hamming_distance_neon(a: &[u8], b: &[u8], dims: usize) -> u32 {
306 debug_assert_eq!(
308 a.len(),
309 b.len(),
310 "hamming_distance_neon: slice lengths differ ({} vs {})",
311 a.len(),
312 b.len()
313 );
314
315 let full_bytes = dims / 8;
317 const SIMD_WIDTH: usize = 16;
318 let chunks = full_bytes / SIMD_WIDTH;
319
320 let mut sum_u64 = vdupq_n_u64(0);
324
325 for c in 0..chunks {
326 let base = c * SIMD_WIDTH;
327 let va = vld1q_u8(a.as_ptr().add(base));
328 let vb = vld1q_u8(b.as_ptr().add(base));
329
330 let xor = veorq_u8(va, vb);
332
333 let popcnt = vcntq_u8(xor);
335
336 let sum_u16 = vpaddlq_u8(popcnt);
338 let sum_u32 = vpaddlq_u16(sum_u16);
339 sum_u64 = vaddq_u64(sum_u64, vpaddlq_u32(sum_u32));
340 }
341
342 let total = vgetq_lane_u64(sum_u64, 0) + vgetq_lane_u64(sum_u64, 1);
344 let mut result = total as u32;
345
346 let remainder_start = chunks * SIMD_WIDTH;
348 for i in remainder_start..full_bytes {
349 result += (a[i] ^ b[i]).count_ones();
350 }
351
352 let r = dims % 8;
355 if r != 0 {
356 let mask = 0xFFu8 << (8 - r); result += ((a[full_bytes] ^ b[full_bytes]) & mask).count_ones();
358 }
359
360 result
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn generate_vector(dim: usize, seed: u64) -> Vec<f32> {
368 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
369 (0..dim)
370 .map(|i| {
371 state = state
372 .wrapping_mul(6364136223846793005)
373 .wrapping_add(1442695040888963407)
374 .wrapping_add(i as u64);
375 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
376 unit * 2.0 - 1.0
377 })
378 .collect()
379 }
380
381 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
382 #[test]
383 fn test_binary_quantize_explicit_simd_matches_scalar_and_is_dispatched() {
384 #[cfg(target_arch = "x86_64")]
385 if !std::arch::is_x86_feature_detected!("avx2") {
386 return;
387 }
388
389 for threshold in [0.0, 0.25, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
390 for dim in [0usize, 1, 3, 4, 7, 8, 9, 31, 32, 33, 383, 384, 385] {
391 let mut input = generate_vector(dim, 900 + dim as u64);
392 if dim > 0 {
393 input[0] = f32::NAN;
394 }
395 if dim > 1 {
396 input[1] = f32::INFINITY;
397 }
398 if dim > 2 {
399 input[2] = f32::NEG_INFINITY;
400 }
401
402 let packed_len = dim.div_ceil(8);
403 let scalar = quantize_binary_scalar(&input, threshold, packed_len);
404 #[cfg(target_arch = "aarch64")]
405 let simd = unsafe { quantize_binary_neon(&input, threshold, packed_len) };
407 #[cfg(target_arch = "x86_64")]
408 let simd = unsafe { quantize_binary_avx2(&input, threshold, packed_len) };
410 assert_eq!(
411 simd, scalar,
412 "explicit SIMD mismatch at dim={dim}, threshold={threshold}"
413 );
414 }
415 }
416
417 let input = generate_vector(385, 1_063);
418 let before = BINARY_QUANTIZE_SIMD_HITS.with(std::cell::Cell::get);
419 let quantized = BinaryVector::from_f32(&input);
420 let after = BINARY_QUANTIZE_SIMD_HITS.with(std::cell::Cell::get);
421 assert_eq!(
422 after,
423 before + 1,
424 "BinaryVector::from_f32 did not execute its explicit SIMD quantizer"
425 );
426 assert_eq!(
427 quantized.data,
428 quantize_binary_scalar(&input, 0.0, input.len().div_ceil(8))
429 );
430 }
431
432 #[test]
433 fn test_binary_quantize_basic() {
434 let v = vec![0.5, -0.3, 0.0, -1.0, 1.0, 0.1, -0.1, 0.9];
435 let bv = BinaryVector::from_f32(&v);
436 assert_eq!(bv.data.len(), 1); assert_eq!(bv.dims, 8);
438
439 assert_eq!(bv.data[0], 0xAD, "packed bits: {:08b}", bv.data[0]);
441 }
442
443 #[test]
444 fn test_binary_roundtrip() {
445 let v = vec![0.5, -0.3, 0.0, -1.0, 1.0, 0.1, -0.1, 0.9];
446 let bv = BinaryVector::from_f32(&v);
447 let deq = bv.to_f32();
448
449 assert_eq!(deq, vec![1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0]);
451 }
452
453 #[test]
454 fn test_binary_hamming_distance() {
455 let v = generate_vector(384, 42);
457 let bv = BinaryVector::from_f32(&v);
458 assert_eq!(bv.hamming_distance(&bv), 0);
459
460 let neg_v: Vec<f32> = v.iter().map(|x| -x).collect();
462 let neg_bv = BinaryVector::from_f32(&neg_v);
463 let hamming = bv.hamming_distance(&neg_bv);
466 assert!(hamming > 350, "hamming={hamming}, expected close to 384");
468 }
469
470 #[test]
471 fn test_binary_cosine_approx_identical() {
472 let v = generate_vector(384, 55);
473 let bv = BinaryVector::from_f32(&v);
474 let cos_dist = bv.cosine_distance_approx(&bv);
475 assert!(
476 cos_dist.abs() < 1e-5,
477 "Identical binary vectors should have 0 cosine distance, got {cos_dist}"
478 );
479 }
480
481 #[test]
482 fn test_binary_cosine_approx_quality() {
483 let a = generate_vector(384, 101);
484 let b = generate_vector(384, 202);
485
486 let dot: f32 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
488 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
489 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
490 let f32_cos = dot / (norm_a * norm_b);
491
492 let ba = BinaryVector::from_f32(&a);
493 let bb = BinaryVector::from_f32(&b);
494 let bin_cos = ba.cosine_similarity_approx(&bb);
495
496 assert!(
498 (f32_cos - bin_cos).abs() < 0.35,
499 "Binary cosine too far from f32: f32={f32_cos}, binary={bin_cos}"
500 );
501 }
502
503 #[test]
504 fn test_binary_memory_savings() {
505 let v = generate_vector(384, 999);
506 let bv = BinaryVector::from_f32(&v);
507
508 assert_eq!(bv.data.len(), 48);
511 }
512
513 #[test]
514 fn test_binary_non_multiple_of_8_dims() {
515 let v = generate_vector(385, 77);
517 let bv = BinaryVector::from_f32(&v);
518 assert_eq!(bv.data.len(), 49);
519 assert_eq!(bv.dims, 385);
520
521 let deq = bv.to_f32();
523 assert_eq!(deq.len(), 385);
524 }
525
526 #[test]
527 fn test_binary_with_threshold() {
528 let v = vec![0.5, 0.3, 0.1, -0.1, -0.3, -0.5, 0.7, 0.2];
529 let bv = BinaryVector::from_f32_with_threshold(&v, 0.25);
531 let deq = bv.to_f32();
532 assert_eq!(deq, vec![1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0]);
534 }
535
536 #[test]
537 fn test_binary_nan_inf_handling() {
538 let v = vec![
539 f32::NAN,
540 f32::INFINITY,
541 f32::NEG_INFINITY,
542 1.0,
543 -1.0,
544 0.0,
545 0.5,
546 -0.5,
547 ];
548 let bv = BinaryVector::from_f32(&v);
549 let deq = bv.to_f32();
550 assert_eq!(deq.len(), 8);
551 for &val in &deq {
552 assert!(val == 1.0 || val == -1.0, "Binary should produce +/-1.0");
553 }
554 }
555
556 #[test]
557 fn test_hamming_scalar_vs_neon_parity() {
558 let a = generate_vector(384, 111);
560 let b = generate_vector(384, 222);
561 let ba = BinaryVector::from_f32(&a);
562 let bb = BinaryVector::from_f32(&b);
563
564 let scalar_result = hamming_distance_scalar(&ba.data, &bb.data, ba.dims);
565 let dispatch_result = ba.hamming_distance(&bb);
566
567 assert_eq!(
568 scalar_result, dispatch_result,
569 "Scalar and dispatched Hamming should match"
570 );
571 }
572
573 #[test]
576 fn test_hamming_short_data_returns_max() {
577 let a = BinaryVector {
579 dims: 128,
580 data: vec![0xFFu8; 4],
581 norm: 1.0,
582 };
583 let b = BinaryVector {
584 dims: 128,
585 data: vec![0x00u8; 4],
586 norm: 1.0,
587 };
588 assert_eq!(
589 hamming_distance_binary(&a, &b),
590 u32::MAX,
591 "Short data must yield u32::MAX, not an OOB read"
592 );
593 }
594
595 #[test]
596 fn test_hamming_one_side_short_returns_max() {
597 let a = BinaryVector {
599 dims: 128,
600 data: vec![0xFFu8; 16],
601 norm: 1.0,
602 };
603 let b = BinaryVector {
604 dims: 128,
605 data: vec![0x00u8; 8],
606 norm: 1.0,
607 };
608 assert_eq!(hamming_distance_binary(&a, &b), u32::MAX);
609 }
610
611 #[test]
612 fn test_hamming_correct_data_still_works() {
613 let v = generate_vector(128, 42);
615 let bv = BinaryVector::from_f32(&v);
616 assert_eq!(bv.hamming_distance(&bv), 0);
617 }
618
619 #[test]
620 fn test_binary_to_f32_short_data_returns_empty() {
621 let bv = BinaryVector {
623 dims: 128,
624 data: vec![0xFFu8; 4],
625 norm: 1.0,
626 };
627 let result = bv.to_f32();
628 assert!(
629 result.is_empty(),
630 "to_f32 on malformed BinaryVector must return empty Vec"
631 );
632 }
633
634 #[test]
635 fn test_binary_to_f32_exact_length_works() {
636 let v = generate_vector(128, 7);
638 let bv = BinaryVector::from_f32(&v);
639 let deq = bv.to_f32();
640 assert_eq!(deq.len(), 128);
641 }
642
643 #[test]
653 fn test_hamming_ignores_padding_bits() {
654 let clean = BinaryVector {
657 dims: 12,
658 data: vec![0b10101010u8, 0b11110000u8],
660 norm: 1.0,
661 };
662 let dirty = BinaryVector {
663 dims: 12,
664 data: vec![0b10101010u8, 0b11111111u8],
666 norm: 1.0,
667 };
668
669 assert_eq!(
671 hamming_distance_scalar(&clean.data, &dirty.data, 12),
672 0,
673 "scalar: padding bits must not be counted"
674 );
675 assert_eq!(
676 clean.hamming_distance(&dirty),
677 0,
678 "dispatch: padding bits must not be counted"
679 );
680
681 assert_eq!(
683 clean.cosine_distance_approx(&dirty),
684 0.0,
685 "cosine_distance_approx: padding bits must not be counted"
686 );
687 }
688
689 #[test]
695 fn test_hamming_partial_byte_count() {
696 let a = BinaryVector {
697 dims: 12,
698 data: vec![0b10101010u8, 0b11110000u8],
699 norm: 1.0,
700 };
701 let b = BinaryVector {
702 dims: 12,
703 data: vec![0b01010101u8, 0b00000000u8],
704 norm: 1.0,
705 };
706
707 assert_eq!(hamming_distance_scalar(&a.data, &b.data, 12), 12);
708 assert_eq!(a.hamming_distance(&b), 12);
709 }
710}