├── test ├── source │ ├── main.cpp │ ├── random_helpers.cpp │ ├── termination.cpp │ ├── params.cpp │ ├── concepts.cpp │ ├── crossover.cpp │ ├── fitness.cpp │ ├── mutation.cpp │ ├── selection.cpp │ └── genetic.cpp └── CMakeLists.txt ├── research ├── IJCSE10-01-03-29.pdf ├── tr2_pseudocode.pdf ├── The Continuous Genetic Algorithm.pdf └── AGeneticAlgorithmForTheBinPackingProblem.pdf ├── examples ├── CMakeLists.txt └── phrase_guess.cpp ├── CONTRIBUTING.md ├── include └── genetic │ ├── op │ ├── mutation │ │ ├── no_op.h │ │ ├── value_generator.h │ │ ├── composite_mutation.h │ │ ├── value_insertion.h │ │ ├── value_replacement.h │ │ └── value_mutation.h │ ├── termination │ │ ├── fitness.h │ │ ├── generations.h │ │ └── fitness_hysteresis.h │ ├── fitness │ │ ├── accumulation.h │ │ ├── element_wise_comparison.h │ │ └── composite_fitness.h │ ├── selection │ │ ├── rank_selection.h │ │ └── roulette_selection.h │ └── crossover │ │ └── random_crossover.h │ ├── fitness.h │ ├── mutation.h │ ├── termination.h │ ├── crossover.h │ ├── selection.h │ ├── details │ ├── random_helpers.h │ ├── crossover_helpers.h │ └── concepts.h │ ├── params.h │ └── genetic.h ├── .clang-format ├── cmake ├── CPM.cmake └── tools.cmake ├── documentation ├── CMakeLists.txt └── Doxyfile ├── LICENSE ├── .cmake-format ├── .gitignore ├── CMakeLists.txt ├── README.md └── CMakePresets.json /test/source/main.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /research/IJCSE10-01-03-29.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperPaul123/genetic/HEAD/research/IJCSE10-01-03-29.pdf -------------------------------------------------------------------------------- /research/tr2_pseudocode.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperPaul123/genetic/HEAD/research/tr2_pseudocode.pdf -------------------------------------------------------------------------------- /research/The Continuous Genetic Algorithm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperPaul123/genetic/HEAD/research/The Continuous Genetic Algorithm.pdf -------------------------------------------------------------------------------- /research/AGeneticAlgorithmForTheBinPackingProblem.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperPaul123/genetic/HEAD/research/AGeneticAlgorithmForTheBinPackingProblem.pdf -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB SOURCES "*.cpp") 2 | 3 | foreach(source ${SOURCES}) 4 | get_filename_component(name ${source} NAME_WE) 5 | add_executable(${name} ${source}) 6 | target_link_libraries(${name} PUBLIC genetic) 7 | endforeach() 8 | -------------------------------------------------------------------------------- /test/source/random_helpers.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // index generator concept 5 | static_assert(dp::genetic::concepts::index_generator); 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcomed. Open a pull-request or an issue. 4 | 5 | ## Code of conduct 6 | 7 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. 8 | 9 | [code-of-conduct]: https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md 10 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/no_op.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace dp::genetic { 4 | /** 5 | * @brief No-op mutator, returns the value unchanged. 6 | */ 7 | struct no_op_mutator { 8 | template 9 | constexpr T operator()(const T& t) const { 10 | return t; 11 | } 12 | }; 13 | } // namespace dp::genetic 14 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AccessModifierOffset: '-2' 4 | AlignTrailingComments: 'true' 5 | AllowAllParametersOfDeclarationOnNextLine: 'false' 6 | BreakBeforeBraces: Attach 7 | ColumnLimit: '100' 8 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' 9 | IncludeBlocks: Regroup 10 | IndentPPDirectives: AfterHash 11 | IndentWidth: '4' 12 | NamespaceIndentation: All 13 | BreakBeforeBinaryOperators: None 14 | BreakBeforeTernaryOperators: 'true' 15 | AlwaysBreakTemplateDeclarations: 'Yes' 16 | ... 17 | -------------------------------------------------------------------------------- /include/genetic/op/termination/fitness.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace dp::genetic { 4 | namespace details { 5 | // todo:: add customization for comparison method 6 | struct fitness_termination_criteria_op { 7 | double target_fitness{}; 8 | template 9 | bool operator()(T, double fitness) { 10 | return fitness >= target_fitness; 11 | } 12 | }; 13 | } // namespace details 14 | 15 | /** 16 | * @brief Fitness termination criteria for case where fitness is greater than or equal to target 17 | * fitness. 18 | */ 19 | using fitness_termination = details::fitness_termination_criteria_op; 20 | 21 | } // namespace dp::genetic 22 | -------------------------------------------------------------------------------- /include/genetic/op/fitness/accumulation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace dp::genetic { 6 | namespace details { 7 | struct accumulation_fitness_op { 8 | template 9 | constexpr ScoreType operator()(Range&& value) const { 10 | return std::accumulate(std::ranges::begin(value), std::ranges::end(value), 11 | ScoreType{}); 12 | } 13 | }; 14 | } // namespace details 15 | 16 | /// @brief Fitness operator that accumulates the fitness of a range of values. 17 | inline constexpr auto accumulation_fitness = details::accumulation_fitness_op{}; 18 | } // namespace dp::genetic 19 | -------------------------------------------------------------------------------- /test/source/termination.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | TEST_CASE("Generations termination") { 6 | std::string chromosome{}; 7 | auto termination = dp::genetic::generations_termination(1234); 8 | std::uint64_t count{1}; 9 | while (!dp::genetic::should_terminate(termination, chromosome, 0.0)) { 10 | ++count; 11 | } 12 | 13 | CHECK_EQ(count, termination.max_generations); 14 | } 15 | 16 | TEST_CASE("Fitness termination") { 17 | std::string chromosome{}; 18 | auto fitness_term = dp::genetic::fitness_termination{100.}; 19 | CHECK(dp::genetic::should_terminate(fitness_term, chromosome, 110.0)); 20 | CHECK_FALSE(dp::genetic::should_terminate(fitness_term, chromosome, 99.99)); 21 | } 22 | -------------------------------------------------------------------------------- /test/source/params.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | TEST_CASE("Testing constructing genetic params") { 9 | dp::genetic::params> pars( 10 | [](std::vector val) -> double { 11 | return std::accumulate(val.begin(), val.end(), 0.0); 12 | }, 13 | [](const std::vector&) -> std::vector { return {}; }); 14 | } 15 | 16 | TEST_CASE("Create params with builder") { 17 | const auto genetic_params = dp::genetic::params::builder() 18 | .with_fitness_operator([](const std::string&) { return 0.0; }) 19 | .build(); 20 | 21 | CHECK(genetic_params.fitness_operator()("") == 0.0); 22 | } 23 | -------------------------------------------------------------------------------- /cmake/CPM.cmake: -------------------------------------------------------------------------------- 1 | set(CPM_DOWNLOAD_VERSION 0.40.2) 2 | 3 | if(CPM_SOURCE_CACHE) 4 | # Expand relative path. This is important if the provided path contains a tilde (~) 5 | get_filename_component(CPM_SOURCE_CACHE ${CPM_SOURCE_CACHE} ABSOLUTE) 6 | set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 7 | elseif(DEFINED ENV{CPM_SOURCE_CACHE}) 8 | set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 9 | else() 10 | set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 11 | endif() 12 | 13 | if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) 14 | message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") 15 | file(DOWNLOAD 16 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 17 | ${CPM_DOWNLOAD_LOCATION} 18 | ) 19 | endif() 20 | 21 | include(${CPM_DOWNLOAD_LOCATION}) 22 | -------------------------------------------------------------------------------- /include/genetic/op/termination/generations.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace dp::genetic { 6 | namespace details { 7 | struct generations_termination_op { 8 | std::uint_least64_t max_generations{1000}; 9 | explicit generations_termination_op(std::uint_least64_t max_gens = 1000) 10 | : max_generations(max_gens), count_(max_gens) {} 11 | template 12 | [[nodiscard]] constexpr bool operator()(T, double) { 13 | count_--; 14 | return count_ == 0; 15 | } 16 | 17 | private: 18 | std::uint_least64_t count_{max_generations}; 19 | }; 20 | } // namespace details 21 | 22 | /** 23 | * @brief Termination criteria for a fixed number of generations. 24 | */ 25 | using generations_termination = details::generations_termination_op; 26 | } // namespace dp::genetic 27 | -------------------------------------------------------------------------------- /documentation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20 FATAL_ERROR) 2 | 3 | project(genetic_docs) 4 | 5 | # ---- Dependencies ---- 6 | 7 | include(../cmake/CPM.cmake) 8 | 9 | CPMAddPackage("gh:jothepro/doxygen-awesome-css#v1.6.1") 10 | find_package(Doxygen REQUIRED) 11 | 12 | # ---- Doxygen variables ---- 13 | 14 | # set Doxyfile variables 15 | set(DOXYGEN_PROJECT_NAME genetic) 16 | set(DOXYGEN_PROJECT_VERSION ${genetic_VERSION}) 17 | set(DOXYGEN_PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/..") 18 | set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/doxygen") 19 | 20 | configure_file(${CMAKE_CURRENT_LIST_DIR}/Doxyfile ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) 21 | 22 | add_custom_target( 23 | GenerateDocs 24 | ${CMAKE_COMMAND} -E make_directory "${DOXYGEN_OUTPUT_DIRECTORY}" 25 | COMMAND Doxygen::doxygen ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile 26 | COMMAND echo "Docs written to: ${DOXYGEN_OUTPUT_DIRECTORY}" 27 | WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" 28 | ) 29 | -------------------------------------------------------------------------------- /include/genetic/fitness.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include "genetic/details/concepts.h" 7 | #include "genetic/op/fitness/accumulation.h" 8 | #include "genetic/op/fitness/composite_fitness.h" 9 | #include "genetic/op/fitness/element_wise_comparison.h" 10 | 11 | namespace dp::genetic { 12 | /** 13 | * @brief Evaluates fitness of a range using the provided fitness operator. 14 | * 15 | * @tparam Range The input type. 16 | * @tparam FitnessOp The fitness operator type. 17 | * @tparam T The return type of the fitness operator. 18 | */ 19 | template , Range>> 21 | requires concepts::fitness_operator, Range> 22 | constexpr T evaluate_fitness(FitnessOp&& fitness_op, const Range& range) { 23 | return std::invoke(std::forward(fitness_op), range); 24 | } 25 | } // namespace dp::genetic 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul Tsouchlos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /include/genetic/mutation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "details/random_helpers.h" 7 | #include "genetic/details/concepts.h" 8 | #include "genetic/op/mutation/composite_mutation.h" 9 | #include "genetic/op/mutation/no_op.h" 10 | #include "genetic/op/mutation/value_generator.h" 11 | #include "genetic/op/mutation/value_insertion.h" 12 | #include "genetic/op/mutation/value_mutation.h" 13 | #include "genetic/op/mutation/value_replacement.h" 14 | 15 | namespace dp::genetic { 16 | 17 | /** 18 | * @brief Mutate a value using the provided mutation operator. 19 | * 20 | * @tparam T The input value type. 21 | * @tparam Mutator The mutation operator. 22 | * @param mutator The mutation operator, takes in a value and returns a mutated value. 23 | * @param input_value The input value to mutate. 24 | * @return The mutated value. 25 | */ 26 | template Mutator> 27 | [[nodiscard]] constexpr T mutate(Mutator&& mutator, const T& input_value) { 28 | return std::invoke(std::forward(mutator), input_value); 29 | } 30 | } // namespace dp::genetic 31 | -------------------------------------------------------------------------------- /include/genetic/termination.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "genetic/details/concepts.h" 6 | #include "genetic/op/termination/fitness.h" 7 | #include "genetic/op/termination/fitness_hysteresis.h" 8 | #include "genetic/op/termination/generations.h" 9 | 10 | namespace dp::genetic { 11 | /** 12 | * @brief Determines if the genetic algorithm should terminate. 13 | * @details This function is a helper function that will call the termination operator 14 | * with the given fitness value and the termination object. 15 | * @tparam T The type of the termination operator. 16 | * @tparam TerminationOp The type of the termination object. 17 | * @param termination_op The termination operator. 18 | * @param t The termination object. 19 | * @param fitness The fitness value. 20 | * @return True if the algorithm should terminate, false otherwise. 21 | */ 22 | template 23 | requires dp::genetic::concepts::termination_operator 24 | constexpr bool should_terminate(TerminationOp &&termination_op, T &&t, double fitness) { 25 | return std::invoke(std::forward(termination_op), t, fitness); 26 | } 27 | } // namespace dp::genetic 28 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/value_generator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "genetic/details/concepts.h" 7 | #include "genetic/details/random_helpers.h" 8 | 9 | namespace dp::genetic { 10 | /** 11 | * @brief Generates a value from a pool of possible values. Value is selected randomly 12 | */ 13 | template >, 15 | dp::genetic::concepts::index_generator IndexGenerator = 16 | dp::genetic::uniform_integral_generator> 17 | struct pooled_value_generator { 18 | pooled_value_generator(const T possible_values) : values_(std::move(possible_values)) {} 19 | [[nodiscard]] constexpr ValueType operator()() { 20 | static IndexGenerator index_generator{}; 21 | const auto output_index = 22 | index_generator(static_cast(0), 23 | static_cast(std::ranges::size(values_) - 1)); 24 | 25 | auto location = std::ranges::begin(values_) + output_index; 26 | return *location; 27 | } 28 | 29 | private: 30 | T values_; 31 | }; 32 | } // namespace dp::genetic 33 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/composite_mutation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace dp::genetic { 4 | /** 5 | * @brief Composite mutator that chains together multiple mutation operators. 6 | * 7 | * @tparam Args The mutation operators to chain together. 8 | */ 9 | template 10 | struct composite_mutator { 11 | // TODO: constraint args to be mutation operators? 12 | 13 | private: 14 | // list of mutators 15 | std::tuple mutators_; 16 | // https://stackoverflow.com/questions/75039429/chaining-callables-in-c 17 | /// @brief Helper to help us chain mutator calls together 18 | template 19 | decltype(auto) call_helper(Arg&& arg) { 20 | if constexpr (index == 0) { 21 | return std::forward(arg); 22 | } else { 23 | return std::get(mutators_)( 24 | call_helper(std::forward(arg))); 25 | } 26 | } 27 | 28 | public: 29 | explicit composite_mutator(Args&&... args) : mutators_(std::move(args)...) {} 30 | template 31 | T operator()(T t) { 32 | return call_helper(t); 33 | } 34 | }; 35 | } // namespace dp::genetic 36 | -------------------------------------------------------------------------------- /include/genetic/op/selection/rank_selection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include "genetic/details/concepts.h" 5 | #include "genetic/op/selection/roulette_selection.h" 6 | 7 | namespace dp::genetic { 8 | /** 9 | * @brief Perform rank selection on a population. 10 | */ 11 | struct rank_selection { 12 | template , 14 | typename FitnessResult = std::invoke_result_t> 15 | std::pair operator()(Range population, UnaryOperator fitness_op) { 16 | // assume population is sorted already 17 | auto reverse_view = population | std::views::reverse; 18 | 19 | // fitness evaluator/operator 20 | auto rank_fitness_op = [&](const T& value) -> FitnessResult { 21 | auto location = std::find(reverse_view.begin(), reverse_view.end(), value); 22 | return static_cast( 23 | static_cast(std::distance(reverse_view.begin(), location)) + 1.0); 24 | }; 25 | 26 | roulette_selection selection{}; 27 | // use roulette selection for the rest 28 | return selection(reverse_view, rank_fitness_op); 29 | } 30 | }; 31 | } // namespace dp::genetic -------------------------------------------------------------------------------- /include/genetic/crossover.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "genetic/details/concepts.h" 8 | #include "genetic/details/crossover_helpers.h" 9 | #include "genetic/details/random_helpers.h" 10 | #include "genetic/op/crossover/random_crossover.h" 11 | 12 | namespace dp::genetic { 13 | namespace details { 14 | struct make_children_fn { 15 | template CrossoverOp, 16 | typename SimpleType = std::remove_cvref_t, 17 | typename IndexProvider = genetic::uniform_integral_generator> 18 | requires(std::is_default_constructible_v || 19 | std::is_trivially_default_constructible_v) 20 | constexpr auto operator()(CrossoverOp &&crossover_op, const T &first, const T &second) { 21 | return std::invoke(std::forward(crossover_op), first, second); 22 | } 23 | }; 24 | 25 | } // namespace details 26 | 27 | /** 28 | * @brief A helper function to create children from a crossover operator. 29 | * @details Takes in a crossover operator, two parents and returns the children. 30 | */ 31 | inline auto make_children = details::make_children_fn{}; 32 | 33 | } // namespace dp::genetic 34 | -------------------------------------------------------------------------------- /documentation/Doxyfile: -------------------------------------------------------------------------------- 1 | # Configuration for Doxygen for use with CMake 2 | # Only options that deviate from the default are included 3 | # To create a new Doxyfile containing all available options, call `doxygen -g` 4 | 5 | # Get Project name and version from CMake 6 | PROJECT_NAME = @DOXYGEN_PROJECT_NAME@ 7 | PROJECT_NUMBER = @DOXYGEN_PROJECT_VERSION@ 8 | 9 | # Add sources 10 | INPUT = @DOXYGEN_PROJECT_ROOT@/README.md @DOXYGEN_PROJECT_ROOT@/include @DOXYGEN_PROJECT_ROOT@/documentation/pages 11 | EXTRACT_ALL = YES 12 | EXTRACT_PRIVATE = NO 13 | RECURSIVE = YES 14 | OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT_DIRECTORY@ 15 | 16 | # Use the README as a main page 17 | USE_MDFILE_AS_MAINPAGE = @DOXYGEN_PROJECT_ROOT@/README.md 18 | 19 | # set relative include paths 20 | FULL_PATH_NAMES = YES 21 | STRIP_FROM_PATH = @DOXYGEN_PROJECT_ROOT@/include @DOXYGEN_PROJECT_ROOT@ 22 | 23 | # We use m.css to generate the html documentation, so we only need XML output 24 | GENERATE_XML = NO 25 | GENERATE_HTML = YES 26 | GENERATE_LATEX = YES 27 | XML_PROGRAMLISTING = NO 28 | CREATE_SUBDIRS = YES 29 | 30 | HTML_EXTRA_STYLESHEET = @doxygen-awesome-css_SOURCE_DIR@/doxygen-awesome.css 31 | HTML_COLORSTYLE_HUE = 209 32 | HTML_COLORSTYLE_SAT = 255 33 | HTML_COLORSTYLE_GAMMA = 113 34 | GENERATE_TREEVIEW = YES 35 | HAVE_DOT = YES 36 | DOT_IMAGE_FORMAT = svg 37 | DOT_TRANSPARENT = YES 38 | EXAMPLE_PATH = @DOXYGEN_PROJECT_ROOT@/examples/ 39 | -------------------------------------------------------------------------------- /.cmake-format: -------------------------------------------------------------------------------- 1 | format: 2 | tab_size: 4 3 | line_width: 100 4 | dangle_parens: true 5 | max_pargs_hwrap: 4 6 | 7 | parse: 8 | additional_commands: 9 | cpmaddpackage: 10 | pargs: 11 | nargs: '*' 12 | flags: [] 13 | spelling: CPMAddPackage 14 | kwargs: &cpmaddpackagekwargs 15 | NAME: 1 16 | FORCE: 1 17 | VERSION: 1 18 | GIT_TAG: 1 19 | DOWNLOAD_ONLY: 1 20 | GITHUB_REPOSITORY: 1 21 | GITLAB_REPOSITORY: 1 22 | GIT_REPOSITORY: 1 23 | SVN_REPOSITORY: 1 24 | SVN_REVISION: 1 25 | SOURCE_DIR: 1 26 | DOWNLOAD_COMMAND: 1 27 | FIND_PACKAGE_ARGUMENTS: 1 28 | NO_CACHE: 1 29 | GIT_SHALLOW: 1 30 | URL: 1 31 | URL_HASH: 1 32 | URL_MD5: 1 33 | DOWNLOAD_NAME: 1 34 | DOWNLOAD_NO_EXTRACT: 1 35 | HTTP_USERNAME: 1 36 | HTTP_PASSWORD: 1 37 | OPTIONS: + 38 | cpmfindpackage: 39 | pargs: 40 | nargs: '*' 41 | flags: [] 42 | spelling: CPMFindPackage 43 | kwargs: *cpmaddpackagekwargs 44 | packageproject: 45 | pargs: 46 | nargs: '*' 47 | flags: [] 48 | spelling: packageProject 49 | kwargs: 50 | NAME: 1 51 | VERSION: 1 52 | NAMESPACE: 1 53 | INCLUDE_DIR: 1 54 | INCLUDE_DESTINATION: 1 55 | BINARY_DIR: 1 56 | COMPATIBILITY: 1 57 | VERSION_HEADER: 1 58 | DEPENDENCIES: + 59 | -------------------------------------------------------------------------------- /include/genetic/op/fitness/element_wise_comparison.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace dp::genetic { 6 | 7 | /** 8 | * @brief Fitness operator that compares two ranges element-wise. 9 | * 10 | * @tparam Range The chromosome std::range type 11 | * @tparam ScoreType 12 | */ 13 | template 14 | struct element_wise_comparison { 15 | explicit element_wise_comparison(Range solution, ScoreType match_score) 16 | : solution_(std::move(solution)), match_score_(std::move(match_score)) {} 17 | constexpr ScoreType operator()(const Range& value) const { 18 | ScoreType score{}; 19 | const auto sol_length = std::ranges::distance(solution_); 20 | const auto val_length = std::ranges::distance(value); 21 | std::ranges::range_difference_t index; 22 | for (index = 0; index < std::min(sol_length, val_length); ++index) { 23 | auto solution_val = solution_.at(index); 24 | auto val = value.at(index); 25 | if (val == solution_val) score += match_score_; 26 | } 27 | 28 | // subtract for difference in length 29 | if (sol_length != val_length) { 30 | score -= std::abs(sol_length - val_length); 31 | } 32 | 33 | return score; 34 | } 35 | 36 | private: 37 | Range solution_; 38 | ScoreType match_score_; 39 | }; 40 | } // namespace dp::genetic 41 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20 FATAL_ERROR) 2 | 3 | project(genetic_tests LANGUAGES CXX) 4 | 5 | # ---- Options ---- 6 | option(TEST_INSTALLED_VERSION "Test the version found by find_package" OFF) 7 | 8 | # --- Import tools ---- 9 | 10 | include(../cmake/tools.cmake) 11 | 12 | # ---- Dependencies ---- 13 | include(../cmake/CPM.cmake) 14 | 15 | CPMAddPackage("gh:doctest/doctest#v2.4.11") 16 | CPMAddPackage("gh:TheLartians/Format.cmake@1.7.0") 17 | 18 | if(TEST_INSTALLED_VERSION) 19 | find_package(dp::genetic REQUIRED) 20 | endif() 21 | 22 | # ---- Create binary ---- 23 | 24 | file(GLOB sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp) 25 | add_executable(${PROJECT_NAME} ${sources}) 26 | target_link_libraries(${PROJECT_NAME} doctest::doctest dp::genetic) 27 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) 28 | 29 | # enable compiler warnings 30 | if(NOT TEST_INSTALLED_VERSION) 31 | target_compile_options( 32 | ${PROJECT_NAME} INTERFACE $<$:-Wall -Wpedantic -Wextra 33 | -Werror> 34 | ) 35 | target_compile_options(${PROJECT_NAME} INTERFACE $<$:/W4 /WX>) 36 | target_compile_definitions( 37 | ${PROJECT_NAME} PUBLIC $<$:DOCTEST_CONFIG_USE_STD_HEADERS> 38 | ) 39 | endif() 40 | 41 | # Note: doctest and similar testing frameworks can automatically configure CMake tests. For other 42 | include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake) 43 | doctest_discover_tests(${PROJECT_NAME}) 44 | -------------------------------------------------------------------------------- /include/genetic/selection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "genetic/op/selection/rank_selection.h" 12 | #include "genetic/op/selection/roulette_selection.h" 13 | 14 | namespace dp::genetic { 15 | 16 | /** 17 | * @brief Select parents from a population using the provided selection operator. 18 | * 19 | * @tparam Population 20 | * @tparam SelectionOperator 21 | * @tparam FitnessOperator 22 | * @tparam T 23 | * @tparam FitnessResult 24 | * @tparam T> 25 | */ 26 | template , 28 | typename FitnessResult = std::invoke_result_t> 29 | requires concepts::fitness_operator && 30 | concepts::selection_operator 31 | constexpr inline std::pair select_parents(SelectionOperator&& selection_op, 32 | Population&& population, 33 | FitnessOperator&& fitness_op) { 34 | return std::invoke(std::forward(selection_op), 35 | std::forward(population), 36 | std::forward(fitness_op)); 37 | } 38 | 39 | } // namespace dp::genetic 40 | -------------------------------------------------------------------------------- /test/source/concepts.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace dp::genetic::type_traits; 8 | 9 | /// @brief type traits concepts tests 10 | /// @{ 11 | 12 | // value type 13 | static_assert(has_value_type>); 14 | static_assert(has_value_type &>); 15 | 16 | // size_type 17 | static_assert(has_size_type>); 18 | static_assert(has_size_type); 19 | 20 | // size 21 | static_assert(has_size>); 22 | static_assert(has_size); 23 | 24 | // has push back 25 | static_assert(has_push_back>); 26 | static_assert(has_push_back); 27 | 28 | // has reserve 29 | static_assert(has_reserve>); 30 | static_assert(has_reserve); 31 | 32 | // has empty 33 | static_assert(has_empty>); 34 | static_assert(has_empty); 35 | static_assert(!has_empty); 36 | 37 | // number concept 38 | static_assert(number); 39 | static_assert(number); 40 | static_assert(number); 41 | static_assert(number); 42 | static_assert(number); 43 | static_assert(number); 44 | static_assert(number); 45 | 46 | // addable 47 | static_assert(addable); 48 | static_assert(addable); 49 | static_assert(addable); 50 | static_assert(addable); 51 | static_assert(addable); 52 | static_assert(!addable>); 53 | 54 | struct sample { 55 | std::int32_t first{}; 56 | std::int64_t second{}; 57 | }; 58 | 59 | sample operator+(const sample &first, const sample &second) { 60 | return {.first = first.first + second.first, .second = first.second + second.second}; 61 | } 62 | 63 | static_assert(addable); 64 | 65 | /// @} -------------------------------------------------------------------------------- /include/genetic/details/random_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace dp::genetic { 7 | namespace details { 8 | template 10 | T initialize_random_engine() { 11 | std::random_device source; 12 | auto random_data = 13 | std::views::iota(std::size_t(), (NumberOfSeeds - 1) / sizeof(source()) + 1) | 14 | std::views::transform([&](auto) { return source(); }); 15 | std::seed_seq seeds(std::begin(random_data), std::end(random_data)); 16 | return T(seeds); 17 | } 18 | 19 | template 20 | T initialize_random_engine(const std::seed_seq &seed_seq) { 21 | return T(seed_seq); 22 | } 23 | } // namespace details 24 | 25 | struct uniform_integral_generator { 26 | template 27 | auto operator()(const T &lower_bound, const T &upper_bound) { 28 | // generate random crossover points 29 | thread_local auto device = details::initialize_random_engine(); 30 | std::uniform_int_distribution dist(lower_bound, upper_bound); 31 | return dist(device); 32 | } 33 | }; 34 | 35 | struct uniform_floating_point_generator { 36 | template 38 | auto operator()(const T &lower_bound, const T &upper_bound) { 39 | thread_local auto device = details::initialize_random_engine(); 40 | std::uniform_real_distribution dist(lower_bound, upper_bound); 41 | return dist(device); 42 | } 43 | }; 44 | } // namespace dp::genetic 45 | -------------------------------------------------------------------------------- /include/genetic/op/termination/fitness_hysteresis.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | namespace dp::genetic { 4 | namespace details { 5 | /** 6 | * @brief Checks if fitness changes by a significant amount within a given number of 7 | * generations. 8 | * @details Checks if previous fitness and the new fitness differ by at least some 9 | * threshold. If they don't, a generations count is incremented. When the generation count 10 | * reaches the max, the functor returns true to terminate. 11 | */ 12 | struct fitness_hysteresis_op { 13 | double fitness_variation_threshold{1.0}; 14 | std::uint_least64_t max_generations_between_changes{1000}; 15 | explicit fitness_hysteresis_op(double fitness_threshold, 16 | std::uint_least64_t max_generations_between) 17 | : fitness_variation_threshold(fitness_threshold), 18 | max_generations_between_changes(max_generations_between) {} 19 | template 20 | [[nodiscard]] constexpr bool operator()(T, double fitness) { 21 | if (std::abs(previous_fitness_ - fitness) > fitness_variation_threshold) { 22 | // significant change in fitness 23 | previous_fitness_ = fitness; 24 | // reset our count 25 | count_ = 0; 26 | } else { 27 | // fitness did not change significantly 28 | count_++; 29 | } 30 | 31 | if (count_ >= max_generations_between_changes) return true; 32 | 33 | return false; 34 | } 35 | 36 | private: 37 | double previous_fitness_{}; 38 | std::uint_least64_t count_{}; 39 | }; 40 | } // namespace details 41 | 42 | using fitness_hysteresis = details::fitness_hysteresis_op; 43 | } // namespace dp::genetic 44 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/value_insertion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "genetic/details/concepts.h" 7 | #include "genetic/op/mutation/value_generator.h" 8 | 9 | namespace dp::genetic { 10 | /** 11 | * @brief Inserts n values at random positions by randomly selecting values from the input 12 | * range. 13 | * @tparam T Population type 14 | * @tparam IndexGenerator Random number generator that generates indices 15 | */ 16 | template , 17 | dp::genetic::concepts::index_generator IndexGenerator = 18 | dp::genetic::uniform_integral_generator> 19 | struct value_insertion_mutator { 20 | template 21 | requires std::invocable && 22 | std::convertible_to, ValueType> 23 | explicit value_insertion_mutator(ValueGenerator&& generator, 24 | std::uint_least64_t number_of_insertions = 1) 25 | : generator_(std::forward(generator)), 26 | number_of_insertions_(number_of_insertions) {} 27 | 28 | template 29 | [[nodiscard]] T operator()(const T& value) { 30 | static IndexGenerator index_generator{}; 31 | T return_value = value; 32 | 33 | for (std::size_t i = 0; i < number_of_insertions_; ++i) { 34 | const auto output_index = 35 | index_generator(static_cast(0), 36 | static_cast(std::ranges::size(value)) - 1); 37 | auto location = std::ranges::begin(return_value) + output_index; 38 | // todo: abstract the insertion logic 39 | return_value.insert(location, std::invoke(generator_)); 40 | } 41 | 42 | return return_value; 43 | } 44 | 45 | private: 46 | std::function generator_; 47 | std::uint_least64_t number_of_insertions_; 48 | }; 49 | } // namespace dp::genetic 50 | -------------------------------------------------------------------------------- /cmake/tools.cmake: -------------------------------------------------------------------------------- 1 | # this file contains a list of tools that can be activated and downloaded on-demand each tool is 2 | # enabled during configuration by passing an additional `-DUSE_=` argument to CMake 3 | 4 | # only activate tools for top level project 5 | if(NOT PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 6 | return() 7 | endif() 8 | 9 | include(${CMAKE_CURRENT_LIST_DIR}/CPM.cmake) 10 | 11 | # enables sanitizers support using the the `USE_SANITIZER` flag available values are: Address, 12 | # Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined' 13 | if(USE_SANITIZER OR USE_STATIC_ANALYZER) 14 | CPMAddPackage("gh:StableCoder/cmake-scripts#1f822d1fc87c8d7720c074cde8a278b44963c354") 15 | 16 | if(USE_SANITIZER) 17 | include(${cmake-scripts_SOURCE_DIR}/sanitizers.cmake) 18 | endif() 19 | 20 | if(USE_STATIC_ANALYZER) 21 | if("clang-tidy" IN_LIST USE_STATIC_ANALYZER) 22 | set(CLANG_TIDY 23 | ON 24 | CACHE INTERNAL "" 25 | ) 26 | else() 27 | set(CLANG_TIDY 28 | OFF 29 | CACHE INTERNAL "" 30 | ) 31 | endif() 32 | if("iwyu" IN_LIST USE_STATIC_ANALYZER) 33 | set(IWYU 34 | ON 35 | CACHE INTERNAL "" 36 | ) 37 | else() 38 | set(IWYU 39 | OFF 40 | CACHE INTERNAL "" 41 | ) 42 | endif() 43 | if("cppcheck" IN_LIST USE_STATIC_ANALYZER) 44 | set(CPPCHECK 45 | ON 46 | CACHE INTERNAL "" 47 | ) 48 | else() 49 | set(CPPCHECK 50 | OFF 51 | CACHE INTERNAL "" 52 | ) 53 | endif() 54 | 55 | include(${cmake-scripts_SOURCE_DIR}/tools.cmake) 56 | 57 | clang_tidy(${CLANG_TIDY_ARGS}) 58 | include_what_you_use(${IWYU_ARGS}) 59 | cppcheck(${CPPCHECK_ARGS}) 60 | endif() 61 | endif() 62 | 63 | # enables CCACHE support through the USE_CCACHE flag possible values are: YES, NO or equivalent 64 | if(USE_CCACHE) 65 | CPMAddPackage("gh:TheLartians/Ccache.cmake@1.2.3") 66 | endif() 67 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/value_replacement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "genetic/details/concepts.h" 8 | 9 | namespace dp::genetic { 10 | 11 | /** 12 | * @brief Replaces a random value in the range with a new value. 13 | */ 14 | template < 15 | std::ranges::sized_range Range, 16 | dp::genetic::concepts::value_generator> ValueGenerator, 17 | dp::genetic::concepts::index_generator IndexGenerator = 18 | dp::genetic::uniform_integral_generator> 19 | struct value_replacement { 20 | /** 21 | * @brief Construct a new value replacement object with a given value generator and the 22 | * number of replacements to make. 23 | * 24 | * @param generator A callable object that generates a new value. 25 | * @param num_replacements The number of replacements to make. 26 | */ 27 | explicit value_replacement(const ValueGenerator& generator, 28 | std::uint_least64_t num_replacements = 1) 29 | : generator_(generator), number_of_replacements_(num_replacements) {} 30 | 31 | template 32 | constexpr T operator()(const T& t) { 33 | static IndexGenerator index_generator{}; 34 | auto return_value = t; 35 | for (std::uint_least64_t _ : 36 | std::views::iota(static_cast(0), number_of_replacements_)) { 37 | const auto output_index = 38 | index_generator(static_cast(0), 39 | static_cast(std::ranges::size(t) - 1)); 40 | 41 | auto location = std::ranges::begin(return_value) + output_index; 42 | auto value = std::invoke(generator_); 43 | // ensure we don't replace the value with the same value 44 | while (value == *location) value = std::invoke(generator_); 45 | std::swap(*location, value); 46 | } 47 | return return_value; 48 | } 49 | 50 | private: 51 | ValueGenerator generator_; 52 | std::uint_least64_t number_of_replacements_; 53 | }; 54 | 55 | } // namespace dp::genetic 56 | -------------------------------------------------------------------------------- /include/genetic/op/selection/roulette_selection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "genetic/details/concepts.h" 6 | #include "genetic/details/random_helpers.h" 7 | 8 | namespace dp::genetic { 9 | /** 10 | * @brief Perform roulette selection on a population. 11 | */ 12 | struct roulette_selection { 13 | template , 15 | typename FitnessResult = std::invoke_result_t> 16 | std::pair operator()(Range population, UnaryOperator fitness_op) { 17 | // convert our data to work with std::accumulate 18 | auto data = population | std::views::common; 19 | // generate sum 20 | FitnessResult sum = std::accumulate(data.begin(), data.end(), FitnessResult{}, 21 | [&](FitnessResult current_sum, const T& value) { 22 | return current_sum + fitness_op(value); 23 | }); 24 | 25 | thread_local auto generator = uniform_floating_point_generator{}; 26 | auto first_value = generator(0.0, 1.0); 27 | auto second_value = generator(0.0, 1.0); 28 | 29 | auto threshold1 = first_value * sum; 30 | auto threshold2 = second_value * sum; 31 | 32 | std::pair return_pair{}; 33 | 34 | auto first_found = false; 35 | auto second_found = false; 36 | 37 | FitnessResult accumulator{}; 38 | for (const auto& value : population) { 39 | accumulator += fitness_op(value); 40 | if (accumulator >= threshold1 && !first_found) { 41 | return_pair.first = value; 42 | first_found = true; 43 | } 44 | if (accumulator >= threshold2 && !second_found) { 45 | return_pair.second = value; 46 | second_found = true; 47 | } 48 | if (first_found && second_found) break; 49 | } 50 | 51 | // pick 2 parents and return them 52 | return return_pair; 53 | } 54 | }; 55 | } // namespace dp::genetic 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build* 2 | /.vscode 3 | /cpm_modules 4 | .DS_Store 5 | .vs 6 | out 7 | CMakeFiles 8 | 9 | # c++ ignores 10 | # Prerequisites 11 | *.d 12 | 13 | # Compiled Object files 14 | *.slo 15 | *.lo 16 | *.o 17 | *.obj 18 | 19 | # Precompiled Headers 20 | *.gch 21 | *.pch 22 | 23 | # Compiled Dynamic libraries 24 | *.so 25 | *.dylib 26 | *.dll 27 | 28 | # Fortran module files 29 | *.mod 30 | *.smod 31 | 32 | # Compiled Static libraries 33 | *.lai 34 | *.la 35 | *.a 36 | *.lib 37 | 38 | # Executables 39 | *.exe 40 | *.out 41 | *.app 42 | 43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 45 | 46 | # User-specific stuff 47 | .idea/**/workspace.xml 48 | .idea/**/tasks.xml 49 | .idea/**/usage.statistics.xml 50 | .idea/**/dictionaries 51 | .idea/**/shelf 52 | 53 | # AWS User-specific 54 | .idea/**/aws.xml 55 | 56 | # Generated files 57 | .idea/**/contentModel.xml 58 | 59 | # Sensitive or high-churn files 60 | .idea/**/dataSources/ 61 | .idea/**/dataSources.ids 62 | .idea/**/dataSources.local.xml 63 | .idea/**/sqlDataSources.xml 64 | .idea/**/dynamic.xml 65 | .idea/**/uiDesigner.xml 66 | .idea/**/dbnavigator.xml 67 | 68 | # Gradle 69 | .idea/**/gradle.xml 70 | .idea/**/libraries 71 | 72 | # Gradle and Maven with auto-import 73 | # When using Gradle or Maven with auto-import, you should exclude module files, 74 | # since they will be recreated, and may cause churn. Uncomment if using 75 | # auto-import. 76 | # .idea/artifacts 77 | # .idea/compiler.xml 78 | # .idea/jarRepositories.xml 79 | # .idea/modules.xml 80 | # .idea/*.iml 81 | # .idea/modules 82 | # *.iml 83 | # *.ipr 84 | 85 | # CMake 86 | cmake-build-*/ 87 | 88 | # Mongo Explorer plugin 89 | .idea/**/mongoSettings.xml 90 | 91 | # File-based project format 92 | *.iws 93 | 94 | # IntelliJ 95 | out/ 96 | 97 | # mpeltonen/sbt-idea plugin 98 | .idea_modules/ 99 | 100 | # JIRA plugin 101 | atlassian-ide-plugin.xml 102 | 103 | # Cursive Clojure plugin 104 | .idea/replstate.xml 105 | 106 | # SonarLint plugin 107 | .idea/sonarlint/ 108 | 109 | # Crashlytics plugin (for Android Studio and IntelliJ) 110 | com_crashlytics_export_strings.xml 111 | crashlytics.properties 112 | crashlytics-build.properties 113 | fabric.properties 114 | 115 | # Editor-based Rest Client 116 | .idea/httpRequests 117 | 118 | # Android studio 3.1+ serialized cache file 119 | .idea/caches/build_file_checksums.ser 120 | 121 | # Visual Studio Code 122 | .vscode/* 123 | !.vscode/settings.json 124 | !.vscode/tasks.json 125 | !.vscode/launch.json 126 | !.vscode/extensions.json 127 | !.vscode/*.code-snippets 128 | 129 | # Local History for Visual Studio Code 130 | .history/ 131 | 132 | # Built Visual Studio Code Extensions 133 | *.vsix -------------------------------------------------------------------------------- /include/genetic/op/crossover/random_crossover.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "genetic/details/crossover_helpers.h" 7 | 8 | namespace dp::genetic { 9 | /** 10 | * @brief Randomly crosses over two parent ranges to produce a child range. 11 | * @details The pivot index (where the "splice" occurs) is randomly chosen using an 12 | * IndexProvider which defaults to a uniform integral generator. If the parents are empty, 13 | * the child will be default constructed. 14 | */ 15 | struct random_crossover { 16 | template , 17 | typename IndexProvider = genetic::uniform_integral_generator> 18 | requires(std::is_default_constructible_v || 19 | std::is_trivially_default_constructible_v) 20 | auto operator()(T &&first, T &&second) { 21 | const auto &first_size = std::ranges::distance(first); 22 | const auto &second_size = std::ranges::distance(second); 23 | 24 | if (first_size == 0 || second_size == 0) { 25 | return SimpleType{}; 26 | } 27 | 28 | static thread_local IndexProvider index_provider{}; 29 | const auto &first_pivot = 30 | index_provider(static_cast(0), first_size); 31 | const auto &second_pivot = 32 | index_provider(static_cast(0), second_size); 33 | 34 | // construct our children 35 | SimpleType child{}; 36 | 37 | if constexpr (dp::genetic::type_traits::has_push_back) { 38 | // check for reserve() and empty() to try and save on allocations 39 | if constexpr (dp::genetic::type_traits::has_reserve) { 40 | // calculate the child size 41 | auto child_size = details::calculate_crossover_output_size( 42 | first, second, first_pivot, second_pivot); 43 | 44 | // check for empty and if empty, reserve 45 | if constexpr (dp::genetic::type_traits::has_empty) { 46 | if (child.empty()) child.reserve(child_size); 47 | 48 | } else { 49 | // blindly reserve to try and save on allocations 50 | child.reserve(child_size); 51 | } 52 | } 53 | // has push_back so we use a back inserter 54 | details::cross(first, second, first_pivot, second_pivot, std::back_inserter(child)); 55 | } else { 56 | // otherwise, assume we can insert directly into the type 57 | details::cross(first, second, first_pivot, second_pivot, std::ranges::begin(child)); 58 | } 59 | 60 | return std::move(child); 61 | } 62 | }; 63 | } // namespace dp::genetic 64 | -------------------------------------------------------------------------------- /test/source/crossover.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace dp::genetic; 10 | 11 | // cross over concept checks 12 | static_assert(concepts::crossover_operator); 13 | // TODO: Add support for arithmetic types 14 | // static_assert(concepts::crossover_operator); 15 | static_assert(concepts::crossover_operator>); 16 | static_assert(concepts::crossover_operator>); 17 | static_assert(concepts::crossover_operator>); 18 | 19 | TEST_CASE("Test generic cross technique") { 20 | constexpr auto first_point = 2; 21 | constexpr auto second_point = 4; 22 | 23 | std::string first(4, 'a'); 24 | std::string second(6, 'b'); 25 | 26 | std::string child1; 27 | std::string child2; 28 | details::cross(first, second, first_point, second_point, std::back_inserter(child1)); 29 | details::cross(second, first, second_point, first_point, std::back_inserter(child2)); 30 | 31 | CHECK(child1 == "aabb"); 32 | CHECK(child2 == "bbbbaa"); 33 | 34 | const auto& c1_size = 35 | details::calculate_crossover_output_size(first, second, first_point, second_point); 36 | const auto& c2_size = 37 | details::calculate_crossover_output_size(second, first, second_point, first_point); 38 | CHECK(child1.size() == c1_size); 39 | CHECK(child2.size() == c2_size); 40 | 41 | std::array a_first{1, 1, 1, 1}; 42 | std::array a_second{2, 2, 2, 2, 2, 2}; 43 | static_assert(std::is_same_v); 44 | 45 | std::vector v_c1, v_c2; 46 | details::cross(a_first, a_second, first_point, second_point, std::back_inserter(v_c1)); 47 | details::cross(a_second, a_first, second_point, first_point, std::back_inserter(v_c2)); 48 | 49 | // do a cross over but pre-compute size and pre-allocate 50 | std::vector v_c1_pre_alloc( 51 | details::calculate_crossover_output_size(a_first, a_second, first_point, second_point)); 52 | std::vector v_c2_pre_alloc( 53 | details::calculate_crossover_output_size(a_second, a_first, second_point, first_point)); 54 | details::cross(a_first, a_second, first_point, second_point, std::begin(v_c1_pre_alloc)); 55 | details::cross(a_second, a_first, second_point, first_point, std::begin(v_c2_pre_alloc)); 56 | } 57 | 58 | TEST_CASE("Test default crossover operator") { 59 | dp::genetic::random_crossover crossover{}; 60 | 61 | using namespace std::string_literals; 62 | 63 | const auto& p1 = "aabb"s; 64 | const auto& p2 = "bbaa"s; 65 | 66 | const std::string& child1 = dp::genetic::make_children(crossover, p1, p2); 67 | const std::string& child2 = dp::genetic::make_children(crossover, p2, p1); 68 | 69 | std::cout << child1 << '\n' << child2 << '\n' << std::endl; 70 | } 71 | -------------------------------------------------------------------------------- /test/source/fitness.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | static_assert(dp::genetic::concepts::fitness_operator); 10 | static_assert(std::invocable); 11 | static_assert(dp::genetic::concepts::fitness_operator< 12 | dp::genetic::element_wise_comparison, std::string>); 13 | static_assert(dp::genetic::concepts::fitness_operator< 14 | dp::genetic::element_wise_comparison>, std::vector>); 15 | 16 | TEST_CASE("Accumulation fitness operator") { 17 | const std::vector values{1.0, 2.0, 3.0, 4.0}; 18 | const auto fitness = dp::genetic::evaluate_fitness(dp::genetic::accumulation_fitness, values); 19 | 20 | CHECK(fitness == 10.0); 21 | 22 | constexpr std::array arr_values{1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; 23 | const auto arr_fitness = 24 | dp::genetic::evaluate_fitness(dp::genetic::accumulation_fitness, arr_values); 25 | CHECK(arr_fitness == 21.f); 26 | 27 | std::unordered_map map_values{{"a", 1}, {"b", 2}, {"c", 3}}; 28 | const auto map_value_fitness = dp::genetic::evaluate_fitness( 29 | dp::genetic::accumulation_fitness, map_values | std::ranges::views::values); 30 | 31 | CHECK(map_value_fitness == 6); 32 | } 33 | 34 | TEST_CASE("Element-wise fitness") { 35 | const std::vector data{1.0, 2.0, 3.0, 4.0}; 36 | const std::vector solution{1.0, 2.0, 4.0}; 37 | 38 | const auto fitness = 39 | dp::genetic::evaluate_fitness(dp::genetic::element_wise_comparison(solution, 1.0), data); 40 | CHECK(fitness == 1.0); 41 | } 42 | 43 | TEST_CASE("Composite fitness") { 44 | const std::vector data{1.0, 2.0, 3.0, 4.0}; 45 | const std::vector solution{1.0, 2.0, 4.0}; 46 | 47 | // clang-format off 48 | dp::genetic::composite_sum_fitness composite{ 49 | dp::genetic::accumulation_fitness, 50 | dp::genetic::element_wise_comparison(solution, 1.0), 51 | [](const auto& rng) { 52 | return static_cast(rng.size()) * 2.0; 53 | } 54 | }; 55 | // clang-format on 56 | 57 | const auto fitness = dp::genetic::evaluate_fitness(composite, data); 58 | 59 | // accumulation fitness will give us 10.0 60 | // element-wise fitness will give us 1.0 (2 matches * 1.0 - 1 mismatch in size * 1.0) 61 | // total fitness will be 11.0 + data.size() * 2 62 | CHECK(fitness == 11.0 + data.size() * 2); 63 | 64 | // clang-format off 65 | dp::genetic::composite_difference_fitness composite_diff{ 66 | dp::genetic::accumulation_fitness, 67 | [](const auto& rng) { return static_cast(rng.size());}, 68 | [](const auto&) {return 1.0;} 69 | }; 70 | // clang-format on 71 | 72 | const auto diff_fitness = dp::genetic::evaluate_fitness(composite_diff, data); 73 | 74 | CHECK(diff_fitness == 10.0 - static_cast(data.size()) - 1.0); 75 | 76 | // check product 77 | 78 | // clang-format off 79 | dp::genetic::composite_product_fitness composite_product{ 80 | composite, 81 | composite_diff, 82 | [](const auto& rng) { return rng.size() * 2; } 83 | }; 84 | // clang-format on 85 | 86 | const auto product_fitness = dp::genetic::evaluate_fitness(composite_product, data); 87 | CHECK(product_fitness == fitness * diff_fitness * data.size() * 2); 88 | } 89 | -------------------------------------------------------------------------------- /include/genetic/details/crossover_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace dp::genetic { 7 | namespace details { 8 | /** 9 | * @brief Calculates the output size of the child given the parents and the pivot point. 10 | * Pairs with the default_crossover operator and the cross function to allow for child 11 | * pre-allocation to avoid using back_inserter. 12 | * @tparam FirstParent Type for the first parent 13 | * @tparam SecondParent Type for the second parent 14 | * @param first The first parent for the child 15 | * @param second The second parent for the child 16 | * @param first_pivot The first pivot point, or where the first parent will be "split" to 17 | * create the child. 18 | * @param second_pivot The second pivot point, or where the second parent will be "split" 19 | * @return The output size of the child. 20 | */ 21 | template 22 | auto calculate_crossover_output_size(FirstParent &&first, SecondParent &&second, 23 | const std::size_t &first_pivot, 24 | const std::size_t &second_pivot) -> std::size_t { 25 | auto first_pivot_point = std::ranges::begin(first) + first_pivot; 26 | auto second_pivot_point = std::ranges::begin(second) + second_pivot; 27 | auto child_size = std::ranges::distance(std::ranges::begin(first), first_pivot_point) + 28 | std::ranges::distance(second_pivot_point, std::ranges::end(second)); 29 | 30 | return child_size; 31 | } 32 | 33 | template > 35 | requires std::ranges::input_range && 36 | std::ranges::input_range && 37 | std::is_same_v, 38 | dp::genetic::type_traits::element_type_t> 39 | void cross(FirstParent &&first, SecondParent &&second, const std::size_t &first_pivot, 40 | const std::size_t &second_pivot, std::output_iterator auto child_output) { 41 | const auto &parent1_first_half_size = std::ranges::distance( 42 | std::ranges::begin(first), std::ranges::begin(first) + first_pivot); 43 | 44 | const auto &parent2_first_half_size = std::ranges::distance( 45 | std::ranges::begin(second), std::ranges::begin(second) + second_pivot); 46 | 47 | auto parent1_first_part = first | std::ranges::views::take(parent1_first_half_size); 48 | 49 | auto parent2_second_part = second | std::ranges::views::drop(parent2_first_half_size); 50 | 51 | // child is the combination of the first half of parent 1 + second half of parent 2 52 | std::ranges::copy(parent1_first_part, child_output); 53 | std::ranges::copy(parent2_second_part, child_output); 54 | } 55 | 56 | template > 58 | requires std::ranges::input_range && 59 | std::ranges::input_range && 60 | std::is_same_v, 61 | dp::genetic::type_traits::element_type_t> 62 | void cross(FirstParent &&first, SecondParent &&second, const std::size_t &pivot, 63 | std::output_iterator auto child_output) { 64 | details::cross(first, second, pivot, pivot, child_output); 65 | } 66 | } // namespace details 67 | } // namespace dp::genetic 68 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20 FATAL_ERROR) 2 | 3 | project( 4 | genetic 5 | VERSION 0.2.0 6 | LANGUAGES CXX 7 | ) 8 | 9 | set(CMAKE_CXX_STANDARD 23) 10 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 11 | 12 | if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) 13 | message( 14 | FATAL_ERROR 15 | "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there." 16 | ) 17 | endif() 18 | 19 | find_program(CCACHE_EXE ccache) 20 | if(EXISTS ${CCACHE_EXE}) 21 | message(STATUS "Found ccache ${CCACHE_EXE}") 22 | set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_EXE}) 23 | endif() 24 | 25 | # ---- Add dependencies via CPM ---- 26 | # see https://github.com/TheLartians/CPM.cmake for more info 27 | include(cmake/CPM.cmake) 28 | 29 | # PackageProject.cmake will be used to make our target installable 30 | CPMAddPackage("gh:TheLartians/PackageProject.cmake@1.6.0") 31 | 32 | CPMAddPackage( 33 | NAME thread-pool 34 | GITHUB_REPOSITORY DeveloperPaul123/thread-pool 35 | VERSION 0.6.2 36 | GIT_TAG 0.6.2 37 | OPTIONS "TP_BUILD_TESTS OFF" "TP_BUILD_BENCHMARKS OFF" "TP_BUILD_EXAMPLES OFF" 38 | ) 39 | 40 | # ---- Add source files ---- 41 | 42 | # Note: globbing sources is considered bad practice as CMake's generators may not detect new files 43 | # automatically. Keep that in mind when changing files, or explicitly mention them here. 44 | file(GLOB_RECURSE headers CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h") 45 | 46 | # ---- Create library ---- 47 | add_library(${PROJECT_NAME} INTERFACE) 48 | add_library(dp::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 49 | 50 | message(STATUS "Compiler version: ${CMAKE_CXX_COMPILER_VERSION}") 51 | 52 | if((MSVC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.32) 53 | OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12) 54 | OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 17) 55 | ) 56 | option(DP_GENETIC_EXPERIMENTAL "Turn on experimental C++23 feature support" ON) 57 | endif() 58 | 59 | if(DEFINED DP_GENETIC_EXPERIMENTAL AND DP_GENETIC_EXPERIMENTAL) 60 | message(STATUS "Using experimental C++23 features.") 61 | target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_23) 62 | else() 63 | target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_20) 64 | endif() 65 | 66 | # being a cross-platform target, we enforce standards conformance on MSVC 67 | target_compile_options(${PROJECT_NAME} INTERFACE $<$:/permissive->) 68 | 69 | target_include_directories( 70 | ${PROJECT_NAME} INTERFACE $ 71 | $ 72 | ) 73 | 74 | target_link_libraries(${PROJECT_NAME} INTERFACE dp::thread-pool) 75 | 76 | # ---- Create an installable target ---- 77 | # this allows users to install and find the library via `find_package()`. 78 | 79 | # the location where the project's version header will be placed should match the project's regular 80 | # header paths 81 | string(TOLOWER ${PROJECT_NAME}/version.h VERSION_HEADER_LOCATION) 82 | 83 | packageProject( 84 | NAME ${PROJECT_NAME} 85 | VERSION ${PROJECT_VERSION} 86 | NAMESPACE ${PROJECT_NAME} 87 | BINARY_DIR ${PROJECT_BINARY_DIR} 88 | INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include 89 | INCLUDE_DESTINATION include/${PROJECT_NAME}-${PROJECT_VERSION} 90 | VERSION_HEADER "${VERSION_HEADER_LOCATION}" 91 | COMPATIBILITY SameMajorVersion 92 | DEPENDENCIES "" 93 | ) 94 | 95 | option(DP_GENETIC_BUILD_TESTS "Turn on to build unit tests." ON) 96 | option(DP_GENETIC_BUILD_EXAMPLES "Turn on to build examples." ON) 97 | option(DP_GENETIC_BUILD_BENCHMARKS "Turn on to build benchmarks." ON) 98 | 99 | if(${DP_GENETIC_BUILD_TESTS}) 100 | enable_testing() 101 | add_subdirectory(test) 102 | endif() 103 | if(${DP_GENETIC_BUILD_EXAMPLES}) 104 | add_subdirectory(examples) 105 | endif() 106 | if(${DP_GENETIC_BUILD_BENCHMARKS}) 107 | # add_subdirectory(benchmark) 108 | endif() 109 | -------------------------------------------------------------------------------- /examples/phrase_guess.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct random_word_generator { 12 | [[nodiscard]] std::string operator()(std::string_view char_set, std::size_t max_length) const { 13 | static thread_local auto generator = dp::genetic::uniform_integral_generator{}; 14 | const auto& out_length = generator(static_cast(1), max_length); 15 | std::string out(out_length, '\0'); 16 | std::generate_n(out.begin(), out_length, 17 | [&]() { return char_set[generator(std::size_t{0}, char_set.size() - 1)]; }); 18 | return out; 19 | } 20 | }; 21 | 22 | [[nodiscard]] bool is_space(char q) noexcept { 23 | static constexpr auto ws = {' ', '\t', '\n', '\v', '\r', '\f'}; 24 | return std::ranges::any_of(ws, [q](auto p) { return p == q; }); 25 | }; 26 | 27 | const std::string alphabet = R"(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!,. ,')"; 28 | 29 | int main(int argc, char** argv) { 30 | std::string solution; 31 | auto prompt = std::format("Acceptable characters: {}\nEnter a phrase: ", alphabet); 32 | std::cout << prompt; 33 | std::getline(std::cin, solution); 34 | 35 | constexpr auto is_space = [](const auto& c) { return std::isspace(c); }; 36 | 37 | // filter and trim input to only include characters in the alphabet that we accept 38 | solution = solution | std::ranges::views::filter([](const auto& c) { 39 | return std::ranges::find(alphabet, c) != std::end(alphabet); 40 | }) | 41 | std::ranges::views::drop_while(isspace) | std::views::reverse | 42 | std::ranges::views::drop_while(isspace) | std::views::reverse | 43 | std::ranges::to(); 44 | 45 | std::cout << std::format("Using filtered text: {}\n", solution); 46 | 47 | const std::string available_chars = alphabet; 48 | const auto max_word_length = solution.size() + solution.size() / 2; 49 | 50 | // generate random words as our initial population 51 | random_word_generator word_generator{}; 52 | // generate initial population 53 | constexpr auto initial_pop_size = 1000; 54 | std::vector initial_population(initial_pop_size); 55 | std::generate_n(initial_population.begin(), initial_pop_size, 56 | [&] { return word_generator(available_chars, max_word_length); }); 57 | 58 | // fitness evaluator 59 | dp::genetic::element_wise_comparison fitness_op(solution, 1.0); 60 | dp::genetic::pooled_value_generator value_generator(alphabet); 61 | 62 | auto string_mutator = dp::genetic::composite_mutator{ 63 | [&](const std::string& input) { 64 | if (input.empty()) { 65 | return word_generator(available_chars, max_word_length); 66 | } 67 | return input; 68 | }, 69 | dp::genetic::value_replacement>{ 71 | value_generator}}; 72 | 73 | // termination criteria 74 | auto termination = dp::genetic::fitness_termination(fitness_op(solution)); 75 | 76 | static_assert( 77 | dp::genetic::concepts::selection_operator, decltype(fitness_op)>); 79 | // algorithm settings 80 | dp::genetic::algorithm_settings settings{0.3, 0.6, 0.3}; 81 | dp::genetic::params params = 82 | dp::genetic::params::builder() 83 | .with_mutation_operator(string_mutator) 84 | .with_crossover_operator(dp::genetic::random_crossover{}) 85 | .with_fitness_operator(fitness_op) 86 | .with_termination_operator(termination) 87 | .build(); 88 | 89 | auto start = std::chrono::steady_clock::now(); 90 | auto [best, fitness] = 91 | dp::genetic::solve(initial_population, settings, params, [](const auto& stats) { 92 | std::cout << "best: " << stats.current_best.best 93 | << " fitness: " << stats.current_best.fitness << "\n"; 94 | }); 95 | auto stop = std::chrono::steady_clock::now(); 96 | auto time_ms = std::chrono::duration_cast(stop - start).count(); 97 | std::cout << "Total time (ms): " << std::to_string(time_ms) << "\n"; 98 | 99 | return 0; 100 | } 101 | -------------------------------------------------------------------------------- /test/source/mutation.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | static_assert(dp::genetic::concepts::mutation_operator); 7 | static_assert(dp::genetic::concepts::mutation_operator); 8 | static_assert(dp::genetic::concepts::mutation_operator); 9 | 10 | static_assert(dp::genetic::concepts::mutation_operator< 11 | dp::genetic::composite_mutator, 13 | dp::genetic::pooled_value_generator>>>, 14 | std::string>); 15 | static_assert(dp::genetic::concepts::mutation_operator< 16 | dp::genetic::composite_mutator, dp::genetic::pooled_value_generator>>>, 18 | std::vector>); 19 | 20 | TEST_CASE("Value replacement mutator") { 21 | const std::string alphabet = R"(abcdefghijklmnopqrstuvwxyz)"; 22 | dp::genetic::pooled_value_generator value_generator(alphabet); 23 | dp::genetic::value_replacement> 24 | mutator(value_generator); 25 | const std::string value = "demo"; 26 | const auto new_value = dp::genetic::mutate(mutator, value); 27 | 28 | // same length, but different values 29 | CHECK_NE(value, new_value); 30 | CHECK(new_value.length() == value.length()); 31 | 32 | std::cout << "Value replacement mutator: " << new_value << "\n"; 33 | } 34 | 35 | TEST_CASE("Value insertion mutator") { 36 | const std::string alphabet = R"(abcdefghijklmnopqrstuvwxyz)"; 37 | dp::genetic::pooled_value_generator value_generator(alphabet); 38 | dp::genetic::value_insertion_mutator mutator(value_generator); 39 | const std::string value = "demo"; 40 | const auto new_value = dp::genetic::mutate(mutator, value); 41 | CHECK_NE(value, new_value); 42 | CHECK(new_value.length() == value.length() + 1); 43 | 44 | std::cout << "Value insertion mutator: " << new_value << "\n"; 45 | 46 | constexpr std::uint64_t insertions = 10; 47 | 48 | std::vector value_range{}; 49 | std::ranges::generate_n(std::back_inserter(value_range), insertions, 50 | [i = 0]() mutable { return i++; }); 51 | dp::genetic::pooled_value_generator> int_value_gen{value_range}; 52 | dp::genetic::value_insertion_mutator> vector_mutator(int_value_gen, 53 | insertions); 54 | 55 | auto input = std::vector{1, 2, 3, 4}; 56 | auto vector_result = dp::genetic::mutate(vector_mutator, input); 57 | CHECK(vector_result.size() == input.size() + insertions); 58 | } 59 | 60 | TEST_CASE("Composite mutator") { 61 | dp::genetic::composite_mutator mutator{ 62 | [](const std::string& string) { return string + "part1"; }, 63 | [](const std::string& string) { return string + "part2"; }}; 64 | using namespace std::string_literals; 65 | const auto result = dp::genetic::mutate(mutator, "test"s); 66 | 67 | CHECK_EQ(result, "testpart1part2"); 68 | } 69 | 70 | TEST_CASE("Value mutator") { 71 | const auto double_value_mutator = dp::genetic::double_value_mutator(-0.1, 0.1); 72 | const std::vector xyz{1.0, 2.0, 3.0}; 73 | auto new_value = dp::genetic::mutate(double_value_mutator, xyz); 74 | for (auto i = 0; i < xyz.size(); ++i) { 75 | CHECK(std::abs(new_value[i] - xyz[i]) <= 0.1); 76 | } 77 | 78 | const auto float_value_mutator = dp::genetic::float_value_mutator(-1.f, 1.f); 79 | const std::vector xyz_f{1.f, 2.f, 3.f}; 80 | auto new_value_f = dp::genetic::mutate(float_value_mutator, xyz_f); 81 | for (auto i = 0; i < xyz_f.size(); ++i) { 82 | CHECK(std::abs(new_value_f[i] - xyz_f[i]) <= 1.f); 83 | } 84 | 85 | const auto int_value_mutator = dp::genetic::integral_value_mutator(-5, 5); 86 | const std::vector xyz_int{1u, 2u, 3u}; 87 | auto new_value_int = dp::genetic::mutate(int_value_mutator, xyz_int); 88 | for (auto i = 0; i < xyz_int.size(); ++i) { 89 | int diff = new_value_int[i] - xyz_int[i]; 90 | CHECK(std::abs(diff) <= 5); 91 | } 92 | 93 | const std::array xy_array{1.0, 2.0}; 94 | static_assert(dp::genetic::type_traits::is_std_array>); 95 | auto new_value_array = dp::genetic::mutate(double_value_mutator, xy_array); 96 | for (auto i = 0; i < xy_array.size(); ++i) { 97 | CHECK(std::abs(new_value_array[i] - xy_array[i]) <= 0.1); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /include/genetic/op/mutation/value_mutation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "genetic/details/concepts.h" 6 | 7 | namespace dp::genetic { 8 | namespace details { 9 | 10 | struct number_generator_helper_op { 11 | template 12 | [[nodiscard]] constexpr Number operator()(Number lower, Number upper) const { 13 | if constexpr (std::is_floating_point()) { 14 | static dp::genetic::uniform_floating_point_generator float_gen{}; 15 | return float_gen(lower, upper); 16 | } else { 17 | static dp::genetic::uniform_integral_generator int_gen{}; 18 | return int_gen(lower, upper); 19 | } 20 | } 21 | }; 22 | 23 | template 24 | struct range_converter_helper_op { 25 | template 26 | requires std::ranges::range 27 | OutputRange operator()(const InputRange& input) { 28 | // check for std::array and std::ranges::sized_range 29 | if constexpr (std::ranges::sized_range && 30 | dp::genetic::type_traits::is_std_array) { 31 | OutputRange output{}; 32 | for (auto i = 0; i < std::ranges::size(output); ++i) { 33 | output[i] = input[i]; 34 | } 35 | return output; 36 | 37 | } else { 38 | // otherwise, just convert to the output range 39 | // note we have to wrap this in an "else" otherwise it won't compile due to it 40 | // being ill-formed for the "to" conversion 41 | return input | std::ranges::to>(); 42 | } 43 | } 44 | }; 45 | 46 | constexpr inline auto number_generator = number_generator_helper_op{}; 47 | template 48 | using range_converter_helper = range_converter_helper_op; 49 | 50 | template 51 | struct value_mutation_op { 52 | Number lower_bound; 53 | Number upper_bound; 54 | 55 | template >> 57 | requires type_traits::addable 58 | [[nodiscard]] T operator()(const T& t) const { 59 | namespace vw = std::ranges::views; 60 | auto result = 61 | t | vw::transform([low = lower_bound, up = upper_bound](const auto& value) { 62 | return static_cast( 63 | std::plus()(value, number_generator(low, up))); 64 | }); 65 | 66 | auto converted_range = range_converter_helper>{}(result); 67 | return std::move(converted_range); 68 | } 69 | }; 70 | } // namespace details 71 | 72 | /** 73 | * @brief Mutates a range of values by adding a random number within the bounds. 74 | * 75 | * @param lower_bound The lower bound of the random number. 76 | * @param upper_bound The upper bound of the random number. 77 | * @return The mutated value. 78 | */ 79 | inline auto double_value_mutator(double lower_bound, double upper_bound) { 80 | return details::value_mutation_op{lower_bound, upper_bound}; 81 | } 82 | 83 | /** 84 | * @brief Mutates a range of values by adding a random number within the bounds. 85 | * 86 | * @param lower_bound The lower bound of the random number. 87 | * @param upper_bound The upper bound of the random number. 88 | * @return The mutated value. 89 | */ 90 | inline auto float_value_mutator(float lower_bound, float upper_bound) { 91 | return details::value_mutation_op{lower_bound, upper_bound}; 92 | } 93 | 94 | /** 95 | * @brief Mutates a range of values by adding a random number within the bounds. 96 | * 97 | * @param lower_bound The lower bound of the random number. 98 | * @param upper_bound The upper bound of the random number. 99 | * @return The mutated value. 100 | */ 101 | template 102 | constexpr inline auto integral_value_mutator(Number lower_bound, Number upper_bound) { 103 | return details::value_mutation_op{lower_bound, upper_bound}; 104 | } 105 | } // namespace dp::genetic 106 | -------------------------------------------------------------------------------- /include/genetic/details/concepts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace dp::genetic { 8 | 9 | namespace type_traits { 10 | template 11 | using element_type_t = 12 | std::remove_reference_t()))>; 13 | 14 | template 15 | constexpr inline bool is_std_array = false; 16 | 17 | template 18 | constexpr inline bool is_std_array> = true; 19 | 20 | template > 21 | concept has_value_type = requires(SimpleType) { typename SimpleType::value_type; }; 22 | 23 | template > 24 | concept has_size_type = requires(SimpleType) { typename SimpleType::size_type; }; 25 | 26 | template , 27 | typename SizeType = typename SimpleType::size_type> 28 | concept has_size = has_size_type && requires(T &&t) { 29 | { t.size() } -> std::convertible_to; 30 | }; 31 | 32 | template , 33 | typename ValueType = typename SimpleType::value_type> 34 | concept has_push_back = has_value_type && 35 | requires(SimpleType &&t, ValueType &&value) { t.push_back(value); }; 36 | 37 | template , 38 | typename SizeType = typename SimpleType::size_type> 39 | concept has_reserve = has_size_type && std::integral && 40 | requires(SimpleType &&t, SizeType value) { t.reserve(value); }; 41 | 42 | template > 43 | concept has_empty = requires(SimpleType &&t) { 44 | { t.empty() } -> std::convertible_to; 45 | }; 46 | 47 | template 48 | concept number = std::integral || std::floating_point; 49 | 50 | template 51 | concept addable = requires(T first, T2 second) { 52 | { first + second } -> std::convertible_to; 53 | }; 54 | 55 | template 56 | concept subtractable = requires(T first, T2 second) { 57 | { first + second } -> std::convertible_to; 58 | }; 59 | } // namespace type_traits 60 | 61 | namespace concepts { 62 | /// @brief Custom concepts needed for genetic algorithm class 63 | /// @{ 64 | template 65 | concept mutation_operator = 66 | std::invocable && std::is_same_v, T>; 67 | 68 | template 69 | concept fitness_operator = std::invocable && requires(Fn fn) { 70 | { fn(std::declval()) } -> type_traits::number; 71 | }; 72 | 73 | template , 74 | class Result = std::invoke_result_t> 75 | concept crossover_operator = 76 | std::invocable && std::is_convertible_v; 77 | 78 | template > 80 | concept termination_operator = 81 | std::invocable && dp::genetic::type_traits::number && 82 | std::convertible_to; 83 | 84 | template 85 | concept selection_operator = 86 | std::invocable && 87 | std::is_same_v, std::pair>; 88 | 89 | template 90 | concept index_generator = 91 | std::integral> && requires(T &&t, Index &&l, Index &&u) { 92 | { t(l, u) } -> std::convertible_to>; 93 | }; 94 | 95 | template 96 | concept value_generator = 97 | std::invocable && std::convertible_to, ValueType>; 98 | 99 | template > 101 | concept population = std::ranges::range && std::is_same_v; 102 | /// @} 103 | } // namespace concepts 104 | 105 | } // namespace dp::genetic 106 | -------------------------------------------------------------------------------- /include/genetic/params.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "crossover.h" 7 | #include "details/concepts.h" 8 | #include "fitness.h" 9 | #include "mutation.h" 10 | #include "selection.h" 11 | #include "termination.h" 12 | 13 | namespace dp::genetic { 14 | template > 15 | requires dp::genetic::concepts::population 16 | class params { 17 | public: 18 | /// @brief Type definitions 19 | /// @{ 20 | using value_type = ChromosomeType; 21 | using population_type = PopulationType; 22 | using mutation_operator_type = std::function; 23 | using crossover_operator_type = 24 | std::function; 25 | using fitness_evaluation_type = std::function; 26 | using termination_evaluation_type = std::function; 27 | using selection_operator_type = std::function( 28 | PopulationType, fitness_evaluation_type)>; 29 | /// @} 30 | 31 | template 36 | requires concepts::mutation_operator && 37 | concepts::fitness_operator && 38 | concepts::crossover_operator && 39 | concepts::termination_operator< 40 | TerminationOperator, ChromosomeType, 41 | std::invoke_result_t> && 42 | concepts::selection_operator 44 | explicit params(FitnessOperator&& fitness = FitnessOperator{}, 45 | MutationOperator&& mutator = MutationOperator{}, 46 | TerminationOperator&& terminator = TerminationOperator{}, 47 | CrossoverOperator&& crosser = CrossoverOperator{}, 48 | SelectionOperator selection_operator = SelectionOperator{}) 49 | : mutator_(std::forward(mutator)), 50 | crossover_(std::forward(crosser)), 51 | fitness_(std::forward(fitness)), 52 | termination_(std::forward(terminator)), 53 | selection_(std::forward(selection_operator)) {} 54 | 55 | [[nodiscard]] auto&& fitness_operator() const { return fitness_; } 56 | [[nodiscard]] auto&& mutation_operator() const { return mutator_; } 57 | [[nodiscard]] auto&& crossover_operator() const { return crossover_; } 58 | [[nodiscard]] auto&& termination_operator() const { return termination_; } 59 | [[nodiscard]] auto&& selection_operator() const { return selection_; } 60 | /// @brief builder class that helps with parameter construction 61 | class builder { 62 | public: 63 | builder() = default; 64 | 65 | builder& with_fitness_operator( 66 | dp::genetic::concepts::fitness_operator auto&& op) { 67 | data_.fitness_ = op; 68 | return *this; 69 | } 70 | 71 | builder& with_mutation_operator( 72 | dp::genetic::concepts::mutation_operator auto&& op) { 73 | data_.mutator_ = op; 74 | return *this; 75 | } 76 | 77 | template 78 | requires dp::genetic::concepts::termination_operator 79 | builder& with_termination_operator(Fn&& op) { 80 | data_.termination_ = std::forward(op); 81 | return *this; 82 | } 83 | 84 | builder& with_crossover_operator( 85 | dp::genetic::concepts::crossover_operator auto&& op) { 86 | data_.crossover_ = std::forward(op); 87 | return *this; 88 | } 89 | 90 | template 91 | builder& with_selection_operator(dp::genetic::concepts::selection_operator< 92 | ChromosomeType, PopulationType, UnaryOp> auto&& op) { 93 | data_.selection_ = std::forward(op); 94 | return *this; 95 | } 96 | 97 | [[nodiscard]] auto build() const { return data_; } 98 | 99 | private: 100 | params data_{}; 101 | }; 102 | 103 | private: 104 | friend class builder; 105 | 106 | mutation_operator_type mutator_; 107 | crossover_operator_type crossover_; 108 | fitness_evaluation_type fitness_; 109 | termination_evaluation_type termination_; 110 | selection_operator_type selection_; 111 | }; 112 | } // namespace dp::genetic 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | genetic 3 |

