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,no_run
136/// use fugue_evo::interactive::prelude::*;
137/// use fugue_evo::prelude::*;
138/// # fn get_user_feedback(_request: &EvaluationRequest<RealVector>) -> EvaluationResponse {
139/// #     unimplemented!("collect a person's feedback")
140/// # }
141/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
142///
143/// let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
144///     .population_size(12)
145///     .evaluation_mode(EvaluationMode::BatchSelection)
146///     .bounds(MultiBounds::symmetric(1.0, 4))
147///     .selection(TournamentSelection::new(2))
148///     .crossover(SbxCrossover::new(15.0))
149///     .mutation(PolynomialMutation::new(20.0))
150///     .build()?;
151///
152/// let mut rng = rand::thread_rng();
153///
154/// loop {
155///     match iga.step(&mut rng) {
156///         StepResult::NeedsEvaluation(request) => {
157///             let response = get_user_feedback(&request);
158///             iga.provide_response(response);
159///         }
160///         StepResult::GenerationComplete { generation, .. } => {
161///             println!("Generation {} complete", generation);
162///         }
163///         StepResult::Complete(result) => {
164///             println!("Evolution complete: {}", result.termination_reason);
165///             break;
166///         }
167///     }
168/// }
169/// # Ok(())
170/// # }
171/// ```
172pub struct InteractiveGA<G, S, C, M>
173where
174    G: EvolutionaryGenome,
175{
176    config: InteractiveGAConfig,
177    bounds: Option<MultiBounds>,
178    selection: S,
179    crossover: C,
180    mutation: M,
181    session: InteractiveSession<G>,
182    state: AlgorithmState,
183    /// Indices of candidates still needing evaluation this generation
184    unevaluated_indices: Vec<usize>,
185    /// Index for pairwise comparison scheduling
186    comparison_index: usize,
187}
188
189impl<G, S, C, M> InteractiveGA<G, S, C, M>
190where
191    G: EvolutionaryGenome + Clone + Send + Sync,
192    S: SelectionOperator<G>,
193    C: CrossoverOperator<G>,
194    M: MutationOperator<G>,
195{
196    /// Create a new InteractiveGA
197    pub fn new(
198        config: InteractiveGAConfig,
199        bounds: Option<MultiBounds>,
200        selection: S,
201        crossover: C,
202        mutation: M,
203    ) -> Self {
204        let aggregator = FitnessAggregator::new(config.aggregation_model.clone());
205        Self {
206            config,
207            bounds,
208            selection,
209            crossover,
210            mutation,
211            session: InteractiveSession::new(aggregator),
212            state: AlgorithmState::Initializing,
213            unevaluated_indices: Vec::new(),
214            comparison_index: 0,
215        }
216    }
217
218    /// Resume from a saved session
219    pub fn from_session(
220        session: InteractiveSession<G>,
221        config: InteractiveGAConfig,
222        bounds: Option<MultiBounds>,
223        selection: S,
224        crossover: C,
225        mutation: M,
226    ) -> Self {
227        let unevaluated: Vec<usize> = session
228            .population
229            .iter()
230            .enumerate()
231            .filter(|(_, c)| !c.is_evaluated())
232            .map(|(i, _)| i)
233            .collect();
234
235        let state = if session.population.is_empty() {
236            AlgorithmState::Initializing
237        } else if unevaluated.is_empty() {
238            AlgorithmState::ReadyForEvolution
239        } else {
240            AlgorithmState::AwaitingEvaluation {
241                pending_request_ids: Vec::new(),
242            }
243        };
244
245        Self {
246            config,
247            bounds,
248            selection,
249            crossover,
250            mutation,
251            session,
252            state,
253            unevaluated_indices: unevaluated,
254            comparison_index: 0,
255        }
256    }
257
258    /// Get the current session
259    pub fn session(&self) -> &InteractiveSession<G> {
260        &self.session
261    }
262
263    /// Get mutable reference to session (for custom modifications)
264    pub fn session_mut(&mut self) -> &mut InteractiveSession<G> {
265        &mut self.session
266    }
267
268    /// Get the configuration
269    pub fn config(&self) -> &InteractiveGAConfig {
270        &self.config
271    }
272
273    /// Get coverage statistics
274    pub fn coverage_stats(&self) -> CoverageStats {
275        self.session.coverage_stats()
276    }
277
278    /// Check if algorithm should terminate
279    fn should_terminate(&self) -> Option<String> {
280        if self.config.max_generations > 0 && self.session.generation >= self.config.max_generations
281        {
282            return Some(format!(
283                "Reached maximum generations ({})",
284                self.config.max_generations
285            ));
286        }
287        None
288    }
289
290    /// Initialize the population
291    fn initialize_population<R: Rng>(&mut self, rng: &mut R) {
292        // Need bounds for genome generation
293        let bounds = self
294            .bounds
295            .clone()
296            .unwrap_or_else(|| MultiBounds::symmetric(1.0, 1));
297
298        for _ in 0..self.config.population_size {
299            let genome = G::generate(rng, &bounds);
300            self.session.add_candidate(genome);
301        }
302
303        self.unevaluated_indices = (0..self.config.population_size).collect();
304        self.comparison_index = 0;
305    }
306
307    /// Create an evaluation request based on the current mode
308    fn create_evaluation_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
309        match self.config.evaluation_mode {
310            EvaluationMode::Rating => self.create_rating_request(rng),
311            EvaluationMode::Pairwise => self.create_pairwise_request(rng),
312            EvaluationMode::BatchSelection => self.create_batch_request(rng),
313            EvaluationMode::Adaptive => self.create_adaptive_request(rng),
314        }
315    }
316
317    fn create_rating_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
318        let batch_size = self.config.batch_size.min(self.session.population.len());
319        if batch_size == 0 {
320            return None;
321        }
322
323        // Use selection strategy to pick candidates
324        let selected_indices = self.config.selection_strategy.select_batch(
325            &self.session.population,
326            &self.session.aggregator,
327            batch_size,
328            rng,
329        );
330
331        if selected_indices.is_empty() {
332            return None;
333        }
334
335        let candidates: Vec<Candidate<G>> = selected_indices
336            .iter()
337            .filter_map(|&i| self.session.population.get(i).cloned())
338            .collect();
339
340        let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
341        self.state = AlgorithmState::AwaitingEvaluation {
342            pending_request_ids: ids,
343        };
344
345        Some(EvaluationRequest::rate(candidates))
346    }
347
348    fn create_pairwise_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
349        let pop_size = self.session.population.len();
350        if pop_size < 2 {
351            return None;
352        }
353
354        // Use selection strategy for intelligent pair selection
355        let pair = self.config.selection_strategy.select_pair(
356            &self.session.population,
357            &self.session.aggregator,
358            rng,
359        );
360
361        let (idx_a, idx_b) = match pair {
362            Some(p) => p,
363            None => {
364                // Fallback to round-robin if strategy returns None
365                let idx_a = self.comparison_index % pop_size;
366                let idx_b = (self.comparison_index + 1) % pop_size;
367                (idx_a, idx_b)
368            }
369        };
370
371        self.comparison_index += 1;
372
373        let candidate_a = self.session.population.get(idx_a)?.clone();
374        let candidate_b = self.session.population.get(idx_b)?.clone();
375
376        let ids = vec![candidate_a.id, candidate_b.id];
377        self.state = AlgorithmState::AwaitingEvaluation {
378            pending_request_ids: ids,
379        };
380
381        Some(EvaluationRequest::compare(candidate_a, candidate_b))
382    }
383
384    fn create_batch_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
385        let batch_size = self.config.batch_size.min(self.session.population.len());
386        if batch_size < 2 {
387            // Need at least 2 for selection
388            return self.create_rating_request(rng); // Fall back
389        }
390
391        // Use selection strategy to pick candidates
392        let selected_indices = self.config.selection_strategy.select_batch(
393            &self.session.population,
394            &self.session.aggregator,
395            batch_size,
396            rng,
397        );
398
399        if selected_indices.len() < 2 {
400            return self.create_rating_request(rng);
401        }
402
403        let candidates: Vec<Candidate<G>> = selected_indices
404            .iter()
405            .filter_map(|&i| self.session.population.get(i).cloned())
406            .collect();
407
408        let ids: Vec<CandidateId> = candidates.iter().map(|c| c.id).collect();
409        self.state = AlgorithmState::AwaitingEvaluation {
410            pending_request_ids: ids,
411        };
412
413        let select_count = self.config.select_count.min(candidates.len() - 1);
414        Some(EvaluationRequest::select_from_batch(
415            candidates,
416            select_count,
417        ))
418    }
419
420    fn create_adaptive_request<R: Rng>(&mut self, rng: &mut R) -> Option<EvaluationRequest<G>> {
421        // Simple adaptive strategy: use rating for initial coverage,
422        // then switch to pairwise for refinement
423        let coverage = self.session.coverage_stats().coverage;
424        if coverage < 0.5 {
425            self.create_rating_request(rng)
426        } else {
427            self.create_pairwise_request(rng)
428        }
429    }
430
431    /// Provide user response to an evaluation request
432    pub fn provide_response(&mut self, response: EvaluationResponse) {
433        let was_skipped = response.is_skip();
434        self.session.record_response(was_skipped);
435
436        if was_skipped {
437            // Put unevaluated candidates back if skipped
438            if let AlgorithmState::AwaitingEvaluation {
439                pending_request_ids,
440            } = &self.state
441            {
442                for id in pending_request_ids {
443                    if let Some(pos) = self.session.population.iter().position(|c| c.id == *id) {
444                        if !self.unevaluated_indices.contains(&pos) {
445                            self.unevaluated_indices.push(pos);
446                        }
447                    }
448                }
449            }
450            self.state = AlgorithmState::AwaitingEvaluation {
451                pending_request_ids: Vec::new(),
452            };
453            return;
454        }
455
456        // Process the response
457        let updated = match &response {
458            EvaluationResponse::Ratings(ratings) => {
459                self.session.aggregator.process_response(&response);
460                ratings.iter().map(|(id, _)| *id).collect::<Vec<_>>()
461            }
462            EvaluationResponse::PairwiseWinner(winner) => {
463                // Capture BOTH compared ids returned by process_pairwise so the
464                // winner AND the loser have their fitness re-synced from the
465                // (now Bradley-Terry-refit, EV-06) aggregator, not just the winner.
466                let mut ids = Vec::new();
467                if let AlgorithmState::AwaitingEvaluation {
468                    pending_request_ids,
469                } = &self.state
470                {
471                    if pending_request_ids.len() == 2 {
472                        let id_a = pending_request_ids[0];
473                        let id_b = pending_request_ids[1];
474                        ids = self
475                            .session
476                            .aggregator
477                            .process_pairwise(id_a, id_b, *winner)
478                            .into_iter()
479                            .map(|(id, _)| id)
480                            .collect();
481                    }
482                }
483                ids
484            }
485            EvaluationResponse::BatchSelected(selected) => {
486                if let AlgorithmState::AwaitingEvaluation {
487                    pending_request_ids,
488                } = &self.state
489                {
490                    self.session
491                        .aggregator
492                        .process_batch_selection(pending_request_ids, selected);
493                }
494                selected.clone()
495            }
496            EvaluationResponse::Skip => Vec::new(),
497        };
498
499        // Update candidate fitness estimates with uncertainty. These are pure
500        // setters (they do NOT touch evaluation_count — see EV-26 / EV-64).
501        for id in updated {
502            if let Some(estimate) = self.session.aggregator.get_fitness_estimate(&id) {
503                self.session.update_fitness_with_uncertainty(id, estimate);
504            } else if let Some(fitness) = self.session.aggregator.get_fitness(&id) {
505                // Fallback to point estimate only
506                self.session.update_fitness(id, fitness);
507            }
508        }
509
510        // Single owner of evaluation counting (EV-26 / EV-64): every candidate
511        // that was actually presented in this request is counted exactly once,
512        // regardless of whether the aggregator produced an estimate for it. This
513        // is symmetric in pairwise mode (both compared candidates get +1).
514        if let AlgorithmState::AwaitingEvaluation {
515            pending_request_ids,
516        } = &self.state
517        {
518            for id in pending_request_ids {
519                if let Some(candidate) = self.session.get_candidate_mut(*id) {
520                    candidate.record_evaluation();
521                }
522            }
523        }
524
525        // Transition state
526        self.state = AlgorithmState::AwaitingEvaluation {
527            pending_request_ids: Vec::new(),
528        };
529    }
530
531    /// Advance the algorithm one step
532    pub fn step<R: Rng>(&mut self, rng: &mut R) -> StepResult<G>
533    where
534        G: Serialize + for<'de> Deserialize<'de>,
535    {
536        loop {
537            match &self.state {
538                AlgorithmState::Initializing => {
539                    self.initialize_population(rng);
540                    self.state = AlgorithmState::AwaitingEvaluation {
541                        pending_request_ids: Vec::new(),
542                    };
543                }
544
545                AlgorithmState::AwaitingEvaluation {
546                    pending_request_ids,
547                } => {
548                    // If we have a pending request, wait for response
549                    if !pending_request_ids.is_empty() {
550                        // This shouldn't happen in normal flow, but handle it
551                        continue;
552                    }
553
554                    // Check if we have enough coverage
555                    let coverage = self.session.coverage_stats();
556
557                    // For pairwise mode, check comparison count instead
558                    let enough_coverage = match self.config.evaluation_mode {
559                        EvaluationMode::Pairwise => {
560                            let target =
561                                self.config.population_size * self.config.comparisons_per_candidate;
562                            self.comparison_index >= target
563                        }
564                        _ => coverage.coverage >= self.config.min_coverage,
565                    };
566
567                    if enough_coverage {
568                        self.state = AlgorithmState::ReadyForEvolution;
569                        continue;
570                    }
571
572                    // Create next evaluation request
573                    if let Some(request) = self.create_evaluation_request(rng) {
574                        self.session.record_request(&request);
575                        return StepResult::NeedsEvaluation(request);
576                    } else {
577                        // No more candidates to evaluate
578                        self.state = AlgorithmState::ReadyForEvolution;
579                    }
580                }
581
582                AlgorithmState::ReadyForEvolution => {
583                    // Check termination
584                    if let Some(reason) = self.should_terminate() {
585                        self.state = AlgorithmState::Terminated {
586                            reason: reason.clone(),
587                        };
588                        continue;
589                    }
590
591                    let generation = self.session.generation;
592                    let best_fitness = self.session.best_candidate().and_then(|c| c.fitness());
593                    let coverage = self.session.coverage_stats().coverage;
594
595                    // Perform evolution
596                    self.evolve_generation(rng);
597
598                    return StepResult::GenerationComplete {
599                        generation,
600                        best_fitness,
601                        coverage,
602                    };
603                }
604
605                AlgorithmState::Terminated { reason } => {
606                    let best_candidates = self
607                        .session
608                        .ranked_candidates()
609                        .into_iter()
610                        .take(self.config.elitism_count.max(3))
611                        .cloned()
612                        .collect();
613
614                    return StepResult::Complete(Box::new(InteractiveResult {
615                        best_candidates,
616                        generations: self.session.generation,
617                        total_evaluations: self.session.evaluations_requested,
618                        session: self.session.clone(),
619                        termination_reason: reason.clone(),
620                    }));
621                }
622            }
623        }
624    }
625
626    /// Perform selection and create next generation
627    fn evolve_generation<R: Rng>(&mut self, rng: &mut R)
628    where
629        G: Serialize + for<'de> Deserialize<'de>,
630    {
631        let pop_size = self.config.population_size;
632
633        // Get current population with fitness (genome, fitness) pairs
634        let evaluated: Vec<(G, f64)> = self
635            .session
636            .population
637            .iter()
638            .filter_map(|c| c.fitness_estimate.map(|f| (c.genome.clone(), f)))
639            .collect();
640
641        if evaluated.is_empty() {
642            // No evaluated individuals, can't evolve
643            self.session.advance_generation();
644            return;
645        }
646
647        // Preserve elites - collect first to avoid borrow issues.
648        //
649        // EV-63: carry each elite over WITH its original CandidateId (and its
650        // accumulated evaluation history: fitness estimate, uncertainty, and
651        // evaluation_count) intact. The FitnessAggregator keys every candidate's
652        // ratings/wins/comparisons by CandidateId, so re-minting a fresh id here
653        // (the previous behavior) orphaned all of an elite's feedback every
654        // generation and violated the documented stable-ID contract. Elites stay
655        // at the front of the population, matching the `elitism_count..` reset of
656        // `unevaluated_indices` below.
657        let mut new_population: Vec<Candidate<G>> = Vec::with_capacity(pop_size);
658        let elites: Vec<Candidate<G>> = self
659            .session
660            .ranked_candidates()
661            .into_iter()
662            .take(self.config.elitism_count)
663            .cloned()
664            .collect();
665
666        for elite in elites {
667            new_population.push(elite);
668        }
669
670        // Fill rest with offspring
671        while new_population.len() < pop_size {
672            // Selection - returns index into evaluated pool
673            let parent1_idx = self.selection.select(&evaluated, rng);
674            let parent2_idx = self.selection.select(&evaluated, rng);
675
676            let parent1 = &evaluated[parent1_idx].0;
677            let parent2 = &evaluated[parent2_idx].0;
678
679            // Crossover
680            let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
681                match self.crossover.crossover(parent1, parent2, rng).genome() {
682                    Some((c1, c2)) => (c1, c2),
683                    None => (parent1.clone(), parent2.clone()),
684                }
685            } else {
686                (parent1.clone(), parent2.clone())
687            };
688
689            // Mutation (in-place)
690            if rng.gen::<f64>() < self.config.mutation_probability {
691                self.mutation.mutate(&mut child1, rng);
692            }
693
694            let id = self.session.next_id();
695            new_population.push(Candidate::with_generation(
696                id,
697                child1,
698                self.session.generation + 1,
699            ));
700
701            if new_population.len() < pop_size {
702                if rng.gen::<f64>() < self.config.mutation_probability {
703                    self.mutation.mutate(&mut child2, rng);
704                }
705
706                let id = self.session.next_id();
707                new_population.push(Candidate::with_generation(
708                    id,
709                    child2,
710                    self.session.generation + 1,
711                ));
712            }
713        }
714
715        // Update session
716        self.session.replace_population(new_population);
717        self.session.advance_generation();
718
719        // Reset evaluation tracking for new generation
720        self.unevaluated_indices =
721            (self.config.elitism_count..self.config.population_size).collect();
722        self.comparison_index = 0;
723        self.state = AlgorithmState::AwaitingEvaluation {
724            pending_request_ids: Vec::new(),
725        };
726    }
727
728    /// Manually terminate the algorithm
729    pub fn terminate(&mut self, reason: &str) {
730        self.state = AlgorithmState::Terminated {
731            reason: reason.to_string(),
732        };
733    }
734}
735
736/// Builder for InteractiveGA
737pub struct InteractiveGABuilder<G, S, C, M>
738where
739    G: EvolutionaryGenome,
740{
741    config: InteractiveGAConfig,
742    bounds: Option<MultiBounds>,
743    selection: Option<S>,
744    crossover: Option<C>,
745    mutation: Option<M>,
746    _phantom: PhantomData<G>,
747}
748
749impl<G> InteractiveGABuilder<G, (), (), ()>
750where
751    G: EvolutionaryGenome,
752{
753    /// Create a new builder with default configuration
754    pub fn new() -> Self {
755        Self {
756            config: InteractiveGAConfig::default(),
757            bounds: None,
758            selection: None,
759            crossover: None,
760            mutation: None,
761            _phantom: PhantomData,
762        }
763    }
764}
765
766impl<G> Default for InteractiveGABuilder<G, (), (), ()>
767where
768    G: EvolutionaryGenome,
769{
770    fn default() -> Self {
771        Self::new()
772    }
773}
774
775impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
776where
777    G: EvolutionaryGenome,
778{
779    /// Set the population size
780    pub fn population_size(mut self, size: usize) -> Self {
781        self.config.population_size = size;
782        self
783    }
784
785    /// Set the elitism count
786    pub fn elitism_count(mut self, count: usize) -> Self {
787        self.config.elitism_count = count;
788        self
789    }
790
791    /// Set the crossover probability
792    pub fn crossover_probability(mut self, prob: f64) -> Self {
793        self.config.crossover_probability = prob;
794        self
795    }
796
797    /// Set the mutation probability
798    pub fn mutation_probability(mut self, prob: f64) -> Self {
799        self.config.mutation_probability = prob;
800        self
801    }
802
803    /// Set the evaluation mode
804    pub fn evaluation_mode(mut self, mode: EvaluationMode) -> Self {
805        self.config.evaluation_mode = mode;
806        self
807    }
808
809    /// Set the batch size
810    pub fn batch_size(mut self, size: usize) -> Self {
811        self.config.batch_size = size;
812        self
813    }
814
815    /// Set the select count for batch selection mode
816    pub fn select_count(mut self, count: usize) -> Self {
817        self.config.select_count = count;
818        self
819    }
820
821    /// Set the minimum coverage threshold
822    pub fn min_coverage(mut self, coverage: f64) -> Self {
823        self.config.min_coverage = coverage.clamp(0.0, 1.0);
824        self
825    }
826
827    /// Set comparisons per candidate for pairwise mode
828    pub fn comparisons_per_candidate(mut self, count: usize) -> Self {
829        self.config.comparisons_per_candidate = count;
830        self
831    }
832
833    /// Set maximum generations (0 = unlimited)
834    pub fn max_generations(mut self, max: usize) -> Self {
835        self.config.max_generations = max;
836        self
837    }
838
839    /// Set the aggregation model
840    pub fn aggregation_model(mut self, model: AggregationModel) -> Self {
841        self.config.aggregation_model = model;
842        self
843    }
844
845    /// Set the active learning selection strategy
846    ///
847    /// # Example
848    ///
849    /// ```rust
850    /// # use fugue_evo::interactive::prelude::*;
851    /// # use fugue_evo::prelude::RealVector;
852    /// let builder = InteractiveGABuilder::<RealVector, (), (), ()>::new()
853    ///     .selection_strategy(SelectionStrategy::UncertaintySampling {
854    ///         uncertainty_weight: 1.0,
855    ///     });
856    /// ```
857    pub fn selection_strategy(mut self, strategy: SelectionStrategy) -> Self {
858        self.config.selection_strategy = strategy;
859        self
860    }
861
862    /// Set the search space bounds
863    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
864        self.bounds = Some(bounds);
865        self
866    }
867
868    /// Set the selection operator
869    pub fn selection<NewS>(self, selection: NewS) -> InteractiveGABuilder<G, NewS, C, M>
870    where
871        NewS: SelectionOperator<G>,
872    {
873        InteractiveGABuilder {
874            config: self.config,
875            bounds: self.bounds,
876            selection: Some(selection),
877            crossover: self.crossover,
878            mutation: self.mutation,
879            _phantom: PhantomData,
880        }
881    }
882
883    /// Set the crossover operator
884    pub fn crossover<NewC>(self, crossover: NewC) -> InteractiveGABuilder<G, S, NewC, M>
885    where
886        NewC: CrossoverOperator<G>,
887    {
888        InteractiveGABuilder {
889            config: self.config,
890            bounds: self.bounds,
891            selection: self.selection,
892            crossover: Some(crossover),
893            mutation: self.mutation,
894            _phantom: PhantomData,
895        }
896    }
897
898    /// Set the mutation operator
899    pub fn mutation<NewM>(self, mutation: NewM) -> InteractiveGABuilder<G, S, C, NewM>
900    where
901        NewM: MutationOperator<G>,
902    {
903        InteractiveGABuilder {
904            config: self.config,
905            bounds: self.bounds,
906            selection: self.selection,
907            crossover: self.crossover,
908            mutation: Some(mutation),
909            _phantom: PhantomData,
910        }
911    }
912}
913
914impl<G, S, C, M> InteractiveGABuilder<G, S, C, M>
915where
916    G: EvolutionaryGenome + Clone + Send + Sync,
917    S: SelectionOperator<G>,
918    C: CrossoverOperator<G>,
919    M: MutationOperator<G>,
920{
921    /// Build the InteractiveGA
922    pub fn build(self) -> Result<InteractiveGA<G, S, C, M>, EvolutionError> {
923        let selection = self
924            .selection
925            .ok_or_else(|| EvolutionError::Configuration("Selection operator required".into()))?;
926        let crossover = self
927            .crossover
928            .ok_or_else(|| EvolutionError::Configuration("Crossover operator required".into()))?;
929        let mutation = self
930            .mutation
931            .ok_or_else(|| EvolutionError::Configuration("Mutation operator required".into()))?;
932
933        Ok(InteractiveGA::new(
934            self.config,
935            self.bounds,
936            selection,
937            crossover,
938            mutation,
939        ))
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946    use crate::genome::real_vector::RealVector;
947    use crate::operators::crossover::SbxCrossover;
948    use crate::operators::mutation::PolynomialMutation;
949    use crate::operators::selection::TournamentSelection;
950    use rand::SeedableRng;
951
952    #[test]
953    fn test_interactive_ga_builder() {
954        let result = InteractiveGABuilder::<RealVector, (), (), ()>::new()
955            .population_size(10)
956            .evaluation_mode(EvaluationMode::Rating)
957            .selection(TournamentSelection::new(2))
958            .crossover(SbxCrossover::new(15.0))
959            .mutation(PolynomialMutation::new(20.0))
960            .build();
961
962        assert!(result.is_ok());
963        let iga = result.unwrap();
964        assert_eq!(iga.config().population_size, 10);
965    }
966
967    #[test]
968    fn test_interactive_ga_initialization() {
969        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
970
971        let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
972            .population_size(5)
973            .evaluation_mode(EvaluationMode::Rating)
974            .batch_size(2)
975            .selection(TournamentSelection::new(2))
976            .crossover(SbxCrossover::new(15.0))
977            .mutation(PolynomialMutation::new(20.0))
978            .build()
979            .unwrap();
980
981        let result = iga.step(&mut rng);
982
983        match result {
984            StepResult::NeedsEvaluation(request) => {
985                assert!(request.candidate_count() <= 2);
986            }
987            _ => panic!("Expected NeedsEvaluation"),
988        }
989
990        assert_eq!(iga.session().population.len(), 5);
991    }
992
993    #[test]
994    fn test_provide_response() {
995        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
996
997        let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
998            .population_size(4)
999            .evaluation_mode(EvaluationMode::Rating)
1000            .batch_size(4)
1001            .min_coverage(1.0)
1002            .selection(TournamentSelection::new(2))
1003            .crossover(SbxCrossover::new(15.0))
1004            .mutation(PolynomialMutation::new(20.0))
1005            .build()
1006            .unwrap();
1007
1008        // Get first request
1009        let result = iga.step(&mut rng);
1010        let request = match result {
1011            StepResult::NeedsEvaluation(r) => r,
1012            _ => panic!("Expected NeedsEvaluation"),
1013        };
1014
1015        // Provide ratings
1016        let ids = request.candidate_ids();
1017        let ratings: Vec<_> = ids
1018            .into_iter()
1019            .enumerate()
1020            .map(|(i, id)| (id, (i + 1) as f64 * 2.0))
1021            .collect();
1022        iga.provide_response(EvaluationResponse::ratings(ratings));
1023
1024        // Should be ready for evolution
1025        let result = iga.step(&mut rng);
1026        match result {
1027            StepResult::GenerationComplete { generation, .. } => {
1028                assert_eq!(generation, 0);
1029            }
1030            _ => panic!("Expected GenerationComplete"),
1031        }
1032    }
1033
1034    #[test]
1035    fn test_pairwise_mode() {
1036        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1037
1038        let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1039            .population_size(4)
1040            .evaluation_mode(EvaluationMode::Pairwise)
1041            .comparisons_per_candidate(2)
1042            .selection(TournamentSelection::new(2))
1043            .crossover(SbxCrossover::new(15.0))
1044            .mutation(PolynomialMutation::new(20.0))
1045            .build()
1046            .unwrap();
1047
1048        let result = iga.step(&mut rng);
1049
1050        match result {
1051            StepResult::NeedsEvaluation(EvaluationRequest::PairwiseComparison { .. }) => {}
1052            _ => panic!("Expected PairwiseComparison request"),
1053        }
1054    }
1055
1056    #[test]
1057    fn test_batch_selection_mode() {
1058        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1059
1060        let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1061            .population_size(6)
1062            .evaluation_mode(EvaluationMode::BatchSelection)
1063            .batch_size(4)
1064            .select_count(2)
1065            .selection(TournamentSelection::new(2))
1066            .crossover(SbxCrossover::new(15.0))
1067            .mutation(PolynomialMutation::new(20.0))
1068            .build()
1069            .unwrap();
1070
1071        let result = iga.step(&mut rng);
1072
1073        match result {
1074            StepResult::NeedsEvaluation(EvaluationRequest::BatchSelection {
1075                candidates,
1076                select_count,
1077                ..
1078            }) => {
1079                assert_eq!(candidates.len(), 4);
1080                assert_eq!(select_count, 2);
1081            }
1082            _ => panic!("Expected BatchSelection request"),
1083        }
1084    }
1085
1086    #[test]
1087    fn test_skip_response() {
1088        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1089
1090        let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
1091            .population_size(4)
1092            .evaluation_mode(EvaluationMode::Rating)
1093            .batch_size(2)
1094            .selection(TournamentSelection::new(2))
1095            .crossover(SbxCrossover::new(15.0))
1096            .mutation(PolynomialMutation::new(20.0))
1097            .build()
1098            .unwrap();
1099
1100        // Get request
1101        let _ = iga.step(&mut rng);
1102
1103        // Skip it
1104        iga.provide_response(EvaluationResponse::skip());
1105
1106        assert_eq!(iga.session().skipped, 1);
1107        assert_eq!(iga.session().responses_received, 0);
1108    }
1109}