Struct genetic_algorithm::strategy::hill_climb::HillClimb

source ·
pub struct HillClimb<G: IncrementalGenotype, F: Fitness<Allele = G::Allele>, SR: HillClimbReporter<Allele = G::Allele>> {
    pub genotype: G,
    pub fitness: F,
    pub config: HillClimbConfig,
    pub state: HillClimbState<G::Allele>,
    pub reporter: SR,
    pub rng: SmallRng,
}
Expand description

The HillClimb strategy is an iterative algorithm that starts with an arbitrary solution to a problem, then attempts to find a better solution by making an incremental change to the solution

There are 4 variants:

  • HillClimbVariant::Stochastic: does not examine all neighbors before deciding how to move. Rather, it selects a neighbor at random, and decides (based on the improvement in that neighbour) whether to move to that neighbor or to examine another
  • HillClimbVariant::SteepestAscent: all neighbours are compared and the one with the best improvement is chosen.
  • HillClimbVariant::StochasticSecondary: like Stochastic, but also randomly tries a random neighbour of the neighbour. Useful when a single mutation would generally not lead to improvement, because the problem space behaves more like a UniqueGenotype where genes must be swapped (but the UniqueGenotype doesn’t map to the problem space well)
  • HillClimbVariant::SteepestAscentSecondary: like SteepestAscent, but also neighbours of neighbours are in scope. This is O(n^2) with regards to the SteepestAscent variant, so use with caution.

The ending conditions are one or more of the following:

  • target_fitness_score: when the ultimate goal in terms of fitness score is known and reached
  • max_stale_generations: when the ultimate goal in terms of fitness score is unknown and one depends on some convergion threshold, or one wants a duration limitation next to the target_fitness_score

There are optional mutation distance limitations for RangeGenotype and MultiRangeGenotype neighbouring chromosomes. Listed in descending priority:

  • With allele_mutation_scaled_range(s) set on genotype:
    • Mutation distance only on edges of current scale (e.g. -1 and +1 for -1..-1 scale)
    • Scale down after max_stale_generations is reached and reset stale_generations to zero
    • Only trigger max_stale_generations ending condition when already reached the smallest scale
  • With allele_mutation_range(s) set on genotype:
  • With only allele_range(s) set on genotype:

Using scaling for HillClimbVariant::StochasticSecondary and HillClimbVariant::SteepestAscentSecondary doesn’t make sense, though it will work.

There are reporting hooks in the loop receiving the HillClimbState, which can by handled by an HillClimbReporter (e.g. HillClimbReporterNoop, HillClimbReporterSimple). But you are encouraged to roll your own, see HillClimbReporter.

From the HillClimbBuilder level, there are several calling mechanisms:

  • call: this runs a single HillClimb strategy
  • call_repeatedly: this runs multiple independent HillClimb strategies and returns the best one (or short circuits when the target_fitness_score is reached)
  • call_par_repeatedly: this runs multiple independent HillClimb strategies in parallel and returns the best one (or short circuits when the target_fitness_score is reached). This is separate and independent from the with_par_fitness() flag on the builder, which determines multithreading of the fitness calculation inside the HillClimb strategy. Both can be combined.

Multithreading inside the HillClimbVariant::Stochastic and HillClimbVariant::StochasticSecondary using the with_par_fitness() builder step does nothing, due to the sequential nature of the search. But call_par_repeatedly still effectively multithreads for these variants as the sequential nature is only internal to the HillClimb strategy.

All multithreading mechanisms are implemented using rayon::iter and std::sync::mpsc.

See HillClimbBuilder for initialization options.

Example:

use genetic_algorithm::strategy::hill_climb::prelude::*;
use genetic_algorithm::fitness::placeholders::SumGenes;