4 | 5 | [![say thanks](https://img.shields.io/badge/Say%20Thanks-👍-1EAEDB.svg)](https://github.com/DeveloperPaul123/genetic/stargazers) 6 | [![Discord](https://img.shields.io/discord/652515194572111872)](https://discord.gg/CX2ybByRnt) 7 | 8 | A flexible and performant implementation of the genetic algorithm in C++20/23. 9 | 10 | ## Features 11 | 12 | - Built entirely with C++20/23 13 | - Includes many default operators for most use cases 14 | - Supply your own operations for: 15 | - Selection 16 | - Crossover 17 | - Mutation 18 | - Fitness 19 | - Termination 20 | 21 | ## Integration 22 | 23 | `dp::genetic` is a header only library. All the files needed are in `include/genetic`. 24 | 25 | ### CMake 26 | 27 | `genetic` defines two CMake targets: 28 | 29 | - `Genetic::Genetic` 30 | - `dp::genetic` 31 | 32 | You can then use `find_package()`: 33 | 34 | ```cmake 35 | find_package(dp::genetic REQUIRED) 36 | ``` 37 | 38 | Alternatively, you can use something like [CPM](https://github.com/TheLartians/CPM) which is based on CMake's `Fetch_Content` module. 39 | 40 | ```cmake 41 | CPMAddPackage( 42 | NAME genetic 43 | GITHUB_REPOSITORY DeveloperPaul123/genetic 44 | GIT_TAG 0.1.0 # change this to latest commit or release tag 45 | ) 46 | ``` 47 | 48 | ## Usage 49 | 50 | [Knapsack problem](https://en.wikipedia.org/wiki/Knapsack_problem) example: 51 | 52 | ```cpp 53 | 54 | struct knapsack_box { 55 | int value; 56 | int weight; 57 | auto operator<=>(const knapsack_box&) const = default; 58 | }; 59 | 60 | // weight capacity of our knapsack 61 | constexpr auto max_weight = 15; 62 | 63 | // available boxes for the knapsack 64 | std::vector available_items = {{4, 12}, {2, 1}, {10, 4}, {1, 1}, {2, 2}}; 65 | 66 | // fitness evaluator (omitted for brevity) 67 | auto fitness = { // ...}; 68 | 69 | // random mutation operator (omitted for brevity) 70 | auto mutator = { // ... }; 71 | 72 | // crossover operator (i.e. child generator, omitted for brevity) 73 | auto crossover = { // ... }; 74 | 75 | // the solution is all the boxes except for the heaviest one. 76 | const knapsack solution = {-1, 1, 2, 3, 4}; 77 | const knapsack all_items = {0, 1, 2, 3, 4}; 78 | 79 | // genetic algorithm settings. 80 | constexpr dp::genetic::algorithm_settings settings{0.1, 0.5, 0.25}; 81 | 82 | // generate an initial random population 83 | constexpr auto population_size = 2; 84 | std::vector initial_population{}; 85 | initial_population.reserve(population_size); 86 | 87 | // generate the initial population 88 | std::ranges::generate_n(std::back_inserter(initial_population), population_size, 89 | knapsack_generator); 90 | 91 | // define the termination criteria 92 | auto termination = dp::genetic::fitness_termination_criteria(fitness(solution)); 93 | 94 | // setup the params object for the algorithm 95 | auto params = dp::genetic::params::builder() 96 | .with_mutation_operator(mutator) 97 | .with_crossover_operator(crossover) 98 | .with_fitness_operator(fitness) 99 | .with_termination_operator(termination) 100 | .build(); 101 | 102 | auto [best, fitness] = dp::genetic::solve(initial_population, settings, params); 103 | 104 | ``` 105 | 106 | For more details see the `/examples` folder and the unit tests under `/test`. 107 | 108 | ## Building 109 | 110 | This project has been built with: 111 | 112 | - Visual Studio 2022 113 | - Clang `10.+` (via WSL on Windows) 114 | - GCC `11.+` (via WSL on Windows) 115 | - CMake `3.21+` 116 | 117 | To build, run: 118 | 119 | ```bash 120 | cmake -S . -B build 121 | cmake --build build 122 | ``` 123 | 124 | ### Build Options 125 | 126 | | Option | Description | Default | 127 | |:-------|:------------|:--------:| 128 | | `DP_GENETIC_BUILD_TESTS` | Turn on to build unit tests. Required for formatting build targets. | ON | 129 | | `DP_GENETIC_BUILD_EXAMPLES` | Turn on to build examples | ON | 130 | 131 | ### Run clang-format 132 | 133 | Use the following commands from the project's root directory to check and fix C++ and CMake source style. 134 | This requires _clang-format_, _cmake-format_ and _pyyaml_ to be installed on the current system. To use this feature you must turn on `TP_BUILD_TESTS`. 135 | 136 | ```bash 137 | # view changes 138 | cmake --build build/test --target format 139 | 140 | # apply changes 141 | cmake --build build/test --target fix-format 142 | ``` 143 | See [Format.cmake](https://github.com/TheLartians/Format.cmake) for details. 144 | 145 | ### Build the documentation 146 | 147 | The documentation is automatically built and [published](https://developerpaul123.github.io/genetic) whenever a [GitHub Release](https://help.github.com/en/github/administering-a-repository/managing-releases-in-a-repository) is created. 148 | To manually build documentation, call the following command. 149 | 150 | ```bash 151 | cmake -S documentation -B build/doc 152 | cmake --build build/doc --target GenerateDocs 153 | # view the docs 154 | open build/doc/doxygen/html/index.html 155 | ``` 156 | 157 | To build the documentation locally, you will need [Doxygen](https://www.doxygen.nl/) and [Graphviz](https://graphviz.org/) on your system. 158 | 159 | ## Contributing 160 | 161 | Contributions are very welcome. Please see [contribution guidelines for more info](CONTRIBUTING.md). 162 | 163 | ## License 164 | 165 | The project is licensed under the MIT license. See [LICENSE](LICENSE) for more details. 166 | 167 | ## Author 168 | 169 | | [
@DeveloperPaul123](https://github.com/DeveloperPaul123) | 170 | |:----:| 171 | -------------------------------------------------------------------------------- /include/genetic/op/fitness/composite_fitness.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace dp::genetic { 5 | 6 | namespace details { 7 | 8 | template 9 | void for_each_tuple_impl(TupleT&& tp, Fn&& fn, std::index_sequence) { 10 | (fn(std::get(std::forward(tp))), ...); 11 | } 12 | 13 | template >> 15 | void for_each_tuple(TupleT&& tp, Fn&& fn) { 16 | for_each_tuple_impl(std::forward(tp), std::forward(fn), 17 | std::make_index_sequence{}); 18 | } 19 | 20 | /** 21 | * @brief A composite fitness function that allows you to chain together multiple fitness 22 | * functions with a custom binary operator to combine the results. By default, it uses 23 | * std::plus<>. 24 | */ 25 | template 26 | struct composite_fitness { 27 | private: 28 | // list of fitness evaluators 29 | std::tuple fitness_ops_; 30 | 31 | public: 32 | explicit composite_fitness(Args... args) 33 | requires(sizeof...(Args) > 0) // must have at least 1 operator 34 | : fitness_ops_(args...) {} 35 | template >>, Range>> 38 | ScoreType evaluate(const Range& data) { 39 | // initialize the result to the first fitness function 40 | ScoreType result = std::invoke(std::get<0>(fitness_ops_), data); 41 | 42 | // loop over the rest of them if there are more 43 | if constexpr (sizeof...(Args) > 1) { 44 | auto rest_of_tuples = std::apply( 45 | [&](auto&& head, auto&&... tail) { 46 | // ignore the head and return tail 47 | return std::make_tuple(std::forward(tail)...); 48 | }, 49 | fitness_ops_); 50 | 51 | details::for_each_tuple(rest_of_tuples, [&](auto&& op) { 52 | // update result based on the previous result and the new score 53 | result = std::invoke(BinaryOperator{}, result, std::invoke(op, data)); 54 | }); 55 | } 56 | 57 | return result; 58 | } 59 | }; 60 | 61 | template 62 | struct composite_base { 63 | private: 64 | using type = composite_fitness; 65 | static constexpr type make_argument_tuple(Args... args) { return type(args...); } 66 | 67 | protected: 68 | type fitness_; 69 | 70 | public: 71 | explicit composite_base(Args... args) : fitness_{make_argument_tuple(args...)} {} 72 | }; 73 | 74 | } // namespace details 75 | 76 | /** 77 | * @brief Composite fitness function that sums the results of the fitness functions. 78 | * 79 | * @tparam Args The fitness functions to chain together. 80 | */ 81 | template 82 | struct composite_sum_fitness : details::composite_base { 83 | explicit composite_sum_fitness(Args... args) : details::composite_base(args...) {} 84 | template >>, Range>> 87 | ScoreType operator()(const Range& data) { 88 | return this->fitness_.template evaluate>(data); 89 | } 90 | }; 91 | 92 | /** 93 | * @brief Composite fitness function that subtracts the results of the fitness functions. 94 | * 95 | * @tparam Args The fitness functions to chain together. 96 | */ 97 | template 98 | struct composite_difference_fitness : details::composite_base { 99 | explicit composite_difference_fitness(Args... args) 100 | : details::composite_base(args...) {} 101 | template >>, Range>> 104 | ScoreType operator()(const Range& data) { 105 | return this->fitness_.template evaluate>(data); 106 | } 107 | }; 108 | 109 | /** 110 | * @brief Composite fitness function that multiplies the results of the fitness functions. 111 | * 112 | * @tparam Args The fitness functions to chain together. 113 | */ 114 | template 115 | struct composite_product_fitness : details::composite_base { 116 | explicit composite_product_fitness(Args... args) 117 | : details::composite_base(args...) {} 118 | template >>, Range>> 121 | ScoreType operator()(const Range& data) { 122 | return this->fitness_.template evaluate>(data); 123 | } 124 | }; 125 | 126 | } // namespace dp::genetic 127 | -------------------------------------------------------------------------------- /test/source/selection.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Helper function for testing selectors 12 | template 13 | std::unordered_map test_selection(Selector& selector, const T& test_value, 14 | PopulationType initial_population, 15 | const unsigned& selection_count = 1000) { 16 | auto fitness_op = [test_value](const std::string& value) -> double { 17 | double score = 0.0; 18 | score -= 19 | std::abs(static_cast(test_value.size()) - static_cast(value.size())) * 20 | 10.0; 21 | for (unsigned i = 0; i < test_value.size(); i++) { 22 | score += test_value[i] == value[i]; 23 | } 24 | return score; 25 | }; 26 | 27 | std::unordered_map selection_histogram; 28 | 29 | // do 1000 selections and build up a selection histogram 30 | for (unsigned i = 0; i < selection_count; i++) { 31 | const auto& [parent1, parent2] = 32 | dp::genetic::select_parents(selector, initial_population, fitness_op); 33 | if (!selection_histogram.contains(parent1)) { 34 | selection_histogram[parent1] = 0; 35 | } else { 36 | ++selection_histogram[parent1]; 37 | } 38 | 39 | if (!selection_histogram.contains(parent2)) { 40 | selection_histogram[parent2] = 0; 41 | } else { 42 | ++selection_histogram[parent2]; 43 | } 44 | } 45 | 46 | return selection_histogram; 47 | } 48 | 49 | TEST_CASE("Roulette selection") { 50 | const std::string test_value = "test"; 51 | 52 | // create a sample population where 1 member has a much larger fitness compared to the others. 53 | const std::vector initial_population{"tesa", "aaaa", "bbbb", "aaa", "bbb"}; 54 | 55 | dp::genetic::roulette_selection selection{}; 56 | const auto selection_histogram = test_selection(selection, test_value, initial_population); 57 | 58 | for (auto& [string, count] : selection_histogram) { 59 | std::cout << string << " : " << std::to_string(count) << "\n"; 60 | } 61 | 62 | const auto [string_value, count] = *std::ranges::max_element( 63 | selection_histogram, [](auto first, auto second) { return first.second < second.second; }); 64 | CHECK(string_value == "tesa"); 65 | } 66 | 67 | TEST_CASE("Roulette selection with ranges::view") { 68 | const std::string test_value = "test"; 69 | using data = std::pair; 70 | // create a sample population where 1 member has a much larger fitness compared to the others. 71 | const std::vector initial_population{data{"tesa", 0}, data{"aaaa", 1}, data{"bbbb", 2}, 72 | data{"aaa", 3}, data{"bbb", 4}}; 73 | 74 | dp::genetic::roulette_selection selection{}; 75 | const auto selection_histogram = 76 | test_selection(selection, test_value, initial_population | std::views::elements<0>); 77 | 78 | for (auto& [string, count] : selection_histogram) { 79 | std::cout << string << " : " << std::to_string(count) << "\n"; 80 | } 81 | 82 | const auto [string_value, count] = *std::ranges::max_element( 83 | selection_histogram, [](auto first, auto second) { return first.second < second.second; }); 84 | CHECK(string_value == "tesa"); 85 | } 86 | 87 | TEST_CASE("Rank selection") { 88 | dp::genetic::rank_selection selector{}; 89 | const std::string test_value = "test"; 90 | 91 | // create a sample population where 1 member has a much larger fitness compared to the others. 92 | const std::vector initial_population{"tesa", "aaaa", "bbbb", "aaa", "bbb"}; 93 | 94 | const auto selection_histogram = test_selection(selector, test_value, initial_population); 95 | 96 | for (auto& [string, count] : selection_histogram) { 97 | std::cout << string << " : " << std::to_string(count) << "\n"; 98 | } 99 | 100 | constexpr auto comp_op = [](auto first, auto second) { return first.second < second.second; }; 101 | const auto [string_value, count] = *std::ranges::max_element(selection_histogram, comp_op); 102 | const auto [min_value, min_count] = *std::ranges::min_element(selection_histogram, comp_op); 103 | 104 | CHECK(string_value == "tesa"); 105 | CHECK(min_value == "bbb"); 106 | } 107 | 108 | TEST_CASE("Rank selection with ranges::view") { 109 | dp::genetic::rank_selection selector{}; 110 | const std::string test_value = "test"; 111 | 112 | using data = std::pair; 113 | 114 | // create a sample population where 1 member has a much larger fitness compared to the others. 115 | std::vector initial_population{data{"tesa", 0}, data{"aaaa", 1}, data{"bbbb", 2}, 116 | data{"aaa", 3}, data{"bbb", 4}}; 117 | 118 | const auto selection_histogram = 119 | test_selection(selector, test_value, initial_population | std::views::elements<0>); 120 | for (auto& [string, count] : selection_histogram) { 121 | std::cout << string << " : " << std::to_string(count) << "\n"; 122 | } 123 | 124 | constexpr auto comp_op = [](auto first, auto second) { return first.second < second.second; }; 125 | const auto [string_value, count] = *std::ranges::max_element(selection_histogram, comp_op); 126 | const auto [min_value, min_count] = *std::ranges::min_element(selection_histogram, comp_op); 127 | 128 | CHECK(string_value == "tesa"); 129 | CHECK(min_value == "bbb"); 130 | } 131 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "configurePresets": [ 4 | { 5 | "name": "linux-base", 6 | "hidden": true, 7 | "description": "Target the Windows Subsystem for Linux (WSL) or a remote Linux system.", 8 | "generator": "Ninja", 9 | "binaryDir": "${sourceDir}/out/build/${presetName}", 10 | "installDir": "${sourceDir}/out/install/${presetName}", 11 | "condition": { 12 | "type": "equals", 13 | "lhs": "${hostSystemName}", 14 | "rhs": "Linux" 15 | }, 16 | "vendor": { 17 | "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { 18 | "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" 19 | } 20 | } 21 | }, 22 | { 23 | "name": "windows-base", 24 | "description": "Target Windows with Ninja and MSVC", 25 | "hidden": true, 26 | "generator": "Ninja", 27 | "binaryDir": "${sourceDir}/out/build/${presetName}", 28 | "installDir": "${sourceDir}/out/install/${presetName}", 29 | "cacheVariables": { 30 | "CMAKE_C_COMPILER": "cl", 31 | "CMAKE_CXX_COMPILER": "cl" 32 | }, 33 | "condition": { 34 | "type": "equals", 35 | "lhs": "${hostSystemName}", 36 | "rhs": "Windows" 37 | }, 38 | "architecture": { 39 | "value": "x64", 40 | "strategy": "external" 41 | } 42 | }, 43 | { 44 | "name": "gcc-base", 45 | "hidden": true, 46 | "inherits": "linux-base", 47 | "cacheVariables": { 48 | "CMAKE_C_COMPILER": "gcc", 49 | "CMAKE_CXX_COMPILER": "g++" 50 | } 51 | }, 52 | { 53 | "name": "gcc-debug", 54 | "inherits": "gcc-base", 55 | "displayName": "GCC Debug", 56 | "cacheVariables": { 57 | "CMAKE_BUILD_TYPE": "Debug" 58 | } 59 | }, 60 | { 61 | "name": "gcc-release", 62 | "inherits": "gcc-base", 63 | "displayName": "GCC Release", 64 | "cacheVariables": { 65 | "CMAKE_BUILD_TYPE": "Release" 66 | } 67 | }, 68 | { 69 | "name": "clang-base", 70 | "hidden": true, 71 | "inherits": "linux-base", 72 | "cacheVariables": { 73 | "CMAKE_C_COMPILER": "clang", 74 | "CMAKE_CXX_COMPILER": "clang++" 75 | } 76 | }, 77 | { 78 | "name": "clang-debug", 79 | "inherits": "clang-base", 80 | "displayName": "Clang Debug", 81 | "cacheVariables": { 82 | "CMAKE_BUILD_TYPE": "Debug" 83 | } 84 | }, 85 | { 86 | "name": "clang-release", 87 | "inherits": "clang-base", 88 | "displayName": "Clang Release", 89 | "cacheVariables": { 90 | "CMAKE_BUILD_TYPE": "Release" 91 | } 92 | }, 93 | { 94 | "name": "x64-debug", 95 | "displayName": "x64 Debug", 96 | "description": "Target Windows (64-bit) with Ninja and MSVC (Debug)", 97 | "inherits": "windows-base", 98 | "architecture": { 99 | "value": "x64", 100 | "strategy": "external" 101 | }, 102 | "cacheVariables": { 103 | "CMAKE_BUILD_TYPE": "Debug" 104 | } 105 | }, 106 | { 107 | "name": "x64-debug-clang", 108 | "displayName": "x64 Debug Clang", 109 | "description": "Target Windows (64-bit) with Ninja and Clang. (Debug)", 110 | "inherits": "windows-base", 111 | "architecture": { 112 | "value": "x64", 113 | "strategy": "external" 114 | }, 115 | "cacheVariables": { 116 | "CMAKE_BUILD_TYPE": "Debug", 117 | "CMAKE_C_COMPILER": "clang", 118 | "CMAKE_CXX_COMPILER": "clang++" 119 | } 120 | }, 121 | { 122 | "name": "x64-release", 123 | "displayName": "x64 Release", 124 | "description": "Target Windows (64-bit) with Ninja and MSVC. (Release)", 125 | "inherits": "x64-debug", 126 | "cacheVariables": { 127 | "CMAKE_BUILD_TYPE": "Release" 128 | } 129 | }, 130 | { 131 | "name": "visual-studio", 132 | "displayName": "Visual Studio x64", 133 | "description": "Using compilers for Visual Studio 17 2022 (x64 architecture)", 134 | "generator": "Visual Studio 17 2022", 135 | "toolset": "host=x64", 136 | "architecture": "x64", 137 | "binaryDir": "${sourceDir}/out/build/${presetName}", 138 | "cacheVariables": { 139 | "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", 140 | "CMAKE_C_COMPILER": "cl.exe", 141 | "CMAKE_CXX_COMPILER": "cl.exe" 142 | }, 143 | "condition": { 144 | "type": "equals", 145 | "lhs": "${hostSystemName}", 146 | "rhs": "Windows" 147 | } 148 | } 149 | ], 150 | "buildPresets": [ 151 | { 152 | "name": "visual-studio-debug", 153 | "displayName": "Visual Studio x64 Debug", 154 | "configurePreset": "visual-studio", 155 | "configuration": "Debug" 156 | }, 157 | { 158 | "name": "visual-studio-release", 159 | "displayName": "Visual Studio x64 Debug", 160 | "configurePreset": "visual-studio", 161 | "configuration": "Release" 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /include/genetic/genetic.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "genetic/details/concepts.h" 14 | #include "genetic/params.h" 15 | #include "genetic/selection.h" 16 | 17 | namespace dp { 18 | 19 | namespace genetic { 20 | namespace details { 21 | /** 22 | * @brief Fitness comparator for the population 23 | * @tparam Comparator compare operator (std::less, std::greater, etc) 24 | */ 25 | template > 26 | struct fitness_sort_op { 27 | template 28 | bool operator()(const T& first, const T& second) { 29 | Comparator cmp; 30 | return cmp(std::get(first), std::get(second)); 31 | } 32 | }; 33 | 34 | struct elitism_op { 35 | template 36 | constexpr auto operator()(Population&& population, 37 | std::size_t number_elitism) const { 38 | namespace vw = std::ranges::views; 39 | namespace rng = std::ranges; 40 | // no elitism, so return empty population 41 | if (number_elitism == 0) return vw::take(population, 0); 42 | 43 | // sort so that largest fitness item is at front 44 | rng::partial_sort( 45 | population, 46 | population.begin() + std::min(population.size(), number_elitism + 1), 47 | details::fitness_sort_op>{}); 48 | 49 | // select the first n in the current population 50 | return vw::take(population, number_elitism); 51 | } 52 | }; 53 | 54 | inline constexpr auto elitism = elitism_op{}; 55 | } // namespace details 56 | 57 | /// @brief Settings type for probabilities 58 | struct algorithm_settings { 59 | double elitism_rate = 0.0; 60 | double mutation_rate = 0.5; 61 | double crossover_rate = 0.2; 62 | }; 63 | 64 | template 65 | struct results { 66 | ChromosomeType best; 67 | double fitness{}; 68 | }; 69 | 70 | template 71 | struct iteration_statistics { 72 | results current_best; 73 | std::size_t current_generation_count{}; 74 | std::size_t population_size{}; 75 | }; 76 | 77 | template 78 | struct problem_description { 79 | Fitness fitness_op; 80 | }; 81 | 82 | template , 84 | typename IterationCallback = 85 | std::function&)>> 86 | requires std::default_initializable && 87 | concepts::population 88 | results solve( 89 | const PopulationType& initial_population, const algorithm_settings& settings, 90 | dp::genetic::params parameters = {}, 91 | const IterationCallback& callback = [](const iteration_statistics&) { 92 | }) { 93 | using chromosome_metadata = std::pair; 94 | using population = std::vector; 95 | using iteration_stats = iteration_statistics; 96 | namespace vw = std::ranges::views; 97 | namespace rng = std::ranges; 98 | 99 | static dp::thread_pool worker_pool{}; 100 | 101 | population current_population; 102 | // initialize our population 103 | rng::transform( 104 | initial_population, std::back_inserter(current_population), 105 | [&](ChromosomeType value) { 106 | return chromosome_metadata{ 107 | value, dp::genetic::evaluate_fitness(parameters.fitness_operator(), value)}; 108 | }); 109 | // sort by fitness 110 | rng::sort(current_population, details::fitness_sort_op{}); 111 | 112 | auto best_element = *rng::max_element( 113 | current_population, [](const std::pair& first, 114 | const std::pair& second) { 115 | return std::get(first) < std::get(second); 116 | }); 117 | 118 | iteration_stats stats{}; 119 | stats.current_best.best = std::get(best_element); 120 | stats.current_best.fitness = std::get(best_element); 121 | 122 | while (!dp::genetic::should_terminate(parameters.termination_operator(), 123 | std::get(best_element), 124 | std::get(best_element))) { 125 | auto number_elitism = static_cast(std::round( 126 | static_cast(current_population.size()) * settings.elitism_rate)); 127 | // perform elitism selection if it is enabled 128 | if (number_elitism == 0 && settings.elitism_rate > 0.0) number_elitism = 2; 129 | // generate elite population 130 | auto elite_population = details::elitism(current_population, number_elitism); 131 | 132 | // cross over 133 | auto crossover_number = static_cast(std::round( 134 | static_cast(current_population.size()) * settings.crossover_rate)); 135 | if (crossover_number <= 1) crossover_number = 4; 136 | 137 | // create a new "generation", we will also insert the elite population into this one 138 | std::vector>> 139 | future_results{}; 140 | future_results.reserve(crossover_number); 141 | 142 | for (std::size_t i = 0; i < crossover_number; i++) { 143 | std::future> future = 144 | worker_pool.enqueue( 145 | [](params prms, const population& pop) 146 | -> std::pair { 147 | // randomly select 2 parents 148 | auto chromosomes_only_view = pop | std::views::elements<0> | 149 | std::ranges::to(); 150 | auto [parent1, parent2] = dp::genetic::select_parents( 151 | prms.selection_operator(), chromosomes_only_view, 152 | [&prms](const auto& values) { 153 | return genetic::evaluate_fitness(prms.fitness_operator(), 154 | values); 155 | }); 156 | 157 | // generate two children from each parent sets 158 | auto child1 = dp::genetic::make_children(prms.crossover_operator(), 159 | parent1, parent2); 160 | auto child2 = dp::genetic::make_children(prms.crossover_operator(), 161 | parent2, parent1); 162 | 163 | // mutate the children 164 | child1 = dp::genetic::mutate(prms.mutation_operator(), child1); 165 | child2 = dp::genetic::mutate(prms.mutation_operator(), child2); 166 | 167 | // return the result + their fitness 168 | return std::make_pair( 169 | std::make_pair(child1, dp::genetic::evaluate_fitness( 170 | prms.fitness_operator(), child1)), 171 | std::make_pair(child2, dp::genetic::evaluate_fitness( 172 | prms.fitness_operator(), child2))); 173 | }, 174 | parameters, current_population); 175 | future_results.emplace_back(std::move(future)); 176 | } 177 | 178 | population crossover_population; 179 | crossover_population.reserve(crossover_number * 2 + elite_population.size()); 180 | 181 | for (auto& result : future_results) { 182 | auto [child1, child2] = result.get(); 183 | crossover_population.push_back(child1); 184 | crossover_population.push_back(child2); 185 | } 186 | 187 | if (!rng::empty(elite_population)) { 188 | // add elite population directly to new population 189 | crossover_population.insert(crossover_population.end(), 190 | elite_population.begin(), elite_population.end()); 191 | } 192 | 193 | // sort crossover population by fitness (lowest first) 194 | rng::sort(crossover_population, details::fitness_sort_op{}); 195 | 196 | // reset the current population 197 | current_population.clear(); 198 | // assign/move the new generation 199 | current_population = std::move(crossover_population); 200 | 201 | // update the best element 202 | auto temp_best_element = *rng::max_element(current_population); 203 | const auto [element, best_fitness] = temp_best_element; 204 | const auto previous_best_fitness = std::get(best_element); 205 | if (std::abs(best_fitness - previous_best_fitness) > 0.0) { 206 | // better fitness 207 | best_element = temp_best_element; 208 | } else { 209 | // current best did not improve previous best 210 | // insert previous best into the current population 211 | if (!current_population.empty()) current_population[0] = best_element; 212 | } 213 | 214 | // send callback stats for each generation 215 | stats.current_best.best = std::get(best_element); 216 | stats.current_best.fitness = std::get(best_element); 217 | stats.population_size = current_population.size(); 218 | ++stats.current_generation_count; 219 | callback(std::add_const_t(stats)); 220 | } 221 | 222 | return {std::get(best_element), std::get(best_element)}; 223 | } 224 | 225 | namespace experimental { 226 | template > 228 | requires std::default_initializable && 229 | concepts::population 230 | struct solve_impl { 231 | template 232 | std::pair operator()( 233 | const PopulationType& initial_population, auto&& description) const { 234 | // TODO solve the actual problem 235 | return {}; 236 | } 237 | }; 238 | 239 | template 240 | inline constexpr auto solve_problem = solve_impl{}; 241 | } // namespace experimental 242 | } // namespace genetic 243 | 244 | } // namespace dp 245 | -------------------------------------------------------------------------------- /test/source/genetic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // type declaration for knapsack problem 20 | // declared here to be used in ostream operator 21 | using knapsack = std::array; 22 | 23 | // print helper for knapsack, put in std:: namespace to allow for ADL 24 | // other option is to define this before including doctest.h 25 | // see https://github.com/doctest/doctest/issues/551 26 | namespace std { 27 | inline std::ostream& operator<<(std::ostream& out, const knapsack& ks) { 28 | out << "[ "; 29 | std::ranges::copy_n(ks.begin(), static_cast(ks.size()) - 1, 30 | std::ostream_iterator(out, ", ")); 31 | // print last element 32 | out << ks[ks.size() - 1] << " ]"; 33 | return out; 34 | } 35 | } // namespace std 36 | 37 | TEST_CASE("Knapsack problem") { 38 | // knapsack problem as described here: https://en.wikipedia.org/wiki/Knapsack_problem 39 | 40 | struct knapsack_box { 41 | int value; 42 | int weight; 43 | auto operator<=>(const knapsack_box&) const = default; 44 | }; 45 | 46 | // weight capacity of our knapsack 47 | constexpr auto max_weight = 15; 48 | 49 | // available boxes for the knapsack : {value, weight} 50 | std::vector available_items = {{4, 12}, {2, 1}, {10, 4}, {1, 1}, {2, 2}}; 51 | 52 | // fitness evaluator 53 | auto fitness = [&available_items](const knapsack ks) -> int { 54 | auto value_sum = 0; 55 | auto weight_sum = 0; 56 | for (const auto& index : ks) { 57 | if (index >= 0 && index < static_cast(available_items.size())) { 58 | value_sum += available_items[index].value; 59 | weight_sum += available_items[index].weight; 60 | } 61 | } 62 | // if the weight is less than the max, it adds to the value 63 | if (weight_sum > max_weight) value_sum -= 25 * std::abs(weight_sum - max_weight); 64 | return value_sum; 65 | }; 66 | 67 | auto engine = dp::genetic::details::initialize_random_engine(); 68 | 69 | // random mutation operator 70 | auto mutator = [&engine, &available_items](const knapsack& ks) { 71 | knapsack output = ks; 72 | 73 | std::uniform_int_distribution distribution(0, ks.size() - 1); 74 | const auto index = distribution(engine); 75 | 76 | if (std::ranges::count(output, -1) > 0) { 77 | // the output has some empty spaces, so there is the potential for us to add a new 78 | // number to the output 79 | assert(!available_items.empty()); 80 | std::uniform_int_distribution item_dist(0, available_items.size() - 1); 81 | auto new_value = item_dist(engine); 82 | // only allow unique values 83 | while (std::ranges::find(output, new_value) != std::end(output)) { 84 | new_value = item_dist(engine); 85 | } 86 | 87 | output[index] = static_cast(new_value); 88 | } else { 89 | // the output already has unique numbers in it, so we'll just shuffle it 90 | std::ranges::shuffle(output, engine); 91 | } 92 | 93 | return output; 94 | }; 95 | 96 | // crossover operator (i.e. child generator) 97 | auto crossover = [](const knapsack& first, const knapsack& second) { 98 | knapsack child{}; 99 | std::ranges::fill(child, -1); 100 | 101 | auto first_copy_end = first.begin() + 3; 102 | const auto first_negative = std::ranges::find(first, -1); 103 | if (first_negative != first.end() && first_negative < first_copy_end) { 104 | first_copy_end = first_negative; 105 | } 106 | 107 | // copy first elements over. 108 | std::ranges::copy(first.begin(), first_copy_end, child.begin()); 109 | 110 | // find the first negative value in the child knapsack (i.e. it's empty) 111 | auto child_first_negative = std::ranges::find(child, -1); 112 | 113 | // we need to copy from child_first_negative the first "negative_number_count" numbers that 114 | // are not already in the child knapsack 115 | for (const auto& value : second) { 116 | if (child_first_negative == child.end()) break; 117 | if (std::ranges::find(child, value) == child.end()) { 118 | *child_first_negative = value; 119 | child_first_negative += 1; 120 | } 121 | } 122 | return child; 123 | }; 124 | 125 | // check that the crossover function works correctly 126 | const knapsack p1 = {1, -1, -1, -1, -1}; 127 | const knapsack p2 = {0, 2, 3, -1, -1}; 128 | const knapsack p3 = {0, 1, -1, -1, -1}; 129 | const knapsack p4 = {0, 1, 2, 3, -1}; 130 | auto c1 = dp::genetic::make_children(crossover, p1, p2); 131 | auto c2 = dp::genetic::make_children(crossover, p2, p1); 132 | auto c3 = dp::genetic::make_children(crossover, p2, p4); 133 | auto c4 = dp::genetic::make_children(crossover, p3, p4); 134 | 135 | CHECK_EQ(c1, knapsack({1, 0, 2, 3, -1})); 136 | CHECK_EQ(c2, knapsack({0, 2, 3, 1, -1})); 137 | CHECK_EQ(c3, knapsack({0, 2, 3, 1, -1})); 138 | CHECK_EQ(c4, knapsack({0, 1, 2, 3, -1})); 139 | 140 | // the solution is all the boxes except for the heaviest one. 141 | const knapsack solution = {-1, 1, 2, 3, 4}; 142 | const knapsack all_items = {0, 1, 2, 3, 4}; 143 | 144 | // assert that the actual solution has the highest fitness value. 145 | // this needs to be true for the algorithm to converge correctly. 146 | CHECK(fitness(solution) > fitness(all_items)); 147 | 148 | // genetic algorithm settings. 149 | constexpr dp::genetic::algorithm_settings settings{0.1, 0.5, 0.25}; 150 | 151 | // generate an initial random population 152 | constexpr auto population_size = 2; 153 | std::vector initial_population{}; 154 | initial_population.reserve(population_size); 155 | 156 | // random length uniform distribution 157 | std::uniform_int_distribution length_dist(1, 4); 158 | 159 | // random value uniform distribution 160 | std::uniform_int_distribution values_dist(0, available_items.size() - 1); 161 | 162 | // generator lambda 163 | auto knapsack_generator = [&engine, &length_dist, &values_dist]() { 164 | knapsack basic; 165 | std::ranges::fill(basic, -1); 166 | 167 | const auto random_length = length_dist(engine); 168 | for (auto i = 0; i < random_length; ++i) { 169 | auto value = values_dist(engine); 170 | // only allow unique values 171 | while (std::ranges::find(basic, value) != std::end(basic)) { 172 | value = values_dist(engine); 173 | } 174 | basic[i] = static_cast(value); 175 | } 176 | return basic; 177 | }; 178 | 179 | // generate the initial population 180 | std::ranges::generate_n(std::back_inserter(initial_population), population_size, 181 | knapsack_generator); 182 | 183 | // define the termination criteria 184 | auto termination = dp::genetic::fitness_termination(fitness(solution)); 185 | 186 | static_assert(dp::genetic::concepts::termination_operator); 188 | 189 | static_assert( 190 | dp::genetic::concepts::selection_operator, decltype(fitness)>); 192 | 193 | auto params = dp::genetic::params::builder() 194 | .with_mutation_operator(mutator) 195 | .with_crossover_operator(crossover) 196 | .with_fitness_operator(fitness) 197 | .with_termination_operator(termination) 198 | .build(); 199 | 200 | auto start = std::chrono::steady_clock::now(); 201 | auto [best, _] = dp::genetic::solve(initial_population, settings, params, [](auto& stats) { 202 | std::cout << "best: " << stats.current_best.best 203 | << " fitness: " << stats.current_best.fitness 204 | << " pop size: " << std::to_string(stats.current_generation_count) << "\n"; 205 | }); 206 | auto stop = std::chrono::steady_clock::now(); 207 | auto time_ms = std::chrono::duration_cast(stop - start).count(); 208 | std::cout << "Total time (ms): " << std::to_string(time_ms) << "\n"; 209 | // sort the best to match against the solution 210 | std::ranges::sort(best); 211 | CHECK(best == solution); 212 | } 213 | 214 | TEST_CASE("Beale function") { 215 | // define our data type as a 2D "vector" 216 | using data_t = std::array; 217 | const auto fitness = [](const data_t& value) -> double { 218 | const auto x = value[0]; 219 | const auto y = value[1]; 220 | // multiply by -1 to maximize the function 221 | const auto xy = x * y; 222 | const auto y_squared = std::pow(y, 2); 223 | const auto y_cubed = std::pow(y, 3); 224 | return -(std::pow(1.5 - x + xy, 2) + std::pow(2.25 - x + x * y_squared, 2) + 225 | std::pow(2.625 - x + x * y_cubed, 2)); 226 | }; 227 | 228 | CHECK(fitness({3, 0.5}) == doctest::Approx(0.0).epsilon(0.01)); 229 | 230 | dp::genetic::uniform_floating_point_generator generator{}; 231 | auto generate_value = [&generator] { return generator(-4.5, 4.5); }; 232 | 233 | std::vector initial_population; 234 | 235 | // generate our initial population 236 | std::ranges::generate_n(std::back_inserter(initial_population), 10'000, [&generate_value]() { 237 | return std::array{generate_value(), generate_value()}; 238 | }); 239 | 240 | constexpr double increment = 0.00001; 241 | 242 | auto double_mutator = dp::genetic::double_value_mutator(-increment, increment); 243 | // if fitness doesn't change a significant amount in 30 generations, terminate 244 | auto termination = dp::genetic::fitness_hysteresis{1.e-8, 30}; 245 | // auto termination = dp::genetic::generations_termination{50'000}; 246 | const auto params = dp::genetic::params::builder() 247 | .with_fitness_operator(fitness) 248 | .with_mutation_operator(double_mutator) 249 | .with_crossover_operator(dp::genetic::random_crossover{}) 250 | .with_termination_operator(termination) 251 | .build(); 252 | 253 | const auto [best, _] = dp::genetic::solve( 254 | initial_population, dp::genetic::algorithm_settings{.elitism_rate = 0.25}, params, 255 | [](auto& stats) { 256 | std::cout << std::format("best: [{}, {}]", stats.current_best.best[0], 257 | stats.current_best.best[1]) 258 | << " fitness: " << stats.current_best.fitness 259 | << " pop size: " << std::to_string(stats.current_generation_count) << "\n"; 260 | }); 261 | 262 | const auto [x, y] = best; 263 | CHECK(x == doctest::Approx(3.0).epsilon(0.001)); 264 | CHECK(y == doctest::Approx(0.5).epsilon(0.001)); 265 | } 266 | 267 | TEST_CASE("sin(x)") { 268 | // define our data type as a 1D "vector" 269 | using data_t = std::array; 270 | const auto fitness = [](const data_t& value) -> double { 271 | const auto x = value[0]; 272 | return std::sin(x); 273 | }; 274 | 275 | dp::genetic::uniform_floating_point_generator generator{}; 276 | auto generate_value = [&generator] { return generator(-std::numbers::pi, std::numbers::pi); }; 277 | 278 | std::vector initial_population; 279 | 280 | // generate our initial population 281 | std::ranges::generate_n(std::back_inserter(initial_population), 10'000, 282 | [&generate_value]() { return std::array{generate_value()}; }); 283 | 284 | constexpr double increment = 0.00001; 285 | 286 | auto double_mutator = dp::genetic::double_value_mutator(-increment, increment); 287 | // if fitness doesn't change a significant amount in 30 generations, terminate 288 | auto termination = dp::genetic::fitness_hysteresis{1.e-8, 30}; 289 | 290 | const auto params = dp::genetic::params::builder() 291 | .with_fitness_operator(fitness) 292 | .with_mutation_operator(double_mutator) 293 | .with_crossover_operator(dp::genetic::random_crossover{}) 294 | .with_termination_operator(termination) 295 | .build(); 296 | 297 | const auto [best, _] = dp::genetic::solve( 298 | initial_population, dp::genetic::algorithm_settings{.elitism_rate = 0.25}, params, 299 | [](auto& stats) { 300 | std::cout << std::format("best: [{}, {}]", stats.current_best.best[0], 301 | stats.current_best.fitness) 302 | << " fitness: " << stats.current_best.fitness 303 | << " pop size: " << std::to_string(stats.current_generation_count) << "\n"; 304 | }); 305 | 306 | const auto [x] = best; 307 | CHECK(x == doctest::Approx(std::numbers::pi / 2.).epsilon(0.001)); 308 | } 309 | --------------------------------------------------------------------------------