Skip to main content

wm_core/
coords.rs

1//! Holographic Coordinates — 6D spatial-temporal memory addressing
2//!
3//! Each memory has 6D holographic coordinates enabling spatial queries
4//! via LMDB cursor range scans. Preserved from v2.
5//!
6//! Also provides `Coordinate5D` — a 5D holographic coordinate system
7//! (x, y, z, w, v) ported from v2 for spatial memory indexing and
8//! constellation clustering.
9
10use serde::{Deserialize, Serialize};
11
12/// 6D holographic coordinate for a memory.
13///
14/// Stored as a composite LMDB key enabling spatial range queries.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct HolographicCoords {
17    /// Which of 14 galaxies (0-13)
18    pub galaxy: u8,
19    /// Spatial sector within galaxy (0-65535)
20    pub sector: u16,
21    /// Radial distance from center (0.0 = center, 1.0 = edge)
22    pub radial: f32,
23    /// Angular position in radians (0 to 2π)
24    pub angular: f32,
25    /// Temporal coordinate (Unix timestamp in microseconds)
26    pub temporal: u64,
27    /// Consciousness resonance frequency (0.0 to 1.0)
28    pub consciousness: f32,
29}
30
31impl HolographicCoords {
32    /// Create new coordinates for a memory in the given galaxy.
33    #[must_use]
34    pub fn new(galaxy: crate::Galaxy, temporal: u64) -> Self {
35        use std::time::{SystemTime, UNIX_EPOCH};
36        let now = SystemTime::now()
37            .duration_since(UNIX_EPOCH)
38            .unwrap_or_default()
39            .as_micros() as u64;
40        Self {
41            galaxy: galaxy as u8,
42            sector: 0,
43            radial: 0.5,
44            angular: 0.0,
45            temporal: temporal.max(now),
46            consciousness: 0.5,
47        }
48    }
49
50    /// Encode as a sortable composite key for LMDB.
51    ///
52    /// Format: galaxy(1) + sector(2) + temporal(8) + radial(4) + angular(4) + consciousness(4)
53    /// = 23 bytes total
54    #[must_use]
55    pub fn encode_key(&self) -> Vec<u8> {
56        let mut key = Vec::with_capacity(23);
57        key.push(self.galaxy);
58        key.extend_from_slice(&self.sector.to_be_bytes());
59        key.extend_from_slice(&self.temporal.to_be_bytes());
60        key.extend_from_slice(&self.radial.to_be_bytes());
61        key.extend_from_slice(&self.angular.to_be_bytes());
62        key.extend_from_slice(&self.consciousness.to_be_bytes());
63        key
64    }
65
66    /// Decode from a composite key.
67    #[must_use]
68    pub const fn decode_key(key: &[u8]) -> Option<Self> {
69        if key.len() < 23 {
70            return None;
71        }
72        Some(Self {
73            galaxy: key[0],
74            sector: u16::from_be_bytes([key[1], key[2]]),
75            temporal: u64::from_be_bytes([
76                key[3], key[4], key[5], key[6], key[7], key[8], key[9], key[10],
77            ]),
78            radial: f32::from_be_bytes([key[11], key[12], key[13], key[14]]),
79            angular: f32::from_be_bytes([key[15], key[16], key[17], key[18]]),
80            consciousness: f32::from_be_bytes([key[19], key[20], key[21], key[22]]),
81        })
82    }
83
84    /// Compute the galactic distance between two coordinates.
85    ///
86    /// This is a weighted distance metric combining radial, angular,
87    /// temporal, and consciousness dimensions.
88    #[must_use]
89    pub fn distance_to(&self, other: &Self) -> f32 {
90        let radial_diff = (self.radial - other.radial).abs();
91        let angular_diff = {
92            let d = (self.angular - other.angular).abs();
93            let pi2 = std::f32::consts::TAU;
94            d.min(pi2 - d) / pi2 // normalized to [0, 0.5]
95        };
96        let temporal_diff = {
97            let max_ts = self.temporal.max(other.temporal);
98            let min_ts = self.temporal.min(other.temporal);
99            if max_ts == 0 {
100                0.0
101            } else {
102                (max_ts - min_ts) as f32 / max_ts as f32
103            }
104        };
105        let consciousness_diff = (self.consciousness - other.consciousness).abs();
106
107        // Weighted additive distance (v2 used multiplicative which was buggy)
108        consciousness_diff.mul_add(
109            0.2,
110            radial_diff.mul_add(0.3, angular_diff * 0.2) + temporal_diff * 0.3,
111        )
112    }
113}
114
115// ── 5D Holographic Coordinate System ─────────────────────────────────
116
117/// Spatial zone within the holographic memory space.
118///
119/// Determines the radial region a coordinate falls into, used by
120/// constellation detection for clustering nearby memories.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122pub enum Zone {
123    /// Innermost zone (radial < 0.2) — core memories
124    Core,
125    /// Inner ring (0.2 ≤ radial < 0.4) — frequently accessed
126    InnerRing,
127    /// Middle ring (0.4 ≤ radial < 0.6) — standard memories
128    MidRing,
129    /// Outer ring (0.6 ≤ radial < 0.8) — peripheral memories
130    OuterRing,
131    /// Far edge (radial ≥ 0.8) — fading/archival memories
132    FarEdge,
133}
134
135impl Zone {
136    /// Classify a radial value into a zone.
137    #[must_use]
138    pub fn from_radial(radial: f32) -> Self {
139        match radial {
140            r if r < 0.2 => Self::Core,
141            r if r < 0.4 => Self::InnerRing,
142            r if r < 0.6 => Self::MidRing,
143            r if r < 0.8 => Self::OuterRing,
144            _ => Self::FarEdge,
145        }
146    }
147
148    /// Numeric zone index (0 = Core, 4 = `FarEdge`).
149    #[must_use]
150    pub const fn index(self) -> u8 {
151        self as u8
152    }
153
154    /// Human-readable name.
155    #[must_use]
156    pub const fn name(self) -> &'static str {
157        match self {
158            Self::Core => "core",
159            Self::InnerRing => "inner_ring",
160            Self::MidRing => "mid_ring",
161            Self::OuterRing => "outer_ring",
162            Self::FarEdge => "far_edge",
163        }
164    }
165}
166
167/// 5D holographic coordinate for spatial memory indexing.
168///
169/// Ported from v2's Rust implementation. Each dimension is normalized
170/// to [0.0, 1.0] and derived deterministically from content via
171/// `Coordinate5D::encode(text)`.
172///
173/// - **x**: semantic axis (content hash byte 0-3)
174/// - **y**: semantic axis (content hash byte 4-7)
175/// - **z**: semantic axis (content hash byte 8-11)
176/// - **w**: temporal weight (recency-based)
177/// - **v**: consciousness resonance (importance-based)
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct Coordinate5D {
180    /// Semantic axis X (content-derived)
181    pub x: f32,
182    /// Semantic axis Y (content-derived)
183    pub y: f32,
184    /// Semantic axis Z (content-derived)
185    pub z: f32,
186    /// Temporal weight (0 = old, 1 = recent)
187    pub w: f32,
188    /// Consciousness resonance (0 = low importance, 1 = high)
189    pub v: f32,
190}
191
192impl Coordinate5D {
193    /// Create a coordinate from explicit values.
194    #[must_use]
195    pub const fn new(x: f32, y: f32, z: f32, w: f32, v: f32) -> Self {
196        Self {
197            x: x.clamp(0.0, 1.0),
198            y: y.clamp(0.0, 1.0),
199            z: z.clamp(0.0, 1.0),
200            w: w.clamp(0.0, 1.0),
201            v: v.clamp(0.0, 1.0),
202        }
203    }
204
205    /// Deterministically encode text content into a 5D coordinate.
206    ///
207    /// Uses SHA-256 of the text to derive x, y, z dimensions.
208    /// The w (temporal) and v (consciousness) dimensions default to
209    /// 0.5 and should be updated when the memory is created with
210    /// actual temporal and importance data.
211    #[must_use]
212    pub fn encode(text: &str) -> Self {
213        use sha2::{Digest, Sha256};
214        let hash = Sha256::digest(text.as_bytes());
215        let x = u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]) as f32 / u32::MAX as f32;
216        let y = u32::from_be_bytes([hash[4], hash[5], hash[6], hash[7]]) as f32 / u32::MAX as f32;
217        let z = u32::from_be_bytes([hash[8], hash[9], hash[10], hash[11]]) as f32 / u32::MAX as f32;
218        Self {
219            x,
220            y,
221            z,
222            w: 0.5,
223            v: 0.5,
224        }
225    }
226
227    /// Create a coordinate from pre-computed semantic scores.
228    ///
229    /// Unlike `encode()` which uses SHA-256 hash bytes (semantically meaningless),
230    /// this constructor accepts x/y/z values derived from semantic analysis of
231    /// the content (e.g., TF-IDF anchor projection). This enables similar content
232    /// to produce similar coordinates.
233    #[must_use]
234    pub const fn from_semantic(
235        x: f32,
236        y: f32,
237        z: f32,
238        temporal_weight: f32,
239        importance: f32,
240    ) -> Self {
241        Self {
242            x: x.clamp(0.0, 1.0),
243            y: y.clamp(0.0, 1.0),
244            z: z.clamp(0.0, 1.0),
245            w: temporal_weight.clamp(0.0, 1.0),
246            v: importance.clamp(0.0, 1.0),
247        }
248    }
249
250    /// Encode text with temporal and importance context.
251    #[must_use]
252    pub fn encode_with_context(text: &str, temporal_weight: f32, importance: f32) -> Self {
253        let mut coord = Self::encode(text);
254        coord.w = temporal_weight.clamp(0.0, 1.0);
255        coord.v = importance.clamp(0.0, 1.0);
256        coord
257    }
258
259    /// Euclidean distance in 5D space.
260    #[must_use]
261    pub fn distance_to(&self, other: &Self) -> f32 {
262        let dx = self.x - other.x;
263        let dy = self.y - other.y;
264        let dz = self.z - other.z;
265        let dw = self.w - other.w;
266        let dv = self.v - other.v;
267        dv.mul_add(dv, dw.mul_add(dw, dz.mul_add(dz, dx.mul_add(dx, dy * dy))))
268            .sqrt()
269    }
270
271    /// Weighted distance — emphasizes semantic dimensions (x, y, z)
272    /// over temporal (w) and consciousness (v).
273    #[must_use]
274    pub fn semantic_distance_to(&self, other: &Self) -> f32 {
275        let dx = (self.x - other.x) * 0.35;
276        let dy = (self.y - other.y) * 0.35;
277        let dz = (self.z - other.z) * 0.20;
278        let dw = (self.w - other.w) * 0.05;
279        let dv = (self.v - other.v) * 0.05;
280        (dx * dx + dy * dy + dz * dz + dw * dw + dv * dv).sqrt()
281    }
282
283    /// Which zone this coordinate falls into, based on radial distance
284    /// from the center (0.5, 0.5, 0.5, 0.5, 0.5).
285    #[must_use]
286    pub fn zone(&self) -> Zone {
287        let center = Self::new(0.5, 0.5, 0.5, 0.5, 0.5);
288        let radial = self.distance_to(&center) / 1.118; // sqrt(5 * 0.25) = max distance
289        Zone::from_radial(radial)
290    }
291
292    /// Encode as a sortable composite key for LMDB (20 bytes).
293    #[must_use]
294    pub fn encode_key(&self) -> Vec<u8> {
295        let mut key = Vec::with_capacity(20);
296        key.extend_from_slice(&self.x.to_be_bytes());
297        key.extend_from_slice(&self.y.to_be_bytes());
298        key.extend_from_slice(&self.z.to_be_bytes());
299        key.extend_from_slice(&self.w.to_be_bytes());
300        key.extend_from_slice(&self.v.to_be_bytes());
301        key
302    }
303
304    /// Decode from a composite key.
305    #[must_use]
306    pub const fn decode_key(key: &[u8]) -> Option<Self> {
307        if key.len() < 20 {
308            return None;
309        }
310        Some(Self {
311            x: f32::from_be_bytes([key[0], key[1], key[2], key[3]]),
312            y: f32::from_be_bytes([key[4], key[5], key[6], key[7]]),
313            z: f32::from_be_bytes([key[8], key[9], key[10], key[11]]),
314            w: f32::from_be_bytes([key[12], key[13], key[14], key[15]]),
315            v: f32::from_be_bytes([key[16], key[17], key[18], key[19]]),
316        })
317    }
318}
319
320/// Find nearby coordinates within a radius.
321///
322/// Returns indices of memories whose 5D coordinate is within `radius`
323/// of the `center` coordinate, sorted by distance (nearest first).
324#[must_use]
325pub fn find_nearby(
326    center: &Coordinate5D,
327    candidates: &[(usize, Coordinate5D)],
328    radius: f32,
329) -> Vec<(usize, f32)> {
330    let mut results: Vec<(usize, f32)> = candidates
331        .iter()
332        .map(|(idx, coord)| (*idx, center.distance_to(coord)))
333        .filter(|(_, dist)| *dist <= radius)
334        .collect();
335    results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
336    results
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn coords_encode_decode_roundtrip() {
345        let coords = HolographicCoords {
346            galaxy: 3,
347            sector: 42,
348            radial: 0.75,
349            angular: 1.5,
350            temporal: 1700000000,
351            consciousness: 0.8,
352        };
353        let key = coords.encode_key();
354        let decoded = HolographicCoords::decode_key(&key).unwrap();
355        assert_eq!(coords.galaxy, decoded.galaxy);
356        assert_eq!(coords.sector, decoded.sector);
357        assert_eq!(coords.temporal, decoded.temporal);
358        assert!((coords.radial - decoded.radial).abs() < f32::EPSILON);
359    }
360
361    #[test]
362    fn coords_distance_identical() {
363        let coords = HolographicCoords {
364            galaxy: 0,
365            sector: 0,
366            radial: 0.5,
367            angular: 0.0,
368            temporal: 1000,
369            consciousness: 0.5,
370        };
371        assert_eq!(coords.distance_to(&coords), 0.0);
372    }
373
374    #[test]
375    fn coords_distance_different_galaxies() {
376        let a = HolographicCoords {
377            galaxy: 0,
378            sector: 0,
379            radial: 0.5,
380            angular: 0.0,
381            temporal: 1000,
382            consciousness: 0.5,
383        };
384        let b = HolographicCoords {
385            galaxy: 1,
386            sector: 0,
387            radial: 0.5,
388            angular: 0.0,
389            temporal: 1000,
390            consciousness: 0.5,
391        };
392        // Same coordinates except galaxy — distance should be 0
393        // (galaxy is used for routing, not distance)
394        assert_eq!(a.distance_to(&b), 0.0);
395    }
396
397    // ── Coordinate5D Tests ────────────────────────────────────────────
398
399    #[test]
400    fn coord5d_encode_deterministic() {
401        let a = Coordinate5D::encode("hello world");
402        let b = Coordinate5D::encode("hello world");
403        assert_eq!(a, b);
404    }
405
406    #[test]
407    fn coord5d_encode_different_text() {
408        let a = Coordinate5D::encode("hello world");
409        let b = Coordinate5D::encode("goodbye world");
410        assert_ne!(a, b);
411    }
412
413    #[test]
414    fn coord5d_encode_in_range() {
415        let coord = Coordinate5D::encode("test content");
416        assert!(coord.x >= 0.0 && coord.x <= 1.0);
417        assert!(coord.y >= 0.0 && coord.y <= 1.0);
418        assert!(coord.z >= 0.0 && coord.z <= 1.0);
419    }
420
421    #[test]
422    fn coord5d_encode_with_context() {
423        let coord = Coordinate5D::encode_with_context("test", 0.8, 0.9);
424        assert!((coord.w - 0.8).abs() < f32::EPSILON);
425        assert!((coord.v - 0.9).abs() < f32::EPSILON);
426    }
427
428    #[test]
429    fn coord5d_distance_identical() {
430        let coord = Coordinate5D::encode("test");
431        assert!((coord.distance_to(&coord)).abs() < f32::EPSILON);
432    }
433
434    #[test]
435    fn coord5d_distance_positive() {
436        let a = Coordinate5D::new(0.0, 0.0, 0.0, 0.0, 0.0);
437        let b = Coordinate5D::new(1.0, 1.0, 1.0, 1.0, 1.0);
438        let dist = a.distance_to(&b);
439        assert!(dist > 0.0);
440        // sqrt(5) ≈ 2.236
441        assert!((dist - 2.236068).abs() < 0.001);
442    }
443
444    #[test]
445    fn coord5d_semantic_distance_weighted() {
446        let a = Coordinate5D::new(0.0, 0.0, 0.0, 0.0, 0.0);
447        let b = Coordinate5D::new(1.0, 0.0, 0.0, 0.0, 0.0);
448        let c = Coordinate5D::new(0.0, 0.0, 0.0, 1.0, 0.0);
449        let dist_x = a.semantic_distance_to(&b);
450        let dist_w = a.semantic_distance_to(&c);
451        // x-axis should have more weight than w-axis
452        assert!(dist_x > dist_w);
453    }
454
455    #[test]
456    fn coord5d_key_roundtrip() {
457        let coord = Coordinate5D::new(0.1, 0.2, 0.3, 0.4, 0.5);
458        let key = coord.encode_key();
459        let decoded = Coordinate5D::decode_key(&key).unwrap();
460        assert!((coord.x - decoded.x).abs() < f32::EPSILON);
461        assert!((coord.y - decoded.y).abs() < f32::EPSILON);
462        assert!((coord.z - decoded.z).abs() < f32::EPSILON);
463        assert!((coord.w - decoded.w).abs() < f32::EPSILON);
464        assert!((coord.v - decoded.v).abs() < f32::EPSILON);
465    }
466
467    #[test]
468    fn zone_from_radial() {
469        assert_eq!(Zone::from_radial(0.1), Zone::Core);
470        assert_eq!(Zone::from_radial(0.3), Zone::InnerRing);
471        assert_eq!(Zone::from_radial(0.5), Zone::MidRing);
472        assert_eq!(Zone::from_radial(0.7), Zone::OuterRing);
473        assert_eq!(Zone::from_radial(0.9), Zone::FarEdge);
474    }
475
476    #[test]
477    fn zone_name_and_index() {
478        assert_eq!(Zone::Core.name(), "core");
479        assert_eq!(Zone::Core.index(), 0);
480        assert_eq!(Zone::FarEdge.name(), "far_edge");
481        assert_eq!(Zone::FarEdge.index(), 4);
482    }
483
484    #[test]
485    fn coord5d_zone_classification() {
486        let center = Coordinate5D::new(0.5, 0.5, 0.5, 0.5, 0.5);
487        assert_eq!(center.zone(), Zone::Core);
488        let edge = Coordinate5D::new(0.0, 0.0, 0.0, 0.0, 0.0);
489        assert_eq!(edge.zone(), Zone::FarEdge);
490    }
491
492    #[test]
493    fn find_nearby_returns_sorted() {
494        let center = Coordinate5D::new(0.5, 0.5, 0.5, 0.5, 0.5);
495        let candidates = vec![
496            (0, Coordinate5D::new(0.9, 0.5, 0.5, 0.5, 0.5)), // far
497            (1, Coordinate5D::new(0.51, 0.5, 0.5, 0.5, 0.5)), // near
498            (2, Coordinate5D::new(0.0, 0.0, 0.0, 0.0, 0.0)), // very far
499            (3, Coordinate5D::new(0.5, 0.5, 0.5, 0.5, 0.5)), // identical
500        ];
501        let results = find_nearby(&center, &candidates, 1.0);
502        assert_eq!(results.len(), 3); // excludes the very far one
503        assert_eq!(results[0].0, 3); // nearest is identical
504        assert_eq!(results[1].0, 1); // next is near
505        assert_eq!(results[2].0, 0); // farthest within radius
506    }
507
508    #[test]
509    fn find_nearby_empty_for_tiny_radius() {
510        let center = Coordinate5D::new(0.5, 0.5, 0.5, 0.5, 0.5);
511        let candidates = vec![(0, Coordinate5D::new(0.9, 0.5, 0.5, 0.5, 0.5))];
512        let results = find_nearby(&center, &candidates, 0.01);
513        assert!(results.is_empty());
514    }
515}