use burn::tensor::{Tensor, TensorData, backend::Backend};
use rlevo_core::fitness::{FitnessEvaluable, Landscape};
pub trait FitnessFn<G>: Send {
fn evaluate_one(&mut self, member: &G) -> f32;
}
pub trait BatchFitnessFn<B: Backend, G>: Send {
fn evaluate_batch(&mut self, population: &G, device: &B::Device) -> Tensor<B, 1>;
}
#[derive(Debug)]
pub struct FromFitnessEvaluable<FE, L> {
evaluator: FE,
landscape: L,
}
impl<FE, L> FromFitnessEvaluable<FE, L> {
pub fn new(evaluator: FE, landscape: L) -> Self {
Self {
evaluator,
landscape,
}
}
pub fn landscape(&self) -> &L {
&self.landscape
}
}
impl<FE, L, B> BatchFitnessFn<B, Tensor<B, 2>> for FromFitnessEvaluable<FE, L>
where
B: Backend,
FE: FitnessEvaluable<Individual = Vec<f64>, Landscape = L> + Send,
L: Send + Sync,
{
fn evaluate_batch(&mut self, population: &Tensor<B, 2>, device: &B::Device) -> Tensor<B, 1> {
let dims = population.shape().dims;
assert_eq!(dims.len(), 2, "population tensor must be rank 2");
let pop_size = dims[0];
let genome_dim = dims[1];
let flat = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("tensor data must be readable as f32");
debug_assert_eq!(flat.len(), pop_size * genome_dim);
let mut fitness = Vec::with_capacity(pop_size);
let mut individual = Vec::with_capacity(genome_dim);
for row in 0..pop_size {
individual.clear();
let start = row * genome_dim;
individual.extend(
flat[start..start + genome_dim]
.iter()
.map(|&v| f64::from(v)),
);
let f = self.evaluator.evaluate(&individual, &self.landscape);
#[allow(clippy::cast_possible_truncation)]
fitness.push(f as f32);
}
let data = TensorData::new(fitness, [pop_size]);
Tensor::<B, 1>::from_data(data, device)
}
}
#[derive(Debug)]
pub struct FromLandscape<L> {
landscape: L,
}
impl<L> FromLandscape<L> {
pub fn new(landscape: L) -> Self {
Self { landscape }
}
pub fn landscape(&self) -> &L {
&self.landscape
}
}
impl<L, B> BatchFitnessFn<B, Tensor<B, 2>> for FromLandscape<L>
where
B: Backend,
L: Landscape,
{
fn evaluate_batch(&mut self, population: &Tensor<B, 2>, device: &B::Device) -> Tensor<B, 1> {
let dims = population.shape().dims;
assert_eq!(dims.len(), 2, "population tensor must be rank 2");
let pop_size = dims[0];
let genome_dim = dims[1];
let flat = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("tensor data must be readable as f32");
debug_assert_eq!(flat.len(), pop_size * genome_dim);
let mut fitness = Vec::with_capacity(pop_size);
let mut individual = Vec::with_capacity(genome_dim);
for row in 0..pop_size {
individual.clear();
let start = row * genome_dim;
individual.extend(
flat[start..start + genome_dim]
.iter()
.map(|&v| f64::from(v)),
);
let f = self.landscape.evaluate(&individual);
#[allow(clippy::cast_possible_truncation)]
fitness.push(f as f32);
}
let data = TensorData::new(fitness, [pop_size]);
Tensor::<B, 1>::from_data(data, device)
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::NdArray;
type TestBackend = NdArray;
#[derive(Debug, Clone, Copy)]
struct Sphere;
struct SphereFit;
impl FitnessEvaluable for SphereFit {
type Individual = Vec<f64>;
type Landscape = Sphere;
fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
x.iter().map(|v| v * v).sum()
}
}
#[test]
fn from_fitness_evaluable_preserves_row_order() {
let device = Default::default();
let data = TensorData::new(
vec![1.0_f32, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0],
[3, 3],
);
let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
let mut adapter = FromFitnessEvaluable::new(SphereFit, Sphere);
let fitness = adapter.evaluate_batch(&pop, &device);
let values = fitness.into_data().into_vec::<f32>().unwrap();
assert_eq!(values.len(), 3);
approx::assert_relative_eq!(values[0], 1.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[1], 4.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[2], 9.0, epsilon = 1e-6);
}
#[test]
fn from_landscape_preserves_row_order() {
struct SphereLandscape;
impl Landscape for SphereLandscape {
fn evaluate(&self, x: &[f64]) -> f64 {
x.iter().map(|v| v * v).sum()
}
}
let device = Default::default();
let data = TensorData::new(
vec![1.0_f32, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0],
[3, 3],
);
let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
let mut adapter = FromLandscape::new(SphereLandscape);
let fitness = adapter.evaluate_batch(&pop, &device);
let values = fitness.into_data().into_vec::<f32>().unwrap();
assert_eq!(values.len(), 3);
approx::assert_relative_eq!(values[0], 1.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[1], 4.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[2], 9.0, epsilon = 1e-6);
}
}