Skip to content

Commit af2349a

Browse files
committed
Implement Phase 5: Examples and documentation
- Add comprehensive examples: - sphere_optimization: Basic continuous optimization - rastrigin_benchmark: Multimodal function optimization - cma_es_example: CMA-ES for Rosenbrock - island_model: Parallel island model - checkpointing: Save/restore evolution state - symbolic_regression: GP with tree genomes - hyperparameter_learning: Bayesian hyperparameter adaptation - Add README with: - Quick start guide - Feature overview - Algorithm descriptions - Example usage
1 parent e740519 commit af2349a

8 files changed

Lines changed: 1063 additions & 0 deletions

README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# fugue-evo
2+
3+
A Probabilistic Genetic Algorithm Library for Rust.
4+
5+
This library implements genetic algorithms through the lens of probabilistic programming, treating evolution as Bayesian inference over solution spaces.
6+
7+
## Features
8+
9+
- **Multiple Algorithms**: Simple GA, CMA-ES, NSGA-II, Island Model
10+
- **Flexible Genomes**: Real-valued vectors, bit strings, permutations, and GP trees
11+
- **Rich Operators**: SBX crossover, polynomial mutation, tournament selection, and more
12+
- **Probabilistic Integration**: Fugue PPL integration for trace-based evolutionary operators
13+
- **Bayesian Learning**: Online hyperparameter adaptation using conjugate priors
14+
- **Production Ready**: Checkpointing, convergence detection, parallel evaluation
15+
16+
## Quick Start
17+
18+
Add to your `Cargo.toml`:
19+
20+
```toml
21+
[dependencies]
22+
fugue-evo = "0.1"
23+
```
24+
25+
Basic optimization example:
26+
27+
```rust
28+
use fugue_evo::prelude::*;
29+
use rand::SeedableRng;
30+
31+
fn main() -> Result<(), Box<dyn std::error::Error>> {
32+
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
33+
34+
// Optimize the 10-D Sphere function
35+
let fitness = Sphere::new(10);
36+
let bounds = MultiBounds::symmetric(5.12, 10);
37+
38+
let result = SimpleGABuilder::<RealVector, f64, _, _, _, _, _>::new()
39+
.population_size(100)
40+
.bounds(bounds)
41+
.selection(TournamentSelection::new(3))
42+
.crossover(SbxCrossover::new(20.0))
43+
.mutation(PolynomialMutation::new(20.0))
44+
.fitness(fitness)
45+
.max_generations(200)
46+
.build()?
47+
.run(&mut rng)?;
48+
49+
println!("Best fitness: {}", result.best_fitness);
50+
Ok(())
51+
}
52+
```
53+
54+
## Examples
55+
56+
The `examples/` directory contains demonstrations of various features:
57+
58+
- `sphere_optimization.rs` - Basic continuous optimization
59+
- `rastrigin_benchmark.rs` - Multimodal function optimization
60+
- `cma_es_example.rs` - CMA-ES for Rosenbrock function
61+
- `island_model.rs` - Parallel island model evolution
62+
- `checkpointing.rs` - Save and restore evolution state
63+
- `symbolic_regression.rs` - Genetic programming with tree genomes
64+
- `hyperparameter_learning.rs` - Bayesian hyperparameter adaptation
65+
66+
Run an example:
67+
68+
```bash
69+
cargo run --example sphere_optimization
70+
```
71+
72+
## Core Concepts
73+
74+
### Fitness as Likelihood
75+
76+
Selection pressure maps directly to Bayesian conditioning. Higher fitness increases the probability of selection, analogous to likelihood weighting in probabilistic inference.
77+
78+
### Learnable Operators
79+
80+
The library supports automatic inference of optimal crossover, mutation, and selection hyperparameters using Bayesian conjugate priors that update online during evolution.
81+
82+
### Flexible Genomes
83+
84+
The `EvolutionaryGenome` trait provides a unified abstraction supporting:
85+
- `RealVector` - Continuous optimization
86+
- `BitString` - Binary/combinatorial problems
87+
- `Permutation` - Ordering problems (TSP, scheduling)
88+
- `TreeGenome` - Genetic programming
89+
90+
### Fugue Integration
91+
92+
Genomes can be converted to Fugue PPL traces for probabilistic operations:
93+
94+
```rust
95+
let trace = genome.to_trace();
96+
let recovered = RealVector::from_trace(&trace)?;
97+
```
98+
99+
## Algorithms
100+
101+
### Simple GA
102+
103+
Standard generational genetic algorithm with configurable operators.
104+
105+
### CMA-ES
106+
107+
Covariance Matrix Adaptation Evolution Strategy for continuous optimization. Adapts the full covariance matrix of a multivariate normal distribution.
108+
109+
### NSGA-II
110+
111+
Non-dominated Sorting Genetic Algorithm II for multi-objective optimization. Finds Pareto-optimal solutions.
112+
113+
### Island Model
114+
115+
Parallel evolution with multiple subpopulations and periodic migration. Supports ring, fully-connected, and star topologies.
116+
117+
## License
118+
119+
Licensed under either of Apache License, Version 2.0 or MIT license at your option.

