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>
173where
174 G: EvolutionaryGenome,
175{
176 config: InteractiveGAConfig,
177 bounds: Option<MultiBounds>,
178 selection: S,
179 crossover: C,
180 mutation: M,
181 session: InteractiveSession<G>,
182 state: AlgorithmState,
183 unevaluated_indices: Vec<usize>,
185 comparison_index: usize,
187}
188
189impl<G, S, C, M> InteractiveGA<G, S, C, M>
190where
191 G: EvolutionaryGenome + Clone + Send + Sync,
192 S: SelectionOperator<G>,
193 C: CrossoverOperator<G>,
194 M: MutationOperator<G>,
195{
196 pub fn new(
198 config: InteractiveGAConfig,
199 bounds: Option<MultiBounds>,
200 selection: S,
201 crossover: C,
202 mutation: M,
203 ) -> Self {
204 let aggregator = FitnessAggregator::new(config.aggregation_model.clone());
205 Self {
206 config,
207 bounds,
208 selection,
209 crossover,
210 mutation,
211 session: InteractiveSession::new(aggregator),
212 state: AlgorithmState::Initializing,
213 unevaluated_indices: Vec::new(),
214 comparison_index: 0,
215 }
216 }
217
218 pub fn from_session(
220 session: InteractiveSession<G>,
221 config: InteractiveGAConfig,
222 bounds: Option<MultiBounds>,
223 selection: S,
224 crossover: C,
225 mutation: M,
226 ) -> Self {
227 let unevaluated: Vec<usize> = session
228 .population
229 .iter()
230 .enumerate()
231 .filter(|(_, c)| !c.is_evaluated())
232 .map(|(i, _)| i)
233 .collect();
234
235 let state = if session.population.is_empty() {
236 AlgorithmState::Initializing
237 } else if unevaluated.is_empty() {
238 AlgorithmState::ReadyForEvolution
239 } else {
240 AlgorithmState::AwaitingEvaluation {
241 pending_request_ids: Vec::new(),
242 }
243 };
244
245 Self {
246 config,
247 bounds,
248 selection,
249 crossover,
250 mutation,
251 session,
252 state,
253 unevaluated_indices: unevaluated,
254 comparison_index: 0,
255 }
256 }
257
258 pub fn session(&self) -> &InteractiveSession<G> {
260 &self.session
261 }
262
263 pub fn session_mut(&mut self) -> &mut InteractiveSession<G> {
265 &mut self.session
266 }
267
268 pub fn config(&self) -> &InteractiveGAConfig {
270 &self.config
271 }
272
273 pub fn coverage_stats(&self) -> CoverageStats {
275 self.session.coverage_stats()
276 }
277
278 fn should_terminate(&self) -> Option<String> {
280 if self.config.max_generations > 0 && self.session.generation >= self.config.max_generations
281 {
282 return Some(format!(
283 "Reached maximum generations ({})",
284 self.config.max_generations
285 ));
286 }
287 None
288 }
289
290 fn initialize_population<R: Rng>(&mut self, rng: &mut R) {
292 let bounds = self
294 .bounds
295 .clone()
296 .unwrap_or_else(|| MultiBounds::symmetric(1.0, 1));
297
298 for _ in 0..self.config.population_size {
299 let genome = G::generate(rng, &bounds);
300 self.session.add_candidate(genome);
301 }
302
303 self.unevaluated_indices = (0..self.config.population_size).collect();
304 self.comparison_index = 0;
305 }
306
307 fn create_evaluation_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
309 match self.config.evaluation_mode {
310 EvaluationMode::Rating => self.create_rating_request(rng),
311 EvaluationMode::Pairwise => self.create_pairwise_request(rng),
312 EvaluationMode::BatchSelection => self.create_batch_request(rng),
313 EvaluationMode::Adaptive => self.create_adaptive_request(rng),
314 }
315 }
316
317 fn create_rating_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
318 let batch_size = self.config.batch_size.min(self.session.population.len());
319 if batch_size == 0 {
320 return None;
321 }
322
323 let selected_indices = self.config.selection_strategy.select_batch(
325 &self.session.population,
326 &self.session.aggregator,
327 batch_size,
328 rng,
329 );
330
331 if selected_indices.is_empty() {
332 return None;
333 }
334
335 let candidates: Vec<Candidate<G>> = selected_indices
336 .iter()
337 .filter_map(|&i| self.session.population.get(i).cloned())
338 .collect();
339
340 let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
341 self.state = AlgorithmState::AwaitingEvaluation {
342 pending_request_ids: ids,
343 };
344
345 Some(EvaluationRequest::rate(candidates))
346 }
347
348 fn create_pairwise_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
349 let pop_size = self.session.population.len();
350 if pop_size < 2 {
351 return None;
352 }
353
354 let pair = self.config.selection_strategy.select_pair(
356 &self.session.population,
357 &self.session.aggregator,
358 rng,
359 );
360
361 let (idx_a, idx_b) = match pair {
362 Some(p) => p,
363 None => {
364 let idx_a = self.comparison_index % pop_size;
366 let idx_b = (self.comparison_index + 1) % pop_size;
367 (idx_a, idx_b)
368 }
369 };
370
371 self.comparison_index += 1;
372
373 let candidate_a = self.session.population.get(idx_a)?.clone();
374 let candidate_b = self.session.population.get(idx_b)?.clone();
375
376 let ids = vec![candidate_a.id, candidate_b.id];
377 self.state = AlgorithmState::AwaitingEvaluation {
378 pending_request_ids: ids,
379 };
380
381 Some(EvaluationRequest::compare(candidate_a, candidate_b))
382 }
383
384 fn create_batch_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
385 let batch_size = self.config.batch_size.min(self.session.population.len());
386 if batch_size < 2 {
387 return self.create_rating_request(rng); }
390
391 let selected_indices = self.config.selection_strategy.select_batch(
393 &self.session.population,
394 &self.session.aggregator,
395 batch_size,
396 rng,
397 );
398
399 if selected_indices.len() < 2 {
400 return self.create_rating_request(rng);
401 }
402
403 let candidates: Vec<Candidate<G>> = selected_indices
404 .iter()
405 .filter_map(|&i| self.session.population.get(i).cloned())
406 .collect();
407
408 let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
409 self.state = AlgorithmState::AwaitingEvaluation {
410 pending_request_ids: ids,
411 };
412
413 let select_count = self.config.select_count.min(candidates.len() - 1);
414 Some(EvaluationRequest::select_from_batch(
415 candidates,
416 select_count,
417 ))
418 }
419
420 fn create_adaptive_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
421 let coverage = self.session.coverage_stats().coverage;
424 if coverage < 0.5 {
425 self.create_rating_request(rng)
426 } else {
427 self.create_pairwise_request(rng)
428 }
429 }
430
431 pub fn provide_response(&mut self, response: EvaluationResponse) {
433 let was_skipped = response.is_skip();
434 self.session.record_response(was_skipped);
435
436 if was_skipped {
437 if let AlgorithmState::AwaitingEvaluation {
439 pending_request_ids,
440 } = &self.state
441 {
442 for id in pending_request_ids {
443 if let Some(pos) = self.session.population.iter().position(|c| c.id == *id) {
444 if !self.unevaluated_indices.contains(&pos) {
445 self.unevaluated_indices.push(pos);
446 }
447 }
448 }
449 }
450 self.state = AlgorithmState::AwaitingEvaluation {
451 pending_request_ids: Vec::new(),
452 };
453 return;
454 }
455
456 let updated = match &response {
458 EvaluationResponse::Ratings(ratings) => {
459 self.session.aggregator.process_response(&response);
460 ratings.iter().map(|(id, _)| *id).collect::<Vec<_>>()
461 }
462 EvaluationResponse::PairwiseWinner(winner) => {
463 let mut ids = Vec::new();
467 if let AlgorithmState::AwaitingEvaluation {
468 pending_request_ids,
469 } = &self.state
470 {
471 if pending_request_ids.len() == 2 {
472 let id_a = pending_request_ids[0];
473 let id_b = pending_request_ids[1];
474 ids = self
475 .session
476 .aggregator
477 .process_pairwise(id_a, id_b, *winner)
478 .into_iter()
479 .map(|(id, _)| id)
480 .collect();
481 }
482 }
483 ids
484 }
485 EvaluationResponse::BatchSelected(selected) => {
486 if let AlgorithmState::AwaitingEvaluation {
487 pending_request_ids,
488 } = &self.state
489 {
490 self.session
491 .aggregator
492 .process_batch_selection(pending_request_ids, selected);
493 }
494 selected.clone()
495 }
496 EvaluationResponse::Skip => Vec::new(),
497 };
498
499 for id in updated {
502 if let Some(estimate) = self.session.aggregator.get_fitness_estimate(&id) {
503 self.session.update_fitness_with_uncertainty(id, estimate);
504 } else if let Some(fitness) = self.session.aggregator.get_fitness(&id) {
505 self.session.update_fitness(id, fitness);
507 }
508 }
509
510 if let AlgorithmState::AwaitingEvaluation {
515 pending_request_ids,
516 } = &self.state
517 {
518 for id in pending_request_ids {
519 if let Some(candidate) = self.session.get_candidate_mut(*id) {
520 candidate.record_evaluation();
521 }
522 }
523 }
524
525 self.state = AlgorithmState::AwaitingEvaluation {
527 pending_request_ids: Vec::new(),
528 };
529 }
530
531 pub fn step<R: Rng>(&mut self, rng: &mut R) -> StepResult<G>
533 where
534 G: Serialize + for<'de> Deserialize<'de>,
535 {
536 loop {
537 match &self.state {
538 AlgorithmState::Initializing => {
539 self.initialize_population(rng);
540 self.state = AlgorithmState::AwaitingEvaluation {
541 pending_request_ids: Vec::new(),
542 };
543 }
544
545 AlgorithmState::AwaitingEvaluation {
546 pending_request_ids,
547 } => {
548 if !pending_request_ids.is_empty() {
550 continue;
552 }
553
554 let coverage = self.session.coverage_stats();
556
557 let enough_coverage = match self.config.evaluation_mode {
559 EvaluationMode::Pairwise => {
560 let target =
561 self.config.population_size * self.config.comparisons_per_candidate;
562 self.comparison_index >= target
563 }
564 _ => coverage.coverage >= self.config.min_coverage,
565 };
566
567 if enough_coverage {
568 self.state = AlgorithmState::ReadyForEvolution;
569 continue;
570 }
571
572 if let Some(request) = self.create_evaluation_request(rng) {
574 self.session.record_request(&request);
575 return StepResult::NeedsEvaluation(request);
576 } else {
577 self.state = AlgorithmState::ReadyForEvolution;
579 }
580 }
581
582 AlgorithmState::ReadyForEvolution => {
583 if let Some(reason) = self.should_terminate() {
585 self.state = AlgorithmState::Terminated {
586 reason: reason.clone(),
587 };
588 continue;
589 }
590
591 let generation = self.session.generation;
592 let best_fitness = self.session.best_candidate().and_then(|c| c.fitness());
593 let coverage = self.session.coverage_stats().coverage;
594
595 self.evolve_generation(rng);
597
598 return StepResult::GenerationComplete {
599 generation,
600 best_fitness,
601 coverage,
602 };
603 }
604
605 AlgorithmState::Terminated { reason } => {
606 let best_candidates = self
607 .session
608 .ranked_candidates()
609 .into_iter()
610 .take(self.config.elitism_count.max(3))
611 .cloned()
612 .collect();
613
614 return StepResult::Complete(Box::new(InteractiveResult {
615 best_candidates,
616 generations: self.session.generation,
617 total_evaluations: self.session.evaluations_requested,
618 session: self.session.clone(),
619 termination_reason: reason.clone(),
620 }));
621 }
622 }
623 }
624 }
625
626 fn evolve_generation<R: Rng>(&mut self, rng: &mut R)
628 where
629 G: Serialize + for<'de> Deserialize<'de>,
630 {
631 let pop_size = self.config.population_size;
632
633 let evaluated: Vec<(G, f64)> = self
635 .session
636 .population
637 .iter()
638 .filter_map(|c| c.fitness_estimate.map(|f| (c.genome.clone(), f)))
639 .collect();
640
641 if evaluated.is_empty() {
642 self.session.advance_generation();
644 return;
645 }
646
647 let mut new_population: Vec<Candidate<G>> = Vec::with_capacity(pop_size);
658 let elites: Vec<Candidate<G>> = self
659 .session
660 .ranked_candidates()
661 .into_iter()
662 .take(self.config.elitism_count)
663 .cloned()
664 .collect();
665
666 for elite in elites {
667 new_population.push(elite);
668 }
669
670 while new_population.len() < pop_size {
672 let parent1_idx = self.selection.select(&evaluated, rng);
674 let parent2_idx = self.selection.select(&evaluated, rng);
675
676 let parent1 = &evaluated[parent1_idx].0;
677 let parent2 = &evaluated[parent2_idx].0;
678
679 let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
681 match self.crossover.crossover(parent1, parent2, rng).genome() {
682 Some((c1, c2)) => (c1, c2),
683 None => (parent1.clone(), parent2.clone()),
684 }
685 } else {
686 (parent1.clone(), parent2.clone())
687 };
688
689 if rng.gen::<f64>() < self.config.mutation_probability {
691 self.mutation.mutate(&mut child1, rng);
692 }
693
694 let id = self.session.next_id();
695 new_population.push(Candidate::with_generation(
696 id,
697 child1,
698 self.session.generation + 1,
699 ));
700
701 if new_population.len() < pop_size {
702 if rng.gen::<f64>() < self.config.mutation_probability {
703 self.mutation.mutate(&mut child2, rng);
704 }
705
706 let id = self.session.next_id();
707 new_population.push(Candidate::with_generation(
708 id,
709 child2,
710 self.session.generation + 1,
711 ));
712 }
713 }
714
715 self.session.replace_population(new_population);
717 self.session.advance_generation();
718
719 self.unevaluated_indices =
721 (self.config.elitism_count..self.config.population_size).collect();
722 self.comparison_index = 0;
723 self.state = AlgorithmState::AwaitingEvaluation {
724 pending_request_ids: Vec::new(),
725 };
726 }
727
728 pub fn terminate(&mut self, reason: &str) {
730 self.state = AlgorithmState::Terminated {
731 reason: reason.to_string(),
732 };
733 }
734}
735
736pub struct InteractiveGABuilder<G, S, C, M>
738where
739 G: EvolutionaryGenome,
740{
741 config: InteractiveGAConfig,
742 bounds: Option<MultiBounds>,
743 selection: Option<S>,
744 crossover: Option<C>,
745 mutation: Option<M>,
746 _phantom: PhantomData<G>,
747}
748
749impl<G> InteractiveGABuilder<G, (), (), ()>
750where
751 G: EvolutionaryGenome,
752{
753 pub fn new() -> Self {
755 Self {
756 config: InteractiveGAConfig::default(),
757 bounds: None,
758 selection: None,
759 crossover: None,
760 mutation: None,
761 _phantom: PhantomData,
762 }
763 }
764}
765
766impl<G> Default for InteractiveGABuilder<G, (), (), ()>
767where
768 G: EvolutionaryGenome,
769{
770 fn default() -> Self {
771 Self::new()
772 }
773}
774
775impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
776where
777 G: EvolutionaryGenome,
778{
779 pub fn population_size(mut self, size: usize) -> Self {
781 self.config.population_size = size;
782 self
783 }
784
785 pub fn elitism_count(mut self, count: usize) -> Self {
787 self.config.elitism_count = count;
788 self
789 }
790
791 pub fn crossover_probability(mut self, prob: f64) -> Self {
793 self.config.crossover_probability = prob;
794 self
795 }
796
797 pub fn mutation_probability(mut self, prob: f64) -> Self {
799 self.config.mutation_probability = prob;
800 self
801 }
802
803 pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
805 self.config.evaluation_mode = mode;
806 self
807 }
808
809 pub fn batch_size(mut self, size: usize) -> Self {
811 self.config.batch_size = size;
812 self
813 }
814
815 pub fn select_count(mut self, count: usize) -> Self {
817 self.config.select_count = count;
818 self
819 }
820
821 pub fn min_coverage(mut self, coverage: f64) -> Self {
823 self.config.min_coverage = coverage.clamp(0.0, 1.0);
824 self
825 }
826
827 pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
829 self.config.comparisons_per_candidate = count;
830 self
831 }
832
833 pub fn max_generations(mut self, max: usize) -> Self {
835 self.config.max_generations = max;
836 self
837 }
838
839 pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
841 self.config.aggregation_model = model;
842 self
843 }
844
845 pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
858 self.config.selection_strategy = strategy;
859 self
860 }
861
862 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
864 self.bounds = Some(bounds);
865 self
866 }
867
868 pub fn selection<NewS>(self, selection: NewS) -> InteractiveGABuilder<G, NewS, C, M>
870 where
871 NewS: SelectionOperator<G>,
872 {
873 InteractiveGABuilder {
874 config: self.config,
875 bounds: self.bounds,
876 selection: Some(selection),
877 crossover: self.crossover,
878 mutation: self.mutation,
879 _phantom: PhantomData,
880 }
881 }
882
883 pub fn crossover<NewC>(self, crossover: NewC) -> InteractiveGABuilder<G, S, NewC, M>
885 where
886 NewC: CrossoverOperator<G>,
887 {
888 InteractiveGABuilder {
889 config: self.config,
890 bounds: self.bounds,
891 selection: self.selection,
892 crossover: Some(crossover),
893 mutation: self.mutation,
894 _phantom: PhantomData,
895 }
896 }
897
898 pub fn mutation<NewM>(self, mutation: NewM) -> InteractiveGABuilder<G, S, C, NewM>
900 where
901 NewM: MutationOperator<G>,
902 {
903 InteractiveGABuilder {
904 config: self.config,
905 bounds: self.bounds,
906 selection: self.selection,
907 crossover: self.crossover,
908 mutation: Some(mutation),
909 _phantom: PhantomData,
910 }
911 }
912}
913
914impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
915where
916 G: EvolutionaryGenome + Clone + Send + Sync,
917 S: SelectionOperator<G>,
918 C: CrossoverOperator<G>,
919 M: MutationOperator<G>,
920{
921 pub fn build(self) -> Result<InteractiveGA<G, S, C, M>, EvolutionError> {
923 let selection = self
924 .selection
925 .ok_or_else(|| EvolutionError::Configuration("Selection operator required".into()))?;
926 let crossover = self
927 .crossover
928 .ok_or_else(|| EvolutionError::Configuration("Crossover operator required".into()))?;
929 let mutation = self
930 .mutation
931 .ok_or_else(|| EvolutionError::Configuration("Mutation operator required".into()))?;
932
933 Ok(InteractiveGA::new(
934 self.config,
935 self.bounds,
936 selection,
937 crossover,
938 mutation,
939 ))
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946 use crate::genome::real_vector::RealVector;
947 use crate::operators::crossover::SbxCrossover;
948 use crate::operators::mutation::PolynomialMutation;
949 use crate::operators::selection::TournamentSelection;
950 use rand::SeedableRng;
951
952 #[test]
953 fn test_interactive_ga_builder() {
954 let result = InteractiveGABuilder::<RealVector, (), (), ()>::new()
955 .population_size(10)
956 .evaluation_mode(EvaluationMode::Rating)
957 .selection(TournamentSelection::new(2))
958 .crossover(SbxCrossover::new(15.0))
959 .mutation(PolynomialMutation::new(20.0))
960 .build();
961
962 assert!(result.is_ok());
963 let iga = result.unwrap();
964 assert_eq!(iga.config().population_size, 10);
965 }
966
967 #[test]
968 fn test_interactive_ga_initialization() {
969 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
970
971 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
972 .population_size(5)
973 .evaluation_mode(EvaluationMode::Rating)
974 .batch_size(2)
975 .selection(TournamentSelection::new(2))
976 .crossover(SbxCrossover::new(15.0))
977 .mutation(PolynomialMutation::new(20.0))
978 .build()
979 .unwrap();
980
981 let result = iga.step(&mut rng);
982
983 match result {
984 StepResult::NeedsEvaluation(request) => {
985 assert!(request.candidate_count() <= 2);
986 }
987 _ => panic!("Expected NeedsEvaluation"),
988 }
989
990 assert_eq!(iga.session().population.len(), 5);
991 }
992
993 #[test]
994 fn test_provide_response() {
995 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
996
997 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
998 .population_size(4)
999 .evaluation_mode(EvaluationMode::Rating)
1000 .batch_size(4)
1001 .min_coverage(1.0)
1002 .selection(TournamentSelection::new(2))
1003 .crossover(SbxCrossover::new(15.0))
1004 .mutation(PolynomialMutation::new(20.0))
1005 .build()
1006 .unwrap();
1007
1008 let result = iga.step(&mut rng);
1010 let request = match result {
1011 StepResult::NeedsEvaluation(r) => r,
1012 _ => panic!("Expected NeedsEvaluation"),
1013 };
1014
1015 let ids = request.candidate_ids();
1017 let ratings: Vec<_> = ids
1018 .into_iter()
1019 .enumerate()
1020 .map(|(i, id)| (id, (i + 1) as f64 * 2.0))
1021 .collect();
1022 iga.provide_response(EvaluationResponse::ratings(ratings));
1023
1024 let result = iga.step(&mut rng);
1026 match result {
1027 StepResult::GenerationComplete { generation, .. } => {
1028 assert_eq!(generation, 0);
1029 }
1030 _ => panic!("Expected GenerationComplete"),
1031 }
1032 }
1033
1034 #[test]
1035 fn test_pairwise_mode() {
1036 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1037
1038 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1039 .population_size(4)
1040 .evaluation_mode(EvaluationMode::Pairwise)
1041 .comparisons_per_candidate(2)
1042 .selection(TournamentSelection::new(2))
1043 .crossover(SbxCrossover::new(15.0))
1044 .mutation(PolynomialMutation::new(20.0))
1045 .build()
1046 .unwrap();
1047
1048 let result = iga.step(&mut rng);
1049
1050 match result {
1051 StepResult::NeedsEvaluation(EvaluationRequest::PairwiseComparison { .. }) => {}
1052 _ => panic!("Expected PairwiseComparison request"),
1053 }
1054 }
1055
1056 #[test]
1057 fn test_batch_selection_mode() {
1058 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1059
1060 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1061 .population_size(6)
1062 .evaluation_mode(EvaluationMode::BatchSelection)
1063 .batch_size(4)
1064 .select_count(2)
1065 .selection(TournamentSelection::new(2))
1066 .crossover(SbxCrossover::new(15.0))
1067 .mutation(PolynomialMutation::new(20.0))
1068 .build()
1069 .unwrap();
1070
1071 let result = iga.step(&mut rng);
1072
1073 match result {
1074 StepResult::NeedsEvaluation(EvaluationRequest::BatchSelection {
1075 candidates,
1076 select_count,
1077 ..
1078 }) => {
1079 assert_eq!(candidates.len(), 4);
1080 assert_eq!(select_count, 2);
1081 }
1082 _ => panic!("Expected BatchSelection request"),
1083 }
1084 }
1085
1086 #[test]
1087 fn test_skip_response() {
1088 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1089
1090 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1091 .population_size(4)
1092 .evaluation_mode(EvaluationMode::Rating)
1093 .batch_size(2)
1094 .selection(TournamentSelection::new(2))
1095 .crossover(SbxCrossover::new(15.0))
1096 .mutation(PolynomialMutation::new(20.0))
1097 .build()
1098 .unwrap();
1099
1100 let _ = iga.step(&mut rng);
1102
1103 iga.provide_response(EvaluationResponse::skip());
1105
1106 assert_eq!(iga.session().skipped, 1);
1107 assert_eq!(iga.session().responses_received, 0);
1108 }
1109}