1use rand::Rng;
7use serde::{Deserialize, Serialize};
8use std::marker::PhantomData;
9
10use super::aggregation::{AggregationModel, FitnessAggregator};
11use super::evaluator::{Candidate, CandidateId, EvaluationRequest, EvaluationResponse};
12use super::selection_strategy::SelectionStrategy;
13use super::session::{CoverageStats, InteractiveSession};
14use super::traits::EvaluationMode;
15use crate::error::EvolutionError;
16use crate::genome::bounds::MultiBounds;
17use crate::genome::traits::EvolutionaryGenome;
18use crate::operators::traits::{CrossoverOperator, MutationOperator, SelectionOperator};
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct InteractiveGAConfig {
23 pub population_size: usize,
25 pub elitism_count: usize,
27 pub crossover_probability: f64,
29 pub mutation_probability: f64,
31 pub evaluation_mode: EvaluationMode,
33 pub batch_size: usize,
35 pub select_count: usize,
37 pub min_coverage: f64,
39 pub comparisons_per_candidate: usize,
41 pub max_generations: usize,
43 pub aggregation_model: AggregationModel,
45 #[serde(default)]
47 pub selection_strategy: SelectionStrategy,
48}
49
50impl Default for InteractiveGAConfig {
51 fn default() -> Self {
52 Self {
53 population_size: 20, elitism_count: 2,
55 crossover_probability: 0.8,
56 mutation_probability: 0.2,
57 evaluation_mode: EvaluationMode::Rating,
58 batch_size: 6,
59 select_count: 2,
60 min_coverage: 0.8, comparisons_per_candidate: 3,
62 max_generations: 0, aggregation_model: AggregationModel::DirectRating {
64 default_rating: 5.0,
65 },
66 selection_strategy: SelectionStrategy::Sequential,
67 }
68 }
69}
70
71#[derive(Clone, Debug)]
73enum AlgorithmState {
74 Initializing,
76 AwaitingEvaluation {
78 pending_request_ids: Vec<CandidateId>,
80 },
81 ReadyForEvolution,
83 Terminated { reason: String },
85}
86
87#[derive(Clone, Debug)]
89pub enum StepResult<G>
90where
91 G: EvolutionaryGenome,
92{
93 NeedsEvaluation(EvaluationRequest<G>),
95
96 GenerationComplete {
98 generation: usize,
100 best_fitness: Option<f64>,
102 coverage: f64,
104 },
105
106 Complete(Box<InteractiveResult<G>>),
108}
109
110#[derive(Clone, Debug)]
112pub struct InteractiveResult<G>
113where
114 G: EvolutionaryGenome,
115{
116 pub best_candidates: Vec<Candidate<G>>,
118 pub generations: usize,
120 pub total_evaluations: usize,
122 pub session: InteractiveSession<G>,
124 pub termination_reason: String,
126}
127
128pub struct InteractiveGA<G, S, C, M>
162where
163 G: EvolutionaryGenome,
164{
165 config: InteractiveGAConfig,
166 bounds: Option<MultiBounds>,
167 selection: S,
168 crossover: C,
169 mutation: M,
170 session: InteractiveSession<G>,
171 state: AlgorithmState,
172 unevaluated_indices: Vec<usize>,
174 comparison_index: usize,
176}
177
178impl<G, S, C, M> InteractiveGA<G, S, C, M>
179where
180 G: EvolutionaryGenome + Clone + Send + Sync,
181 S: SelectionOperator<G>,
182 C: CrossoverOperator<G>,
183 M: MutationOperator<G>,
184{
185 pub fn new(
187 config: InteractiveGAConfig,
188 bounds: Option<MultiBounds>,
189 selection: S,
190 crossover: C,
191 mutation: M,
192 ) -> Self {
193 let aggregator = FitnessAggregator::new(config.aggregation_model.clone());
194 Self {
195 config,
196 bounds,
197 selection,
198 crossover,
199 mutation,
200 session: InteractiveSession::new(aggregator),
201 state: AlgorithmState::Initializing,
202 unevaluated_indices: Vec::new(),
203 comparison_index: 0,
204 }
205 }
206
207 pub fn from_session(
209 session: InteractiveSession<G>,
210 config: InteractiveGAConfig,
211 bounds: Option<MultiBounds>,
212 selection: S,
213 crossover: C,
214 mutation: M,
215 ) -> Self {
216 let unevaluated: Vec<usize> = session
217 .population
218 .iter()
219 .enumerate()
220 .filter(|(_, c)| !c.is_evaluated())
221 .map(|(i, _)| i)
222 .collect();
223
224 let state = if session.population.is_empty() {
225 AlgorithmState::Initializing
226 } else if unevaluated.is_empty() {
227 AlgorithmState::ReadyForEvolution
228 } else {
229 AlgorithmState::AwaitingEvaluation {
230 pending_request_ids: Vec::new(),
231 }
232 };
233
234 Self {
235 config,
236 bounds,
237 selection,
238 crossover,
239 mutation,
240 session,
241 state,
242 unevaluated_indices: unevaluated,
243 comparison_index: 0,
244 }
245 }
246
247 pub fn session(&self) -> &InteractiveSession<G> {
249 &self.session
250 }
251
252 pub fn session_mut(&mut self) -> &mut InteractiveSession<G> {
254 &mut self.session
255 }
256
257 pub fn config(&self) -> &InteractiveGAConfig {
259 &self.config
260 }
261
262 pub fn coverage_stats(&self) -> CoverageStats {
264 self.session.coverage_stats()
265 }
266
267 fn should_terminate(&self) -> Option<String> {
269 if self.config.max_generations > 0 && self.session.generation >= self.config.max_generations
270 {
271 return Some(format!(
272 "Reached maximum generations ({})",
273 self.config.max_generations
274 ));
275 }
276 None
277 }
278
279 fn initialize_population<R: Rng>(&mut self, rng: &mut R) {
281 let bounds = self
283 .bounds
284 .clone()
285 .unwrap_or_else(|| MultiBounds::symmetric(1.0, 1));
286
287 for _ in 0..self.config.population_size {
288 let genome = G::generate(rng, &bounds);
289 self.session.add_candidate(genome);
290 }
291
292 self.unevaluated_indices = (0..self.config.population_size).collect();
293 self.comparison_index = 0;
294 }
295
296 fn create_evaluation_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
298 match self.config.evaluation_mode {
299 EvaluationMode::Rating => self.create_rating_request(rng),
300 EvaluationMode::Pairwise => self.create_pairwise_request(rng),
301 EvaluationMode::BatchSelection => self.create_batch_request(rng),
302 EvaluationMode::Adaptive => self.create_adaptive_request(rng),
303 }
304 }
305
306 fn create_rating_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
307 let batch_size = self.config.batch_size.min(self.session.population.len());
308 if batch_size == 0 {
309 return None;
310 }
311
312 let selected_indices = self.config.selection_strategy.select_batch(
314 &self.session.population,
315 &self.session.aggregator,
316 batch_size,
317 rng,
318 );
319
320 if selected_indices.is_empty() {
321 return None;
322 }
323
324 let candidates: Vec<Candidate<G>> = selected_indices
325 .iter()
326 .filter_map(|&i| self.session.population.get(i).cloned())
327 .collect();
328
329 let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
330 self.state = AlgorithmState::AwaitingEvaluation {
331 pending_request_ids: ids,
332 };
333
334 Some(EvaluationRequest::rate(candidates))
335 }
336
337 fn create_pairwise_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
338 let pop_size = self.session.population.len();
339 if pop_size < 2 {
340 return None;
341 }
342
343 let pair = self.config.selection_strategy.select_pair(
345 &self.session.population,
346 &self.session.aggregator,
347 rng,
348 );
349
350 let (idx_a, idx_b) = match pair {
351 Some(p) => p,
352 None => {
353 let idx_a = self.comparison_index % pop_size;
355 let idx_b = (self.comparison_index + 1) % pop_size;
356 (idx_a, idx_b)
357 }
358 };
359
360 self.comparison_index += 1;
361
362 let candidate_a = self.session.population.get(idx_a)?.clone();
363 let candidate_b = self.session.population.get(idx_b)?.clone();
364
365 let ids = vec![candidate_a.id, candidate_b.id];
366 self.state = AlgorithmState::AwaitingEvaluation {
367 pending_request_ids: ids,
368 };
369
370 Some(EvaluationRequest::compare(candidate_a, candidate_b))
371 }
372
373 fn create_batch_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
374 let batch_size = self.config.batch_size.min(self.session.population.len());
375 if batch_size < 2 {
376 return self.create_rating_request(rng); }
379
380 let selected_indices = self.config.selection_strategy.select_batch(
382 &self.session.population,
383 &self.session.aggregator,
384 batch_size,
385 rng,
386 );
387
388 if selected_indices.len() < 2 {
389 return self.create_rating_request(rng);
390 }
391
392 let candidates: Vec<Candidate<G>> = selected_indices
393 .iter()
394 .filter_map(|&i| self.session.population.get(i).cloned())
395 .collect();
396
397 let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
398 self.state = AlgorithmState::AwaitingEvaluation {
399 pending_request_ids: ids,
400 };
401
402 let select_count = self.config.select_count.min(candidates.len() - 1);
403 Some(EvaluationRequest::select_from_batch(
404 candidates,
405 select_count,
406 ))
407 }
408
409 fn create_adaptive_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
410 let coverage = self.session.coverage_stats().coverage;
413 if coverage < 0.5 {
414 self.create_rating_request(rng)
415 } else {
416 self.create_pairwise_request(rng)
417 }
418 }
419
420 pub fn provide_response(&mut self, response: EvaluationResponse) {
422 let was_skipped = response.is_skip();
423 self.session.record_response(was_skipped);
424
425 if was_skipped {
426 if let AlgorithmState::AwaitingEvaluation {
428 pending_request_ids,
429 } = &self.state
430 {
431 for id in pending_request_ids {
432 if let Some(pos) = self.session.population.iter().position(|c| c.id == *id) {
433 if !self.unevaluated_indices.contains(&pos) {
434 self.unevaluated_indices.push(pos);
435 }
436 }
437 }
438 }
439 self.state = AlgorithmState::AwaitingEvaluation {
440 pending_request_ids: Vec::new(),
441 };
442 return;
443 }
444
445 let updated = match &response {
447 EvaluationResponse::Ratings(ratings) => {
448 self.session.aggregator.process_response(&response);
449 ratings.iter().map(|(id, _)| *id).collect::<Vec<_>>()
450 }
451 EvaluationResponse::PairwiseWinner(winner) => {
452 if let AlgorithmState::AwaitingEvaluation {
453 pending_request_ids,
454 } = &self.state
455 {
456 if pending_request_ids.len() == 2 {
457 let id_a = pending_request_ids[0];
458 let id_b = pending_request_ids[1];
459 self.session
460 .aggregator
461 .process_pairwise(id_a, id_b, *winner);
462 }
463 }
464 winner.map(|w| vec![w]).unwrap_or_default()
465 }
466 EvaluationResponse::BatchSelected(selected) => {
467 if let AlgorithmState::AwaitingEvaluation {
468 pending_request_ids,
469 } = &self.state
470 {
471 self.session
472 .aggregator
473 .process_batch_selection(pending_request_ids, selected);
474 }
475 selected.clone()
476 }
477 EvaluationResponse::Skip => Vec::new(),
478 };
479
480 for id in updated {
482 if let Some(estimate) = self.session.aggregator.get_fitness_estimate(&id) {
483 self.session.update_fitness_with_uncertainty(id, estimate);
484 } else if let Some(fitness) = self.session.aggregator.get_fitness(&id) {
485 self.session.update_fitness(id, fitness);
487 }
488 }
489
490 if let AlgorithmState::AwaitingEvaluation {
492 pending_request_ids,
493 } = &self.state
494 {
495 for id in pending_request_ids {
496 if let Some(candidate) = self.session.get_candidate_mut(*id) {
497 candidate.record_evaluation();
498 }
499 }
500 }
501
502 self.state = AlgorithmState::AwaitingEvaluation {
504 pending_request_ids: Vec::new(),
505 };
506 }
507
508 pub fn step<R: Rng>(&mut self, rng: &mut R) -> StepResult<G>
510 where
511 G: Serialize + for<'de> Deserialize<'de>,
512 {
513 loop {
514 match &self.state {
515 AlgorithmState::Initializing => {
516 self.initialize_population(rng);
517 self.state = AlgorithmState::AwaitingEvaluation {
518 pending_request_ids: Vec::new(),
519 };
520 }
521
522 AlgorithmState::AwaitingEvaluation {
523 pending_request_ids,
524 } => {
525 if !pending_request_ids.is_empty() {
527 continue;
529 }
530
531 let coverage = self.session.coverage_stats();
533
534 let enough_coverage = match self.config.evaluation_mode {
536 EvaluationMode::Pairwise => {
537 let target =
538 self.config.population_size * self.config.comparisons_per_candidate;
539 self.comparison_index >= target
540 }
541 _ => coverage.coverage >= self.config.min_coverage,
542 };
543
544 if enough_coverage {
545 self.state = AlgorithmState::ReadyForEvolution;
546 continue;
547 }
548
549 if let Some(request) = self.create_evaluation_request(rng) {
551 self.session.record_request(&request);
552 return StepResult::NeedsEvaluation(request);
553 } else {
554 self.state = AlgorithmState::ReadyForEvolution;
556 }
557 }
558
559 AlgorithmState::ReadyForEvolution => {
560 if let Some(reason) = self.should_terminate() {
562 self.state = AlgorithmState::Terminated {
563 reason: reason.clone(),
564 };
565 continue;
566 }
567
568 let generation = self.session.generation;
569 let best_fitness = self.session.best_candidate().and_then(|c| c.fitness());
570 let coverage = self.session.coverage_stats().coverage;
571
572 self.evolve_generation(rng);
574
575 return StepResult::GenerationComplete {
576 generation,
577 best_fitness,
578 coverage,
579 };
580 }
581
582 AlgorithmState::Terminated { reason } => {
583 let best_candidates = self
584 .session
585 .ranked_candidates()
586 .into_iter()
587 .take(self.config.elitism_count.max(3))
588 .cloned()
589 .collect();
590
591 return StepResult::Complete(Box::new(InteractiveResult {
592 best_candidates,
593 generations: self.session.generation,
594 total_evaluations: self.session.evaluations_requested,
595 session: self.session.clone(),
596 termination_reason: reason.clone(),
597 }));
598 }
599 }
600 }
601 }
602
603 fn evolve_generation<R: Rng>(&mut self, rng: &mut R)
605 where
606 G: Serialize + for<'de> Deserialize<'de>,
607 {
608 let pop_size = self.config.population_size;
609
610 let evaluated: Vec<(G, f64)> = self
612 .session
613 .population
614 .iter()
615 .filter_map(|c| c.fitness_estimate.map(|f| (c.genome.clone(), f)))
616 .collect();
617
618 if evaluated.is_empty() {
619 self.session.advance_generation();
621 return;
622 }
623
624 let mut new_population: Vec<Candidate<G>> = Vec::with_capacity(pop_size);
626 let elites: Vec<_> = self
627 .session
628 .ranked_candidates()
629 .into_iter()
630 .take(self.config.elitism_count)
631 .map(|c| (c.genome.clone(), c.fitness_estimate))
632 .collect();
633
634 let next_gen = self.session.generation + 1;
635 for (genome, fitness) in elites {
636 let id = self.session.next_id();
637 let mut candidate = Candidate::with_generation(id, genome, next_gen);
638 candidate.fitness_estimate = fitness;
640 new_population.push(candidate);
641 }
642
643 while new_population.len() < pop_size {
645 let parent1_idx = self.selection.select(&evaluated, rng);
647 let parent2_idx = self.selection.select(&evaluated, rng);
648
649 let parent1 = &evaluated[parent1_idx].0;
650 let parent2 = &evaluated[parent2_idx].0;
651
652 let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
654 match self.crossover.crossover(parent1, parent2, rng).genome() {
655 Some((c1, c2)) => (c1, c2),
656 None => (parent1.clone(), parent2.clone()),
657 }
658 } else {
659 (parent1.clone(), parent2.clone())
660 };
661
662 if rng.gen::<f64>() < self.config.mutation_probability {
664 self.mutation.mutate(&mut child1, rng);
665 }
666
667 let id = self.session.next_id();
668 new_population.push(Candidate::with_generation(
669 id,
670 child1,
671 self.session.generation + 1,
672 ));
673
674 if new_population.len() < pop_size {
675 if rng.gen::<f64>() < self.config.mutation_probability {
676 self.mutation.mutate(&mut child2, rng);
677 }
678
679 let id = self.session.next_id();
680 new_population.push(Candidate::with_generation(
681 id,
682 child2,
683 self.session.generation + 1,
684 ));
685 }
686 }
687
688 self.session.replace_population(new_population);
690 self.session.advance_generation();
691
692 self.unevaluated_indices =
694 (self.config.elitism_count..self.config.population_size).collect();
695 self.comparison_index = 0;
696 self.state = AlgorithmState::AwaitingEvaluation {
697 pending_request_ids: Vec::new(),
698 };
699 }
700
701 pub fn terminate(&mut self, reason: &str) {
703 self.state = AlgorithmState::Terminated {
704 reason: reason.to_string(),
705 };
706 }
707}
708
709pub struct InteractiveGABuilder<G, S, C, M>
711where
712 G: EvolutionaryGenome,
713{
714 config: InteractiveGAConfig,
715 bounds: Option<MultiBounds>,
716 selection: Option<S>,
717 crossover: Option<C>,
718 mutation: Option<M>,
719 _phantom: PhantomData<G>,
720}
721
722impl<G> InteractiveGABuilder<G, (), (), ()>
723where
724 G: EvolutionaryGenome,
725{
726 pub fn new() -> Self {
728 Self {
729 config: InteractiveGAConfig::default(),
730 bounds: None,
731 selection: None,
732 crossover: None,
733 mutation: None,
734 _phantom: PhantomData,
735 }
736 }
737}
738
739impl<G> Default for InteractiveGABuilder<G, (), (), ()>
740where
741 G: EvolutionaryGenome,
742{
743 fn default() -> Self {
744 Self::new()
745 }
746}
747
748impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
749where
750 G: EvolutionaryGenome,
751{
752 pub fn population_size(mut self, size: usize) -> Self {
754 self.config.population_size = size;
755 self
756 }
757
758 pub fn elitism_count(mut self, count: usize) -> Self {
760 self.config.elitism_count = count;
761 self
762 }
763
764 pub fn crossover_probability(mut self, prob: f64) -> Self {
766 self.config.crossover_probability = prob;
767 self
768 }
769
770 pub fn mutation_probability(mut self, prob: f64) -> Self {
772 self.config.mutation_probability = prob;
773 self
774 }
775
776 pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
778 self.config.evaluation_mode = mode;
779 self
780 }
781
782 pub fn batch_size(mut self, size: usize) -> Self {
784 self.config.batch_size = size;
785 self
786 }
787
788 pub fn select_count(mut self, count: usize) -> Self {
790 self.config.select_count = count;
791 self
792 }
793
794 pub fn min_coverage(mut self, coverage: f64) -> Self {
796 self.config.min_coverage = coverage.clamp(0.0, 1.0);
797 self
798 }
799
800 pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
802 self.config.comparisons_per_candidate = count;
803 self
804 }
805
806 pub fn max_generations(mut self, max: usize) -> Self {
808 self.config.max_generations = max;
809 self
810 }
811
812 pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
814 self.config.aggregation_model = model;
815 self
816 }
817
818 pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
828 self.config.selection_strategy = strategy;
829 self
830 }
831
832 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
834 self.bounds = Some(bounds);
835 self
836 }
837
838 pub fn selection<NewS>(self, selection: NewS) -> InteractiveGABuilder<G, NewS, C, M>
840 where
841 NewS: SelectionOperator<G>,
842 {
843 InteractiveGABuilder {
844 config: self.config,
845 bounds: self.bounds,
846 selection: Some(selection),
847 crossover: self.crossover,
848 mutation: self.mutation,
849 _phantom: PhantomData,
850 }
851 }
852
853 pub fn crossover<NewC>(self, crossover: NewC) -> InteractiveGABuilder<G, S, NewC, M>
855 where
856 NewC: CrossoverOperator<G>,
857 {
858 InteractiveGABuilder {
859 config: self.config,
860 bounds: self.bounds,
861 selection: self.selection,
862 crossover: Some(crossover),
863 mutation: self.mutation,
864 _phantom: PhantomData,
865 }
866 }
867
868 pub fn mutation<NewM>(self, mutation: NewM) -> InteractiveGABuilder<G, S, C, NewM>
870 where
871 NewM: MutationOperator<G>,
872 {
873 InteractiveGABuilder {
874 config: self.config,
875 bounds: self.bounds,
876 selection: self.selection,
877 crossover: self.crossover,
878 mutation: Some(mutation),
879 _phantom: PhantomData,
880 }
881 }
882}
883
884impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
885where
886 G: EvolutionaryGenome + Clone + Send + Sync,
887 S: SelectionOperator<G>,
888 C: CrossoverOperator<G>,
889 M: MutationOperator<G>,
890{
891 pub fn build(self) -> Result<InteractiveGA<G, S, C, M>, EvolutionError> {
893 let selection = self
894 .selection
895 .ok_or_else(|| EvolutionError::Configuration("Selection operator required".into()))?;
896 let crossover = self
897 .crossover
898 .ok_or_else(|| EvolutionError::Configuration("Crossover operator required".into()))?;
899 let mutation = self
900 .mutation
901 .ok_or_else(|| EvolutionError::Configuration("Mutation operator required".into()))?;
902
903 Ok(InteractiveGA::new(
904 self.config,
905 self.bounds,
906 selection,
907 crossover,
908 mutation,
909 ))
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916 use crate::genome::real_vector::RealVector;
917 use crate::operators::crossover::SbxCrossover;
918 use crate::operators::mutation::PolynomialMutation;
919 use crate::operators::selection::TournamentSelection;
920 use rand::SeedableRng;
921
922 #[test]
923 fn test_interactive_ga_builder() {
924 let result = InteractiveGABuilder::<RealVector, (), (), ()>::new()
925 .population_size(10)
926 .evaluation_mode(EvaluationMode::Rating)
927 .selection(TournamentSelection::new(2))
928 .crossover(SbxCrossover::new(15.0))
929 .mutation(PolynomialMutation::new(20.0))
930 .build();
931
932 assert!(result.is_ok());
933 let iga = result.unwrap();
934 assert_eq!(iga.config().population_size, 10);
935 }
936
937 #[test]
938 fn test_interactive_ga_initialization() {
939 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
940
941 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
942 .population_size(5)
943 .evaluation_mode(EvaluationMode::Rating)
944 .batch_size(2)
945 .selection(TournamentSelection::new(2))
946 .crossover(SbxCrossover::new(15.0))
947 .mutation(PolynomialMutation::new(20.0))
948 .build()
949 .unwrap();
950
951 let result = iga.step(&mut rng);
952
953 match result {
954 StepResult::NeedsEvaluation(request) => {
955 assert!(request.candidate_count() <= 2);
956 }
957 _ => panic!("Expected NeedsEvaluation"),
958 }
959
960 assert_eq!(iga.session().population.len(), 5);
961 }
962
963 #[test]
964 fn test_provide_response() {
965 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
966
967 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
968 .population_size(4)
969 .evaluation_mode(EvaluationMode::Rating)
970 .batch_size(4)
971 .min_coverage(1.0)
972 .selection(TournamentSelection::new(2))
973 .crossover(SbxCrossover::new(15.0))
974 .mutation(PolynomialMutation::new(20.0))
975 .build()
976 .unwrap();
977
978 let result = iga.step(&mut rng);
980 let request = match result {
981 StepResult::NeedsEvaluation(r) => r,
982 _ => panic!("Expected NeedsEvaluation"),
983 };
984
985 let ids = request.candidate_ids();
987 let ratings: Vec<_> = ids
988 .into_iter()
989 .enumerate()
990 .map(|(i, id)| (id, (i + 1) as f64 * 2.0))
991 .collect();
992 iga.provide_response(EvaluationResponse::ratings(ratings));
993
994 let result = iga.step(&mut rng);
996 match result {
997 StepResult::GenerationComplete { generation, .. } => {
998 assert_eq!(generation, 0);
999 }
1000 _ => panic!("Expected GenerationComplete"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_pairwise_mode() {
1006 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1007
1008 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1009 .population_size(4)
1010 .evaluation_mode(EvaluationMode::Pairwise)
1011 .comparisons_per_candidate(2)
1012 .selection(TournamentSelection::new(2))
1013 .crossover(SbxCrossover::new(15.0))
1014 .mutation(PolynomialMutation::new(20.0))
1015 .build()
1016 .unwrap();
1017
1018 let result = iga.step(&mut rng);
1019
1020 match result {
1021 StepResult::NeedsEvaluation(EvaluationRequest::PairwiseComparison { .. }) => {}
1022 _ => panic!("Expected PairwiseComparison request"),
1023 }
1024 }
1025
1026 #[test]
1027 fn test_batch_selection_mode() {
1028 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1029
1030 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1031 .population_size(6)
1032 .evaluation_mode(EvaluationMode::BatchSelection)
1033 .batch_size(4)
1034 .select_count(2)
1035 .selection(TournamentSelection::new(2))
1036 .crossover(SbxCrossover::new(15.0))
1037 .mutation(PolynomialMutation::new(20.0))
1038 .build()
1039 .unwrap();
1040
1041 let result = iga.step(&mut rng);
1042
1043 match result {
1044 StepResult::NeedsEvaluation(EvaluationRequest::BatchSelection {
1045 candidates,
1046 select_count,
1047 ..
1048 }) => {
1049 assert_eq!(candidates.len(), 4);
1050 assert_eq!(select_count, 2);
1051 }
1052 _ => panic!("Expected BatchSelection request"),
1053 }
1054 }
1055
1056 #[test]
1057 fn test_skip_response() {
1058 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1059
1060 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1061 .population_size(4)
1062 .evaluation_mode(EvaluationMode::Rating)
1063 .batch_size(2)
1064 .selection(TournamentSelection::new(2))
1065 .crossover(SbxCrossover::new(15.0))
1066 .mutation(PolynomialMutation::new(20.0))
1067 .build()
1068 .unwrap();
1069
1070 let _ = iga.step(&mut rng);
1072
1073 iga.provide_response(EvaluationResponse::skip());
1075
1076 assert_eq!(iga.session().skipped, 1);
1077 assert_eq!(iga.session().responses_received, 0);
1078 }
1079}