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 {
113 type Genome: EvolutionaryGenome;
115
116 fn evaluation_mode(&self) -> EvaluationMode;
118
119 fn request_evaluation(
125 &self,
126 candidates: &[Candidate<Self::Genome>],
127 ) -> EvaluationRequest<Self::Genome>;
128
129 fn process_response(
135 &mut self,
136 response: EvaluationResponse,
137 aggregator: &mut FitnessAggregator,
138 ) -> Vec<(CandidateId, f64)>;
139
140 fn on_generation_start(&mut self, _generation: usize, _population_size: usize) {}
145
146 fn on_evaluation_skipped(&mut self) {}
150}
151
152#[derive(Clone, Debug)]
157pub struct DefaultInteractiveFitness<G>
158where
159 G: EvolutionaryGenome,
160{
161 mode: EvaluationMode,
162 batch_size: usize,
163 select_count: usize,
164 _marker: std::marker::PhantomData<G>,
165}
166
167impl<G> DefaultInteractiveFitness<G>
168where
169 G: EvolutionaryGenome,
170{
171 pub fn new(mode: EvaluationMode) -> Self {
173 Self {
174 mode,
175 batch_size: 6,
176 select_count: 2,
177 _marker: std::marker::PhantomData,
178 }
179 }
180
181 pub fn with_batch_size(mut self, size: usize) -> Self {
183 self.batch_size = size;
184 self
185 }
186
187 pub fn with_select_count(mut self, count: usize) -> Self {
189 self.select_count = count;
190 self
191 }
192}
193
194impl<G> Default for DefaultInteractiveFitness<G>
195where
196 G: EvolutionaryGenome,
197{
198 fn default() -> Self {
199 Self::new(EvaluationMode::Rating)
200 }
201}
202
203impl<G> InteractiveFitness for DefaultInteractiveFitness<G>
204where
205 G: EvolutionaryGenome + Clone + Send + Sync,
206{
207 type Genome = G;
208
209 fn evaluation_mode(&self) -> EvaluationMode {
210 self.mode
211 }
212
213 fn request_evaluation(
214 &self,
215 candidates: &[Candidate<Self::Genome>],
216 ) -> EvaluationRequest<Self::Genome> {
217 match self.mode {
218 EvaluationMode::Rating => EvaluationRequest::rate(candidates.to_vec()),
219 EvaluationMode::Pairwise => {
220 if candidates.len() >= 2 {
222 EvaluationRequest::compare(candidates[0].clone(), candidates[1].clone())
223 } else if candidates.len() == 1 {
224 EvaluationRequest::rate(candidates.to_vec())
226 } else {
227 EvaluationRequest::rate(vec![])
228 }
229 }
230 EvaluationMode::BatchSelection => {
231 let batch: Vec<_> = candidates.iter().take(self.batch_size).cloned().collect();
232 EvaluationRequest::select_from_batch(batch, self.select_count)
233 }
234 EvaluationMode::Adaptive => {
235 EvaluationRequest::rate(candidates.to_vec())
237 }
238 }
239 }
240
241 fn process_response(
242 &mut self,
243 response: EvaluationResponse,
244 aggregator: &mut FitnessAggregator,
245 ) -> Vec<(CandidateId, f64)> {
246 aggregator.process_response(&response)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::genome::real_vector::RealVector;
254 use crate::interactive::aggregation::AggregationModel;
255
256 #[test]
257 fn test_evaluation_mode_default() {
258 assert_eq!(EvaluationMode::default(), EvaluationMode::Rating);
259 }
260
261 #[test]
262 fn test_evaluation_mode_description() {
263 assert!(!EvaluationMode::Rating.description().is_empty());
264 assert!(!EvaluationMode::Pairwise.description().is_empty());
265 assert!(!EvaluationMode::BatchSelection.description().is_empty());
266 assert!(!EvaluationMode::Adaptive.description().is_empty());
267 }
268
269 #[test]
270 fn test_default_interactive_fitness_rating() {
271 let fitness: DefaultInteractiveFitness<RealVector> =
272 DefaultInteractiveFitness::new(EvaluationMode::Rating);
273
274 let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
275 let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
276
277 let request = fitness.request_evaluation(&[c1, c2]);
278 match request {
279 EvaluationRequest::RateCandidates { candidates, .. } => {
280 assert_eq!(candidates.len(), 2);
281 }
282 _ => panic!("Expected RateCandidates request"),
283 }
284 }
285
286 #[test]
287 fn test_default_interactive_fitness_pairwise() {
288 let fitness: DefaultInteractiveFitness<RealVector> =
289 DefaultInteractiveFitness::new(EvaluationMode::Pairwise);
290
291 let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
292 let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
293
294 let request = fitness.request_evaluation(&[c1, c2]);
295 match request {
296 EvaluationRequest::PairwiseComparison { .. } => {}
297 _ => panic!("Expected PairwiseComparison request"),
298 }
299 }
300
301 #[test]
302 fn test_default_interactive_fitness_batch() {
303 let fitness: DefaultInteractiveFitness<RealVector> =
304 DefaultInteractiveFitness::new(EvaluationMode::BatchSelection)
305 .with_batch_size(4)
306 .with_select_count(2);
307
308 let candidates: Vec<_> = (0..6)
309 .map(|i| Candidate::new(CandidateId(i), RealVector::new(vec![i as f64])))
310 .collect();
311
312 let request = fitness.request_evaluation(&candidates);
313 match request {
314 EvaluationRequest::BatchSelection {
315 candidates,
316 select_count,
317 ..
318 } => {
319 assert_eq!(candidates.len(), 4); assert_eq!(select_count, 2);
321 }
322 _ => panic!("Expected BatchSelection request"),
323 }
324 }
325
326 #[test]
327 fn test_default_interactive_fitness_process_response() {
328 let mut fitness: DefaultInteractiveFitness<RealVector> =
329 DefaultInteractiveFitness::new(EvaluationMode::Rating);
330 let mut aggregator = FitnessAggregator::new(AggregationModel::DirectRating {
331 default_rating: 5.0,
332 });
333
334 let response =
335 EvaluationResponse::ratings(vec![(CandidateId(0), 8.0), (CandidateId(1), 6.0)]);
336
337 let updated = fitness.process_response(response, &mut aggregator);
338 assert_eq!(updated.len(), 2);
339 }
340}