fugue_evo/interactive/
traits.rs1use serde::{Deserialize, Serialize};
7
8use super::aggregation::FitnessAggregator;
9use super::evaluator::{Candidate, CandidateId, EvaluationRequest, EvaluationResponse};
10use crate::genome::traits::EvolutionaryGenome;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum EvaluationMode {
17 Rating,
21
22 Pairwise,
27
28 BatchSelection,
32
33 Adaptive,
37}
38
39impl EvaluationMode {
40 pub fn description(&self) -> &'static str {
42 match self {
43 Self::Rating => "Rate each candidate on a numeric scale",
44 Self::Pairwise => "Compare pairs and select the better one",
45 Self::BatchSelection => "Select favorites from a batch",
46 Self::Adaptive => "System adapts evaluation method automatically",
47 }
48 }
49}
50
51impl Default for EvaluationMode {
52 fn default() -> Self {
53 Self::Rating
54 }
55}
56
57pub trait InteractiveFitness: Send + Sync {
112 type Genome: EvolutionaryGenome;
114
115 fn evaluation_mode(&self) -> EvaluationMode;
117
118 fn request_evaluation(
124 &self,
125 candidates: &[Candidate<Self::Genome>],
126 ) -> EvaluationRequest<Self::Genome>;
127
128 fn process_response(
134 &mut self,
135 response: EvaluationResponse,
136 aggregator: &mut FitnessAggregator,
137 ) -> Vec<(CandidateId, f64)>;
138
139 fn on_generation_start(&mut self, _generation: usize, _population_size: usize) {}
144
145 fn on_evaluation_skipped(&mut self) {}
149}
150
151#[derive(Clone, Debug)]
156pub struct DefaultInteractiveFitness<G>
157where
158 G: EvolutionaryGenome,
159{
160 mode: EvaluationMode,
161 batch_size: usize,
162 select_count: usize,
163 _marker: std::marker::PhantomData<G>,
164}
165
166impl<G> DefaultInteractiveFitness<G>
167where
168 G: EvolutionaryGenome,
169{
170 pub fn new(mode: EvaluationMode) -> Self {
172 Self {
173 mode,
174 batch_size: 6,
175 select_count: 2,
176 _marker: std::marker::PhantomData,
177 }
178 }
179
180 pub fn with_batch_size(mut self, size: usize) -> Self {
182 self.batch_size = size;
183 self
184 }
185
186 pub fn with_select_count(mut self, count: usize) -> Self {
188 self.select_count = count;
189 self
190 }
191}
192
193impl<G> Default for DefaultInteractiveFitness<G>
194where
195 G: EvolutionaryGenome,
196{
197 fn default() -> Self {
198 Self::new(EvaluationMode::Rating)
199 }
200}
201
202impl<G> InteractiveFitness for DefaultInteractiveFitness<G>
203where
204 G: EvolutionaryGenome + Clone + Send + Sync,
205{
206 type Genome = G;
207
208 fn evaluation_mode(&self) -> EvaluationMode {
209 self.mode
210 }
211
212 fn request_evaluation(
213 &self,
214 candidates: &[Candidate<Self::Genome>],
215 ) -> EvaluationRequest<Self::Genome> {
216 match self.mode {
217 EvaluationMode::Rating => EvaluationRequest::rate(candidates.to_vec()),
218 EvaluationMode::Pairwise => {
219 if candidates.len() >= 2 {
221 EvaluationRequest::compare(candidates[0].clone(), candidates[1].clone())
222 } else if candidates.len() == 1 {
223 EvaluationRequest::rate(candidates.to_vec())
225 } else {
226 EvaluationRequest::rate(vec![])
227 }
228 }
229 EvaluationMode::BatchSelection => {
230 let batch: Vec<_> = candidates.iter().take(self.batch_size).cloned().collect();
231 EvaluationRequest::select_from_batch(batch, self.select_count)
232 }
233 EvaluationMode::Adaptive => {
234 EvaluationRequest::rate(candidates.to_vec())
236 }
237 }
238 }
239
240 fn process_response(
241 &mut self,
242 response: EvaluationResponse,
243 aggregator: &mut FitnessAggregator,
244 ) -> Vec<(CandidateId, f64)> {
245 aggregator.process_response(&response)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::genome::real_vector::RealVector;
253 use crate::interactive::aggregation::AggregationModel;
254
255 #[test]
256 fn test_evaluation_mode_default() {
257 assert_eq!(EvaluationMode::default(), EvaluationMode::Rating);
258 }
259
260 #[test]
261 fn test_evaluation_mode_description() {
262 assert!(!EvaluationMode::Rating.description().is_empty());
263 assert!(!EvaluationMode::Pairwise.description().is_empty());
264 assert!(!EvaluationMode::BatchSelection.description().is_empty());
265 assert!(!EvaluationMode::Adaptive.description().is_empty());
266 }
267
268 #[test]
269 fn test_default_interactive_fitness_rating() {
270 let fitness: DefaultInteractiveFitness<RealVector> =
271 DefaultInteractiveFitness::new(EvaluationMode::Rating);
272
273 let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
274 let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
275
276 let request = fitness.request_evaluation(&[c1, c2]);
277 match request {
278 EvaluationRequest::RateCandidates { candidates, .. } => {
279 assert_eq!(candidates.len(), 2);
280 }
281 _ => panic!("Expected RateCandidates request"),
282 }
283 }
284
285 #[test]
286 fn test_default_interactive_fitness_pairwise() {
287 let fitness: DefaultInteractiveFitness<RealVector> =
288 DefaultInteractiveFitness::new(EvaluationMode::Pairwise);
289
290 let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
291 let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
292
293 let request = fitness.request_evaluation(&[c1, c2]);
294 match request {
295 EvaluationRequest::PairwiseComparison { .. } => {}
296 _ => panic!("Expected PairwiseComparison request"),
297 }
298 }
299
300 #[test]
301 fn test_default_interactive_fitness_batch() {
302 let fitness: DefaultInteractiveFitness<RealVector> =
303 DefaultInteractiveFitness::new(EvaluationMode::BatchSelection)
304 .with_batch_size(4)
305 .with_select_count(2);
306
307 let candidates: Vec<_> = (0..6)
308 .map(|i| Candidate::new(CandidateId(i), RealVector::new(vec![i as f64])))
309 .collect();
310
311 let request = fitness.request_evaluation(&candidates);
312 match request {
313 EvaluationRequest::BatchSelection {
314 candidates,
315 select_count,
316 ..
317 } => {
318 assert_eq!(candidates.len(), 4); assert_eq!(select_count, 2);
320 }
321 _ => panic!("Expected BatchSelection request"),
322 }
323 }
324
325 #[test]
326 fn test_default_interactive_fitness_process_response() {
327 let mut fitness: DefaultInteractiveFitness<RealVector> =
328 DefaultInteractiveFitness::new(EvaluationMode::Rating);
329 let mut aggregator = FitnessAggregator::new(AggregationModel::DirectRating {
330 default_rating: 5.0,
331 });
332
333 let response =
334 EvaluationResponse::ratings(vec![(CandidateId(0), 8.0), (CandidateId(1), 6.0)]);
335
336 let updated = fitness.process_response(response, &mut aggregator);
337 assert_eq!(updated.len(), 2);
338 }
339}