Skip to main content

fugue_evo/interactive/
algorithm.rs

1//! Interactive Genetic Algorithm implementation
2//!
3//! This module provides the `InteractiveGA` algorithm which uses a step-based
4//! iterator pattern to allow human-in-the-loop fitness evaluation.
5
6use 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/// Configuration for Interactive GA
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct InteractiveGAConfig {
23    /// Population size (smaller than standard GA for human evaluation)
24    pub population_size: usize,
25    /// Number of elite individuals to preserve
26    pub elitism_count: usize,
27    /// Crossover probability
28    pub crossover_probability: f64,
29    /// Mutation probability
30    pub mutation_probability: f64,
31    /// Evaluation mode
32    pub evaluation_mode: EvaluationMode,
33    /// Number of candidates per evaluation batch
34    pub batch_size: usize,
35    /// Number to select in batch selection mode
36    pub select_count: usize,
37    /// Minimum coverage fraction before proceeding to next generation
38    pub min_coverage: f64,
39    /// Number of comparisons per candidate per generation (for pairwise mode)
40    pub comparisons_per_candidate: usize,
41    /// Maximum generations (0 = unlimited)
42    pub max_generations: usize,
43    /// Aggregation model for fitness computation
44    pub aggregation_model: AggregationModel,
45    /// Active learning strategy for candidate selection
46    #[serde(default)]
47    pub selection_strategy: SelectionStrategy,
48}
49
50impl Default for InteractiveGAConfig {
51    fn default() -> Self {
52        Self {
53            population_size: 20, // Smaller for human evaluation
54            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, // 80% must be evaluated
61            comparisons_per_candidate: 3,
62            max_generations: 0, // Unlimited
63            aggregation_model: AggregationModel::DirectRating {
64                default_rating: 5.0,
65            },
66            selection_strategy: SelectionStrategy::Sequential,
67        }
68    }
69}
70
71/// Internal state machine for the algorithm
72#[derive(Clone, Debug)]
73enum AlgorithmState {
74    /// Need to initialize population
75    Initializing,
76    /// Waiting for evaluation responses
77    AwaitingEvaluation {
78        /// Current request being processed
79        pending_request_ids: Vec<CandidateId>,
80    },
81    /// Ready to perform selection and create next generation
82    ReadyForEvolution,
83    /// Evolution complete
84    Terminated { reason: String },
85}
86
87/// Result of calling `step()` on the algorithm
88#[derive(Clone, Debug)]
89pub enum StepResult<G>
90where
91    G: EvolutionaryGenome,
92{
93    /// Algorithm needs user input
94    NeedsEvaluation(EvaluationRequest<G>),
95
96    /// Generation complete, ready to continue
97    GenerationComplete {
98        /// Generation number that completed
99        generation: usize,
100        /// Best fitness in the generation
101        best_fitness: Option<f64>,
102        /// Evaluation coverage achieved
103        coverage: f64,
104    },
105
106    /// Evolution terminated
107    Complete(Box<InteractiveResult<G>>),
108}
109
110/// Final result of interactive evolution
111#[derive(Clone, Debug)]
112pub struct InteractiveResult<G>
113where
114    G: EvolutionaryGenome,
115{
116    /// Best candidates found
117    pub best_candidates: Vec<Candidate<G>>,
118    /// Number of generations completed
119    pub generations: usize,
120    /// Total evaluation requests made
121    pub total_evaluations: usize,
122    /// Final session state
123    pub session: InteractiveSession<G>,
124    /// Termination reason
125    pub termination_reason: String,
126}
127
128/// Step-based Interactive Genetic Algorithm
129///
130/// Unlike standard GA algorithms that run to completion, InteractiveGA yields
131/// control between evaluations, allowing the caller to interact with users.
132///
133/// # Example
134///
135/// ```rust,ignore
136/// use fugue_evo::interactive::prelude::*;
137///
138/// let mut iga = InteractiveGABuilder::<MyGenome>::new()
139///     .population_size(12)
140///     .evaluation_mode(EvaluationMode::BatchSelection)
141///     .build()?;
142///
143/// let mut rng = rand::thread_rng();
144///
145/// loop {
146///     match iga.step(&mut rng) {
147///         StepResult::NeedsEvaluation(request) => {
148///             let response = get_user_feedback(&request);
149///             iga.provide_response(response);
150///         }
151///         StepResult::GenerationComplete { generation, .. } => {
152///             println!("Generation {} complete", generation);
153///         }
154///         StepResult::Complete(result) => {
155///             println!("Evolution complete: {}", result.termination_reason);
156///             break;
157///         }
158///     }
159/// }
160/// ```
161pub 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    /// Indices of candidates still needing evaluation this generation
173    unevaluated_indices: Vec<usize>,
174    /// Index for pairwise comparison scheduling
175    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    /// Create a new InteractiveGA
186    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    /// Resume from a saved session
208    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    /// Get the current session
248    pub fn session(&self) -> &InteractiveSession<G> {
249        &self.session
250    }
251
252    /// Get mutable reference to session (for custom modifications)
253    pub fn session_mut(&mut self) -> &mut InteractiveSession<G> {
254        &mut self.session
255    }
256
257    /// Get the configuration
258    pub fn config(&self) -> &InteractiveGAConfig {
259        &self.config
260    }
261
262    /// Get coverage statistics
263    pub fn coverage_stats(&self) -> CoverageStats {
264        self.session.coverage_stats()
265    }
266
267    /// Check if algorithm should terminate
268    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    /// Initialize the population
280    fn initialize_population<R: Rng>(&mut self, rng: &mut R) {
281        // Need bounds for genome generation
282        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    /// Create an evaluation request based on the current mode
297    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        // Use selection strategy to pick candidates
313        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        // Use selection strategy for intelligent pair selection
344        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                // Fallback to round-robin if strategy returns None
354                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            // Need at least 2 for selection
377            return self.create_rating_request(rng); // Fall back
378        }
379
380        // Use selection strategy to pick candidates
381        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        // Simple adaptive strategy: use rating for initial coverage,
411        // then switch to pairwise for refinement
412        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    /// Provide user response to an evaluation request
421    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            // Put unevaluated candidates back if skipped
427            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        // Process the response
446        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                // Capture BOTH compared ids returned by process_pairwise so the
453                // winner AND the loser have their fitness re-synced from the
454                // (now Bradley-Terry-refit, EV-06) aggregator, not just the winner.
455                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        // Update candidate fitness estimates with uncertainty. These are pure
489        // setters (they do NOT touch evaluation_count — see EV-26 / EV-64).
490        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                // Fallback to point estimate only
495                self.session.update_fitness(id, fitness);
496            }
497        }
498
499        // Single owner of evaluation counting (EV-26 / EV-64): every candidate
500        // that was actually presented in this request is counted exactly once,
501        // regardless of whether the aggregator produced an estimate for it. This
502        // is symmetric in pairwise mode (both compared candidates get +1).
503        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        // Transition state
515        self.state = AlgorithmState::AwaitingEvaluation {
516            pending_request_ids: Vec::new(),
517        };
518    }
519
520    /// Advance the algorithm one step
521    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 we have a pending request, wait for response
538                    if !pending_request_ids.is_empty() {
539                        // This shouldn't happen in normal flow, but handle it
540                        continue;
541                    }
542
543                    // Check if we have enough coverage
544                    let coverage = self.session.coverage_stats();
545
546                    // For pairwise mode, check comparison count instead
547                    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                    // Create next evaluation request
562                    if let Some(request) = self.create_evaluation_request(rng) {
563                        self.session.record_request(&request);
564                        return StepResult::NeedsEvaluation(request);
565                    } else {
566                        // No more candidates to evaluate
567                        self.state = AlgorithmState::ReadyForEvolution;
568                    }
569                }
570
571                AlgorithmState::ReadyForEvolution => {
572                    // Check termination
573                    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                    // Perform evolution
585                    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    /// Perform selection and create next generation
616    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        // Get current population with fitness (genome, fitness) pairs
623        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            // No evaluated individuals, can't evolve
632            self.session.advance_generation();
633            return;
634        }
635
636        // Preserve elites - collect first to avoid borrow issues.
637        //
638        // EV-63: carry each elite over WITH its original CandidateId (and its
639        // accumulated evaluation history: fitness estimate, uncertainty, and
640        // evaluation_count) intact. The FitnessAggregator keys every candidate's
641        // ratings/wins/comparisons by CandidateId, so re-minting a fresh id here
642        // (the previous behavior) orphaned all of an elite's feedback every
643        // generation and violated the documented stable-ID contract. Elites stay
644        // at the front of the population, matching the `elitism_count..` reset of
645        // `unevaluated_indices` below.
646        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        // Fill rest with offspring
660        while new_population.len() < pop_size {
661            // Selection - returns index into evaluated pool
662            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            // Crossover
669            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            // Mutation (in-place)
679            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        // Update session
705        self.session.replace_population(new_population);
706        self.session.advance_generation();
707
708        // Reset evaluation tracking for new generation
709        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    /// Manually terminate the algorithm
718    pub fn terminate(&mut self, reason: &str) {
719        self.state = AlgorithmState::Terminated {
720            reason: reason.to_string(),
721        };
722    }
723}
724
725/// Builder for InteractiveGA
726pub 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    /// Create a new builder with default configuration
743    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    /// Set the population size
769    pub fn population_size(mut self, size: usize) -> Self {
770        self.config.population_size = size;
771        self
772    }
773
774    /// Set the elitism count
775    pub fn elitism_count(mut self, count: usize) -> Self {
776        self.config.elitism_count = count;
777        self
778    }
779
780    /// Set the crossover probability
781    pub fn crossover_probability(mut self, prob: f64) -> Self {
782        self.config.crossover_probability = prob;
783        self
784    }
785
786    /// Set the mutation probability
787    pub fn mutation_probability(mut self, prob: f64) -> Self {
788        self.config.mutation_probability = prob;
789        self
790    }
791
792    /// Set the evaluation mode
793    pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
794        self.config.evaluation_mode = mode;
795        self
796    }
797
798    /// Set the batch size
799    pub fn batch_size(mut self, size: usize) -> Self {
800        self.config.batch_size = size;
801        self
802    }
803
804    /// Set the select count for batch selection mode
805    pub fn select_count(mut self, count: usize) -> Self {
806        self.config.select_count = count;
807        self
808    }
809
810    /// Set the minimum coverage threshold
811    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    /// Set comparisons per candidate for pairwise mode
817    pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
818        self.config.comparisons_per_candidate = count;
819        self
820    }
821
822    /// Set maximum generations (0 = unlimited)
823    pub fn max_generations(mut self, max: usize) -> Self {
824        self.config.max_generations = max;
825        self
826    }
827
828    /// Set the aggregation model
829    pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
830        self.config.aggregation_model = model;
831        self
832    }
833
834    /// Set the active learning selection strategy
835    ///
836    /// # Example
837    ///
838    /// ```rust,ignore
839    /// .selection_strategy(SelectionStrategy::UncertaintySampling {
840    ///     uncertainty_weight: 1.0,
841    /// })
842    /// ```
843    pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
844        self.config.selection_strategy = strategy;
845        self
846    }
847
848    /// Set the search space bounds
849    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
850        self.bounds = Some(bounds);
851        self
852    }
853
854    /// Set the selection operator
855    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    /// Set the crossover operator
870    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    /// Set the mutation operator
885    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    /// Build the InteractiveGA
908    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        // Get first request
995        let result = iga.step(&mut rng);
996        let request = match result {
997            StepResult::NeedsEvaluation(r) => r,
998            _ => panic!("Expected NeedsEvaluation"),
999        };
1000
1001        // Provide ratings
1002        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        // Should be ready for evolution
1011        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        // Get request
1087        let _ = iga.step(&mut rng);
1088
1089        // Skip it
1090        iga.provide_response(EvaluationResponse::skip());
1091
1092        assert_eq!(iga.session().skipped, 1);
1093        assert_eq!(iga.session().responses_received, 0);
1094    }
1095}