ecrs/ga.rs
1#![cfg(feature = "ga")]
2//! Implementation of genetic algorithm and genetic operators
3//!
4//! #### Description
5//!
6//! Evolutionary computation can be perceived as group of optimization
7//! algorithms behaviour of which is mainly based on naturally occuring
8//! processes. In this case, the process is Darwin's evolution and it's
9//! "the strongest is the most likely to survive" (apologies to all biologists)
10//! rule. This is the basis behind evolutionary (in particular - genetic) algorithms.
11//!
12//! For better grasp of theory we recommend taking a look into literature such as
13//! "Introduction to Genetic Algorithms" by S. N. Sivanandam & S. N. Deepa
14//! (there are plenty more papers & books on this topic though). Here, below
15//! we explain shortly only the most important terminology.
16//!
17//! The basic term used by genetic algorithm is `individual` (see our [`Individual`](crate::ga::individual::Individual) type).
18//! It describes any point (usually called `solution`) from problem domain. Its encoded version
19//! e.g. to bitstring is called `chromosome` (see [`Chromosome`](crate::ga::individual::Chromosome) type).
20//! The `chromosome` is comprised of `genes`. The possible set of values that gene can take is called `allele`.
21//!
22//! Let's look at example.
23//!
24//! Let's say that we want to optimize $f(x) = x^2$ where
25//! $x \in {0, 1, 2, \ldots, 7}$. The possible solutions (individuals) would be any of the values
26//! from domain - 0, 1, 2, 3. Let 1 be an individual then, and `001` be its `chromosome` (enconding).
27//! Then the `allele` would be ${0, 1}$ for each genee (set of possible gene values).
28//!
29//! The overall structure of genetic algorithm:
30//!
31//! 1. Generate initial population
32//! 2. Evalute population (what is the value of fitness function for each individual?)
33//! 3. Apply selection operator
34//! 4. Apply crossover operator
35//! 5. Apply mutation operator
36//! 6. Apply replacement operator
37//! 7. Termination condition satisfied? Yes -> END, no -> go to 2
38//!
39//! The `population` is a set of feasible solutions to the problem (individuals). Usually initial
40//! `population` is created by random generation (see our [population generators](crate::ga::population)).
41//!
42//! Such `population` is later transformed (evolves) by applying series of transformers (operators).
43//!
44//! For description of each operator we recommend reading their docs.
45//!
46//!
47//! * See [selection operators](crate::ga::operators::selection)
48//! * See [crossover operators](crate::ga::operators::crossover)
49//! * See [mutation operators](crate::ga::operators::mutation)
50//! * See [replacement operators](crate::ga::operators::replacement)
51//!
52//! #### Basic usage
53//!
54//! The instance of genetic algorithm is created with usage of its builder (see [Builder](crate::ga::builder::Builder)).
55//!
56//! ```no_run
57//! use ecrs::prelude::*;
58//! use ecrs::ga;
59//!
60//! # fn rastrigin_fitness(chromosome: &Vec<f64>) -> f64 {
61//! # 1000.0 * f64::exp(-ecrs::test_functions::rastrigin(chromosome))
62//! # }
63//!
64//! let mut res = ga::Builder::new::<
65//! ga::individual::RealValueIndividual,
66//! mutation::Identity,
67//! crossover::SinglePoint,
68//! selection::Boltzmann,
69//! replacement::WeakParent,
70//! population::RandomPoints,
71//! fitness::FnBasedFitness<ga::individual::RealValueIndividual>,
72//! ga::probe::AggregatedProbe<ga::individual::RealValueIndividual>
73//! >()
74//! .set_max_generation_count(50_000)
75//! .set_population_size(100)
76//! .set_fitness_fn(rastrigin_fitness)
77//! .set_crossover_operator(ops::crossover::SinglePoint::new())
78//! .set_replacement_operator(ops::replacement::WeakParent::new())
79//! .set_mutation_operator(ops::mutation::Identity::new())
80//! .set_population_generator(population::RandomPoints::with_constraints(
81//! 3,
82//! vec![-5.12..5.12, -5.12..5.12, -5.12..5.12],
83//! ))
84//! .set_selection_operator(ga::operators::selection::Boltzmann::new(0.05, 80.0, 500, false))
85//! .set_probe(
86//! ga::probe::AggregatedProbe::new()
87//! .add_probe(ga::probe::PolicyDrivenProbe::new(
88//! ga::probe::ElapsedTime::new(std::time::Duration::from_millis(200), std::time::Duration::ZERO),
89//! ga::probe::StdoutProbe,
90//! ))
91//! .add_probe(ga::probe::PolicyDrivenProbe::new(
92//! ga::probe::GenerationInterval::new(500, 100),
93//! ga::probe::StdoutProbe,
94//! )),
95//! )
96//! .build();
97//!
98//! // Run the algorithm
99//! let result = res.run();
100//! ```
101//!
102//! Hella, so many configuration steps?! Let me be clear: there are evem more configuration possibilites. But they are **possibilities**!
103//! The minimum you must specify:
104//!
105//! 1. Fitness function (the algorithm must know what it is optimizing)
106//! 2. Problem dimension
107//! 3. Population generator (the algorithm must be able to create initial population)
108//! 4. Probe (the logging object -- if you don't want to see any logs other than final result just pass [Empty probe](crate::ga::probes::Empty))
109//!
110//! The defaults for operators and parameters are provided for two types of chromosomes: bit string and real valued vector (see docs of [Builder](crage::ga::builder::Builder)),
111//! but keep in mind that these default options might not be even good for your particular problem as the operators & parameters should be
112//! tailored individually for each problem.
113//!
114//! * See [probes & configuration](ecrs::ga::probe)
115//! * See [population generators](ecrs::ga::population)
116//! * See [fitness & configuration](ecrs::ga::fitness)
117//! * See [available params](self::GAParams)
118
119pub mod builder;
120pub mod individual;
121pub mod operators;
122pub mod population;
123pub mod probe;
124
125use crate::ga::operators::fitness::Fitness;
126pub use builder::*;
127pub use individual::Individual;
128pub use probe::CsvProbe;
129pub use probe::JsonProbe;
130pub use probe::Probe;
131pub use probe::StdoutProbe;
132use std::marker::PhantomData;
133
134use self::individual::IndividualTrait;
135use self::{
136 operators::{
137 crossover::CrossoverOperator, mutation::MutationOperator, replacement::ReplacementOperator,
138 selection::SelectionOperator,
139 },
140 population::PopulationGenerator,
141};
142
143pub struct GAParams {
144 pub selection_rate: f64,
145 pub mutation_rate: f64,
146 pub population_size: usize,
147 pub generation_limit: usize,
148 pub max_duration: std::time::Duration,
149}
150
151pub struct GAConfig<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>
152where
153 IndividualT: IndividualTrait,
154 MutOpT: MutationOperator<IndividualT>,
155 CrossOpT: CrossoverOperator<IndividualT>,
156 SelOpT: SelectionOperator<IndividualT>,
157 ReplOpT: ReplacementOperator<IndividualT>,
158 PopGenT: PopulationGenerator<IndividualT>,
159 FitnessT: Fitness<IndividualT>,
160 ProbeT: Probe<IndividualT>,
161{
162 pub params: GAParams,
163 pub fitness_fn: FitnessT,
164 pub mutation_operator: MutOpT,
165 pub crossover_operator: CrossOpT,
166 pub selection_operator: SelOpT,
167 pub replacement_operator: ReplOpT,
168 pub population_factory: PopGenT,
169 pub probe: ProbeT,
170 _phantom: PhantomData<IndividualT::ChromosomeT>,
171}
172
173#[derive(Default)]
174pub struct GAMetadata {
175 pub start_time: Option<std::time::Instant>,
176 pub duration: Option<std::time::Duration>,
177 pub generation: usize,
178}
179
180impl GAMetadata {
181 pub fn new(
182 start_time: Option<std::time::Instant>,
183 duration: Option<std::time::Duration>,
184 generation: usize,
185 ) -> Self {
186 GAMetadata {
187 start_time,
188 duration,
189 generation,
190 }
191 }
192}
193
194pub struct GeneticSolver<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>
195where
196 IndividualT: IndividualTrait,
197 MutOpT: MutationOperator<IndividualT>,
198 CrossOpT: CrossoverOperator<IndividualT>,
199 SelOpT: SelectionOperator<IndividualT>,
200 ReplOpT: ReplacementOperator<IndividualT>,
201 PopGenT: PopulationGenerator<IndividualT>,
202 FitnessT: Fitness<IndividualT>,
203 ProbeT: Probe<IndividualT>,
204{
205 config: GAConfig<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>,
206 metadata: GAMetadata,
207}
208
209impl<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>
210 GeneticSolver<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>
211where
212 IndividualT: IndividualTrait,
213 MutOpT: MutationOperator<IndividualT>,
214 CrossOpT: CrossoverOperator<IndividualT>,
215 SelOpT: SelectionOperator<IndividualT>,
216 ReplOpT: ReplacementOperator<IndividualT>,
217 PopGenT: PopulationGenerator<IndividualT>,
218 FitnessT: Fitness<IndividualT>,
219 ProbeT: Probe<IndividualT>,
220{
221 pub fn new(
222 config: GAConfig<IndividualT, MutOpT, CrossOpT, SelOpT, ReplOpT, PopGenT, FitnessT, ProbeT>,
223 ) -> Self {
224 assert_eq!(config.params.population_size % 2, 0); // Required for most of operators right
225 // now
226 GeneticSolver {
227 config,
228 metadata: GAMetadata::new(None, None, 0),
229 }
230 }
231
232 #[inline]
233 fn find_best_individual(population: &[IndividualT]) -> &IndividualT {
234 population.iter().min().unwrap()
235 }
236
237 #[inline]
238 fn eval_pop(&mut self, population: &mut [IndividualT]) {
239 population
240 .iter_mut()
241 .filter(|idv| idv.requires_evaluation())
242 .for_each(|idv| *idv.fitness_mut() = (self.config.fitness_fn).apply(idv));
243 }
244
245 #[inline(always)]
246 fn gen_pop(&mut self) -> Vec<IndividualT> {
247 self.config
248 .population_factory
249 .generate(self.config.params.population_size)
250 }
251
252 pub fn run(&mut self) -> Option<IndividualT> {
253 self.metadata.start_time = Some(std::time::Instant::now());
254 self.config.probe.on_start(&self.metadata);
255
256 let mut population = self.gen_pop();
257
258 self.eval_pop(&mut population);
259
260 self.config.probe.on_initial_population_created(&population);
261
262 let mut best_individual_all_time = Self::find_best_individual(&population).clone();
263
264 self.metadata.duration = Some(self.metadata.start_time.unwrap().elapsed());
265 self.config
266 .probe
267 .on_new_best(&self.metadata, &best_individual_all_time);
268
269 for generation_no in 1..=self.config.params.generation_limit {
270 self.metadata.generation = generation_no;
271 self.metadata.duration = Some(self.metadata.start_time.unwrap().elapsed());
272
273 self.config.probe.on_iteration_start(&self.metadata);
274
275 // 2. Evaluate fitness for each individual.
276 self.eval_pop(&mut population);
277
278 // 4. Create mating pool by applying selection operator.
279 let mating_pool: Vec<&IndividualT> =
280 self.config
281 .selection_operator
282 .apply(&self.metadata, &population, population.len());
283
284 // 5. From mating pool create new generation (apply crossover & mutation).
285 let mut children: Vec<IndividualT> = Vec::with_capacity(self.config.params.population_size);
286
287 // FIXME: Do not assume that population size is an even number.
288 for parents in mating_pool.chunks(2) {
289 let crt_children = self.config.crossover_operator.apply(parents[0], parents[1]);
290
291 children.push(crt_children.0);
292 children.push(crt_children.1);
293 }
294
295 children.iter_mut().for_each(|child| {
296 self.config
297 .mutation_operator
298 .apply(child, self.config.params.mutation_rate)
299 });
300
301 if self.config.replacement_operator.requires_children_fitness() {
302 self.eval_pop(&mut children);
303 }
304
305 // 6. Replacement - merge new generation with old one
306 population = self.config.replacement_operator.apply(population, children);
307
308 // 7. Check for stop condition (Is good enough individual found)? If not goto 2.
309 self.eval_pop(&mut population);
310
311 self.config.probe.on_new_generation(&self.metadata, &population);
312
313 let best_individual = Self::find_best_individual(&population);
314 self.config
315 .probe
316 .on_best_fit_in_generation(&self.metadata, best_individual);
317
318 if *best_individual < best_individual_all_time {
319 best_individual_all_time = best_individual.clone();
320 self.config
321 .probe
322 .on_new_best(&self.metadata, &best_individual_all_time);
323 }
324
325 self.config.probe.on_iteration_end(&self.metadata);
326
327 if self.metadata.start_time.unwrap().elapsed() >= self.config.params.max_duration {
328 break;
329 }
330 }
331
332 self.config
333 .probe
334 .on_end(&self.metadata, &population, &best_individual_all_time);
335 Some(best_individual_all_time)
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::GAMetadata;
342
343 #[test]
344 fn gametadata_can_be_constructed_with_new_fn() {
345 GAMetadata::new(None, None, 0);
346 }
347}