Skip to main content

writerslogic_warm/core/
phase_hvec.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Quantized-phase hypervector (qFHRR, arXiv 2604.25939).
5//!
6//! Stores D phases as indices in Z_N. Binding is phase-add mod N, unbinding is
7//! phase-sub mod N, similarity is the mean cosine of phase differences, and
8//! bundling superposes unit phasors (integer cos/sin LUT) and recovers a phase per
9//! dimension via `atan2`. The stored state is integer phases and the bundle is a
10//! deterministic integer fold of its inputs, so a phase-vector memory is an exact,
11//! replay-verifiable function of what was added -- similarity is a float readout at
12//! query time, exactly like [`super::entangled::EntangledHVec`].
13//!
14//! This is the deterministic substrate for the phase resonator
15//! ([`super::phase_resonator`]); §20 of `docs/PREREGISTRATION-binding-readout.md`
16//! shows it matches float FHRR factorization capacity down to 4-bit phase (N=16),
17//! and §15-18 characterise its continuous associative-memory behaviour. Unlike the
18//! sparse-binary core, phase binding is NOT self-inverse, so it does not leak
19//! pairing the way XOR does.
20
21use std::borrow::Borrow;
22use std::f64::consts::TAU;
23use std::sync::OnceLock;
24
25/// Fixed-point unit for the integer cos/sin LUT used in bundling.
26const SCALE: i64 = 1 << 14;
27
28// Caching power-of-two resolutions bounds the initialized table payload to less than 1.51 MiB.
29const 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
55/// Small tables avoid widening in the inner loop; large tables use exact `i16` components.
56fn 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    /// A random phase vector of `dim` dimensions at resolution `n` (phase levels),
103    /// deterministic in `seed`. `n` must be in `2..=65536`.
104    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    /// Construct from explicit phase indices; every phase must be `< n`.
116    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    /// Number of dimensions (phase components).
133    pub fn dim(&self) -> usize {
134        self.dim
135    }
136    /// Phase resolution: the number of discrete phase levels `N` (each phase is in
137    /// `0..n`).
138    pub fn n(&self) -> u32 {
139        self.n
140    }
141    /// The raw phase indices, one per dimension, each in `0..n`.
142    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    /// Binding: phase-add mod N. Non-self-inverse; inverse is [`Self::unbind`].
152    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    /// Unbinding: phase-sub mod N. `bind(a,b).unbind(b) == a` exactly.
172    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    /// Mean cosine of phase differences in `[-1, 1]`. 1.0 for identical vectors,
196    /// ~0 for independent ones. Float readout (query time).
197    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    /// Superpose (bundle) phase vectors: sum unit phasors via integer LUT, recover
220    /// a phase per dimension with `atan2`. Deterministic integer fold -> the result
221    /// is an exact, replay-verifiable function of the inputs (and their order does
222    /// not matter, addition being commutative). Empty input yields a 0-dim vector.
223    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        // XOR leaks because bind(a,a) collapses; phase-add does not.
308        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        // Empty vectors are vacuously identical (guarded in `similarity`).
368        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}