Skip to main content

holographic_memory/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;
23
24/// Fixed-point unit for the integer cos/sin LUT used in bundling.
25const SCALE: i64 = 1 << 14;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct PhaseHVec {
29    dim: usize,
30    n: u32,
31    phases: Vec<u16>,
32}
33
34fn mix(x: u64, k: u64) -> u64 {
35    let mut h = x.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ k.wrapping_mul(0xC2B2_AE3D_27D4_EB4F);
36    h ^= h >> 29;
37    h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
38    h ^= h >> 32;
39    h
40}
41
42/// Integer cos/sin lookup tables of length `n`, scaled by `SCALE`.
43fn luts(n: u32) -> (Vec<i64>, Vec<i64>) {
44    let cos = (0..n)
45        .map(|k| (SCALE as f64 * (TAU * k as f64 / n as f64).cos()).round() as i64)
46        .collect();
47    let sin = (0..n)
48        .map(|k| (SCALE as f64 * (TAU * k as f64 / n as f64).sin()).round() as i64)
49        .collect();
50    (cos, sin)
51}
52
53impl PhaseHVec {
54    /// A random phase vector of `dim` dimensions at resolution `n` (phase levels),
55    /// deterministic in `seed`. `n` must be in `2..=65536`.
56    pub fn new_random(dim: usize, n: u32, seed: u64) -> Self {
57        assert!(
58            (2..=65536).contains(&n),
59            "phase resolution n must be in 2..=65536"
60        );
61        let phases = (0..dim)
62            .map(|d| (mix(d as u64, seed) % n as u64) as u16)
63            .collect();
64        Self { dim, n, phases }
65    }
66
67    /// Construct from explicit phase indices; every phase must be `< n`.
68    pub fn from_phases(phases: Vec<u16>, n: u32) -> Self {
69        assert!(
70            (2..=65536).contains(&n),
71            "phase resolution n must be in 2..=65536"
72        );
73        assert!(
74            phases.iter().all(|&p| (p as u32) < n),
75            "phase index out of range"
76        );
77        Self {
78            dim: phases.len(),
79            n,
80            phases,
81        }
82    }
83
84    /// Number of dimensions (phase components).
85    pub fn dim(&self) -> usize {
86        self.dim
87    }
88    /// Phase resolution: the number of discrete phase levels `N` (each phase is in
89    /// `0..n`).
90    pub fn n(&self) -> u32 {
91        self.n
92    }
93    /// The raw phase indices, one per dimension, each in `0..n`.
94    pub fn phases(&self) -> &[u16] {
95        &self.phases
96    }
97
98    fn check(&self, other: &Self) {
99        assert_eq!(self.dim, other.dim, "phase-vector dimension mismatch");
100        assert_eq!(self.n, other.n, "phase-vector resolution mismatch");
101    }
102
103    /// Binding: phase-add mod N. Non-self-inverse; inverse is [`Self::unbind`].
104    pub fn bind(&self, other: &Self) -> Self {
105        self.check(other);
106        let n = self.n;
107        let phases = self
108            .phases
109            .iter()
110            .zip(&other.phases)
111            .map(|(&a, &b)| ((a as u32 + b as u32) % n) as u16)
112            .collect();
113        Self {
114            dim: self.dim,
115            n: self.n,
116            phases,
117        }
118    }
119
120    /// Unbinding: phase-sub mod N. `bind(a,b).unbind(b) == a` exactly.
121    pub fn unbind(&self, other: &Self) -> Self {
122        self.check(other);
123        let n = self.n;
124        let phases = self
125            .phases
126            .iter()
127            .zip(&other.phases)
128            .map(|(&a, &b)| ((a as u32 + n - b as u32) % n) as u16)
129            .collect();
130        Self {
131            dim: self.dim,
132            n: self.n,
133            phases,
134        }
135    }
136
137    /// Mean cosine of phase differences in `[-1, 1]`. 1.0 for identical vectors,
138    /// ~0 for independent ones. Float readout (query time).
139    pub fn similarity(&self, other: &Self) -> f64 {
140        self.check(other);
141        if self.dim == 0 {
142            return 1.0;
143        }
144        let n = self.n as f64;
145        let s: f64 = self
146            .phases
147            .iter()
148            .zip(&other.phases)
149            .map(|(&a, &b)| (TAU * (a as f64 - b as f64) / n).cos())
150            .sum();
151        s / self.dim as f64
152    }
153
154    /// Superpose (bundle) phase vectors: sum unit phasors via integer LUT, recover
155    /// a phase per dimension with `atan2`. Deterministic integer fold -> the result
156    /// is an exact, replay-verifiable function of the inputs (and their order does
157    /// not matter, addition being commutative). Empty input yields a 0-dim vector.
158    pub fn bundle<V: Borrow<Self>>(vectors: &[V]) -> Self {
159        if vectors.is_empty() {
160            return Self {
161                dim: 0,
162                n: 2,
163                phases: Vec::new(),
164            };
165        }
166        let first = vectors[0].borrow();
167        let (dim, n) = (first.dim, first.n);
168        let (cos, sin) = luts(n);
169        let mut re = vec![0i64; dim];
170        let mut im = vec![0i64; dim];
171        for v in vectors {
172            let v = v.borrow();
173            assert_eq!(v.dim, dim, "phase-vector dimension mismatch in bundle");
174            assert_eq!(v.n, n, "phase-vector resolution mismatch in bundle");
175            for d in 0..dim {
176                re[d] += cos[v.phases[d] as usize];
177                im[d] += sin[v.phases[d] as usize];
178            }
179        }
180        let phases = (0..dim)
181            .map(|d| {
182                let ang = (im[d] as f64).atan2(re[d] as f64);
183                let idx = (ang / TAU * n as f64).round() as i64;
184                idx.rem_euclid(n as i64) as u16
185            })
186            .collect();
187        Self { dim, n, phases }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn bind_unbind_is_exact_roundtrip() {
197        let a = PhaseHVec::new_random(512, 256, 1);
198        let b = PhaseHVec::new_random(512, 256, 2);
199        let recovered = a.bind(&b).unbind(&b);
200        assert_eq!(
201            recovered.phases(),
202            a.phases(),
203            "unbind(bind(a,b),b) must equal a"
204        );
205    }
206
207    #[test]
208    fn similarity_self_is_one_random_near_zero() {
209        let a = PhaseHVec::new_random(4096, 256, 7);
210        assert!((a.similarity(&a) - 1.0).abs() < 1e-9);
211        let b = PhaseHVec::new_random(4096, 256, 8);
212        assert!(
213            a.similarity(&b).abs() < 0.05,
214            "independent vectors ~orthogonal"
215        );
216    }
217
218    #[test]
219    fn binding_is_not_self_inverse() {
220        // XOR leaks because bind(a,a) collapses; phase-add does not.
221        let a = PhaseHVec::new_random(2048, 256, 3);
222        let self_bound = a.bind(&a);
223        assert!(
224            self_bound.similarity(&a).abs() < 0.2,
225            "phase self-bind must not collapse back to a (non-self-inverse)"
226        );
227    }
228
229    #[test]
230    fn bundle_preserves_membership() {
231        let members: Vec<PhaseHVec> = (0..5)
232            .map(|i| PhaseHVec::new_random(4096, 256, 100 + i))
233            .collect();
234        let bundle = PhaseHVec::bundle(&members);
235        let member_sim = bundle.similarity(&members[0]);
236        let nonmember = PhaseHVec::new_random(4096, 256, 999);
237        assert!(
238            member_sim > bundle.similarity(&nonmember) + 0.05,
239            "a bundled member ({member_sim:.3}) must score above a non-member"
240        );
241    }
242
243    #[test]
244    fn bundle_is_deterministic_and_order_free() {
245        let v: Vec<PhaseHVec> = (0..4)
246            .map(|i| PhaseHVec::new_random(256, 64, 10 + i))
247            .collect();
248        let ab = PhaseHVec::bundle(&v);
249        let mut rev = v.clone();
250        rev.reverse();
251        let ba = PhaseHVec::bundle(&rev);
252        assert_eq!(
253            ab.phases(),
254            ba.phases(),
255            "bundle must be order-independent and deterministic"
256        );
257    }
258
259    #[test]
260    fn bundle_empty_is_zero_dim() {
261        let empty: Vec<PhaseHVec> = Vec::new();
262        let b = PhaseHVec::bundle(&empty);
263        assert_eq!(b.dim(), 0);
264        assert!(b.phases().is_empty());
265    }
266
267    #[test]
268    fn bundle_single_element_is_identity() {
269        let a = PhaseHVec::new_random(512, 256, 42);
270        let b = PhaseHVec::bundle(std::slice::from_ref(&a));
271        assert_eq!(
272            b.phases(),
273            a.phases(),
274            "bundling one vector must return its own phases"
275        );
276    }
277
278    #[test]
279    fn zero_dim_similarity_is_one() {
280        // Empty vectors are vacuously identical (guarded in `similarity`).
281        let a = PhaseHVec::from_phases(Vec::new(), 2);
282        let b = PhaseHVec::from_phases(Vec::new(), 2);
283        assert_eq!(a.dim(), 0);
284        assert!((a.similarity(&b) - 1.0).abs() < 1e-12);
285    }
286
287    #[test]
288    fn minimum_resolution_roundtrip() {
289        // N=2 (1-bit phase) is the substrate minimum; bind/unbind must stay exact.
290        let a = PhaseHVec::new_random(256, 2, 3);
291        let b = PhaseHVec::new_random(256, 2, 4);
292        assert_eq!(a.bind(&b).unbind(&b).phases(), a.phases());
293    }
294
295    #[test]
296    fn maximum_resolution_constructs() {
297        // N at the upper bound (65536) must construct and self-match.
298        let a = PhaseHVec::new_random(128, 65536, 7);
299        assert_eq!(a.n(), 65536);
300        assert!((a.similarity(&a) - 1.0).abs() < 1e-9);
301    }
302}