use std::fmt;
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Clone)]
pub enum FitnessMetric {
HarmonicRichness,
RhythmicVariance,
TimbralDiversity,
UserDefined(Arc<dyn Fn(&[[f64; 16]]) -> f64 + Send + Sync>),
}
impl fmt::Debug for FitnessMetric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FitnessMetric::HarmonicRichness => write!(f, "HarmonicRichness"),
FitnessMetric::RhythmicVariance => write!(f, "RhythmicVariance"),
FitnessMetric::TimbralDiversity => write!(f, "TimbralDiversity"),
FitnessMetric::UserDefined(_) => write!(f, "UserDefined(<fn>)"),
}
}
}
impl PartialEq for FitnessMetric {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(FitnessMetric::HarmonicRichness, FitnessMetric::HarmonicRichness)
| (FitnessMetric::RhythmicVariance, FitnessMetric::RhythmicVariance)
| (FitnessMetric::TimbralDiversity, FitnessMetric::TimbralDiversity)
)
}
}
impl fmt::Display for FitnessMetric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FitnessMetric::HarmonicRichness => write!(f, "Harmonic Richness"),
FitnessMetric::RhythmicVariance => write!(f, "Rhythmic Variance"),
FitnessMetric::TimbralDiversity => write!(f, "Timbral Diversity"),
FitnessMetric::UserDefined(_) => write!(f, "User Defined"),
}
}
}
#[derive(Clone, Debug)]
pub struct EvolutionConfig {
pub population_size: usize,
pub generations: usize,
pub mutation_rate: f64,
pub crossover_rate: f64,
pub eval_steps: usize,
pub eval_dt: f64,
pub tournament_size: usize,
}
impl Default for EvolutionConfig {
fn default() -> Self {
Self {
population_size: 40,
generations: 50,
mutation_rate: 0.1,
crossover_rate: 0.7,
eval_steps: 2000,
eval_dt: 0.005,
tournament_size: 3,
}
}
}
#[derive(Clone, Debug)]
pub struct Individual {
pub params: Vec<f64>,
pub fitness: f64,
}
impl Individual {
pub fn new(params: Vec<f64>) -> Self {
Self {
params,
fitness: f64::NEG_INFINITY,
}
}
}
#[derive(Clone, Debug)]
pub struct SavedEvolution {
pub name: String,
pub params: Vec<f64>,
pub fitness: f64,
pub metric: String,
pub generation: usize,
}
impl SavedEvolution {
pub fn to_toml_snippet(&self) -> String {
let params_str = self
.params
.iter()
.map(|p| format!("{p:.6}"))
.collect::<Vec<_>>()
.join(", ");
format!(
"[[evolved_preset]]\n\
name = \"{}\"\n\
metric = \"{}\"\n\
fitness = {:.6}\n\
generation = {}\n\
params = [{}]\n",
self.name, self.metric, self.fitness, self.generation, params_str
)
}
}
#[derive(Clone, Debug, Default)]
pub struct EvolutionState {
pub current_generation: usize,
pub best_fitness: f64,
pub fitness_history: Vec<f64>,
pub best_params: Vec<f64>,
pub running: bool,
pub total_generations: usize,
}
pub type SharedEvolutionState = Arc<Mutex<EvolutionState>>;
#[derive(Clone, Copy, Debug)]
pub struct ParamBounds {
pub min: f64,
pub max: f64,
}
impl ParamBounds {
pub fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
pub fn range(&self) -> f64 {
self.max - self.min
}
pub fn clamp(&self, v: f64) -> f64 {
v.clamp(self.min, self.max)
}
}
pub struct ParameterEvolution {
config: EvolutionConfig,
metric: FitnessMetric,
bounds: Vec<ParamBounds>,
state: SharedEvolutionState,
population: Vec<Individual>,
rng_seed: u64,
}
impl ParameterEvolution {
pub fn new(
config: EvolutionConfig,
metric: FitnessMetric,
bounds: Vec<ParamBounds>,
state: SharedEvolutionState,
) -> Self {
let rng_seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(12345);
Self {
config,
metric,
bounds,
state,
population: Vec::new(),
rng_seed,
}
}
fn next_u64(&mut self) -> u64 {
let mut x = self.rng_seed;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.rng_seed = x;
x
}
fn rand_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn rand_normal(&mut self) -> f64 {
let u1 = self.rand_f64().max(1e-15);
let u2 = self.rand_f64();
(-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
}
fn initialise_population(&mut self) {
let n = self.config.population_size;
let bounds: Vec<_> = self.bounds.iter().map(|b| (b.min, b.range())).collect();
self.population = (0..n)
.map(|_| {
let params = bounds
.iter()
.map(|(min, range)| min + self.rand_f64() * range)
.collect();
Individual::new(params)
})
.collect();
}
fn evaluate_population<F>(&mut self, ode_step: &F)
where
F: Fn(&mut [f64; 16], &[f64]) + Sync,
{
let to_evaluate: Vec<(usize, Vec<f64>)> = self
.population
.iter()
.enumerate()
.filter(|(_, ind)| !ind.fitness.is_finite())
.map(|(i, ind)| (i, ind.params.clone()))
.collect();
for (i, params) in to_evaluate {
let fitness = self.evaluate_individual(¶ms, ode_step);
self.population[i].fitness = fitness;
}
}
fn evaluate_individual<F>(&mut self, params: &[f64], ode_step: &F) -> f64
where
F: Fn(&mut [f64; 16], &[f64]),
{
let mut state = [0.01f64; 16];
let mut trajectory: Vec<[f64; 16]> = Vec::with_capacity(self.config.eval_steps);
for _ in 0..200 {
ode_step(&mut state, params);
}
for _ in 0..self.config.eval_steps {
ode_step(&mut state, params);
trajectory.push(state);
}
self.compute_fitness(&trajectory)
}
fn compute_fitness(&self, trajectory: &[[f64; 16]]) -> f64 {
if trajectory.is_empty() {
return 0.0;
}
match &self.metric {
FitnessMetric::HarmonicRichness => harmonic_richness(trajectory),
FitnessMetric::RhythmicVariance => rhythmic_variance(trajectory),
FitnessMetric::TimbralDiversity => timbral_diversity(trajectory),
FitnessMetric::UserDefined(f) => f(trajectory).clamp(0.0, 1.0),
}
}
fn tournament_select(&mut self) -> usize {
let k = self.config.tournament_size.min(self.population.len());
let mut best_idx = self.random_index(self.population.len());
for _ in 1..k {
let idx = self.random_index(self.population.len());
if self.population[idx].fitness > self.population[best_idx].fitness {
best_idx = idx;
}
}
best_idx
}
fn random_index(&mut self, len: usize) -> usize {
(self.next_u64() as usize) % len
}
fn crossover(&mut self, a: &[f64], b: &[f64]) -> (Vec<f64>, Vec<f64>) {
if self.rand_f64() > self.config.crossover_rate || a.len() != b.len() {
return (a.to_vec(), b.to_vec());
}
let mut child_a = a.to_vec();
let mut child_b = b.to_vec();
for i in 0..a.len() {
if self.rand_f64() < 0.5 {
child_a[i] = b[i];
child_b[i] = a[i];
}
}
(child_a, child_b)
}
fn mutate(&mut self, params: &mut Vec<f64>) {
let bounds: Vec<_> = self.bounds.iter().map(|b| (b.min, b.max, b.range())).collect();
let mutation_rate = self.config.mutation_rate;
for (p, (min, max, range)) in params.iter_mut().zip(bounds.iter()) {
if self.rand_f64() < mutation_rate {
let sigma = range * 0.05;
*p += self.rand_normal() * sigma;
*p = p.clamp(*min, *max);
}
}
}
fn best_index(&self) -> usize {
self.population
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.fitness.partial_cmp(&b.fitness).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn run<F>(&mut self, preset_name: &str, ode_step: F) -> SavedEvolution
where
F: Fn(&mut [f64; 16], &[f64]) + Sync,
{
self.initialise_population();
{
let mut s = self.state.lock();
s.running = true;
s.current_generation = 0;
s.best_fitness = f64::NEG_INFINITY;
s.fitness_history.clear();
s.total_generations = self.config.generations;
}
let mut global_best = Individual::new(
self.bounds.iter().map(|b| (b.min + b.max) * 0.5).collect(),
);
for gen in 0..self.config.generations {
self.evaluate_population(&ode_step);
let bi = self.best_index();
let gen_best = self.population[bi].clone();
if gen_best.fitness > global_best.fitness {
global_best = gen_best.clone();
}
{
let mut s = self.state.lock();
s.current_generation = gen + 1;
s.best_fitness = global_best.fitness;
s.fitness_history.push(global_best.fitness);
s.best_params = global_best.params.clone();
}
let mut next_gen: Vec<Individual> = Vec::with_capacity(self.config.population_size);
next_gen.push(Individual {
params: global_best.params.clone(),
fitness: global_best.fitness,
});
while next_gen.len() < self.config.population_size {
let pa_idx = self.tournament_select();
let pb_idx = self.tournament_select();
let pa = self.population[pa_idx].params.clone();
let pb = self.population[pb_idx].params.clone();
let (mut ca, mut cb) = self.crossover(&pa, &pb);
self.mutate(&mut ca);
self.mutate(&mut cb);
next_gen.push(Individual::new(ca));
if next_gen.len() < self.config.population_size {
next_gen.push(Individual::new(cb));
}
}
self.population = next_gen;
}
{
let mut s = self.state.lock();
s.running = false;
}
SavedEvolution {
name: preset_name.to_string(),
params: global_best.params,
fitness: global_best.fitness,
metric: self.metric.to_string(),
generation: self.config.generations,
}
}
}
fn harmonic_richness(trajectory: &[[f64; 16]]) -> f64 {
let n = trajectory.len();
if n < 64 {
return 0.0;
}
let x: Vec<f64> = trajectory.iter().map(|s| s[0]).collect();
let mean = x.iter().sum::<f64>() / n as f64;
let x: Vec<f64> = x.iter().map(|v| v - mean).collect();
let r0: f64 = x.iter().map(|v| v * v).sum::<f64>() / n as f64;
if r0 < 1e-12 {
return 0.0;
}
let max_lag = n / 4;
let mut peak = 0.0f64;
for lag in 8..max_lag {
let r: f64 = x[..n - lag]
.iter()
.zip(&x[lag..])
.map(|(a, b)| a * b)
.sum::<f64>()
/ (n - lag) as f64;
peak = peak.max(r.abs());
}
(peak / r0).clamp(0.0, 1.0)
}
fn rhythmic_variance(trajectory: &[[f64; 16]]) -> f64 {
let n = trajectory.len();
if n < 32 {
return 0.0;
}
let env: Vec<f64> = trajectory.iter().map(|s| s[0].abs()).collect();
let window = 8_usize;
let smoothed: Vec<f64> = env
.windows(window)
.map(|w| w.iter().sum::<f64>() / window as f64)
.collect();
let mean = smoothed.iter().sum::<f64>() / smoothed.len() as f64;
let mut onsets: Vec<usize> = Vec::new();
for i in 1..smoothed.len().saturating_sub(1) {
if smoothed[i] > mean && smoothed[i] > smoothed[i - 1] && smoothed[i] > smoothed[i + 1] {
onsets.push(i);
}
}
if onsets.len() < 3 {
return 0.0;
}
let iois: Vec<f64> = onsets
.windows(2)
.map(|w| (w[1] - w[0]) as f64)
.collect();
let mean_ioi = iois.iter().sum::<f64>() / iois.len() as f64;
if mean_ioi < 1e-12 {
return 0.0;
}
let var = iois.iter().map(|&x| (x - mean_ioi).powi(2)).sum::<f64>() / iois.len() as f64;
let cv = var.sqrt() / mean_ioi;
let score = (-((cv - 0.5) / 0.3).powi(2)).exp();
score.clamp(0.0, 1.0)
}
fn timbral_diversity(trajectory: &[[f64; 16]]) -> f64 {
let n = trajectory.len();
if n < 16 {
return 0.0;
}
let centroids: Vec<f64> = trajectory
.iter()
.map(|s| {
let total: f64 = s[..4].iter().map(|v| v.abs()).sum();
if total < 1e-12 {
return 0.0;
}
s[..4]
.iter()
.enumerate()
.map(|(i, v)| i as f64 * v.abs())
.sum::<f64>()
/ total
})
.collect();
let mean = centroids.iter().sum::<f64>() / n as f64;
let std =
(centroids.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64).sqrt();
(std / 1.5).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn lorenz_step(state: &mut [f64; 16], params: &[f64]) {
let (sigma, rho, beta) = (params[0], params[1], params[2]);
let dt = 0.005;
let (x, y, z) = (state[0], state[1], state[2]);
let dx = sigma * (y - x);
let dy = x * (rho - z) - y;
let dz = x * y - beta * z;
state[0] += dx * dt;
state[1] += dy * dt;
state[2] += dz * dt;
}
fn lorenz_bounds() -> Vec<ParamBounds> {
vec![
ParamBounds::new(5.0, 15.0),
ParamBounds::new(20.0, 35.0),
ParamBounds::new(1.0, 4.0),
]
}
#[test]
fn evolution_runs_without_panic() {
let cfg = EvolutionConfig {
population_size: 6,
generations: 3,
eval_steps: 100,
..Default::default()
};
let state: SharedEvolutionState = Arc::new(Mutex::new(EvolutionState::default()));
let mut evo = ParameterEvolution::new(
cfg,
FitnessMetric::HarmonicRichness,
lorenz_bounds(),
Arc::clone(&state),
);
let saved = evo.run("test_preset", lorenz_step);
assert!(!saved.params.is_empty());
assert!(saved.fitness.is_finite());
}
#[test]
fn fitness_history_length_matches_generations() {
let generations = 4;
let cfg = EvolutionConfig {
population_size: 4,
generations,
eval_steps: 50,
..Default::default()
};
let state2: SharedEvolutionState = Arc::new(Mutex::new(EvolutionState::default()));
let mut evo2 = ParameterEvolution::new(
cfg,
FitnessMetric::RhythmicVariance,
lorenz_bounds(),
Arc::clone(&state2),
);
evo2.run("h", lorenz_step);
let hist = state2.lock().fitness_history.clone();
assert_eq!(hist.len(), generations, "history length {}", hist.len());
}
#[test]
fn saved_evolution_toml_contains_name() {
let se = SavedEvolution {
name: "My Preset".into(),
params: vec![10.0, 28.0, 2.666],
fitness: 0.75,
metric: "HarmonicRichness".into(),
generation: 5,
};
let t = se.to_toml_snippet();
assert!(t.contains("My Preset"), "toml: {t}");
assert!(t.contains("0.750000"), "toml: {t}");
}
#[test]
fn param_bounds_clamp() {
let b = ParamBounds::new(1.0, 5.0);
assert_eq!(b.clamp(0.0), 1.0);
assert_eq!(b.clamp(10.0), 5.0);
assert_eq!(b.clamp(3.0), 3.0);
}
#[test]
fn harmonic_richness_sine_like_scores_high() {
let n = 1024;
let traj: Vec<[f64; 16]> = (0..n)
.map(|i| {
let mut s = [0.0f64; 16];
s[0] = (i as f64 * std::f64::consts::TAU / 64.0).sin();
s
})
.collect();
let score = harmonic_richness(&traj);
assert!(score > 0.3, "sine richness={score}");
}
#[test]
fn timbral_diversity_constant_scores_zero() {
let traj: Vec<[f64; 16]> = vec![[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; 200];
let score = timbral_diversity(&traj);
assert!(score < 0.05, "constant diversity={score}");
}
}