Skip to content

Commit 8634a2c

Browse files
committed
Add elitism_rate to ExtensionMassExtinction and ExtensionMassDegeneration
Fix best_chromosome_indices for all None fitness
1 parent b2848ab commit 8634a2c

15 files changed

+355
-100
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.20.1] - 2025-05-08
8+
### Added
9+
* Add `elitism_rate` to `ExtensionMassExtinction` and
10+
`ExtensionMassDegeneration`. The `elitism_rate` ensures the passing of the best
11+
chromosomes before extinction or degeneration is applied
12+
13+
### Fixed
14+
* Fix `best_chromosome_indices()` on `Population` in case where there are no
15+
fitness values other than None
16+
717
## [0.20.0] - 2025-05-08
818

919
### Changed

benches/extension.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
4949
for genes_size in &genes_sizes {
5050
let extensions: Vec<ExtensionWrapper> = vec![
5151
ExtensionMassGenesis::new(population_size).into(),
52-
ExtensionMassExtinction::new(population_size, 0.10).into(),
53-
ExtensionMassDegeneration::new(population_size, 10).into(),
52+
ExtensionMassExtinction::new(population_size, 0.10, 0.02).into(),
53+
ExtensionMassDegeneration::new(population_size, 10, 0.02).into(),
5454
];
5555
for mut extension in extensions {
5656
group.throughput(Throughput::Elements(population_size as u64));

examples/evolve_scrabble.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,9 @@ fn main() {
279279
.with_crossover(CrossoverUniform::new(0.7, 0.8))
280280
// .with_select(SelectTournament::new(0.5, 0.02, 4))
281281
.with_select(SelectElite::new(0.5, 0.02))
282-
.with_extension(ExtensionMassDegeneration::new(2, 10))
282+
.with_extension(ExtensionMassDegeneration::new(2, 10, 0.02))
283283
// .with_extension(ExtensionMassGenesis::new(2))
284-
// .with_extension(ExtensionMassExtinction::new(2, 0.1))
284+
// .with_extension(ExtensionMassExtinction::new(2, 0.1, 0.02))
285285
// .with_reporter(EvolveReporterSimple::default())
286286
// .with_reporter(EvolveReporterSimple::new_with_flags(
287287
// 100, false, false, false, true,

src/extension.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,25 @@ pub trait Extension: Clone + Send + Sync + std::fmt::Debug {
3030
reporter: &mut SR,
3131
rng: &mut R,
3232
);
33+
34+
fn extract_elite_chromosomes<G: EvolveGenotype>(
35+
&self,
36+
state: &mut EvolveState<G>,
37+
config: &EvolveConfig,
38+
elitism_size: usize,
39+
) -> Vec<G::Chromosome> {
40+
let mut elite_chromosomes: Vec<G::Chromosome> = Vec::with_capacity(elitism_size);
41+
for index in state
42+
.population
43+
.best_chromosome_indices(elitism_size, config.fitness_ordering)
44+
.into_iter()
45+
.rev()
46+
{
47+
let chromosome = state.population.chromosomes.swap_remove(index);
48+
elite_chromosomes.push(chromosome);
49+
}
50+
elite_chromosomes
51+
}
3352
}
3453

3554
#[derive(Clone, Debug)]

src/extension/mass_degeneration.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ use rand::Rng;
66
use std::time::Instant;
77

88
/// Simulates a cambrian explosion. The controlling metric is population cardinality in the
9-
/// population after selection. When this cardinality drops to the threshold, the full population
10-
/// is mutated the provided number of times, where the [Genotype](crate::genotype::Genotype)
11-
/// determines whether this is random, relative or scaled.
9+
/// population after selection. When this cardinality drops to the threshold, the population
10+
/// (except for the two best chromosomes) is mutated the provided number of times, where the
11+
/// [Genotype](crate::genotype::Genotype) determines whether this is random, relative or scaled.
12+
/// The elitism_rate ensures the passing of the best chromosomes before mutations are applied.
1213
///
1314
/// Duplicate mutations of the same gene are allowed. There is no change in population size.
1415
#[derive(Debug, Clone)]
1516
pub struct MassDegeneration {
1617
pub cardinality_threshold: usize,
1718
pub number_of_mutations: usize,
19+
pub elitism_rate: f32,
1820
}
1921

2022
impl Extension for MassDegeneration {
@@ -36,6 +38,15 @@ impl Extension for MassDegeneration {
3638
state,
3739
config,
3840
);
41+
let population_size = state.population.size();
42+
43+
let elitism_size = ((population_size as f32 * self.elitism_rate).ceil()
44+
as usize)
45+
.min(population_size);
46+
let mut elite_chromosomes =
47+
self.extract_elite_chromosomes(state, config, elitism_size);
48+
let elitism_size = elite_chromosomes.len();
49+
3950
for chromosome in state.population.chromosomes.iter_mut() {
4051
genotype.mutate_chromosome_genes(
4152
self.number_of_mutations,
@@ -45,6 +56,15 @@ impl Extension for MassDegeneration {
4556
rng,
4657
);
4758
}
59+
60+
state.population.chromosomes.append(&mut elite_chromosomes);
61+
// move back to front, elite_chromosomes internally unordered
62+
for i in 0..elitism_size {
63+
state
64+
.population
65+
.chromosomes
66+
.swap(i, population_size - 1 - i);
67+
}
4868
}
4969
}
5070
state.add_duration(StrategyAction::Extension, now.elapsed());
@@ -53,10 +73,11 @@ impl Extension for MassDegeneration {
5373
}
5474

5575
impl MassDegeneration {
56-
pub fn new(cardinality_threshold: usize, number_of_rounds: usize) -> Self {
76+
pub fn new(cardinality_threshold: usize, number_of_rounds: usize, elitism_rate: f32) -> Self {
5777
Self {
5878
cardinality_threshold,
5979
number_of_mutations: number_of_rounds,
80+
elitism_rate,
6081
}
6182
}
6283
}

src/extension/mass_extinction.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ use std::time::Instant;
88
/// Simulates a cambrian explosion. The controlling metric is population cardinality in the
99
/// population after selection. When this cardinality drops to the threshold, the population is
1010
/// randomly reduced regardless of fitness using the survival_rate (fraction of population).
11+
/// The elitism_rate ensures the passing of the best chromosomes before random reduction starts.
1112
///
1213
/// Population will recover in the following generations
1314
#[derive(Debug, Clone)]
1415
pub struct MassExtinction {
1516
pub cardinality_threshold: usize,
1617
pub survival_rate: f32,
18+
pub elitism_rate: f32,
1719
}
1820

1921
impl Extension for MassExtinction {
@@ -35,16 +37,37 @@ impl Extension for MassExtinction {
3537
state,
3638
config,
3739
);
40+
let population_size = state.population.size();
41+
42+
let elitism_size = ((population_size as f32 * self.elitism_rate).ceil()
43+
as usize)
44+
.min(population_size);
45+
let mut elite_chromosomes =
46+
self.extract_elite_chromosomes(state, config, elitism_size);
47+
let elitism_size = elite_chromosomes.len();
48+
49+
let remaining_size: usize = ((population_size as f32 * self.survival_rate)
50+
.ceil() as usize)
51+
.min(population_size)
52+
.max(2);
53+
54+
let remaining_size = remaining_size.saturating_sub(elitism_size);
3855

39-
let remaining_size: usize = std::cmp::max(
40-
(state.population.size() as f32 * self.survival_rate).ceil() as usize,
41-
2,
42-
);
4356
state.population.shuffle(rng);
4457
genotype.chromosome_destructor_truncate(
4558
&mut state.population.chromosomes,
4659
remaining_size,
4760
);
61+
62+
state.population.chromosomes.append(&mut elite_chromosomes);
63+
let population_size = state.population.size();
64+
// move back to front, elite_chromosomes internally unordered
65+
for i in 0..elitism_size {
66+
state
67+
.population
68+
.chromosomes
69+
.swap(i, population_size - 1 - i);
70+
}
4871
}
4972
}
5073
state.add_duration(StrategyAction::Extension, now.elapsed());
@@ -53,10 +76,11 @@ impl Extension for MassExtinction {
5376
}
5477

5578
impl MassExtinction {
56-
pub fn new(cardinality_threshold: usize, survival_rate: f32) -> Self {
79+
pub fn new(cardinality_threshold: usize, survival_rate: f32, elitism_rate: f32) -> Self {
5780
Self {
5881
cardinality_threshold,
5982
survival_rate,
83+
elitism_rate,
6084
}
6185
}
6286
}

src/extension/mass_genesis.rs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,17 @@ impl Extension for MassGenesis {
3434
state,
3535
config,
3636
);
37-
if let Some(best_chromosome_index) = state
38-
.population
39-
.best_chromosome_index(config.fitness_ordering)
40-
{
41-
let best_chromosome = state
42-
.population
43-
.chromosomes
44-
.swap_remove(best_chromosome_index);
45-
genotype
46-
.chromosome_destructor_truncate(&mut state.population.chromosomes, 0);
47-
state
48-
.population
49-
.chromosomes
50-
.push(genotype.chromosome_cloner(&best_chromosome));
51-
state.population.chromosomes.push(best_chromosome);
52-
}
37+
38+
let mut elite_chromosomes = self.extract_elite_chromosomes(state, config, 2);
39+
let elitism_size = elite_chromosomes.len();
40+
41+
let remaining_size = 2usize.saturating_sub(elitism_size);
42+
43+
genotype.chromosome_destructor_truncate(
44+
&mut state.population.chromosomes,
45+
remaining_size,
46+
);
47+
state.population.chromosomes.append(&mut elite_chromosomes);
5348
}
5449
}
5550
state.add_duration(StrategyAction::Extension, now.elapsed());

src/population.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,22 @@ impl<C: Chromosome> Population<C> {
7777
.enumerate()
7878
.collect();
7979

80-
let index = amount.min(data.len().saturating_sub(1));
81-
let (lesser, _median, _greater) = match fitness_ordering {
82-
FitnessOrdering::Maximize => {
83-
data.select_nth_unstable_by_key(index, |(_, score)| Reverse(*score))
84-
}
85-
FitnessOrdering::Minimize => {
86-
data.select_nth_unstable_by_key(index, |(_, score)| *score)
87-
}
88-
};
89-
let mut result: Vec<usize> = lesser.iter().map(|(idx, _)| *idx).collect();
90-
result.sort_unstable();
91-
result
80+
if data.is_empty() {
81+
Vec::new()
82+
} else {
83+
let index = amount.min(data.len().saturating_sub(1));
84+
let (lesser, _median, _greater) = match fitness_ordering {
85+
FitnessOrdering::Maximize => {
86+
data.select_nth_unstable_by_key(index, |(_, score)| Reverse(*score))
87+
}
88+
FitnessOrdering::Minimize => {
89+
data.select_nth_unstable_by_key(index, |(_, score)| *score)
90+
}
91+
};
92+
let mut result: Vec<usize> = lesser.iter().map(|(idx, _)| *idx).collect();
93+
result.sort_unstable();
94+
result
95+
}
9296
}
9397

9498
pub fn age_mean(&self) -> f32 {

src/strategy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
//! let builder = StrategyBuilder::new()
4242
//! .with_genotype(genotype) // (E,H,P) the genotype
4343
//! .with_select(SelectElite::new(0.5, 0.02)) // (E) sort the chromosomes by fitness to determine crossover order. Strive to replace 50% of the population with offspring. Allow 2% through the non-generational best chromosomes gate before selection and replacement
44-
//! .with_extension(ExtensionMassExtinction::new(10, 0.1)) // (E) optional builder step, simulate cambrian explosion by mass extinction, when population cardinality drops to 10 after the selection, trim to 10% of population
44+
//! .with_extension(ExtensionMassExtinction::new(10, 0.1, 0.02)) // (E) optional builder step, simulate cambrian explosion by mass extinction, when population cardinality drops to 10 after the selection, trim to 10% of population
4545
//! .with_crossover(CrossoverUniform::new(0.7, 0.8)) // (E) crossover all individual genes between 2 chromosomes for offspring with 70% parent selection (30% do not produce offspring) and 80% chance of crossover (20% of parents just clone)
4646
//! .with_mutate(MutateSingleGene::new(0.2)) // (E) mutate offspring for a single gene with a 20% probability per chromosome
4747
//! .with_fitness(CountTrue) // (E,H,P) count the number of true values in the chromosomes

src/strategy/evolve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ pub enum EvolveVariant {
146146
/// .with_genotype(genotype)
147147
///
148148
/// .with_select(SelectElite::new(0.5, 0.02)) // sort the chromosomes by fitness to determine crossover order. Strive to replace 50% of the population with offspring. Allow 2% through the non-generational best chromosomes gate before selection and replacement
149-
/// .with_extension(ExtensionMassExtinction::new(10, 0.1)) // optional builder step, simulate cambrian explosion by mass extinction, when population cardinality drops to 10 after the selection, trim to 10% of population
149+
/// .with_extension(ExtensionMassExtinction::new(10, 0.1, 0.02)) // optional builder step, simulate cambrian explosion by mass extinction, when population cardinality drops to 10 after the selection, trim to 10% of population
150150
/// .with_crossover(CrossoverUniform::new(0.7, 0.8)) // crossover all individual genes between 2 chromosomes for offspring with 70% parent selection (30% do not produce offspring) and 80% chance of crossover (20% of parents just clone)
151151
/// .with_mutate(MutateSingleGene::new(0.2)) // mutate offspring for a single gene with a 20% probability per chromosome
152152
/// .with_fitness(CountTrue) // count the number of true values in the chromosomes

0 commit comments

Comments
 (0)