Skip to content

Commit 6b3c73f

Browse files
committed
Move evolve lifecycle steps into before and after plugin hooks
Update crossover tests to reflect this added responsibility as well
1 parent 5788b15 commit 6b3c73f

18 files changed

Lines changed: 152 additions & 68 deletions

src/crossover.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
//! High values converge faster, but risk losing good solutions. Low values have poor exploration
1515
//! and risk of premature convergence
1616
//!
17-
//! Normally the crossover adds children to the popluation, thus increasing the population_size
18-
//! above the target_population_size. Selection will reduce this again in the next generation
17+
//! As crossover adds offspring (with age zero) it is also the responsibility of the crossover to
18+
//! increment the age of the parents. This is done because the definition of "offspring" is "age
19+
//! zero". So this should be done in-sync and is therefore not a generational loop concern of Evolve.
20+
//!
21+
//! Normally the crossover adds children to the population, thus increasing the population_size
22+
//! above the target_population_size. Selection will reduce this again in the next generation.
1923
mod clone;
2024
mod multi_gene;
2125
mod multi_point;
@@ -79,7 +83,9 @@ pub type CrossoverAllele<C> = <<C as Crossover>::Genotype as Genotype>::Allele;
7983
/// let selected_population_size =
8084
/// (existing_population_size as f32 * self.selection_rate).ceil() as usize;
8185
///
82-
/// // Important!!! Append offspring as recycled clones from parents (will crossover later)
86+
/// // Important!!! Increment age, as the old offspring now become parents
87+
/// state.population.increment_age();
88+
/// // Important!!! Append offspring as recycled clones from parents (will reset age and crossover later)
8389
/// // Use population's methods for safe chromosome recycling
8490
/// state.population.extend_from_within(selected_population_size);
8591
///
@@ -97,7 +103,7 @@ pub type CrossoverAllele<C> = <<C as Crossover>::Genotype as Genotype>::Allele;
97103
/// std::mem::swap(&mut offspring1.genes[even_index], &mut offspring2.genes[even_index]);
98104
/// // MultiRangeGenotype specific methods are available if needed
99105
/// }
100-
/// // Important!!! Remember to reset the chromosome metadata after manipulation
106+
/// // Important!!! Remember to reset the chromosome metadata after manipulation (also resets age)
101107
/// offspring1.reset_metadata(genotype.genes_hashing);
102108
/// offspring2.reset_metadata(genotype.genes_hashing);
103109
/// }
@@ -118,6 +124,15 @@ pub type CrossoverAllele<C> = <<C as Crossover>::Genotype as Genotype>::Allele;
118124
pub trait Crossover: Clone + Send + Sync + std::fmt::Debug {
119125
type Genotype: EvolveGenotype;
120126

127+
fn before(
128+
&mut self,
129+
_genotype: &Self::Genotype,
130+
_state: &mut EvolveState<Self::Genotype>,
131+
_config: &EvolveConfig,
132+
) {
133+
// Default no-op implementation
134+
}
135+
121136
fn call<R: Rng, SR: StrategyReporter<Genotype = Self::Genotype>>(
122137
&mut self,
123138
genotype: &Self::Genotype,
@@ -127,6 +142,16 @@ pub trait Crossover: Clone + Send + Sync + std::fmt::Debug {
127142
rng: &mut R,
128143
);
129144

145+
/// Optionally update population cardinality after crossover, disabled by default
146+
fn after(
147+
&mut self,
148+
_genotype: &Self::Genotype,
149+
_state: &mut EvolveState<Self::Genotype>,
150+
_config: &EvolveConfig,
151+
) {
152+
// state.update_population_cardinality(genotype, config);
153+
}
154+
130155
/// to guard against invalid Crossover strategies which break the internal consistency
131156
/// of the genes, unique genotypes can't simply exchange genes without gene duplication issues
132157
fn require_crossover_indexes(&self) -> bool {

src/crossover/clone.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ impl<G: EvolveGenotype> Crossover for Clone<G> {
2828
let existing_population_size = state.population.chromosomes.len();
2929
let selected_population_size =
3030
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
31+
32+
state.population.increment_age();
3133
state
3234
.population
3335
.extend_from_within(selected_population_size);

src/crossover/multi_gene.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ impl<G: EvolveGenotype> Crossover for MultiGene<G> {
3939
let existing_population_size = state.population.chromosomes.len();
4040
let selected_population_size =
4141
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
42+
state.population.increment_age();
4243
state
4344
.population
4445
.extend_from_within(selected_population_size);

src/crossover/multi_point.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ impl<G: EvolveGenotype> Crossover for MultiPoint<G> {
3939
let existing_population_size = state.population.chromosomes.len();
4040
let selected_population_size =
4141
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
42+
state.population.increment_age();
4243
state
4344
.population
4445
.extend_from_within(selected_population_size);

src/crossover/rejuvenate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ impl<G: EvolveGenotype> Crossover for Rejuvenate<G> {
3333
let dropped_population_size = (existing_population_size - selected_population_size).max(0);
3434

3535
state.population.truncate(selected_population_size);
36+
state.population.increment_age();
3637
state.population.extend_from_within(dropped_population_size);
3738

3839
state

src/crossover/single_gene.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl<G: EvolveGenotype> Crossover for SingleGene<G> {
3636
let existing_population_size = state.population.chromosomes.len();
3737
let selected_population_size =
3838
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
39+
state.population.increment_age();
3940
state
4041
.population
4142
.extend_from_within(selected_population_size);

src/crossover/single_point.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl<G: EvolveGenotype> Crossover for SinglePoint<G> {
3535
let existing_population_size = state.population.chromosomes.len();
3636
let selected_population_size =
3737
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
38+
state.population.increment_age();
3839
state
3940
.population
4041
.extend_from_within(selected_population_size);

src/crossover/uniform.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl<G: EvolveGenotype> Crossover for Uniform<G> {
3838
let existing_population_size = state.population.chromosomes.len();
3939
let selected_population_size =
4040
(existing_population_size as f32 * self.selection_rate).ceil() as usize;
41+
state.population.increment_age();
4142
state
4243
.population
4344
.extend_from_within(selected_population_size);

src/mutate.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ pub type MutateAllele<M> = <<M as Mutate>::Genotype as Genotype>::Allele;
8989
pub trait Mutate: Clone + Send + Sync + std::fmt::Debug {
9090
type Genotype: EvolveGenotype;
9191

92+
fn before(
93+
&mut self,
94+
_genotype: &Self::Genotype,
95+
_state: &mut EvolveState<Self::Genotype>,
96+
_config: &EvolveConfig,
97+
) {
98+
// Default no-op implementation
99+
}
100+
92101
fn call<R: Rng, SR: StrategyReporter<Genotype = Self::Genotype>>(
93102
&mut self,
94103
genotype: &Self::Genotype,
@@ -97,6 +106,16 @@ pub trait Mutate: Clone + Send + Sync + std::fmt::Debug {
97106
reporter: &mut SR,
98107
rng: &mut R,
99108
);
109+
110+
/// Optionally update population cardinality after crossover, disabled by default
111+
fn after(
112+
&mut self,
113+
_genotype: &Self::Genotype,
114+
_state: &mut EvolveState<Self::Genotype>,
115+
_config: &EvolveConfig,
116+
) {
117+
// state.update_population_cardinality(genotype, config);
118+
}
100119
}
101120

102121
#[derive(Clone, Debug)]

src/select.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ pub type SelectAllele<S> = <<S as Select>::Genotype as Genotype>::Allele;
9898
pub trait Select: Clone + Send + Sync + std::fmt::Debug {
9999
type Genotype: EvolveGenotype;
100100

101+
/// filter by age, is basic first selection step, always applied as before hook
102+
fn before(
103+
&mut self,
104+
_genotype: &Self::Genotype,
105+
state: &mut EvolveState<Self::Genotype>,
106+
config: &EvolveConfig,
107+
) {
108+
if let Some(max_chromosome_age) = config.max_chromosome_age {
109+
// TODO: use something like partition_in_place when stable
110+
for i in (0..state.population.chromosomes.len()).rev() {
111+
if state.population.chromosomes[i].age() >= max_chromosome_age {
112+
let chromosome = state.population.chromosomes.swap_remove(i);
113+
state.population.drop_chromosome(chromosome);
114+
}
115+
}
116+
}
117+
}
118+
101119
fn call<R: Rng, SR: StrategyReporter<Genotype = Self::Genotype>>(
102120
&mut self,
103121
genotype: &Self::Genotype,
@@ -107,6 +125,17 @@ pub trait Select: Clone + Send + Sync + std::fmt::Debug {
107125
rng: &mut R,
108126
);
109127

128+
/// Optionally update population cardinality after crossover, enabled by default as the main
129+
/// interest in population cardinality is after the selection phase
130+
fn after(
131+
&mut self,
132+
genotype: &Self::Genotype,
133+
state: &mut EvolveState<Self::Genotype>,
134+
config: &EvolveConfig,
135+
) {
136+
state.update_population_cardinality(genotype, config);
137+
}
138+
110139
fn extract_elite_chromosomes(
111140
&self,
112141
state: &mut EvolveState<Self::Genotype>,

0 commit comments

Comments
 (0)