radiate-core 1.3.1

Core traits and interfaces for the Radiate genetic algorithm library.
Documentation
use crate::metric_names;
use crate::{Expr, stats::metric_fields};

const KP: f32 = 0.05_f32;
const KI: f32 = 0.005_f32;
const KD: f32 = 0.02_f32;

pub fn species_error_signal(count: usize) -> Expr {
    Expr::select(metric_names::SPECIES_COUNT).error(count as f32)
}

pub fn species_target_control(target: usize, base_val: f32) -> Expr {
    let target_f32 = target as f32;
    let raw_error = species_error_signal(target);

    // Proportional: smoothed count so single-gen bursts don't cause hard jumps
    let proportional = Expr::select(metric_names::SPECIES_COUNT)
        .rolling(3)
        .mean()
        .error(target_f32)
        * KP;

    // Integral: accumulated recent error over a rolling window
    // Derivative: velocity of the error — anticipates rising/falling count
    let integral = raw_error.clone().rolling(20).sum() * KI;
    let derivative = raw_error.rolling(5).slope() * KD;

    Expr::when(Expr::select(metric_names::INDEX).lt(2_i32))
        .then(base_val)
        .otherwise(
            Expr::select(metric_names::SPECIES_THRESHOLD) + proportional + integral + derivative,
        )
        .clamp(0.0_f32, target_f32 * 2.5_f32)
        .alias(metric_names::SPECIES_THRESHOLD)
}

// Rolling slope of best score — useful for limits and convergence detection
pub fn score_trend_signal(window: usize) -> Expr {
    Expr::select(metric_names::BEST_SCORES)
        .rolling(window)
        .slope()
        .alias(format!("{}.[{}]", metric_names::SCORES_TREND, window))
}

// Coefficient of variation — normalized score spread
pub fn score_cv_signal(window: usize) -> Expr {
    Expr::select(metric_names::BEST_SCORES)
        .rolling(window)
        .stddev()
        .div(
            Expr::select(metric_names::BEST_SCORES)
                .rolling(window)
                .mean(),
        )
}

// Throttles add-vertex/add-edge rates as genome grows past target
pub fn genome_size_throttle(base_rate: impl Into<Expr>, target_size: usize) -> Expr {
    let pressure = Expr::select(metric_names::GENOME_SIZE)
        .rolling(10)
        .mean()
        .div(target_size as f32)
        .clamp(1.0_f32, 5.0_f32);
    base_rate.into().div(pressure)
}

// Higher when diversity is low, lower when healthy
pub fn diversity_signal(window: usize, min: f32, max: f32) -> Expr {
    let diversity = Expr::select(metric_names::PCT_DIVERSITY)
        .rolling(window)
        .mean();
    (Expr::lit(1.0_f32) - diversity)
        .mul(max - min)
        .add(min)
        .clamp(min, max)
        .alias(format!("{}.[{}]", metric_names::PCT_DIVERSITY, window))
}

// True when best score hasn't meaningfully moved in `window` generations
pub fn stagnation_expr(window: usize, epsilon: f32) -> Expr {
    Expr::select(metric_names::BEST_SCORES)
        .rolling(window)
        .slope()
        .abs()
        .lt(epsilon)
}

// Bloat pressure: throttle growth mutation only when genome size is
// growing WITHOUT a corresponding fitness payoff. Distinguishes genuine
// bloat (size↑, corr weak) from justified growth (size↑, corr strong) —
pub fn bloat_pressure_signal(base_rate: impl Into<Expr>, corr_floor: f32) -> Expr {
    let base_rate = base_rate.into();
    let growing = Expr::select(metric_names::GENOME_SIZE)
        .attr(metric_fields::MEAN)
        .rolling(10)
        .slope()
        .gt(0.0_f32);

    let weak_payoff = Expr::select(metric_names::SIZE_SCORE_CORR)
        .rolling(10)
        .mean()
        .abs()
        .lt(corr_floor);

    Expr::warmup(1)
        .then(
            Expr::when(growing.and(weak_payoff))
                .then(base_rate.clone() * 0.5_f32)
                .otherwise(base_rate.clone()),
        )
        .otherwise(base_rate)
}