// the search space
let genotype = RangeGenotype::builder()     // f32 alleles
    .with_genes_size(16)                    // 16 genes
    .with_allele_range(0.0..=1.0)           // allow gene values between 0.0 and 1.0
    .with_allele_mutation_range(-0.1..=0.1) // neighbouring step size randomly sampled from range
    .with_allele_mutation_scaled_range(vec![
      -0.1..=0.1,
      -0.01..=0.01,
      -0.001..=0.001
     ]) // neighbouring step size equal to start/end of each scaled range
    .build()
    .unwrap();

// the search strategy
let hill_climb = HillClimb::builder()
    .with_genotype(genotype)
    .with_variant(HillClimbVariant::SteepestAscent)   // check all neighbours for each round
    .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
    .with_fitness_ordering(FitnessOrdering::Minimize) // aim for the lowest sum
    .with_par_fitness(true)                           // optional, defaults to false, use parallel fitness calculation
    .with_target_fitness_score(10)                    // ending condition if sum of genes is <= 0.00010 in the best chromosome
    .with_valid_fitness_score(100)                    // block ending conditions until at least the sum of genes <= 0.00100 is reached in the best chromosome
    .with_max_stale_generations(1000)                 // stop searching if there is no improvement in fitness score for 1000 generations
    .with_replace_on_equal_fitness(true)              // optional, defaults to true, crucial for some type of problems with discrete fitness steps like nqueens
    .with_reporter(HillClimbReporterSimple::new(100)) // optional, report every 100 generations
    .with_rng_seed_from_u64(0)                        // for testing with deterministic results
    .call()
    .unwrap();

// it's all about the best chromosome after all
let best_chromosome = hill_climb.best_chromosome().unwrap();
assert_eq!(best_chromosome.genes.into_iter().map(|v| v <= 1e-3).collect::<Vec<_>>(), vec![true; 16])

Fields§

§genotype: G§fitness: F§config: HillClimbConfig§state: HillClimbState<G::Allele>§reporter: SR§rng: SmallRng

Implementations§

Trait Implementations§

source§

impl<G: IncrementalGenotype, F: Fitness<Allele = G::Allele>, SR: HillClimbReporter<Allele = G::Allele>> Display for HillClimb<G, F, SR>

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
source§

impl<G: IncrementalGenotype, F: Fitness<Allele = G::Allele>, SR: HillClimbReporter<Allele = G::Allele>> Strategy<G> for HillClimb<G, F, SR>

source§

impl<G: IncrementalGenotype, F: Fitness<Allele = G::Allele>, SR: HillClimbReporter<Allele = G::Allele>> TryFrom<Builder<G, F, SR>> for HillClimb<G, F, SR>

§

type Error = TryFromBuilderError

The type returned in the event of a conversion error.
source§

fn try_from(builder: HillClimbBuilder<G, F, SR>) -> Result<Self, Self::Error>

Performs the conversion.

Auto Trait Implementations§

§

impl<G, F, SR> Freeze for HillClimb<G, F, SR>
where G: Freeze, F: Freeze, SR: Freeze,

§

impl<G, F, SR> RefUnwindSafe for HillClimb<G, F, SR>

§

impl<G, F, SR> Send for HillClimb<G, F, SR>

§

impl<G, F, SR> Sync for HillClimb<G, F, SR>

§

impl<G, F, SR> Unpin for HillClimb<G, F, SR>
where G: Unpin, F: Unpin, SR: Unpin, <G as Genotype>::Allele: Unpin,

§

impl<G, F, SR> UnwindSafe for HillClimb<G, F, SR>

Blanket Implementations§

source§

impl<T> Any for T
where T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for T
where T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for T
where U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> IntoEither for T

source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
source§

impl<T> Pointable for T

source§

const ALIGN: usize = _

The alignment of pointer.
§

type Init = T

The type for initializers.
source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
source§

impl<T> ToString for T
where T: Display + ?Sized,

source§

default fn to_string(&self) -> String

Converts the given value to a String. Read more
source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

source§

fn vzip(self) -> V