1mod builder;
3pub mod prelude;
4mod reporter;
5
6pub use self::builder::{
7 Builder as HillClimbBuilder, TryFromBuilderError as TryFromHillClimbBuilderError,
8};
9
10use super::{
11 Strategy, StrategyAction, StrategyConfig, StrategyReporter, StrategyReporterNoop,
12 StrategyState, StrategyVariant,
13};
14use crate::chromosome::{Chromosome, GenesOwner};
15use crate::fitness::{Fitness, FitnessOrdering, FitnessValue};
16use crate::genotype::{HillClimbGenotype, MutationType};
17use crate::population::Population;
18use rand::prelude::SliceRandom;
19use rand::rngs::SmallRng;
20use std::cell::RefCell;
21use std::collections::HashMap;
22use std::fmt;
23use std::time::{Duration, Instant};
24use thread_local::ThreadLocal;
25
26pub use self::reporter::Simple as HillClimbReporterSimple;
27pub use crate::strategy::reporter::Duration as HillClimbReporterDuration;
28pub use crate::strategy::reporter::Noop as HillClimbReporterNoop;
29
30#[derive(Copy, Clone, Debug, Default)]
31pub enum HillClimbVariant {
32 #[default]
33 Stochastic,
34 SteepestAscent,
35}
36
37pub struct HillClimb<
145 G: HillClimbGenotype,
146 F: Fitness<Genotype = G>,
147 SR: StrategyReporter<Genotype = G>,
148> {
149 pub genotype: G,
150 pub fitness: F,
151 pub config: HillClimbConfig,
152 pub state: HillClimbState<G>,
153 pub reporter: SR,
154 pub rng: SmallRng,
155}
156
157pub struct HillClimbConfig {
158 pub variant: HillClimbVariant,
159 pub fitness_ordering: FitnessOrdering,
160 pub par_fitness: bool,
161 pub replace_on_equal_fitness: bool,
162
163 pub target_fitness_score: Option<FitnessValue>,
164 pub max_stale_generations: Option<usize>,
165 pub valid_fitness_score: Option<FitnessValue>,
166}
167
168pub struct HillClimbState<G: HillClimbGenotype> {
170 pub current_iteration: usize,
171 pub current_generation: usize,
172 pub stale_generations: usize,
173 pub best_generation: usize,
174 pub best_fitness_score: Option<FitnessValue>,
175 pub durations: HashMap<StrategyAction, Duration>,
176 pub chromosome: Option<G::Chromosome>,
177 pub population: Population<G::Chromosome>,
178 pub current_scale_index: Option<usize>,
179}
180
181impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genotype = G>> Strategy<G>
182 for HillClimb<G, F, SR>
183{
184 fn call(&mut self) {
185 let now = Instant::now();
186 self.reporter
187 .on_enter(&self.genotype, &self.state, &self.config);
188 let mut fitness_thread_local: Option<ThreadLocal<RefCell<F>>> = None;
189 if self.config.par_fitness {
190 fitness_thread_local = Some(ThreadLocal::new());
191 }
192
193 self.setup();
194 self.reporter
195 .on_start(&self.genotype, &self.state, &self.config);
196 while !self.is_finished() {
197 self.state.current_generation += 1;
198 match self.config.variant {
199 HillClimbVariant::Stochastic => {
200 self.genotype
201 .load_best_genes(self.state.chromosome.as_mut().unwrap());
202 self.genotype.mutate_chromosome_genes(
203 1,
204 true,
205 self.state.chromosome.as_mut().unwrap(),
206 self.state.current_scale_index,
207 &mut self.rng,
208 );
209 self.fitness.call_for_state_chromosome(
210 &self.genotype,
211 &mut self.state,
212 &self.config,
213 );
214 self.state.update_best_chromosome_from_state_chromosome(
215 &mut self.genotype,
216 &self.config,
217 &mut self.reporter,
218 );
219 }
220 HillClimbVariant::SteepestAscent => {
221 self.genotype
222 .load_best_genes(self.state.chromosome.as_mut().unwrap());
223 self.genotype
224 .chromosome_destructor_truncate(&mut self.state.population.chromosomes, 0);
225 self.genotype.fill_neighbouring_population(
226 self.state.chromosome.as_ref().unwrap(),
227 &mut self.state.population,
228 self.state.current_scale_index,
229 &mut self.rng,
230 );
231 self.fitness.call_for_state_population(
232 &self.genotype,
233 &mut self.state,
234 &self.config,
235 fitness_thread_local.as_ref(),
236 );
237 self.state.update_best_chromosome_from_state_population(
238 &mut self.genotype,
239 &self.config,
240 &mut self.reporter,
241 &mut self.rng,
242 );
243 }
244 }
245 self.reporter
246 .on_new_generation(&self.genotype, &self.state, &self.config);
247 self.state.scale(&self.genotype, &self.config);
248 }
249 self.reporter
250 .on_finish(&self.genotype, &self.state, &self.config);
251 self.cleanup(fitness_thread_local.as_mut());
252 self.state.close_duration(now.elapsed());
253 self.reporter
254 .on_exit(&self.genotype, &self.state, &self.config);
255 }
256 fn best_generation(&self) -> usize {
257 self.state.best_generation
258 }
259 fn best_fitness_score(&self) -> Option<FitnessValue> {
260 self.state.best_fitness_score()
261 }
262 fn best_genes(&self) -> Option<G::Genes> {
263 if self.state.best_fitness_score().is_some() {
264 Some(self.genotype.best_genes().clone())
265 } else {
266 None
267 }
268 }
269 fn flush_reporter(&mut self, output: &mut Vec<u8>) {
270 self.reporter.flush(output);
271 }
272}
273impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genotype = G>>
274 HillClimb<G, F, SR>
275where
276 G::Chromosome: GenesOwner<Genes = G::Genes>,
277{
278 pub fn best_chromosome(&self) -> Option<G::Chromosome> {
279 if let Some(best_genes) = self.best_genes() {
280 let mut chromosome = G::Chromosome::new(best_genes);
281 chromosome.set_fitness_score(self.best_fitness_score());
282 Some(chromosome)
283 } else {
284 None
285 }
286 }
287}
288
289impl<G: HillClimbGenotype, F: Fitness<Genotype = G>> HillClimb<G, F, StrategyReporterNoop<G>> {
290 pub fn builder() -> HillClimbBuilder<G, F, StrategyReporterNoop<G>> {
291 HillClimbBuilder::new()
292 }
293}
294impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genotype = G>>
295 HillClimb<G, F, SR>
296{
297 pub fn setup(&mut self) {
298 let now = Instant::now();
299 self.genotype.chromosomes_setup();
300
301 let chromosome = self.genotype.chromosome_constructor_random(&mut self.rng);
302 self.state.chromosome = Some(chromosome);
303 self.state
304 .add_duration(StrategyAction::SetupAndCleanup, now.elapsed());
305
306 match self.config.variant {
307 HillClimbVariant::Stochastic => self.fitness.call_for_state_chromosome(
308 &self.genotype,
309 &mut self.state,
310 &self.config,
311 ),
312 HillClimbVariant::SteepestAscent => {
313 }
315 }
316
317 self.state.best_generation = self.state.current_generation;
319 self.state.best_fitness_score = self.state.chromosome.as_ref().unwrap().fitness_score();
320 self.genotype
321 .save_best_genes(self.state.chromosome.as_ref().unwrap());
322
323 self.reporter
324 .on_new_best_chromosome(&self.genotype, &self.state, &self.config);
325 }
326 pub fn cleanup(&mut self, fitness_thread_local: Option<&mut ThreadLocal<RefCell<F>>>) {
327 let now = Instant::now();
328 self.state.chromosome.take();
329 std::mem::take(&mut self.state.population.chromosomes);
330 self.genotype.chromosomes_cleanup();
331 if let Some(thread_local) = fitness_thread_local {
332 thread_local.clear();
333 }
334 self.state
335 .add_duration(StrategyAction::SetupAndCleanup, now.elapsed());
336 }
337 fn is_finished(&self) -> bool {
338 self.allow_finished_by_valid_fitness_score()
339 && (self.is_finished_by_max_stale_generations()
340 || self.is_finished_by_target_fitness_score())
341 }
342
343 fn is_finished_by_max_stale_generations(&self) -> bool {
344 if let Some(max_stale_generations) = self.config.max_stale_generations {
345 self.state.stale_generations >= max_stale_generations
346 } else {
347 false
348 }
349 }
350
351 fn is_finished_by_target_fitness_score(&self) -> bool {
352 if let Some(target_fitness_score) = self.config.target_fitness_score {
353 if let Some(fitness_score) = self.best_fitness_score() {
354 match self.config.fitness_ordering {
355 FitnessOrdering::Maximize => fitness_score >= target_fitness_score,
356 FitnessOrdering::Minimize => fitness_score <= target_fitness_score,
357 }
358 } else {
359 false
360 }
361 } else {
362 false
363 }
364 }
365
366 fn allow_finished_by_valid_fitness_score(&self) -> bool {
367 if let Some(valid_fitness_score) = self.config.valid_fitness_score {
368 if let Some(fitness_score) = self.best_fitness_score() {
369 match self.config.fitness_ordering {
370 FitnessOrdering::Maximize => fitness_score >= valid_fitness_score,
371 FitnessOrdering::Minimize => fitness_score <= valid_fitness_score,
372 }
373 } else {
374 true
375 }
376 } else {
377 true
378 }
379 }
380}
381
382impl StrategyConfig for HillClimbConfig {
383 fn fitness_ordering(&self) -> FitnessOrdering {
384 self.fitness_ordering
385 }
386 fn par_fitness(&self) -> bool {
387 self.par_fitness
388 }
389 fn replace_on_equal_fitness(&self) -> bool {
390 self.replace_on_equal_fitness
391 }
392 fn variant(&self) -> StrategyVariant {
393 StrategyVariant::HillClimb(self.variant)
394 }
395}
396
397impl<G: HillClimbGenotype> StrategyState<G> for HillClimbState<G> {
398 fn chromosome_as_ref(&self) -> &Option<G::Chromosome> {
399 &self.chromosome
400 }
401 fn population_as_ref(&self) -> &Population<G::Chromosome> {
402 &self.population
403 }
404 fn chromosome_as_mut(&mut self) -> &mut Option<G::Chromosome> {
405 &mut self.chromosome
406 }
407 fn population_as_mut(&mut self) -> &mut Population<G::Chromosome> {
408 &mut self.population
409 }
410 fn best_fitness_score(&self) -> Option<FitnessValue> {
411 self.best_fitness_score
412 }
413 fn best_generation(&self) -> usize {
414 self.best_generation
415 }
416 fn current_generation(&self) -> usize {
417 self.current_generation
418 }
419 fn current_iteration(&self) -> usize {
420 self.current_iteration
421 }
422 fn stale_generations(&self) -> usize {
423 self.stale_generations
424 }
425 fn increment_stale_generations(&mut self) {
426 self.stale_generations += 1;
427 }
428 fn reset_stale_generations(&mut self) {
429 self.stale_generations = 0;
430 }
431 fn current_scale_index(&self) -> Option<usize> {
432 self.current_scale_index
433 }
434 fn population_cardinality(&self) -> Option<usize> {
435 None
436 }
437 fn durations(&self) -> &HashMap<StrategyAction, Duration> {
438 &self.durations
439 }
440 fn add_duration(&mut self, action: StrategyAction, duration: Duration) {
441 *self.durations.entry(action).or_default() += duration;
442 }
443 fn total_duration(&self) -> Duration {
444 self.durations.values().sum()
445 }
446}
447
448impl<G: HillClimbGenotype> HillClimbState<G> {
449 fn update_best_chromosome_from_state_chromosome<SR: StrategyReporter<Genotype = G>>(
450 &mut self,
451 genotype: &mut G,
452 config: &HillClimbConfig,
453 reporter: &mut SR,
454 ) {
455 if let Some(chromosome) = self.chromosome.as_ref() {
456 let now = Instant::now();
457 match self.is_better_chromosome(
458 chromosome,
459 &config.fitness_ordering,
460 config.replace_on_equal_fitness,
461 ) {
462 (true, true) => {
463 self.best_generation = self.current_generation;
464 self.best_fitness_score = chromosome.fitness_score();
465 genotype.save_best_genes(chromosome);
466 reporter.on_new_best_chromosome(genotype, self, config);
467 self.reset_stale_generations();
468 }
469 (true, false) => {
470 genotype.save_best_genes(chromosome);
471 reporter.on_new_best_chromosome_equal_fitness(genotype, self, config);
472 self.increment_stale_generations()
473 }
474 _ => self.increment_stale_generations(),
475 }
476 self.add_duration(StrategyAction::UpdateBestChromosome, now.elapsed());
477 }
478 }
479 fn update_best_chromosome_from_state_population<SR: StrategyReporter<Genotype = G>>(
480 &mut self,
481 genotype: &mut G,
482 config: &HillClimbConfig,
483 reporter: &mut SR,
484 rng: &mut SmallRng,
485 ) {
486 let now = Instant::now();
487 if config.replace_on_equal_fitness {
488 self.population.chromosomes.shuffle(rng);
490 }
491 if let Some(contending_chromosome) =
492 self.population.best_chromosome(config.fitness_ordering)
493 {
494 match self.is_better_chromosome(
495 contending_chromosome,
496 &config.fitness_ordering,
497 config.replace_on_equal_fitness,
498 ) {
499 (true, true) => {
500 self.best_generation = self.current_generation;
501 self.best_fitness_score = contending_chromosome.fitness_score();
502 genotype.save_best_genes(contending_chromosome);
503 reporter.on_new_best_chromosome(genotype, self, config);
504 self.reset_stale_generations();
505 }
506 (true, false) => {
507 genotype.save_best_genes(contending_chromosome);
508 reporter.on_new_best_chromosome_equal_fitness(genotype, self, config);
509 self.increment_stale_generations()
510 }
511 _ => self.increment_stale_generations(),
512 }
513 } else {
514 self.increment_stale_generations();
515 }
516 self.add_duration(StrategyAction::UpdateBestChromosome, now.elapsed());
517 }
518 fn scale(&mut self, genotype: &G, config: &HillClimbConfig) {
519 if let Some(current_scale_index) = self.current_scale_index {
520 if let Some(max_stale_generations) = config.max_stale_generations {
521 if let Some(max_scale_index) = genotype.max_scale_index() {
522 if self.stale_generations >= max_stale_generations
523 && current_scale_index < max_scale_index
524 {
525 self.current_scale_index = Some(current_scale_index + 1);
526 self.reset_stale_generations();
527 }
528 }
529 }
530 }
531 }
532}
533
534impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genotype = G>>
535 TryFrom<HillClimbBuilder<G, F, SR>> for HillClimb<G, F, SR>
536{
537 type Error = TryFromHillClimbBuilderError;
538
539 fn try_from(builder: HillClimbBuilder<G, F, SR>) -> Result<Self, Self::Error> {
540 if builder.genotype.is_none() {
541 Err(TryFromHillClimbBuilderError(
542 "HillClimb requires a Genotype",
543 ))
544 } else if builder.fitness.is_none() {
545 Err(TryFromHillClimbBuilderError("HillClimb requires a Fitness"))
546 } else if builder.max_stale_generations.is_none() && builder.target_fitness_score.is_none()
547 {
548 Err(TryFromHillClimbBuilderError(
549 "HillClimb requires at least a max_stale_generations or target_fitness_score ending condition",
550 ))
551 } else {
552 let rng = builder.rng();
553 let genotype = builder.genotype.unwrap();
554 let state = HillClimbState::new(&genotype);
555
556 Ok(Self {
557 genotype,
558 fitness: builder.fitness.unwrap(),
559 config: HillClimbConfig {
560 variant: builder.variant.unwrap_or_default(),
561 fitness_ordering: builder.fitness_ordering,
562 par_fitness: builder.par_fitness,
563 max_stale_generations: builder.max_stale_generations,
564 target_fitness_score: builder.target_fitness_score,
565 valid_fitness_score: builder.valid_fitness_score,
566 replace_on_equal_fitness: builder.replace_on_equal_fitness,
567 },
568 state,
569 reporter: builder.reporter,
570 rng,
571 })
572 }
573 }
574}
575
576impl Default for HillClimbConfig {
577 fn default() -> Self {
578 Self {
579 variant: Default::default(),
580 fitness_ordering: FitnessOrdering::Maximize,
581 par_fitness: false,
582 max_stale_generations: None,
583 target_fitness_score: None,
584 valid_fitness_score: None,
585 replace_on_equal_fitness: false,
586 }
587 }
588}
589impl HillClimbConfig {
590 pub fn new() -> Self {
591 Self::default()
592 }
593}
594
595impl<G: HillClimbGenotype> HillClimbState<G> {
596 pub fn new(genotype: &G) -> Self {
597 let base = Self {
598 current_iteration: 0,
599 current_generation: 0,
600 stale_generations: 0,
601 current_scale_index: None,
602 best_generation: 0,
603 best_fitness_score: None,
604 chromosome: None,
605 population: Population::new_empty(),
606 durations: HashMap::new(),
607 };
608 match genotype.mutation_type() {
609 MutationType::Scaled => Self {
610 current_scale_index: Some(0),
611 ..base
612 },
613 MutationType::Relative => base,
614 MutationType::Random => base,
615 }
616 }
617}
618
619impl<G: HillClimbGenotype, F: Fitness<Genotype = G>, SR: StrategyReporter<Genotype = G>>
620 fmt::Display for HillClimb<G, F, SR>
621{
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 writeln!(f, "hill_climb:")?;
624 writeln!(f, " fitness: {:?}", self.fitness)?;
625 writeln!(f)?;
626
627 writeln!(f, "{}", self.config)?;
628 writeln!(f, "{}", self.state)?;
629 writeln!(f, "{}", self.genotype)
630 }
631}
632
633impl fmt::Display for HillClimbConfig {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 writeln!(f, "hill_climb_config:")?;
636 writeln!(f, " variant: {:?}", self.variant)?;
637
638 writeln!(
639 f,
640 " max_stale_generations: {:?}",
641 self.max_stale_generations
642 )?;
643 writeln!(f, " valid_fitness_score: {:?}", self.valid_fitness_score)?;
644 writeln!(f, " target_fitness_score: {:?}", self.target_fitness_score)?;
645 writeln!(f, " fitness_ordering: {:?}", self.fitness_ordering)?;
646 writeln!(f, " par_fitness: {:?}", self.par_fitness)
647 }
648}
649
650impl<G: HillClimbGenotype> fmt::Display for HillClimbState<G> {
651 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652 writeln!(f, "hill_climb_state:")?;
653 writeln!(f, " current iteration: {:?}", self.current_iteration)?;
654 writeln!(f, " current generation: {:?}", self.current_generation)?;
655 writeln!(f, " stale generations: {:?}", self.stale_generations)?;
656 writeln!(
657 f,
658 " scale index (current/max): {:?}",
659 self.current_scale_index
660 )?;
661 writeln!(f, " best fitness score: {:?}", self.best_fitness_score())
662 }
663}