Skip to main content

fugue_evo/interactive/
traits.rs

1//! Interactive fitness traits
2//!
3//! This module defines the `InteractiveFitness` trait for human-in-the-loop
4//! fitness evaluation, as well as supporting types for evaluation modes.
5
6use serde::{Deserialize, Serialize};
7
8use super::aggregation::FitnessAggregator;
9use super::evaluator::{Candidate, CandidateId, EvaluationRequest, EvaluationResponse};
10use crate::genome::traits::EvolutionaryGenome;
11
12/// Evaluation mode for interactive fitness
13///
14/// Determines how user feedback is collected during evolution.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum EvaluationMode {
17    /// User rates each candidate independently on a numeric scale
18    ///
19    /// Best for: Absolute quality assessment, when users can easily assign scores
20    Rating,
21
22    /// User compares pairs of candidates and selects the better one
23    ///
24    /// Best for: When relative comparisons are easier than absolute ratings,
25    /// provides consistent transitive preferences
26    Pairwise,
27
28    /// User selects top N favorites from a batch
29    ///
30    /// Best for: Quick evaluation of many candidates, implicit ranking
31    BatchSelection,
32
33    /// System chooses evaluation mode adaptively based on population state
34    ///
35    /// May switch between modes based on coverage, convergence, or user fatigue
36    Adaptive,
37}
38
39impl EvaluationMode {
40    /// Returns a human-readable description of this mode
41    pub fn description(&self) -> &'static str {
42        match self {
43            Self::Rating => "Rate each candidate on a numeric scale",
44            Self::Pairwise => "Compare pairs and select the better one",
45            Self::BatchSelection => "Select favorites from a batch",
46            Self::Adaptive => "System adapts evaluation method automatically",
47        }
48    }
49}
50
51impl Default for EvaluationMode {
52    fn default() -> Self {
53        Self::Rating
54    }
55}
56
57/// Trait for interactive fitness evaluation
58///
59/// Unlike the synchronous [`Fitness`](crate::fitness::traits::Fitness) trait that returns
60/// immediate values, `InteractiveFitness` generates evaluation requests that must
61/// be fulfilled by user interaction.
62///
63/// # Design
64///
65/// The trait is designed around a request/response pattern:
66/// 1. Algorithm calls `request_evaluation()` with candidates needing feedback
67/// 2. UI presents the request to the user and collects their response
68/// 3. Algorithm calls `process_response()` to update fitness estimates
69///
70/// # Example Implementation
71///
72/// ```rust
73/// use fugue_evo::interactive::prelude::*;
74/// use fugue_evo::prelude::RealVector;
75///
76/// struct ArtFitness {
77///     mode: EvaluationMode,
78/// }
79///
80/// impl InteractiveFitness for ArtFitness {
81///     type Genome = RealVector;
82///
83///     fn evaluation_mode(&self) -> EvaluationMode {
84///         self.mode
85///     }
86///
87///     fn request_evaluation(
88///         &self,
89///         candidates: &[Candidate<Self::Genome>],
90///     ) -> EvaluationRequest<Self::Genome> {
91///         match self.mode {
92///             EvaluationMode::Rating => {
93///                 EvaluationRequest::rate(candidates.to_vec())
94///             }
95///             EvaluationMode::BatchSelection => {
96///                 EvaluationRequest::select_from_batch(candidates.to_vec(), 3)
97///             }
98///             _ => unimplemented!()
99///         }
100///     }
101///
102///     fn process_response(
103///         &mut self,
104///         response: EvaluationResponse,
105///         aggregator: &mut FitnessAggregator,
106///     ) -> Vec<(CandidateId, f64)> {
107///         // Delegate to aggregator for standard processing
108///         aggregator.process_response(&response)
109///     }
110/// }
111/// ```
112pub trait InteractiveFitness: Send + Sync {
113    /// The genome type being evaluated
114    type Genome: EvolutionaryGenome;
115
116    /// Get the preferred evaluation mode for this fitness function
117    fn evaluation_mode(&self) -> EvaluationMode;
118
119    /// Generate an evaluation request for the given candidates
120    ///
121    /// The returned request will be presented to the user for feedback.
122    /// The implementation should select candidates appropriately for the
123    /// current evaluation mode.
124    fn request_evaluation(
125        &self,
126        candidates: &[Candidate<Self::Genome>],
127    ) -> EvaluationRequest<Self::Genome>;
128
129    /// Process user response and update fitness estimates
130    ///
131    /// Returns the updated fitness values for affected candidates.
132    /// The aggregator maintains cumulative statistics and should be
133    /// used for fitness computation.
134    fn process_response(
135        &mut self,
136        response: EvaluationResponse,
137        aggregator: &mut FitnessAggregator,
138    ) -> Vec<(CandidateId, f64)>;
139
140    /// Optional: Called at the start of each generation
141    ///
142    /// Allows the fitness function to adjust strategy based on
143    /// population state or user fatigue.
144    fn on_generation_start(&mut self, _generation: usize, _population_size: usize) {}
145
146    /// Optional: Called when an evaluation is skipped
147    ///
148    /// Allows tracking of user fatigue or disengagement.
149    fn on_evaluation_skipped(&mut self) {}
150}
151
152/// Default interactive fitness implementation using a fixed evaluation mode
153///
154/// This provides a simple implementation that delegates all processing
155/// to the fitness aggregator. Suitable for most use cases.
156#[derive(Clone, Debug)]
157pub struct DefaultInteractiveFitness<G>
158where
159    G: EvolutionaryGenome,
160{
161    mode: EvaluationMode,
162    batch_size: usize,
163    select_count: usize,
164    _marker: std::marker::PhantomData<G>,
165}
166
167impl<G> DefaultInteractiveFitness<G>
168where
169    G: EvolutionaryGenome,
170{
171    /// Create a new default interactive fitness with the given mode
172    pub fn new(mode: EvaluationMode) -> Self {
173        Self {
174            mode,
175            batch_size: 6,
176            select_count: 2,
177            _marker: std::marker::PhantomData,
178        }
179    }
180
181    /// Set the batch size for batch selection mode
182    pub fn with_batch_size(mut self, size: usize) -> Self {
183        self.batch_size = size;
184        self
185    }
186
187    /// Set how many candidates to select in batch selection mode
188    pub fn with_select_count(mut self, count: usize) -> Self {
189        self.select_count = count;
190        self
191    }
192}
193
194impl<G> Default for DefaultInteractiveFitness<G>
195where
196    G: EvolutionaryGenome,
197{
198    fn default() -> Self {
199        Self::new(EvaluationMode::Rating)
200    }
201}
202
203impl<G> InteractiveFitness for DefaultInteractiveFitness<G>
204where
205    G: EvolutionaryGenome + Clone + Send + Sync,
206{
207    type Genome = G;
208
209    fn evaluation_mode(&self) -> EvaluationMode {
210        self.mode
211    }
212
213    fn request_evaluation(
214        &self,
215        candidates: &[Candidate<Self::Genome>],
216    ) -> EvaluationRequest<Self::Genome> {
217        match self.mode {
218            EvaluationMode::Rating => EvaluationRequest::rate(candidates.to_vec()),
219            EvaluationMode::Pairwise => {
220                // Select two candidates for comparison
221                if candidates.len() >= 2 {
222                    EvaluationRequest::compare(candidates[0].clone(), candidates[1].clone())
223                } else if candidates.len() == 1 {
224                    // Fall back to rating if only one candidate
225                    EvaluationRequest::rate(candidates.to_vec())
226                } else {
227                    EvaluationRequest::rate(vec![])
228                }
229            }
230            EvaluationMode::BatchSelection => {
231                let batch: Vec<_> = candidates.iter().take(self.batch_size).cloned().collect();
232                EvaluationRequest::select_from_batch(batch, self.select_count)
233            }
234            EvaluationMode::Adaptive => {
235                // Default to rating for adaptive mode
236                EvaluationRequest::rate(candidates.to_vec())
237            }
238        }
239    }
240
241    fn process_response(
242        &mut self,
243        response: EvaluationResponse,
244        aggregator: &mut FitnessAggregator,
245    ) -> Vec<(CandidateId, f64)> {
246        aggregator.process_response(&response)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::genome::real_vector::RealVector;
254    use crate::interactive::aggregation::AggregationModel;
255
256    #[test]
257    fn test_evaluation_mode_default() {
258        assert_eq!(EvaluationMode::default(), EvaluationMode::Rating);
259    }
260
261    #[test]
262    fn test_evaluation_mode_description() {
263        assert!(!EvaluationMode::Rating.description().is_empty());
264        assert!(!EvaluationMode::Pairwise.description().is_empty());
265        assert!(!EvaluationMode::BatchSelection.description().is_empty());
266        assert!(!EvaluationMode::Adaptive.description().is_empty());
267    }
268
269    #[test]
270    fn test_default_interactive_fitness_rating() {
271        let fitness: DefaultInteractiveFitness<RealVector> =
272            DefaultInteractiveFitness::new(EvaluationMode::Rating);
273
274        let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
275        let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
276
277        let request = fitness.request_evaluation(&[c1, c2]);
278        match request {
279            EvaluationRequest::RateCandidates { candidates, .. } => {
280                assert_eq!(candidates.len(), 2);
281            }
282            _ => panic!("Expected RateCandidates request"),
283        }
284    }
285
286    #[test]
287    fn test_default_interactive_fitness_pairwise() {
288        let fitness: DefaultInteractiveFitness<RealVector> =
289            DefaultInteractiveFitness::new(EvaluationMode::Pairwise);
290
291        let c1 = Candidate::new(CandidateId(0), RealVector::new(vec![1.0]));
292        let c2 = Candidate::new(CandidateId(1), RealVector::new(vec![2.0]));
293
294        let request = fitness.request_evaluation(&[c1, c2]);
295        match request {
296            EvaluationRequest::PairwiseComparison { .. } => {}
297            _ => panic!("Expected PairwiseComparison request"),
298        }
299    }
300
301    #[test]
302    fn test_default_interactive_fitness_batch() {
303        let fitness: DefaultInteractiveFitness<RealVector> =
304            DefaultInteractiveFitness::new(EvaluationMode::BatchSelection)
305                .with_batch_size(4)
306                .with_select_count(2);
307
308        let candidates: Vec<_> = (0..6)
309            .map(|i| Candidate::new(CandidateId(i), RealVector::new(vec![i as f64])))
310            .collect();
311
312        let request = fitness.request_evaluation(&candidates);
313        match request {
314            EvaluationRequest::BatchSelection {
315                candidates,
316                select_count,
317                ..
318            } => {
319                assert_eq!(candidates.len(), 4); // batch_size
320                assert_eq!(select_count, 2);
321            }
322            _ => panic!("Expected BatchSelection request"),
323        }
324    }
325
326    #[test]
327    fn test_default_interactive_fitness_process_response() {
328        let mut fitness: DefaultInteractiveFitness<RealVector> =
329            DefaultInteractiveFitness::new(EvaluationMode::Rating);
330        let mut aggregator = FitnessAggregator::new(AggregationModel::DirectRating {
331            default_rating: 5.0,
332        });
333
334        let response =
335            EvaluationResponse::ratings(vec![(CandidateId(0), 8.0), (CandidateId(1), 6.0)]);
336
337        let updated = fitness.process_response(response, &mut aggregator);
338        assert_eq!(updated.len(), 2);
339    }
340}