1#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
7
8use rand::RngExt;
9
10#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
11use rayon::prelude::*;
12
13use crate::error::Result;
14
15pub use crate::hyperdim_batch::batch_cosine_similarity;
16pub use crate::hyperdim_binary::BHVec10240;
17pub use crate::hyperdim_ops::{Hypervector, bundle_word_scalar};
18
19#[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
21use crate::hyperdim_simd::{and_simd_avx2, bind_simd_avx2, hamming_distance_simd_avx2};
22#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
23use crate::hyperdim_simd::{and_simd_neon, bind_simd_neon, hamming_distance_simd_neon};
24#[cfg(all(
25 not(target_arch = "wasm32"),
26 any(target_arch = "x86_64", target_arch = "x86")
27))]
28use crate::hyperdim_simd::{and_simd_x86, bind_simd_x86};
29
30#[cfg(all(target_arch = "x86_64", not(target_arch = "wasm32")))]
31use crate::hyperdim_simd_bundle::bundle_block_avx2;
32
33#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
34use crate::hyperdim_simd_bundle::bundle_block_neon;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[must_use]
39pub struct HVec10240 {
40 pub data: [u128; 80],
41}
42
43impl HVec10240 {
44 pub const DIMENSION: usize = 10240;
45 pub const WORDS: usize = 80;
46
47 pub const fn zero() -> Self {
49 Self { data: [0u128; 80] }
50 }
51
52 pub fn random() -> Self {
58 let mut rng = rand::rng();
59 let mut data = [0u128; 80];
60 rng.fill(&mut data);
61 Self { data }
62 }
63
64 pub fn new_seeded(seed: u64) -> Self {
68 use rand::SeedableRng;
69 use rand::rngs::StdRng;
70 let mut rng = StdRng::seed_from_u64(seed);
71 let mut data = [0u128; 80];
72 rng.fill(&mut data);
73 Self { data }
74 }
75
76 pub fn sparse(density: f32) -> Self {
78 let mut rng = rand::rng();
79 let mut data = [0u128; 80];
80 let bits_to_set = (Self::DIMENSION as f32 * density) as usize;
81
82 for _ in 0..bits_to_set {
83 let pos = rng.random_range(0..Self::DIMENSION);
84 let word = pos / 128;
85 let bit = pos % 128;
86 data[word] |= 1u128 << bit;
87 }
88
89 Self { data }
90 }
91
92 pub fn set_bit(&mut self, pos: usize) {
97 assert!(
98 pos < Self::DIMENSION,
99 "bit position {pos} out of range (max {})",
100 Self::DIMENSION
101 );
102 let word = pos / 128;
103 let bit = pos % 128;
104 self.data[word] |= 1u128 << bit;
105 }
106
107 #[allow(clippy::needless_range_loop)]
115 pub fn bundle(vectors: &[Self]) -> Result<Self> {
116 let num_vectors = vectors.len();
117 if num_vectors == 0 {
118 return Ok(Self::zero());
119 }
120 if num_vectors == 1 {
121 return Ok(vectors[0]);
122 }
123 if num_vectors == 2 {
124 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
125 {
126 if is_x86_feature_detected!("avx2") {
127 return Ok(Self {
129 data: unsafe { and_simd_avx2(&vectors[0].data, &vectors[1].data) },
130 });
131 } else {
132 return Ok(Self {
133 data: and_simd_x86(&vectors[0].data, &vectors[1].data),
134 });
135 }
136 }
137
138 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86"))]
139 {
140 return Ok(Self {
141 data: and_simd_x86(&vectors[0].data, &vectors[1].data),
142 });
143 }
144
145 #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
146 {
147 return Ok(Self {
149 data: unsafe { and_simd_neon(&vectors[0].data, &vectors[1].data) },
150 });
151 }
152
153 #[cfg(any(
154 target_arch = "wasm32",
155 all(
156 not(target_arch = "wasm32"),
157 not(any(
158 target_arch = "x86_64",
159 target_arch = "x86",
160 target_arch = "aarch64"
161 ))
162 )
163 ))]
164 {
165 let mut res = Self::zero();
166 for i in 0..80 {
167 res.data[i] = vectors[0].data[i] & vectors[1].data[i];
168 }
169 return Ok(res);
170 }
171 }
172
173 let threshold = num_vectors / 2 + 1;
174 let num_planes = (usize::BITS - num_vectors.leading_zeros()) as usize;
175
176 #[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
177 if num_vectors >= 256 {
181 #[cfg(target_arch = "x86_64")]
182 if is_x86_feature_detected!("avx2") {
183 let mut data = [0u128; 80];
184 data.par_chunks_mut(2).enumerate().for_each(|(i, chunk)| {
185 let res = unsafe {
187 crate::hyperdim_simd_bundle::bundle_block_avx2_single(
188 vectors,
189 i * 2,
190 threshold,
191 num_planes,
192 )
193 };
194 unsafe {
196 std::arch::x86_64::_mm256_storeu_si256(chunk.as_mut_ptr().cast(), res);
197 }
198 });
199 return Ok(Self { data });
200 }
201
202 #[cfg(target_arch = "aarch64")]
203 {
204 let mut data = [0u128; 80];
205 data.par_iter_mut().enumerate().for_each(|(i, word)| {
206 let res = unsafe {
208 crate::hyperdim_simd_bundle::bundle_block_neon_single(
209 vectors, i, threshold, num_planes,
210 )
211 };
212 unsafe {
214 std::arch::aarch64::vst1q_u8(word as *mut u128 as *mut u8, res);
215 }
216 });
217 return Ok(Self { data });
218 }
219
220 #[cfg(not(target_arch = "aarch64"))]
224 {
225 let mut data = [0u128; 80];
226 data.par_iter_mut().enumerate().for_each(|(i, word)| {
227 *word = bundle_word_scalar(vectors, i, threshold, num_planes);
228 });
229 return Ok(Self { data });
230 }
231 }
232
233 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
234 if is_x86_feature_detected!("avx2") {
235 return Ok(Self {
237 data: unsafe { bundle_block_avx2(vectors, threshold, num_planes) },
238 });
239 }
240
241 #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
242 {
243 return Ok(Self {
245 data: unsafe { bundle_block_neon(vectors, threshold, num_planes) },
246 });
247 }
248
249 #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
250 {
251 let mut data = [0u128; 80];
252 #[allow(clippy::needless_range_loop)]
253 for i in 0..80 {
254 data[i] = bundle_word_scalar(vectors, i, threshold, num_planes);
255 }
256 Ok(Self { data })
257 }
258 }
259
260 pub fn bind(&self, other: &Self) -> Self {
262 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
263 {
264 if is_x86_feature_detected!("avx2") {
266 Self {
268 data: unsafe { bind_simd_avx2(&self.data, &other.data) },
269 }
270 } else {
271 Self {
272 data: bind_simd_x86(&self.data, &other.data),
273 }
274 }
275 }
276
277 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86"))]
278 {
279 Self {
280 data: bind_simd_x86(&self.data, &other.data),
281 }
282 }
283
284 #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
285 {
286 Self {
290 data: unsafe { bind_simd_neon(&self.data, &other.data) },
291 }
292 }
293
294 #[cfg(target_arch = "wasm32")]
295 {
296 let mut result = [0u128; 80];
297 for i in 0..80 {
298 result[i] = self.data[i] ^ other.data[i];
299 }
300 Self { data: result }
301 }
302
303 #[cfg(all(
304 not(target_arch = "wasm32"),
305 not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"))
306 ))]
307 {
308 let mut result = [0u128; 80];
309 for i in 0..80 {
310 result[i] = self.data[i] ^ other.data[i];
311 }
312 Self { data: result }
313 }
314 }
315
316 #[must_use]
320 pub fn cosine_similarity(&self, other: &Self) -> f32 {
321 let distance = self.hamming_distance(other);
322 1.0 - (distance as f32 / 5120.0)
326 }
327
328 #[must_use]
335 pub fn hamming_distance(&self, other: &Self) -> u32 {
336 #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
337 {
338 if is_x86_feature_detected!("avx2") {
339 unsafe { hamming_distance_simd_avx2(&self.data, &other.data) }
341 } else {
342 crate::hyperdim_simd::hamming_distance_optimized(&self.data, &other.data)
343 }
344 }
345
346 #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
347 {
348 unsafe { hamming_distance_simd_neon(&self.data, &other.data) }
350 }
351
352 #[cfg(any(
353 target_arch = "wasm32",
354 not(any(target_arch = "x86_64", target_arch = "aarch64"))
355 ))]
356 {
357 crate::hyperdim_simd::hamming_distance_optimized(&self.data, &other.data)
358 }
359 }
360
361 #[allow(clippy::needless_range_loop)]
366 pub fn permute(&self, shift: usize) -> Self {
367 let mut result = [0u128; 80];
368 let bit_shift = shift % 128;
369 let word_shift = (shift / 128) % 80;
370
371 if bit_shift == 0 {
373 let (left, right) = self.data.split_at(word_shift);
374 result[..80 - word_shift].copy_from_slice(right);
375 result[80 - word_shift..].copy_from_slice(left);
376 return Self { data: result };
377 }
378
379 let inv_bit_shift = 128 - bit_shift;
380
381 let limit = 79 - word_shift;
384 for i in 0..limit {
385 let src1 = i + word_shift;
386 let src2 = src1 + 1;
387 result[i] = (self.data[src1] << bit_shift) | (self.data[src2] >> inv_bit_shift);
388 }
389
390 result[limit] = (self.data[79] << bit_shift) | (self.data[0] >> inv_bit_shift);
393
394 for i in limit + 1..80 {
396 let src1 = i + word_shift - 80;
397 let src2 = src1 + 1;
398 result[i] = (self.data[src1] << bit_shift) | (self.data[src2] >> inv_bit_shift);
399 }
400
401 Self { data: result }
402 }
403
404 pub fn to_bytes(&self) -> Vec<u8> {
406 let mut bytes = Vec::with_capacity(1280);
407 #[cfg(target_endian = "little")]
408 {
409 let data_bytes: &[u8; 1280] = unsafe { &*(self.data.as_ptr() as *const [u8; 1280]) };
414 bytes.extend_from_slice(data_bytes);
415 }
416 #[cfg(not(target_endian = "little"))]
417 {
418 for word in &self.data {
419 bytes.extend_from_slice(&word.to_le_bytes());
420 }
421 }
422 bytes
423 }
424
425 #[allow(clippy::missing_const_for_fn)] pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
428 if bytes.len() != 1280 {
429 return Err(crate::error::MemoryError::InvalidDimension {
430 expected: 1280,
431 actual: bytes.len(),
432 });
433 }
434
435 #[allow(unused_mut)]
436 let mut data = [0u128; 80];
437 #[cfg(target_endian = "little")]
438 {
439 unsafe {
444 std::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr() as *mut u8, 1280);
445 }
446 }
447 #[cfg(not(target_endian = "little"))]
448 {
449 for i in 0..80 {
450 let mut word_bytes = [0u8; 16];
451 word_bytes.copy_from_slice(&bytes[i * 16..(i + 1) * 16]);
452 data[i] = u128::from_le_bytes(word_bytes);
453 }
454 }
455
456 Ok(Self { data })
457 }
458}
459
460pub use crate::bundle::BundleAccumulator;