diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e39106..0757de6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,4 @@ cmake_minimum_required(VERSION 3.25) -macro(compile_options target_name) - target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) - target_link_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) -endmacro() macro(sanitizers target_name) if (${ENABLE_ADDRSAN} MATCHES ON) @@ -21,7 +17,39 @@ macro(sanitizers target_name) endif () endmacro() -project(blt-gp VERSION 0.2.1) +macro(compile_options target_name) + if (NOT ${MOLD} STREQUAL MOLD-NOTFOUND) + target_compile_options(${target_name} PUBLIC -fuse-ld=mold) + endif () + + target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) + target_link_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) + sanitizers(${target_name}) +endmacro() + +macro(blt_add_project name source type) + + project(${name}-${type}) + + add_executable(${name}-${type} ${source}) + + target_link_libraries(${name}-${type} PRIVATE BLT blt-gp Threads::Threads) + + compile_options(${name}-${type}) + target_compile_definitions(${name}-${type} PRIVATE BLT_DEBUG_LEVEL=${DEBUG_LEVEL}) + + if (${TRACK_ALLOCATIONS}) + target_compile_definitions(${name}-${type} PRIVATE BLT_TRACK_ALLOCATIONS=1) + endif () + + add_test(NAME ${name} COMMAND ${name}-${type}) + + set_property(TEST ${name} PROPERTY FAIL_REGULAR_EXPRESSION "FAIL;ERROR;FATAL;exception") + + project(blt-gp) +endmacro() + +project(blt-gp VERSION 0.2.2) include(CTest) @@ -49,8 +77,7 @@ file(GLOB_RECURSE PROJECT_BUILD_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") add_library(blt-gp ${PROJECT_BUILD_FILES}) -target_compile_options(blt-gp PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) -target_link_options(blt-gp PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) +compile_options(blt-gp) find_program(MOLD "mold") @@ -67,70 +94,10 @@ if (${TRACK_ALLOCATIONS}) target_compile_definitions(blt-gp PRIVATE BLT_TRACK_ALLOCATIONS=1) endif () -if (${ENABLE_ADDRSAN} MATCHES ON) - target_compile_options(blt-gp PRIVATE -fsanitize=address) - target_link_options(blt-gp PRIVATE -fsanitize=address) -endif () - -if (${ENABLE_UBSAN} MATCHES ON) - target_compile_options(blt-gp PRIVATE -fsanitize=undefined) - target_link_options(blt-gp PRIVATE -fsanitize=undefined) -endif () - -if (${ENABLE_TSAN} MATCHES ON) - target_compile_options(blt-gp PRIVATE -fsanitize=thread) - target_link_options(blt-gp PRIVATE -fsanitize=thread) -endif () - -macro(blt_add_project name source type) - - project(${name}-${type}) - - #add_compile_options(-fuse-ld=mold) - - add_executable(${name}-${type} ${source}) - - target_link_libraries(${name}-${type} PRIVATE BLT blt-gp Threads::Threads) - - target_compile_options(${name}-${type} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) - target_link_options(${name}-${type} PRIVATE -Wall -Wextra -Wpedantic -Wno-comment) - target_compile_definitions(${name}-${type} PRIVATE BLT_DEBUG_LEVEL=${DEBUG_LEVEL}) - - if (${TRACK_ALLOCATIONS}) - target_compile_definitions(${name}-${type} PRIVATE BLT_TRACK_ALLOCATIONS=1) - endif () - - if (${ENABLE_ADDRSAN} MATCHES ON) - target_compile_options(${name}-${type} PRIVATE -fsanitize=address) - target_link_options(${name}-${type} PRIVATE -fsanitize=address) - endif () - - if (${ENABLE_UBSAN} MATCHES ON) - target_compile_options(${name}-${type} PRIVATE -fsanitize=undefined) - target_link_options(${name}-${type} PRIVATE -fsanitize=undefined) - endif () - - if (${ENABLE_TSAN} MATCHES ON) - target_compile_options(${name}-${type} PRIVATE -fsanitize=thread) - target_link_options(${name}-${type} PRIVATE -fsanitize=thread) - endif () - - add_test(NAME ${name} COMMAND ${name}-${type}) - - # set (passRegex "Pass" "Passed" "PASS" "PASSED") - # set (failRegex "WARN" "FAIL" "ERROR" "FATAL") - set(failRegex "\\[WARN\\]" "FAIL" "ERROR" "FATAL" "exception") - - # set_property (TEST ${name} PROPERTY PASS_REGULAR_EXPRESSION "${passRegex}") - set_property(TEST ${name} PROPERTY FAIL_REGULAR_EXPRESSION "${failRegex}") - - project(blt-gp) -endmacro() - if (${BUILD_EXAMPLES}) - blt_add_project(blt-symbolic-regression examples/symbolic_regression.cpp example) - blt_add_project(blt-rice-classification examples/rice_classification.cpp example) + blt_add_project(blt-symbolic-regression examples/src/symbolic_regression.cpp example) + blt_add_project(blt-rice-classification examples/src/rice_classification.cpp example) endif () diff --git a/examples/operations_common.h b/examples/operations_common.h deleted file mode 100644 index 5140595..0000000 --- a/examples/operations_common.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -/* - * Copyright (C) 2024 Brett Terpstra - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef BLT_GP_OPERATIONS_COMMON_H -#define BLT_GP_OPERATIONS_COMMON_H - -#include - -blt::gp::operation_t add([](float a, float b) { return a + b; }, "add"); -blt::gp::operation_t sub([](float a, float b) { return a - b; }, "sub"); -blt::gp::operation_t mul([](float a, float b) { return a * b; }, "mul"); -blt::gp::operation_t pro_div([](float a, float b) { return b == 0.0f ? 1.0f : a / b; }, "div"); -blt::gp::operation_t op_sin([](float a) { return std::sin(a); }, "sin"); -blt::gp::operation_t op_cos([](float a) { return std::cos(a); }, "cos"); -blt::gp::operation_t op_exp([](float a) { return std::exp(a); }, "exp"); -blt::gp::operation_t op_log([](float a) { return a == 0.0f ? 0.0f : std::log(a); }, "log"); - -#endif //BLT_GP_OPERATIONS_COMMON_H diff --git a/examples/rice_classification.cpp b/examples/src/rice_classification.cpp similarity index 100% rename from examples/rice_classification.cpp rename to examples/src/rice_classification.cpp diff --git a/examples/src/symbolic_regression.cpp b/examples/src/symbolic_regression.cpp new file mode 100644 index 0000000..4529d78 --- /dev/null +++ b/examples/src/symbolic_regression.cpp @@ -0,0 +1,41 @@ +/* + * + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "../symbolic_regression.h" + +static const unsigned long SEED = std::random_device()(); + +blt::gp::prog_config_t config = blt::gp::prog_config_t() + .set_initial_min_tree_size(2) + .set_initial_max_tree_size(6) + .set_elite_count(2) + .set_crossover_chance(0.9) + .set_mutation_chance(0.0) + .set_reproduction_chance(0.25) + .set_max_generations(50) + .set_pop_size(500) + .set_thread_count(0); + +int main() +{ + blt::gp::example::symbolic_regression_t regression{config, SEED}; + regression.execute(); + + BLT_TRACE("%lf vs %lf", blt::gp::parent_fitness.load(std::memory_order_relaxed), blt::gp::child_fitness.load(std::memory_order_relaxed)); + + return 0; +} \ No newline at end of file diff --git a/examples/symbolic_regression.cpp b/examples/symbolic_regression.cpp deleted file mode 100644 index aebb0ba..0000000 --- a/examples/symbolic_regression.cpp +++ /dev/null @@ -1,196 +0,0 @@ -/* - * - * Copyright (C) 2024 Brett Terpstra - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include -#include -#include -#include -#include -#include -#include "operations_common.h" -#include "blt/math/averages.h" - -//static constexpr long SEED = 41912; -static const unsigned long SEED = std::random_device()(); - -struct context -{ - float x, y; -}; - -std::array training_cases; - -blt::gp::mutation_t mut; - -blt::gp::prog_config_t config = blt::gp::prog_config_t() - .set_initial_min_tree_size(2) - .set_initial_max_tree_size(6) - .set_elite_count(2) - .set_crossover_chance(0.9) - .set_mutation_chance(0.1) - .set_reproduction_chance(0) - .set_max_generations(50) - .set_pop_size(500) - .set_thread_count(0); -//blt::gp::prog_config_t config = blt::gp::prog_config_t() -// .set_initial_min_tree_size(2) -// .set_initial_max_tree_size(6) -// .set_elite_count(2) -// .set_crossover_chance(0.9) -// .set_mutation_chance(0.1) -// .set_reproduction_chance(0) -// .set_max_generations(50) -// .set_pop_size(500) -// .set_thread_count(0); - -blt::gp::gp_program program{SEED, config}; - -auto lit = blt::gp::operation_t([]() { - return program.get_random().get_float(-1.0f, 1.0f); -}, "lit").set_ephemeral(); - -blt::gp::operation_t op_x([](const context& context) { - return context.x; -}, "x"); - -constexpr auto fitness_function = [](blt::gp::tree_t& current_tree, blt::gp::fitness_t& fitness, blt::size_t) { - constexpr double value_cutoff = 1.e15; - for (auto& fitness_case : training_cases) - { - auto diff = std::abs(fitness_case.y - current_tree.get_evaluation_value(fitness_case)); - if (diff < value_cutoff) - { - fitness.raw_fitness += diff; - if (diff <= 0.01) - fitness.hits++; - } else - fitness.raw_fitness += value_cutoff; - } - fitness.standardized_fitness = fitness.raw_fitness; - fitness.adjusted_fitness = (1.0 / (1.0 + fitness.standardized_fitness)); - return static_cast(fitness.hits) == training_cases.size(); -}; - -float example_function(float x) -{ - return x * x * x * x + x * x * x + x * x + x; -} - -int main() -{ - BLT_INFO("Starting BLT-GP Symbolic Regression Example"); - BLT_START_INTERVAL("Symbolic Regression", "Main"); - BLT_DEBUG("Setup Fitness cases"); - for (auto& fitness_case : training_cases) - { - constexpr float range = 10; - constexpr float half_range = range / 2.0; - auto x = program.get_random().get_float(-half_range, half_range); - auto y = example_function(x); - fitness_case = {x, y}; - } - - BLT_DEBUG("Setup Types and Operators"); - blt::gp::operator_builder builder{}; - program.set_operations(builder.build(add, sub, mul, pro_div, op_sin, op_cos, op_exp, op_log, lit, op_x)); - - BLT_DEBUG("Generate Initial Population"); - auto sel = blt::gp::select_fitness_proportionate_t{}; - program.generate_population(program.get_typesystem().get_type().id(), fitness_function, sel, sel, sel); - - BLT_DEBUG("Begin Generation Loop"); - while (!program.should_terminate()) - { - BLT_TRACE("------------{Begin Generation %ld}------------", program.get_current_generation()); - BLT_TRACE("Creating next generation"); - BLT_START_INTERVAL("Symbolic Regression", "Gen"); - program.create_next_generation(); - BLT_END_INTERVAL("Symbolic Regression", "Gen"); - BLT_TRACE("Move to next generation"); - BLT_START_INTERVAL("Symbolic Regression", "Fitness"); - program.next_generation(); - BLT_TRACE("Evaluate Fitness"); - program.evaluate_fitness(); - BLT_END_INTERVAL("Symbolic Regression", "Fitness"); - BLT_TRACE("----------------------------------------------"); - std::cout << std::endl; - } - - BLT_END_INTERVAL("Symbolic Regression", "Main"); - - auto best = program.get_best_individuals<3>(); - - BLT_INFO("Best approximations:"); - for (auto& i_ref : best) - { - auto& i = i_ref.get(); - BLT_DEBUG("Fitness: %lf, stand: %lf, raw: %lf", i.fitness.adjusted_fitness, i.fitness.standardized_fitness, i.fitness.raw_fitness); - i.tree.print(program, std::cout); - std::cout << "\n"; - } - auto& stats = program.get_population_stats(); - BLT_INFO("Stats:"); - BLT_INFO("Average fitness: %lf", stats.average_fitness.load()); - BLT_INFO("Best fitness: %lf", stats.best_fitness.load()); - BLT_INFO("Worst fitness: %lf", stats.worst_fitness.load()); - BLT_INFO("Overall fitness: %lf", stats.overall_fitness.load()); - // TODO: make stats helper - - BLT_PRINT_PROFILE("Symbolic Regression", blt::PRINT_CYCLES | blt::PRINT_THREAD | blt::PRINT_WALL); - -#ifdef BLT_TRACK_ALLOCATIONS - BLT_TRACE("Total Allocations: %ld times with a total of %s, peak allocated bytes %s", blt::gp::tracker.getAllocations(), - blt::byte_convert_t(blt::gp::tracker.getAllocatedBytes()).convert_to_nearest_type().to_pretty_string().c_str(), - blt::byte_convert_t(blt::gp::tracker.getPeakAllocatedBytes()).convert_to_nearest_type().to_pretty_string().c_str()); - BLT_TRACE("------------------------------------------------------"); - auto evaluation_calls_v = blt::gp::evaluation_calls.get_calls(); - auto evaluation_allocations_v = blt::gp::evaluation_allocations.get_calls(); - BLT_TRACE("Total Evaluation Calls: %ld; Peak Bytes Allocated %s", evaluation_calls_v, - blt::string::bytes_to_pretty(blt::gp::evaluation_calls.get_value()).c_str()); - BLT_TRACE("Total Evaluation Allocations: %ld; Bytes %s; Average %s", evaluation_allocations_v, - blt::string::bytes_to_pretty(blt::gp::evaluation_allocations.get_value()).c_str(), - blt::string::bytes_to_pretty(blt::average(blt::gp::evaluation_allocations.get_value(), evaluation_allocations_v)).c_str()); - BLT_TRACE("Percent Evaluation calls allocate? %lf%%", blt::average(evaluation_allocations_v, evaluation_calls_v) * 100); - BLT_TRACE("------------------------------------------------------"); - auto crossover_calls_v = blt::gp::crossover_calls.get_calls(); - auto crossover_allocations_v = blt::gp::crossover_allocations.get_calls(); - auto mutation_calls_v = blt::gp::mutation_calls.get_calls(); - auto mutation_allocations_v = blt::gp::mutation_allocations.get_calls(); - auto reproduction_calls_v = blt::gp::reproduction_calls.get_calls(); - auto reproduction_allocations_v = blt::gp::reproduction_allocations.get_calls(); - BLT_TRACE("Total Crossover Calls: %ld; Peak Bytes Allocated %s", crossover_calls_v, - blt::string::bytes_to_pretty(blt::gp::crossover_calls.get_value()).c_str()); - BLT_TRACE("Total Mutation Calls: %ld; Peak Bytes Allocated %s", mutation_calls_v, - blt::string::bytes_to_pretty(blt::gp::mutation_calls.get_value()).c_str()); - BLT_TRACE("Total Reproduction Calls: %ld; Peak Bytes Allocated %s", reproduction_calls_v, - blt::string::bytes_to_pretty(blt::gp::reproduction_calls.get_value()).c_str()); - BLT_TRACE("Total Crossover Allocations: %ld; Bytes %s; Average %s", crossover_allocations_v, - blt::string::bytes_to_pretty(blt::gp::crossover_allocations.get_value()).c_str(), - blt::string::bytes_to_pretty(blt::average(blt::gp::crossover_allocations.get_value(), crossover_allocations_v)).c_str()); - BLT_TRACE("Total Mutation Allocations: %ld; Bytes %s; Average %s", mutation_allocations_v, - blt::string::bytes_to_pretty(blt::gp::mutation_allocations.get_value()).c_str(), - blt::string::bytes_to_pretty(blt::average(blt::gp::mutation_allocations.get_value(), mutation_allocations_v)).c_str()); - BLT_TRACE("Total Reproduction Allocations: %ld; Bytes %s; Average %s", reproduction_allocations_v, - blt::string::bytes_to_pretty(blt::gp::reproduction_allocations.get_value()).c_str(), - blt::string::bytes_to_pretty(blt::average(blt::gp::reproduction_allocations.get_value(), reproduction_allocations_v)).c_str()); - BLT_TRACE("Percent Crossover calls allocate? %lf%%", blt::average(crossover_allocations_v, crossover_calls_v) * 100); - BLT_TRACE("Percent Mutation calls allocate? %lf%%", blt::average(mutation_allocations_v, mutation_calls_v) * 100); - BLT_TRACE("Percent Reproduction calls allocate? %lf%%", blt::average(reproduction_allocations_v, reproduction_calls_v) * 100); -#endif - - return 0; -} \ No newline at end of file diff --git a/examples/symbolic_regression.h b/examples/symbolic_regression.h new file mode 100644 index 0000000..fec80a0 --- /dev/null +++ b/examples/symbolic_regression.h @@ -0,0 +1,167 @@ +#pragma once +/* + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BLT_GP_EXAMPLE_SYMBOLIC_REGRESSION_H +#define BLT_GP_EXAMPLE_SYMBOLIC_REGRESSION_H + +#include +#include +#include +#include +#include + +namespace blt::gp::example +{ + class symbolic_regression_t + { + public: + struct context + { + float x, y; + }; + + private: + bool fitness_function(const tree_t& current_tree, fitness_t& fitness, size_t) const + { + constexpr static double value_cutoff = 1.e15; + for (auto& fitness_case : training_cases) + { + const auto diff = std::abs(fitness_case.y - current_tree.get_evaluation_value(fitness_case)); + if (diff < value_cutoff) + { + fitness.raw_fitness += diff; + if (diff <= 0.01) + fitness.hits++; + } + else + fitness.raw_fitness += value_cutoff; + } + fitness.standardized_fitness = fitness.raw_fitness; + fitness.adjusted_fitness = (1.0 / (1.0 + fitness.standardized_fitness)); + return static_cast(fitness.hits) == training_cases.size(); + } + + static float example_function(const float x) + { + return x * x * x * x + x * x * x + x * x + x; + } + + public: + symbolic_regression_t(const prog_config_t& config, const size_t seed): program{seed, config} + { + BLT_INFO("Starting BLT-GP Symbolic Regression Example"); + BLT_DEBUG("Setup Fitness cases"); + for (auto& fitness_case : training_cases) + { + constexpr float range = 10; + constexpr float half_range = range / 2.0; + const auto x = program.get_random().get_float(-half_range, half_range); + const auto y = example_function(x); + fitness_case = {x, y}; + } + } + + template + auto make_operations(operator_builder& builder) + { + static operation_t add{[](const float a, const float b) { return a + b; }, "add"}; + static operation_t sub([](const float a, const float b) { return a - b; }, "sub"); + static operation_t mul([](const float a, const float b) { return a * b; }, "mul"); + static operation_t pro_div([](const float a, const float b) { return b == 0.0f ? 1.0f : a / b; }, "div"); + static operation_t op_sin([](const float a) { return std::sin(a); }, "sin"); + static operation_t op_cos([](const float a) { return std::cos(a); }, "cos"); + static operation_t op_exp([](const float a) { return std::exp(a); }, "exp"); + static operation_t op_log([](const float a) { return a == 0.0f ? 0.0f : std::log(a); }, "log"); + static auto lit = operation_t([this]() + { + return program.get_random().get_float(-1.0f, 1.0f); + }, "lit").set_ephemeral(); + + static operation_t op_x([](const context& context) + { + return context.x; + }, "x"); + + return builder.build(add, sub, mul, pro_div, op_sin, op_cos, op_exp, op_log, lit, op_x); + } + + void execute() + { + BLT_DEBUG("Setup Types and Operators"); + operator_builder builder{}; + program.set_operations(make_operations(builder)); + + BLT_DEBUG("Generate Initial Population"); + auto sel = select_tournament_t{}; + auto fitness = [this](const tree_t& c, fitness_t& f, const size_t i) { return fitness_function(c, f, i); }; + program.generate_population(program.get_typesystem().get_type().id(), fitness, sel, sel, sel); + + BLT_DEBUG("Begin Generation Loop"); + while (!program.should_terminate()) + { + auto cross = crossover_calls.start_measurement(); + auto mut = mutation_calls.start_measurement(); + auto repo = reproduction_calls.start_measurement(); + BLT_TRACE("------------{Begin Generation %ld}------------", program.get_current_generation()); + BLT_TRACE("Creating next generation"); + program.create_next_generation(); + BLT_TRACE("Move to next generation"); + program.next_generation(); + BLT_TRACE("Evaluate Fitness"); + program.evaluate_fitness(); + const auto& stats = program.get_population_stats(); + BLT_TRACE("Avg Fit: %lf, Best Fit: %lf, Worst Fit: %lf, Overall Fit: %lf", + stats.average_fitness.load(std::memory_order_relaxed), stats.best_fitness.load(std::memory_order_relaxed), + stats.worst_fitness.load(std::memory_order_relaxed), stats.overall_fitness.load(std::memory_order_relaxed)); + crossover_calls.stop_measurement(cross); + mutation_calls.stop_measurement(mut); + reproduction_calls.stop_measurement(repo); + const auto total = (cross.get_call_difference() * 2) + mut.get_call_difference() + repo.get_call_difference(); + BLT_TRACE("Calls Crossover: %ld, Mutation %ld, Reproduction %ld; %ld", cross.get_call_difference(), mut.get_call_difference(), repo.get_call_difference(), total); + BLT_TRACE("Value Crossover: %ld, Mutation %ld, Reproduction %ld; %ld", cross.get_value_difference(), mut.get_value_difference(), repo.get_value_difference(), (cross.get_value_difference() * 2 + mut.get_value_difference() + repo.get_value_difference()) - total); + BLT_TRACE("----------------------------------------------"); + std::cout << std::endl; + } + + const auto best = program.get_best_individuals<3>(); + + BLT_INFO("Best approximations:"); + for (auto& i_ref : best) + { + auto& i = i_ref.get(); + BLT_DEBUG("Fitness: %lf, stand: %lf, raw: %lf", i.fitness.adjusted_fitness, i.fitness.standardized_fitness, i.fitness.raw_fitness); + i.tree.print(program, std::cout); + std::cout << "\n"; + } + const auto& stats = program.get_population_stats(); + BLT_INFO("Stats:"); + BLT_INFO("Average fitness: %lf", stats.average_fitness.load()); + BLT_INFO("Best fitness: %lf", stats.best_fitness.load()); + BLT_INFO("Worst fitness: %lf", stats.worst_fitness.load()); + BLT_INFO("Overall fitness: %lf", stats.overall_fitness.load()); + // TODO: make stats helper + } + + private: + gp_program program; + mutation_t mut; + std::array training_cases{}; + }; +} + +#endif //BLT_GP_EXAMPLE_SYMBOLIC_REGRESSION_H diff --git a/include/blt/gp/allocator.h b/include/blt/gp/allocator.h index 807d45a..2ec392c 100644 --- a/include/blt/gp/allocator.h +++ b/include/blt/gp/allocator.h @@ -21,105 +21,123 @@ #include #include +#include #include namespace blt::gp { +#ifdef BLT_TRACK_ALLOCATIONS + inline allocation_tracker_t tracker; + + // population gen specifics + inline call_tracker_t crossover_calls; + inline call_tracker_t mutation_calls; + inline call_tracker_t reproduction_calls; + inline call_tracker_t crossover_allocations; + inline call_tracker_t mutation_allocations; + inline call_tracker_t reproduction_allocations; + + // for evaluating fitness + inline call_tracker_t evaluation_calls; + inline call_tracker_t evaluation_allocations; +#endif + class aligned_allocator { - public: - void* allocate(blt::size_t bytes) // NOLINT - { + public: + void* allocate(blt::size_t bytes) // NOLINT + { #ifdef BLT_TRACK_ALLOCATIONS - tracker.allocate(bytes); -// std::cout << "Hey our aligned allocator allocated " << bytes << " bytes!\n"; + tracker.allocate(bytes); + // std::cout << "Hey our aligned allocator allocated " << bytes << " bytes!\n"; #endif - return std::aligned_alloc(8, bytes); - } - - void deallocate(void* ptr, blt::size_t bytes) // NOLINT - { - if (ptr == nullptr) - return; + return std::aligned_alloc(8, bytes); + } + + void deallocate(void* ptr, blt::size_t bytes) // NOLINT + { + if (ptr == nullptr) + return; #ifdef BLT_TRACK_ALLOCATIONS - tracker.deallocate(bytes); -// std::cout << "[Hey our aligned allocator deallocated " << bytes << " bytes!]\n"; + tracker.deallocate(bytes); + // std::cout << "[Hey our aligned allocator deallocated " << bytes << " bytes!]\n"; #else (void) bytes; #endif - std::free(ptr); - } + std::free(ptr); + } }; - - template + + template class tracked_allocator_t { - public: - using value_type = T; - using reference = T&; - using const_reference = const T&; - using pointer = T*; - using const_pointer = const T*; - using void_pointer = void*; - using const_void_pointer = const void*; - using difference_type = blt::ptrdiff_t; - using size_type = blt::size_t; - template - struct rebind - { - typedef tracked_allocator_t other; - }; - - pointer allocate(size_type n) - { + public: + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using void_pointer = void*; + using const_void_pointer = const void*; + using difference_type = blt::ptrdiff_t; + using size_type = blt::size_t; + + template + struct rebind + { + typedef tracked_allocator_t other; + }; + + pointer allocate(size_type n) + { #ifdef BLT_TRACK_ALLOCATIONS - tracker.allocate(n * sizeof(T)); -// std::cout << "Hey our tracked allocator allocated " << (n * sizeof(T)) << " bytes!\n"; + tracker.allocate(n * sizeof(T)); + // std::cout << "Hey our tracked allocator allocated " << (n * sizeof(T)) << " bytes!\n"; #endif - return static_cast(std::malloc(n * sizeof(T))); - } - - pointer allocate(size_type n, const_void_pointer) - { - return allocate(n); - } - - void deallocate(pointer p, size_type n) - { + return static_cast(std::malloc(n * sizeof(T))); + } + + pointer allocate(size_type n, const_void_pointer) + { + return allocate(n); + } + + void deallocate(pointer p, size_type n) + { #ifdef BLT_TRACK_ALLOCATIONS - tracker.deallocate(n * sizeof(T)); -// std::cout << "[Hey our tracked allocator deallocated " << (n * sizeof(T)) << " bytes!]\n"; + ::blt::gp::tracker.deallocate(n * sizeof(T)); + // std::cout << "[Hey our tracked allocator deallocated " << (n * sizeof(T)) << " bytes!]\n"; #else (void) n; #endif - std::free(p); - } - - template - void construct(U* p, Args&& ... args) - { - new(p) T(std::forward(args)...); - } - - template - void destroy(U* p) - { - p->~T(); - } - - [[nodiscard]] size_type max_size() const noexcept - { - return std::numeric_limits::max(); - } + std::free(p); + } + + template + void construct(U* p, Args&&... args) + { + new(p) T(std::forward(args)...); + } + + template + void destroy(U* p) + { + p->~T(); + } + + [[nodiscard]] size_type max_size() const noexcept + { + return std::numeric_limits::max(); + } }; - - template + + template inline static bool operator==(const tracked_allocator_t& lhs, const tracked_allocator_t& rhs) noexcept { return &lhs == &rhs; } - - template + + template inline static bool operator!=(const tracked_allocator_t& lhs, const tracked_allocator_t& rhs) noexcept { return &lhs != &rhs; diff --git a/include/blt/gp/fwdecl.h b/include/blt/gp/fwdecl.h index 3e30e69..52a6dcc 100644 --- a/include/blt/gp/fwdecl.h +++ b/include/blt/gp/fwdecl.h @@ -32,22 +32,6 @@ namespace blt::gp { -#ifdef BLT_TRACK_ALLOCATIONS - inline allocation_tracker_t tracker; - - // population gen specifics - inline call_tracker_t crossover_calls; - inline call_tracker_t mutation_calls; - inline call_tracker_t reproduction_calls; - inline call_tracker_t crossover_allocations; - inline call_tracker_t mutation_allocations; - inline call_tracker_t reproduction_allocations; - - // for evaluating fitness - inline call_tracker_t evaluation_calls; - inline call_tracker_t evaluation_allocations; -#endif - class gp_program; class type; diff --git a/include/blt/gp/program.h b/include/blt/gp/program.h index 288a12f..46f53ea 100644 --- a/include/blt/gp/program.h +++ b/include/blt/gp/program.h @@ -60,13 +60,13 @@ namespace blt::gp { blt::u32 argc = 0; blt::u32 argc_context = 0; - + [[nodiscard]] bool is_terminal() const { return argc == 0; } }; - + struct operator_info_t { // types of the arguments @@ -78,14 +78,14 @@ namespace blt::gp // per operator function callable (slow) detail::operator_func_t func; }; - + struct operator_metadata_t { blt::size_t arg_size_bytes = 0; blt::size_t return_size_bytes = 0; argc_t argc{}; }; - + struct program_operator_storage_t { // indexed from return TYPE ID, returns index of operator @@ -99,312 +99,325 @@ namespace blt::gp tracked_vector print_funcs; tracked_vector destroy_funcs; tracked_vector> names; - + detail::eval_func_t eval_func; - + type_provider system; }; - - template + + template class operator_builder { - friend class gp_program; - - friend class blt::gp::detail::operator_storage_test; - - public: - explicit operator_builder() = default; - - template - program_operator_storage_t& build(Operators& ... operators) - { - blt::size_t largest_args = 0; - blt::size_t largest_returns = 0; - blt::u32 largest_argc = 0; - operator_metadata_t meta; - ((meta = add_operator(operators), largest_argc = std::max(meta.argc.argc, largest_argc), - largest_args = std::max(meta.arg_size_bytes, largest_args), largest_returns = std::max(meta.return_size_bytes, - largest_returns)), ...); + friend class gp_program; -// largest = largest * largest_argc; - blt::size_t largest = largest_args * largest_argc * largest_returns * largest_argc; - - storage.eval_func = [&operators..., largest](const tree_t& tree, void* context) -> evaluation_context& { - const auto& ops = tree.get_operations(); - const auto& vals = tree.get_values(); - - static thread_local evaluation_context results{}; - results.values.reset(); - results.values.reserve(largest); - - blt::size_t total_so_far = 0; - blt::size_t op_pos = 0; - - for (const auto& operation : blt::reverse_iterate(ops.begin(), ops.end())) + friend class blt::gp::detail::operator_storage_test; + + public: + explicit operator_builder() = default; + + template + program_operator_storage_t& build(Operators&... operators) + { + blt::size_t largest_args = 0; + blt::size_t largest_returns = 0; + blt::u32 largest_argc = 0; + operator_metadata_t meta; + ((meta = add_operator(operators), largest_argc = std::max(meta.argc.argc, largest_argc), + largest_args = std::max(meta.arg_size_bytes, largest_args), largest_returns = std::max(meta.return_size_bytes, + largest_returns)), ...); + + // largest = largest * largest_argc; + blt::size_t largest = largest_args * largest_argc * largest_returns * largest_argc; + + storage.eval_func = [&operators..., largest](const tree_t& tree, void* context) -> evaluation_context& { + const auto& ops = tree.get_operations(); + const auto& vals = tree.get_values(); + + static thread_local evaluation_context results{}; + results.values.reset(); + results.values.reserve(largest); + + blt::size_t total_so_far = 0; + blt::size_t op_pos = 0; + + for (const auto& operation : blt::reverse_iterate(ops.begin(), ops.end())) + { + op_pos++; + if (operation.is_value) { - op_pos++; - if (operation.is_value) - { - total_so_far += stack_allocator::aligned_size(operation.type_size); - results.values.copy_from(vals.from(total_so_far), stack_allocator::aligned_size(operation.type_size)); - continue; - } - call_jmp_table(operation.id, context, results.values, results.values, operators...); - } - - return results; - }; - - blt::hashset_t has_terminals; - - for (const auto& v : blt::enumerate(storage.terminals)) - { - if (!v.second.empty()) - has_terminals.insert(v.first); - } - - for (const auto& op_r : blt::enumerate(storage.non_terminals)) - { - if (op_r.second.empty()) + total_so_far += stack_allocator::aligned_size(operation.type_size); + results.values.copy_from(vals.from(total_so_far), stack_allocator::aligned_size(operation.type_size)); continue; - auto return_type = op_r.first; - tracked_vector> ordered_terminals; - for (const auto& op : op_r.second) - { - // count number of terminals - blt::size_t terminals = 0; - for (const auto& type : storage.operators[op].argument_types) - { - if (has_terminals.contains(type)) - terminals++; - } - ordered_terminals.emplace_back(op, terminals); } - bool found_terminal_inputs = false; - bool matches_argc = false; - for (const auto& terms : ordered_terminals) - { - if (terms.second == storage.operators[terms.first].argc.argc) - matches_argc = true; - if (terms.second != 0) - found_terminal_inputs = true; - if (matches_argc && found_terminal_inputs) - break; - } - if (!found_terminal_inputs) - BLT_ABORT(("Failed to find function with terminal arguments for return type " + std::to_string(return_type)).c_str()); - if (!matches_argc) - { - BLT_ABORT(("Failed to find a function which purely translates types " - "(that is all input types are terminals) for return type " + std::to_string(return_type)).c_str()); - } - - std::sort(ordered_terminals.begin(), ordered_terminals.end(), [](const auto& a, const auto& b) { - return a.second > b.second; - }); - - auto first_size = *ordered_terminals.begin(); - auto iter = ordered_terminals.begin(); - while (++iter != ordered_terminals.end() && iter->second == first_size.second) - {} - - ordered_terminals.erase(iter, ordered_terminals.end()); - - storage.operators_ordered_terminals[return_type] = ordered_terminals; + call_jmp_table(operation.id, context, results.values, results.values, operators...); } - - return storage; - } - - program_operator_storage_t&& grab() + + return results; + }; + + blt::hashset_t has_terminals; + + for (const auto& v : blt::enumerate(storage.terminals)) { - return std::move(storage); + if (!v.second.empty()) + has_terminals.insert(v.first); } - - private: - template - auto add_operator(operation_t& op) + + for (const auto& op_r : blt::enumerate(storage.non_terminals)) { - // check for types we can register - (storage.system.register_type(), ...); - storage.system.register_type(); - - auto return_type_id = storage.system.get_type().id(); - auto operator_id = blt::gp::operator_id(storage.operators.size()); - op.id = operator_id; - - operator_info_t info; - - if constexpr (sizeof...(Args) > 0) + if (op_r.second.empty()) + continue; + auto return_type = op_r.first; + tracked_vector> ordered_terminals; + for (const auto& op : op_r.second) { - (add_non_context_argument>(info.argument_types), ...); - } - - info.argc.argc_context = info.argc.argc = sizeof...(Args); - info.return_type = return_type_id; - info.func = op.template make_callable(); - - ((std::is_same_v, Context> ? info.argc.argc -= 1 : (blt::size_t) nullptr), ...); - - auto& operator_list = info.argc.argc == 0 ? storage.terminals : storage.non_terminals; - operator_list[return_type_id].push_back(operator_id); - - BLT_ASSERT(info.argc.argc_context - info.argc.argc <= 1 && "Cannot pass multiple context as arguments!"); - - storage.operators.push_back(info); - - operator_metadata_t meta; - if constexpr (sizeof...(Args) != 0) - { - meta.arg_size_bytes = (stack_allocator::aligned_size(sizeof(Args)) + ...); - } - meta.return_size_bytes = sizeof(Return); - meta.argc = info.argc; - - storage.operator_metadata.push_back(meta); - storage.print_funcs.push_back([&op](std::ostream& out, stack_allocator& stack) { - if constexpr (blt::meta::is_streamable_v) + // count number of terminals + blt::size_t terminals = 0; + for (const auto& type : storage.operators[op].argument_types) { - out << stack.from(0); - (void) (op); // remove warning - } else - { - out << "[Printing Value on '" << (op.get_name() ? *op.get_name() : "") << "' Not Supported!]"; + if (has_terminals.contains(type)) + terminals++; } + ordered_terminals.emplace_back(op, terminals); + } + bool found_terminal_inputs = false; + bool matches_argc = false; + for (const auto& terms : ordered_terminals) + { + if (terms.second == storage.operators[terms.first].argc.argc) + matches_argc = true; + if (terms.second != 0) + found_terminal_inputs = true; + if (matches_argc && found_terminal_inputs) + break; + } + if (!found_terminal_inputs) + BLT_ABORT(("Failed to find function with terminal arguments for return type " + std::to_string(return_type)).c_str()); + if (!matches_argc) + { + BLT_ABORT(("Failed to find a function which purely translates types " + "(that is all input types are terminals) for return type " + std::to_string(return_type)).c_str()); + } + + std::sort(ordered_terminals.begin(), ordered_terminals.end(), [](const auto& a, const auto& b) + { + return a.second > b.second; }); - storage.destroy_funcs.push_back([](detail::destroy_t type, stack_allocator& alloc) { - switch (type) + + auto first_size = *ordered_terminals.begin(); + auto iter = ordered_terminals.begin(); + while (++iter != ordered_terminals.end() && iter->second == first_size.second) + { + } + + ordered_terminals.erase(iter, ordered_terminals.end()); + + storage.operators_ordered_terminals[return_type] = ordered_terminals; + } + + return storage; + } + + program_operator_storage_t&& grab() + { + return std::move(storage); + } + + private: + template + auto add_operator(operation_t& op) + { + // check for types we can register + (storage.system.register_type(), ...); + storage.system.register_type(); + + auto return_type_id = storage.system.get_type().id(); + auto operator_id = blt::gp::operator_id(storage.operators.size()); + op.id = operator_id; + + operator_info_t info; + + if constexpr (sizeof...(Args) > 0) + { + (add_non_context_argument>(info.argument_types), ...); + } + + info.argc.argc_context = info.argc.argc = sizeof...(Args); + info.return_type = return_type_id; + info.func = op.template make_callable(); + + ((std::is_same_v, Context> ? info.argc.argc -= 1 : (blt::size_t)nullptr), ...); + + auto& operator_list = info.argc.argc == 0 ? storage.terminals : storage.non_terminals; + operator_list[return_type_id].push_back(operator_id); + + BLT_ASSERT(info.argc.argc_context - info.argc.argc <= 1 && "Cannot pass multiple context as arguments!"); + + storage.operators.push_back(info); + + operator_metadata_t meta; + if constexpr (sizeof...(Args) != 0) + { + meta.arg_size_bytes = (stack_allocator::aligned_size(sizeof(Args)) + ...); + } + meta.return_size_bytes = sizeof(Return); + meta.argc = info.argc; + + storage.operator_metadata.push_back(meta); + storage.print_funcs.push_back([&op](std::ostream& out, stack_allocator& stack) + { + if constexpr (blt::meta::is_streamable_v) + { + out << stack.from(0); + (void)(op); // remove warning + } + else + { + out << "[Printing Value on '" << (op.get_name() ? *op.get_name() : "") << "' Not Supported!]"; + } + }); + storage.destroy_funcs.push_back([](detail::destroy_t type, stack_allocator& alloc) + { + switch (type) + { + case detail::destroy_t::ARGS: + alloc.call_destructors(); + break; + case detail::destroy_t::RETURN: + if constexpr (detail::has_func_drop_v>) { - case detail::destroy_t::ARGS: - alloc.call_destructors(); - break; - case detail::destroy_t::RETURN: - if constexpr (detail::has_func_drop_v>) - { - alloc.from>(0).drop(); - } - break; + alloc.from>(0).drop(); } - }); - storage.names.push_back(op.get_name()); - if (op.is_ephemeral()) - storage.ephemeral_leaf_operators.insert(operator_id); - return meta; - } - - template - void add_non_context_argument(decltype(operator_info_t::argument_types)& types) - { - if constexpr (!std::is_same_v>) - { - types.push_back(storage.system.get_type().id()); + break; } - } - - template - static inline void execute(void* context, stack_allocator& write_stack, stack_allocator& read_stack, Operator& operation) + }); + storage.names.push_back(op.get_name()); + if (op.is_ephemeral()) + storage.ephemeral_leaf_operators.insert(operator_id); + return meta; + } + + template + void add_non_context_argument(decltype(operator_info_t::argument_types)& types) + { + if constexpr (!std::is_same_v>) { - if constexpr (std::is_same_v, Context>) - { - write_stack.push(operation(context, read_stack)); - } else - { - write_stack.push(operation(read_stack)); - } + types.push_back(storage.system.get_type().id()); } - - template - static inline bool call(blt::size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, Operator& operation) + } + + template + static void execute(void* context, stack_allocator& write_stack, stack_allocator& read_stack, Operator& operation) + { + if constexpr (std::is_same_v, Context>) { - if (id == op) - { - execute(context, write_stack, read_stack, operation); - return false; - } - return true; + write_stack.push(operation(context, read_stack)); } - - template - static inline void call_jmp_table_internal(size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, - std::integer_sequence, Operators& ... operators) + else { - if (op >= sizeof...(operator_ids)) - { - BLT_UNREACHABLE; - } - (call(op, context, write_stack, read_stack, operators) && ...); + write_stack.push(operation(read_stack)); } - - template - static inline void call_jmp_table(size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, - Operators& ... operators) + } + + template + static bool call(blt::size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, Operator& operation) + { + if (id == op) { - call_jmp_table_internal(op, context, write_stack, read_stack, std::index_sequence_for(), operators...); + execute(context, write_stack, read_stack, operation); + return false; } - - program_operator_storage_t storage; + return true; + } + + template + static void call_jmp_table_internal(size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, + std::integer_sequence, Operators&... operators) + { + if (op >= sizeof...(operator_ids)) + { + BLT_UNREACHABLE; + } + (call(op, context, write_stack, read_stack, operators) && ...); + } + + template + static void call_jmp_table(size_t op, void* context, stack_allocator& write_stack, stack_allocator& read_stack, + Operators&... operators) + { + call_jmp_table_internal(op, context, write_stack, read_stack, std::index_sequence_for(), operators...); + } + + program_operator_storage_t storage; }; - + class gp_program { - public: - /** - * Note about context size: This is required as context is passed to every operator in the GP tree, this context will be provided by your - * call to one of the evaluator functions. This was the nicest way to provide this as C++ lacks reflection - * - * @param engine random engine to use throughout the program. - * @param context_size number of arguments which are always present as "context" to the GP system / operators - */ - explicit gp_program(blt::u64 seed): seed_func([seed] { return seed; }) - { create_threads(); } - - explicit gp_program(blt::u64 seed, prog_config_t config): seed_func([seed] { return seed; }), config(config) - { create_threads(); } - - explicit gp_program(std::function seed_func): seed_func(std::move(seed_func)) - { create_threads(); } - - explicit gp_program(std::function seed_func, prog_config_t config): seed_func(std::move(seed_func)), config(config) - { create_threads(); } - - ~gp_program() + public: + /** + * Note about context size: This is required as context is passed to every operator in the GP tree, this context will be provided by your + * call to one of the evaluator functions. This was the nicest way to provide this as C++ lacks reflection + * + * @param seed + */ + explicit gp_program(blt::u64 seed): seed_func([seed] { return seed; }) + { + create_threads(); + } + + explicit gp_program(blt::u64 seed, const prog_config_t& config): seed_func([seed] { return seed; }), config(config) + { + create_threads(); + } + + explicit gp_program(std::function seed_func): seed_func(std::move(seed_func)) + { + create_threads(); + } + + explicit gp_program(std::function seed_func, const prog_config_t& config): seed_func(std::move(seed_func)), config(config) + { + create_threads(); + } + + ~gp_program() + { + thread_helper.lifetime_over = true; + thread_helper.barrier.notify_all(); + thread_helper.thread_function_condition.notify_all(); + for (auto& thread : thread_helper.threads) { - thread_helper.lifetime_over = true; - thread_helper.barrier.notify_all(); - thread_helper.thread_function_condition.notify_all(); - for (auto& thread : thread_helper.threads) - { - if (thread->joinable()) - thread->join(); - } + if (thread->joinable()) + thread->join(); } - - void create_next_generation() - { + } + + void create_next_generation() + { #ifdef BLT_TRACK_ALLOCATIONS auto gen_alloc = blt::gp::tracker.start_measurement(); #endif - // should already be empty - thread_helper.next_gen_left.store(config.population_size, std::memory_order_release); - (*thread_execution_service)(0); + // should already be empty + thread_helper.next_gen_left.store(config.population_size, std::memory_order_release); + (*thread_execution_service)(0); #ifdef BLT_TRACK_ALLOCATIONS blt::gp::tracker.stop_measurement(gen_alloc); gen_alloc.pretty_print("Generation"); #endif - } - - void next_generation() - { - std::swap(current_pop, next_pop); - current_generation++; - } - - void evaluate_fitness() - { + } + + void next_generation() + { + std::swap(current_pop, next_pop); + ++current_generation; + } + + void evaluate_fitness() + { #ifdef BLT_TRACK_ALLOCATIONS auto fitness_alloc = blt::gp::tracker.start_measurement(); #endif - evaluate_fitness_internal(); + evaluate_fitness_internal(); #ifdef BLT_TRACK_ALLOCATIONS blt::gp::tracker.stop_measurement(fitness_alloc); fitness_alloc.pretty_print("Fitness"); @@ -415,426 +428,439 @@ namespace blt::gp evaluation_allocations.call(fitness_alloc.getAllocatedByteDifference()); } #endif - - } - - void reset_program(type_id root_type, bool eval_fitness_now = true) + } + + void reset_program(type_id root_type, bool eval_fitness_now = true) + { + current_generation = 0; + current_pop = config.pop_initializer.get().generate( + {*this, root_type, config.population_size, config.initial_min_tree_size, config.initial_max_tree_size}); + next_pop = population_t(current_pop); + BLT_ASSERT_MSG(current_pop.get_individuals().size() == config.population_size, + ("cur pop size: " + std::to_string(current_pop.get_individuals().size())).c_str()); + BLT_ASSERT_MSG(next_pop.get_individuals().size() == config.population_size, + ("next pop size: " + std::to_string(next_pop.get_individuals().size())).c_str()); + if (eval_fitness_now) + evaluate_fitness_internal(); + } + + void kill() + { + thread_helper.lifetime_over = true; + } + + /** + * takes in a reference to a function for the fitness evaluation function (must return a value convertable to double) + * The lambda must accept a tree for evaluation, and an index (current tree) + * + * tree_t& current_tree, blt::size_t index_of_tree + * + * Container must be concurrently accessible from multiple threads using operator[] + * + * NOTE: 0 is considered the best, in terms of standardized fitness + */ + template )> + void generate_population(type_id root_type, FitnessFunc& fitness_function, + Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, + CreationFunc& func = default_next_pop_creator, bool eval_fitness_now = true) + { + using LambdaReturn = typename decltype(blt::meta::lambda_helper(fitness_function))::Return; + current_pop = config.pop_initializer.get().generate( + {*this, root_type, config.population_size, config.initial_min_tree_size, config.initial_max_tree_size}); + next_pop = population_t(current_pop); + BLT_ASSERT_MSG(current_pop.get_individuals().size() == config.population_size, + ("cur pop size: " + std::to_string(current_pop.get_individuals().size())).c_str()); + BLT_ASSERT_MSG(next_pop.get_individuals().size() == config.population_size, + ("next pop size: " + std::to_string(next_pop.get_individuals().size())).c_str()); + if (config.threads == 1) { - current_generation = 0; - current_pop = config.pop_initializer.get().generate( - {*this, root_type, config.population_size, config.initial_min_tree_size, config.initial_max_tree_size}); - next_pop = population_t(current_pop); - BLT_ASSERT_MSG(current_pop.get_individuals().size() == config.population_size, - ("cur pop size: " + std::to_string(current_pop.get_individuals().size())).c_str()); - BLT_ASSERT_MSG(next_pop.get_individuals().size() == config.population_size, - ("next pop size: " + std::to_string(next_pop.get_individuals().size())).c_str()); - if (eval_fitness_now) - evaluate_fitness_internal(); - } - - void kill() - { - thread_helper.lifetime_over = true; - } - - /** - * takes in a reference to a function for the fitness evaluation function (must return a value convertable to double) - * The lambda must accept a tree for evaluation, and an index (current tree) - * - * tree_t& current_tree, blt::size_t index_of_tree - * - * Container must be concurrently accessible from multiple threads using operator[] - * - * NOTE: 0 is considered the best, in terms of standardized fitness - */ - template)> - void generate_population(type_id root_type, FitnessFunc& fitness_function, - Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, - CreationFunc& func = default_next_pop_creator, bool eval_fitness_now = true) - { - using LambdaReturn = typename decltype(blt::meta::lambda_helper(fitness_function))::Return; - current_pop = config.pop_initializer.get().generate( - {*this, root_type, config.population_size, config.initial_min_tree_size, config.initial_max_tree_size}); - next_pop = population_t(current_pop); - BLT_ASSERT_MSG(current_pop.get_individuals().size() == config.population_size, - ("cur pop size: " + std::to_string(current_pop.get_individuals().size())).c_str()); - BLT_ASSERT_MSG(next_pop.get_individuals().size() == config.population_size, - ("next pop size: " + std::to_string(next_pop.get_individuals().size())).c_str()); - if (config.threads == 1) - { - BLT_INFO("Starting with single thread variant!"); - thread_execution_service = std::unique_ptr>(new std::function( - [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection, &func](blt::size_t) { - if (thread_helper.evaluation_left > 0) + BLT_INFO("Starting with single thread variant!"); + thread_execution_service = std::unique_ptr>(new std::function( + [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection, &func](blt::size_t) + { + if (thread_helper.evaluation_left > 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (const auto& [index, ind] : blt::enumerate(current_pop.get_individuals())) + { + if constexpr (std::is_same_v || std::is_convertible_v) { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (const auto& [index, ind] : blt::enumerate(current_pop.get_individuals())) - { - if constexpr (std::is_same_v || std::is_convertible_v) - { - auto result = fitness_function(ind.tree, ind.fitness, index); - if (result) - fitness_should_exit = true; - } else - fitness_function(ind.tree, ind.fitness, index); - - if (ind.fitness.adjusted_fitness > current_stats.best_fitness) - current_stats.best_fitness = ind.fitness.adjusted_fitness; - - if (ind.fitness.adjusted_fitness < current_stats.worst_fitness) - current_stats.worst_fitness = ind.fitness.adjusted_fitness; - - current_stats.overall_fitness = current_stats.overall_fitness + ind.fitness.adjusted_fitness; - } - for (auto& ind : current_pop) - { - auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - thread_helper.evaluation_left = 0; + auto result = fitness_function(ind.tree, ind.fitness, index); + if (result) + fitness_should_exit = true; } - if (thread_helper.next_gen_left > 0) + else + fitness_function(ind.tree, ind.fitness, index); + + if (ind.fitness.adjusted_fitness > current_stats.best_fitness) + current_stats.best_fitness = ind.fitness.adjusted_fitness; + + if (ind.fitness.adjusted_fitness < current_stats.worst_fitness) + current_stats.worst_fitness = ind.fitness.adjusted_fitness; + + current_stats.overall_fitness = current_stats.overall_fitness + ind.fitness.adjusted_fitness; + } + for (auto& ind : current_pop) + { + auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + thread_helper.evaluation_left = 0; + } + if (thread_helper.next_gen_left > 0) + { + auto args = get_selector_args(); + + crossover_selection.pre_process(*this, current_pop); + mutation_selection.pre_process(*this, current_pop); + reproduction_selection.pre_process(*this, current_pop); + + blt::size_t start = perform_elitism(args, next_pop); + + while (start < config.population_size) + { + tree_t& c1 = next_pop.get_individuals()[start].tree; + tree_t* c2 = nullptr; + if (start + 1 < config.population_size) + c2 = &next_pop.get_individuals()[start + 1].tree; + start += func(args, crossover_selection, mutation_selection, reproduction_selection, c1, c2, fitness_function); + } + + thread_helper.next_gen_left = 0; + } + })); + } + else + { + BLT_INFO("Starting thread execution service!"); + std::scoped_lock lock(thread_helper.thread_function_control); + thread_execution_service = std::unique_ptr>(new std::function( + [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection, &func](blt::size_t id) + { + thread_helper.barrier.wait(); + if (thread_helper.evaluation_left > 0) + { + while (thread_helper.evaluation_left > 0) + { + blt::size_t size = 0; + blt::size_t begin = 0; + blt::size_t end = thread_helper.evaluation_left.load(std::memory_order_relaxed); + do { - auto args = get_selector_args(); - - crossover_selection.pre_process(*this, current_pop); + size = std::min(end, config.evaluation_size); + begin = end - size; + } + while (!thread_helper.evaluation_left.compare_exchange_weak(end, end - size, + std::memory_order::memory_order_relaxed, + std::memory_order::memory_order_relaxed)); + for (blt::size_t i = begin; i < end; i++) + { + auto& ind = current_pop.get_individuals()[i]; + + + if constexpr (std::is_same_v || std::is_convertible_v) + { + auto result = fitness_function(ind.tree, ind.fitness, i); + if (result) + fitness_should_exit = true; + } + else + { + fitness_function(ind.tree, ind.fitness, i); + } + + auto old_best = current_stats.best_fitness.load(std::memory_order_relaxed); + while (ind.fitness.adjusted_fitness > old_best && + !current_stats.best_fitness.compare_exchange_weak(old_best, ind.fitness.adjusted_fitness, + std::memory_order_relaxed, + std::memory_order_relaxed)) + { + } + + auto old_worst = current_stats.worst_fitness.load(std::memory_order_relaxed); + while (ind.fitness.adjusted_fitness < old_worst && + !current_stats.worst_fitness.compare_exchange_weak(old_worst, ind.fitness.adjusted_fitness, + std::memory_order_relaxed, + std::memory_order_relaxed)) + { + } + + auto old_overall = current_stats.overall_fitness.load(std::memory_order_relaxed); + while (!current_stats.overall_fitness.compare_exchange_weak(old_overall, + ind.fitness.adjusted_fitness + old_overall, + std::memory_order_relaxed, + std::memory_order_relaxed)) + { + } + } + } + } + if (thread_helper.next_gen_left > 0) + { + auto args = get_selector_args(); + if (id == 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (auto& ind : current_pop) + { + auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + + crossover_selection.pre_process(*this, current_pop); + if (&crossover_selection != &mutation_selection) mutation_selection.pre_process(*this, current_pop); + if (&crossover_selection != &reproduction_selection) reproduction_selection.pre_process(*this, current_pop); - - blt::size_t start = perform_elitism(args, next_pop); - - while (start < config.population_size) - { - tree_t& c1 = next_pop.get_individuals()[start].tree; - tree_t* c2 = nullptr; - if (start + 1 < config.population_size) - c2 = &next_pop.get_individuals()[start + 1].tree; - start += func(args, crossover_selection, mutation_selection, reproduction_selection, c1, c2); - } - - thread_helper.next_gen_left = 0; - } - })); - } else - { - BLT_INFO("Starting thread execution service!"); - std::scoped_lock lock(thread_helper.thread_function_control); - thread_execution_service = std::unique_ptr>(new std::function( - [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection, &func](blt::size_t id) { - thread_helper.barrier.wait(); - if (thread_helper.evaluation_left > 0) + auto elite_amount = perform_elitism(args, next_pop); + thread_helper.next_gen_left -= elite_amount; + } + thread_helper.barrier.wait(); + + while (thread_helper.next_gen_left > 0) + { + blt::size_t size = 0; + blt::size_t begin = 0; + blt::size_t end = thread_helper.next_gen_left.load(std::memory_order_relaxed); + do { - while (thread_helper.evaluation_left > 0) - { - blt::size_t size = 0; - blt::size_t begin = 0; - blt::size_t end = thread_helper.evaluation_left.load(std::memory_order_relaxed); - do - { - size = std::min(end, config.evaluation_size); - begin = end - size; - } while (!thread_helper.evaluation_left.compare_exchange_weak(end, end - size, - std::memory_order::memory_order_relaxed, - std::memory_order::memory_order_relaxed)); - for (blt::size_t i = begin; i < end; i++) - { - auto& ind = current_pop.get_individuals()[i]; - - - if constexpr (std::is_same_v || std::is_convertible_v) - { - auto result = fitness_function(ind.tree, ind.fitness, i); - if (result) - fitness_should_exit = true; - } else - { - fitness_function(ind.tree, ind.fitness, i); - } - - auto old_best = current_stats.best_fitness.load(std::memory_order_relaxed); - while (ind.fitness.adjusted_fitness > old_best && - !current_stats.best_fitness.compare_exchange_weak(old_best, ind.fitness.adjusted_fitness, - std::memory_order_relaxed, - std::memory_order_relaxed)); - - auto old_worst = current_stats.worst_fitness.load(std::memory_order_relaxed); - while (ind.fitness.adjusted_fitness < old_worst && - !current_stats.worst_fitness.compare_exchange_weak(old_worst, ind.fitness.adjusted_fitness, - std::memory_order_relaxed, - std::memory_order_relaxed)); - - auto old_overall = current_stats.overall_fitness.load(std::memory_order_relaxed); - while (!current_stats.overall_fitness.compare_exchange_weak(old_overall, - ind.fitness.adjusted_fitness + old_overall, - std::memory_order_relaxed, - std::memory_order_relaxed)); - } - } + size = std::min(end, config.evaluation_size); + begin = end - size; } - if (thread_helper.next_gen_left > 0) + while (!thread_helper.next_gen_left.compare_exchange_weak(end, end - size, + std::memory_order::memory_order_relaxed, + std::memory_order::memory_order_relaxed)); + + while (begin != end) { - auto args = get_selector_args(); - if (id == 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (auto& ind : current_pop) - { - auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - - crossover_selection.pre_process(*this, current_pop); - if (&crossover_selection != &mutation_selection) - mutation_selection.pre_process(*this, current_pop); - if (&crossover_selection != &reproduction_selection) - reproduction_selection.pre_process(*this, current_pop); - auto elite_amount = perform_elitism(args, next_pop); - thread_helper.next_gen_left -= elite_amount; - } - thread_helper.barrier.wait(); - - while (thread_helper.next_gen_left > 0) - { - blt::size_t size = 0; - blt::size_t begin = 0; - blt::size_t end = thread_helper.next_gen_left.load(std::memory_order_relaxed); - do - { - size = std::min(end, config.evaluation_size); - begin = end - size; - } while (!thread_helper.next_gen_left.compare_exchange_weak(end, end - size, - std::memory_order::memory_order_relaxed, - std::memory_order::memory_order_relaxed)); - - while (begin != end) - { - auto index = config.elites + begin; - tree_t& c1 = next_pop.get_individuals()[index].tree; - tree_t* c2 = nullptr; - if (begin + 1 < end) - c2 = &next_pop.get_individuals()[index + 1].tree; - begin += func(args, crossover_selection, mutation_selection, reproduction_selection, c1, c2); - } - - } + auto index = config.elites + begin; + tree_t& c1 = next_pop.get_individuals()[index].tree; + tree_t* c2 = nullptr; + if (begin + 1 < end) + c2 = &next_pop.get_individuals()[index + 1].tree; + begin += func(args, crossover_selection, mutation_selection, reproduction_selection, c1, c2, fitness_function); } - thread_helper.barrier.wait(); - })); - thread_helper.thread_function_condition.notify_all(); - } - if (eval_fitness_now) - evaluate_fitness_internal(); + } + } + thread_helper.barrier.wait(); + })); + thread_helper.thread_function_condition.notify_all(); } - - [[nodiscard]] bool should_terminate() const + if (eval_fitness_now) + evaluate_fitness_internal(); + } + + [[nodiscard]] bool should_terminate() const + { + return current_generation >= config.max_generations || fitness_should_exit; + } + + [[nodiscard]] bool should_thread_terminate() const + { + return thread_helper.lifetime_over; + } + + operator_id select_terminal(type_id id) + { + // we wanted a terminal, but could not find one, so we will select from a function that has a terminal + if (storage.terminals[id].empty()) + return select_non_terminal_too_deep(id); + return get_random().select(storage.terminals[id]); + } + + operator_id select_non_terminal(type_id id) + { + // non-terminal doesn't exist, return a terminal. This is useful for types that are defined only to have a random value, nothing more. + // was considering an std::optional<> but that would complicate the generator code considerably. I'll mark this as a TODO for v2 + if (storage.non_terminals[id].empty()) + return select_terminal(id); + return get_random().select(storage.non_terminals[id]); + } + + operator_id select_non_terminal_too_deep(type_id id) + { + // this should probably be an error. + if (storage.operators_ordered_terminals[id].empty()) + BLT_ABORT("An impossible state has been reached. Please consult the manual. Error 43"); + return get_random().select(storage.operators_ordered_terminals[id]).first; + } + + auto& get_current_pop() + { + return current_pop; + } + + [[nodiscard]] random_t& get_random() const; + + [[nodiscard]] type_provider& get_typesystem() + { + return storage.system; + } + + [[nodiscard]] operator_info_t& get_operator_info(operator_id id) + { + return storage.operators[id]; + } + + [[nodiscard]] detail::print_func_t& get_print_func(operator_id id) + { + return storage.print_funcs[id]; + } + + [[nodiscard]] detail::destroy_func_t& get_destroy_func(operator_id id) + { + return storage.destroy_funcs[id]; + } + + [[nodiscard]] std::optional get_name(operator_id id) + { + return storage.names[id]; + } + + [[nodiscard]] tracked_vector& get_type_terminals(type_id id) + { + return storage.terminals[id]; + } + + [[nodiscard]] tracked_vector& get_type_non_terminals(type_id id) + { + return storage.non_terminals[id]; + } + + [[nodiscard]] detail::eval_func_t& get_eval_func() + { + return storage.eval_func; + } + + [[nodiscard]] auto get_current_generation() const + { + return current_generation.load(); + } + + [[nodiscard]] const auto& get_population_stats() const + { + return current_stats; + } + + [[nodiscard]] bool is_operator_ephemeral(operator_id id) + { + return storage.ephemeral_leaf_operators.contains(static_cast(id)); + } + + void set_operations(program_operator_storage_t op) + { + storage = std::move(op); + } + + template + std::array get_best_indexes() + { + std::array arr; + + tracked_vector> values; + values.reserve(current_pop.get_individuals().size()); + + for (const auto& ind : blt::enumerate(current_pop.get_individuals())) + values.emplace_back(ind.first, ind.second.fitness.adjusted_fitness); + + std::sort(values.begin(), values.end(), [](const auto& a, const auto& b) + { + return a.second > b.second; + }); + + for (blt::size_t i = 0; i < std::min(size, config.population_size); i++) + arr[i] = values[i].first; + for (blt::size_t i = std::min(size, config.population_size); i < size; i++) + arr[i] = 0; + + return arr; + } + + template + auto get_best_trees() + { + return convert_array, size>>(get_best_indexes(), + [this](auto&& arr, blt::size_t index) -> tree_t& { + return current_pop.get_individuals()[arr[index]].tree; + }, + std::make_integer_sequence()); + } + + template + auto get_best_individuals() + { + return convert_array, size>>(get_best_indexes(), + [this](auto&& arr, blt::size_t index) -> individual_t& { + return current_pop.get_individuals()[arr[index]]; + }, + std::make_integer_sequence()); + } + + private: + selector_args get_selector_args() + { + return {*this, current_pop, current_stats, config, get_random()}; + } + + template + Return convert_array(std::array&& arr, Accessor&& accessor, + std::integer_sequence) + { + return Return{accessor(arr, indexes)...}; + } + + void create_threads(); + + void evaluate_fitness_internal() + { + statistic_history.push_back(current_stats); + current_stats.clear(); + thread_helper.evaluation_left.store(config.population_size, std::memory_order_release); + (*thread_execution_service)(0); + + current_stats.average_fitness = current_stats.overall_fitness / static_cast(config.population_size); + } + + private: + program_operator_storage_t storage; + std::function seed_func; + prog_config_t config{}; + + population_t current_pop; + population_t next_pop; + + std::atomic_uint64_t current_generation = 0; + + std::atomic_bool fitness_should_exit = false; + + population_stats current_stats{}; + tracked_vector statistic_history; + + struct concurrency_storage + { + std::vector> threads; + + std::mutex thread_function_control{}; + std::condition_variable thread_function_condition{}; + + std::atomic_uint64_t evaluation_left = 0; + std::atomic_uint64_t next_gen_left = 0; + + std::atomic_bool lifetime_over = false; + blt::barrier barrier; + + explicit concurrency_storage(blt::size_t threads): barrier(threads, lifetime_over) { - return current_generation >= config.max_generations || fitness_should_exit; } - - [[nodiscard]] bool should_thread_terminate() const - { - return thread_helper.lifetime_over; - } - - inline operator_id select_terminal(type_id id) - { - // we wanted a terminal, but could not find one, so we will select from a function that has a terminal - if (storage.terminals[id].empty()) - return select_non_terminal_too_deep(id); - return get_random().select(storage.terminals[id]); - } - - inline operator_id select_non_terminal(type_id id) - { - // non-terminal doesn't exist, return a terminal. This is useful for types that are defined only to have a random value, nothing more. - // was considering an std::optional<> but that would complicate the generator code considerably. I'll mark this as a TODO for v2 - if (storage.non_terminals[id].empty()) - return select_terminal(id); - return get_random().select(storage.non_terminals[id]); - } - - inline operator_id select_non_terminal_too_deep(type_id id) - { - // this should probably be an error. - if (storage.operators_ordered_terminals[id].empty()) - BLT_ABORT("An impossible state has been reached. Please consult the manual. Error 43"); - return get_random().select(storage.operators_ordered_terminals[id]).first; - } - - inline auto& get_current_pop() - { - return current_pop; - } - - [[nodiscard]] random_t& get_random() const; - - [[nodiscard]] inline type_provider& get_typesystem() - { - return storage.system; - } - - [[nodiscard]] inline operator_info_t& get_operator_info(operator_id id) - { - return storage.operators[id]; - } - - [[nodiscard]] inline detail::print_func_t& get_print_func(operator_id id) - { - return storage.print_funcs[id]; - } - - [[nodiscard]] inline detail::destroy_func_t& get_destroy_func(operator_id id) - { - return storage.destroy_funcs[id]; - } - - [[nodiscard]] inline std::optional get_name(operator_id id) - { - return storage.names[id]; - } - - [[nodiscard]] inline tracked_vector& get_type_terminals(type_id id) - { - return storage.terminals[id]; - } - - [[nodiscard]] inline tracked_vector& get_type_non_terminals(type_id id) - { - return storage.non_terminals[id]; - } - - [[nodiscard]] inline detail::eval_func_t& get_eval_func() - { - return storage.eval_func; - } - - [[nodiscard]] inline auto get_current_generation() const - { - return current_generation.load(); - } - - [[nodiscard]] inline const auto& get_population_stats() const - { - return current_stats; - } - - [[nodiscard]] inline bool is_operator_ephemeral(operator_id id) - { - return storage.ephemeral_leaf_operators.contains(static_cast(id)); - } - - inline void set_operations(program_operator_storage_t op) - { - storage = std::move(op); - } - - template - std::array get_best_indexes() - { - std::array arr; - - tracked_vector> values; - values.reserve(current_pop.get_individuals().size()); - - for (const auto& ind : blt::enumerate(current_pop.get_individuals())) - values.emplace_back(ind.first, ind.second.fitness.adjusted_fitness); - - std::sort(values.begin(), values.end(), [](const auto& a, const auto& b) { - return a.second > b.second; - }); - - for (blt::size_t i = 0; i < std::min(size, config.population_size); i++) - arr[i] = values[i].first; - for (blt::size_t i = std::min(size, config.population_size); i < size; i++) - arr[i] = 0; - - return arr; - } - - template - auto get_best_trees() - { - return convert_array, size>>(get_best_indexes(), - [this](auto&& arr, blt::size_t index) -> tree_t& { - return current_pop.get_individuals()[arr[index]].tree; - }, - std::make_integer_sequence()); - } - - template - auto get_best_individuals() - { - return convert_array, size>>(get_best_indexes(), - [this](auto&& arr, blt::size_t index) -> individual_t& { - return current_pop.get_individuals()[arr[index]]; - }, - std::make_integer_sequence()); - } - - private: - inline selector_args get_selector_args() - { - return {*this, current_pop, current_stats, config, get_random()}; - } - - template - inline Return convert_array(std::array&& arr, Accessor&& accessor, - std::integer_sequence) - { - return Return{accessor(arr, indexes)...}; - } - - void create_threads(); - - void evaluate_fitness_internal() - { - statistic_history.push_back(current_stats); - current_stats.clear(); - thread_helper.evaluation_left.store(config.population_size, std::memory_order_release); - (*thread_execution_service)(0); - - current_stats.average_fitness = current_stats.overall_fitness / static_cast(config.population_size); - } - - private: - program_operator_storage_t storage; - std::function seed_func; - prog_config_t config{}; - - population_t current_pop; - population_t next_pop; - - std::atomic_uint64_t current_generation = 0; - - std::atomic_bool fitness_should_exit = false; - - population_stats current_stats{}; - tracked_vector statistic_history; - - struct concurrency_storage - { - std::vector> threads; - - std::mutex thread_function_control{}; - std::condition_variable thread_function_condition{}; - - std::atomic_uint64_t evaluation_left = 0; - std::atomic_uint64_t next_gen_left = 0; - - std::atomic_bool lifetime_over = false; - blt::barrier barrier; - - explicit concurrency_storage(blt::size_t threads): barrier(threads, lifetime_over) - {} - } thread_helper{config.threads == 0 ? std::thread::hardware_concurrency() : config.threads}; - - std::unique_ptr> thread_execution_service = nullptr; + } thread_helper{config.threads == 0 ? std::thread::hardware_concurrency() : config.threads}; + + std::unique_ptr> thread_execution_service = nullptr; }; - } #endif //BLT_GP_PROGRAM_H diff --git a/include/blt/gp/selection.h b/include/blt/gp/selection.h index b6e4272..e096762 100644 --- a/include/blt/gp/selection.h +++ b/include/blt/gp/selection.h @@ -25,10 +25,10 @@ #include #include #include "blt/format/format.h" +#include namespace blt::gp { - struct selector_args { gp_program& program; @@ -37,18 +37,19 @@ namespace blt::gp prog_config_t& config; random_t& random; }; - - constexpr inline auto perform_elitism = [](const selector_args& args, population_t& next_pop) { + + constexpr inline auto perform_elitism = [](const selector_args& args, population_t& next_pop) + { auto& [program, current_pop, current_stats, config, random] = args; - + if (config.elites > 0 && current_pop.get_individuals().size() >= config.elites) { static thread_local tracked_vector> values; values.clear(); - + for (blt::size_t i = 0; i < config.elites; i++) values.emplace_back(i, current_pop.get_individuals()[i].fitness.adjusted_fitness); - + for (const auto& ind : blt::enumerate(current_pop.get_individuals())) { for (blt::size_t i = 0; i < config.elites; i++) @@ -67,165 +68,218 @@ namespace blt::gp } } } - + for (blt::size_t i = 0; i < config.elites; i++) next_pop.get_individuals()[i].copy_fast(current_pop.get_individuals()[values[i].first].tree); return config.elites; } return 0ul; }; - - template + + inline std::atomic parent_fitness = 0; + inline std::atomic child_fitness = 0; + + template constexpr inline auto default_next_pop_creator = []( - blt::gp::selector_args& args, Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, - tree_t& c1, tree_t* c2) { + selector_args& args, Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, + tree_t& c1, tree_t* c2, const std::function& test_fitness_func) + { auto& [program, current_pop, current_stats, config, random] = args; - - int sel = random.get_i32(0, 3); - switch (sel) + + switch (random.get_i32(0, 3)) { - case 0: - if (c2 == nullptr) - return 0; - // everyone gets a chance once per loop. - if (random.choice(config.crossover_chance)) + case 0: + if (c2 == nullptr) + return 0; + // everyone gets a chance once per loop. + if (random.choice(config.crossover_chance)) + { +#ifdef BLT_TRACK_ALLOCATIONS + auto state = tracker.start_measurement_thread_local(); +#endif + // crossover + + const tree_t* p1; + const tree_t* p2; + double parent_val = 0; + do { -#ifdef BLT_TRACK_ALLOCATIONS - auto state = tracker.start_measurement_thread_local(); -#endif - // crossover - const tree_t* p1; - const tree_t* p2; - do - { - p1 = &crossover_selection.select(program, current_pop); - p2 = &crossover_selection.select(program, current_pop); - c1.copy_fast(*p1); - c2->copy_fast(*p2); - } while (!config.crossover.get().apply(program, *p1, *p2, c1, *c2)); -#ifdef BLT_TRACK_ALLOCATIONS - tracker.stop_measurement_thread_local(state); - crossover_calls.call(); - crossover_calls.set_value(std::max(crossover_calls.get_value(), state.getAllocatedByteDifference())); - if (state.getAllocatedByteDifference() != 0) - { - crossover_allocations.call(state.getAllocatedByteDifference()); - } -#endif - return 2; + p1 = &crossover_selection.select(program, current_pop); + p2 = &crossover_selection.select(program, current_pop); + + fitness_t fitness1; + fitness_t fitness2; + test_fitness_func(*p1, fitness1, 0); + test_fitness_func(*p2, fitness2, 0); + parent_val = fitness1.adjusted_fitness + fitness2.adjusted_fitness; + // BLT_TRACE("%ld> P1 Fit: %lf, P2 Fit: %lf", val, fitness1.adjusted_fitness, fitness2.adjusted_fitness); + + c1.copy_fast(*p1); + c2->copy_fast(*p2); + + crossover_calls.value(1); } - break; - case 1: - if (random.choice(config.mutation_chance)) + while (!config.crossover.get().apply(program, *p1, *p2, c1, *c2)); + fitness_t fitness1; + fitness_t fitness2; + test_fitness_func(c1, fitness1, 0); + test_fitness_func(*c2, fitness2, 0); + + const auto child_val = fitness1.adjusted_fitness + fitness2.adjusted_fitness; + + auto old_parent_val = parent_fitness.load(std::memory_order_relaxed); + while (!parent_fitness.compare_exchange_weak(old_parent_val, old_parent_val + parent_val, std::memory_order_relaxed, + std::memory_order_relaxed)) { -#ifdef BLT_TRACK_ALLOCATIONS - auto state = tracker.start_measurement_thread_local(); -#endif - // mutation - const tree_t* p; - do - { - p = &mutation_selection.select(program, current_pop); - c1.copy_fast(*p); - } while (!config.mutator.get().apply(program, *p, c1)); -#ifdef BLT_TRACK_ALLOCATIONS - tracker.stop_measurement_thread_local(state); - mutation_calls.call(); - mutation_calls.set_value(std::max(mutation_calls.get_value(), state.getAllocatedByteDifference())); - if (state.getAllocationDifference() != 0) - { - mutation_allocations.call(state.getAllocatedByteDifference()); - } -#endif - return 1; } - break; - case 2: - if (config.reproduction_chance > 0 && random.choice(config.reproduction_chance)) + + auto old_child_val = child_fitness.load(std::memory_order_relaxed); + while (!child_fitness.compare_exchange_weak(old_child_val, old_child_val + child_val, std::memory_order_relaxed, + std::memory_order_relaxed)) { -#ifdef BLT_TRACK_ALLOCATIONS - auto state = tracker.start_measurement_thread_local(); -#endif - // reproduction - c1.copy_fast(reproduction_selection.select(program, current_pop)); -#ifdef BLT_TRACK_ALLOCATIONS - tracker.stop_measurement_thread_local(state); - reproduction_calls.call(); - reproduction_calls.set_value(std::max(reproduction_calls.get_value(), state.getAllocatedByteDifference())); - if (state.getAllocationDifference() != 0) - { - reproduction_allocations.call(state.getAllocatedByteDifference()); - } -#endif - return 1; } - break; - default: + + // BLT_TRACE("%ld> C1 Fit: %lf, C2 Fit: %lf", val, fitness1.adjusted_fitness, fitness2.adjusted_fitness); +#ifdef BLT_TRACK_ALLOCATIONS + tracker.stop_measurement_thread_local(state); + crossover_calls.call(); + if (state.getAllocatedByteDifference() != 0) + { + crossover_allocations.call(state.getAllocatedByteDifference()); + crossover_allocations.set_value(std::max(crossover_allocations.get_value(), state.getAllocatedByteDifference())); + } +#endif + return 2; + } + break; + case 1: + if (random.choice(config.mutation_chance)) + { +#ifdef BLT_TRACK_ALLOCATIONS + auto state = tracker.start_measurement_thread_local(); +#endif + // mutation + const tree_t* p; + do + { + p = &mutation_selection.select(program, current_pop); + c1.copy_fast(*p); + mutation_calls.value(1); + } + while (!config.mutator.get().apply(program, *p, c1)); +#ifdef BLT_TRACK_ALLOCATIONS + tracker.stop_measurement_thread_local(state); + mutation_calls.call(); + if (state.getAllocationDifference() != 0) + { + mutation_allocations.call(state.getAllocatedByteDifference()); + mutation_allocations.set_value(std::max(mutation_allocations.get_value(), state.getAllocatedByteDifference())); + } +#endif + return 1; + } + break; + case 2: + if (config.reproduction_chance > 0 && random.choice(config.reproduction_chance)) + { +#ifdef BLT_TRACK_ALLOCATIONS + auto state = tracker.start_measurement_thread_local(); +#endif + // reproduction + c1.copy_fast(reproduction_selection.select(program, current_pop)); +#ifdef BLT_TRACK_ALLOCATIONS + tracker.stop_measurement_thread_local(state); + reproduction_calls.call(); + reproduction_calls.value(1); + if (state.getAllocationDifference() != 0) + { + reproduction_allocations.call(state.getAllocatedByteDifference()); + reproduction_allocations.set_value(std::max(reproduction_allocations.get_value(), state.getAllocatedByteDifference())); + } +#endif + return 1; + } + break; + default: #if BLT_DEBUG_LEVEL > 0 BLT_ABORT("This is not possible!"); #else - BLT_UNREACHABLE; + BLT_UNREACHABLE; #endif } return 0; }; - + class selection_t { - public: - /** - * @param program gp program to select with, used in randoms - * @param pop population to select from - * @param stats the populations statistics - * @return - */ - virtual const tree_t& select(gp_program& program, const population_t& pop) = 0; - - virtual void pre_process(gp_program&, population_t&) - {} - - virtual ~selection_t() = default; + public: + /** + * @param program gp program to select with, used in randoms + * @param pop population to select from + * @param stats the populations statistics + * @return + */ + virtual const tree_t& select(gp_program& program, const population_t& pop) = 0; + + /** + * Is run once on a single thread before selection begins. allows you to preprocess the generation for fitness metrics. + * TODO a method for parallel execution + */ + virtual void pre_process(gp_program&, population_t&) + { + } + + virtual ~selection_t() = default; }; - - class select_best_t : public selection_t + + class select_best_t final : public selection_t { - public: - const tree_t& select(gp_program& program, const population_t& pop) final; + public: + void pre_process(gp_program&, population_t&) override; + + const tree_t& select(gp_program& program, const population_t& pop) override; + + private: + std::atomic_uint64_t index = 0; }; - - class select_worst_t : public selection_t + + class select_worst_t final : public selection_t { - public: - const tree_t& select(gp_program& program, const population_t& pop) final; + public: + void pre_process(gp_program&, population_t&) override; + + const tree_t& select(gp_program& program, const population_t& pop) override; + + private: + std::atomic_uint64_t index = 0; }; - - class select_random_t : public selection_t + + class select_random_t final : public selection_t { - public: - const tree_t& select(gp_program& program, const population_t& pop) final; + public: + const tree_t& select(gp_program& program, const population_t& pop) override; }; - - class select_tournament_t : public selection_t + + class select_tournament_t final : public selection_t { - public: - explicit select_tournament_t(blt::size_t selection_size = 3): selection_size(selection_size) - { - if (selection_size == 0) - BLT_ABORT("Unable to select with this size. Must select at least 1 individual_t!"); - } - - const tree_t& select(gp_program& program, const population_t& pop) final; - - private: - const blt::size_t selection_size; + public: + explicit select_tournament_t(const size_t selection_size = 3): selection_size(selection_size) + { + if (selection_size == 0) + BLT_ABORT("Unable to select with this size. Must select at least 1 individual_t!"); + } + + const tree_t& select(gp_program& program, const population_t& pop) override; + + private: + const size_t selection_size; }; - - class select_fitness_proportionate_t : public selection_t + + class select_fitness_proportionate_t final : public selection_t { - public: - const tree_t& select(gp_program& program, const population_t& pop) final; + public: + const tree_t& select(gp_program& program, const population_t& pop) override; }; - } #endif //BLT_GP_SELECTION_H diff --git a/include/blt/gp/stats.h b/include/blt/gp/stats.h index 5d87e02..ae0efd6 100644 --- a/include/blt/gp/stats.h +++ b/include/blt/gp/stats.h @@ -316,14 +316,15 @@ namespace blt::gp return secondary_value.load(); } - call_data_t start_measurement() + call_data_t start_measurement() const { - return {primary_calls.load(), 0}; + return {primary_calls.load(), secondary_value.load()}; } - void stop_measurement(call_data_t& data) + void stop_measurement(call_data_t& data) const { data.end_calls = primary_calls.load(); + data.end_value = secondary_value.load(); } private: diff --git a/include/blt/gp/tree.h b/include/blt/gp/tree.h index e35ebd4..991217c 100644 --- a/include/blt/gp/tree.h +++ b/include/blt/gp/tree.h @@ -133,7 +133,7 @@ namespace blt::gp * Helper template for returning the result of the last evaluation */ template - T get_evaluation_value(evaluation_context& context) + T get_evaluation_value(evaluation_context& context) const { return context.values.pop(); } @@ -142,7 +142,7 @@ namespace blt::gp * Helper template for returning the result of the last evaluation */ template - T& get_evaluation_ref(evaluation_context& context) + T& get_evaluation_ref(evaluation_context& context) const { return context.values.from(0); } @@ -151,13 +151,13 @@ namespace blt::gp * Helper template for returning the result of evaluation (this calls it) */ template - T get_evaluation_value(const Context& context) + T get_evaluation_value(const Context& context) const { return evaluate(context).values.template pop(); } template - T get_evaluation_value() + T get_evaluation_value() const { return evaluate().values.pop(); } diff --git a/src/selection.cpp b/src/selection.cpp index a81983c..7c39cdd 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -20,57 +20,63 @@ namespace blt::gp { - + void select_best_t::pre_process(gp_program&, population_t& pop) + { + std::sort(pop.begin(), pop.end(), [](const auto& a, const auto& b) + { + return a.fitness.adjusted_fitness > b.fitness.adjusted_fitness; + }); + index = 0; + } + const tree_t& select_best_t::select(gp_program&, const population_t& pop) { - auto& first = pop.get_individuals()[0]; - double best_fitness = first.fitness.adjusted_fitness; - const tree_t* tree = &first.tree; - for (auto& ind : pop.get_individuals()) - { - if (ind.fitness.adjusted_fitness > best_fitness) - { - best_fitness = ind.fitness.adjusted_fitness; - tree = &ind.tree; - } - } - return *tree; + const auto size = pop.get_individuals().size(); + return pop.get_individuals()[index.fetch_add(1, std::memory_order_relaxed) % size].tree; } - + + void select_worst_t::pre_process(gp_program&, population_t& pop) + { + std::sort(pop.begin(), pop.end(), [](const auto& a, const auto& b) + { + return a.fitness.adjusted_fitness < b.fitness.adjusted_fitness; + }); + index = 0; + } + const tree_t& select_worst_t::select(gp_program&, const population_t& pop) { - auto& first = pop.get_individuals()[0]; - double worst_fitness = first.fitness.adjusted_fitness; - const tree_t* tree = &first.tree; - for (auto& ind : pop.get_individuals()) - { - if (ind.fitness.adjusted_fitness < worst_fitness) - { - worst_fitness = ind.fitness.adjusted_fitness; - tree = &ind.tree; - } - } - return *tree; + const auto size = pop.get_individuals().size(); + return pop.get_individuals()[index.fetch_add(1, std::memory_order_relaxed) % size].tree; } - + const tree_t& select_random_t::select(gp_program& program, const population_t& pop) { return pop.get_individuals()[program.get_random().get_size_t(0ul, pop.get_individuals().size())].tree; } - + const tree_t& select_tournament_t::select(gp_program& program, const population_t& pop) { - blt::u64 best = program.get_random().get_u64(0, pop.get_individuals().size()); + thread_local hashset_t already_selected; + already_selected.clear(); auto& i_ref = pop.get_individuals(); - for (blt::size_t i = 0; i < selection_size; i++) + + u64 best = program.get_random().get_u64(0, pop.get_individuals().size()); + for (size_t i = 0; i < selection_size; i++) { - auto sel_point = program.get_random().get_u64(0ul, pop.get_individuals().size()); + u64 sel_point; + do + { + sel_point = program.get_random().get_u64(0ul, pop.get_individuals().size());; + } + while (already_selected.contains(sel_point)); + already_selected.insert(sel_point); if (i_ref[sel_point].fitness.adjusted_fitness > i_ref[best].fitness.adjusted_fitness) best = sel_point; } return i_ref[best].tree; } - + const tree_t& select_fitness_proportionate_t::select(gp_program& program, const population_t& pop) { auto& stats = program.get_population_stats(); @@ -81,7 +87,8 @@ namespace blt::gp { if (choice <= stats.normalized_fitness[index]) return ref.tree; - } else + } + else { if (choice > stats.normalized_fitness[index - 1] && choice <= stats.normalized_fitness[index]) return ref.tree; @@ -91,4 +98,4 @@ namespace blt::gp return pop.get_individuals()[0].tree; //BLT_ABORT("Unable to find individual"); } -} \ No newline at end of file +}