From dcefa93d29c60d148c98d64970e6a0c726bc4bbd Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 17 Apr 2025 17:45:32 -0400 Subject: [PATCH] serial broken --- .idea/editor.xml | 238 ---- CMakeLists.txt | 2 +- examples/rice_classification.h | 4 +- examples/src/rice_classification.cpp | 4 +- include/blt/gp/program.h | 1852 +++++++++++++------------- include/blt/gp/tree.h | 16 + src/tree.cpp | 35 +- tests/serialization_test.cpp | 37 +- 8 files changed, 1028 insertions(+), 1160 deletions(-) diff --git a/.idea/editor.xml b/.idea/editor.xml index 329a4e3..6df7d16 100644 --- a/.idea/editor.xml +++ b/.idea/editor.xml @@ -1,248 +1,10 @@ - diff --git a/CMakeLists.txt b/CMakeLists.txt index d63341f..3a9ff7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ macro(compile_options target_name) sanitizers(${target_name}) endmacro() -project(blt-gp VERSION 0.5.8) +project(blt-gp VERSION 0.5.9) include(CTest) diff --git a/examples/rice_classification.h b/examples/rice_classification.h index 2b03156..b779e2f 100644 --- a/examples/rice_classification.h +++ b/examples/rice_classification.h @@ -116,8 +116,8 @@ namespace blt::gp::example mutation_sel = &sel; if (reproduction_sel == nullptr) reproduction_sel = &sel; - program.generate_population(program.get_typesystem().get_type().id(), fitness_function_ref, *crossover_sel, *mutation_sel, - *reproduction_sel); + program.generate_initial_population(program.get_typesystem().get_type().id()); + program.setup_generational_evaluation(fitness_function_ref, *crossover_sel, *mutation_sel, *reproduction_sel); } void print_best(const size_t amount = 3) diff --git a/examples/src/rice_classification.cpp b/examples/src/rice_classification.cpp index 8f8b95b..b31325d 100644 --- a/examples/src/rice_classification.cpp +++ b/examples/src/rice_classification.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include #include #include @@ -191,7 +191,7 @@ void blt::gp::example::rice_classification_t::load_rice_data(const std::string_v training_cases.insert(training_cases.end(), c.begin(), c.end()); training_cases.insert(training_cases.end(), o.begin(), o.end()); std::shuffle(training_cases.begin(), training_cases.end(), program.get_random()); - BLT_INFO("Created testing set of size %ld, training set is of size %ld", testing_cases.size(), training_cases.size()); + BLT_INFO("Created testing set of size {}, training set is of size {}", testing_cases.size(), training_cases.size()); } blt::gp::confusion_matrix_t blt::gp::example::rice_classification_t::test_individual(const individual_t& individual) const diff --git a/include/blt/gp/program.h b/include/blt/gp/program.h index fc6bc3c..25132ca 100644 --- a/include/blt/gp/program.h +++ b/include/blt/gp/program.h @@ -56,309 +56,316 @@ namespace blt::gp { - struct argc_t - { - blt::u32 argc = 0; - blt::u32 argc_context = 0; + struct argc_t + { + blt::u32 argc = 0; + blt::u32 argc_context = 0; - [[nodiscard]] bool is_terminal() const - { - return argc == 0; - } - }; + [[nodiscard]] bool is_terminal() const + { + return argc == 0; + } + }; - struct operator_info_t - { - // types of the arguments - tracked_vector argument_types; - // return type of this operator - type_id return_type; - // number of arguments for this operator - argc_t argc; - // per operator function callable (slow) - detail::operator_func_t func; - }; + struct operator_info_t + { + // types of the arguments + tracked_vector argument_types; + // return type of this operator + type_id return_type; + // number of arguments for this operator + argc_t argc; + // 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 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 - expanding_buffer> terminals; - expanding_buffer> non_terminals; - expanding_buffer>> operators_ordered_terminals; - // indexed from OPERATOR ID (operator number) to a bitfield of flags - hashmap_t operator_flags; + struct program_operator_storage_t + { + // indexed from return TYPE ID, returns index of operator + expanding_buffer> terminals; + expanding_buffer> non_terminals; + expanding_buffer>> operators_ordered_terminals; + // indexed from OPERATOR ID (operator number) to a bitfield of flags + hashmap_t operator_flags; - tracked_vector operators; - tracked_vector operator_metadata; - tracked_vector print_funcs; - tracked_vector destroy_funcs; - tracked_vector> names; + tracked_vector operators; + tracked_vector operator_metadata; + tracked_vector print_funcs; + tracked_vector destroy_funcs; + tracked_vector> names; - detail::eval_func_t eval_func; + detail::eval_func_t eval_func; - type_provider system; - }; + type_provider system; + }; - template - class operator_builder - { - friend class gp_program; + template + class operator_builder + { + friend class gp_program; - friend class blt::gp::detail::operator_storage_test; + friend class blt::gp::detail::operator_storage_test; - public: - explicit operator_builder() = default; + 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)), ...); + 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; - size_t largest = largest_args * largest_argc * largest_returns * largest_argc; + // largest = largest * largest_argc; + size_t largest = largest_args * largest_argc * largest_returns * largest_argc; - storage.eval_func = tree_t::make_execution_lambda(largest, operators...); + storage.eval_func = tree_t::make_execution_lambda(largest, operators...); - blt::hashset_t has_terminals; + blt::hashset_t has_terminals; - for (const auto& [index, value] : blt::enumerate(storage.terminals)) - { - if (!value.empty()) - has_terminals.insert(index); - } + for (const auto& [index, value] : blt::enumerate(storage.terminals)) + { + if (!value.empty()) + has_terminals.insert(index); + } - for (const auto& [index, value] : blt::enumerate(storage.non_terminals)) - { - if (value.empty()) - continue; - auto return_type = index; - tracked_vector> ordered_terminals; - for (const auto& op : value) - { - // 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()); - } + for (const auto& [index, value] : blt::enumerate(storage.non_terminals)) + { + if (value.empty()) + continue; + auto return_type = index; + tracked_vector> ordered_terminals; + for (const auto& op : value) + { + // 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; - }); + 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) - {} + 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()); + ordered_terminals.erase(iter, ordered_terminals.end()); - storage.operators_ordered_terminals[return_type] = ordered_terminals; - } + storage.operators_ordered_terminals[return_type] = ordered_terminals; + } - return storage; - } + return storage; + } - program_operator_storage_t&& grab() - { - return std::move(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(); + 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; + 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; + operator_info_t info; - if constexpr (sizeof...(Args) > 0) - { - (add_non_context_argument>(info.argument_types), ...); - } + 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(); + 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 : 0), ...); + ((std::is_same_v, Context> ? info.argc.argc -= 1 : 0), ...); - auto& operator_list = info.argc.argc == 0 ? storage.terminals : storage.non_terminals; - operator_list[return_type_id].push_back(operator_id); + 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!"); + BLT_ASSERT(info.argc.argc_context - info.argc.argc <= 1 && "Cannot pass multiple context as arguments!"); - storage.operators.push_back(info); + storage.operators.push_back(info); - operator_metadata_t meta; - if constexpr (sizeof...(Args) != 0) - { - meta.arg_size_bytes = (stack_allocator::aligned_size() + ...); - } - meta.return_size_bytes = stack_allocator::aligned_size(); - meta.argc = info.argc; + operator_metadata_t meta; + if constexpr (sizeof...(Args) != 0) + { + meta.arg_size_bytes = (stack_allocator::aligned_size() + ...); + } + meta.return_size_bytes = stack_allocator::aligned_size(); + 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([](const detail::destroy_t type, u8* data) { - switch (type) - { - case detail::destroy_t::PTR: - case detail::destroy_t::RETURN: - if constexpr (detail::has_func_drop_v>) - { - reinterpret_cast*>(data)->drop(); - } - break; - } - }); - storage.names.push_back(op.get_name()); - storage.operator_flags.emplace(operator_id, operator_special_flags{op.is_ephemeral(), op.return_has_ephemeral_drop()}); - return meta; - } + 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([](const detail::destroy_t type, u8* data) + { + switch (type) + { + case detail::destroy_t::PTR: + case detail::destroy_t::RETURN: + if constexpr (detail::has_func_drop_v>) + { + reinterpret_cast*>(data)->drop(); + } + break; + } + }); + storage.names.push_back(op.get_name()); + storage.operator_flags.emplace(operator_id, operator_special_flags{op.is_ephemeral(), op.return_has_ephemeral_drop()}); + 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()); - } - } + 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()); + } + } - private: - program_operator_storage_t storage; - }; + private: + 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 seed - */ - explicit gp_program(blt::u64 seed): seed_func([seed] { - return seed; - }) - { - create_threads(); - set_config(config); - } + 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 seed + */ + explicit gp_program(blt::u64 seed): seed_func([seed] + { + return seed; + }) + { + create_threads(); + set_config(config); + } - explicit gp_program(blt::u64 seed, const prog_config_t& config): seed_func([seed] { - return seed; - }) - { - create_threads(); - set_config(config); - } + explicit gp_program(blt::u64 seed, const prog_config_t& config): seed_func([seed] + { + return seed; + }) + { + create_threads(); + set_config(config); + } - /** - * - * @param seed_func Function which provides a new random seed every time it is called. - * This will be used by each thread to initialize a new random number generator - */ - explicit gp_program(std::function seed_func): seed_func(std::move(seed_func)) - { - create_threads(); - set_config(config); - } + /** + * + * @param seed_func Function which provides a new random seed every time it is called. + * This will be used by each thread to initialize a new random number generator + */ + explicit gp_program(std::function seed_func): seed_func(std::move(seed_func)) + { + create_threads(); + set_config(config); + } - explicit gp_program(std::function seed_func, const prog_config_t& config): seed_func(std::move(seed_func)) - { - create_threads(); - set_config(config); - } + explicit gp_program(std::function seed_func, const prog_config_t& config): seed_func(std::move(seed_func)) + { + create_threads(); + set_config(config); + } - ~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) - { - if (thread->joinable()) - thread->join(); - } - } + ~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) + { + if (thread->joinable()) + thread->join(); + } + } - void create_next_generation() - { - #ifdef BLT_TRACK_ALLOCATIONS + 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(selection_probabilities.replacement_amount.value_or(config.population_size), std::memory_order_release); - (*thread_execution_service)(0); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + // should already be empty + thread_helper.next_gen_left.store(selection_probabilities.replacement_amount.value_or(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 - } +#endif + } - void next_generation() - { - std::swap(current_pop, next_pop); - ++current_generation; - } + void next_generation() + { + std::swap(current_pop, next_pop); + ++current_generation; + } - void evaluate_fitness() - { - #ifdef BLT_TRACK_ALLOCATIONS + void evaluate_fitness() + { +#ifdef BLT_TRACK_ALLOCATIONS auto fitness_alloc = blt::gp::tracker.start_measurement(); - #endif - evaluate_fitness_internal(); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + evaluate_fitness_internal(); +#ifdef BLT_TRACK_ALLOCATIONS blt::gp::tracker.stop_measurement(fitness_alloc); fitness_alloc.pretty_print("Fitness"); evaluation_calls.call(); @@ -367,566 +374,597 @@ namespace blt::gp { evaluation_allocations.call(fitness_alloc.getAllocatedByteDifference()); } - #endif - } - - 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; - } - - void generate_initial_population(const type_id root_type) - { - 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()); - } - - /** - * 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: the larger the adjusted fitness, the better. - */ - template - void setup_generational_evaluation(FitnessFunc& fitness_function, Crossover& crossover_selection, Mutation& mutation_selection, - Reproduction& reproduction_selection, bool eval_fitness_now = true) - { - if (config.threads == 1) - { - BLT_INFO("Starting generational with single thread variant!"); - thread_execution_service = std::unique_ptr>(new std::function( - [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection](size_t) { - single_threaded_fitness_eval()(fitness_function); - - if (thread_helper.next_gen_left > 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (const auto& ind : current_pop) - { - const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - - const 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); - - size_t start = detail::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 += perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); - } - - thread_helper.next_gen_left = 0; - } - })); - } else - { - BLT_INFO("Starting generational 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](const size_t id) { - thread_helper.barrier.wait(); - - multi_threaded_fitness_eval()(fitness_function, id); - - if (thread_helper.next_gen_left > 0) - { - thread_helper.barrier.wait(); - auto args = get_selector_args(); - if (id == 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (const auto& ind : current_pop) - { - const 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); - const auto elite_amount = detail::perform_elitism(args, next_pop); - thread_helper.next_gen_left -= elite_amount; - } - thread_helper.barrier.wait(); - - while (thread_helper.next_gen_left > 0) - { - size_t size = 0; - size_t begin = 0; - 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 += perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); - } - } - } - thread_helper.barrier.wait(); - })); - thread_helper.thread_function_condition.notify_all(); - } - if (eval_fitness_now) - evaluate_fitness_internal(); - } - - template - void setup_steady_state_evaluation(FitnessFunc& fitness_function, SelectionStrat& replacement_strategy, size_t replacement_amount, - Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, - const bool eval_fitness_now = true) - { - selection_probabilities.replacement_amount = replacement_amount; - if (config.threads == 1) - { - BLT_INFO("Starting steady state with single thread variant!"); - thread_execution_service = std::unique_ptr>(new std::function( - [this, &fitness_function, &replacement_strategy, &crossover_selection, &mutation_selection, &reproduction_selection](size_t) { - single_threaded_fitness_eval()(fitness_function); - - if (thread_helper.next_gen_left > 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (const auto& ind : current_pop) - { - const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - - next_pop = population_t(current_pop); - - replacement_strategy.pre_process(*this, next_pop); - crossover_selection.pre_process(*this, current_pop); - mutation_selection.pre_process(*this, current_pop); - reproduction_selection.pre_process(*this, current_pop); - - while (thread_helper.next_gen_left > 0) - { - tree_t& c1 = replacement_strategy.select(*this, next_pop); - tree_t* c2 = nullptr; - if (thread_helper.next_gen_left > 1) - while (c2 != &c1) - c2 = &replacement_strategy.select(*this, next_pop); - thread_helper.next_gen_left -= perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, - c2); - } - - thread_helper.next_gen_left = 0; - } - })); - } else - { - BLT_INFO("Starting steady state thread execution service!"); - std::scoped_lock lock(thread_helper.thread_function_control); - thread_execution_service = std::unique_ptr>(new std::function( - [this, &fitness_function, &replacement_strategy, &crossover_selection, &mutation_selection, &reproduction_selection]( - const size_t id) { - thread_helper.barrier.wait(); - - multi_threaded_fitness_eval()(fitness_function, id); - - if (thread_helper.next_gen_left > 0) - { - thread_helper.barrier.wait(); - if (id == 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - for (const auto& ind : current_pop) - { - const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - - current_pop = population_t(next_pop); - - replacement_strategy.pre_process(*this, next_pop); - 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); - } - thread_helper.barrier.wait(); - - while (thread_helper.next_gen_left > 0) - { - size_t size = 0; - size_t end = thread_helper.next_gen_left.load(std::memory_order_relaxed); - do - { - size = std::min(end, config.evaluation_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 (size > 0) - { - tree_t& c1 = replacement_strategy.select(*this, next_pop); - tree_t* c2 = nullptr; - if (thread_helper.next_gen_left > 1) - while (c2 != &c1) - c2 = &replacement_strategy.select(*this, next_pop); - size -= perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); - } - } - } - thread_helper.barrier.wait(); - })); - thread_helper.thread_function_condition.notify_all(); - } - 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(const 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(const 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(const 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]] const prog_config_t& get_config() const - { - return config; - } - - void set_config(const prog_config_t& config) - { - this->config = config; - selection_probabilities.update(this->config); - } - - [[nodiscard]] type_provider& get_typesystem() - { - return storage.system; - } - - [[nodiscard]] operator_info_t& get_operator_info(const operator_id id) - { - return storage.operators[id]; - } - - [[nodiscard]] detail::print_func_t& get_print_func(const operator_id id) - { - return storage.print_funcs[id]; - } - - [[nodiscard]] detail::destroy_func_t& get_destroy_func(const operator_id id) - { - return storage.destroy_funcs[id]; - } - - [[nodiscard]] std::optional get_name(const operator_id id) const - { - return storage.names[id]; - } - - [[nodiscard]] tracked_vector& get_type_terminals(const type_id id) - { - return storage.terminals[id]; - } - - [[nodiscard]] tracked_vector& get_type_non_terminals(const 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(const operator_id id) const - { - return storage.operator_flags.find(static_cast(id))->second.is_ephemeral(); - } - - [[nodiscard]] bool operator_has_ephemeral_drop(const operator_id id) const - { - return storage.operator_flags.find(static_cast(id))->second.has_ephemeral_drop(); - } - - [[nodiscard]] operator_special_flags get_operator_flags(const operator_id id) const - { - return storage.operator_flags.find(static_cast(id))->second; - } - - 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& [index, value] : enumerate(current_pop.get_individuals())) - values.emplace_back(index, value.fitness.adjusted_fitness); - - std::sort(values.begin(), values.end(), [](const auto& a, const auto& b) { - return a.second > b.second; - }); - - for (size_t i = 0; i < std::min(size, config.population_size); ++i) - arr[i] = values[i].first; - for (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, 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, size_t index) -> individual_t& { - return current_pop.get_individuals()[arr[index]]; - }, std::make_integer_sequence()); - } - - void save_generation(fs::writer_t& writer); - - void save_state(fs::writer_t& writer); - - void load_generation(fs::reader_t& reader); - - void load_state(fs::reader_t& reader); - - private: - template - auto single_threaded_fitness_eval() - { - return [this](FitnessFunc& fitness_function) { - if (thread_helper.evaluation_left > 0) - { - current_stats.normalized_fitness.clear(); - double sum_of_prob = 0; - perform_fitness_function(0, current_pop.get_individuals().size(), fitness_function); - for (const auto& ind : current_pop) - { - const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); - current_stats.normalized_fitness.push_back(sum_of_prob + prob); - sum_of_prob += prob; - } - std::sort(current_pop.begin(), current_pop.end(), [](const auto& a, const auto& b) - { - return a.fitness.adjusted_fitness > b.fitness.adjusted_fitness; - }); - thread_helper.evaluation_left = 0; - } - }; - } - - template - auto multi_threaded_fitness_eval() - { - return [this](FitnessFunc& fitness_function, size_t thread_id) { - if (thread_helper.evaluation_left > 0) - { - thread_helper.barrier.wait(); - while (thread_helper.evaluation_left > 0) - { - size_t size = 0; - size_t begin = 0; - 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)); - perform_fitness_function(begin, end, fitness_function); - } - thread_helper.barrier.wait(); - if (thread_id == 0) - { - std::sort(current_pop.begin(), current_pop.end(), [](const auto& a, const auto& b) - { - return a.fitness.adjusted_fitness > b.fitness.adjusted_fitness; - }); - } - thread_helper.barrier.wait(); - } - }; - } - - template - void perform_fitness_function(const size_t begin, const size_t end, FitnessFunction& fitness_function) - { - using LambdaReturn = std::invoke_result_t; - for (size_t i = begin; i < end; i++) - { - auto& ind = current_pop.get_individuals()[i]; - - ind.fitness = {}; - if constexpr (std::is_same_v || std::is_convertible_v) - { - if (fitness_function(ind.tree, ind.fitness, i)) - 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)) - {} - } - } - - template - size_t perform_selection(Crossover& crossover, Mutation& mutation, Reproduction& reproduction, tree_t& c1, tree_t* c2) - { - if (get_random().choice(selection_probabilities.crossover_chance)) - { - auto ptr = c2; - if (ptr == nullptr) - ptr = &tree_t::get_thread_local(*this); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + } + + 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; + } + + void generate_initial_population(const type_id root_type) + { + 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()); + } + + /** + * 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: the larger the adjusted fitness, the better. + */ + template + void setup_generational_evaluation(FitnessFunc& fitness_function, Crossover& crossover_selection, Mutation& mutation_selection, + Reproduction& reproduction_selection, bool eval_fitness_now = true) + { + if (config.threads == 1) + { + BLT_INFO("Starting generational with single thread variant!"); + thread_execution_service = std::unique_ptr>(new std::function( + [this, &fitness_function, &crossover_selection, &mutation_selection, &reproduction_selection](size_t) + { + single_threaded_fitness_eval()(fitness_function); + + if (thread_helper.next_gen_left > 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (const auto& ind : current_pop) + { + const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + + const 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); + + size_t start = detail::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 += perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); + } + + thread_helper.next_gen_left = 0; + } + })); + } + else + { + BLT_INFO("Starting generational 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](const size_t id) + { + thread_helper.barrier.wait(); + + multi_threaded_fitness_eval()(fitness_function, id); + + if (thread_helper.next_gen_left > 0) + { + thread_helper.barrier.wait(); + auto args = get_selector_args(); + if (id == 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (const auto& ind : current_pop) + { + const 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); + const auto elite_amount = detail::perform_elitism(args, next_pop); + thread_helper.next_gen_left -= elite_amount; + } + thread_helper.barrier.wait(); + + while (thread_helper.next_gen_left > 0) + { + size_t size = 0; + size_t begin = 0; + 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 += perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); + } + } + } + thread_helper.barrier.wait(); + })); + thread_helper.thread_function_condition.notify_all(); + } + if (eval_fitness_now) + { + BLT_ASSERT( + !current_pop.get_individuals().empty() && current_pop.get_individuals().size() == config.population_size && + "Attempted to evaluate fitness but population was empty. Did you forget to call generate_initial_population()?" && + "You can also pass false to the function to prevent immediate fitness evaluation"); + evaluate_fitness_internal(); + } + } + + template + void setup_steady_state_evaluation(FitnessFunc& fitness_function, SelectionStrat& replacement_strategy, size_t replacement_amount, + Crossover& crossover_selection, Mutation& mutation_selection, Reproduction& reproduction_selection, + const bool eval_fitness_now = true) + { + selection_probabilities.replacement_amount = replacement_amount; + if (config.threads == 1) + { + BLT_INFO("Starting steady state with single thread variant!"); + thread_execution_service = std::unique_ptr>(new std::function( + [this, &fitness_function, &replacement_strategy, &crossover_selection, &mutation_selection, &reproduction_selection](size_t) + { + single_threaded_fitness_eval()(fitness_function); + + if (thread_helper.next_gen_left > 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (const auto& ind : current_pop) + { + const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + + next_pop = population_t(current_pop); + + replacement_strategy.pre_process(*this, next_pop); + crossover_selection.pre_process(*this, current_pop); + mutation_selection.pre_process(*this, current_pop); + reproduction_selection.pre_process(*this, current_pop); + + while (thread_helper.next_gen_left > 0) + { + tree_t& c1 = replacement_strategy.select(*this, next_pop); + tree_t* c2 = nullptr; + if (thread_helper.next_gen_left > 1) + while (c2 != &c1) + c2 = &replacement_strategy.select(*this, next_pop); + thread_helper.next_gen_left -= perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, + c2); + } + + thread_helper.next_gen_left = 0; + } + })); + } + else + { + BLT_INFO("Starting steady state thread execution service!"); + std::scoped_lock lock(thread_helper.thread_function_control); + thread_execution_service = std::unique_ptr>(new std::function( + [this, &fitness_function, &replacement_strategy, &crossover_selection, &mutation_selection, &reproduction_selection]( + const size_t id) + { + thread_helper.barrier.wait(); + + multi_threaded_fitness_eval()(fitness_function, id); + + if (thread_helper.next_gen_left > 0) + { + thread_helper.barrier.wait(); + if (id == 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + for (const auto& ind : current_pop) + { + const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + + current_pop = population_t(next_pop); + + replacement_strategy.pre_process(*this, next_pop); + 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); + } + thread_helper.barrier.wait(); + + while (thread_helper.next_gen_left > 0) + { + size_t size = 0; + size_t end = thread_helper.next_gen_left.load(std::memory_order_relaxed); + do + { + size = std::min(end, config.evaluation_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 (size > 0) + { + tree_t& c1 = replacement_strategy.select(*this, next_pop); + tree_t* c2 = nullptr; + if (thread_helper.next_gen_left > 1) + while (c2 != &c1) + c2 = &replacement_strategy.select(*this, next_pop); + size -= perform_selection(crossover_selection, mutation_selection, reproduction_selection, c1, c2); + } + } + } + thread_helper.barrier.wait(); + })); + thread_helper.thread_function_condition.notify_all(); + } + if (eval_fitness_now) + { + BLT_ASSERT( + !current_pop.get_individuals().empty() && current_pop.get_individuals().size() == config.population_size && + "Attempted to evaluate fitness but population was empty. Did you forget to call generate_initial_population()?" && + "You can also pass false to the function to prevent immediate fitness evaluation"); + 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(const 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(const 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(const 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]] const prog_config_t& get_config() const + { + return config; + } + + void set_config(const prog_config_t& config) + { + this->config = config; + selection_probabilities.update(this->config); + } + + [[nodiscard]] type_provider& get_typesystem() + { + return storage.system; + } + + [[nodiscard]] operator_info_t& get_operator_info(const operator_id id) + { + return storage.operators[id]; + } + + [[nodiscard]] detail::print_func_t& get_print_func(const operator_id id) + { + return storage.print_funcs[id]; + } + + [[nodiscard]] detail::destroy_func_t& get_destroy_func(const operator_id id) + { + return storage.destroy_funcs[id]; + } + + [[nodiscard]] std::optional get_name(const operator_id id) const + { + return storage.names[id]; + } + + [[nodiscard]] tracked_vector& get_type_terminals(const type_id id) + { + return storage.terminals[id]; + } + + [[nodiscard]] tracked_vector& get_type_non_terminals(const 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(const operator_id id) const + { + return storage.operator_flags.find(static_cast(id))->second.is_ephemeral(); + } + + [[nodiscard]] bool operator_has_ephemeral_drop(const operator_id id) const + { + return storage.operator_flags.find(static_cast(id))->second.has_ephemeral_drop(); + } + + [[nodiscard]] operator_special_flags get_operator_flags(const operator_id id) const + { + return storage.operator_flags.find(static_cast(id))->second; + } + + 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& [index, value] : enumerate(current_pop.get_individuals())) + values.emplace_back(index, value.fitness.adjusted_fitness); + + std::sort(values.begin(), values.end(), [](const auto& a, const auto& b) + { + return a.second > b.second; + }); + + for (size_t i = 0; i < std::min(size, config.population_size); ++i) + arr[i] = values[i].first; + for (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, 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, size_t index) -> individual_t& + { + return current_pop.get_individuals()[arr[index]]; + }, std::make_integer_sequence()); + } + + void save_generation(fs::writer_t& writer); + + void save_state(fs::writer_t& writer); + + void load_generation(fs::reader_t& reader); + + void load_state(fs::reader_t& reader); + + private: + template + auto single_threaded_fitness_eval() + { + return [this](FitnessFunc& fitness_function) + { + if (thread_helper.evaluation_left > 0) + { + current_stats.normalized_fitness.clear(); + double sum_of_prob = 0; + perform_fitness_function(0, current_pop.get_individuals().size(), fitness_function); + for (const auto& ind : current_pop) + { + const auto prob = (ind.fitness.adjusted_fitness / current_stats.overall_fitness); + current_stats.normalized_fitness.push_back(sum_of_prob + prob); + sum_of_prob += prob; + } + std::sort(current_pop.begin(), current_pop.end(), [](const auto& a, const auto& b) + { + return a.fitness.adjusted_fitness > b.fitness.adjusted_fitness; + }); + thread_helper.evaluation_left = 0; + } + }; + } + + template + auto multi_threaded_fitness_eval() + { + return [this](FitnessFunc& fitness_function, size_t thread_id) + { + if (thread_helper.evaluation_left > 0) + { + thread_helper.barrier.wait(); + while (thread_helper.evaluation_left > 0) + { + size_t size = 0; + size_t begin = 0; + 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)); + perform_fitness_function(begin, end, fitness_function); + } + thread_helper.barrier.wait(); + if (thread_id == 0) + { + std::sort(current_pop.begin(), current_pop.end(), [](const auto& a, const auto& b) + { + return a.fitness.adjusted_fitness > b.fitness.adjusted_fitness; + }); + } + thread_helper.barrier.wait(); + } + }; + } + + template + void perform_fitness_function(const size_t begin, const size_t end, FitnessFunction& fitness_function) + { + using LambdaReturn = std::invoke_result_t; + for (size_t i = begin; i < end; i++) + { + auto& ind = current_pop.get_individuals()[i]; + + ind.fitness = {}; + if constexpr (std::is_same_v || std::is_convertible_v) + { + if (fitness_function(ind.tree, ind.fitness, i)) + 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)) + { + } + } + } + + template + size_t perform_selection(Crossover& crossover, Mutation& mutation, Reproduction& reproduction, tree_t& c1, tree_t* c2) + { + if (get_random().choice(selection_probabilities.crossover_chance)) + { + auto ptr = c2; + if (ptr == nullptr) + ptr = &tree_t::get_thread_local(*this); +#ifdef BLT_TRACK_ALLOCATIONS auto state = tracker.start_measurement_thread_local(); - #endif - const tree_t* p1; - const tree_t* p2; - size_t runs = 0; - do - { - // BLT_TRACE("%lu %p %p", runs, &c1, &tree); - p1 = &crossover.select(*this, current_pop); - p2 = &crossover.select(*this, current_pop); - // BLT_TRACE("%p %p || %lu", p1, p2, current_pop.get_individuals().size()); +#endif + const tree_t* p1; + const tree_t* p2; + size_t runs = 0; + do + { + // BLT_TRACE("%lu %p %p", runs, &c1, &tree); + p1 = &crossover.select(*this, current_pop); + p2 = &crossover.select(*this, current_pop); + // BLT_TRACE("%p %p || %lu", p1, p2, current_pop.get_individuals().size()); - c1.copy_fast(*p1); - ptr->copy_fast(*p2); - // ptr->copy_fast(*p2); + c1.copy_fast(*p1); + ptr->copy_fast(*p2); + // ptr->copy_fast(*p2); - if (++runs >= config.crossover.get().get_config().max_crossover_iterations) - return 0; - #ifdef BLT_TRACK_ALLOCATIONS + if (++runs >= config.crossover.get().get_config().max_crossover_iterations) + return 0; +#ifdef BLT_TRACK_ALLOCATIONS crossover_calls.value(1); - #endif - } while (!config.crossover.get().apply(*this, *p1, *p2, c1, *ptr)); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + } + while (!config.crossover.get().apply(*this, *p1, *p2, c1, *ptr)); +#ifdef BLT_TRACK_ALLOCATIONS tracker.stop_measurement_thread_local(state); crossover_calls.call(); if (state.getAllocatedByteDifference() != 0) @@ -934,30 +972,31 @@ namespace blt::gp crossover_allocations.call(state.getAllocatedByteDifference()); crossover_allocations.set_value(std::max(crossover_allocations.get_value(), state.getAllocatedByteDifference())); } - #endif - if (c2 == nullptr) - { - tree_t::get_thread_local(*this).clear(*this); - return 1; - } - return 2; - } - if (get_random().choice(selection_probabilities.mutation_chance)) - { - #ifdef BLT_TRACK_ALLOCATIONS +#endif + if (c2 == nullptr) + { + tree_t::get_thread_local(*this).clear(*this); + return 1; + } + return 2; + } + if (get_random().choice(selection_probabilities.mutation_chance)) + { +#ifdef BLT_TRACK_ALLOCATIONS auto state = tracker.start_measurement_thread_local(); - #endif - // mutation - const tree_t* p; - do - { - p = &mutation.select(*this, current_pop); - c1.copy_fast(*p); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + // mutation + const tree_t* p; + do + { + p = &mutation.select(*this, current_pop); + c1.copy_fast(*p); +#ifdef BLT_TRACK_ALLOCATIONS mutation_calls.value(1); - #endif - } while (!config.mutator.get().apply(*this, *p, c1)); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + } + while (!config.mutator.get().apply(*this, *p, c1)); +#ifdef BLT_TRACK_ALLOCATIONS tracker.stop_measurement_thread_local(state); mutation_calls.call(); if (state.getAllocationDifference() != 0) @@ -965,17 +1004,17 @@ namespace blt::gp mutation_allocations.call(state.getAllocatedByteDifference()); mutation_allocations.set_value(std::max(mutation_allocations.get_value(), state.getAllocatedByteDifference())); } - #endif - return 1; - } - if (selection_probabilities.reproduction_chance > 0) - { - #ifdef BLT_TRACK_ALLOCATIONS +#endif + return 1; + } + if (selection_probabilities.reproduction_chance > 0) + { +#ifdef BLT_TRACK_ALLOCATIONS auto state = tracker.start_measurement_thread_local(); - #endif - // reproduction - c1.copy_fast(reproduction.select(*this, current_pop)); - #ifdef BLT_TRACK_ALLOCATIONS +#endif + // reproduction + c1.copy_fast(reproduction.select(*this, current_pop)); +#ifdef BLT_TRACK_ALLOCATIONS tracker.stop_measurement_thread_local(state); reproduction_calls.call(); reproduction_calls.value(1); @@ -984,88 +1023,89 @@ namespace blt::gp reproduction_allocations.call(state.getAllocatedByteDifference()); reproduction_allocations.set_value(std::max(reproduction_allocations.get_value(), state.getAllocatedByteDifference())); } - #endif - return 1; - } +#endif + return 1; + } - return 0; - } + return 0; + } - selector_args get_selector_args() - { - return {*this, current_pop, current_stats, config, get_random()}; - } + 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)...}; - } + template + Return convert_array(std::array&& arr, Accessor&& accessor, std::integer_sequence) + { + return Return{accessor(arr, indexes)...}; + } - void create_threads(); + 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); + 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); - } + 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{}; + private: + program_operator_storage_t storage; + std::function seed_func; + prog_config_t config{}; - // internal cache which stores already calculated probability values - struct - { - double crossover_chance = 0; - double mutation_chance = 0; - double reproduction_chance = 0; + // internal cache which stores already calculated probability values + struct + { + double crossover_chance = 0; + double mutation_chance = 0; + double reproduction_chance = 0; - std::optional replacement_amount; + std::optional replacement_amount; - void update(const prog_config_t& config) - { - const auto total = config.crossover_chance + config.mutation_chance + config.reproduction_chance; - crossover_chance = config.crossover_chance / total; - mutation_chance = config.mutation_chance / total; - reproduction_chance = config.reproduction_chance / total; - } - } selection_probabilities; + void update(const prog_config_t& config) + { + const auto total = config.crossover_chance + config.mutation_chance + config.reproduction_chance; + crossover_chance = config.crossover_chance / total; + mutation_chance = config.mutation_chance / total; + reproduction_chance = config.reproduction_chance / total; + } + } selection_probabilities; - population_t current_pop; - population_t next_pop; + population_t current_pop; + population_t next_pop; - std::atomic_uint64_t current_generation = 0; + std::atomic_uint64_t current_generation = 0; - std::atomic_bool fitness_should_exit = false; + std::atomic_bool fitness_should_exit = false; - population_stats current_stats{}; - tracked_vector statistic_history; + population_stats current_stats{}; + tracked_vector statistic_history; - struct concurrency_storage - { - std::vector> threads; + struct concurrency_storage + { + std::vector> threads; - std::mutex thread_function_control{}; - std::condition_variable thread_function_condition{}; + 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_uint64_t evaluation_left = 0; + std::atomic_uint64_t next_gen_left = 0; - std::atomic_bool lifetime_over = false; - blt::barrier_t barrier; + std::atomic_bool lifetime_over = false; + blt::barrier_t barrier; - explicit concurrency_storage(blt::size_t threads): barrier(threads, lifetime_over) - {} - } thread_helper{config.threads == 0 ? std::thread::hardware_concurrency() : config.threads}; + 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; - }; + std::unique_ptr> thread_execution_service = nullptr; + }; } #endif //BLT_GP_PROGRAM_H diff --git a/include/blt/gp/tree.h b/include/blt/gp/tree.h index f9a4be5..263e0af 100644 --- a/include/blt/gp/tree.h +++ b/include/blt/gp/tree.h @@ -88,6 +88,8 @@ namespace blt::gp return m_flags; } + friend bool operator==(const op_container_t& a, const op_container_t& b); + private: size_t m_type_size; operator_id m_id; @@ -625,6 +627,13 @@ namespace blt::gp static tree_t& get_thread_local(gp_program& program); + friend bool operator==(const tree_t& a, const tree_t& b); + + friend bool operator!=(const tree_t& a, const tree_t& b) + { + return !(a == b); + } + private: void handle_operator_inserted(const op_container_t& op); @@ -755,6 +764,13 @@ namespace blt::gp individual_t& operator=(const individual_t&) = delete; individual_t& operator=(individual_t&&) = default; + + friend bool operator==(const individual_t& a, const individual_t& b); + + friend bool operator!=(const individual_t& a, const individual_t& b) + { + return !(a == b); + } }; class population_t diff --git a/src/tree.cpp b/src/tree.cpp index 5f7a78b..b59a2d6 100644 --- a/src/tree.cpp +++ b/src/tree.cpp @@ -717,12 +717,12 @@ namespace blt::gp operator_id id; std::memcpy(&id, in, sizeof(operator_id)); in += sizeof(operator_id); - operations.push_back({ + operations.emplace_back( m_program->get_typesystem().get_type(m_program->get_operator_info(id).return_type).size(), id, m_program->is_operator_ephemeral(id), m_program->get_operator_flags(id) - }); + ); } size_t val_size; std::memcpy(&val_size, in, sizeof(size_t)); @@ -740,17 +740,17 @@ namespace blt::gp { operator_id id; BLT_ASSERT(file.read(&id, sizeof(operator_id)) == sizeof(operator_id)); - operations.push_back({ + operations.emplace_back( m_program->get_typesystem().get_type(m_program->get_operator_info(id).return_type).size(), id, m_program->is_operator_ephemeral(id), m_program->get_operator_flags(id) - }); + ); } - size_t val_size; - BLT_ASSERT(file.read(&val_size, sizeof(size_t)) == sizeof(size_t)); - values.resize(val_size); - BLT_ASSERT(file.read(values.data(), val_size) == val_size); + size_t bytes_in_head; + BLT_ASSERT(file.read(&bytes_in_head, sizeof(size_t)) == sizeof(size_t)); + values.resize(bytes_in_head); + BLT_ASSERT(file.read(values.data(), bytes_in_head) == bytes_in_head); } void tree_t::modify_operator(const size_t point, operator_id new_id, std::optional return_type) @@ -787,4 +787,23 @@ namespace blt::gp handle_operator_inserted(operations[point]); } } + + bool operator==(const tree_t& a, const tree_t& b) + { + if (a.operations.size() != b.operations.size()) + return false; + if (a.values.bytes_in_head() != b.values.bytes_in_head()) + return false; + return std::equal(a.operations.begin(), a.operations.end(), b.operations.begin()); + } + + bool operator==(const op_container_t& a, const op_container_t& b) + { + return a.id() == b.id(); + } + + bool operator==(const individual_t& a, const individual_t& b) + { + return a.tree == b.tree; + } } diff --git a/tests/serialization_test.cpp b/tests/serialization_test.cpp index 3512c56..c101fa8 100644 --- a/tests/serialization_test.cpp +++ b/tests/serialization_test.cpp @@ -15,9 +15,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include + #include "../examples/symbolic_regression.h" #include #include +#include +#include +#include +#include using namespace blt::gp; @@ -35,7 +41,7 @@ prog_config_t config = prog_config_t() .set_reproduction_chance(0.1) .set_max_generations(50) .set_pop_size(500) - .set_thread_count(1); + .set_thread_count(0); example::symbolic_regression_t regression{691ul, config}; @@ -84,11 +90,16 @@ bool fitness_function(const tree_t& current_tree, fitness_t& fitness, size_t) int main() { operator_builder builder{}; - builder.build(addf, subf, mulf, pro_divf, op_sinf, op_cosf, op_expf, op_logf, litf, op_xf); - regression.get_program().set_operations(builder.grab()); + const auto& operators = builder.build(addf, subf, mulf, pro_divf, op_sinf, op_cosf, op_expf, op_logf, litf, op_xf); + regression.get_program().set_operations(operators); auto& program = regression.get_program(); static auto sel = select_tournament_t{}; + + gp_program test_program{691}; + test_program.set_operations(operators); + test_program.setup_generational_evaluation(fitness_function, sel, sel, sel, false); + program.generate_initial_population(program.get_typesystem().get_type().id()); program.setup_generational_evaluation(fitness_function, sel, sel, sel); while (!program.should_terminate()) @@ -100,5 +111,25 @@ int main() program.next_generation(); BLT_TRACE("Evaluate Fitness"); program.evaluate_fitness(); + { + std::filesystem::remove("serialization_test.data"); + std::ofstream stream{"serialization_test.data", std::ios::binary}; + blt::fs::fstream_writer_t writer{stream}; + program.save_generation(writer); + } + { + std::ifstream stream{"serialization_test.data", std::ios::binary}; + blt::fs::fstream_reader_t reader{stream}; + test_program.load_generation(reader); + } + // do a quick validation check + for (const auto& [saved, loaded] : blt::zip(program.get_current_pop(), test_program.get_current_pop())) + { + if (saved.tree != loaded.tree) + { + BLT_ERROR("Serializer Failed to correctly serialize tree to disk, trees are not equal!"); + std::exit(1); + } + } } }