Skip to content

Commit 9af7006

Browse files
committed
Add max_generations next to max_stale_generations as ending condition (both blocked by valid_fitness_score), just for completeness
1 parent 03a27be commit 9af7006

File tree

8 files changed

+134
-8
lines changed

8 files changed

+134
-8
lines changed

src/strategy.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
//! .with_target_fitness_score(0) // (E,H) ending condition if 0 times true in the best chromosome
5353
//! .with_valid_fitness_score(1) // (E,H) block ending conditions until at most a 1 times true in the best chromosome
5454
//! .with_max_stale_generations(100) // (E,H) stop searching if there is no improvement in fitness score for 100 generations
55+
//! .with_max_generations(1_000_000) // (E,H) optional, stop searching after 1M generations
5556
//! .with_max_chromosome_age(10) // (E) kill chromosomes after 10 generations
5657
//! .with_reporter(StrategyReporterSimple::new(usize::MAX)) // (E,H,P) optional builder step, report on new best chromsomes only
5758
//! .with_replace_on_equal_fitness(true) // (E,H,P) optional, defaults to false, maybe useful to avoid repeatedly seeding with the same best chromosomes after mass extinction events

src/strategy/builder.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub struct Builder<
3333
pub fitness_cache: Option<FitnessCache>,
3434
pub max_chromosome_age: Option<usize>,
3535
pub max_stale_generations: Option<usize>,
36+
pub max_generations: Option<usize>,
3637
pub mutate: Option<M>,
3738
pub par_fitness: bool,
3839
pub replace_on_equal_fitness: bool,
@@ -58,6 +59,7 @@ impl<
5859
variant: None,
5960
target_population_size: 0,
6061
max_stale_generations: None,
62+
max_generations: None,
6163
max_chromosome_age: None,
6264
target_fitness_score: None,
6365
valid_fitness_score: None,
@@ -122,6 +124,14 @@ impl<
122124
self.max_stale_generations = max_stale_generations_option;
123125
self
124126
}
127+
pub fn with_max_generations(mut self, max_generations: usize) -> Self {
128+
self.max_generations = Some(max_generations);
129+
self
130+
}
131+
pub fn with_max_generations_option(mut self, max_generations_option: Option<usize>) -> Self {
132+
self.max_generations = max_generations_option;
133+
self
134+
}
125135
pub fn with_max_chromosome_age(mut self, max_chromosome_age: usize) -> Self {
126136
self.max_chromosome_age = Some(max_chromosome_age);
127137
self
@@ -199,6 +209,7 @@ impl<
199209
variant: self.variant,
200210
target_population_size: self.target_population_size,
201211
max_stale_generations: self.max_stale_generations,
212+
max_generations: self.max_generations,
202213
max_chromosome_age: self.max_chromosome_age,
203214
target_fitness_score: self.target_fitness_score,
204215
valid_fitness_score: self.valid_fitness_score,
@@ -224,6 +235,7 @@ impl<
224235
variant: self.variant,
225236
target_population_size: self.target_population_size,
226237
max_stale_generations: self.max_stale_generations,
238+
max_generations: self.max_generations,
227239
max_chromosome_age: self.max_chromosome_age,
228240
target_fitness_score: self.target_fitness_score,
229241
valid_fitness_score: self.valid_fitness_score,
@@ -291,6 +303,7 @@ impl<
291303
genotype: self.genotype,
292304
target_population_size: self.target_population_size,
293305
max_stale_generations: self.max_stale_generations,
306+
max_generations: self.max_generations,
294307
max_chromosome_age: self.max_chromosome_age,
295308
target_fitness_score: self.target_fitness_score,
296309
valid_fitness_score: self.valid_fitness_score,
@@ -312,6 +325,7 @@ impl<
312325
genotype: self.genotype,
313326
variant: None,
314327
max_stale_generations: self.max_stale_generations,
328+
max_generations: self.max_generations,
315329
target_fitness_score: self.target_fitness_score,
316330
valid_fitness_score: self.valid_fitness_score,
317331
fitness_ordering: self.fitness_ordering,

src/strategy/evolve.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub enum EvolveVariant {
5252
/// * target_fitness_score: when the ultimate goal in terms of fitness score is known and reached
5353
/// * max_stale_generations: when the ultimate goal in terms of fitness score is unknown and one depends on some convergion
5454
/// threshold, or one wants a duration limitation next to the target_fitness_score
55+
/// * max_generations: when the ultimate goal in terms of fitness score is unknown and there is a effort constraint
5556
///
5657
/// General Hyper-parameters:
5758
/// * `replacement_rate` (selection): the target fraction of the population which exists of
@@ -156,7 +157,8 @@ pub enum EvolveVariant {
156157
/// .with_target_population_size(100) // evolve with 100 chromosomes
157158
/// .with_target_fitness_score(0) // ending condition if 0 times true in the best chromosome
158159
/// .with_valid_fitness_score(10) // block ending conditions until at most a 10 times true in the best chromosome
159-
/// .with_max_stale_generations(1000) // stop searching if there is no improvement in fitness score for 1000 generations
160+
/// .with_max_stale_generations(1000) // stop searching if there is no improvement in fitness score for 1000 generations (per scaled_range)
161+
/// .with_max_generations(1_000_000) // optional, stop searching after 1M generations
160162
/// .with_max_chromosome_age(10) // kill chromosomes after 10 generations
161163
/// .with_reporter(EvolveReporterSimple::new(100)) // optional builder step, report every 100 generations
162164
/// .with_replace_on_equal_fitness(true) // optional, defaults to false, maybe useful to avoid repeatedly seeding with the same best chromosomes after mass extinction events
@@ -202,6 +204,7 @@ pub struct EvolveConfig {
202204

203205
pub target_fitness_score: Option<FitnessValue>,
204206
pub max_stale_generations: Option<usize>,
207+
pub max_generations: Option<usize>,
205208
pub valid_fitness_score: Option<FitnessValue>,
206209
pub fitness_cache: Option<FitnessCache>,
207210

@@ -410,6 +413,7 @@ impl<
410413
fn is_finished(&self) -> bool {
411414
self.allow_finished_by_valid_fitness_score()
412415
&& (self.is_finished_by_max_stale_generations()
416+
|| self.is_finished_by_max_generations()
413417
|| self.is_finished_by_target_fitness_score())
414418
}
415419

@@ -421,6 +425,14 @@ impl<
421425
}
422426
}
423427

428+
fn is_finished_by_max_generations(&self) -> bool {
429+
if let Some(max_generations) = self.config.max_generations {
430+
self.state.current_generation >= max_generations
431+
} else {
432+
false
433+
}
434+
}
435+
424436
fn is_finished_by_target_fitness_score(&self) -> bool {
425437
if let Some(target_fitness_score) = self.config.target_fitness_score {
426438
if let Some(fitness_score) = self.best_fitness_score() {
@@ -653,10 +665,12 @@ impl<
653665
Err(TryFromEvolveBuilderError(
654666
"Evolve requires a target_population_size > 0",
655667
))
656-
} else if builder.max_stale_generations.is_none() && builder.target_fitness_score.is_none()
668+
} else if builder.max_stale_generations.is_none()
669+
&& builder.max_generations.is_none()
670+
&& builder.target_fitness_score.is_none()
657671
{
658672
Err(TryFromEvolveBuilderError(
659-
"Evolve requires at least a max_stale_generations or target_fitness_score ending condition",
673+
"Evolve requires at least a max_stale_generations, max_generations or target_fitness_score ending condition",
660674
))
661675
} else {
662676
let rng = builder.rng();
@@ -676,6 +690,7 @@ impl<
676690
config: EvolveConfig {
677691
target_population_size,
678692
max_stale_generations: builder.max_stale_generations,
693+
max_generations: builder.max_generations,
679694
max_chromosome_age: builder.max_chromosome_age,
680695
target_fitness_score: builder.target_fitness_score,
681696
valid_fitness_score: builder.valid_fitness_score,
@@ -699,6 +714,7 @@ impl Default for EvolveConfig {
699714
variant: Default::default(),
700715
target_population_size: 0,
701716
max_stale_generations: None,
717+
max_generations: None,
702718
max_chromosome_age: None,
703719
target_fitness_score: None,
704720
valid_fitness_score: None,
@@ -785,6 +801,7 @@ impl fmt::Display for EvolveConfig {
785801
" max_stale_generations: {:?}",
786802
self.max_stale_generations
787803
)?;
804+
writeln!(f, " max_generations: {:?}", self.max_generations)?;
788805
writeln!(f, " max_chromosome_age: {:?}", self.max_chromosome_age)?;
789806
writeln!(f, " valid_fitness_score: {:?}", self.valid_fitness_score)?;
790807
writeln!(f, " target_fitness_score: {:?}", self.target_fitness_score)?;

src/strategy/evolve/builder.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct Builder<
2626
pub genotype: Option<G>,
2727
pub target_population_size: usize,
2828
pub max_stale_generations: Option<usize>,
29+
pub max_generations: Option<usize>,
2930
pub max_chromosome_age: Option<usize>,
3031
pub target_fitness_score: Option<FitnessValue>,
3132
pub valid_fitness_score: Option<FitnessValue>,
@@ -50,6 +51,7 @@ impl<G: EvolveGenotype, M: Mutate, F: Fitness<Genotype = G>, S: Crossover, C: Se
5051
genotype: None,
5152
target_population_size: 0,
5253
max_stale_generations: None,
54+
max_generations: None,
5355
max_chromosome_age: None,
5456
target_fitness_score: None,
5557
valid_fitness_score: None,
@@ -109,6 +111,14 @@ impl<
109111
self.max_stale_generations = max_stale_generations_option;
110112
self
111113
}
114+
pub fn with_max_generations(mut self, max_generations: usize) -> Self {
115+
self.max_generations = Some(max_generations);
116+
self
117+
}
118+
pub fn with_max_generations_option(mut self, max_generations_option: Option<usize>) -> Self {
119+
self.max_generations = max_generations_option;
120+
self
121+
}
112122
pub fn with_max_chromosome_age(mut self, max_chromosome_age: usize) -> Self {
113123
self.max_chromosome_age = Some(max_chromosome_age);
114124
self
@@ -185,6 +195,7 @@ impl<
185195
genotype: self.genotype,
186196
target_population_size: self.target_population_size,
187197
max_stale_generations: self.max_stale_generations,
198+
max_generations: self.max_generations,
188199
max_chromosome_age: self.max_chromosome_age,
189200
target_fitness_score: self.target_fitness_score,
190201
valid_fitness_score: self.valid_fitness_score,
@@ -209,6 +220,7 @@ impl<
209220
genotype: self.genotype,
210221
target_population_size: self.target_population_size,
211222
max_stale_generations: self.max_stale_generations,
223+
max_generations: self.max_generations,
212224
max_chromosome_age: self.max_chromosome_age,
213225
target_fitness_score: self.target_fitness_score,
214226
valid_fitness_score: self.valid_fitness_score,

src/strategy/hill_climb.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub enum HillClimbVariant {
5353
/// * set to a low value for [HillClimbVariant::SteepestAscent], preferably even `1`, unless
5454
/// there is a replace_on_equal_fitness consideration or some remaining randomness in the neighbouring population (see RangeGenotype
5555
/// below)
56+
/// * max_generations: when the ultimate goal in terms of fitness score is unknown and there is a effort constraint
5657
///
5758
/// There are optional mutation distance limitations for
5859
/// [RangeGenotype](crate::genotype::RangeGenotype) and
@@ -131,7 +132,8 @@ pub enum HillClimbVariant {
131132
/// .with_par_fitness(true) // optional, defaults to false, use parallel fitness calculation
132133
/// .with_target_fitness_score(0) // ending condition if sum of genes is <= 0.00001 in the best chromosome
133134
/// .with_valid_fitness_score(100) // block ending conditions until at least the sum of genes <= 0.00100 is reached in the best chromosome
134-
/// .with_max_stale_generations(1000) // stop searching if there is no improvement in fitness score for 1000 generations
135+
/// .with_max_stale_generations(1000) // stop searching if there is no improvement in fitness score for 1000 generations (per scaled_range)
136+
/// .with_max_generations(1_000_000) // optional, stop searching after 1M generations
135137
/// .with_replace_on_equal_fitness(true) // optional, defaults to true, crucial for some type of problems with discrete fitness steps like nqueens
136138
/// .with_reporter(HillClimbReporterSimple::new(100)) // optional, report every 100 generations
137139
/// .with_rng_seed_from_u64(0) // for testing with deterministic results
@@ -164,6 +166,7 @@ pub struct HillClimbConfig {
164166

165167
pub target_fitness_score: Option<FitnessValue>,
166168
pub max_stale_generations: Option<usize>,
169+
pub max_generations: Option<usize>,
167170
pub valid_fitness_score: Option<FitnessValue>,
168171
pub fitness_cache: Option<FitnessCache>,
169172
}
@@ -340,6 +343,7 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
340343
fn is_finished(&self) -> bool {
341344
self.allow_finished_by_valid_fitness_score()
342345
&& (self.is_finished_by_max_stale_generations()
346+
|| self.is_finished_by_max_generations()
343347
|| self.is_finished_by_target_fitness_score())
344348
}
345349

@@ -351,6 +355,14 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
351355
}
352356
}
353357

358+
fn is_finished_by_max_generations(&self) -> bool {
359+
if let Some(max_generations) = self.config.max_generations {
360+
self.state.current_generation >= max_generations
361+
} else {
362+
false
363+
}
364+
}
365+
354366
fn is_finished_by_target_fitness_score(&self) -> bool {
355367
if let Some(target_fitness_score) = self.config.target_fitness_score {
356368
if let Some(fitness_score) = self.best_fitness_score() {
@@ -549,10 +561,12 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
549561
))
550562
} else if builder.fitness.is_none() {
551563
Err(TryFromHillClimbBuilderError("HillClimb requires a Fitness"))
552-
} else if builder.max_stale_generations.is_none() && builder.target_fitness_score.is_none()
564+
} else if builder.max_stale_generations.is_none()
565+
&& builder.max_generations.is_none()
566+
&& builder.target_fitness_score.is_none()
553567
{
554568
Err(TryFromHillClimbBuilderError(
555-
"HillClimb requires at least a max_stale_generations or target_fitness_score ending condition",
569+
"HillClimb requires at least a max_stale_generations, max_generations or target_fitness_score ending condition",
556570
))
557571
} else {
558572
let rng = builder.rng();
@@ -568,6 +582,7 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
568582
fitness_cache: builder.fitness_cache,
569583
par_fitness: builder.par_fitness,
570584
max_stale_generations: builder.max_stale_generations,
585+
max_generations: builder.max_generations,
571586
target_fitness_score: builder.target_fitness_score,
572587
valid_fitness_score: builder.valid_fitness_score,
573588
replace_on_equal_fitness: builder.replace_on_equal_fitness,
@@ -588,6 +603,7 @@ impl Default for HillClimbConfig {
588603
fitness_cache: None,
589604
par_fitness: false,
590605
max_stale_generations: None,
606+
max_generations: None,
591607
target_fitness_score: None,
592608
valid_fitness_score: None,
593609
replace_on_equal_fitness: false,
@@ -648,6 +664,7 @@ impl fmt::Display for HillClimbConfig {
648664
" max_stale_generations: {:?}",
649665
self.max_stale_generations
650666
)?;
667+
writeln!(f, " max_generations: {:?}", self.max_generations)?;
651668
writeln!(f, " valid_fitness_score: {:?}", self.valid_fitness_score)?;
652669
writeln!(f, " target_fitness_score: {:?}", self.target_fitness_score)?;
653670
writeln!(f, " fitness_ordering: {:?}", self.fitness_ordering)?;

src/strategy/hill_climb/builder.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct Builder<
2323
pub fitness_cache: Option<FitnessCache>,
2424
pub par_fitness: bool,
2525
pub max_stale_generations: Option<usize>,
26+
pub max_generations: Option<usize>,
2627
pub target_fitness_score: Option<FitnessValue>,
2728
pub valid_fitness_score: Option<FitnessValue>,
2829
pub replace_on_equal_fitness: bool,
@@ -42,6 +43,7 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>> Default
4243
fitness_cache: None,
4344
par_fitness: false,
4445
max_stale_generations: None,
46+
max_generations: None,
4547
target_fitness_score: None,
4648
valid_fitness_score: None,
4749
replace_on_equal_fitness: true,
@@ -103,6 +105,14 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
103105
self.max_stale_generations = max_stale_generations_option;
104106
self
105107
}
108+
pub fn with_max_generations(mut self, max_generations: usize) -> Self {
109+
self.max_generations = Some(max_generations);
110+
self
111+
}
112+
pub fn with_max_generations_option(mut self, max_generations_option: Option<usize>) -> Self {
113+
self.max_generations = max_generations_option;
114+
self
115+
}
106116
pub fn with_target_fitness_score(mut self, target_fitness_score: FitnessValue) -> Self {
107117
self.target_fitness_score = Some(target_fitness_score);
108118
self
@@ -141,6 +151,7 @@ impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genoty
141151
fitness_cache: self.fitness_cache,
142152
par_fitness: self.par_fitness,
143153
max_stale_generations: self.max_stale_generations,
154+
max_generations: self.max_generations,
144155
target_fitness_score: self.target_fitness_score,
145156
valid_fitness_score: self.valid_fitness_score,
146157
replace_on_equal_fitness: self.replace_on_equal_fitness,

tests/strategy/evolve_test.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn build_invalid_missing_ending_condition() {
2626
assert_eq!(
2727
evolve.err(),
2828
Some(TryFromEvolveBuilderError(
29-
"Evolve requires at least a max_stale_generations or target_fitness_score ending condition"
29+
"Evolve requires at least a max_stale_generations, max_generations or target_fitness_score ending condition"
3030
))
3131
);
3232
}
@@ -141,6 +141,34 @@ fn call_binary_max_stale_generations_minimize() {
141141
);
142142
}
143143

144+
#[test]
145+
fn call_binary_max_generations_maximize() {
146+
let genotype = BinaryGenotype::builder()
147+
.with_genes_size(10)
148+
.build()
149+
.unwrap();
150+
let evolve = Evolve::builder()
151+
.with_genotype(genotype)
152+
.with_target_population_size(100)
153+
.with_max_generations(50)
154+
.with_mutate(MutateSingleGene::new(0.1))
155+
.with_fitness(CountTrue)
156+
.with_crossover(CrossoverSingleGene::new(0.7, 0.8))
157+
.with_select(SelectTournament::new(0.5, 0.02, 4))
158+
.with_extension(ExtensionNoop::new())
159+
.with_reporter(StrategyReporterNoop::new())
160+
.with_rng_seed_from_u64(0)
161+
.call()
162+
.unwrap();
163+
164+
println!("{:#?}", evolve.best_genes());
165+
assert_eq!(evolve.best_fitness_score(), Some(10));
166+
assert_eq!(
167+
evolve.best_genes().unwrap(),
168+
vec![true, true, true, true, true, true, true, true, true, true]
169+
);
170+
}
171+
144172
#[test]
145173
fn call_binary_max_stale_generations_and_valid_fitness_score_maximize() {
146174
let genotype = BinaryGenotype::builder()

0 commit comments

Comments
 (0)