use super::atom::ComputeAtom;
use super::kuramoto::KuramotoProjection;
use super::splat::SplatProjection;
use crate::phase::kuramoto::{kuramoto_step, Omega};
use crate::primitives::vector::cosine_similarity;
#[derive(Debug, Clone, Copy, Default)]
pub struct AudioMetrics {
pub bass: f32,
pub mid: f32,
pub high: f32,
pub entropy: f32,
}
pub fn gravity_coupling(
embedding_a: &[f32],
embedding_b: &[f32],
k_base: f32,
gravity_scale: f32,
) -> f32 {
let sim = cosine_similarity(embedding_a, embedding_b);
k_base * gravity_scale * sim.max(0.0)
}
pub fn coupled_step(
atoms: &mut [ComputeAtom],
audio: &AudioMetrics,
k_base: f32,
gravity_scale: f32,
dt: f32,
) -> Vec<f32> {
let n = atoms.len();
if n == 0 {
return vec![];
}
let k = k_base * (1.0 + audio.bass * 2.0);
let mut phases = Vec::with_capacity(n);
let mut omegas = Vec::with_capacity(n);
for atom in atoms.iter() {
if let Some(kp) = atom.read_projection::<KuramotoProjection>() {
phases.push(kp.theta);
omegas.push(kp.omega);
} else {
phases.push(0.0);
omegas.push(0.0);
}
}
let adjacency: Vec<Vec<f32>> = if atoms.iter().any(|a| a.layout.has(super::projection::ProjectionId::Splat)) {
let embeddings: Vec<Option<SplatProjection>> = atoms
.iter()
.map(|a| a.read_projection::<SplatProjection>())
.collect();
(0..n)
.map(|i| {
(0..n)
.map(|j| {
if i == j {
return 0.0;
}
match (&embeddings[i], &embeddings[j]) {
(Some(ei), Some(ej)) => {
gravity_coupling(&ei.embedding, &ej.embedding, k, gravity_scale)
}
_ => k, }
})
.collect()
})
.collect()
} else {
vec![vec![k; n]; n]
};
let adj_refs: Vec<&[f32]> = adjacency.iter().map(|row| row.as_slice()).collect();
let new_phases = kuramoto_step(
&phases,
&Omega::PerNode(omegas),
1.0, Some(&adj_refs),
dt,
true, );
for (i, atom) in atoms.iter_mut().enumerate() {
if let Some(mut kp) = atom.read_projection::<KuramotoProjection>() {
kp.theta = new_phases[i];
atom.write_projection(&kp);
}
}
new_phases
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::layout::ProjectionLayout;
use crate::phase::kuramoto::order_parameter;
#[test]
fn test_gravity_coupling_identical() {
let e = vec![1.0f32; 384];
let c = gravity_coupling(&e, &e, 1.0, 1.0);
assert!((c - 1.0).abs() < 1e-3, "coupling = {}", c);
}
#[test]
fn test_gravity_coupling_orthogonal() {
let mut a = vec![0.0f32; 384];
let mut b = vec![0.0f32; 384];
a[0] = 1.0;
b[1] = 1.0;
let c = gravity_coupling(&a, &b, 1.0, 1.0);
assert!(c.abs() < 1e-3, "coupling = {}", c);
}
#[test]
fn test_coupled_step_converges() {
let layout = ProjectionLayout::minimal(); let mut atoms = ComputeAtom::create_n(&layout, 6);
for (i, atom) in atoms.iter_mut().enumerate() {
let mut splat = SplatProjection::default();
splat.embedding[0] = 1.0; atom.write_projection(&splat);
let k = KuramotoProjection {
theta: i as f32,
omega: 0.0,
coupling: 1.0,
};
atom.write_projection(&k);
}
let phases_before: Vec<f32> = atoms
.iter()
.filter_map(|a| a.read_projection::<KuramotoProjection>())
.map(|k| k.theta)
.collect();
let r_before = order_parameter(&phases_before).r;
let audio = AudioMetrics { bass: 0.5, mid: 0.3, high: 0.2, entropy: 0.5 };
for _ in 0..1000 {
coupled_step(&mut atoms, &audio, 5.0, 1.0, 0.01);
}
let phases_after: Vec<f32> = atoms
.iter()
.filter_map(|a| a.read_projection::<KuramotoProjection>())
.map(|k| k.theta)
.collect();
let r_after = order_parameter(&phases_after).r;
assert!(
r_after > r_before,
"R should increase: before={} after={}",
r_before,
r_after
);
assert!(r_after > 0.9, "R should converge: R={}", r_after);
}
#[test]
fn test_coupled_step_empty() {
let result = coupled_step(&mut [], &AudioMetrics::default(), 1.0, 1.0, 0.01);
assert!(result.is_empty());
}
#[test]
fn test_coupled_step_preserves_atom_count() {
let layout = ProjectionLayout::minimal();
let mut atoms = ComputeAtom::create_n(&layout, 10);
let result = coupled_step(&mut atoms, &AudioMetrics::default(), 1.0, 1.0, 0.01);
assert_eq!(result.len(), 10);
}
}