Skip to main content

terminals_core/substrate/
atom.rs

1//! ComputeAtom — Contiguous memory unit with typed projection views.
2//!
3//! A ComputeAtom owns a byte buffer whose layout is determined by a
4//! ProjectionLayout. Projections are read/written via typed accessors.
5//! The shape_hash (σ) is recomputed on mutation.
6
7use super::layout::ProjectionLayout;
8use super::projection::{Projection, ProjectionId};
9use super::splat::SplatProjection;
10use super::kuramoto::KuramotoProjection;
11use super::expert::ExpertProjection;
12use super::graph::GraphProjection;
13use super::thermal::ThermalProjection;
14
15/// FNV-1a hash over a byte slice.
16fn fnv1a(data: &[u8]) -> u32 {
17    let mut hash = 0x811c_9dc5u32;
18    for &byte in data {
19        hash ^= byte as u32;
20        hash = hash.wrapping_mul(0x0100_0193);
21    }
22    hash
23}
24
25/// A single computational atom in the substrate.
26#[derive(Debug, Clone)]
27pub struct ComputeAtom {
28    /// Contiguous memory backing all projections.
29    pub buffer: Vec<u8>,
30    /// Frozen layout determining projection offsets.
31    pub layout: ProjectionLayout,
32    /// Shape hash σ — deterministic content address.
33    pub shape_hash: u32,
34}
35
36impl ComputeAtom {
37    /// Create a zero-initialized atom with the given layout.
38    /// All projections start at their default (void instantiation).
39    pub fn new(layout: ProjectionLayout) -> Self {
40        let buffer = vec![0u8; layout.stride];
41        let mut atom = Self {
42            buffer,
43            layout,
44            shape_hash: 0,
45        };
46        atom.recompute_hash();
47        atom
48    }
49
50    /// Create N atoms sharing the same layout.
51    pub fn create_n(layout: &ProjectionLayout, n: usize) -> Vec<Self> {
52        (0..n).map(|_| Self::new(layout.clone())).collect()
53    }
54
55    /// Read a projection from this atom. Returns None if projection not in layout.
56    pub fn read_projection<P: Projection>(&self) -> Option<P> {
57        let offset = self.layout.offset_of(P::id())?;
58        Some(P::read(&self.buffer[offset..]))
59    }
60
61    /// Write a projection into this atom. Returns false if projection not in layout.
62    pub fn write_projection<P: Projection>(&mut self, proj: &P) -> bool {
63        if let Some(offset) = self.layout.offset_of(P::id()) {
64            proj.write(&mut self.buffer[offset..]);
65            self.recompute_hash();
66            true
67        } else {
68            false
69        }
70    }
71
72    /// Get a read-only byte slice for a projection's region.
73    pub fn projection_bytes(&self, id: ProjectionId) -> Option<&[u8]> {
74        let offset = self.layout.offset_of(id)?;
75        let size = match id {
76            ProjectionId::Splat => SplatProjection::byte_size(),
77            ProjectionId::Kuramoto => KuramotoProjection::byte_size(),
78            ProjectionId::Expert => ExpertProjection::byte_size(),
79            ProjectionId::Graph => GraphProjection::byte_size(),
80            ProjectionId::Thermal => ThermalProjection::byte_size(),
81        };
82        Some(&self.buffer[offset..offset + size])
83    }
84
85    /// Recompute shape hash from all active projection contributions.
86    fn recompute_hash(&mut self) {
87        // Hash the full buffer — captures all projection state
88        self.shape_hash = fnv1a(&self.buffer);
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_atom_full_layout() {
98        let atom = ComputeAtom::new(ProjectionLayout::full());
99        assert_eq!(atom.buffer.len(), 1612);
100        assert!(atom.read_projection::<SplatProjection>().is_some());
101        assert!(atom.read_projection::<KuramotoProjection>().is_some());
102        assert!(atom.read_projection::<ExpertProjection>().is_some());
103        assert!(atom.read_projection::<GraphProjection>().is_some());
104        assert!(atom.read_projection::<ThermalProjection>().is_some());
105    }
106
107    #[test]
108    fn test_atom_minimal_layout() {
109        let atom = ComputeAtom::new(ProjectionLayout::minimal());
110        assert!(atom.read_projection::<SplatProjection>().is_some());
111        assert!(atom.read_projection::<KuramotoProjection>().is_some());
112        assert!(atom.read_projection::<ExpertProjection>().is_none());
113        assert!(atom.read_projection::<GraphProjection>().is_none());
114    }
115
116    #[test]
117    fn test_atom_write_read_roundtrip() {
118        let mut atom = ComputeAtom::new(ProjectionLayout::full());
119
120        let k = KuramotoProjection {
121            theta: 1.5,
122            omega: 0.3,
123            coupling: 2.0,
124        };
125        assert!(atom.write_projection(&k));
126
127        let restored = atom.read_projection::<KuramotoProjection>().unwrap();
128        assert!((restored.theta - 1.5).abs() < 1e-6);
129        assert!((restored.omega - 0.3).abs() < 1e-6);
130    }
131
132    #[test]
133    fn test_atom_shape_hash_changes_on_write() {
134        let mut atom = ComputeAtom::new(ProjectionLayout::full());
135        let hash_before = atom.shape_hash;
136
137        let k = KuramotoProjection {
138            theta: 3.14,
139            omega: 1.0,
140            coupling: 1.0,
141        };
142        atom.write_projection(&k);
143        assert_ne!(atom.shape_hash, hash_before);
144    }
145
146    #[test]
147    fn test_create_n() {
148        let layout = ProjectionLayout::minimal();
149        let atoms = ComputeAtom::create_n(&layout, 100);
150        assert_eq!(atoms.len(), 100);
151        assert_eq!(atoms[0].buffer.len(), layout.stride);
152    }
153}