use std::marker::PhantomData;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use crate::fitness::BatchFitnessFn;
use crate::param_reshaper::ParamReshaper;
use rlevo_core::objective::ObjectiveSense;
pub struct ModuleEvalFn<B: Backend, R: ParamReshaper<B>, F> {
reshaper: R,
scorer: F,
sense: ObjectiveSense,
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend, R: ParamReshaper<B>, F> std::fmt::Debug for ModuleEvalFn<B, R, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleEvalFn").finish_non_exhaustive()
}
}
impl<B, R, F> ModuleEvalFn<B, R, F>
where
B: Backend,
R: ParamReshaper<B>,
F: Fn(&R::Module) -> f32 + Send,
{
pub fn new(reshaper: R, scorer: F) -> Self {
Self::with_sense(reshaper, scorer, ObjectiveSense::Maximize)
}
pub fn with_sense(reshaper: R, scorer: F, sense: ObjectiveSense) -> Self {
Self {
reshaper,
scorer,
sense,
_backend: PhantomData,
}
}
#[must_use]
pub fn reshaper(&self) -> &R {
&self.reshaper
}
}
impl<B, R, F> BatchFitnessFn<B, Tensor<B, 2>> for ModuleEvalFn<B, R, F>
where
B: Backend,
R: ParamReshaper<B>,
F: Fn(&R::Module) -> f32 + Send,
{
fn evaluate_batch(&mut self, population: &Tensor<B, 2>, device: &B::Device) -> Tensor<B, 1> {
let [pop_size, num_params] = population.dims();
assert_eq!(
num_params,
self.reshaper.num_params(),
"population genome width must equal reshaper num_params"
);
let mut fitness: Vec<f32> = Vec::with_capacity(pop_size);
for row in 0..pop_size {
#[allow(clippy::single_range_in_vec_init)]
let genome: Tensor<B, 1> = population
.clone()
.slice([row..row + 1])
.reshape([num_params]);
let module = self.reshaper.unflatten(genome);
fitness.push((self.scorer)(&module));
}
Tensor::<B, 1>::from_data(TensorData::new(fitness, [pop_size]), device)
}
fn sense(&self) -> ObjectiveSense {
self.sense
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::param_reshaper::ModuleReshaper;
use burn::backend::Flex;
use burn::module::Module;
use burn::nn::{Linear, LinearConfig};
type TestBackend = Flex;
#[derive(Module, Debug)]
struct TestTiny<B: Backend> {
l: Linear<B>,
}
impl<B: Backend> TestTiny<B> {
fn new(device: &B::Device) -> Self {
Self {
l: LinearConfig::new(2, 1).init(device),
}
}
fn forward(&self, x: Tensor<B, 2>) -> Tensor<B, 2> {
self.l.forward(x)
}
}
#[test]
fn test_module_eval_fn_evaluate_batch_preserves_shape_and_order() {
let device = Default::default();
let reshaper = ModuleReshaper::new(TestTiny::<TestBackend>::new(&device));
let num_params = reshaper.num_params();
assert_eq!(num_params, 3);
let dev = device;
let mut eval = ModuleEvalFn::new(reshaper, move |m: &TestTiny<TestBackend>| {
let x = Tensor::<TestBackend, 2>::from_data(
TensorData::new(vec![1.0f32, 1.0], [1, 2]),
&dev,
);
let y = m.forward(x);
y.into_data()
.into_vec::<f32>()
.expect("output host-read of a tensor this test just built")[0]
});
let pop = Tensor::<TestBackend, 2>::from_data(
TensorData::new(vec![1.0f32, 0.0, 0.0, 0.0, 0.0, 5.0], [2, 3]),
&device,
);
let fitness = eval.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(), 2);
approx::assert_relative_eq!(values[0], 1.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[1], 5.0, epsilon = 1e-6);
}
}