Skip to content

Commit 795d6e9

Browse files
committed
feat: implement human based interfaces
1 parent 6caa350 commit 795d6e9

10 files changed

Lines changed: 3457 additions & 0 deletions

File tree

examples/interactive_evolution.rs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//! Interactive Genetic Algorithm Example
2+
//!
3+
//! This example demonstrates how to use the Interactive GA for human-in-the-loop
4+
//! evolutionary optimization. Instead of an automated fitness function, users
5+
//! provide feedback by rating, comparing, or selecting candidates.
6+
//!
7+
//! In this example, we simulate user feedback with a simple automated scorer,
8+
//! but in a real application, you would present candidates to users via a UI.
9+
10+
use fugue_evo::genome::bounds::Bounds;
11+
use fugue_evo::interactive::prelude::*;
12+
use fugue_evo::prelude::*;
13+
use rand::rngs::StdRng;
14+
use rand::SeedableRng;
15+
16+
/// Simulates user preference for solutions close to a target
17+
struct SimulatedUserPreference {
18+
target: Vec<f64>,
19+
}
20+
21+
impl SimulatedUserPreference {
22+
fn new(dim: usize) -> Self {
23+
// User prefers solutions where values are around 0.5
24+
Self {
25+
target: vec![0.5; dim],
26+
}
27+
}
28+
29+
/// Simulate a user rating (1-10 scale)
30+
fn rate(&self, genome: &RealVector) -> f64 {
31+
let distance: f64 = genome
32+
.genes()
33+
.iter()
34+
.zip(self.target.iter())
35+
.map(|(g, t)| (g - t).powi(2))
36+
.sum::<f64>()
37+
.sqrt();
38+
39+
// Convert distance to rating (closer = higher rating)
40+
let rating = 10.0 - (distance * 5.0).min(9.0);
41+
rating.max(1.0)
42+
}
43+
44+
/// Simulate pairwise comparison
45+
fn compare(&self, a: &RealVector, b: &RealVector) -> std::cmp::Ordering {
46+
let rating_a = self.rate(a);
47+
let rating_b = self.rate(b);
48+
rating_a.partial_cmp(&rating_b).unwrap()
49+
}
50+
}
51+
52+
fn main() -> Result<(), Box<dyn std::error::Error>> {
53+
println!("=== Interactive Genetic Algorithm Demo ===\n");
54+
55+
let mut rng = StdRng::seed_from_u64(42);
56+
57+
const DIM: usize = 5;
58+
let bounds = MultiBounds::uniform(Bounds::new(0.0, 1.0), DIM);
59+
60+
// Create the simulated user preference
61+
let user = SimulatedUserPreference::new(DIM);
62+
63+
// Build the Interactive GA
64+
let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
65+
.population_size(12)
66+
.elitism_count(2)
67+
.evaluation_mode(EvaluationMode::Rating)
68+
.batch_size(4)
69+
.min_coverage(0.8)
70+
.max_generations(5)
71+
.aggregation_model(AggregationModel::DirectRating {
72+
default_rating: 5.0,
73+
})
74+
.bounds(bounds)
75+
.selection(TournamentSelection::new(2))
76+
.crossover(SbxCrossover::new(15.0))
77+
.mutation(PolynomialMutation::new(20.0))
78+
.build()?;
79+
80+
println!("Starting interactive evolution...\n");
81+
println!(
82+
"Configuration: {} individuals, {} mode, {} generations max",
83+
iga.config().population_size,
84+
match iga.config().evaluation_mode {
85+
EvaluationMode::Rating => "rating",
86+
EvaluationMode::Pairwise => "pairwise",
87+
EvaluationMode::BatchSelection => "batch selection",
88+
EvaluationMode::Adaptive => "adaptive",
89+
},
90+
iga.config().max_generations
91+
);
92+
println!();
93+
94+
// Main evolution loop
95+
loop {
96+
match iga.step(&mut rng) {
97+
StepResult::NeedsEvaluation(request) => {
98+
// In a real app, you'd present this to a user via UI
99+
// Here we simulate user feedback
100+
let response = simulate_user_response(&user, &request);
101+
iga.provide_response(response);
102+
}
103+
104+
StepResult::GenerationComplete {
105+
generation,
106+
best_fitness,
107+
coverage,
108+
} => {
109+
println!(
110+
"Generation {} complete: best = {:.2}, coverage = {:.0}%",
111+
generation,
112+
best_fitness.unwrap_or(0.0),
113+
coverage * 100.0
114+
);
115+
}
116+
117+
StepResult::Complete(result) => {
118+
println!("\n=== Evolution Complete ===");
119+
println!("Reason: {}", result.termination_reason);
120+
println!("Generations: {}", result.generations);
121+
println!("Total evaluations: {}", result.total_evaluations);
122+
println!("\nTop 3 candidates:");
123+
124+
for (i, candidate) in result.best_candidates.iter().take(3).enumerate() {
125+
println!(
126+
" #{}: fitness = {:.2}, genes = {:?}",
127+
i + 1,
128+
candidate.fitness_estimate.unwrap_or(0.0),
129+
candidate
130+
.genome
131+
.genes()
132+
.iter()
133+
.map(|g| format!("{:.3}", g))
134+
.collect::<Vec<_>>()
135+
.join(", ")
136+
);
137+
}
138+
break;
139+
}
140+
}
141+
}
142+
143+
// Demonstrate different evaluation modes
144+
println!("\n=== Batch Selection Mode Demo ===\n");
145+
146+
let mut iga_batch = InteractiveGABuilder::<RealVector, (), (), ()>::new()
147+
.population_size(12)
148+
.evaluation_mode(EvaluationMode::BatchSelection)
149+
.batch_size(6)
150+
.select_count(2)
151+
.min_coverage(0.5)
152+
.max_generations(3)
153+
.aggregation_model(AggregationModel::ImplicitRanking {
154+
selected_bonus: 1.0,
155+
not_selected_penalty: 0.3,
156+
base_fitness: 5.0,
157+
})
158+
.bounds(MultiBounds::uniform(Bounds::new(0.0, 1.0), DIM))
159+
.selection(TournamentSelection::new(2))
160+
.crossover(SbxCrossover::new(15.0))
161+
.mutation(PolynomialMutation::new(20.0))
162+
.build()?;
163+
164+
loop {
165+
match iga_batch.step(&mut rng) {
166+
StepResult::NeedsEvaluation(request) => {
167+
let response = simulate_user_response(&user, &request);
168+
iga_batch.provide_response(response);
169+
}
170+
171+
StepResult::GenerationComplete {
172+
generation,
173+
best_fitness,
174+
..
175+
} => {
176+
println!(
177+
"Generation {}: best = {:.2}",
178+
generation,
179+
best_fitness.unwrap_or(0.0)
180+
);
181+
}
182+
183+
StepResult::Complete(result) => {
184+
println!("\nBatch selection mode complete!");
185+
println!(
186+
"Best fitness: {:.2}",
187+
result.best_candidates[0].fitness_estimate.unwrap_or(0.0)
188+
);
189+
break;
190+
}
191+
}
192+
}
193+
194+
Ok(())
195+
}
196+
197+
/// Simulate user response to an evaluation request
198+
fn simulate_user_response(
199+
user: &SimulatedUserPreference,
200+
request: &EvaluationRequest<RealVector>,
201+
) -> EvaluationResponse {
202+
match request {
203+
EvaluationRequest::RateCandidates { candidates, .. } => {
204+
let ratings: Vec<_> = candidates
205+
.iter()
206+
.map(|c| (c.id, user.rate(&c.genome)))
207+
.collect();
208+
EvaluationResponse::ratings(ratings)
209+
}
210+
211+
EvaluationRequest::PairwiseComparison {
212+
candidate_a,
213+
candidate_b,
214+
..
215+
} => {
216+
use std::cmp::Ordering;
217+
match user.compare(&candidate_a.genome, &candidate_b.genome) {
218+
Ordering::Greater => EvaluationResponse::winner(candidate_a.id),
219+
Ordering::Less => EvaluationResponse::winner(candidate_b.id),
220+
Ordering::Equal => EvaluationResponse::tie(),
221+
}
222+
}
223+
224+
EvaluationRequest::BatchSelection {
225+
candidates,
226+
select_count,
227+
..
228+
} => {
229+
// Sort by rating and select top N
230+
let mut rated: Vec<_> = candidates
231+
.iter()
232+
.map(|c| (c.id, user.rate(&c.genome)))
233+
.collect();
234+
rated.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
235+
236+
let selected: Vec<_> = rated
237+
.iter()
238+
.take(*select_count)
239+
.map(|(id, _)| *id)
240+
.collect();
241+
EvaluationResponse::selected(selected)
242+
}
243+
}
244+
}

src/checkpoint/state.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ pub enum AlgorithmState {
135135
island_populations: Vec<Vec<usize>>,
136136
migration_count: usize,
137137
},
138+
/// Interactive GA state
139+
Interactive {
140+
/// Serialized aggregator state (JSON)
141+
aggregator_state: String,
142+
/// Number of pending evaluations
143+
pending_evaluations: usize,
144+
/// Evaluation mode
145+
evaluation_mode: String,
146+
},
138147
/// Custom algorithm state (JSON serialized)
139148
Custom(String),
140149
}

src/error.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ pub enum EvolutionError {
121121
/// Empty population
122122
#[error("Empty population")]
123123
EmptyPopulation,
124+
125+
/// Interactive evaluation error
126+
#[error("Interactive evaluation error: {0}")]
127+
InteractiveEvaluation(String),
128+
129+
/// Insufficient evaluation coverage
130+
#[error("Insufficient coverage: {coverage:.1}% (need {required:.1}%)")]
131+
InsufficientCoverage {
132+
/// Actual coverage achieved
133+
coverage: f64,
134+
/// Required coverage threshold
135+
required: f64,
136+
},
124137
}
125138

126139
/// Result type alias for evolution operations

0 commit comments

Comments
 (0)