From 3ab660911222096794b4517ce1a5f2c2aafb5242 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:10:23 +0200 Subject: [PATCH 01/27] fix ambiguous function call --- src/core/ga_base.impl.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index f76e3580..aee51c5b 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -618,7 +618,7 @@ namespace gapp requires (is_bounded && std::derived_from>) Candidates GA::solve(F fitness_function, Bounds bounds, Population initial_population) { - const size_t chrom_len = fitness_function.chrom_len(); + const size_t chrom_len = fitness_function.FitnessFunctionBase::chrom_len(); return solve(std::make_unique(std::move(fitness_function)), BoundsVector(chrom_len, bounds), max_gen(), std::move(initial_population)); } @@ -636,7 +636,7 @@ namespace gapp requires (is_bounded && std::derived_from>) Candidates GA::solve(F fitness_function, Bounds bounds, size_t generations, Population initial_population) { - const size_t chrom_len = fitness_function.chrom_len(); + const size_t chrom_len = fitness_function.FitnessFunctionBase::chrom_len(); return solve(std::make_unique(std::move(fitness_function)), BoundsVector(chrom_len, bounds), generations, std::move(initial_population)); } From 0403cb9ae8d3586027ce03f00c30902b7c61232f Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:11:00 +0200 Subject: [PATCH 02/27] add metric example --- examples/9_metrics.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/9_metrics.cpp diff --git a/examples/9_metrics.cpp b/examples/9_metrics.cpp new file mode 100644 index 00000000..cb72b928 --- /dev/null +++ b/examples/9_metrics.cpp @@ -0,0 +1,34 @@ +/* Example showing the usage of metrics in the GAs. */ + +#include "gapp.hpp" +#include +#include +#include +#include + +using namespace gapp; + +struct MyMetric : public metrics::Monitor> +{ + double value_at(size_t generation) const noexcept { return data_[generation]; } + void initialize(const GaInfo&) override { data_.clear(); } + void update(const GaInfo& ga) override { data_.push_back(ga.fitness_matrix()[0][0]); } +}; + +int main() +{ + RCGA GA; + GA.track(metrics::FitnessMin{}, metrics::FitnessMax{}, MyMetric{}); + GA.solve(problems::Sphere{ 10 }, Bounds{ -5.0, 5.0 }); + + const MyMetric& metric = GA.get_metric(); + + std::cout << "The values of MyMetric throughout the run:\n"; + for (size_t gen = 0; gen < metric.size(); gen++) + { + std::cout << std::format("Generation {}\t| {:.6f}\n", gen + 1, metric[gen]); + } + + const auto* hypervol = GA.get_metric_if(); // untracked metric + assert(hypervol == nullptr); +} From d11d54ad0506963873b572e7daf2bf3f4959788a Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:11:28 +0200 Subject: [PATCH 03/27] Create metrics.md --- docs/metrics.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/metrics.md diff --git a/docs/metrics.md b/docs/metrics.md new file mode 100644 index 00000000..86597b86 --- /dev/null +++ b/docs/metrics.md @@ -0,0 +1,98 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. **Metrics** +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Metrics + +By default, when you run the GA, the `solve()` function returns a set +of pareto optimal solutions, but you won't have any additional information +about how the population evolved over the run. + +In order to get more insight about a run, you can set a number of metrics +that will be tracked throughout the run. Each metric tracks a particular attribute of the population, +and the value of the metric will be recorded in each generation. + +## Usage + +The tracked metrics must be set before a run, using the `track()` +method of the GAs. You can specify any number of metrics in the arguments +of this function: + +```cpp +// Track the minimum and maximum of the population's fitness values in each +// generation, for each objective +GA.track(metrics::FitnessMin{}, metrics::FitnessMax{}) +// Run the GA +GA.solve(...); +``` + +After the run, you can access the metrics using the `get_metric()` method. +This will return a reference to the given metric, which stores the values for each generation: + +```cpp +// Get the metric tracking the minimal fitness values +const auto& fmin = GA.get_metric(); + +// Print the lowest fitness value of the first objective in the fourth generation +std::cout << fmin[3][0]; +``` + +## Available metrics + +There are a number of metrics already implemented by the library in the +`gapp::metrics` namespace, see: + + - `` for fitness related metrics + - `` for metrics tracking information about + the distribution of the population in the objective space + - `` for other metrics + +All of the metrics that are implemented in the library work for any objective +function, regardless of the number of objectives, but the distribution metrics +are intended to be used for multi-objective optimization problems. + + +## Custom metrics + +If you want to track something that doesn't have a metric already implemented +for it, it's possible to implement your own metrics. Metrics must be derived +from the `Monitor` class, and implement 3 methods: `initialize`, `update`, and +`value_at`: + +```cpp +// The second type parameter of Monitor is the type used to store the +// gathered metrics. This will be used as the type of the data_ field. +class MyMetric : public metrics::Monitor> +{ +public: + // Returns the value of the metric in the given generation. + // Note that this method is not virtual. + double value_at(size_t generation) const noexcept { return data_[generation]; } +private: + // Initialize the metric. Called at the start of a run. + void initialize(const GaInfo& ga) override { data_.clear(); } + + // Update the metric with a new value from the current generation. + // Called once in each generation. + void update(const GaInfo& ga) override + { + // You can access the current state of the GA through + // the ga parameter. + ... + data_.push_back(...); + } +}; +``` + +You can use these custom metrics the same way as any other metric that is implemented +by the library. + +------------------------------------------------------------------------------------------------ From 7bf2b40b5976aa8754daaa14694978afda45e072 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:52:48 +0200 Subject: [PATCH 04/27] add stop_condition(nullptr_t) --- src/core/ga_info.cpp | 5 +++++ src/core/ga_info.hpp | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/core/ga_info.cpp b/src/core/ga_info.cpp index 9770b21c..01bdfcf3 100644 --- a/src/core/ga_info.cpp +++ b/src/core/ga_info.cpp @@ -41,6 +41,11 @@ namespace gapp stop_condition_ = f ? std::move(f) : std::make_unique(); } + void GaInfo::stop_condition(std::nullptr_t) + { + stop_condition_ = std::make_unique(); + } + void GaInfo::stop_condition(StopConditionCallable f) { stop_condition_ = std::make_unique(std::move(f)); diff --git a/src/core/ga_info.hpp b/src/core/ga_info.hpp index aaa1d257..c2e856f2 100644 --- a/src/core/ga_info.hpp +++ b/src/core/ga_info.hpp @@ -207,6 +207,13 @@ namespace gapp */ void stop_condition(std::unique_ptr f); + /** + * Clear the early-stop condition currently set for the GA. \n + * The GA will run for the maximum number of generations set without + * the possibility of stopping earlier. + */ + void stop_condition(std::nullptr_t); + /** * Set an early-stop condition for the algorithm. \n * This is an optional early-stop condition that can be used to stop the run From f58817f408e9f2c607ee700683f3cc97b5dc30a1 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:53:38 +0200 Subject: [PATCH 05/27] restore old GA state after the runs --- src/core/ga_base.impl.hpp | 5 ++++ src/utility/scope_exit.hpp | 61 ++++++++++++++++++++++++++++++++++++++ test/unit/scope_exit.cpp | 43 +++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/utility/scope_exit.hpp create mode 100644 test/unit/scope_exit.cpp diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index aee51c5b..b35f59e0 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -17,6 +17,7 @@ #include "../stop_condition/stop_condition_base.hpp" #include "../utility/algorithm.hpp" #include "../utility/functional.hpp" +#include "../utility/scope_exit.hpp" #include "../utility/utility.hpp" #include #include @@ -529,6 +530,8 @@ namespace gapp { GAPP_ASSERT(fitness_function, "The fitness function can't be a nullptr."); + detail::RestoreOnExit scope_exit{ max_gen_ }; + fitness_function_ = std::move(fitness_function); max_gen(generations); @@ -548,6 +551,8 @@ namespace gapp GAPP_ASSERT(fitness_function, "The fitness function can't be a nullptr."); GAPP_ASSERT(bounds.size() == fitness_function->chrom_len(), "The length of the bounds vector must match the chromosome length."); + detail::RestoreOnExit scope_exit{ max_gen_, std::move(bounds_) }; + fitness_function_ = std::move(fitness_function); max_gen(generations); diff --git a/src/utility/scope_exit.hpp b/src/utility/scope_exit.hpp new file mode 100644 index 00000000..26471c63 --- /dev/null +++ b/src/utility/scope_exit.hpp @@ -0,0 +1,61 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GA_UTILITY_SCOPE_EXIT_HPP +#define GA_UTILITY_SCOPE_EXIT_HPP + +#include +#include +#include +#include + +namespace gapp::detail +{ + template + class [[nodiscard]] ScopeExit + { + public: + constexpr explicit ScopeExit(F on_exit) + noexcept(std::is_nothrow_move_constructible_v) : + on_exit_(std::move(on_exit)) + {} + + ScopeExit(const ScopeExit&) = delete; + ScopeExit(ScopeExit&&) = delete; + ScopeExit& operator=(const ScopeExit&) = delete; + ScopeExit& operator=(ScopeExit&&) = delete; + + constexpr ~ScopeExit() noexcept + { + if (active_) std::invoke(std::move(on_exit_)); + } + + constexpr void release() noexcept { active_ = false; } + + private: + F on_exit_; + bool active_ = true; + }; + + template + class [[nodiscard]] RestoreOnExit + { + public: + template + constexpr explicit RestoreOnExit(Us&&... vars) + noexcept(( std::is_nothrow_constructible_v, Us&&> && ... )) : + vars_(vars...), old_values_(std::forward(vars)...) + {} + + constexpr ~RestoreOnExit() noexcept { vars_ = std::move(old_values_); } + + private: + std::tuple vars_; + std::tuple old_values_; + }; + + template + RestoreOnExit(Ts&&...) -> RestoreOnExit...>; + +} // namespace gapp::detail + +#endif // !GA_UTILITY_SCOPE_EXIT_HPP \ No newline at end of file diff --git a/test/unit/scope_exit.cpp b/test/unit/scope_exit.cpp new file mode 100644 index 00000000..f19ff850 --- /dev/null +++ b/test/unit/scope_exit.cpp @@ -0,0 +1,43 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include "utility/scope_exit.hpp" + +using namespace gapp::detail; + + +TEST_CASE("scope_exit", "[utility]") +{ + int n = 0; + + { + ScopeExit on_exit{ [&] { n = 2; } }; + REQUIRE(n == 0); + } + REQUIRE(n == 2); + + { + ScopeExit on_exit{ [&] { n = 3; } }; + on_exit.release(); + } + REQUIRE(n == 2); +} + +TEST_CASE("restore_on_exit", "[utility]") +{ + int n = 3; + double f = 2.5; + + { + RestoreOnExit on_exit(n, f); + + n = 10; + f = 0.2; + + REQUIRE(n == 10); + REQUIRE(f == 0.2); + } + + REQUIRE(n == 3); + REQUIRE(f == 2.5); +} From dc79afe3e2a329deef35337dce9b16ab3a07fedb Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:53:56 +0200 Subject: [PATCH 06/27] add stop conditions example --- examples/8_stop_conditions.cpp | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples/8_stop_conditions.cpp diff --git a/examples/8_stop_conditions.cpp b/examples/8_stop_conditions.cpp new file mode 100644 index 00000000..088e71c6 --- /dev/null +++ b/examples/8_stop_conditions.cpp @@ -0,0 +1,63 @@ +/* Example showing the usage of the stop conditions in the GAs. */ + +#include "gapp.hpp" +#include +#include +#include +#include + +using namespace gapp; + +class MyStopCondition : public stopping::StopCondition +{ + void initialize(const GaInfo&) override {} + + bool stop_condition(const GaInfo& ga) override + { + return ga.num_fitness_evals() >= 4000; + } +}; + +int main() +{ + BinaryGA GA; + + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + GA.solve(problems::Sphere{ 10 }, /* generations */ 375); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + GA.max_gen(755); + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + GA.solve(problems::Sphere{ 10 }, /* generations */ 175); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + // early-stop conditions + + GA.stop_condition(stopping::FitnessBestStall{ 3 }); + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + GA.stop_condition(nullptr); // same as GA.stop_condition(stopping::NoEarlyStop{}); + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + // composite early-stop conditions + + GA.stop_condition(stopping::FitnessBestStall{ 2 } && stopping::FitnessMeanStall{ 3 }); + GA.solve(problems::Sphere{ 10 }, 5000); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + // custom early-stop conditions + + GA.stop_condition([](const GaInfo& ga) { return ga.num_fitness_evals() >= 10000; }); + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); + + GA.stop_condition(MyStopCondition{}); + GA.solve(problems::Sphere{ 10 }); + std::cout << std::format("The GA ran for {} generations.\n", GA.generation_cntr() + 1); +} From 9db9cfc40127aebff8788291ce533b08d76a9fd8 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:54:14 +0200 Subject: [PATCH 07/27] Create stop-conditions.md --- docs/stop-conditions.md | 121 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/stop-conditions.md diff --git a/docs/stop-conditions.md b/docs/stop-conditions.md new file mode 100644 index 00000000..9cd030b6 --- /dev/null +++ b/docs/stop-conditions.md @@ -0,0 +1,121 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. **Stop conditions** +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Stop conditions + +The stop condition of the GAs determine when the run will be terminated. +By default, a run will stop when reaching the maximum number of generations +specified in `solve()`, or in the case where no number was specified there, +it will be the number of generations set using `max_gen()`, or the default +value: + +```cpp +PermutationGA GA; +GA.solve(f); // uses the default value of max_generations for stopping + +GA.max_gen(1000); +GA.solve(f); // runs the GA for 1000 generations + +GA.solve(f, 500); // runs the GA for 500 generations +``` + +## Early stopping + +In a lot of cases, you might want to use a different stopping criterion instead +of using a set number of generations. For example, it could make sense to stop +the run once the solutions have stopped improving significantly, in order to save +time. + +It is possible to customize the stop condition of the GAs by specifying +an early-stop condition in addition to the maximum number of generations. + +The early-stop condition can be used to terminate a run before it reaches the +maximum number of generations set, when the early-stop condition is met. Note +that even if such a stop condition is set, the GA will still respect the +maximum number of generations set, and it will always stop when reaching that +regardless of the early-stop condition. + +## Usage + +There are a number of generally useful stop conditions already implemented +by the library in the `gapp::stopping` namespace. See +`` for more details. + +The early-stop condition can be set using the `stop_condition()` method: + +```cpp +BinaryGA GA; +// stop the run once the average fitness values of the population +// haven't improved significantly for 5 generations +GA.stop_condition(stopping::FitnessMeanStall{ 5 }); +GA.solve(f); +``` + +If you want to clear the early-stop condition, you can either set it to a +`nullptr`, or use `stopping::NoEarlyStop`: + +```cpp +GA.stop_condition(nullptr); +// or +GA.stop_condition(stopping::NoEarlyStop{}); +``` + +## Composite early-stop conditions + +Stop conditions can be combined to create more complex stop conditions +using `operator&&` and `operator||`. This allows for specifying more complex +stopping criterion without having to write your own custom stop conditions: + +```cpp +GA.stop_condition(stopping::FitnessMeanStall{ 5 } && stopping::FitnessBestStall{ 5 }); +``` + +## Custom early-stop conditions + +If the stop conditions already implemented by the library are not enough, +you can also define your own stop conditions. + +For simple stop conditions, you can use a lambda function: + +```cpp +GA.stop_condition([](const GaInfo& ga) +{ + // Check if the stopping criterion is met, + // return true if the run should be terminated + ... + return false; +}); +``` + +For more complex stop conditions, you can define your own stop condition class. +The class should be derived from `stopping::StopCondition`, and implement +`stop_condition`, and optionally `initialize`: + +```cpp +class MyStopCondition : public stopping::StopCondition +{ + // Check if the stopping criterion is met, + // called once in every generation + bool stop_condition(const GaInfo& ga) override + { + ... + // Return true if the run should be terminated + return false; + } + + // Initialize the stop condition, called once at the start + // of a run. Implementing this is optional. + void initialize(const GaInfo& ga) override { ... } +}; +``` + +------------------------------------------------------------------------------------------------ From 14407ff79973763f421ac82a373597ccc55732f4 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:21:56 +0200 Subject: [PATCH 08/27] add crossover/mutation examples --- examples/7_genetic_operators.cpp | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 examples/7_genetic_operators.cpp diff --git a/examples/7_genetic_operators.cpp b/examples/7_genetic_operators.cpp new file mode 100644 index 00000000..3b6fc5b8 --- /dev/null +++ b/examples/7_genetic_operators.cpp @@ -0,0 +1,76 @@ +/* Example showing the usage of the genetic operators in the GAs. */ + +#include "gapp.hpp" +#include +#include +#include + +using namespace gapp; + +class MyCrossover : public crossover::Crossover +{ +public: + using Crossover::Crossover; + + CandidatePair crossover(const GA&, const Candidate& parent1, const Candidate& parent2) const override + { + auto child1 = parent1; + auto child2 = parent2; + + // perform the crossover ... + + return { std::move(child1), std::move(child2) }; + } +}; + +class MyMutation : public mutation::Mutation +{ +public: + using Mutation::Mutation; + + void mutate(const GA&, const Candidate&, Chromosome& chromosome) const override + { + if (rng::randomReal() < mutation_rate()) + { + std::reverse(chromosome.begin(), chromosome.end()); + } + } +}; + +int main() +{ + PermutationGA ga; + ga.solve(problems::TSP52{}); // run using the default crossover and mutation methods + + // using other crossover, mutation operators + + ga.crossover_method(crossover::perm::Edge{}); // using the default crossover probability + ga.mutation_method(mutation::perm::Inversion{ /* mutation_rate = */ 0.3 }); + + std::cout << "The default crossover probability is "<< ga.crossover_rate() << ".\n"; + + // changing the crossover and mutation probabilities + + ga.crossover_method(crossover::perm::Edge{ /* crossover_rate = */ 0.92 }); + std::cout << "The crossover probability is " << ga.crossover_rate() << ".\n"; + + ga.crossover_rate(0.71); + ga.mutation_rate(0.1); + + std::cout << "The crossover probability is " << ga.crossover_rate() << ".\n"; + std::cout << "The mutation probability is " << ga.mutation_rate() << ".\n"; + + // user defined crossover and mutation methods + + ga.crossover_method(MyCrossover{ /* crossover_rate = */ 0.123 }); + ga.mutation_method(MyMutation{ /* mutation_rate = */ 0.456 }); + + // using a repair function + + ga.repair_function([](const GA&, const Chromosome& chrom) + { + auto new_chrom = chrom; + std::swap(new_chrom.front(), new_chrom.back()); + return new_chrom; + }); +} From 7263efe073ec0b67a4e12fe83b5797244cc3d1cb Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:40:47 +0200 Subject: [PATCH 09/27] Create genetic-operators.md --- docs/genetic-operators.md | 220 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 docs/genetic-operators.md diff --git a/docs/genetic-operators.md b/docs/genetic-operators.md new file mode 100644 index 00000000..9e3613b8 --- /dev/null +++ b/docs/genetic-operators.md @@ -0,0 +1,220 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. **Genetic operators** +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Genetic operators + +The genetic operators are used to create new candidate solutions from the existing +ones in the population, thus providing the basic search mechanism of the genetic +algorithms. The 2 main operators are the crossover and mutation, but the library +also allows for specifying a repair function. + +The selection method is considered to be a part of the `Algorithm` in the library, +so it will not be discussed here. + +The library contains implementations of several crossover and mutation methods +that can be used. These can be found in the `gapp::crossover` and +`gapp::mutation` namespaces respectively. + +As the genetic operators operate on candidate solutions, their +implementations depend on the encoding type used for the GA. A given crossover or +mutation method can only be used with the encoding types it is implemented for. +Because of this, the implemented crossover and mutation methods are further broken +up into multiple namespaces based on the encoding type they can be used with. +For example, the crossover operators are in the following namespaces: + + - `crossover::binary` + - `crossover::real` + - `crossover::perm` + - `crossover::integer` + +Crossover methods in the `binary` namespace can only be used for the `BinaryGA`, +methods in the `real` namespace can only be used for the `RCGA`, and so on. +The mutation methods are organized similarly. + +The library doesn't provide any repair functions since their use in the GAs +is optional. These always have to be defined by the user when they are used. + +## Crossover + +The crossover operator is responsible for generating new solutions from existing +ones. The operator takes 2 solutions selected from the population, and performs +some operation on them with a given probability to generate 2 new solutions. +When the operation is not performed, it returns copies of the parent solutions. + +The crossover operator used by the GA can be set either in the constructor or by +using the `crossover_method` method: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{}); +``` + +The probability of performing the crossover operation is a general parameter +of the crossovers and it can be set for all of the crossover operators either +in their constructors or by using the `crossover_rate` method: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{ /* crossover_rate = */ 0.8 }); +``` + +The GA classes also provide a `crossover_rate` method that can be used to set +the crossover probability for the current crossover operator used by the GA: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{}); +GA.crossover_rate(0.8); +``` + +Some crossover operators may also have additional parameters that are specific +to the given operator. + +## Mutation + +The mutation operator is applied to each of the solutions generated by the +crossovers in order to promote diversity in the population. This help the GA +with exploring more of the search space and avoiding convergence to local +optima. + +The mutation operator used by the GAs can be set similar to the crossover operators, +either in the constructor or by using the `mutation_method` method: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{}); +``` + +Similar to the crossovers, the mutation operators also all have a +mutation probability parameter, but how this probability is interpreted +(either on a per-gene or per-solution basis) depends on the specific +operator. + +The mutation probability can be set similar to the crossover probability, either +in the constructor of the mutation operator, or by using the `mutation_rate` +method: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{ /* mutation_rate = */ 0.1 }); +``` + +The GA classes also provide a `mutation_rate` method that can be used to set +the mutation probability for the current mutation operator of the GA: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{}); +GA.mutation_rate(0.1); +``` + +Similar to the crossovers, some mutation operators may have additional parameters +that are specific to the particular operator. + +## Repair + +The repair function is an additional operator that will be applied to each +solution after the mutations. Using a repair function is optional, and they are +not used in the GAs by default. + +Repair functions can be specified using the `repair_function` method of the GAs: + +```cpp +ga.repair_function([](const GA&, const Chromosome& chrom) +{ + auto new_chrom = chrom; + // do something with new_chrom ... + return new_chrom; +}); +``` + +If a repair function has been set previously, it can be cleared by passing +a nullptr to the setter: + +```cpp +GA.repair_function(nullptr); +``` + +## Custom genetic operators (crossover and mutation) + +In addition to the operators already implemented in the library, +user defined crossover and mutation operators can also be used in the GAs. + +The simplest way to do this is to use a lambda function: + +```cpp +RCGA ga; + +ga.crossover_method([](const GA&, const Candidate& parent1, const Candidate& parent2) +{ + auto child1 = parent1; + auto child2 = parent2; + + // perform the crossover ... + + return CandidatePair{ std::move(child1), std::move(child2) }; +}); + +ga.mutation_method([](const GA& ga, const Candidate& sol, Chromosome& chrom) +{ + for (RealGene& gene : chrom) + { + if (rng::randomReal() < ga.mutation_rate()) + { + // modify the gene ... + } + } +}); +``` + +Alternatively, crossover and mutation operators can also be implemented as +classes derived from `crossover::Crossover` and +`mutation::Mutation` respectively. Crossovers must implement the +`crossover` method, while mutations must implement the `mutate` method: + +```cpp +class MyCrossover : public crossover::Crossover +{ +public: + using Crossover::Crossover; + + CandidatePair crossover(const GA& ga, const Candidate& parent1, const Candidate& parent2) const override + { + // perform the crossover ... + } +}; +``` + +```cpp +class MyMutation : public mutation::Mutation +{ +public: + using Mutation::Mutation; + + void mutate(const GA& ga, const Candidate& candidate, Chromosome& chromosome) const override + { + // perform the mutation on chromosome ... + } +}; +``` + +There are a few things that should be kept in mind for the implementations +of these operators regardless of how they are defined: + + - The crossover implementation shouldn't take the crossover rate into account. + This is done elsewhere. + - The mutation implementation must take the mutation rate into account, as how + the mutation rate is interpreted depends on the specific mutation method. + - The mutation modifies the `chrom` parameter, and does not return anything. + - The implementations should be thread-safe. + +------------------------------------------------------------------------------------------------ From 7c6549e6bed6b8ab9f7cad91fbc8d3f971cc1870 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:29:06 +0200 Subject: [PATCH 10/27] keep the gene bounds after a run --- src/core/ga_base.impl.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index b35f59e0..a29b58cc 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -551,7 +551,7 @@ namespace gapp GAPP_ASSERT(fitness_function, "The fitness function can't be a nullptr."); GAPP_ASSERT(bounds.size() == fitness_function->chrom_len(), "The length of the bounds vector must match the chromosome length."); - detail::RestoreOnExit scope_exit{ max_gen_, std::move(bounds_) }; + detail::RestoreOnExit scope_exit{ max_gen_ }; fitness_function_ = std::move(fitness_function); max_gen(generations); From 23b29026b9a3cbd56cdf8fcbdcfdca5f4997c62b Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:34:33 +0200 Subject: [PATCH 11/27] Create algorithms.md --- docs/algorithms.md | 149 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 docs/algorithms.md diff --git a/docs/algorithms.md b/docs/algorithms.md new file mode 100644 index 00000000..9accde1f --- /dev/null +++ b/docs/algorithms.md @@ -0,0 +1,149 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. **Algorithms** +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Algorithms + +The algorithms are used in the library to define different genetic algorithm +variants. They consist of the selection and population replacement methods, +which define the overall evolution process in combination with the genetic +operators. + +The algorithms in the library belong to 2 categories: single-, and +multi-objective algorithms. These can only be used for single- and +multi-objective optimization problems respectively. It is possible +to implement general algorithms that work for any type of problem, but the +library currently doesn't have such an algorithm. + +There are 3 algorithms provided by the library: + + - SingleObjective (single-objective) + - NSGA-II (multi-objective) + - NSGA-III (multi-objective) + + All of these algorithms are in the `gapp::algorithm` namespace. + +# Selecting the algorithm + +By default, if no algorithm is specified for the GA, one will automatically +be selected based on the number of objectives of the fitness function being used. +This means that the default algorithm used by the GA will always be compatible +with the fitness functions regardless of the number of objectives. + +```cpp +BinaryGA ga; +ga.solve(f); // run using the default algorithm +``` + +It is also possible to select a different algorithm to be used by the GA. +This can be done either in the constructor or by using the `algorithm` method: + +```cpp +BinaryGA ga; +ga.algorithm(algorithm::NSGA3{}); +ga.solve(f); +``` + +The only thing that should be kept in mind when selecting the algorithm this +way is that it needs to be compatible with the fitness function used. +The single-objective algorithms can only be used for single-objective fitness +functions, and the multi-objective algorithms can only be used with multi-objective +fitness functions. + +If an algorithm was explicitly specified, it can be cleared by passing a `nullptr` +to the `algorithm` setter. This will result in the default algorithm being used, +as in the case where no algorithm was explicitly set. + +```cpp +ga.algorithm(nullptr); +ga.solve(f); // uses the default algorithm +``` + +# The single-objective algorithm + +The `SingleObjective` algorithm is not a concrete algorithm implementation +like the NSGA-II and NSGA-III algorithms are. It is simply a wrapper that +combines a selection and a population replacement method. These methods +can be selected independently of eachother in the `SingleObjective` algorithm. + +The library implements several selection and population replacement methods +that can be used. These are in the `gapp::selection` and `gapp::replacement` +namespaces respectively. + +```cpp +BinaryGA ga; +ga.algorithm(algorithm::SingleObjective{}); // use the default selection and replacement methods +ga.solve(f); + +// use tournament selection, and elitism for the population replacement methods +ga.algorithm(algorithm::SingleObjective{ selection::Tournament{}, replacement::Elitism{ 5 } }); +ga.solve(f); +``` + +# Custom algorithms + +In addition to the algorithms provided by the library, it is also possible to +use user-defined algorithms in the GAs. These must be implemented as a class +that is derived from `algorithm::Algorithm`. The class technically only has to implement +the `selectImpl` and `nextPopulationImpl` methods, but more complex, and efficient +algorithm implementations will have to implement several additional methods. + +```cpp +class MyAlgorithm : public algorithm::Algorithm +{ +public: + // ... +}; +``` + +# Custom selection and replacement methods (single-objective) + +For the `SingleObjective` algorithms, it's possible to define additional selection +and replacement methods separately without having to define a completely new +algorithm. + +Simple selection and population replacement methods can be defined using a lambda +or some other callable type. As an example, a simple tournament selection method +could be implemented this way: + +```cpp +algorithm::SingleObjective algorithm; +algorithm.selection_method([](const GaInfo& context, const FitnessMatrix& fmat) +{ + size_t first = rng::randomIdx(fmat); + size_t second = rng::randomIdx(fmat); + + return (fmat[first][0] >= fmat[second][0]) ? first : second; +}); +``` + +More complex operators can be implemented as classes derived from `selection::Selection` +and `replacement::Replacement` respectively. The implementation of the simple +tournament selection shown above could also be implemented this way: + +```cpp +class MyTournamentSelection : public selection::Selection +{ +public: + size_t selectImpl(const GaInfo& context, const FitnessMatrix& fmat) const override + { + size_t first = rng::randomIdx(fmat); + size_t second = rng::randomIdx(fmat); + + return (fmat[first][0] >= fmat[second][0]) ? first : second; + } +}; +``` + +Note that a more general version of the tournament selection operator +is already implemented in the library, called `selection::Tournament`. + +------------------------------------------------------------------------------------------------ From 0a1cb86c91138b5b38792c8c9065fe7b5997376a Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:53:50 +0200 Subject: [PATCH 12/27] Update introduction.md --- docs/introduction.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/introduction.md b/docs/introduction.md index 8951ffcb..600d41ef 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,4 +1,16 @@ -# Introduction + +1. **Introduction** +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Introduction This short introduction will present the basic usage of the library through an example of solving a simple single-objective optimization problem. From 16d92c6969adebdd5408db8822597e4d2003abdc Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:54:13 +0200 Subject: [PATCH 13/27] add algorithm examples --- examples/6_algorithms.cpp | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 examples/6_algorithms.cpp diff --git a/examples/6_algorithms.cpp b/examples/6_algorithms.cpp new file mode 100644 index 00000000..faaf0e4e --- /dev/null +++ b/examples/6_algorithms.cpp @@ -0,0 +1,45 @@ +/* Example showing the usage of the algorithms in the GAs. */ + +#include "gapp.hpp" + +using namespace gapp; + +class MyTournamentSelection : public selection::Selection +{ +public: + size_t selectImpl(const GaInfo&, const FitnessMatrix& fmat) const override + { + size_t first = rng::randomIdx(fmat); + size_t second = rng::randomIdx(fmat); + + return (fmat[first][0] >= fmat[second][0]) ? first : second; + } +}; + +int main() +{ + BinaryGA ga; + ga.solve(problems::Sphere{ 3 }); // run using the default algorithm + ga.solve(problems::Kursawe{}); // the default algorithm works with both single- and multi-objective problems + + // using a different algorithm + + ga.algorithm(algorithm::NSGA3{}); + ga.solve(problems::Kursawe{}); // the NSGA3 algorithm only works with multi-objective problems + + // going back to the default algorithm + + ga.algorithm(nullptr); + ga.solve(problems::Sphere{ 3 }); + ga.solve(problems::Kursawe{}); + + // selecting the selection and replacement methods for the SingleObjective algorithm + + ga.algorithm(algorithm::SingleObjective{ selection::Tournament{}, replacement::Elitism{ 5 } }); + ga.solve(problems::Sphere{ 3 }); + + // user defined selection methods + + ga.algorithm(algorithm::SingleObjective{ MyTournamentSelection{} }); + ga.solve(problems::Sphere{ 3 }); +} From e8040607003f4d4baebbf2155da5fc67c4288a4d Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:54:29 +0200 Subject: [PATCH 14/27] remove pointless overloads from SingleObjective --- src/algorithm/single_objective.cpp | 4 ---- src/algorithm/single_objective.hpp | 37 +++--------------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/src/algorithm/single_objective.cpp b/src/algorithm/single_objective.cpp index 32758db6..8a2cb53f 100644 --- a/src/algorithm/single_objective.cpp +++ b/src/algorithm/single_objective.cpp @@ -12,10 +12,6 @@ namespace gapp::algorithm { - SingleObjective::SingleObjective(std::unique_ptr selection) : - SingleObjective(std::move(selection), std::make_unique()) - {} - SingleObjective::SingleObjective(std::unique_ptr selection, std::unique_ptr replacement) : selection_(std::move(selection)), replacement_(std::move(replacement)) { diff --git a/src/algorithm/single_objective.hpp b/src/algorithm/single_objective.hpp index 9a445a37..0035a63a 100644 --- a/src/algorithm/single_objective.hpp +++ b/src/algorithm/single_objective.hpp @@ -47,20 +47,6 @@ namespace gapp::algorithm */ using ReplacementCallable = std::function(const GaInfo&, FitnessMatrix::const_iterator, FitnessMatrix::const_iterator, FitnessMatrix::const_iterator)>; - /** - * Create a single-objective algorithm using the default selection - * and replacement methods. - */ - SingleObjective(); - - /** - * Create a single-objective algorithm using the default replacement method. - * - * @param selection The selection method to use. - */ - template - requires std::derived_from - explicit SingleObjective(S selection); /** * Create a single-objective algorithm. @@ -68,16 +54,9 @@ namespace gapp::algorithm * @param selection The selection method to use. * @param replacement The replacement policy to use. */ - template + template requires std::derived_from && std::derived_from - SingleObjective(S selection, R replacement); - - /** - * Create a single-objective algorithm using the default replacement method. - * - * @param selection The selection method to use. Can't be a nullptr. - */ - explicit SingleObjective(std::unique_ptr selection); + explicit SingleObjective(S selection = S{}, R replacement = R{}); /** * Create a single-objective algorithm. @@ -85,7 +64,7 @@ namespace gapp::algorithm * @param selection The selection method to use. Can't be a nullptr. * @param replacement The replacement policy to use. Can't be a nullptr. */ - SingleObjective(std::unique_ptr selection, std::unique_ptr replacement); + explicit SingleObjective(std::unique_ptr selection, std::unique_ptr replacement = std::make_unique()); /** * Create a single-objective algorithm using the default replacement method. @@ -189,16 +168,6 @@ namespace gapp::algorithm namespace gapp::algorithm { - inline SingleObjective::SingleObjective() : - selection_(std::make_unique()), replacement_(std::make_unique()) - {} - - template - requires std::derived_from - inline SingleObjective::SingleObjective(S selection) : - SingleObjective(std::move(selection), DefaultReplacement{}) - {} - template requires std::derived_from && std::derived_from inline SingleObjective::SingleObjective(S selection, R replacement) : From 5673c14fbd0a54a63af76a3e5b4ce07b162399eb Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:54:40 +0200 Subject: [PATCH 15/27] remove unused includes --- examples/7_genetic_operators.cpp | 1 - examples/8_stop_conditions.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/7_genetic_operators.cpp b/examples/7_genetic_operators.cpp index 3b6fc5b8..d9d5b98e 100644 --- a/examples/7_genetic_operators.cpp +++ b/examples/7_genetic_operators.cpp @@ -3,7 +3,6 @@ #include "gapp.hpp" #include #include -#include using namespace gapp; diff --git a/examples/8_stop_conditions.cpp b/examples/8_stop_conditions.cpp index 088e71c6..96d2b929 100644 --- a/examples/8_stop_conditions.cpp +++ b/examples/8_stop_conditions.cpp @@ -4,7 +4,6 @@ #include #include #include -#include using namespace gapp; From c91149b35a6fc91ce4f409d2b1a767a7e2ac3821 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:55:13 +0200 Subject: [PATCH 16/27] use the default algorithm when set to nullptr --- src/core/ga_base.decl.hpp | 12 ++++++++---- src/core/ga_base.impl.hpp | 2 -- src/core/ga_info.cpp | 16 +++++++++++----- src/core/ga_info.hpp | 10 +++++++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/core/ga_base.decl.hpp b/src/core/ga_base.decl.hpp index 89580a9c..9176efee 100644 --- a/src/core/ga_base.decl.hpp +++ b/src/core/ga_base.decl.hpp @@ -105,7 +105,8 @@ namespace gapp * The mutation probability used will be deduced from the chromosome length. * * @param population_size The number of candidates in the population. Must be at least 1. - * @param algorithm The algorithm to use. Can't be a nullptr. + * @param algorithm The algorithm to use. The default algorithm will be used if it's a + * nullptr. */ GA(Positive population_size, std::unique_ptr algorithm); @@ -117,7 +118,8 @@ namespace gapp * @param population_size The number of candidates in the population. Must be at least 1. * @param crossover The crossover operator to use. Can't be a nullptr. * @param mutation The mutation operator to use. Can't be a nullptr. - * @param stop_condition The early-stop condition to use. Can't be a nullptr. + * @param stop_condition The early-stop condition to use. No early-stopping will be used + * if it's a nullptr. */ GA(Positive population_size, std::unique_ptr> crossover, @@ -128,10 +130,12 @@ namespace gapp * Create a genetic algorithm using the specified algorithm and operators. * * @param population_size The number of candidates in the population. Must be at least 1. - * @param algorithm The algorithm to use. Can't be a nullptr. + * @param algorithm The algorithm to use. The default algorithm will be used if it's a + * nullptr. * @param crossover The crossover operator to use. Can't be a nullptr. * @param mutation The mutation operator to use. Can't be a nullptr. - * @param stop_condition The early-stop condition to use. Can't be a nullptr. + * @param stop_condition The early-stop condition to use. No early-stopping will be used + * if it's a nullptr. */ GA(Positive population_size, std::unique_ptr algorithm, diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index a29b58cc..31266af0 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -38,10 +38,8 @@ namespace gapp std::unique_ptr stop_condition) : GaInfo(population_size, std::move(algorithm), std::move(stop_condition)), crossover_(std::move(crossover)), mutation_(std::move(mutation)) { - GAPP_ASSERT(algorithm_, "The algorithm can't be a nullptr."); GAPP_ASSERT(crossover_, "The crossover method can't be a nullptr."); GAPP_ASSERT(mutation_, "The mutation method can't be a nullptr."); - GAPP_ASSERT(stop_condition_, "The stop condition can't be a nullptr."); } template diff --git a/src/core/ga_info.cpp b/src/core/ga_info.cpp index 01bdfcf3..0f40a3e9 100644 --- a/src/core/ga_info.cpp +++ b/src/core/ga_info.cpp @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Krisztián Rugási. Subject to the MIT License. */ #include "ga_info.hpp" -#include "../algorithm/algorithm_base.hpp" +#include "../algorithm/single_objective.hpp" #include "../stop_condition/stop_condition.hpp" #include "../utility/utility.hpp" #include @@ -19,7 +19,9 @@ namespace gapp GaInfo::GaInfo(Positive population_size, std::unique_ptr algorithm, std::unique_ptr stop_condition) noexcept : algorithm_(std::move(algorithm)), stop_condition_(std::move(stop_condition)), population_size_(population_size) { - GAPP_ASSERT(stop_condition_, "The stop condition can't be a nullptr."); + use_default_algorithm_ = !algorithm; + if (!algorithm_) algorithm_ = std::make_unique(); + if (!stop_condition_) stop_condition_ = std::make_unique(); } size_t GaInfo::num_fitness_evals() const noexcept @@ -30,10 +32,14 @@ namespace gapp void GaInfo::algorithm(std::unique_ptr f) { - GAPP_ASSERT(f, "The algorithm can't be a nullptr."); + use_default_algorithm_ = !f; + algorithm_ = f ? std::move(f) : std::make_unique(); + } - algorithm_ = std::move(f); - use_default_algorithm_ = false; + void GaInfo::algorithm(std::nullptr_t) + { + use_default_algorithm_ = true; + algorithm_ = std::make_unique(); } void GaInfo::stop_condition(std::unique_ptr f) diff --git a/src/core/ga_info.hpp b/src/core/ga_info.hpp index c2e856f2..6d059f2b 100644 --- a/src/core/ga_info.hpp +++ b/src/core/ga_info.hpp @@ -176,10 +176,18 @@ namespace gapp * be a single-objective algorithm for single-objective problems, and a multi-objective * algorithm for multi-objective problems). * - * @param f The algorithm used by the %GA. Can't be a nullptr. + * @param f The algorithm used by the %GA. The default algorithm will be used if it's a + * nullptr. */ void algorithm(std::unique_ptr f); + /** + * Clear the algorithm currently set for the GA. \n + * The GA will use the default algorithm that is selected based on the number of + * objectives of the fitness functions. + */ + void algorithm(std::nullptr_t); + /** @returns The algorithm used by the %GA. */ [[nodiscard]] const algorithm::Algorithm& algorithm() const& noexcept { GAPP_ASSERT(algorithm_); return *algorithm_; } From cdb1d1c9cfedb4cc6f76519335bf067106b80e68 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Thu, 10 Aug 2023 21:04:00 +0200 Subject: [PATCH 17/27] Fix the headings in algorithms.md --- docs/algorithms.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/algorithms.md b/docs/algorithms.md index 9accde1f..09a776bb 100644 --- a/docs/algorithms.md +++ b/docs/algorithms.md @@ -31,7 +31,7 @@ There are 3 algorithms provided by the library: All of these algorithms are in the `gapp::algorithm` namespace. -# Selecting the algorithm +## Selecting the algorithm By default, if no algorithm is specified for the GA, one will automatically be selected based on the number of objectives of the fitness function being used. @@ -67,7 +67,7 @@ ga.algorithm(nullptr); ga.solve(f); // uses the default algorithm ``` -# The single-objective algorithm +## The single-objective algorithm The `SingleObjective` algorithm is not a concrete algorithm implementation like the NSGA-II and NSGA-III algorithms are. It is simply a wrapper that @@ -88,7 +88,7 @@ ga.algorithm(algorithm::SingleObjective{ selection::Tournament{}, replacement::E ga.solve(f); ``` -# Custom algorithms +## Custom algorithms In addition to the algorithms provided by the library, it is also possible to use user-defined algorithms in the GAs. These must be implemented as a class @@ -104,7 +104,7 @@ public: }; ``` -# Custom selection and replacement methods (single-objective) +## Custom selection and replacement methods (single-objective) For the `SingleObjective` algorithms, it's possible to define additional selection and replacement methods separately without having to define a completely new From 796a24276169caa3d6386dbf5dda147719573169 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Fri, 11 Aug 2023 21:10:44 +0200 Subject: [PATCH 18/27] Create 5_encoding.cpp --- examples/5_encoding.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 examples/5_encoding.cpp diff --git a/examples/5_encoding.cpp b/examples/5_encoding.cpp new file mode 100644 index 00000000..25c4e2fc --- /dev/null +++ b/examples/5_encoding.cpp @@ -0,0 +1,32 @@ +/* Example showing the usage of the GAs with different encodings. */ + +#include "gapp.hpp" + +using namespace gapp; + +int main() +{ + // binary encoding + { + BinaryGA ga; + ga.solve(problems::Sphere{ 3 }); + } + + // real encoding + { + RCGA ga; + ga.solve(problems::Sphere{ 3 }, Bounds{ -10.0, 10.0 }); + } + + // permutation encoding + { + PermutationGA ga; + ga.solve(problems::TSP52{}); + } + + // integer encoding + { + IntegerGA ga; + ga.solve(problems::StringFinder{ "Hello" }, Bounds{ 'A', 'z' }); + } +} From ba6f08615c0d8b786cbdb15c0a8a718baf400699 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Fri, 11 Aug 2023 21:10:59 +0200 Subject: [PATCH 19/27] Create encodings.md --- docs/encodings.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/encodings.md diff --git a/docs/encodings.md b/docs/encodings.md new file mode 100644 index 00000000..3ec6af35 --- /dev/null +++ b/docs/encodings.md @@ -0,0 +1,147 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. **Encodings** +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Encodings + +The library defines several GA classes instead of using just a single one. +The difference between these is the encoding type used to represent the +problems and their solutions. Each of the GA classes is for a particular +encoding type, and it can only be used for objective functions using the +same encoding type. + +The below table shows each of the GA classes in the library, the encoding +(or gene) type used by them, and the problem (or fitness function) type +they can be used for: + + | GA class | Encoding type | Problem type | + |:---------------:|:---------------:|:----------------------:| + | `BinaryGA` | BinaryGene | Binary-encoded | + | `RCGA` | RealGene | Floating-point-encoded | + | `PermutationGA` | PermutationGene | Combinatorial | + | `IntegerGA` | IntegerGene | Integer-encoded | + +The encoding also determines what kind of crossover and mutation methods +can be used in the GA as these genetic operators also depend on the +encoding, and are generally defined for a particular gene type. + +All of the GA classes are in the main `gapp` namespace. + +```cpp +// the fitness function uses permutation encoding +PermutationGA{}.solve(problems::TSP52{}); + +// the fitness function uses real-encoding +RCGA{}.solve(problems::Sphere{}, Bounds{ -10.0, 10.0 }); + +// the fitness function uses binary-encoding +BinaryGA{}.solve(problems::Sphere{}); +``` + +## Solution representation + +The gene type determines how the candidate solutions are encoded in +the population. The representation of the solutions will be a vector +of the gene type used in all cases. Currently there is no way to +change this to use some other data structure. This should be taken +into account when defining new encodings. + +```cpp +template +using Chromosome = std::vector; +``` + +The candidates contain some more information in addition to their +chromosomes, for example their fitness vectors, but these are +independent of the gene type. + +The population is then made up of several candidates encoded in +this way. + +## Custom encodings + +It is also possible to use a different encoding type by defining a +new GA class. In order to do this, you have to: + + - define the gene type that will be used + - define a specialization for `GaTraits` + - specialize `is_bounded` if needed + - define the GA class, derived from `GA` + - define crossover and mutation operators for this encoding + +The gene type may be anything, with one restriction: the types +already used for the existing encodings are reserved and can't +be used to define new encodings. See `` +for the types that are already in use. + +```cpp +using MyGeneType = std::variant; +``` + +The specialization of `GaTraits` for the gene type is +required to define some attributes of the GA. These are the +default crossover and mutation operators that will be used +by the GA if none are specified explicitly, and the default +mutation probability for the mutation operators: + +```cpp +namespace gapp +{ + struct GaTraits + { + using DefaultCrossover = MyCrossover; + using DefaultMutation = MyMutation; + static Probability defaultMutationRate(size_t chrom_len) { return 1.0 / chrom_len; } + }; + +}; // namespace gapp +``` + +Specializing the `is_bounded` variable is only needed if +the gene type used will have it's lower and upper bounds specified +for each gene in the `solve` method. The value should be `true` in +this case, and `false` otherwise. + +```cpp +namespace gapp +{ + // This isn't technically needed, since false is the default value + template<> + inline constexpr bool is_bounded = false; + +} // namespace gapp +``` + +The actual GA class should be derived from `GA` using the desired gene +type for the type parameter `T`. The derived class only has to implement +the `generateCandidate` method, and optionally the `initialize` method: + +```cpp +class MyGA : public GA +{ +public: + // Generate a random candidate solution. This is used to + // create the initial population. + Candidate generateCandidate() const override + { + Candidate candidate; + // ... + return candidate; + } +}; +``` + +In addition to everything above, crossover and mutation operators will +also have to be defined for this encoding type, as these operators depend +on the encoding type, and the operators included in the library will not +work for new encodings. + +------------------------------------------------------------------------------------------------ From 529bb8e86131935ba903601ec72d1433a83fb062 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:31:58 +0200 Subject: [PATCH 20/27] add fitness_functions example --- examples/4_fitness_functions.cpp | 64 ++++++++++++++++++++ examples/{5_encoding.cpp => 5_encodings.cpp} | 0 src/crossover/crossover_base.impl.hpp | 8 +-- src/mutation/mutation_base.impl.hpp | 4 +- 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 examples/4_fitness_functions.cpp rename examples/{5_encoding.cpp => 5_encodings.cpp} (100%) diff --git a/examples/4_fitness_functions.cpp b/examples/4_fitness_functions.cpp new file mode 100644 index 00000000..45dd10c5 --- /dev/null +++ b/examples/4_fitness_functions.cpp @@ -0,0 +1,64 @@ +/* Example showing the definition of fitness function for the GAs. */ + +#include "gapp.hpp" +#include + +using namespace gapp; + +// single-objective fitness function +class XSquare : public FitnessFunction +{ +public: + FitnessVector invoke(const Chromosome& x) const override + { + return { -x[0] * x[0] }; + } +}; + +// multi-objective fitness function +class XSquareMulti : public FitnessFunction +{ +public: + FitnessVector invoke(const Chromosome& x) const override + { + const double f1 = -x[0] * x[0]; + const double f2 = (std::abs(x[0]) <= 2.0) ? 0.0 : 1.0; + + return { f1, f2 }; + } +}; + +// dynamic fitness function +class XSquareDynamic : public FitnessFunction< RealGene, 1> +{ +public: + XSquareDynamic() : FitnessFunction(/* variable_len = */ false, /* dynamic = */ true) {} + + FitnessVector invoke(const Chromosome& x) const override + { + return { -x[0] * x[0] + rng::randomNormal(0.0, 1.0) }; + } +}; + +int main() +{ + RCGA ga; + + // single-objective fitness function + { + auto solutions = ga.solve(XSquare{}, Bounds{ -100.0, 100.0 }); + std::cout << "The maximum of x^2 in [-100.0, 100.0] is at x = " << solutions[0].chromosome[0] << "\n"; + } + + // multi-objective fitness function + { + auto solutions = ga.solve(XSquareMulti{}, Bounds{ -100.0, 100.0 }); + std::cout << "The maximum of x^2 in [-100.0, 100.0] is at x = " << solutions[0].chromosome[0] << "\n"; + } + + // dynamic fitness function + { + auto solutions = ga.solve(XSquareDynamic{}, Bounds{ -100.0, 100.0 }); + std::cout << "The maximum of x^2 in [-100.0, 100.0] is at x = " << solutions[0].chromosome[0] << "\n"; + } +} diff --git a/examples/5_encoding.cpp b/examples/5_encodings.cpp similarity index 100% rename from examples/5_encoding.cpp rename to examples/5_encodings.cpp diff --git a/src/crossover/crossover_base.impl.hpp b/src/crossover/crossover_base.impl.hpp index 0ececbc3..ad37ee01 100644 --- a/src/crossover/crossover_base.impl.hpp +++ b/src/crossover/crossover_base.impl.hpp @@ -54,22 +54,22 @@ namespace gapp::crossover * evaluation for that child can be skipped (if the fitness function is the same) by assigning * it the same fitness as the parent. */ - if (child1.chromosome == parent1.chromosome) + if (child1 == parent1) { child1.fitness = parent1.fitness; child1.is_evaluated = parent1.is_evaluated; } - else if (child1.chromosome == parent2.chromosome) + else if (child1 == parent2) { child1.fitness = parent2.fitness; child1.is_evaluated = parent2.is_evaluated; } - if (child2.chromosome == parent1.chromosome) + if (child2 == parent1) { child2.fitness = parent1.fitness; child2.is_evaluated = parent1.is_evaluated; } - else if (child2.chromosome == parent2.chromosome) + else if (child2 == parent2) { child2.fitness = parent2.fitness; child2.is_evaluated = parent2.is_evaluated; diff --git a/src/mutation/mutation_base.impl.hpp b/src/mutation/mutation_base.impl.hpp index ddfc66ed..b7567ce5 100644 --- a/src/mutation/mutation_base.impl.hpp +++ b/src/mutation/mutation_base.impl.hpp @@ -26,10 +26,10 @@ namespace gapp::mutation /* If the candidate is already evaluated (happens when the crossover didn't change the candidate), its current fitness vector is valid, and we can save a fitness function call when the mutation doesn't change the chromosome. */ - thread_local Chromosome old_chromosome; old_chromosome = candidate.chromosome; + thread_local Candidate old_candidate; old_candidate = candidate; mutate(ga, candidate, candidate.chromosome); - candidate.is_evaluated = (candidate.chromosome == old_chromosome); + candidate.is_evaluated = (candidate == old_candidate); } GAPP_ASSERT(ga.variable_chrom_len() || candidate.chromosome.size() == ga.chrom_len(), From 116f9d07ce7d8642d42c156d61e0de12e039692e Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:32:10 +0200 Subject: [PATCH 21/27] Create fitness-functions.md --- docs/fitness-functions.md | 198 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/fitness-functions.md diff --git a/docs/fitness-functions.md b/docs/fitness-functions.md new file mode 100644 index 00000000..a09caff2 --- /dev/null +++ b/docs/fitness-functions.md @@ -0,0 +1,198 @@ + +1. [Introduction](introduction.md) +2. **Fitness functions** +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Fitness functions + +The `solve` methods of the GAs expect a fitness function +as their first arguments. This fitness function defines +the optimization problem that will be solved by the GA. + +## Defining a fitness function + +Fitness functions have to be implemented as a class derived +either from `FitnessFunctionBase` or `FitnessFunction`, but +before a fitness function can be implemented there are several +things that have to be considered: + +### Encoding + +The first thing that should be considered is the encoding +type, or how the solutions to the problem should be represented +in the population. There are several options provided by the +library, but user-defined encodings can also be used. +The representation is determined by the gene type used in the +fitness function and genetic algorithm classes: the solutions +will be encoded as a vector of the specified gene type. + +```cpp +template +using Chromosome = std::vector; +``` + +The options for the gene type provided by the library are: + + - BinaryGene + - RealGene + - PermutationGene + - IntegerGene + +The gene type is specified as the type parameter of the +`FitnessFunctionBase` and `FitnessFunction` classes. +Later on, the gene type will also determine the GA class +that has to be used to find the optimum the fitness function. + +See [encodings.md](encodings.md) for more information regarding the encodings. + +### Chromosome length + +The length of the chromosomes is another thing that has to be +considered, and has to be specified for the fitness function. +Generally, the chromosome length is going to be equal to the +number of variables in the problem, but this isn't always true. +For example, if the fitness function uses binary-encoding, a +single variable will likely be represented by multiple binary +genes instead of just a single one. + +The chromosome length will also determine the base class that +should be used for defining the fitness function. `FitnessFunctionBase` +can be used for every problem, and the chromosome length is specified +in its constructor. `FitnessFunction` can only be used if the chromosome +length used is known at compile-time. The chromosome length is specified +as a template parameter of the class in this case. + +```cpp +template +class FitnessFunctionBase; + +template +class FitnessFunction; +``` + +By default, the chromosome length is assumed to be constant - +ie. all of the solutions will have the same chromosome length +which will not change throughout the run - but there is also a +way to use variable chromosome lengths. + +### Number of objectives + +The number of objectives is another important property of the +fitness functions, but it doesn't have to be specified explicitly +in the definition of the fitness function. Instead, it will be +deduced from the legth of the fitness vector returned by it. +Note that the fitness function always returns a fitness vector, +even for single-objective problems. In the single-objective case +the length of the fitness vectors will be 1. + +The library assumes that a fitness function will always return +fitness vectors of the same length, which is equal to the number +of objectives. This can't be changed. + +The exact number of objectives is not generally relevant to the +GAs, but wether it is single- or multi-objective will determine +what algorithms can be used in the GAs for the fitness function. +See [algorithms.md](algorithms.md) for more information. + +### Maximization or minimization + +The genetic algorithms in the library will try to find the maximum +of the fitness function, which means that the fitness functions +always have to be implemented for maximization. If we are trying +to find the minimum of some function, it can easily be transformed +to a minimization problem by multiplying the function by -1. + +### Example + +As an example, consider that we are trying to find the minimum +of the function `f(x) = x^2`. The implementation of the fitness +function could be the following in this case: + +```cpp +class XSquare : public FitnessFunction +{ + FitnessVector invoke(const Chromosome& x) const override + { + return { -x[0] * x[0] }; + } +}; +``` + +A fitness function for a multi-objective problem would be implemented +the same way. + +## Other fitness function properties + +There are some other parameters of the fitness function that can +be specified, but these generally don't have to be changed from +their default values. Though more complex fitness functions might +have to set their values differently in some cases. + +### Dynamic fitness functions + +By default, the fitness function is assumed to always return the +same fitness vector for a particular chromosome passed to it as an +argument. This assumption is used to prevent unnecessary fitness +function calls, but this optimization would cause incorrect fitness +vectors to be assigned to some solutions if the assumption is false. + +For fitness functions where the assumption is not true, the value +of the `dynamic` parameter in the constructor of `FitnessFunctionBase` +has to be set to `true`. + +### Variable chromosome lengths + +By default, the chromosome length is assumed to be constant. If +different candidates may have chromosomes of different lengths, +the `variable_len` parameter in the constructor of `FitnessFunctionBase` +has to be set to `true`. + +The chromosome length still has to be specified for the fitness +function, but in this case it will only be used to generate +the solutions of the initial population. + +Note that if variable chromosome lengths are used, the crossover +and mutation operators will also have to be able to handle these. +This means also implementing your own versions of these operators, +as the ones provided by the library don't support variable chromosome +lengths. + +### Example + +```cpp +class MyFitnessFunction : public FitnessFunction +{ + MyFitnessFunction() : FitnessFunction(/* variable_len = */ true, /* dynamic = */ true) {} + + FitnessVector invoke(const Chromosome& x) const override; +}; +``` + +## Other concerns + +### Numeric issues + +The library in general only assumes that the fitness values returned +by the fitness function are valid numbers (ie. no `NaN` values will +be returned). + +Wether infinite fitness values are allowed or not depends on the +selection method used in the GA. If the fitness function can return +fitness vectors that contain infinite values, the selection method +(or the algorithm) will have to be chosen accordingly. + +### Thread safety + +The candidate solutions in the population are evaluated in parallel +in each generation of a run. As a result of this, the implementation +of the `invoke` method in the derived fitness function class must be +thread-safe. + +------------------------------------------------------------------------------------------------ From 360f95120ebd625ebaa2b9c6d1cda8001c663a4d Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:56:56 +0200 Subject: [PATCH 22/27] add references to the docs --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bfea46f8..f21d2686 100644 --- a/README.md +++ b/README.md @@ -86,22 +86,29 @@ target_link_libraries(YourTarget PRIVATE gapp::gapp) ``` -## Examples +## Documentation -The [examples](./examples) directory contains several examples for using the library. +The API documentation is available [here](https://krm7.github.io/gapp/). -* [Minimal example](./examples/1_minimal_example.cpp) -* [Single-objective optimization](./examples/2_basic_single_objective.cpp) -* [Multi-objective optimization](./examples/3_basic_multi_objective.cpp) +Additional documentation for the library can be found in the [docs](./docs) directory: +* [Introduction](./docs/introduction.md) +* [Fitness functions](./docs/fitness-functions.md) +* [Encodings](./docs/encodings.md) +* [Algorithms](./docs/algorithms.md) +* [Genetic operators](./docs/genetic-operators.md) +* [Stop conditions](./docs/stop-conditions.md) +* [Metrics](./docs/metrics.md) +* [Miscellaneous](./docs/miscellaneous.md) -## Documentation -The API documentation is available [here](https://krm7.github.io/gapp/). +## Examples -Additional documentation for the library can be found in the [docs](./docs) directory. +The [examples](./examples) directory contains several examples for using the library. -* [Introduction](./docs/introduction.md) +* [Minimal example](./examples/1_minimal_example.cpp) +* [Single-objective optimization](./examples/2_basic_single_objective.cpp) +* [Multi-objective optimization](./examples/3_basic_multi_objective.cpp) ------------------------------------------------------------------------------------------------- From 8763ccf6967224d57932704cd1ceebff3193a065 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:04:47 +0200 Subject: [PATCH 23/27] Update fitness-functions.md --- docs/fitness-functions.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/fitness-functions.md b/docs/fitness-functions.md index a09caff2..a5d4c13a 100644 --- a/docs/fitness-functions.md +++ b/docs/fitness-functions.md @@ -77,17 +77,12 @@ template class FitnessFunction; ``` -By default, the chromosome length is assumed to be constant - -ie. all of the solutions will have the same chromosome length -which will not change throughout the run - but there is also a -way to use variable chromosome lengths. - ### Number of objectives The number of objectives is another important property of the fitness functions, but it doesn't have to be specified explicitly in the definition of the fitness function. Instead, it will be -deduced from the legth of the fitness vector returned by it. +deduced from the legth of the fitness vectors returned by it. Note that the fitness function always returns a fitness vector, even for single-objective problems. In the single-objective case the length of the fitness vectors will be 1. @@ -105,9 +100,9 @@ See [algorithms.md](algorithms.md) for more information. The genetic algorithms in the library will try to find the maximum of the fitness function, which means that the fitness functions -always have to be implemented for maximization. If we are trying -to find the minimum of some function, it can easily be transformed -to a minimization problem by multiplying the function by -1. +always have to be implemented for maximization. When trying to find +the minimum of some function, the problem can easily be transformed +into a maximization problem by multiplying the function by -1. ### Example @@ -149,10 +144,11 @@ has to be set to `true`. ### Variable chromosome lengths -By default, the chromosome length is assumed to be constant. If -different candidates may have chromosomes of different lengths, -the `variable_len` parameter in the constructor of `FitnessFunctionBase` -has to be set to `true`. +By default, the chromosome length is assumed to be constant, meaning +that all of the solutions will have the same chromosome length +which will not change throughout the run. If different candidates may +have chromosomes of different lengths, the `variable_len` parameter +in the constructor of `FitnessFunctionBase` has to be set to `true`. The chromosome length still has to be specified for the fitness function, but in this case it will only be used to generate From 9f1725512b1f6c0592ec8c6567da159c062dae30 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:46:14 +0200 Subject: [PATCH 24/27] simplify the handling of relative tolerances --- src/utility/math.cpp | 4 ---- src/utility/math.hpp | 47 +++++++++++++++++++++----------------- test/unit/math.cpp | 28 +++++++++++------------ test/unit/nd_sort.cpp | 2 +- test/unit/pareto_front.cpp | 8 +++---- test/unit/pareto_sets.cpp | 2 +- test/unit/replacement.cpp | 2 +- 7 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/utility/math.cpp b/src/utility/math.cpp index c9a7e7de..beff50bc 100644 --- a/src/utility/math.cpp +++ b/src/utility/math.cpp @@ -14,10 +14,6 @@ namespace gapp::math { - constinit std::atomic Tolerances::absolute_tolerance = 1E-12; - constinit std::atomic Tolerances::relative_tolerance_epsilons = 10; - - bool paretoCompareLess(std::span lhs, std::span rhs) noexcept { GAPP_ASSERT(lhs.size() == rhs.size()); diff --git a/src/utility/math.hpp b/src/utility/math.hpp index 8dee7da2..fd85979e 100644 --- a/src/utility/math.hpp +++ b/src/utility/math.hpp @@ -30,13 +30,13 @@ namespace gapp::math template static T abs() noexcept { return T(absolute_tolerance.load(std::memory_order_acquire)); } - /** @returns The current relative tolerance used for floating-point comparisons. */ + /** @returns The current relative tolerance used for floating-point comparisons around @p at. */ template - static T eps() noexcept { return relative_tolerance_epsilons.load(std::memory_order_acquire) * std::numeric_limits::epsilon(); } + static T rel(T at) noexcept { return relative_tolerance.load(std::memory_order_acquire) * at; } private: - GAPP_API static std::atomic absolute_tolerance; - GAPP_API static std::atomic relative_tolerance_epsilons; + GAPP_API inline constinit static std::atomic absolute_tolerance = 1E-12; + GAPP_API inline constinit static std::atomic relative_tolerance = 10 * std::numeric_limits::epsilon(); friend class ScopedTolerances; }; @@ -57,19 +57,22 @@ namespace gapp::math * Create an instance of the class, setting new values for the tolerances * used for floating-point comparisons. * - * @param num_epsilons The number of epsilons to use as the relative tolerance in the comparisons. - * @param abs The absolute tolerance value used for the comparisons. + * @param abs The absolute tolerance value that will be used for the comparisons. Can't be negative. + * @param rel The relative tolerance value around 1.0 that will be used for the comparisons. Can't be negative. */ - ScopedTolerances(unsigned num_epsilons, double abs) noexcept : - old_abs_tol(Tolerances::absolute_tolerance.exchange(abs, std::memory_order_acq_rel)), - old_eps_tol(Tolerances::relative_tolerance_epsilons.exchange(num_epsilons, std::memory_order_acq_rel)) - {} + ScopedTolerances(double abs, double rel) noexcept : + old_absolute_tolerance(Tolerances::absolute_tolerance.exchange(abs, std::memory_order_acq_rel)), + old_relative_tolerance(Tolerances::relative_tolerance.exchange(rel, std::memory_order_acq_rel)) + { + GAPP_ASSERT(abs >= 0.0, "The absolute tolerance value can't be negative."); + GAPP_ASSERT(rel >= 0.0, "The relative tolerance value can't be negative."); + } /** Reset the tolerances to their previous values. */ ~ScopedTolerances() noexcept { - Tolerances::absolute_tolerance.store(old_abs_tol, std::memory_order_release); - Tolerances::relative_tolerance_epsilons.store(old_eps_tol, std::memory_order_release); + Tolerances::absolute_tolerance.store(old_absolute_tolerance, std::memory_order_release); + Tolerances::relative_tolerance.store(old_relative_tolerance, std::memory_order_release); } ScopedTolerances(const ScopedTolerances&) = delete; @@ -78,14 +81,17 @@ namespace gapp::math ScopedTolerances& operator=(ScopedTolerances&&) = delete; private: - double old_abs_tol; - unsigned old_eps_tol; + double old_absolute_tolerance; + double old_relative_tolerance; }; template inline constexpr T inf = std::numeric_limits::infinity(); + template + inline constexpr T eps = std::numeric_limits::epsilon(); + template inline constexpr T small = std::numeric_limits::min(); @@ -182,9 +188,8 @@ namespace gapp::math GAPP_ASSERT(!std::isnan(lhs) && !std::isnan(rhs)); const T diff = lhs - rhs; - const T scale = std::max(std::abs(lhs), std::abs(rhs)); - const T rel_tol = std::min(scale, std::numeric_limits::max()) * Tolerances::eps(); - const T tol = std::max(rel_tol, Tolerances::abs()); + const T scale = std::min(std::max(std::abs(lhs), std::abs(rhs)), std::numeric_limits::max()); + const T tol = std::max(Tolerances::rel(scale), Tolerances::abs()); if (diff > tol) return 1; // lhs < rhs if (diff < -tol) return -1; // lhs > rhs @@ -198,7 +203,7 @@ namespace gapp::math if (scale == inf) return lhs == rhs; // for infinities - return std::abs(lhs - rhs) <= std::max(scale * Tolerances::eps(), Tolerances::abs()); + return std::abs(lhs - rhs) <= std::max(Tolerances::rel(scale), Tolerances::abs()); } template @@ -208,7 +213,7 @@ namespace gapp::math if (scale == inf) return lhs < rhs; // for infinities - return (rhs - lhs) > std::max(scale * Tolerances::eps(), Tolerances::abs()); + return (rhs - lhs) > std::max(Tolerances::rel(scale), Tolerances::abs()); } template @@ -218,7 +223,7 @@ namespace gapp::math if (scale == inf) return lhs < rhs; // for infinities - return (rhs - lhs) > std::max(scale * Tolerances::eps(), Tolerances::abs()); + return (rhs - lhs) > std::max(Tolerances::rel(scale), Tolerances::abs()); } template @@ -228,7 +233,7 @@ namespace gapp::math if (scale == inf) return lhs > rhs; // for infinities - return (lhs - rhs) > std::max(scale * Tolerances::eps(), Tolerances::abs()); + return (lhs - rhs) > std::max(Tolerances::rel(scale), Tolerances::abs()); } template diff --git a/test/unit/math.cpp b/test/unit/math.cpp index 6f677c2f..28eb8780 100644 --- a/test/unit/math.cpp +++ b/test/unit/math.cpp @@ -22,9 +22,9 @@ TEST_CASE("fp_compare", "[math]") SECTION("is_equal") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); REQUIRE(floatIsEqual(0.0, 0.0)); @@ -69,7 +69,7 @@ TEST_CASE("fp_compare", "[math]") SECTION("approx is_equal") { - ScopedTolerances _(10, 1E-12); + ScopedTolerances _(1E-12, 10 * eps); REQUIRE(floatIsEqual(0.0, 1E-13)); REQUIRE(!floatIsEqual(0.0, 1E-11)); @@ -80,9 +80,9 @@ TEST_CASE("fp_compare", "[math]") SECTION("is_less") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); REQUIRE(!floatIsLess(0.0, 0.0)); @@ -134,7 +134,7 @@ TEST_CASE("fp_compare", "[math]") SECTION("approx is_less") { - ScopedTolerances _(10, 1E-12); + ScopedTolerances _(1E-12, 10 * eps); REQUIRE(!floatIsLess(0.0, 1E-13)); REQUIRE(floatIsLess(0.0, 1E-11)); @@ -145,9 +145,9 @@ TEST_CASE("fp_compare", "[math]") SECTION("three-way comparison") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); REQUIRE(floatCompare(0.0, 0.0) == 0); @@ -186,9 +186,9 @@ TEST_CASE("fp_compare", "[math]") SECTION("is_less_not_greater") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); REQUIRE(!floatIsLessAssumeNotGreater(0.0, 0.0)); @@ -230,9 +230,9 @@ TEST_CASE("fp_compare", "[math]") TEST_CASE("pareto_compare_less", "[math]") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); const std::vector vec = { 3.0, 2.0, 1.0 }; @@ -274,9 +274,9 @@ TEST_CASE("pareto_compare_less", "[math]") TEST_CASE("pareto_compare_three_way", "[math]") { - auto [rel, abs] = GENERATE(std::pair{ 0, 0.0 }, std::pair{ 10, 1E-12 }); + auto [abs, rel] = GENERATE(std::pair{ 0.0, 0.0 }, std::pair{ 1E-12, 10 * eps }); - ScopedTolerances _(rel, abs); + ScopedTolerances _(abs, rel); INFO("Relative tolerance eps: " << rel << ", absolute tolerance: " << abs); const std::vector vec = { 3.0, 2.0, 1.0 }; diff --git a/test/unit/nd_sort.cpp b/test/unit/nd_sort.cpp index 4524d55c..8e74245f 100644 --- a/test/unit/nd_sort.cpp +++ b/test/unit/nd_sort.cpp @@ -76,7 +76,7 @@ TEMPLATE_TEST_CASE_SIG("nd_sort", "[pareto_front]", ((auto F), F), fastNonDomina REQUIRE(std::adjacent_find(pareto_fronts.begin(), pareto_fronts.end(), [](auto lhs, auto rhs) { return (rhs.rank - lhs.rank) > 1; }) == pareto_fronts.end()); - math::ScopedTolerances _(0, 0.11); + math::ScopedTolerances _(0.11, 0.0); ParetoFronts pareto_fronts_approx = F(fmat.begin(), fmat.end()); expected_fronts[18].rank = 3; diff --git a/test/unit/pareto_front.cpp b/test/unit/pareto_front.cpp index 5e1b9f76..9d233919 100644 --- a/test/unit/pareto_front.cpp +++ b/test/unit/pareto_front.cpp @@ -27,7 +27,7 @@ TEST_CASE("find_pareto_front_1D", "[pareto_front]") SECTION("multiple optimum") { - ScopedTolerances _(0, 0.0); + ScopedTolerances _(0.0, 0.0); auto optimal_indices = findParetoFront1D(fmat); @@ -36,7 +36,7 @@ TEST_CASE("find_pareto_front_1D", "[pareto_front]") SECTION("multiple optimum approx") { - ScopedTolerances _(0, 0.1); + ScopedTolerances _(0.1, 0.0); auto optimal_indices = findParetoFront1D(fmat); @@ -75,7 +75,7 @@ TEMPLATE_TEST_CASE_SIG("find_pareto_front_nd", "[pareto_front]", ((auto F), F), SECTION("multiple optimum") { - ScopedTolerances _(0, 0.0); + ScopedTolerances _(0.0, 0.0); auto optimal_indices = F(fmat); @@ -84,7 +84,7 @@ TEMPLATE_TEST_CASE_SIG("find_pareto_front_nd", "[pareto_front]", ((auto F), F), SECTION("multiple optimum approx") { - ScopedTolerances _(0, 0.1); + ScopedTolerances _(0.1, 0.0); auto optimal_indices = F(fmat); diff --git a/test/unit/pareto_sets.cpp b/test/unit/pareto_sets.cpp index e9def9a7..031c406f 100644 --- a/test/unit/pareto_sets.cpp +++ b/test/unit/pareto_sets.cpp @@ -27,7 +27,7 @@ static constexpr bool fcomp(const Candidate& lhs, const Candidate& rhs) { TEST_CASE("merge_pareto_sets", "[pareto_front]") { - ScopedTolerances _(0, 0.0); + ScopedTolerances _(0.0, 0.0); FitnessMatrix front1 = { { 10.0, -1.0 }, // diff --git a/test/unit/replacement.cpp b/test/unit/replacement.cpp index a1a7cb63..c77e9e85 100644 --- a/test/unit/replacement.cpp +++ b/test/unit/replacement.cpp @@ -41,7 +41,7 @@ static const FitnessMatrix fitness_mat = { TEST_CASE("replacement_best", "[replacement][single-objective]") { - math::ScopedTolerances _(0, 0.0); + math::ScopedTolerances _(0.0, 0.0); std::unique_ptr replacement = std::make_unique(); const auto indices = replacement->nextPopulationImpl(context, fitness_mat.begin(), fitness_mat.begin() + POPSIZE, fitness_mat.end()); From 3843bede8081caa80236250eac513c11564305a1 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:46:35 +0200 Subject: [PATCH 25/27] add a seed method to the prng --- src/utility/rng.hpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utility/rng.hpp b/src/utility/rng.hpp index 2468d945..430c8a11 100644 --- a/src/utility/rng.hpp +++ b/src/utility/rng.hpp @@ -49,6 +49,9 @@ namespace gapp::rng /** Generate the next pseudo-random number of the sequence. Thread-safe. */ result_type operator()() noexcept; + /** Set a new seed for the generator. */ + void seed(state_type seed) noexcept; + /** @returns The smallest value that can be generated. */ static constexpr result_type min() noexcept; @@ -127,13 +130,18 @@ namespace gapp::rng { inline AtomicSplitmix64::result_type AtomicSplitmix64::operator()() noexcept { - result_type z = state_.fetch_add(0x9e3779b97f4a7c15, std::memory_order_relaxed) + 0x9e3779b97f4a7c15; + result_type z = state_.fetch_add(0x9e3779b97f4a7c15, std::memory_order_acq_rel) + 0x9e3779b97f4a7c15; z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; return z ^ (z >> 31); } + inline void AtomicSplitmix64::seed(state_type seed) noexcept + { + state_.store(seed, std::memory_order_release); + } + inline constexpr AtomicSplitmix64::result_type AtomicSplitmix64::min() noexcept { return std::numeric_limits::min(); From 393a2cbeaa1b65ba4d076d18d1ac015a41f707e6 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:46:41 +0200 Subject: [PATCH 26/27] Create 10_fp_context.cpp --- examples/10_fp_context.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/10_fp_context.cpp diff --git a/examples/10_fp_context.cpp b/examples/10_fp_context.cpp new file mode 100644 index 00000000..69a0bfdf --- /dev/null +++ b/examples/10_fp_context.cpp @@ -0,0 +1,37 @@ +/* Example showing how to change the tolerances used for floating-point comparisons in the GA. */ + +#include "gapp.hpp" // include everything +#include +#include + +using namespace gapp; + +class SinX : public FitnessFunction +{ + FitnessVector invoke(const Chromosome& x) const override { return { std::sin(x[0]) }; } +}; + +int main() +{ + std::cout << "The default absolute tolerance used is " << math::Tolerances::abs() << "\n"; + std::cout << "The default relative tolerance around 1.0 is " << math::Tolerances::rel(1.0) << "\n"; + + { + auto solutions = RCGA{}.solve(SinX{}, Bounds{ 0.0, 3.14 }); + std::cout << "The maximum of sin(x) in [0.0, 3.14] is at x = " << solutions[0].chromosome[0] << "\n"; + } + + { + math::ScopedTolerances _(/* abs = */ 0.1, /* rel = */ 0.1); + + auto solutions = RCGA{}.solve(SinX{}, Bounds{ 0.0, 3.14 }); + std::cout << "The maximum of sin(x) in [0.0, 3.14] is at x = " << solutions[0].chromosome[0] << "\n"; + } + + { + math::ScopedTolerances _(/* abs = */ 0.0, /* rel = */ 0.0); + + auto solutions = RCGA{}.solve(SinX{}, Bounds{ 0.0, 3.14 }); + std::cout << "The maximum of sin(x) in [0.0, 3.14] is at x = " << solutions[0].chromosome[0] << "\n"; + } +} From 497a16407c7cf703855bb3475e3fe44e0ca88e83 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:47:10 +0200 Subject: [PATCH 27/27] Create miscellaneous.md --- docs/miscellaneous.md | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/miscellaneous.md diff --git a/docs/miscellaneous.md b/docs/miscellaneous.md new file mode 100644 index 00000000..b6cf83a1 --- /dev/null +++ b/docs/miscellaneous.md @@ -0,0 +1,78 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. [Genetic operators](genetic-operators.md) +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. **Miscellaneous** + +------------------------------------------------------------------------------------------------ + +# Miscellaneous + +## Floating-point context + +There are multiple places during the runs of the genetic algorithms +where floating-point numbers are compared. First, the fitness vectors +of the solutions need to be compared to determine which solutions +are better. Additionally, when the GA uses real-encoding, the chromosomes +are also encoded as vectors of floating point numbers, so comparing the +candidate solutions can also involve comparing floating point numbers. + +These comparisons are not done as exact comparisons, but instead use +an absolute and a relative tolerance value. The actual tolerance used for +a comparison will be the greater of the two tolerances. The values used +for these can be found using the `math::Tolerances::abs()` and +`math::Tolerances::rel()` functions. + +```cpp +std::cout << "The default absolute tolerance used is " << math::Tolerances::abs() << "\n"; +std::cout << "The default relative tolerance around 1.0 is " << math::Tolerances::rel(1.0) << "\n"; +``` + +The tolerance values can be changed using the `ScopedTolerances` +class, which expects the new absolute and relative tolerance values +in its constructor. These values will be used for the lifetime of +the `ScopedTolerances` instance, and the destructor of the class +will reset the tolerances to their old values. + +```cpp +math::ScopedTolerances _(/* abs = */ 1e-10, /* rel = */ 1e-12); +``` + +Exact comparisons can be used by setting both tolerance values to 0. + +```cpp +math::ScopedTolerances _(0.0, 0.0); +``` + + +## Random number generation + +Several parts of the GAs depend on random numbers in their +implementations. These numbers are generated using a single +global pseudo-random number generator instance. This PRNG +instance can be accessed as `rng::prng`. There are also +several utility functions for generating random numbers using +the engine in the `rng` namespace, so the generator doesn't +have to be used directly. + +The methods of the PRNG and all of the random generation +utilities are thread-safe, and can be used freely by the user +if needed, for example in the implementation of custom +genetic operators. + +The generator is seeded using a constant value determined +by the value of the `GAPP_SEED` macro. The value of this +can be changed by defining this macro on the command line +while building and using the library. + +The PRNG can also be reseeded using its `seed` method. + +```cpp +rng::prng.seed(new_seed); +``` + +------------------------------------------------------------------------------------------------