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 let mut ids = Vec::new();
456 if let AlgorithmState::AwaitingEvaluation {
457 pending_request_ids,
458 } = &self.state
459 {
460 if pending_request_ids.len() == 2 {
461 let id_a = pending_request_ids[0];
462 let id_b = pending_request_ids[1];
463 ids = self
464 .session
465 .aggregator
466 .process_pairwise(id_a, id_b, *winner)
467 .into_iter()
468 .map(|(id, _)| id)
469 .collect();
470 }
471 }
472 ids
473 }
474 EvaluationResponse::BatchSelected(selected) => {
475 if let AlgorithmState::AwaitingEvaluation {
476 pending_request_ids,
477 } = &self.state
478 {
479 self.session
480 .aggregator
481 .process_batch_selection(pending_request_ids, selected);
482 }
483 selected.clone()
484 }
485 EvaluationResponse::Skip => Vec::new(),
486 };
487
488 for id in updated {
491 if let Some(estimate) = self.session.aggregator.get_fitness_estimate(&id) {
492 self.session.update_fitness_with_uncertainty(id, estimate);
493 } else if let Some(fitness) = self.session.aggregator.get_fitness(&id) {
494 self.session.update_fitness(id, fitness);
496 }
497 }
498
499 if let AlgorithmState::AwaitingEvaluation {
504 pending_request_ids,
505 } = &self.state
506 {
507 for id in pending_request_ids {
508 if let Some(candidate) = self.session.get_candidate_mut(*id) {
509 candidate.record_evaluation();
510 }
511 }
512 }
513
514 self.state = AlgorithmState::AwaitingEvaluation {
516 pending_request_ids: Vec::new(),
517 };
518 }
519
520 pub fn step<R: Rng>(&mut self, rng: &mut R) -> StepResult<G>
522 where
523 G: Serialize + for<'de> Deserialize<'de>,
524 {
525 loop {
526 match &self.state {
527 AlgorithmState::Initializing => {
528 self.initialize_population(rng);
529 self.state = AlgorithmState::AwaitingEvaluation {
530 pending_request_ids: Vec::new(),
531 };
532 }
533
534 AlgorithmState::AwaitingEvaluation {
535 pending_request_ids,
536 } => {
537 if !pending_request_ids.is_empty() {
539 continue;
541 }
542
543 let coverage = self.session.coverage_stats();
545
546 let enough_coverage = match self.config.evaluation_mode {
548 EvaluationMode::Pairwise => {
549 let target =
550 self.config.population_size * self.config.comparisons_per_candidate;
551 self.comparison_index >= target
552 }
553 _ => coverage.coverage >= self.config.min_coverage,
554 };
555
556 if enough_coverage {
557 self.state = AlgorithmState::ReadyForEvolution;
558 continue;
559 }
560
561 if let Some(request) = self.create_evaluation_request(rng) {
563 self.session.record_request(&request);
564 return StepResult::NeedsEvaluation(request);
565 } else {
566 self.state = AlgorithmState::ReadyForEvolution;
568 }
569 }
570
571 AlgorithmState::ReadyForEvolution => {
572 if let Some(reason) = self.should_terminate() {
574 self.state = AlgorithmState::Terminated {
575 reason: reason.clone(),
576 };
577 continue;
578 }
579
580 let generation = self.session.generation;
581 let best_fitness = self.session.best_candidate().and_then(|c| c.fitness());
582 let coverage = self.session.coverage_stats().coverage;
583
584 self.evolve_generation(rng);
586
587 return StepResult::GenerationComplete {
588 generation,
589 best_fitness,
590 coverage,
591 };
592 }
593
594 AlgorithmState::Terminated { reason } => {
595 let best_candidates = self
596 .session
597 .ranked_candidates()
598 .into_iter()
599 .take(self.config.elitism_count.max(3))
600 .cloned()
601 .collect();
602
603 return StepResult::Complete(Box::new(InteractiveResult {
604 best_candidates,
605 generations: self.session.generation,
606 total_evaluations: self.session.evaluations_requested,
607 session: self.session.clone(),
608 termination_reason: reason.clone(),
609 }));
610 }
611 }
612 }
613 }
614
615 fn evolve_generation<R: Rng>(&mut self, rng: &mut R)
617 where
618 G: Serialize + for<'de> Deserialize<'de>,
619 {
620 let pop_size = self.config.population_size;
621
622 let evaluated: Vec<(G, f64)> = self
624 .session
625 .population
626 .iter()
627 .filter_map(|c| c.fitness_estimate.map(|f| (c.genome.clone(), f)))
628 .collect();
629
630 if evaluated.is_empty() {
631 self.session.advance_generation();
633 return;
634 }
635
636 let mut new_population: Vec<Candidate<G>> = Vec::with_capacity(pop_size);
647 let elites: Vec<Candidate<G>> = self
648 .session
649 .ranked_candidates()
650 .into_iter()
651 .take(self.config.elitism_count)
652 .cloned()
653 .collect();
654
655 for elite in elites {
656 new_population.push(elite);
657 }
658
659 while new_population.len() < pop_size {
661 let parent1_idx = self.selection.select(&evaluated, rng);
663 let parent2_idx = self.selection.select(&evaluated, rng);
664
665 let parent1 = &evaluated[parent1_idx].0;
666 let parent2 = &evaluated[parent2_idx].0;
667
668 let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
670 match self.crossover.crossover(parent1, parent2, rng).genome() {
671 Some((c1, c2)) => (c1, c2),
672 None => (parent1.clone(), parent2.clone()),
673 }
674 } else {
675 (parent1.clone(), parent2.clone())
676 };
677
678 if rng.gen::<f64>() < self.config.mutation_probability {
680 self.mutation.mutate(&mut child1, rng);
681 }
682
683 let id = self.session.next_id();
684 new_population.push(Candidate::with_generation(
685 id,
686 child1,
687 self.session.generation + 1,
688 ));
689
690 if new_population.len() < pop_size {
691 if rng.gen::<f64>() < self.config.mutation_probability {
692 self.mutation.mutate(&mut child2, rng);
693 }
694
695 let id = self.session.next_id();
696 new_population.push(Candidate::with_generation(
697 id,
698 child2,
699 self.session.generation + 1,
700 ));
701 }
702 }
703
704 self.session.replace_population(new_population);
706 self.session.advance_generation();
707
708 self.unevaluated_indices =
710 (self.config.elitism_count..self.config.population_size).collect();
711 self.comparison_index = 0;
712 self.state = AlgorithmState::AwaitingEvaluation {
713 pending_request_ids: Vec::new(),
714 };
715 }
716
717 pub fn terminate(&mut self, reason: &str) {
719 self.state = AlgorithmState::Terminated {
720 reason: reason.to_string(),
721 };
722 }
723}
724
725pub struct InteractiveGABuilder<G, S, C, M>
727where
728 G: EvolutionaryGenome,
729{
730 config: InteractiveGAConfig,
731 bounds: Option<MultiBounds>,
732 selection: Option<S>,
733 crossover: Option<C>,
734 mutation: Option<M>,
735 _phantom: PhantomData<G>,
736}
737
738impl<G> InteractiveGABuilder<G, (), (), ()>
739where
740 G: EvolutionaryGenome,
741{
742 pub fn new() -> Self {
744 Self {
745 config: InteractiveGAConfig::default(),
746 bounds: None,
747 selection: None,
748 crossover: None,
749 mutation: None,
750 _phantom: PhantomData,
751 }
752 }
753}
754
755impl<G> Default for InteractiveGABuilder<G, (), (), ()>
756where
757 G: EvolutionaryGenome,
758{
759 fn default() -> Self {
760 Self::new()
761 }
762}
763
764impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
765where
766 G: EvolutionaryGenome,
767{
768 pub fn population_size(mut self, size: usize) -> Self {
770 self.config.population_size = size;
771 self
772 }
773
774 pub fn elitism_count(mut self, count: usize) -> Self {
776 self.config.elitism_count = count;
777 self
778 }
779
780 pub fn crossover_probability(mut self, prob: f64) -> Self {
782 self.config.crossover_probability = prob;
783 self
784 }
785
786 pub fn mutation_probability(mut self, prob: f64) -> Self {
788 self.config.mutation_probability = prob;
789 self
790 }
791
792 pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
794 self.config.evaluation_mode = mode;
795 self
796 }
797
798 pub fn batch_size(mut self, size: usize) -> Self {
800 self.config.batch_size = size;
801 self
802 }
803
804 pub fn select_count(mut self, count: usize) -> Self {
806 self.config.select_count = count;
807 self
808 }
809
810 pub fn min_coverage(mut self, coverage: f64) -> Self {
812 self.config.min_coverage = coverage.clamp(0.0, 1.0);
813 self
814 }
815
816 pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
818 self.config.comparisons_per_candidate = count;
819 self
820 }
821
822 pub fn max_generations(mut self, max: usize) -> Self {
824 self.config.max_generations = max;
825 self
826 }
827
828 pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
830 self.config.aggregation_model = model;
831 self
832 }
833
834 pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
844 self.config.selection_strategy = strategy;
845 self
846 }
847
848 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
850 self.bounds = Some(bounds);
851 self
852 }
853
854 pub fn selection<NewS>(self, selection: NewS) -> InteractiveGABuilder<G, NewS, C, M>
856 where
857 NewS: SelectionOperator<G>,
858 {
859 InteractiveGABuilder {
860 config: self.config,
861 bounds: self.bounds,
862 selection: Some(selection),
863 crossover: self.crossover,
864 mutation: self.mutation,
865 _phantom: PhantomData,
866 }
867 }
868
869 pub fn crossover<NewC>(self, crossover: NewC) -> InteractiveGABuilder<G, S, NewC, M>
871 where
872 NewC: CrossoverOperator<G>,
873 {
874 InteractiveGABuilder {
875 config: self.config,
876 bounds: self.bounds,
877 selection: self.selection,
878 crossover: Some(crossover),
879 mutation: self.mutation,
880 _phantom: PhantomData,
881 }
882 }
883
884 pub fn mutation<NewM>(self, mutation: NewM) -> InteractiveGABuilder<G, S, C, NewM>
886 where
887 NewM: MutationOperator<G>,
888 {
889 InteractiveGABuilder {
890 config: self.config,
891 bounds: self.bounds,
892 selection: self.selection,
893 crossover: self.crossover,
894 mutation: Some(mutation),
895 _phantom: PhantomData,
896 }
897 }
898}
899
900impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
901where
902 G: EvolutionaryGenome + Clone + Send + Sync,
903 S: SelectionOperator<G>,
904 C: CrossoverOperator<G>,
905 M: MutationOperator<G>,
906{
907 pub fn build(self) -> Result<InteractiveGA<G, S, C, M>, EvolutionError> {
909 let selection = self
910 .selection
911 .ok_or_else(|| EvolutionError::Configuration("Selection operator required".into()))?;
912 let crossover = self
913 .crossover
914 .ok_or_else(|| EvolutionError::Configuration("Crossover operator required".into()))?;
915 let mutation = self
916 .mutation
917 .ok_or_else(|| EvolutionError::Configuration("Mutation operator required".into()))?;
918
919 Ok(InteractiveGA::new(
920 self.config,
921 self.bounds,
922 selection,
923 crossover,
924 mutation,
925 ))
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::genome::real_vector::RealVector;
933 use crate::operators::crossover::SbxCrossover;
934 use crate::operators::mutation::PolynomialMutation;
935 use crate::operators::selection::TournamentSelection;
936 use rand::SeedableRng;
937
938 #[test]
939 fn test_interactive_ga_builder() {
940 let result = InteractiveGABuilder::<RealVector, (), (), ()>::new()
941 .population_size(10)
942 .evaluation_mode(EvaluationMode::Rating)
943 .selection(TournamentSelection::new(2))
944 .crossover(SbxCrossover::new(15.0))
945 .mutation(PolynomialMutation::new(20.0))
946 .build();
947
948 assert!(result.is_ok());
949 let iga = result.unwrap();
950 assert_eq!(iga.config().population_size, 10);
951 }
952
953 #[test]
954 fn test_interactive_ga_initialization() {
955 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
956
957 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
958 .population_size(5)
959 .evaluation_mode(EvaluationMode::Rating)
960 .batch_size(2)
961 .selection(TournamentSelection::new(2))
962 .crossover(SbxCrossover::new(15.0))
963 .mutation(PolynomialMutation::new(20.0))
964 .build()
965 .unwrap();
966
967 let result = iga.step(&mut rng);
968
969 match result {
970 StepResult::NeedsEvaluation(request) => {
971 assert!(request.candidate_count() <= 2);
972 }
973 _ => panic!("Expected NeedsEvaluation"),
974 }
975
976 assert_eq!(iga.session().population.len(), 5);
977 }
978
979 #[test]
980 fn test_provide_response() {
981 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
982
983 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
984 .population_size(4)
985 .evaluation_mode(EvaluationMode::Rating)
986 .batch_size(4)
987 .min_coverage(1.0)
988 .selection(TournamentSelection::new(2))
989 .crossover(SbxCrossover::new(15.0))
990 .mutation(PolynomialMutation::new(20.0))
991 .build()
992 .unwrap();
993
994 let result = iga.step(&mut rng);
996 let request = match result {
997 StepResult::NeedsEvaluation(r) => r,
998 _ => panic!("Expected NeedsEvaluation"),
999 };
1000
1001 let ids = request.candidate_ids();
1003 let ratings: Vec<_> = ids
1004 .into_iter()
1005 .enumerate()
1006 .map(|(i, id)| (id, (i + 1) as f64 * 2.0))
1007 .collect();
1008 iga.provide_response(EvaluationResponse::ratings(ratings));
1009
1010 let result = iga.step(&mut rng);
1012 match result {
1013 StepResult::GenerationComplete { generation, .. } => {
1014 assert_eq!(generation, 0);
1015 }
1016 _ => panic!("Expected GenerationComplete"),
1017 }
1018 }
1019
1020 #[test]
1021 fn test_pairwise_mode() {
1022 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1023
1024 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1025 .population_size(4)
1026 .evaluation_mode(EvaluationMode::Pairwise)
1027 .comparisons_per_candidate(2)
1028 .selection(TournamentSelection::new(2))
1029 .crossover(SbxCrossover::new(15.0))
1030 .mutation(PolynomialMutation::new(20.0))
1031 .build()
1032 .unwrap();
1033
1034 let result = iga.step(&mut rng);
1035
1036 match result {
1037 StepResult::NeedsEvaluation(EvaluationRequest::PairwiseComparison { .. }) => {}
1038 _ => panic!("Expected PairwiseComparison request"),
1039 }
1040 }
1041
1042 #[test]
1043 fn test_batch_selection_mode() {
1044 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1045
1046 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1047 .population_size(6)
1048 .evaluation_mode(EvaluationMode::BatchSelection)
1049 .batch_size(4)
1050 .select_count(2)
1051 .selection(TournamentSelection::new(2))
1052 .crossover(SbxCrossover::new(15.0))
1053 .mutation(PolynomialMutation::new(20.0))
1054 .build()
1055 .unwrap();
1056
1057 let result = iga.step(&mut rng);
1058
1059 match result {
1060 StepResult::NeedsEvaluation(EvaluationRequest::BatchSelection {
1061 candidates,
1062 select_count,
1063 ..
1064 }) => {
1065 assert_eq!(candidates.len(), 4);
1066 assert_eq!(select_count, 2);
1067 }
1068 _ => panic!("Expected BatchSelection request"),
1069 }
1070 }
1071
1072 #[test]
1073 fn test_skip_response() {
1074 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1075
1076 let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1077 .population_size(4)
1078 .evaluation_mode(EvaluationMode::Rating)
1079 .batch_size(2)
1080 .selection(TournamentSelection::new(2))
1081 .crossover(SbxCrossover::new(15.0))
1082 .mutation(PolynomialMutation::new(20.0))
1083 .build()
1084 .unwrap();
1085
1086 let _ = iga.step(&mut rng);
1088
1089 iga.provide_response(EvaluationResponse::skip());
1091
1092 assert_eq!(iga.session().skipped, 1);
1093 assert_eq!(iga.session().responses_received, 0);
1094 }
1095}