use serde::{Deserialize, Serialize};
use super::atom::ComputeAtom;
use super::kuramoto::KuramotoProjection;
use crate::phase::kuramoto::order_parameter;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ConvergenceWitness {
pub r: f32,
pub entropy: f32,
pub converged: bool,
pub step: u32,
}
impl Default for ConvergenceWitness {
fn default() -> Self {
Self {
r: 0.0,
entropy: 0.0,
converged: false,
step: 0,
}
}
}
pub const CONVERGENCE_THRESHOLD: f32 = 0.9;
pub fn compute_witness(atoms: &[ComputeAtom], step: u32) -> ConvergenceWitness {
if atoms.is_empty() {
return ConvergenceWitness::default();
}
let phases: Vec<f32> = atoms
.iter()
.filter_map(|a| a.read_projection::<KuramotoProjection>())
.map(|k| k.theta)
.collect();
if phases.is_empty() {
return ConvergenceWitness {
r: 0.0,
entropy: 0.0,
converged: false,
step,
};
}
let op = order_parameter(&phases);
let entropy = compute_activation_entropy(atoms);
ConvergenceWitness {
r: op.r,
entropy,
converged: op.r >= CONVERGENCE_THRESHOLD,
step,
}
}
fn compute_activation_entropy(atoms: &[ComputeAtom]) -> f32 {
use super::expert::ExpertProjection;
let activations: Vec<f32> = atoms
.iter()
.filter_map(|a| {
a.read_projection::<ExpertProjection>()
.map(|e| e.activation.max(0.0))
})
.collect();
if activations.is_empty() {
let couplings: Vec<f32> = atoms
.iter()
.filter_map(|a| {
a.read_projection::<KuramotoProjection>()
.map(|k| k.coupling.max(0.0))
})
.collect();
return shannon_entropy(&couplings);
}
shannon_entropy(&activations)
}
fn shannon_entropy(values: &[f32]) -> f32 {
if values.is_empty() {
return 0.0;
}
let total: f32 = values.iter().sum();
if total <= 0.0 {
return 0.0;
}
let mut h = 0.0f32;
for &v in values {
let p = v / total;
if p > 0.0 {
h -= p * p.log2();
}
}
let max_h = (values.len() as f32).log2();
if max_h > 0.0 {
h / max_h
} else {
0.0
}
}
pub fn verify_monotonic(witnesses: &[ConvergenceWitness], tolerance: f32) -> Option<usize> {
for i in 1..witnesses.len() {
if witnesses[i].r < witnesses[i - 1].r - tolerance {
return Some(i);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::layout::ProjectionLayout;
#[test]
fn test_witness_empty_atoms() {
let w = compute_witness(&[], 0);
assert_eq!(w.r, 0.0);
assert!(!w.converged);
}
#[test]
fn test_witness_synchronized_atoms() {
let layout = ProjectionLayout::full();
let mut atoms = ComputeAtom::create_n(&layout, 10);
for atom in &mut atoms {
let k = KuramotoProjection {
theta: 0.0,
omega: 0.0,
coupling: 1.0,
};
atom.write_projection(&k);
}
let w = compute_witness(&atoms, 1);
assert!((w.r - 1.0).abs() < 1e-6, "R = {}", w.r);
assert!(w.converged);
assert_eq!(w.step, 1);
}
#[test]
fn test_witness_desynchronized_atoms() {
let layout = ProjectionLayout::full();
let mut atoms = ComputeAtom::create_n(&layout, 100);
for (i, atom) in atoms.iter_mut().enumerate() {
let theta = 2.0 * std::f32::consts::PI * i as f32 / 100.0;
let k = KuramotoProjection {
theta,
omega: 0.0,
coupling: 1.0,
};
atom.write_projection(&k);
}
let w = compute_witness(&atoms, 5);
assert!(w.r < 0.1, "R = {} (expected < 0.1)", w.r);
assert!(!w.converged);
}
#[test]
fn test_verify_monotonic_clean() {
let witnesses = vec![
ConvergenceWitness { r: 0.3, entropy: 0.5, converged: false, step: 0 },
ConvergenceWitness { r: 0.5, entropy: 0.4, converged: false, step: 1 },
ConvergenceWitness { r: 0.9, entropy: 0.2, converged: true, step: 2 },
];
assert_eq!(verify_monotonic(&witnesses, 0.02), None);
}
#[test]
fn test_verify_monotonic_violation() {
let witnesses = vec![
ConvergenceWitness { r: 0.8, entropy: 0.3, converged: false, step: 0 },
ConvergenceWitness { r: 0.5, entropy: 0.5, converged: false, step: 1 },
];
assert_eq!(verify_monotonic(&witnesses, 0.02), Some(1));
}
#[test]
fn test_shannon_entropy_uniform() {
let values = vec![1.0f32; 8];
let h = shannon_entropy(&values);
assert!((h - 1.0).abs() < 1e-6, "H = {}", h);
}
#[test]
fn test_shannon_entropy_single() {
let values = vec![1.0, 0.0, 0.0, 0.0];
let h = shannon_entropy(&values);
assert!(h < 0.01, "H = {}", h);
}
}