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                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        // Update candidate fitness estimates with uncertainty
481        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                // Fallback to point estimate only
486                self.session.update_fitness(id, fitness);
487            }
488        }
489
490        // Mark candidates as evaluated based on pending request
491        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        // Transition state
503        self.state = AlgorithmState::AwaitingEvaluation {
504            pending_request_ids: Vec::new(),
505        };
506    }
507
508    /// Advance the algorithm one step
509    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 we have a pending request, wait for response
526                    if !pending_request_ids.is_empty() {
527                        // This shouldn't happen in normal flow, but handle it
528                        continue;
529                    }
530
531                    // Check if we have enough coverage
532                    let coverage = self.session.coverage_stats();
533
534                    // For pairwise mode, check comparison count instead
535                    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                    // Create next evaluation request
550                    if let Some(request) = self.create_evaluation_request(rng) {
551                        self.session.record_request(&request);
552                        return StepResult::NeedsEvaluation(request);
553                    } else {
554                        // No more candidates to evaluate
555                        self.state = AlgorithmState::ReadyForEvolution;
556                    }
557                }
558
559                AlgorithmState::ReadyForEvolution => {
560                    // Check termination
561                    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                    // Perform evolution
573                    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    /// Perform selection and create next generation
604    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        // Get current population with fitness (genome, fitness) pairs
611        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            // No evaluated individuals, can't evolve
620            self.session.advance_generation();
621            return;
622        }
623
624        // Preserve elites - collect first to avoid borrow issues
625        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            // Preserve elite fitness
639            candidate.fitness_estimate = fitness;
640            new_population.push(candidate);
641        }
642
643        // Fill rest with offspring
644        while new_population.len() < pop_size {
645            // Selection - returns index into evaluated pool
646            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            // Crossover
653            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            // Mutation (in-place)
663            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        // Update session
689        self.session.replace_population(new_population);
690        self.session.advance_generation();
691
692        // Reset evaluation tracking for new generation
693        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    /// Manually terminate the algorithm
702    pub fn terminate(&mut self, reason: &str) {
703        self.state = AlgorithmState::Terminated {
704            reason: reason.to_string(),
705        };
706    }
707}
708
709/// Builder for InteractiveGA
710pub 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    /// Create a new builder with default configuration
727    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    /// Set the population size
753    pub fn population_size(mut self, size: usize) -> Self {
754        self.config.population_size = size;
755        self
756    }
757
758    /// Set the elitism count
759    pub fn elitism_count(mut self, count: usize) -> Self {
760        self.config.elitism_count = count;
761        self
762    }
763
764    /// Set the crossover probability
765    pub fn crossover_probability(mut self, prob: f64) -> Self {
766        self.config.crossover_probability = prob;
767        self
768    }
769
770    /// Set the mutation probability
771    pub fn mutation_probability(mut self, prob: f64) -> Self {
772        self.config.mutation_probability = prob;
773        self
774    }
775
776    /// Set the evaluation mode
777    pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
778        self.config.evaluation_mode = mode;
779        self
780    }
781
782    /// Set the batch size
783    pub fn batch_size(mut self, size: usize) -> Self {
784        self.config.batch_size = size;
785        self
786    }
787
788    /// Set the select count for batch selection mode
789    pub fn select_count(mut self, count: usize) -> Self {
790        self.config.select_count = count;
791        self
792    }
793
794    /// Set the minimum coverage threshold
795    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    /// Set comparisons per candidate for pairwise mode
801    pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
802        self.config.comparisons_per_candidate = count;
803        self
804    }
805
806    /// Set maximum generations (0 = unlimited)
807    pub fn max_generations(mut self, max: usize) -> Self {
808        self.config.max_generations = max;
809        self
810    }
811
812    /// Set the aggregation model
813    pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
814        self.config.aggregation_model = model;
815        self
816    }
817
818    /// Set the active learning selection strategy
819    ///
820    /// # Example
821    ///
822    /// ```rust,ignore
823    /// .selection_strategy(SelectionStrategy::UncertaintySampling {
824    ///     uncertainty_weight: 1.0,
825    /// })
826    /// ```
827    pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
828        self.config.selection_strategy = strategy;
829        self
830    }
831
832    /// Set the search space bounds
833    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
834        self.bounds = Some(bounds);
835        self
836    }
837
838    /// Set the selection operator
839    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    /// Set the crossover operator
854    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    /// Set the mutation operator
869    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    /// Build the InteractiveGA
892    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        // Get first request
979        let result = iga.step(&mut rng);
980        let request = match result {
981            StepResult::NeedsEvaluation(r) => r,
982            _ => panic!("Expected NeedsEvaluation"),
983        };
984
985        // Provide ratings
986        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        // Should be ready for evolution
995        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        // Get request
1071        let _ = iga.step(&mut rng);
1072
1073        // Skip it
1074        iga.provide_response(EvaluationResponse::skip());
1075
1076        assert_eq!(iga.session().skipped, 1);
1077        assert_eq!(iga.session().responses_received, 0);
1078    }
1079}