terminals_core/substrate/
coupling.rs1use super::atom::ComputeAtom;
11use super::kuramoto::KuramotoProjection;
12use super::splat::SplatProjection;
13use crate::phase::kuramoto::{kuramoto_step, Omega};
14use crate::primitives::vector::cosine_similarity;
15
16#[derive(Debug, Clone, Copy, Default)]
18pub struct AudioMetrics {
19 pub bass: f32,
20 pub mid: f32,
21 pub high: f32,
22 pub entropy: f32,
23}
24
25pub fn gravity_coupling(
28 embedding_a: &[f32],
29 embedding_b: &[f32],
30 k_base: f32,
31 gravity_scale: f32,
32) -> f32 {
33 let sim = cosine_similarity(embedding_a, embedding_b);
34 k_base * gravity_scale * sim.max(0.0)
35}
36
37pub fn coupled_step(
48 atoms: &mut [ComputeAtom],
49 audio: &AudioMetrics,
50 k_base: f32,
51 gravity_scale: f32,
52 dt: f32,
53) -> Vec<f32> {
54 let n = atoms.len();
55 if n == 0 {
56 return vec![];
57 }
58
59 let k = k_base * (1.0 + audio.bass * 2.0);
61
62 let mut phases = Vec::with_capacity(n);
64 let mut omegas = Vec::with_capacity(n);
65 for atom in atoms.iter() {
66 if let Some(kp) = atom.read_projection::<KuramotoProjection>() {
67 phases.push(kp.theta);
68 omegas.push(kp.omega);
69 } else {
70 phases.push(0.0);
71 omegas.push(0.0);
72 }
73 }
74
75 let adjacency: Vec<Vec<f32>> = if atoms.iter().any(|a| a.layout.has(super::projection::ProjectionId::Splat)) {
80 let embeddings: Vec<Option<SplatProjection>> = atoms
82 .iter()
83 .map(|a| a.read_projection::<SplatProjection>())
84 .collect();
85
86 (0..n)
87 .map(|i| {
88 (0..n)
89 .map(|j| {
90 if i == j {
91 return 0.0;
92 }
93 match (&embeddings[i], &embeddings[j]) {
94 (Some(ei), Some(ej)) => {
95 gravity_coupling(&ei.embedding, &ej.embedding, k, gravity_scale)
96 }
97 _ => k, }
99 })
100 .collect()
101 })
102 .collect()
103 } else {
104 vec![vec![k; n]; n]
106 };
107
108 let adj_refs: Vec<&[f32]> = adjacency.iter().map(|row| row.as_slice()).collect();
110
111 let new_phases = kuramoto_step(
113 &phases,
114 &Omega::PerNode(omegas),
115 1.0, Some(&adj_refs),
117 dt,
118 true, );
120
121 for (i, atom) in atoms.iter_mut().enumerate() {
123 if let Some(mut kp) = atom.read_projection::<KuramotoProjection>() {
124 kp.theta = new_phases[i];
125 atom.write_projection(&kp);
126 }
127 }
128
129 new_phases
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use super::super::layout::ProjectionLayout;
136 use crate::phase::kuramoto::order_parameter;
137
138 #[test]
139 fn test_gravity_coupling_identical() {
140 let e = vec![1.0f32; 384];
141 let c = gravity_coupling(&e, &e, 1.0, 1.0);
142 assert!((c - 1.0).abs() < 1e-3, "coupling = {}", c);
143 }
144
145 #[test]
146 fn test_gravity_coupling_orthogonal() {
147 let mut a = vec![0.0f32; 384];
148 let mut b = vec![0.0f32; 384];
149 a[0] = 1.0;
150 b[1] = 1.0;
151 let c = gravity_coupling(&a, &b, 1.0, 1.0);
152 assert!(c.abs() < 1e-3, "coupling = {}", c);
153 }
154
155 #[test]
156 fn test_coupled_step_converges() {
157 let layout = ProjectionLayout::minimal(); let mut atoms = ComputeAtom::create_n(&layout, 6);
159
160 for (i, atom) in atoms.iter_mut().enumerate() {
162 let mut splat = SplatProjection::default();
163 splat.embedding[0] = 1.0; atom.write_projection(&splat);
165
166 let k = KuramotoProjection {
167 theta: i as f32,
168 omega: 0.0,
169 coupling: 1.0,
170 };
171 atom.write_projection(&k);
172 }
173
174 let phases_before: Vec<f32> = atoms
175 .iter()
176 .filter_map(|a| a.read_projection::<KuramotoProjection>())
177 .map(|k| k.theta)
178 .collect();
179 let r_before = order_parameter(&phases_before).r;
180
181 let audio = AudioMetrics { bass: 0.5, mid: 0.3, high: 0.2, entropy: 0.5 };
182
183 for _ in 0..1000 {
185 coupled_step(&mut atoms, &audio, 5.0, 1.0, 0.01);
186 }
187
188 let phases_after: Vec<f32> = atoms
189 .iter()
190 .filter_map(|a| a.read_projection::<KuramotoProjection>())
191 .map(|k| k.theta)
192 .collect();
193 let r_after = order_parameter(&phases_after).r;
194
195 assert!(
196 r_after > r_before,
197 "R should increase: before={} after={}",
198 r_before,
199 r_after
200 );
201 assert!(r_after > 0.9, "R should converge: R={}", r_after);
202 }
203
204 #[test]
205 fn test_coupled_step_empty() {
206 let result = coupled_step(&mut [], &AudioMetrics::default(), 1.0, 1.0, 0.01);
207 assert!(result.is_empty());
208 }
209
210 #[test]
211 fn test_coupled_step_preserves_atom_count() {
212 let layout = ProjectionLayout::minimal();
213 let mut atoms = ComputeAtom::create_n(&layout, 10);
214 let result = coupled_step(&mut atoms, &AudioMetrics::default(), 1.0, 1.0, 0.01);
215 assert_eq!(result.len(), 10);
216 }
217}