Skip to content

Commit 7e6a61e

Browse files
committed
Reduce usage of more precise best_unique_chromosome_indices in
ExtensionMassExtinction and ExtensionMassDegeneration, as uniqueness limits the amount of selected elites, which seems unwanted behaviour
1 parent a5d4763 commit 7e6a61e

File tree

11 files changed

+62
-44
lines changed

11 files changed

+62
-44
lines changed

src/extension.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ pub trait Extension: Clone + Send + Sync + std::fmt::Debug {
3333
rng: &mut R,
3434
);
3535

36-
fn extract_unique_elite_chromosomes<G: EvolveGenotype>(
36+
fn extract_elite_chromosomes<G: EvolveGenotype>(
3737
&self,
3838
_genotype: &mut G,
3939
state: &mut EvolveState<G>,
4040
config: &EvolveConfig,
4141
elitism_size: usize,
4242
) -> Vec<G::Chromosome> {
43-
let mut elite_chromosomes: Vec<G::Chromosome> = Vec::with_capacity(elitism_size);
43+
let mut elite_chromosomes: Vec<G::Chromosome> = Vec::new();
4444
for index in state
4545
.population
46-
.best_unique_chromosome_indices(elitism_size, config.fitness_ordering)
46+
.best_chromosome_indices(elitism_size, config.fitness_ordering)
4747
.into_iter()
4848
.rev()
4949
{
@@ -53,6 +53,26 @@ pub trait Extension: Clone + Send + Sync + std::fmt::Debug {
5353
elite_chromosomes
5454
}
5555

56+
fn extract_unique_elite_chromosomes<G: EvolveGenotype>(
57+
&self,
58+
_genotype: &mut G,
59+
state: &mut EvolveState<G>,
60+
config: &EvolveConfig,
61+
elitism_size: usize,
62+
) -> Vec<G::Chromosome> {
63+
let mut unique_elite_chromosomes: Vec<G::Chromosome> = Vec::new();
64+
for index in state
65+
.population
66+
.best_unique_chromosome_indices(elitism_size, config.fitness_ordering)
67+
.into_iter()
68+
.rev()
69+
{
70+
let chromosome = state.population.chromosomes.swap_remove(index);
71+
unique_elite_chromosomes.push(chromosome);
72+
}
73+
unique_elite_chromosomes
74+
}
75+
5676
fn extract_unique_chromosomes<G: EvolveGenotype>(
5777
&self,
5878
_genotype: &mut G,

src/extension/mass_degeneration.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ 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
/// mutated the provided number of times, where the [Genotype](crate::genotype::Genotype)
11-
/// determines whether this is random, relative or scaled.
12-
/// The elitism_rate ensures the passing of the best chromosomes before mutations are applied.
11+
/// determines whether this is random, relative or scaled. The elitism_rate ensures the passing of
12+
/// the best chromosomes before mutations are applied (doesn't care about best chromosome
13+
/// uniqueness).
1314
///
1415
/// Duplicate mutations of the same gene are allowed. There is no change in population size.
1516
#[derive(Debug, Clone)]
@@ -43,12 +44,8 @@ impl Extension for MassDegeneration {
4344
let elitism_size = ((population_size as f32 * self.elitism_rate).ceil()
4445
as usize)
4546
.min(population_size);
46-
let mut elite_chromosomes = self.extract_unique_elite_chromosomes(
47-
genotype,
48-
state,
49-
config,
50-
elitism_size,
51-
);
47+
let mut elite_chromosomes =
48+
self.extract_elite_chromosomes(genotype, state, config, elitism_size);
5249
let elitism_size = elite_chromosomes.len();
5350

5451
for chromosome in state.population.chromosomes.iter_mut() {

src/extension/mass_extinction.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use std::time::Instant;
77

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
10-
/// 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.
10+
/// randomly reduced regardless of fitness using the survival_rate (fraction of population). The
11+
/// elitism_rate ensures the passing of the best chromosomes before random reduction starts
12+
/// (doesn't care about best chromosome uniqueness).
1213
///
1314
/// Population will recover in the following generations
1415
#[derive(Debug, Clone)]
@@ -42,12 +43,8 @@ impl Extension for MassExtinction {
4243
let elitism_size = ((population_size as f32 * self.elitism_rate).ceil()
4344
as usize)
4445
.min(population_size);
45-
let mut elite_chromosomes = self.extract_unique_elite_chromosomes(
46-
genotype,
47-
state,
48-
config,
49-
elitism_size,
50-
);
46+
let mut elite_chromosomes =
47+
self.extract_elite_chromosomes(genotype, state, config, elitism_size);
5148
let elitism_size = elite_chromosomes.len();
5249

5350
let remaining_size: usize = ((population_size as f32 * self.survival_rate)

src/extension/mass_genesis.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use crate::strategy::{StrategyAction, StrategyState};
66
use rand::Rng;
77
use std::time::Instant;
88

9-
/// A version of [MassExtinction](crate::extension::ExtensionMassExtinction), where only an adam
10-
/// and eve of current best chromosomes survive
9+
/// A version of [MassExtinction](crate::extension::ExtensionMassExtinction), where only an Adam
10+
/// and Eve of current best chromosomes survive. Tries to select distinct Adem and Eve when
11+
/// genes_hash is stored on chromosome, otherwise it will just take 2 of the best (possibly
12+
/// dupicates).
1113
///
1214
/// Population will recover in the following generations
1315
#[derive(Debug, Clone)]
@@ -35,11 +37,14 @@ impl Extension for MassGenesis {
3537
config,
3638
);
3739

38-
let mut elite_chromosomes =
39-
self.extract_unique_elite_chromosomes(genotype, state, config, 2);
40+
let mut elite_chromosomes = if genotype.genes_hashing() {
41+
self.extract_unique_elite_chromosomes(genotype, state, config, 2)
42+
} else {
43+
self.extract_elite_chromosomes(genotype, state, config, 2)
44+
};
4045
let elitism_size = elite_chromosomes.len();
41-
4246
let remaining_size = 2usize.saturating_sub(elitism_size);
47+
4348
genotype.chromosome_destructor_truncate(
4449
&mut state.population.chromosomes,
4550
remaining_size,

src/population.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ impl<C: Chromosome> Population<C> {
6565
}
6666
}
6767

68-
// In summary a bit quirky, but fast and doesn't require genes_hashing.
69-
// Doesn't matter the amount should be much less than the population size.
68+
// In summary a bit quirky, but fast and doesn't require genes_hashing,
69+
// which doesn't matter as the amount should be much less than the population size (usage in elitism_rate)
70+
//
7071
// Returns one less than total size with known fitness due to implementation constraints.
7172
// Does not care about uniqueness of the genes_hash.
7273
pub fn best_chromosome_indices(
@@ -115,8 +116,7 @@ impl<C: Chromosome> Population<C> {
115116
}
116117

117118
// Only works when genes_hash is stored on chromosome, as this is the uniqueness key.
118-
// Assume chromosomes sorted by fitness, takes the first index occurence of a genes_hash
119-
// Returns indices in ascending order (irrespective of fitness)
119+
// Takes the first index occurence of a genes_hash. Returns indices in ascending order (irrespective of fitness)
120120
pub fn best_unique_chromosome_indices(
121121
&self,
122122
amount: usize,

src/strategy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
//! // the search space
3434
//! let genotype = BinaryGenotype::builder()
3535
//! .with_genes_size(10)
36-
//! .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache, optional for better population cardinality estimation)
36+
//! .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache and deduplication extension, optional for better population cardinality estimation)
3737
//! .build()
3838
//! .unwrap();
3939
//!

src/strategy/evolve.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub enum EvolveVariant {
138138
/// // the search space
139139
/// let genotype = BinaryGenotype::builder() // boolean alleles
140140
/// .with_genes_size(100) // 100 genes per chromosome
141-
/// .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache, optional for better population cardinality estimation)
141+
/// .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache and deduplication extension, optional for better population cardinality estimation)
142142
/// .build()
143143
/// .unwrap();
144144
///
@@ -147,7 +147,7 @@ pub enum EvolveVariant {
147147
/// .with_genotype(genotype)
148148
///
149149
/// .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
150-
/// .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
150+
/// .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
151151
/// .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)
152152
/// .with_mutate(MutateSingleGene::new(0.2)) // mutate offspring for a single gene with a 20% probability per chromosome
153153
/// .with_fitness(CountTrue) // count the number of true values in the chromosomes

src/strategy/hill_climb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub enum HillClimbVariant {
111111
/// // the search space
112112
/// let genotype = RangeGenotype::builder() // f32 alleles
113113
/// .with_genes_size(16) // 16 genes
114-
/// .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache)
114+
/// .with_genes_hashing(true) // store genes_hash on chromosome (required for fitness_cache and deduplication extension)
115115
/// .with_allele_range(0.0..=1.0) // allow gene values between 0.0 and 1.0
116116
/// .with_allele_mutation_range(-0.1..=0.1) // neighbouring step size randomly sampled from range
117117
/// .with_allele_mutation_scaled_range(vec![

tests/extension/mass_deduplication_test.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,9 @@ fn never_leaves_less_than_two() {
104104
}
105105

106106
#[test]
107-
fn never_removes_all_if_no_genes_hash() {
107+
fn skips_execution_if_no_genes_hash() {
108108
let mut genotype = BinaryGenotype::builder()
109109
.with_genes_size(3)
110-
.with_genes_hashing(true)
111110
.build()
112111
.unwrap();
113112

@@ -125,8 +124,8 @@ fn never_removes_all_if_no_genes_hash() {
125124
assert_eq!(population.chromosomes.capacity(), 10);
126125

127126
let mut state = EvolveState::new(&genotype);
128-
assert_eq!(population.genes_cardinality(), None); // trigger, because no cardinality if no genes hashes
129-
state.population_cardinality = population.genes_cardinality();
127+
assert_eq!(population.genes_cardinality(), None);
128+
state.population_cardinality = Some(2); // hard trigger, because no cardinality if no genes hashes
130129
state.population = population;
131130
let config = EvolveConfig::new();
132131
let mut reporter = StrategyReporterNoop::new();

tests/extension/mass_degeneration_test.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn degenerates_randomly() {
4040
let config = EvolveConfig::new();
4141
let mut reporter = StrategyReporterNoop::new();
4242
let mut rng = SmallRng::seed_from_u64(0);
43-
ExtensionMassDegeneration::new(3, 2, 0.25).call(
43+
ExtensionMassDegeneration::new(3, 2, 0.33).call(
4444
&mut genotype,
4545
&mut state,
4646
&config,
@@ -54,13 +54,13 @@ fn degenerates_randomly() {
5454
// elite
5555
(vec![true, true, false], Some(1)),
5656
(vec![true, false, false], Some(2)),
57-
// others
58-
(vec![true, true, false], None),
57+
(vec![true, false, false], Some(2)),
58+
// normal
5959
(vec![true, true, true], None),
6060
(vec![true, false, true], None),
61-
(vec![false, false, true], None),
62-
(vec![true, false, false], None),
63-
(vec![true, true, true], None)
61+
(vec![true, true, true], None),
62+
(vec![true, false, true], None),
63+
(vec![true, true, true], None),
6464
]
6565
);
6666
assert_eq!(state.population.chromosomes.capacity(), 10);

0 commit comments

Comments
 (0)