use burn::tensor::{Tensor, TensorData, backend::Backend};
use rlevo_core::fitness::{FitnessEvaluable, Landscape};
use rlevo_core::objective::ObjectiveSense;
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 as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 1>;
fn sense(&self) -> ObjectiveSense;
}
#[derive(Debug)]
pub struct FromFitnessEvaluable<FE, L> {
evaluator: FE,
landscape: L,
sense: ObjectiveSense,
}
impl<FE, L> FromFitnessEvaluable<FE, L> {
pub fn new(evaluator: FE, landscape: L) -> Self {
Self::with_sense(evaluator, landscape, ObjectiveSense::Minimize)
}
pub fn with_sense(evaluator: FE, landscape: L, sense: ObjectiveSense) -> Self {
Self {
evaluator,
landscape,
sense,
}
}
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 as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 1> {
let dims = population.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)
}
fn sense(&self) -> ObjectiveSense {
self.sense
}
}
#[derive(Debug)]
pub struct FromLandscape<L> {
landscape: L,
sense: ObjectiveSense,
}
impl<L: Landscape> FromLandscape<L> {
pub fn new(landscape: L) -> Self {
let sense = landscape.sense();
Self { landscape, sense }
}
pub fn with_sense(landscape: L, sense: ObjectiveSense) -> Self {
Self { landscape, sense }
}
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 as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 1> {
let dims = population.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)
}
fn sense(&self) -> ObjectiveSense {
self.sense
}
}
pub(crate) fn sanitize_fitness(f: f32) -> f32 {
if f.is_nan() {
f32::NEG_INFINITY
} else if f.is_infinite() && f.is_sign_positive() {
f32::MAX
} else {
f
}
}
#[must_use]
pub(crate) fn sanitize_fitness_tensor<B: Backend>(fitness: Tensor<B, 1>) -> Tensor<B, 1> {
let nan_mask = fitness.clone().is_nan();
fitness
.mask_fill(nan_mask, f32::NEG_INFINITY)
.clamp_max(f32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
type TestBackend = Flex;
#[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>()
.expect("fitness host-read of a tensor this test just built");
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() {
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>()
.expect("fitness host-read of a tensor this test just built");
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);
}
struct SphereLandscape;
impl Landscape for SphereLandscape {
fn evaluate(&self, x: &[f64]) -> f64 {
x.iter().map(|v| v * v).sum()
}
}
#[test]
fn sanitize_fitness_scalar_applies_canonical_rule() {
let nan_out: f32 = sanitize_fitness(f32::NAN);
assert!(
nan_out.is_infinite() && nan_out.is_sign_negative(),
"NaN → −∞"
);
approx::assert_relative_eq!(sanitize_fitness(f32::INFINITY), f32::MAX);
let neg_out: f32 = sanitize_fitness(f32::NEG_INFINITY);
assert!(
neg_out.is_infinite() && neg_out.is_sign_negative(),
"−∞ passes through"
);
approx::assert_relative_eq!(sanitize_fitness(2.5), 2.5, epsilon = 1e-6);
approx::assert_relative_eq!(sanitize_fitness(-7.0), -7.0, epsilon = 1e-6);
}
#[test]
fn sanitize_fitness_tensor_matches_scalar_rule() {
let device = Default::default();
let data = TensorData::new(
vec![f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 3.0_f32, -4.0],
[5],
);
let t = Tensor::<TestBackend, 1>::from_data(data, &device);
let out = sanitize_fitness_tensor(t)
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
assert!(
out[0].is_infinite() && out[0].is_sign_negative(),
"NaN → −∞"
);
approx::assert_relative_eq!(out[1], f32::MAX); assert!(
out[2].is_infinite() && out[2].is_sign_negative(),
"−∞ passes through, stays non-finite"
);
approx::assert_relative_eq!(out[3], 3.0, epsilon = 1e-6);
approx::assert_relative_eq!(out[4], -4.0, epsilon = 1e-6);
}
#[test]
fn from_fitness_evaluable_output_is_pop_size_shaped_and_row_aligned() {
let device = Default::default();
let data = TensorData::new(vec![1.0_f32, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0], [4, 2]);
let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
let mut adapter = FromFitnessEvaluable::new(SphereFit, Sphere);
let fitness = adapter.evaluate_batch(&pop, &device);
assert_eq!(fitness.dims(), [4]);
let values = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
for (i, &v) in values.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let expected = ((i + 1) * (i + 1)) as f32;
approx::assert_relative_eq!(v, expected, epsilon = 1e-6);
}
}
#[test]
fn from_landscape_output_is_pop_size_shaped_and_row_aligned() {
let device = Default::default();
let data = TensorData::new(vec![1.0_f32, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0], [4, 2]);
let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
let mut adapter = FromLandscape::new(SphereLandscape);
let fitness = adapter.evaluate_batch(&pop, &device);
assert_eq!(fitness.dims(), [4]);
let values = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
for (i, &v) in values.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let expected = ((i + 1) * (i + 1)) as f32;
approx::assert_relative_eq!(v, expected, epsilon = 1e-6);
}
}
}