galapagos/
lib.rs

1//! # Galapagos
2//!
3//! Low-dependency, simple evolutionary solver.
4
5use rand::{self, Rng};
6
7#[derive(PartialEq)]
8pub enum Goal {
9    Maximize,
10    Minimize,
11}
12
13/// A single solution attempt.
14#[derive(Debug, Clone)]
15pub struct Individual {
16    /// The values this individual has set for its sliders. Some people refer to
17    /// these as "genes".
18    pub values: Vec<f32>,
19    /// Cached result of calling `f` with this individual's `values`.
20    pub fitness: f32,
21}
22
23/// The near-optimal answer for a given series of evolutions.
24#[derive(Debug)]
25pub struct Solution {
26    /// The most fit individual from a population that evolved over many
27    /// generations.
28    pub champion: Individual,
29    /// Optionally store the history of each generation's most fit individual.
30    /// Useful to plot the behavior for debugging.
31    pub history: Option<Vec<(f32, f32)>>,
32}
33
34/// Solver configuration.
35#[derive(Debug)]
36pub struct Config {
37    /// The number of individuals to evaluate in each generation.
38    pub population_size: u32,
39    /// The number of evolution iterations to run.
40    pub generation_count: u32,
41    /// The likelihood of any two "parents" of mixing their "genes" instead of
42    /// passing them on directly.
43    pub crossover_rate: f32,
44    /// The likelihood of any one gene changing in value.
45    pub mutation_rate: f32,
46    /// Whether or not to store a vector recording the fitness of each
47    /// generation's most fit individual.
48    pub record_history: bool,
49}
50
51impl Default for Config {
52    fn default() -> Self {
53        Config {
54            population_size: 200,
55            generation_count: 1_000,
56            crossover_rate: 0.70,
57            mutation_rate: 0.10,
58            record_history: false,
59        }
60    }
61}
62
63/// Attempts to find a near-optimal solution for a given equation.
64///
65/// # Arguments
66///
67/// * `f` - The function to solve.
68/// * `sliders` - List of tuples representing min, max bounds for a "slider"
69/// that the solver can modulate when searching for solutions.
70/// * `goal` - Whether to search for a maximal or minimal solution.
71/// * `cfg` - Solver configuration.
72///
73/// # Examples
74///
75/// ```
76/// use galapagos::{solve, Goal};
77/// let solution = solve(
78///     |xs| {
79///         // Sphere function
80///         let x = xs[0];
81///         let y = xs[1];
82///         let z = xs[2];
83///         x.powi(2) + y.powi(2) + z.powi(2)
84///     },
85///     &[(-5.0, 5.0), (-5.0, 5.0), (-5.0, 5.0)],
86///     Goal::Minimize,
87///     Default::default(),
88/// );
89/// assert_eq!(solution.champion.values.len(), 3);
90/// ```
91pub fn solve<F: Fn(&[f32]) -> f32>(
92    f: F,
93    sliders: &[(f32, f32)],
94    goal: Goal,
95    cfg: Config,
96) -> Solution {
97    assert!(
98        cfg.population_size > 2,
99        "Population size should be at least two"
100    );
101    assert!(
102        cfg.population_size % 2 == 0,
103        "We only support even-numbered population size at the moment"
104    );
105    assert!(cfg.generation_count > 1, "Need at least one generation");
106
107    let evaluate_fitness = |idv: &mut Individual| -> () {
108        idv.fitness = f(&idv.values);
109    };
110
111    // Function that returns the most fitness individual from a list of
112    // candidates.
113    let compete = |xs: Vec<Individual>| -> Individual {
114        assert!(xs.len() >= 2);
115        match goal {
116            Goal::Maximize => xs
117                .iter()
118                .max_by(|x, y| x.fitness.partial_cmp(&y.fitness).unwrap())
119                .unwrap()
120                .clone(),
121            Goal::Minimize => xs
122                .iter()
123                .min_by(|x, y| x.fitness.partial_cmp(&y.fitness).unwrap())
124                .unwrap()
125                .clone(),
126        }
127    };
128
129    let mut rng = rand::thread_rng();
130
131    // population size (1,000)
132    let mut parents: Vec<Individual> = Vec::with_capacity(cfg.population_size as usize);
133    for _ in 0..cfg.population_size {
134        let mut xs = Vec::with_capacity(sliders.len());
135        for j in 0..sliders.len() {
136            xs.push(rng.gen_range(sliders[j].0..=sliders[j].1));
137        }
138        let mut i = Individual {
139            values: xs,
140            fitness: 0.0,
141        };
142        evaluate_fitness(&mut i);
143        parents.push(i);
144    }
145
146    let mut history: Vec<(f32, f32)> = if cfg.record_history {
147        Vec::with_capacity(cfg.generation_count as usize)
148    } else {
149        Vec::with_capacity(0)
150    };
151
152    for generation in 0..cfg.generation_count as usize {
153        // host a tournament
154        let mut children: Vec<Individual> = Vec::with_capacity(cfg.population_size as usize);
155        for _ in 0..cfg.population_size {
156            let i = rng.gen_range(0..cfg.population_size as usize);
157            let j = rng.gen_range(0..cfg.population_size as usize);
158            let lhs = &parents[i];
159            let rhs = &parents[j];
160            let winner = compete(vec![lhs.clone(), rhs.clone()]);
161            children.push(winner);
162        }
163        let champion = compete(parents);
164
165        if cfg.record_history {
166            history.push((generation as f32, champion.fitness))
167        }
168
169        // crossover the winners
170        for i in 0..(cfg.population_size as usize / 2) {
171            // crossover rate
172            if rng.gen::<f32>() < cfg.crossover_rate {
173                let mut mom = children[i * 2].clone();
174                let mut dad = children[i * 2 + 1].clone();
175
176                let point = rng.gen_range(0..sliders.len());
177                for j in 0..sliders.len() {
178                    mom.values[j] = if j < point {
179                        mom.values[j]
180                    } else {
181                        dad.values[j]
182                    };
183                    dad.values[j] = if j < point {
184                        dad.values[j]
185                    } else {
186                        mom.values[j]
187                    };
188                }
189
190                children[i * 2] = mom;
191                children[i * 2 + 1] = dad;
192            }
193        }
194
195        // mutate the winners
196        for i in 0..cfg.population_size as usize {
197            let x = &mut children[i];
198            for j in 0..sliders.len() {
199                if rng.gen::<f32>() < cfg.mutation_rate {
200                    x.values[j] =
201                        (x.values[j] + rng.gen_range(-0.5..=0.5)).clamp(sliders[j].0, sliders[j].1);
202                }
203            }
204            evaluate_fitness(x);
205        }
206        parents = children;
207    }
208    let champion = compete(parents);
209    Solution {
210        champion,
211        history: if cfg.record_history {
212            Some(history)
213        } else {
214            None
215        },
216    }
217}