genetic_algorithm/strategy/
hill_climb.rs

1//! A solution strategy for finding the best chromosome, when search space is convex with little local optima or crossover is impossible or inefficient
2mod 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
37/// The HillClimb strategy is an iterative algorithm that starts with a single arbitrary solution
38/// to a problem (unless the genotype seeds specific genes to sample a single starting point from),
39/// then attempts to find a better solution by making an incremental change to the solution
40///
41/// There are 2 variants:
42/// * [HillClimbVariant::Stochastic]: does not examine all neighbors before deciding how to move.
43///   Rather, it selects a neighbor at random, and decides (based on the improvement in that
44///   neighbour) whether to move to that neighbor or to examine another
45/// * [HillClimbVariant::SteepestAscent]: all neighbours are compared and the one with the best
46///   improvement is chosen.
47///
48/// The ending conditions are one or more of the following:
49/// * target_fitness_score: when the ultimate goal in terms of fitness score is known and reached
50/// * max_stale_generations: when the ultimate goal in terms of fitness score is unknown and one depends on some convergion
51///   threshold, or one wants a duration limitation next to the target_fitness_score.
52///   * set to a high value for [HillClimbVariant::Stochastic]
53///   * set to a low value for [HillClimbVariant::SteepestAscent], preferably even `1`, unless
54///   there is a replace_on_equal_fitness consideration or some remaining randomness in the neighbouring population (see RangeGenotype
55///   below)
56///
57/// There are optional mutation distance limitations for
58/// [RangeGenotype](crate::genotype::RangeGenotype) and
59/// [MultiRangeGenotype](crate::genotype::MultiRangeGenotype) neighbouring chromosomes. Listed in
60/// descending priority:
61/// * With allele_mutation_scaled_range(s) set on genotype:
62///     * Mutation distance only on edges of current scale (e.g. -1 and +1 for -1..-1 scale)
63///         * Pick random edge for [HillClimbVariant::Stochastic]
64///         * Take both edges per gene for [HillClimbVariant::SteepestAscent]
65///     * Scale down after max_stale_generations is reached and reset stale_generations to zero
66///     * Only trigger max_stale_generations ending condition when already reached the smallest scale
67///     * max_stale_generations could be set to 1, as there is no remaining randomness
68/// * With allele_mutation_range(s) set on genotype:
69///     * Mutation distance taken uniformly from mutation range
70///         * Sample single random value for [HillClimbVariant::Stochastic]
71///         * Ensure to sample both a higer and lower value per gene for [HillClimbVariant::SteepestAscent]
72///     * Standard max_stale_generations ending condition
73///     * max_stale_generations should be set somewhat higher than 1 as there is some remaining randomness
74/// * With only allele_range(s) set on genotype (not advised for hill climbing):
75///     * Mutate uniformly over the complete allele range
76///         * Sample single random value for [HillClimbVariant::Stochastic]
77///         * Ensure to sample both a higer and lower value per gene for [HillClimbVariant::SteepestAscent]
78///     * Standard max_stale_generations ending condition
79///     * max_stale_generations should be set substantially higher than 1 as there is a lot remaining randomness
80///
81/// There are reporting hooks in the loop receiving the [HillClimbState], which can by handled by an
82/// [StrategyReporter] (e.g. [HillClimbReporterDuration], [HillClimbReporterSimple]). But you are encouraged to
83/// roll your own, see [StrategyReporter].
84///
85/// From the [HillClimbBuilder] level, there are several calling mechanisms:
86/// * [call](HillClimbBuilder::call): this runs a single [HillClimb] strategy
87/// * [call_repeatedly](HillClimbBuilder::call_repeatedly): this runs multiple independent [HillClimb]
88///   strategies and returns the best one (or short circuits when the target_fitness_score is
89///   reached)
90/// * [call_par_repeatedly](HillClimbBuilder::call_par_repeatedly): this runs multiple independent
91///   [HillClimb] strategies in parallel and returns the best one (or short circuits when the
92///   target_fitness_score is reached). This is separate and independent from the
93///   `with_par_fitness()` flag on the builder, which determines multithreading of the fitness
94///   calculation inside the [HillClimb] strategy. Both can be combined.
95///
96/// Multithreading inside the [HillClimbVariant::Stochastic] using the `with_par_fitness()` builder
97/// step does nothing, due to the sequential nature of the search. But
98/// [call_par_repeatedly](HillClimbBuilder::call_par_repeatedly) still effectively multithreads for
99/// these variants as the sequential nature is only internal to the [HillClimb] strategy.
100///
101/// All multithreading mechanisms are implemented using [rayon::iter] and [std::sync::mpsc].
102///
103/// See [HillClimbBuilder] for initialization options.
104///
105/// Example:
106/// ```
107/// use genetic_algorithm::strategy::hill_climb::prelude::*;
108/// use genetic_algorithm::fitness::placeholders::SumGenes;
109///
110/// // the search space
111/// let genotype = RangeGenotype::builder()     // f32 alleles
112///     .with_genes_size(16)                    // 16 genes
113///     .with_allele_range(0.0..=1.0)           // allow gene values between 0.0 and 1.0
114///     .with_allele_mutation_range(-0.1..=0.1) // neighbouring step size randomly sampled from range
115///     .with_allele_mutation_scaled_range(vec![
116///       -0.1..=0.1,
117///       -0.01..=0.01,
118///       -0.001..=0.001
119///      ]) // neighbouring step size equal to start/end of each scaled range
120///     .build()
121///     .unwrap();
122///
123/// // the search strategy
124/// let hill_climb = HillClimb::builder()
125///     .with_genotype(genotype)
126///     .with_variant(HillClimbVariant::SteepestAscent)   // check all neighbours for each round
127///     .with_fitness(SumGenes::new_with_precision(1e-5)) // sum the gene values of the chromosomes with precision 0.00001, which means multiply fitness score (isize) by 100_000
128///     .with_fitness_ordering(FitnessOrdering::Minimize) // aim for the lowest sum
129///     .with_par_fitness(true)                           // optional, defaults to false, use parallel fitness calculation
130///     .with_target_fitness_score(0)                     // ending condition if sum of genes is <= 0.00001 in the best chromosome
131///     .with_valid_fitness_score(100)                    // block ending conditions until at least the sum of genes <= 0.00100 is reached in the best chromosome
132///     .with_max_stale_generations(1000)                 // stop searching if there is no improvement in fitness score for 1000 generations
133///     .with_replace_on_equal_fitness(true)              // optional, defaults to true, crucial for some type of problems with discrete fitness steps like nqueens
134///     .with_reporter(HillClimbReporterSimple::new(100)) // optional, report every 100 generations
135///     .with_rng_seed_from_u64(0)                        // for testing with deterministic results
136///     .call()
137///     .unwrap();
138///
139/// // it's all about the best genes after all
140/// let (best_genes, best_fitness_score) = hill_climb.best_genes_and_fitness_score().unwrap();
141/// assert_eq!(best_genes.into_iter().map(|v| v <= 1e-3).collect::<Vec<_>>(), vec![true; 16]);
142/// assert_eq!(best_fitness_score, 0);
143/// ```
144pub 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
168/// Stores the state of the HillClimb strategy.
169pub 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                // skip so calculate_for_chromosome does not have to be implemented on Fitness
314            }
315        }
316
317        // best by definition
318        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            // shuffle, so we don't repeatedly take the same best chromosome in sideways move
489            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}