use super::atom::ComputeAtom;
use super::coupling::{coupled_step, AudioMetrics};
use super::expert::ExpertProjection;
use super::fold::fold_to_json;
use super::graph::{GraphProjection, PadicAddr};
use super::kuramoto::KuramotoProjection;
use super::layout::ProjectionLayout;
use super::sematon::Sematon;
use super::splat::SplatProjection;
use super::witness::{compute_witness, ConvergenceWitness};
use crate::primitives::vector;
pub struct SubstrateEngine {
atoms: Vec<ComputeAtom>,
layout: ProjectionLayout,
step: u32,
witness: ConvergenceWitness,
k_base: f32,
gravity_scale: f32,
}
impl SubstrateEngine {
pub fn new(n: usize, full_layout: bool, k_base: f32, gravity_scale: f32) -> Self {
let layout = if full_layout {
ProjectionLayout::full()
} else {
ProjectionLayout::minimal()
};
let atoms = ComputeAtom::create_n(&layout, n);
Self {
atoms,
layout,
step: 0,
witness: ConvergenceWitness::default(),
k_base,
gravity_scale,
}
}
pub fn tick(&mut self, bass: f32, mid: f32, high: f32, entropy: f32, dt: f32) -> [f32; 4] {
let audio = AudioMetrics {
bass,
mid,
high,
entropy,
};
coupled_step(
&mut self.atoms,
&audio,
self.k_base,
self.gravity_scale,
dt,
);
self.step += 1;
self.witness = compute_witness(&self.atoms, self.step);
[
self.witness.r,
self.witness.entropy,
if self.witness.converged { 1.0 } else { 0.0 },
self.step as f32,
]
}
pub fn phases(&self) -> Vec<f32> {
self.atoms
.iter()
.map(|a| {
a.read_projection::<KuramotoProjection>()
.map(|k| k.theta)
.unwrap_or(0.0)
})
.collect()
}
pub fn witness_array(&self) -> [f32; 4] {
[
self.witness.r,
self.witness.entropy,
if self.witness.converged { 1.0 } else { 0.0 },
self.step as f32,
]
}
pub fn set_embedding(&mut self, index: usize, embedding: &[f32]) -> bool {
if index >= self.atoms.len() || embedding.len() != 384 {
return false;
}
let mut splat = self.atoms[index]
.read_projection::<SplatProjection>()
.unwrap_or_default();
splat.embedding.copy_from_slice(embedding);
self.atoms[index].write_projection(&splat)
}
pub fn set_phase(&mut self, index: usize, theta: f32, omega: f32) -> bool {
if index >= self.atoms.len() {
return false;
}
let mut kp = self.atoms[index]
.read_projection::<KuramotoProjection>()
.unwrap_or_default();
kp.theta = theta;
kp.omega = omega;
self.atoms[index].write_projection(&kp)
}
pub fn set_expert(&mut self, index: usize, intent: f32, activation: f32, gate: f32) -> bool {
if index >= self.atoms.len() {
return false;
}
if !self.layout.has(super::projection::ProjectionId::Expert) {
return false;
}
let ep = ExpertProjection {
intent,
activation,
gate,
};
self.atoms[index].write_projection(&ep)
}
pub fn set_neighbor(&mut self, index: usize, slot: usize, neighbor_index: u16) -> bool {
if index >= self.atoms.len() || slot >= super::graph::MAX_NEIGHBORS {
return false;
}
if !self.layout.has(super::projection::ProjectionId::Graph) {
return false;
}
let mut gp = self.atoms[index]
.read_projection::<GraphProjection>()
.unwrap_or_default();
gp.neighbors[slot] = neighbor_index;
self.atoms[index].write_projection(&gp)
}
pub fn atom_count(&self) -> usize {
self.atoms.len()
}
pub fn step_count(&self) -> u32 {
self.step
}
pub fn stride(&self) -> usize {
self.layout.stride
}
pub fn converged(&self) -> bool {
self.witness.converged
}
pub fn order_parameter(&self) -> f32 {
self.witness.r
}
pub fn embeddings_flat(&self) -> Vec<f32> {
let mut out = Vec::with_capacity(self.atoms.len() * 384);
for atom in &self.atoms {
if let Some(splat) = atom.read_projection::<SplatProjection>() {
out.extend_from_slice(&splat.embedding);
} else {
out.extend(std::iter::repeat(0.0f32).take(384));
}
}
out
}
fn centroid_embedding(&self) -> Vec<f32> {
let mut centroid = vec![0.0f32; 384];
let mut count = 0usize;
for atom in &self.atoms {
if let Some(splat) = atom.read_projection::<SplatProjection>() {
for (i, &v) in splat.embedding.iter().enumerate() {
centroid[i] += v;
}
count += 1;
}
}
if count == 0 {
return centroid;
}
let n = count as f32;
for v in &mut centroid {
*v /= n;
}
vector::normalize(&mut centroid);
centroid
}
pub fn extract_sematon(&self, source: &str) -> String {
let centroid = self.centroid_embedding();
let address = PadicAddr {
base: self.step,
coeff0: self.atoms.len() as u16,
coeff1: 0,
};
let sematon = Sematon::new(centroid, self.witness, address, source);
fold_to_json(&sematon)
}
pub fn atom_shape_hash(&self, index: usize) -> Option<u32> {
self.atoms.get(index).map(|a| a.shape_hash)
}
pub fn set_coupling_params(&mut self, k_base: f32, gravity_scale: f32) {
self.k_base = k_base;
self.gravity_scale = gravity_scale;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_engine_create_full() {
let engine = SubstrateEngine::new(10, true, 1.0, 1.0);
assert_eq!(engine.atom_count(), 10);
assert_eq!(engine.stride(), 1612);
assert_eq!(engine.step_count(), 0);
assert!(!engine.converged());
}
#[test]
fn test_engine_create_minimal() {
let engine = SubstrateEngine::new(5, false, 1.0, 1.0);
assert_eq!(engine.atom_count(), 5);
assert_eq!(engine.stride(), 1548);
}
#[test]
fn test_engine_set_and_read_embedding() {
let mut engine = SubstrateEngine::new(3, true, 1.0, 1.0);
let mut emb = vec![0.0f32; 384];
emb[0] = 1.0;
assert!(engine.set_embedding(0, &emb));
assert!(engine.set_embedding(1, &emb));
assert!(!engine.set_embedding(10, &emb)); assert!(!engine.set_embedding(0, &[1.0; 10]));
let flat = engine.embeddings_flat();
assert_eq!(flat.len(), 3 * 384);
assert!((flat[0] - 1.0).abs() < 1e-6);
}
#[test]
fn test_engine_set_phase() {
let mut engine = SubstrateEngine::new(3, false, 1.0, 1.0);
assert!(engine.set_phase(0, 1.5, 0.1));
assert!(engine.set_phase(2, 3.0, 0.2));
assert!(!engine.set_phase(5, 0.0, 0.0));
let phases = engine.phases();
assert_eq!(phases.len(), 3);
assert!((phases[0] - 1.5).abs() < 1e-6);
assert!((phases[2] - 3.0).abs() < 1e-6);
}
#[test]
fn test_engine_set_expert() {
let mut engine = SubstrateEngine::new(2, true, 1.0, 1.0);
assert!(engine.set_expert(0, 0.5, 0.8, 1.0));
let mut engine_min = SubstrateEngine::new(2, false, 1.0, 1.0);
assert!(!engine_min.set_expert(0, 0.5, 0.8, 1.0));
}
#[test]
fn test_engine_tick_returns_witness() {
let mut engine = SubstrateEngine::new(6, false, 5.0, 1.0);
let mut emb = vec![0.0f32; 384];
emb[0] = 1.0;
for i in 0..6 {
engine.set_embedding(i, &emb);
engine.set_phase(i, i as f32, 0.0);
}
let w = engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
assert_eq!(w.len(), 4);
assert!(w[0] >= 0.0 && w[0] <= 1.0); assert_eq!(w[3], 1.0); assert_eq!(engine.step_count(), 1);
}
#[test]
fn test_engine_convergence_after_many_ticks() {
let mut engine = SubstrateEngine::new(6, false, 5.0, 1.0);
let mut emb = vec![0.0f32; 384];
emb[0] = 1.0;
for i in 0..6 {
engine.set_embedding(i, &emb);
engine.set_phase(i, i as f32, 0.0);
}
for _ in 0..1000 {
engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
}
assert!(
engine.order_parameter() > 0.9,
"R = {} (expected > 0.9)",
engine.order_parameter()
);
assert!(engine.converged());
}
#[test]
fn test_engine_extract_sematon() {
let mut engine = SubstrateEngine::new(4, false, 5.0, 1.0);
let mut emb = vec![0.0f32; 384];
emb[0] = 1.0;
for i in 0..4 {
engine.set_embedding(i, &emb);
}
engine.tick(0.5, 0.3, 0.2, 0.5, 0.01);
let json = engine.extract_sematon("test-surface");
assert!(!json.is_empty());
assert!(json.contains("test-surface"));
assert!(json.contains("witness_r"));
}
#[test]
fn test_engine_witness_array_matches_tick() {
let mut engine = SubstrateEngine::new(3, false, 1.0, 1.0);
let tick_w = engine.tick(0.0, 0.0, 0.0, 0.0, 0.01);
let read_w = engine.witness_array();
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]); }
#[test]
fn test_engine_set_coupling_params() {
let mut engine = SubstrateEngine::new(2, false, 1.0, 1.0);
engine.set_coupling_params(10.0, 2.0);
engine.tick(0.5, 0.0, 0.0, 0.0, 0.01);
}
#[test]
fn test_engine_empty() {
let mut engine = SubstrateEngine::new(0, true, 1.0, 1.0);
assert_eq!(engine.atom_count(), 0);
let w = engine.tick(0.0, 0.0, 0.0, 0.0, 0.01);
assert_eq!(w[0], 0.0); assert!(engine.phases().is_empty());
}
#[test]
fn test_engine_atom_shape_hash() {
let mut engine = SubstrateEngine::new(2, false, 1.0, 1.0);
let h1 = engine.atom_shape_hash(0).unwrap();
engine.set_phase(0, 3.14, 1.0);
let h2 = engine.atom_shape_hash(0).unwrap();
assert_ne!(h1, h2); assert!(engine.atom_shape_hash(99).is_none()); }
}