1use burn::tensor::{Tensor, TensorData, backend::Backend};
22
23use rlevo_core::fitness::{FitnessEvaluable, Landscape};
24
25pub trait FitnessFn<G>: Send {
30 fn evaluate_one(&mut self, member: &G) -> f32;
32}
33
34pub trait BatchFitnessFn<B: Backend, G>: Send {
40 fn evaluate_batch(&mut self, population: &G, device: &B::Device) -> Tensor<B, 1>;
42}
43
44#[derive(Debug)]
70pub struct FromFitnessEvaluable<FE, L> {
71 evaluator: FE,
72 landscape: L,
73}
74
75impl<FE, L> FromFitnessEvaluable<FE, L> {
76 pub fn new(evaluator: FE, landscape: L) -> Self {
78 Self {
79 evaluator,
80 landscape,
81 }
82 }
83
84 pub fn landscape(&self) -> &L {
86 &self.landscape
87 }
88}
89
90impl<FE, L, B> BatchFitnessFn<B, Tensor<B, 2>> for FromFitnessEvaluable<FE, L>
91where
92 B: Backend,
93 FE: FitnessEvaluable<Individual = Vec<f64>, Landscape = L> + Send,
94 L: Send + Sync,
95{
96 fn evaluate_batch(&mut self, population: &Tensor<B, 2>, device: &B::Device) -> Tensor<B, 1> {
97 let dims = population.shape().dims;
98 assert_eq!(dims.len(), 2, "population tensor must be rank 2");
99 let pop_size = dims[0];
100 let genome_dim = dims[1];
101
102 let flat = population
103 .clone()
104 .into_data()
105 .into_vec::<f32>()
106 .expect("tensor data must be readable as f32");
107 debug_assert_eq!(flat.len(), pop_size * genome_dim);
108
109 let mut fitness = Vec::with_capacity(pop_size);
110 let mut individual = Vec::with_capacity(genome_dim);
111 for row in 0..pop_size {
112 individual.clear();
113 let start = row * genome_dim;
114 individual.extend(
115 flat[start..start + genome_dim]
116 .iter()
117 .map(|&v| f64::from(v)),
118 );
119 let f = self.evaluator.evaluate(&individual, &self.landscape);
120 #[allow(clippy::cast_possible_truncation)]
121 fitness.push(f as f32);
122 }
123
124 let data = TensorData::new(fitness, [pop_size]);
125 Tensor::<B, 1>::from_data(data, device)
126 }
127}
128
129#[derive(Debug)]
142pub struct FromLandscape<L> {
143 landscape: L,
144}
145
146impl<L> FromLandscape<L> {
147 pub fn new(landscape: L) -> Self {
149 Self { landscape }
150 }
151
152 pub fn landscape(&self) -> &L {
154 &self.landscape
155 }
156}
157
158impl<L, B> BatchFitnessFn<B, Tensor<B, 2>> for FromLandscape<L>
159where
160 B: Backend,
161 L: Landscape,
162{
163 fn evaluate_batch(&mut self, population: &Tensor<B, 2>, device: &B::Device) -> Tensor<B, 1> {
164 let dims = population.shape().dims;
165 assert_eq!(dims.len(), 2, "population tensor must be rank 2");
166 let pop_size = dims[0];
167 let genome_dim = dims[1];
168
169 let flat = population
170 .clone()
171 .into_data()
172 .into_vec::<f32>()
173 .expect("tensor data must be readable as f32");
174 debug_assert_eq!(flat.len(), pop_size * genome_dim);
175
176 let mut fitness = Vec::with_capacity(pop_size);
177 let mut individual = Vec::with_capacity(genome_dim);
178 for row in 0..pop_size {
179 individual.clear();
180 let start = row * genome_dim;
181 individual.extend(
182 flat[start..start + genome_dim]
183 .iter()
184 .map(|&v| f64::from(v)),
185 );
186 let f = self.landscape.evaluate(&individual);
187 #[allow(clippy::cast_possible_truncation)]
188 fitness.push(f as f32);
189 }
190
191 let data = TensorData::new(fitness, [pop_size]);
192 Tensor::<B, 1>::from_data(data, device)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use burn::backend::NdArray;
200 type TestBackend = NdArray;
201
202 #[derive(Debug, Clone, Copy)]
203 struct Sphere;
204
205 struct SphereFit;
206 impl FitnessEvaluable for SphereFit {
207 type Individual = Vec<f64>;
208 type Landscape = Sphere;
209 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
210 x.iter().map(|v| v * v).sum()
211 }
212 }
213
214 #[test]
215 fn from_fitness_evaluable_preserves_row_order() {
216 let device = Default::default();
217 let data = TensorData::new(
218 vec![1.0_f32, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0],
219 [3, 3],
220 );
221 let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
222
223 let mut adapter = FromFitnessEvaluable::new(SphereFit, Sphere);
224 let fitness = adapter.evaluate_batch(&pop, &device);
225
226 let values = fitness.into_data().into_vec::<f32>().unwrap();
227 assert_eq!(values.len(), 3);
228 approx::assert_relative_eq!(values[0], 1.0, epsilon = 1e-6);
229 approx::assert_relative_eq!(values[1], 4.0, epsilon = 1e-6);
230 approx::assert_relative_eq!(values[2], 9.0, epsilon = 1e-6);
231 }
232
233 #[test]
234 fn from_landscape_preserves_row_order() {
235 struct SphereLandscape;
236 impl Landscape for SphereLandscape {
237 fn evaluate(&self, x: &[f64]) -> f64 {
238 x.iter().map(|v| v * v).sum()
239 }
240 }
241
242 let device = Default::default();
243 let data = TensorData::new(
244 vec![1.0_f32, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0],
245 [3, 3],
246 );
247 let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
248
249 let mut adapter = FromLandscape::new(SphereLandscape);
250 let fitness = adapter.evaluate_batch(&pop, &device);
251
252 let values = fitness.into_data().into_vec::<f32>().unwrap();
253 assert_eq!(values.len(), 3);
254 approx::assert_relative_eq!(values[0], 1.0, epsilon = 1e-6);
255 approx::assert_relative_eq!(values[1], 4.0, epsilon = 1e-6);
256 approx::assert_relative_eq!(values[2], 9.0, epsilon = 1e-6);
257 }
258}