ruvector_sparse_inference/pi/
angular.rs

1//! Angular and hyperspherical embeddings with π phase encoding
2//!
3//! Many embedding tricks quietly reduce to angles. Cosine similarity is
4//! literally angle-based.
5//!
6//! Using π explicitly:
7//! - Map vectors to phase space
8//! - Encode direction as multiples of π
9//! - Track angular velocity instead of Euclidean distance
10//!
11//! This is extremely friendly to 5-bit and 7-bit systems because:
12//! - Angles saturate naturally
13//! - Wraparound is meaningful
14//! - Overflow becomes topology, not error
15//!
16//! That is exactly how biological systems avoid numeric explosion.
17
18use crate::precision::PrecisionLane;
19use std::f32::consts::PI;
20
21/// Angular embedding projector
22#[derive(Debug, Clone)]
23pub struct AngularEmbedding {
24    /// Precision lane
25    lane: PrecisionLane,
26    /// Dimension of embeddings
27    dimension: usize,
28    /// Phase scale (π / max_value for lane)
29    phase_scale: f32,
30    /// Angular velocity accumulator
31    velocity: Vec<f32>,
32}
33
34impl AngularEmbedding {
35    /// Create a new angular embedding projector
36    pub fn new(lane: PrecisionLane) -> Self {
37        let phase_scale = match lane {
38            PrecisionLane::Bit3 => PI / 4.0,
39            PrecisionLane::Bit5 => PI / 16.0,
40            PrecisionLane::Bit7 => PI / 64.0,
41            PrecisionLane::Float32 => 1.0,
42        };
43
44        Self {
45            lane,
46            dimension: 0,
47            phase_scale,
48            velocity: Vec::new(),
49        }
50    }
51
52    /// Project Euclidean vector to angular space
53    pub fn project(&self, values: &[f32]) -> Vec<f32> {
54        // Compute magnitude for normalization
55        let magnitude = values.iter()
56            .map(|x| x * x)
57            .sum::<f32>()
58            .sqrt()
59            .max(1e-10);
60
61        // Project to unit hypersphere, then to angles
62        values.iter()
63            .map(|&x| {
64                let normalized = x / magnitude;
65                // Map [-1, 1] to [-π, π] with phase scale
66                normalized * PI * self.phase_scale
67            })
68            .collect()
69    }
70
71    /// Unproject from angular space to Euclidean
72    pub fn unproject(&self, angles: &[f32], target_magnitude: f32) -> Vec<f32> {
73        angles.iter()
74            .map(|&angle| {
75                let normalized = angle / (PI * self.phase_scale);
76                normalized * target_magnitude
77            })
78            .collect()
79    }
80
81    /// Compute angular distance between two vectors
82    pub fn angular_distance(&self, a: &[f32], b: &[f32]) -> f32 {
83        if a.len() != b.len() || a.is_empty() {
84            return f32::MAX;
85        }
86
87        let angles_a = self.project(a);
88        let angles_b = self.project(b);
89
90        // Sum of angular differences (with wraparound handling)
91        let mut total_distance = 0.0f32;
92        for (&a, &b) in angles_a.iter().zip(angles_b.iter()) {
93            let diff = (a - b).abs();
94            // Handle wraparound: use shorter arc
95            let wrapped_diff = if diff > PI { 2.0 * PI - diff } else { diff };
96            total_distance += wrapped_diff * wrapped_diff;
97        }
98
99        total_distance.sqrt()
100    }
101
102    /// Update angular velocity (for streaming embeddings)
103    pub fn update_velocity(&mut self, previous: &[f32], current: &[f32]) {
104        if previous.len() != current.len() {
105            return;
106        }
107
108        let prev_angles = self.project(previous);
109        let curr_angles = self.project(current);
110
111        if self.velocity.is_empty() {
112            self.velocity = vec![0.0; current.len()];
113            self.dimension = current.len();
114        }
115
116        // Compute angular velocity (with momentum)
117        let momentum = 0.9f32;
118        for i in 0..self.dimension.min(self.velocity.len()) {
119            let delta = curr_angles[i] - prev_angles[i];
120            // Handle wraparound
121            let wrapped_delta = if delta > PI {
122                delta - 2.0 * PI
123            } else if delta < -PI {
124                delta + 2.0 * PI
125            } else {
126                delta
127            };
128            self.velocity[i] = momentum * self.velocity[i] + (1.0 - momentum) * wrapped_delta;
129        }
130    }
131
132    /// Get current angular velocity
133    pub fn get_velocity(&self) -> &[f32] {
134        &self.velocity
135    }
136
137    /// Predict next position based on angular velocity
138    pub fn predict_next(&self, current: &[f32]) -> Vec<f32> {
139        let angles = self.project(current);
140        if self.velocity.is_empty() {
141            return current.to_vec();
142        }
143
144        let predicted_angles: Vec<f32> = angles.iter()
145            .zip(self.velocity.iter())
146            .map(|(&a, &v)| {
147                let mut next = a + v;
148                // Wrap to [-π, π]
149                while next > PI { next -= 2.0 * PI; }
150                while next < -PI { next += 2.0 * PI; }
151                next
152            })
153            .collect();
154
155        // Unproject with original magnitude
156        let magnitude = current.iter().map(|x| x * x).sum::<f32>().sqrt();
157        self.unproject(&predicted_angles, magnitude)
158    }
159}
160
161/// Phase encoder for quantized values
162#[derive(Debug, Clone)]
163pub struct PhaseEncoder {
164    /// Base frequency (multiples of π)
165    base_frequency: f32,
166    /// Number of harmonics
167    harmonics: usize,
168    /// Lookup table for fast encoding
169    lut: Option<Vec<f32>>,
170}
171
172impl PhaseEncoder {
173    /// Create a new phase encoder
174    pub fn new(base_frequency: f32, harmonics: usize) -> Self {
175        Self {
176            base_frequency,
177            harmonics,
178            lut: None,
179        }
180    }
181
182    /// Initialize lookup table for given quantization levels
183    pub fn with_lut(mut self, levels: usize) -> Self {
184        let mut lut = Vec::with_capacity(levels);
185        for i in 0..levels {
186            let normalized = (i as f32) / (levels - 1) as f32;
187            let phase = normalized * 2.0 * PI * self.base_frequency;
188            lut.push(phase.sin());
189        }
190        self.lut = Some(lut);
191        self
192    }
193
194    /// Encode value to phase
195    pub fn encode(&self, value: f32) -> f32 {
196        let mut encoded = 0.0f32;
197        for h in 0..self.harmonics {
198            let freq = self.base_frequency * (h + 1) as f32;
199            let weight = 1.0 / (h + 1) as f32; // Harmonic weights
200            encoded += weight * (value * freq * PI).sin();
201        }
202        encoded
203    }
204
205    /// Encode quantized value using LUT
206    pub fn encode_quantized(&self, level: usize) -> f32 {
207        if let Some(ref lut) = self.lut {
208            lut.get(level).copied().unwrap_or(0.0)
209        } else {
210            let normalized = level as f32 / 255.0; // Assume 8-bit max
211            self.encode(normalized)
212        }
213    }
214
215    /// Decode phase to approximate value
216    pub fn decode(&self, phase: f32) -> f32 {
217        // Inverse is approximate (lossy)
218        phase.asin() / (self.base_frequency * PI)
219    }
220}
221
222/// Hyperspherical projection for high-dimensional embeddings
223#[derive(Debug, Clone)]
224pub struct HypersphericalProjection {
225    /// Input dimension
226    input_dim: usize,
227    /// Output spherical coordinates (n-1 angles for n dimensions)
228    output_dim: usize,
229    /// Precision lane
230    lane: PrecisionLane,
231}
232
233impl HypersphericalProjection {
234    /// Create a new hyperspherical projection
235    pub fn new(dimension: usize, lane: PrecisionLane) -> Self {
236        Self {
237            input_dim: dimension,
238            output_dim: dimension.saturating_sub(1),
239            lane,
240        }
241    }
242
243    /// Project Cartesian coordinates to hyperspherical (angles)
244    pub fn to_spherical(&self, cartesian: &[f32]) -> Vec<f32> {
245        if cartesian.len() < 2 {
246            return vec![];
247        }
248
249        let n = cartesian.len();
250        let mut angles = Vec::with_capacity(n - 1);
251
252        // Radius (for reference, not returned)
253        let r = cartesian.iter().map(|x| x * x).sum::<f32>().sqrt();
254        if r < 1e-10 {
255            return vec![0.0; n - 1];
256        }
257
258        // Compute angles from the last coordinate backward
259        // φ₁ = arctan2(x₂, x₁)
260        // φₖ = arccos(xₖ₊₁ / √(xₖ₊₁² + ... + xₙ²)) for k > 1
261
262        // First angle (azimuthal)
263        let phi_1 = cartesian[1].atan2(cartesian[0]);
264        angles.push(phi_1);
265
266        // Remaining angles (polar)
267        for k in 1..(n - 1) {
268            let tail_sum: f32 = cartesian[k..].iter().map(|x| x * x).sum();
269            let tail_r = tail_sum.sqrt();
270            if tail_r < 1e-10 {
271                angles.push(0.0);
272            } else {
273                let phi_k = (cartesian[k] / tail_r).clamp(-1.0, 1.0).acos();
274                angles.push(phi_k);
275            }
276        }
277
278        angles
279    }
280
281    /// Project hyperspherical coordinates back to Cartesian
282    pub fn to_cartesian(&self, angles: &[f32], radius: f32) -> Vec<f32> {
283        if angles.is_empty() {
284            return vec![];
285        }
286
287        let n = angles.len() + 1;
288        let mut cartesian = Vec::with_capacity(n);
289
290        // x₁ = r * sin(φₙ₋₁) * ... * sin(φ₂) * cos(φ₁)
291        // x₂ = r * sin(φₙ₋₁) * ... * sin(φ₂) * sin(φ₁)
292        // xₖ = r * sin(φₙ₋₁) * ... * sin(φₖ) * cos(φₖ₋₁) for k > 2
293        // xₙ = r * cos(φₙ₋₁)
294
295        let mut sin_product = radius;
296        for &angle in angles.iter().rev().skip(1) {
297            sin_product *= angle.sin();
298        }
299
300        // First two coordinates
301        cartesian.push(sin_product * angles[0].cos());
302        cartesian.push(sin_product * angles[0].sin());
303
304        // Remaining coordinates
305        sin_product = radius;
306        for i in (1..angles.len()).rev() {
307            sin_product *= angles[i].sin();
308            cartesian.push(sin_product * angles[i - 1].cos());
309        }
310
311        // Last coordinate
312        cartesian.push(radius * angles.last().unwrap_or(&0.0).cos());
313
314        // Note: reconstruction may not be perfect for all inputs
315        cartesian.truncate(n);
316        cartesian
317    }
318
319    /// Compute geodesic distance on hypersphere
320    pub fn geodesic_distance(&self, a: &[f32], b: &[f32]) -> f32 {
321        if a.len() != b.len() || a.is_empty() {
322            return f32::MAX;
323        }
324
325        // Normalize to unit sphere
326        let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
327        let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
328
329        // Compute dot product of normalized vectors
330        let dot: f32 = a.iter()
331            .zip(b.iter())
332            .map(|(&x, &y)| (x / norm_a) * (y / norm_b))
333            .sum();
334
335        // Geodesic distance = arccos(dot product)
336        dot.clamp(-1.0, 1.0).acos()
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_angular_embedding_project() {
346        let embedding = AngularEmbedding::new(PrecisionLane::Bit5);
347        let values = vec![1.0, 2.0, 3.0, 4.0];
348        let angles = embedding.project(&values);
349
350        assert_eq!(angles.len(), values.len());
351        // All angles should be within bounds
352        for &angle in &angles {
353            assert!(angle.abs() <= PI);
354        }
355    }
356
357    #[test]
358    fn test_angular_embedding_roundtrip() {
359        let embedding = AngularEmbedding::new(PrecisionLane::Bit7);
360        let values = vec![1.0, 2.0, 3.0, 4.0];
361        let magnitude = values.iter().map(|x| x * x).sum::<f32>().sqrt();
362
363        let angles = embedding.project(&values);
364        let recovered = embedding.unproject(&angles, magnitude);
365
366        // Should approximately recover original
367        for (&orig, &rec) in values.iter().zip(recovered.iter()) {
368            assert!((orig - rec).abs() < 0.1, "orig={}, rec={}", orig, rec);
369        }
370    }
371
372    #[test]
373    fn test_angular_distance() {
374        let embedding = AngularEmbedding::new(PrecisionLane::Bit5);
375
376        let a = vec![1.0, 0.0, 0.0];
377        let b = vec![0.0, 1.0, 0.0];
378        let c = vec![1.0, 0.0, 0.0];
379
380        let dist_ab = embedding.angular_distance(&a, &b);
381        let dist_ac = embedding.angular_distance(&a, &c);
382
383        assert!(dist_ac < 0.001); // Same vectors
384        assert!(dist_ab > 0.0);   // Different vectors
385    }
386
387    #[test]
388    fn test_phase_encoder() {
389        let encoder = PhaseEncoder::new(1.0, 3);
390
391        let e1 = encoder.encode(0.0);
392        let e2 = encoder.encode(0.5);
393        let e3 = encoder.encode(1.0);
394
395        // Different inputs should produce different outputs
396        assert!(e1 != e2);
397        assert!(e2 != e3);
398    }
399
400    #[test]
401    fn test_phase_encoder_lut() {
402        let encoder = PhaseEncoder::new(1.0, 1).with_lut(16);
403
404        let e1 = encoder.encode_quantized(0);
405        let e2 = encoder.encode_quantized(8);
406        let e3 = encoder.encode_quantized(15);
407
408        assert!(e1 != e2);
409        assert!(e2 != e3);
410    }
411
412    #[test]
413    fn test_hyperspherical_projection() {
414        let proj = HypersphericalProjection::new(3, PrecisionLane::Bit5);
415
416        let cartesian = vec![1.0, 0.0, 0.0];
417        let spherical = proj.to_spherical(&cartesian);
418
419        assert_eq!(spherical.len(), 2);
420    }
421
422    #[test]
423    fn test_geodesic_distance() {
424        let proj = HypersphericalProjection::new(3, PrecisionLane::Bit5);
425
426        let a = vec![1.0, 0.0, 0.0];
427        let b = vec![0.0, 1.0, 0.0];
428        let c = vec![1.0, 0.0, 0.0];
429
430        let dist_ab = proj.geodesic_distance(&a, &b);
431        let dist_ac = proj.geodesic_distance(&a, &c);
432
433        assert!(dist_ac < 0.001); // Same direction
434        assert!((dist_ab - PI / 2.0).abs() < 0.001); // Orthogonal = π/2
435    }
436}