examples/checkpointing.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! Checkpointing and Recovery
2+
//!
3+
//! This example demonstrates how to save and restore evolution state
4+
//! using checkpoints. This is essential for long-running optimizations
5+
//! that may need to be interrupted and resumed.
6+
7+
use fugue_evo::prelude::*;
8+
use rand::SeedableRng;
9+
use rand::rngs::StdRng;
10+
use std::path::PathBuf;
11+
12+
fn main() -> Result<(), Box<dyn std::error::Error>> {
13+
println!("=== Checkpointing and Recovery ===\n");
14+
15+
let checkpoint_dir = PathBuf::from("/tmp/fugue_evo_checkpoints");
16+
17+
// Clean up any existing checkpoints first
18+
if checkpoint_dir.exists() {
19+
std::fs::remove_dir_all(&checkpoint_dir)?;
20+
}
21+
22+
// Run with checkpoints
23+
run_with_checkpoints(&checkpoint_dir)?;
24+
25+
// Demonstrate resuming (in real usage, this would be after a restart)
26+
println!("\n--- Simulating resume from checkpoint ---\n");
27+
resume_from_checkpoint(&checkpoint_dir)?;
28+
29+
// Clean up
30+
if checkpoint_dir.exists() {
31+
std::fs::remove_dir_all(&checkpoint_dir)?;
32+
println!("\nCheckpoint directory cleaned up.");
33+
}
34+
35+
Ok(())
36+
}
37+
38+
fn run_with_checkpoints(checkpoint_dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
39+
let mut rng = StdRng::seed_from_u64(42);
40+
41+
const DIM: usize = 10;
42+
let fitness = Sphere::new(DIM);
43+
let bounds = MultiBounds::symmetric(5.12, DIM);
44+
45+
// Run evolution with periodic checkpoints
46+
let mut population: Population<RealVector, f64> = Population::random(100, &bounds, &mut rng);
47+
population.evaluate(&fitness);
48+
49+
let selection = TournamentSelection::new(3);
50+
let crossover = SbxCrossover::new(20.0);
51+
let mutation = PolynomialMutation::new(20.0);
52+
53+
// Create checkpoint manager
54+
let mut manager = CheckpointManager::new(checkpoint_dir, "evolution")
55+
.every(50) // Save every 50 generations
56+
.keep(3); // Keep last 3 checkpoints
57+
58+
let max_generations = 200;
59+
60+
for gen in 0..max_generations {
61+
// Evolution step
62+
let selection_pool: Vec<_> = population.as_fitness_pairs();
63+
let mut new_pop: Population<RealVector, f64> = Population::with_capacity(100);
64+
65+
// Elitism
66+
if let Some(best) = population.best() {
67+
new_pop.push(best.clone());
68+
}
69+
70+
while new_pop.len() < 100 {
71+
let p1_idx = selection.select(&selection_pool, &mut rng);
72+
let p2_idx = selection.select(&selection_pool, &mut rng);
73+
74+
let (mut c1, mut c2) = crossover
75+
.crossover(&selection_pool[p1_idx].0, &selection_pool[p2_idx].0, &mut rng)
76+
.genome()
77+
.unwrap_or_else(|| {
78+
(selection_pool[p1_idx].0.clone(), selection_pool[p2_idx].0.clone())
79+
});
80+
81+
mutation.mutate(&mut c1, &mut rng);
82+
mutation.mutate(&mut c2, &mut rng);
83+
84+
new_pop.push(Individual::new(c1));
85+
if new_pop.len() < 100 {
86+
new_pop.push(Individual::new(c2));
87+
}
88+
}
89+
90+
new_pop.evaluate(&fitness);
91+
new_pop.set_generation(gen + 1);
92+
population = new_pop;
93+
94+
// Save checkpoint periodically
95+
if manager.should_save(gen + 1) {
96+
let best = population.best().unwrap();
97+
println!(
98+
"Gen {:3}: Best = {:.6} - Saving checkpoint...",
99+
gen + 1,
100+
best.fitness_value()
101+
);
102+
103+
// Create checkpoint with current population
104+
let individuals: Vec<Individual<RealVector>> = population.iter().cloned().collect();
105+
let checkpoint = Checkpoint::new(gen + 1, individuals)
106+
.with_evaluations((gen + 1) * 100);
107+
108+
manager.save(&checkpoint)?;
109+
}
110+
}
111+
112+
let best = population.best().unwrap();
113+
println!("\nFinal result:");
114+
println!(" Best fitness: {:.6}", best.fitness_value());
115+
println!(" Generations: {}", max_generations);
116+
117+
Ok(())
118+
}
119+
120+
fn resume_from_checkpoint(checkpoint_dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
121+
// Find the latest checkpoint file
122+
let entries: Vec<_> = std::fs::read_dir(checkpoint_dir)?
123+
.filter_map(|e| e.ok())
124+
.filter(|e| e.path().extension().map_or(false, |ext| ext == "ckpt"))
125+
.collect();
126+
127+
if entries.is_empty() {
128+
println!("No checkpoint files found!");
129+
return Ok(());
130+
}
131+
132+
// Sort by name to get the latest
133+
let mut paths: Vec<_> = entries.iter().map(|e| e.path()).collect();
134+
paths.sort();
135+
let latest_checkpoint = paths.last().unwrap();
136+
137+
println!("Loading checkpoint: {:?}", latest_checkpoint);
138+
139+
// Load checkpoint
140+
let checkpoint: Checkpoint<RealVector> = load_checkpoint(latest_checkpoint)?;
141+
142+
println!("Loaded checkpoint:");
143+
println!(" Generation: {}", checkpoint.generation);
144+
println!(" Population size: {}", checkpoint.population.len());
145+
println!(" Evaluations: {}", checkpoint.evaluations);
146+
147+
// Find best in loaded population
148+
let best_individual = checkpoint.population.iter()
149+
.filter_map(|ind| ind.fitness.as_ref().map(|f| (ind, f.to_f64())))
150+
.max_by(|(_, f1), (_, f2)| f1.partial_cmp(f2).unwrap());
151+
152+
if let Some((_best, fitness)) = best_individual {
153+
println!(" Best fitness at checkpoint: {:.6}", fitness);
154+
}
155+
156+
// Continue evolution...
157+
let mut rng = StdRng::seed_from_u64(12345); // Different seed for continuation
158+
let fitness = Sphere::new(10);
159+
160+
// Reconstruct population from checkpoint
161+
let mut population: Population<RealVector, f64> = Population::with_capacity(checkpoint.population.len());
162+
for ind in checkpoint.population {
163+
population.push(ind);
164+
}
165+
166+
let selection = TournamentSelection::new(3);
167+
let crossover = SbxCrossover::new(20.0);
168+
let mutation = PolynomialMutation::new(20.0);
169+
170+
let remaining_gens = 200 - checkpoint.generation;
171+
println!("\nContinuing for {} more generations...\n", remaining_gens);
172+
173+
for gen in checkpoint.generation..200 {
174+
let selection_pool: Vec<_> = population.as_fitness_pairs();
175+
let mut new_pop: Population<RealVector, f64> = Population::with_capacity(100);
176+
177+
if let Some(best) = population.best() {
178+
new_pop.push(best.clone());
179+
}
180+
181+
while new_pop.len() < 100 {
182+
let p1_idx = selection.select(&selection_pool, &mut rng);
183+
let p2_idx = selection.select(&selection_pool, &mut rng);
184+
185+
let (mut c1, mut c2) = crossover
186+
.crossover(&selection_pool[p1_idx].0, &selection_pool[p2_idx].0, &mut rng)
187+
.genome()
188+
.unwrap_or_else(|| {
189+
(selection_pool[p1_idx].0.clone(), selection_pool[p2_idx].0.clone())
190+
});
191+
192+
mutation.mutate(&mut c1, &mut rng);
193+
mutation.mutate(&mut c2, &mut rng);
194+
195+
new_pop.push(Individual::new(c1));
196+
if new_pop.len() < 100 {
197+
new_pop.push(Individual::new(c2));
198+
}
199+
}
200+
201+
new_pop.evaluate(&fitness);
202+
new_pop.set_generation(gen + 1);
203+
population = new_pop;
204+
205+
if (gen + 1) % 50 == 0 {
206+
let best = population.best().unwrap();
207+
println!("Gen {:3}: Best = {:.6}", gen + 1, best.fitness_value());
208+
}
209+
}
210+
211+
let best = population.best().unwrap();
212+
println!("\nFinal result after resumption:");
213+
println!(" Best fitness: {:.6}", best.fitness_value());
214+
215+
Ok(())
216+
}

0 commit comments

Comments
 (0)