1use super::atom::ComputeAtom;
12use super::coupling::{coupled_step, AudioMetrics};
13use super::expert::ExpertProjection;
14use super::fold::fold_to_json;
15use super::graph::{GraphProjection, PadicAddr};
16use super::kuramoto::KuramotoProjection;
17use super::layout::ProjectionLayout;
18use super::sematon::Sematon;
19use super::splat::SplatProjection;
20use super::witness::{compute_witness, ConvergenceWitness};
21use crate::primitives::vector;
22
23pub struct SubstrateEngine {
26 atoms: Vec<ComputeAtom>,
27 layout: ProjectionLayout,
28 step: u32,
29 witness: ConvergenceWitness,
30 k_base: f32,
31 gravity_scale: f32,
32}
33
34impl SubstrateEngine {
35 pub fn new(n: usize, full_layout: bool, k_base: f32, gravity_scale: f32) -> Self {
39 let layout = if full_layout {
40 ProjectionLayout::full()
41 } else {
42 ProjectionLayout::minimal()
43 };
44 let atoms = ComputeAtom::create_n(&layout, n);
45 Self {
46 atoms,
47 layout,
48 step: 0,
49 witness: ConvergenceWitness::default(),
50 k_base,
51 gravity_scale,
52 }
53 }
54
55 pub fn tick(&mut self, bass: f32, mid: f32, high: f32, entropy: f32, dt: f32) -> [f32; 4] {
57 let audio = AudioMetrics {
58 bass,
59 mid,
60 high,
61 entropy,
62 };
63
64 coupled_step(
65 &mut self.atoms,
66 &audio,
67 self.k_base,
68 self.gravity_scale,
69 dt,
70 );
71
72 self.step += 1;
73 self.witness = compute_witness(&self.atoms, self.step);
74
75 [
76 self.witness.r,
77 self.witness.entropy,
78 if self.witness.converged { 1.0 } else { 0.0 },
79 self.step as f32,
80 ]
81 }
82
83 pub fn phases(&self) -> Vec<f32> {
85 self.atoms
86 .iter()
87 .map(|a| {
88 a.read_projection::<KuramotoProjection>()
89 .map(|k| k.theta)
90 .unwrap_or(0.0)
91 })
92 .collect()
93 }
94
95 pub fn witness_array(&self) -> [f32; 4] {
97 [
98 self.witness.r,
99 self.witness.entropy,
100 if self.witness.converged { 1.0 } else { 0.0 },
101 self.step as f32,
102 ]
103 }
104
105 pub fn set_embedding(&mut self, index: usize, embedding: &[f32]) -> bool {
108 if index >= self.atoms.len() || embedding.len() != 384 {
109 return false;
110 }
111 let mut splat = self.atoms[index]
112 .read_projection::<SplatProjection>()
113 .unwrap_or_default();
114 splat.embedding.copy_from_slice(embedding);
115 self.atoms[index].write_projection(&splat)
116 }
117
118 pub fn set_phase(&mut self, index: usize, theta: f32, omega: f32) -> bool {
120 if index >= self.atoms.len() {
121 return false;
122 }
123 let mut kp = self.atoms[index]
124 .read_projection::<KuramotoProjection>()
125 .unwrap_or_default();
126 kp.theta = theta;
127 kp.omega = omega;
128 self.atoms[index].write_projection(&kp)
129 }
130
131 pub fn set_expert(&mut self, index: usize, intent: f32, activation: f32, gate: f32) -> bool {
133 if index >= self.atoms.len() {
134 return false;
135 }
136 if !self.layout.has(super::projection::ProjectionId::Expert) {
137 return false;
138 }
139 let ep = ExpertProjection {
140 intent,
141 activation,
142 gate,
143 };
144 self.atoms[index].write_projection(&ep)
145 }
146
147 pub fn set_neighbor(&mut self, index: usize, slot: usize, neighbor_index: u16) -> bool {
149 if index >= self.atoms.len() || slot >= super::graph::MAX_NEIGHBORS {
150 return false;
151 }
152 if !self.layout.has(super::projection::ProjectionId::Graph) {
153 return false;
154 }
155 let mut gp = self.atoms[index]
156 .read_projection::<GraphProjection>()
157 .unwrap_or_default();
158 gp.neighbors[slot] = neighbor_index;
159 self.atoms[index].write_projection(&gp)
160 }
161
162 pub fn atom_count(&self) -> usize {
164 self.atoms.len()
165 }
166
167 pub fn step_count(&self) -> u32 {
169 self.step
170 }
171
172 pub fn stride(&self) -> usize {
174 self.layout.stride
175 }
176
177 pub fn converged(&self) -> bool {
179 self.witness.converged
180 }
181
182 pub fn order_parameter(&self) -> f32 {
184 self.witness.r
185 }
186
187 pub fn embeddings_flat(&self) -> Vec<f32> {
189 let mut out = Vec::with_capacity(self.atoms.len() * 384);
190 for atom in &self.atoms {
191 if let Some(splat) = atom.read_projection::<SplatProjection>() {
192 out.extend_from_slice(&splat.embedding);
193 } else {
194 out.extend(std::iter::repeat(0.0f32).take(384));
195 }
196 }
197 out
198 }
199
200 fn centroid_embedding(&self) -> Vec<f32> {
202 let mut centroid = vec![0.0f32; 384];
203 let mut count = 0usize;
204
205 for atom in &self.atoms {
206 if let Some(splat) = atom.read_projection::<SplatProjection>() {
207 for (i, &v) in splat.embedding.iter().enumerate() {
208 centroid[i] += v;
209 }
210 count += 1;
211 }
212 }
213
214 if count == 0 {
215 return centroid;
216 }
217
218 let n = count as f32;
219 for v in &mut centroid {
220 *v /= n;
221 }
222
223 vector::normalize(&mut centroid);
224 centroid
225 }
226
227 pub fn extract_sematon(&self, source: &str) -> String {
230 let centroid = self.centroid_embedding();
231 let address = PadicAddr {
232 base: self.step,
233 coeff0: self.atoms.len() as u16,
234 coeff1: 0,
235 };
236 let sematon = Sematon::new(centroid, self.witness, address, source);
237 fold_to_json(&sematon)
238 }
239
240 pub fn atom_shape_hash(&self, index: usize) -> Option<u32> {
242 self.atoms.get(index).map(|a| a.shape_hash)
243 }
244
245 pub fn set_coupling_params(&mut self, k_base: f32, gravity_scale: f32) {
247 self.k_base = k_base;
248 self.gravity_scale = gravity_scale;
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_engine_create_full() {
258 let engine = SubstrateEngine::new(10, true, 1.0, 1.0);
259 assert_eq!(engine.atom_count(), 10);
260 assert_eq!(engine.stride(), 1612);
261 assert_eq!(engine.step_count(), 0);
262 assert!(!engine.converged());
263 }
264
265 #[test]
266 fn test_engine_create_minimal() {
267 let engine = SubstrateEngine::new(5, false, 1.0, 1.0);
268 assert_eq!(engine.atom_count(), 5);
269 assert_eq!(engine.stride(), 1548);
270 }
271
272 #[test]
273 fn test_engine_set_and_read_embedding() {
274 let mut engine = SubstrateEngine::new(3, true, 1.0, 1.0);
275 let mut emb = vec![0.0f32; 384];
276 emb[0] = 1.0;
277
278 assert!(engine.set_embedding(0, &emb));
279 assert!(engine.set_embedding(1, &emb));
280 assert!(!engine.set_embedding(10, &emb)); assert!(!engine.set_embedding(0, &[1.0; 10])); let flat = engine.embeddings_flat();
284 assert_eq!(flat.len(), 3 * 384);
285 assert!((flat[0] - 1.0).abs() < 1e-6);
286 }
287
288 #[test]
289 fn test_engine_set_phase() {
290 let mut engine = SubstrateEngine::new(3, false, 1.0, 1.0);
291 assert!(engine.set_phase(0, 1.5, 0.1));
292 assert!(engine.set_phase(2, 3.0, 0.2));
293 assert!(!engine.set_phase(5, 0.0, 0.0)); let phases = engine.phases();
296 assert_eq!(phases.len(), 3);
297 assert!((phases[0] - 1.5).abs() < 1e-6);
298 assert!((phases[2] - 3.0).abs() < 1e-6);
299 }
300
301 #[test]
302 fn test_engine_set_expert() {
303 let mut engine = SubstrateEngine::new(2, true, 1.0, 1.0);
304 assert!(engine.set_expert(0, 0.5, 0.8, 1.0));
305
306 let mut engine_min = SubstrateEngine::new(2, false, 1.0, 1.0);
308 assert!(!engine_min.set_expert(0, 0.5, 0.8, 1.0));
309 }
310
311 #[test]
312 fn test_engine_tick_returns_witness() {
313 let mut engine = SubstrateEngine::new(6, false, 5.0, 1.0);
314
315 let mut emb = vec![0.0f32; 384];
317 emb[0] = 1.0;
318 for i in 0..6 {
319 engine.set_embedding(i, &emb);
320 engine.set_phase(i, i as f32, 0.0);
321 }
322
323 let w = engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
325 assert_eq!(w.len(), 4);
326 assert!(w[0] >= 0.0 && w[0] <= 1.0); assert_eq!(w[3], 1.0); assert_eq!(engine.step_count(), 1);
329 }
330
331 #[test]
332 fn test_engine_convergence_after_many_ticks() {
333 let mut engine = SubstrateEngine::new(6, false, 5.0, 1.0);
334
335 let mut emb = vec![0.0f32; 384];
337 emb[0] = 1.0;
338 for i in 0..6 {
339 engine.set_embedding(i, &emb);
340 engine.set_phase(i, i as f32, 0.0);
341 }
342
343 for _ in 0..1000 {
345 engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
346 }
347
348 assert!(
349 engine.order_parameter() > 0.9,
350 "R = {} (expected > 0.9)",
351 engine.order_parameter()
352 );
353 assert!(engine.converged());
354 }
355
356 #[test]
357 fn test_engine_extract_sematon() {
358 let mut engine = SubstrateEngine::new(4, false, 5.0, 1.0);
359
360 let mut emb = vec![0.0f32; 384];
361 emb[0] = 1.0;
362 for i in 0..4 {
363 engine.set_embedding(i, &emb);
364 }
365
366 engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
368
369 let json = engine.extract_sematon("test-surface");
370 assert!(!json.is_empty());
371 assert!(json.contains("test-surface"));
372 assert!(json.contains("witness_r"));
373 }
374
375 #[test]
376 fn test_engine_witness_array_matches_tick() {
377 let mut engine = SubstrateEngine::new(3, false, 1.0, 1.0);
378 let tick_w = engine.tick(0.0, 0.0, 0.0, 0.0, 0.01);
379 let read_w = engine.witness_array();
380
381 assert_eq!(tick_w[0], read_w[0]); assert_eq!(tick_w[1], read_w[1]); assert_eq!(tick_w[2], read_w[2]); assert_eq!(tick_w[3], read_w[3]); }
386
387 #[test]
388 fn test_engine_set_coupling_params() {
389 let mut engine = SubstrateEngine::new(2, false, 1.0, 1.0);
390 engine.set_coupling_params(10.0, 2.0);
391 engine.tick(0.5, 0.0, 0.0, 0.0, 0.01);
393 }
394
395 #[test]
396 fn test_engine_empty() {
397 let mut engine = SubstrateEngine::new(0, true, 1.0, 1.0);
398 assert_eq!(engine.atom_count(), 0);
399 let w = engine.tick(0.0, 0.0, 0.0, 0.0, 0.01);
400 assert_eq!(w[0], 0.0); assert!(engine.phases().is_empty());
402 }
403
404 #[test]
405 fn test_engine_atom_shape_hash() {
406 let mut engine = SubstrateEngine::new(2, false, 1.0, 1.0);
407 let h1 = engine.atom_shape_hash(0).unwrap();
408
409 engine.set_phase(0, 3.14, 1.0);
410 let h2 = engine.atom_shape_hash(0).unwrap();
411
412 assert_ne!(h1, h2); assert!(engine.atom_shape_hash(99).is_none()); }
415}