use rand::Rng;
use std::f64;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AosStrategy {
ProbabilityMatching {
alpha: f64,
learning_rate: f64,
},
AdaptivePursuit {
beta: f64,
c: f64,
},
MultiArmedBandit {
c: f64,
epsilon: f64,
},
}
impl AosStrategy {
pub fn pm_default() -> Self {
AosStrategy::ProbabilityMatching {
alpha: 0.8,
learning_rate: 0.3,
}
}
pub fn ap_default() -> Self {
AosStrategy::AdaptivePursuit { beta: 0.5, c: 1.5 }
}
pub fn mab_default() -> Self {
AosStrategy::MultiArmedBandit {
c: 1.0,
epsilon: 0.1,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct ArmState {
rewards: Vec<f64>,
cursor: usize,
count: usize,
}
impl ArmState {
fn new(window_size: usize) -> Self {
ArmState {
rewards: vec![0.0; window_size],
cursor: 0,
count: 0,
}
}
fn add_reward(&mut self, reward: f64) {
self.rewards[self.cursor] = reward;
self.cursor = (self.cursor + 1) % self.rewards.len();
self.count = (self.count + 1).min(self.rewards.len());
}
fn mean_reward(&self) -> f64 {
let n = self.count.max(1);
self.rewards[..self.count].iter().sum::<f64>() / n as f64
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AosState {
num_arms: usize,
arms: Vec<ArmState>,
probabilities: Vec<f64>,
selection_counts: Vec<usize>,
total_selections: usize,
strategy: AosStrategy,
window_size: usize,
exploration_generations: usize,
}
impl AosState {
pub fn new(num_arms: usize, strategy: AosStrategy, window_size: usize) -> Self {
let arms: Vec<ArmState> = (0..num_arms).map(|_| ArmState::new(window_size)).collect();
let probabilities = vec![1.0 / num_arms.max(1) as f64; num_arms];
let selection_counts = vec![0; num_arms];
AosState {
num_arms,
arms,
probabilities,
selection_counts,
total_selections: 0,
strategy,
window_size,
exploration_generations: window_size / 2,
}
}
pub fn select_operator(&mut self, rng: &mut impl Rng, generation: usize) -> usize {
if self.num_arms == 0 {
return 0;
}
let op_idx = if generation < self.exploration_generations {
rng.random_range(0..self.num_arms)
} else {
match self.strategy {
AosStrategy::ProbabilityMatching { .. } | AosStrategy::AdaptivePursuit { .. } => {
self.roulette_wheel_select(rng)
}
AosStrategy::MultiArmedBandit { c, epsilon } => self.mab_select(rng, c, epsilon),
}
};
self.selection_counts[op_idx] = self.selection_counts[op_idx].saturating_add(1);
self.total_selections = self.total_selections.saturating_add(1);
op_idx
}
fn roulette_wheel_select(&self, rng: &mut impl Rng) -> usize {
let prob_sum: f64 = self.probabilities.iter().sum();
if prob_sum <= 0.0 {
return rng.random_range(0..self.num_arms);
}
let r = rng.random_range(0.0..prob_sum);
let mut cumulative = 0.0;
for (i, &p) in self.probabilities.iter().enumerate() {
cumulative += p;
if r < cumulative {
return i;
}
}
self.num_arms - 1 }
fn mab_select(&self, rng: &mut impl Rng, c: f64, epsilon: f64) -> usize {
if rng.random_range(0.0..1.0) < epsilon {
return rng.random_range(0..self.num_arms);
}
let total = self.total_selections.max(1) as f64;
let mut best_arm = 0;
let mut best_ucb = f64::NEG_INFINITY;
for i in 0..self.num_arms {
let n = self.selection_counts[i];
if n == 0 {
return i;
}
let mean = self.arms[i].mean_reward();
let ucb = mean + c * (2.0 * total.ln() / n as f64).sqrt();
if ucb > best_ucb {
best_ucb = ucb;
best_arm = i;
}
}
best_arm
}
pub fn record_rewards(&mut self, rewards: &[(usize, f64)]) {
for &(op_idx, reward) in rewards {
if op_idx < self.num_arms {
self.arms[op_idx].add_reward(reward);
}
}
}
pub fn update(&mut self) {
match self.strategy {
AosStrategy::ProbabilityMatching {
alpha,
learning_rate: _,
} => {
self.update_pm(alpha);
}
AosStrategy::AdaptivePursuit { beta, c: _ } => {
self.update_ap(beta);
}
AosStrategy::MultiArmedBandit { .. } => {
}
}
}
fn update_pm(&mut self, alpha: f64) {
let p_min = 1.0 / (self.num_arms.max(1) as f64 * 1.5);
let credits: Vec<f64> = (0..self.num_arms)
.map(|i| self.arms[i].mean_reward())
.collect();
for (prob, credit) in self.probabilities.iter_mut().zip(credits.iter()) {
let new_prob = *prob + alpha * (credit - *prob);
*prob = new_prob.clamp(p_min, 1.0);
}
self.normalize_probabilities();
}
fn update_ap(&mut self, beta: f64) {
let best_idx = (0..self.num_arms)
.max_by(|&a, &b| {
self.arms[a]
.mean_reward()
.partial_cmp(&self.arms[b].mean_reward())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
let n = self.num_arms.max(2) as f64;
for i in 0..self.num_arms {
if i == best_idx {
let target = 0.9;
self.probabilities[i] =
self.probabilities[i] + beta * (target - self.probabilities[i]);
} else {
let target = 0.1 / (n - 1.0);
self.probabilities[i] =
self.probabilities[i] + beta * (target - self.probabilities[i]);
}
self.probabilities[i] = self.probabilities[i].clamp(0.01, 0.99);
}
self.normalize_probabilities();
}
fn normalize_probabilities(&mut self) {
let sum: f64 = self.probabilities.iter().sum();
if sum > 0.0 {
for p in self.probabilities.iter_mut() {
*p /= sum;
}
}
}
pub fn probabilities(&self) -> &[f64] {
&self.probabilities
}
pub fn num_arms(&self) -> usize {
self.num_arms
}
pub fn exploration_generations(&self) -> usize {
self.exploration_generations
}
pub fn window_size(&self) -> usize {
self.window_size
}
pub fn strategy(&self) -> &AosStrategy {
&self.strategy
}
pub fn selection_counts(&self) -> &[usize] {
&self.selection_counts
}
pub fn total_selections(&self) -> usize {
self.total_selections
}
pub fn arm_mean_reward(&self, idx: usize) -> f64 {
if idx < self.num_arms {
self.arms[idx].mean_reward()
} else {
0.0
}
}
}
pub fn compute_normalized_reward(
parent_fitness: f64,
offspring_fitness: f64,
best_fitness: f64,
) -> f64 {
let delta = parent_fitness - offspring_fitness;
let denom = best_fitness.abs().max(f64::EPSILON);
delta / denom
}