1use std::borrow::Borrow;
22use std::f64::consts::TAU;
23use std::sync::OnceLock;
24
25const SCALE: i64 = 1 << 14;
27
28const CACHED_RESOLUTIONS: usize = 16;
30static COSINES: [OnceLock<Box<[f64]>>; CACHED_RESOLUTIONS] =
31 [const { OnceLock::new() }; CACHED_RESOLUTIONS];
32static BUNDLE_LUTS: [OnceLock<BundleLut>; CACHED_RESOLUTIONS] =
33 [const { OnceLock::new() }; CACHED_RESOLUTIONS];
34
35enum BundleLut {
36 Wide(Box<[[i64; 2]]>),
37 Compact(Box<[[i16; 2]]>),
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct PhaseHVec {
42 dim: usize,
43 n: u32,
44 phases: Vec<u16>,
45}
46
47fn mix(x: u64, k: u64) -> u64 {
48 let mut h = x.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ k.wrapping_mul(0xC2B2_AE3D_27D4_EB4F);
49 h ^= h >> 29;
50 h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
51 h ^= h >> 32;
52 h
53}
54
55fn luts(n: u32) -> BundleLut {
57 let entries = (0..n).map(|k| {
58 let angle = TAU * k as f64 / n as f64;
59 [
60 (SCALE as f64 * angle.cos()).round() as i64,
61 (SCALE as f64 * angle.sin()).round() as i64,
62 ]
63 });
64 if n <= 256 {
65 BundleLut::Wide(entries.collect())
66 } else {
67 BundleLut::Compact(entries.map(|[re, im]| [re as i16, im as i16]).collect())
68 }
69}
70
71fn accumulate<T: Copy + Into<i64>, V: Borrow<PhaseHVec>>(
72 vectors: &[V],
73 lut: &[[T; 2]],
74 sums: &mut [[i64; 2]],
75) {
76 for v in vectors {
77 for (sum, &phase) in sums.iter_mut().zip(&v.borrow().phases) {
78 let [cos, sin] = lut[phase as usize];
79 sum[0] += cos.into();
80 sum[1] += sin.into();
81 }
82 }
83}
84
85fn cached_resolution(n: u32) -> Option<usize> {
86 n.is_power_of_two().then(|| n.trailing_zeros() as usize - 1)
87}
88
89fn cosine_lut(n: u32, dim: usize) -> Option<&'static [f64]> {
90 let entry = &COSINES[cached_resolution(n)?];
91 if n > 256 && dim < n as usize {
92 return entry.get().map(AsRef::as_ref);
93 }
94 Some(entry.get_or_init(|| {
95 (0..n)
96 .map(|delta| (TAU * delta as f64 / n as f64).cos())
97 .collect()
98 }))
99}
100
101impl PhaseHVec {
102 pub fn new_random(dim: usize, n: u32, seed: u64) -> Self {
105 assert!(
106 (2..=65536).contains(&n),
107 "phase resolution n must be in 2..=65536"
108 );
109 let phases = (0..dim)
110 .map(|d| (mix(d as u64, seed) % n as u64) as u16)
111 .collect();
112 Self { dim, n, phases }
113 }
114
115 pub fn from_phases(phases: Vec<u16>, n: u32) -> Self {
117 assert!(
118 (2..=65536).contains(&n),
119 "phase resolution n must be in 2..=65536"
120 );
121 assert!(
122 phases.iter().all(|&p| (p as u32) < n),
123 "phase index out of range"
124 );
125 Self {
126 dim: phases.len(),
127 n,
128 phases,
129 }
130 }
131
132 pub fn dim(&self) -> usize {
134 self.dim
135 }
136 pub fn n(&self) -> u32 {
139 self.n
140 }
141 pub fn phases(&self) -> &[u16] {
143 &self.phases
144 }
145
146 fn check(&self, other: &Self) {
147 assert_eq!(self.dim, other.dim, "phase-vector dimension mismatch");
148 assert_eq!(self.n, other.n, "phase-vector resolution mismatch");
149 }
150
151 pub fn bind(&self, other: &Self) -> Self {
153 self.check(other);
154 let n = self.n;
155 let phases = self
156 .phases
157 .iter()
158 .zip(&other.phases)
159 .map(|(&a, &b)| {
160 let sum = a as u32 + b as u32;
161 (if sum >= n { sum - n } else { sum }) as u16
162 })
163 .collect();
164 Self {
165 dim: self.dim,
166 n: self.n,
167 phases,
168 }
169 }
170
171 pub fn unbind(&self, other: &Self) -> Self {
173 self.check(other);
174 let n = self.n;
175 let phases = self
176 .phases
177 .iter()
178 .zip(&other.phases)
179 .map(|(&a, &b)| {
180 let difference = a as u32 + n - b as u32;
181 (if difference >= n {
182 difference - n
183 } else {
184 difference
185 }) as u16
186 })
187 .collect();
188 Self {
189 dim: self.dim,
190 n: self.n,
191 phases,
192 }
193 }
194
195 pub fn similarity(&self, other: &Self) -> f64 {
198 self.check(other);
199 if self.dim == 0 {
200 return 1.0;
201 }
202 let s: f64 = if let Some(cos) = cosine_lut(self.n, self.dim) {
203 self.phases
204 .iter()
205 .zip(&other.phases)
206 .map(|(&a, &b)| cos[a.abs_diff(b) as usize])
207 .sum()
208 } else {
209 let n = self.n as f64;
210 self.phases
211 .iter()
212 .zip(&other.phases)
213 .map(|(&a, &b)| (TAU * (a as f64 - b as f64) / n).cos())
214 .sum()
215 };
216 s / self.dim as f64
217 }
218
219 pub fn bundle<V: Borrow<Self>>(vectors: &[V]) -> Self {
224 if vectors.is_empty() {
225 return Self {
226 dim: 0,
227 n: 2,
228 phases: Vec::new(),
229 };
230 }
231 let first = vectors[0].borrow();
232 let (dim, n) = (first.dim, first.n);
233 for v in vectors {
234 let v = v.borrow();
235 assert_eq!(v.dim, dim, "phase-vector dimension mismatch in bundle");
236 assert_eq!(v.n, n, "phase-vector resolution mismatch in bundle");
237 }
238 if dim == 0 || vectors.len() == 1 {
239 return first.clone();
240 }
241 if n == 2 {
242 let mut ones = vec![0usize; dim];
243 for v in vectors {
244 for (count, &phase) in ones.iter_mut().zip(&v.borrow().phases) {
245 *count += phase as usize;
246 }
247 }
248 let phases = ones
249 .into_iter()
250 .map(|count| u16::from(count > vectors.len() / 2))
251 .collect();
252 return Self { dim, n, phases };
253 }
254 let owned;
255 let lut = if let Some(index) = cached_resolution(n) {
256 BUNDLE_LUTS[index].get_or_init(|| luts(n))
257 } else {
258 owned = luts(n);
259 &owned
260 };
261 let mut sums = vec![[0i64; 2]; dim];
262 match lut {
263 BundleLut::Wide(lut) => accumulate(vectors, lut, &mut sums),
264 BundleLut::Compact(lut) => accumulate(vectors, lut, &mut sums),
265 }
266 let phases = sums
267 .into_iter()
268 .map(|[re, im]| {
269 let ang = (im as f64).atan2(re as f64);
270 let idx = (ang / TAU * n as f64).round() as i64;
271 idx.rem_euclid(n as i64) as u16
272 })
273 .collect();
274 Self { dim, n, phases }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn bind_unbind_is_exact_roundtrip() {
284 let a = PhaseHVec::new_random(512, 256, 1);
285 let b = PhaseHVec::new_random(512, 256, 2);
286 let recovered = a.bind(&b).unbind(&b);
287 assert_eq!(
288 recovered.phases(),
289 a.phases(),
290 "unbind(bind(a,b),b) must equal a"
291 );
292 }
293
294 #[test]
295 fn similarity_self_is_one_random_near_zero() {
296 let a = PhaseHVec::new_random(4096, 256, 7);
297 assert!((a.similarity(&a) - 1.0).abs() < 1e-9);
298 let b = PhaseHVec::new_random(4096, 256, 8);
299 assert!(
300 a.similarity(&b).abs() < 0.05,
301 "independent vectors ~orthogonal"
302 );
303 }
304
305 #[test]
306 fn binding_is_not_self_inverse() {
307 let a = PhaseHVec::new_random(2048, 256, 3);
309 let self_bound = a.bind(&a);
310 assert!(
311 self_bound.similarity(&a).abs() < 0.2,
312 "phase self-bind must not collapse back to a (non-self-inverse)"
313 );
314 }
315
316 #[test]
317 fn bundle_preserves_membership() {
318 let members: Vec<PhaseHVec> = (0..5)
319 .map(|i| PhaseHVec::new_random(4096, 256, 100 + i))
320 .collect();
321 let bundle = PhaseHVec::bundle(&members);
322 let member_sim = bundle.similarity(&members[0]);
323 let nonmember = PhaseHVec::new_random(4096, 256, 999);
324 assert!(
325 member_sim > bundle.similarity(&nonmember) + 0.05,
326 "a bundled member ({member_sim:.3}) must score above a non-member"
327 );
328 }
329
330 #[test]
331 fn bundle_is_deterministic_and_order_free() {
332 let v: Vec<PhaseHVec> = (0..4)
333 .map(|i| PhaseHVec::new_random(256, 64, 10 + i))
334 .collect();
335 let ab = PhaseHVec::bundle(&v);
336 let mut rev = v.clone();
337 rev.reverse();
338 let ba = PhaseHVec::bundle(&rev);
339 assert_eq!(
340 ab.phases(),
341 ba.phases(),
342 "bundle must be order-independent and deterministic"
343 );
344 }
345
346 #[test]
347 fn bundle_empty_is_zero_dim() {
348 let empty: Vec<PhaseHVec> = Vec::new();
349 let b = PhaseHVec::bundle(&empty);
350 assert_eq!(b.dim(), 0);
351 assert!(b.phases().is_empty());
352 }
353
354 #[test]
355 fn bundle_single_element_is_identity() {
356 let a = PhaseHVec::new_random(512, 256, 42);
357 let b = PhaseHVec::bundle(std::slice::from_ref(&a));
358 assert_eq!(
359 b.phases(),
360 a.phases(),
361 "bundling one vector must return its own phases"
362 );
363 }
364
365 #[test]
366 fn zero_dim_similarity_is_one() {
367 let a = PhaseHVec::from_phases(Vec::new(), 2);
369 let b = PhaseHVec::from_phases(Vec::new(), 2);
370 assert_eq!(a.dim(), 0);
371 assert!((a.similarity(&b) - 1.0).abs() < 1e-12);
372 }
373
374 #[test]
375 fn resolution_outside_2_to_65536_is_rejected_and_bounds_roundtrip() {
376 for (n, valid) in [
377 (1u32, false),
378 (2, true),
379 (3, true),
380 (65535, true),
381 (65536, true),
382 (65537, false),
383 ] {
384 let built = std::panic::catch_unwind(|| PhaseHVec::new_random(128, n, 3));
385 assert_eq!(built.is_ok(), valid, "n={n}");
386 if let Ok(a) = built {
387 let b = PhaseHVec::new_random(128, n, 4);
388 assert_eq!(a.n(), n, "n={n}");
389 assert_eq!(a.bind(&b).unbind(&b).phases(), a.phases(), "n={n}");
390 assert!((a.similarity(&a) - 1.0).abs() < 1e-9, "n={n}");
391 }
392 }
393 }
394
395 #[test]
396 fn phase_arithmetic_matches_modular_reference_at_wrap_boundaries() {
397 for n in [2, 3, 15, 16, 17, 255, 256, 257, 65535, 65536] {
398 let phases = [0, 1, n / 2, n - 2, n - 1];
399 for a in phases {
400 for b in phases {
401 let left = PhaseHVec::from_phases(vec![a as u16], n);
402 let right = PhaseHVec::from_phases(vec![b as u16], n);
403 assert_eq!(left.bind(&right).phases(), &[((a + b) % n) as u16]);
404 assert_eq!(left.unbind(&right).phases(), &[((a + n - b) % n) as u16]);
405 }
406 }
407 }
408 }
409
410 #[test]
411 fn similarity_matches_direct_cosine_for_all_phase_differences() {
412 for n in [2, 3, 15, 16, 17, 255, 256, 257, 65535, 65536] {
413 let left = PhaseHVec::from_phases((0..n).map(|p| p as u16).collect(), n);
414 let right = PhaseHVec::from_phases(vec![0; n as usize], n);
415 let reference = left
416 .phases()
417 .iter()
418 .map(|&p| (TAU * p as f64 / n as f64).cos())
419 .sum::<f64>()
420 / n as f64;
421 assert_eq!(left.similarity(&right).to_bits(), reference.to_bits());
422 assert_eq!(right.similarity(&left).to_bits(), reference.to_bits());
423 for pivot in [0, 1, n / 2, n - 2, n - 1] {
424 let right = PhaseHVec::from_phases(vec![pivot as u16; n as usize], n);
425 let reference = left
426 .phases()
427 .iter()
428 .map(|&p| (TAU * (p as f64 - pivot as f64) / n as f64).cos())
429 .sum::<f64>()
430 / n as f64;
431 assert_eq!(left.similarity(&right).to_bits(), reference.to_bits());
432 }
433 }
434 }
435
436 #[test]
437 fn bundles_match_direct_phasor_sums_at_all_resolutions() {
438 for n in [2, 3, 15, 16, 17, 255, 256, 257, 65535, 65536] {
439 let vectors: Vec<_> = (0..7)
440 .map(|seed| PhaseHVec::new_random(129, n, seed))
441 .collect();
442 for count in [1, 2, 3, 7] {
443 let reference: Vec<u16> = (0..129)
444 .map(|d| {
445 let (re, im) = vectors[..count].iter().fold((0i64, 0i64), |(re, im), v| {
446 let phase = TAU * v.phases()[d] as f64 / n as f64;
447 (
448 re + (SCALE as f64 * phase.cos()).round() as i64,
449 im + (SCALE as f64 * phase.sin()).round() as i64,
450 )
451 });
452 let angle = (im as f64).atan2(re as f64);
453 ((angle / TAU * n as f64).round() as i64).rem_euclid(n as i64) as u16
454 })
455 .collect();
456 assert_eq!(PhaseHVec::bundle(&vectors[..count]).phases(), reference);
457 }
458 }
459 }
460
461 #[test]
462 fn singleton_bundle_matches_fixed_point_roundtrip_for_every_phase() {
463 for n in [2, 3, 16, 256, 257, 65535, 65536] {
464 let phases = PhaseHVec::from_phases((0..n).map(|p| p as u16).collect(), n);
465 let reference: Vec<u16> = phases
466 .phases()
467 .iter()
468 .map(|&p| {
469 let angle = TAU * p as f64 / n as f64;
470 let re = (SCALE as f64 * angle.cos()).round() as i64;
471 let im = (SCALE as f64 * angle.sin()).round() as i64;
472 (((im as f64).atan2(re as f64) / TAU * n as f64).round() as i64)
473 .rem_euclid(n as i64) as u16
474 })
475 .collect();
476 assert_eq!(reference, phases.phases());
477 assert_eq!(PhaseHVec::bundle(&[&phases]).phases(), reference);
478 }
479 }
480
481 #[test]
482 fn bundle_keeps_coordinates_independent_at_accumulator_allocation_boundaries() {
483 for n in [16, 257, 65536] {
484 for dim in [511, 512, 513] {
485 let vectors: Vec<_> = (0..32)
486 .map(|seed| PhaseHVec::new_random(dim, n, seed))
487 .collect();
488 for count in [2, 8, 32] {
489 let reference: Vec<_> = (0..dim)
490 .map(|d| {
491 let (re, im) =
492 vectors[..count].iter().fold((0i64, 0i64), |(re, im), v| {
493 let phase = TAU * v.phases()[d] as f64 / n as f64;
494 (
495 re + (SCALE as f64 * phase.cos()).round() as i64,
496 im + (SCALE as f64 * phase.sin()).round() as i64,
497 )
498 });
499 (((im as f64).atan2(re as f64) / TAU * n as f64).round() as i64)
500 .rem_euclid(n as i64) as u16
501 })
502 .collect();
503 assert_eq!(PhaseHVec::bundle(&vectors[..count]).phases(), reference);
504 }
505 }
506 }
507 }
508
509 #[test]
510 fn binary_bundle_matches_phasor_sum_for_every_vote_split() {
511 let zero = PhaseHVec::from_phases(vec![0], 2);
512 let one = PhaseHVec::from_phases(vec![1], 2);
513 for count in [1, 2, 3, 7, 8, 9, 15, 16, 17, 31, 32, 33, 255, 256, 257] {
514 for ones in 0..=count {
515 let mut vectors = vec![&zero; count];
516 vectors[..ones].fill(&one);
517 let (re, im) = vectors.iter().fold((0i64, 0i64), |(re, im), v| {
518 let phase = TAU * v.phases()[0] as f64 / 2.0;
519 (
520 re + (SCALE as f64 * phase.cos()).round() as i64,
521 im + (SCALE as f64 * phase.sin()).round() as i64,
522 )
523 });
524 let reference = (((im as f64).atan2(re as f64) / TAU * 2.0).round() as i64)
525 .rem_euclid(2) as u16;
526 assert_eq!(PhaseHVec::bundle(&vectors).phases(), &[reference]);
527 }
528 }
529 }
530
531 #[test]
532 fn zero_dim_bundles_still_check_resolution() {
533 let vectors = [
534 PhaseHVec::from_phases(Vec::new(), 16),
535 PhaseHVec::from_phases(Vec::new(), 256),
536 ];
537 assert!(std::panic::catch_unwind(|| PhaseHVec::bundle(&vectors)).is_err());
538 }
539}