use crate::{Evaluator, Evolver, Genotype, Phenotype};
use rand::Rng;
use rand::prelude::SeedableRng;
use rand_pcg::Pcg64;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub trait BehaviourDistance: Send + Sync {
fn distance(&self, a: &[f32], b: &[f32]) -> f32;
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct EuclideanDistance;
impl BehaviourDistance for EuclideanDistance {
fn distance(&self, a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ArchivePolicy {
AlwaysAdd,
Probabilistic(f32),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct NoveltyConfig {
pub k: usize,
pub alpha: f32,
pub policy: ArchivePolicy,
}
impl NoveltyConfig {
pub fn new(k: usize, alpha: f32, policy: ArchivePolicy) -> Self {
let cfg = Self { k, alpha, policy };
cfg.validate().expect("invalid NoveltyConfig");
cfg
}
fn validate(&self) -> Result<(), String> {
if self.k == 0 {
return Err("k must be greater than 0".into());
}
if !(0.0..=1.0).contains(&self.alpha) {
return Err(format!("alpha must be in [0.0, 1.0], got {}", self.alpha));
}
if let ArchivePolicy::Probabilistic(p) = self.policy
&& (!(0.0..=1.0).contains(&p) || p <= 0.0)
{
return Err(format!(
"probabilistic archive policy requires 0.0 < p <= 1.0, got {p}"
));
}
Ok(())
}
}
fn cmp_f32_nan_last(a: f32, b: f32) -> Ordering {
match (a.is_nan(), b.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
(false, false) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
}
}
#[derive(Serialize, Deserialize)]
#[serde(bound(
serialize = "G: Genotype, D: BehaviourDistance + Serialize",
deserialize = "G: Genotype, D: BehaviourDistance + Deserialize<'de>"
))]
struct NoveltySearchData<G: Genotype, D: BehaviourDistance> {
population: Vec<Phenotype<G>>,
pop_size: usize,
novelty: Vec<f32>,
behaviour_archive: Vec<Vec<f32>>,
mutation_rate: f32,
elitism: usize,
k: usize,
alpha: f32,
policy: ArchivePolicy,
distance: D,
rng: Pcg64,
}
pub struct NoveltySearch<G: Genotype, D: BehaviourDistance = EuclideanDistance> {
population: Vec<Phenotype<G>>,
pop_size: usize,
novelty: Vec<f32>,
behaviour_archive: Vec<Vec<f32>>,
mutation_rate: f32,
elitism: usize,
k: usize,
alpha: f32,
policy: ArchivePolicy,
distance: D,
rng: Pcg64,
}
impl<G: Genotype, D: BehaviourDistance + Serialize> Serialize for NoveltySearch<G, D> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("NoveltySearch", 11)?;
state.serialize_field("population", &self.population)?;
state.serialize_field("pop_size", &self.pop_size)?;
state.serialize_field("novelty", &self.novelty)?;
state.serialize_field("behaviour_archive", &self.behaviour_archive)?;
state.serialize_field("mutation_rate", &self.mutation_rate)?;
state.serialize_field("elitism", &self.elitism)?;
state.serialize_field("k", &self.k)?;
state.serialize_field("alpha", &self.alpha)?;
state.serialize_field("policy", &self.policy)?;
state.serialize_field("distance", &self.distance)?;
state.serialize_field("rng", &self.rng)?;
state.end()
}
}
impl<'de, G: Genotype, D: BehaviourDistance + Deserialize<'de>> Deserialize<'de>
for NoveltySearch<G, D>
{
fn deserialize<De>(deserializer: De) -> Result<Self, De::Error>
where
De: serde::Deserializer<'de>,
{
use serde::de::Error;
let data = NoveltySearchData::<G, D>::deserialize(deserializer)?;
if data.pop_size != data.population.len() {
return Err(De::Error::custom(format!(
"pop_size ({}) does not match population length ({})",
data.pop_size,
data.population.len()
)));
}
let cfg = NoveltyConfig {
k: data.k,
alpha: data.alpha,
policy: data.policy,
};
cfg.validate().map_err(De::Error::custom)?;
if data.mutation_rate.is_nan() || data.mutation_rate.is_infinite() {
return Err(De::Error::custom("mutation_rate must be a finite number"));
}
if !data.novelty.is_empty() && data.novelty.len() != data.population.len() {
return Err(De::Error::custom(format!(
"novelty length ({}) does not match population length ({})",
data.novelty.len(),
data.population.len()
)));
}
Ok(Self {
population: data.population,
pop_size: data.pop_size,
novelty: data.novelty,
behaviour_archive: data.behaviour_archive,
mutation_rate: data.mutation_rate,
elitism: data.elitism,
k: data.k,
alpha: data.alpha,
policy: data.policy,
distance: data.distance,
rng: data.rng,
})
}
}
impl<G: Genotype> NoveltySearch<G, EuclideanDistance> {
pub fn new(
initial_pop: Vec<G>,
mutation_rate: f32,
elitism: usize,
config: NoveltyConfig,
seed: u64,
) -> Self {
Self::with_distance(
initial_pop,
mutation_rate,
elitism,
config,
EuclideanDistance,
seed,
)
}
}
impl<G: Genotype, D: BehaviourDistance> NoveltySearch<G, D> {
pub fn with_distance(
initial_pop: Vec<G>,
mutation_rate: f32,
elitism: usize,
config: NoveltyConfig,
distance: D,
seed: u64,
) -> Self {
assert!(
!initial_pop.is_empty(),
"initial population must be non-empty"
);
config.validate().expect("invalid NoveltyConfig");
let pop_size = initial_pop.len();
if elitism >= pop_size {
eprintln!(
"Warning: NoveltySearch elitism ({}) >= pop_size ({}). \
Elitism will be clamped to {} to ensure evolution progresses.",
elitism,
pop_size,
pop_size.saturating_sub(1)
);
}
let population: Vec<Phenotype<G>> = initial_pop
.into_iter()
.map(|g| Phenotype {
genotype: g,
fitness: 0.0,
objectives: vec![],
descriptor: vec![],
})
.collect();
Self {
population,
pop_size,
novelty: Vec::new(),
behaviour_archive: Vec::new(),
mutation_rate,
elitism,
k: config.k,
alpha: config.alpha,
policy: config.policy,
distance,
rng: Pcg64::seed_from_u64(seed),
}
}
pub fn pop_size(&self) -> usize {
self.pop_size
}
pub fn k(&self) -> usize {
self.k
}
pub fn alpha(&self) -> f32 {
self.alpha
}
pub fn set_alpha(&mut self, alpha: f32) {
assert!(
(0.0..=1.0).contains(&alpha),
"alpha must be in [0.0, 1.0], got {alpha}"
);
self.alpha = alpha;
}
pub fn policy(&self) -> ArchivePolicy {
self.policy
}
pub fn archive_len(&self) -> usize {
self.behaviour_archive.len()
}
pub fn archive(&self) -> &[Vec<f32>] {
&self.behaviour_archive
}
pub fn novelty(&self) -> &[f32] {
&self.novelty
}
fn score(&self, i: usize) -> f32 {
let nov = self.novelty.get(i).copied().unwrap_or(0.0);
let fit = self.population[i].fitness;
let fit = if fit.is_nan() { 0.0 } else { fit };
self.alpha * nov + (1.0 - self.alpha) * fit
}
fn tournament_pick(&mut self, tournament_size: usize, scores: &[f32]) -> usize {
let n = self.population.len();
let mut best_idx = self.rng.random_range(0..n);
let mut best_score = scores[best_idx];
for _ in 1..tournament_size {
let candidate = self.rng.random_range(0..n);
if cmp_f32_nan_last(scores[candidate], best_score) == Ordering::Greater {
best_idx = candidate;
best_score = scores[candidate];
}
}
best_idx
}
}
fn mean_knn_distance<'a, D, I>(point: &[f32], others: I, k: usize, distance: &D) -> f32
where
D: BehaviourDistance,
I: Iterator<Item = &'a [f32]>,
{
let mut distances: Vec<f32> = others
.filter(|o: &&[f32]| o.len() == point.len())
.map(|o| distance.distance(point, o))
.filter(|d: &f32| !d.is_nan())
.collect();
if distances.is_empty() {
return 0.0;
}
distances.sort_by(|a: &f32, b: &f32| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let take = k.min(distances.len());
distances.iter().take(take).sum::<f32>() / take as f32
}
impl<G: Genotype, D: BehaviourDistance> Evolver<G> for NoveltySearch<G, D> {
fn step<E: Evaluator<G>>(&mut self, evaluator: &E) {
if self.population.is_empty() {
return;
}
#[cfg(feature = "parallel")]
self.population.par_iter_mut().for_each(|p| {
let (f, obj, desc) = evaluator.evaluate(&p.genotype);
p.fitness = f;
p.objectives = obj;
p.descriptor = desc;
});
#[cfg(not(feature = "parallel"))]
for p in &mut self.population {
let (f, obj, desc) = evaluator.evaluate(&p.genotype);
p.fitness = f;
p.objectives = obj;
p.descriptor = desc;
}
let pop_descriptors: Vec<&[f32]> = self
.population
.iter()
.map(|p| p.descriptor.as_slice())
.collect();
let n = self.population.len();
self.novelty.clear();
self.novelty.reserve(n);
for i in 0..n {
let me = pop_descriptors[i];
if me.is_empty() || me.iter().any(|v| v.is_nan()) {
self.novelty.push(0.0);
continue;
}
let neighbours = pop_descriptors
.iter()
.enumerate()
.filter_map(|(j, d)| if j == i { None } else { Some(*d) })
.chain(self.behaviour_archive.iter().map(|v| v.as_slice()));
let nov = mean_knn_distance(me, neighbours, self.k, &self.distance);
self.novelty.push(nov);
}
let add_to_archive: Box<dyn Fn(&mut Pcg64) -> bool> = match self.policy {
ArchivePolicy::AlwaysAdd => Box::new(|_| true),
ArchivePolicy::Probabilistic(p) => Box::new(move |rng| rng.random::<f32>() < p),
};
for p in &self.population {
if p.descriptor.is_empty() || p.descriptor.iter().any(|v| v.is_nan()) {
continue;
}
if add_to_archive(&mut self.rng) {
self.behaviour_archive.push(p.descriptor.clone());
}
}
let mut indices: Vec<usize> = (0..n).collect();
let scores: Vec<f32> = (0..n).map(|i| self.score(i)).collect();
indices.sort_by(|&a, &b| cmp_f32_nan_last(scores[b], scores[a]));
let effective_elitism = self.elitism.min(self.pop_size.saturating_sub(1));
let mut next_gen: Vec<Phenotype<G>> = indices
.iter()
.take(effective_elitism)
.map(|&i| self.population[i].clone())
.collect();
let tournament_size = 3.min(n);
while next_gen.len() < self.pop_size {
let p_a = self.tournament_pick(tournament_size, &scores);
let p_b = self.tournament_pick(tournament_size, &scores);
let mut child_dna = self.population[p_a]
.genotype
.crossover(&self.population[p_b].genotype, &mut self.rng);
child_dna.mutate(&mut self.rng, self.mutation_rate);
next_gen.push(Phenotype {
genotype: child_dna,
fitness: 0.0,
objectives: vec![],
descriptor: vec![],
});
}
self.population = next_gen;
}
fn population(&mut self) -> &[Phenotype<G>] {
&self.population
}
}