From 69f663d40aa1350703c12be66693effab73f5474 Mon Sep 17 00:00:00 2001 From: KRM7 <70973547+KRM7@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:44:48 +0200 Subject: [PATCH] add fitness cache --- docs/api/Doxyfile | 2 +- docs/api/core/fitness_function.rst | 15 +- docs/api/generate_api_docs.sh | 2 +- docs/encodings.md | 27 +- docs/fitness-functions.md | 71 ++++-- examples/4_fitness_functions.cpp | 2 +- gapp.natvis | 28 ++- src/core/candidate.hpp | 13 + src/core/fitness_function.hpp | 47 ++-- src/core/ga_base.decl.hpp | 28 +++ src/core/ga_base.impl.hpp | 31 ++- src/problems/benchmark_function.hpp | 6 +- src/utility/cache.hpp | 209 +++++++++++++++ src/utility/circular_buffer.hpp | 378 ++++++++++++++++++++++++++++ src/utility/iterators.hpp | 11 +- src/utility/utility.hpp | 25 +- test/unit/algorithm.cpp | 35 ++- test/unit/cache.cpp | 316 +++++++++++++++++++++++ test/unit/circular_buffer.cpp | 372 +++++++++++++++++++++++++++ test/unit/metrics.cpp | 11 +- test/unit/test_utils.hpp | 14 +- tools/install.sh | 2 +- 22 files changed, 1576 insertions(+), 69 deletions(-) create mode 100644 src/utility/cache.hpp create mode 100644 src/utility/circular_buffer.hpp create mode 100644 test/unit/cache.cpp create mode 100644 test/unit/circular_buffer.cpp diff --git a/docs/api/Doxyfile b/docs/api/Doxyfile index e351b91e..e3b47393 100644 --- a/docs/api/Doxyfile +++ b/docs/api/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.1 +# Doxyfile 1.9.1 #--------------------------------------------------------------------------- # Project related configuration options diff --git a/docs/api/core/fitness_function.rst b/docs/api/core/fitness_function.rst index 0a6685d2..0f6c5136 100644 --- a/docs/api/core/fitness_function.rst +++ b/docs/api/core/fitness_function.rst @@ -31,4 +31,17 @@ class FitnessFunctionBase :project: gapp :members: :protected-members: - :private-members: \ No newline at end of file + :private-members: + + +class FitnessFunctionInfo +--------------------------------------------------- + +.. code-block:: + + #include + +.. doxygenclass:: gapp::FitnessFunctionInfo + :project: gapp + :members: + :protected-members: diff --git a/docs/api/generate_api_docs.sh b/docs/api/generate_api_docs.sh index 4a442cfd..da9d7adf 100644 --- a/docs/api/generate_api_docs.sh +++ b/docs/api/generate_api_docs.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/sh echo -e "Generating API documentation...\n" diff --git a/docs/encodings.md b/docs/encodings.md index eede8c98..9f98e692 100644 --- a/docs/encodings.md +++ b/docs/encodings.md @@ -48,10 +48,10 @@ BinaryGA{}.solve(problems::Sphere{}); ## Solution representation -The gene type determines how the candidate solutions to the problem -are going be encoded in the population. The representation of the solutions -will be a vector of the gene type used in all cases. Currently, there is no -way to change this to use some other data structure, so this should be taken +How the candidate solutions to a problem are going be encoded in the GA is +determined by the gene type. The representation of the solutions will always +be a vector of the gene type used. There is currently no way to change this +to use some other data structure instead of a vector, so this should be taken into account when defining new encodings. ```cpp @@ -60,12 +60,17 @@ using Chromosome = std::vector; ``` The candidates contain some more information in addition to their -chromosomes, for example their fitness vectors, but these are -independent of the gene type. +chromosomes, like their fitness vectors, but these are independent +of the gene type. They are represented by the `Candidate` class. -The population is then made up of several candidates encoded in +A population is then made up of several candidates encoded in this way. +```cpp +template +using Population = std::vector>; +``` + ### Variable chromosome lengths The length of the chromosomes is specified as part of the fitness function. @@ -92,7 +97,7 @@ new GA class. In order to do this, you have to: - define a specialization for `GaTraits` - specialize `is_bounded` if needed - define the GA class, derived from `GA` - - define crossover and mutation operators for this encoding + - define crossover and mutation operators for the new encoding The gene type may be anything, with one restriction: the types already used for the existing encodings are reserved and can't @@ -105,9 +110,9 @@ using MyGeneType = std::variant; The specialization of `GaTraits` for the gene type is required in order to define some attributes of the GA. These are the default -crossover and mutation operators that will be used by the GA if none -are specified explicitly, and the default mutation probability used -for the mutation operators: +crossover and mutation operators that will be used by the GA when +they are not specified explicitly, and the default mutation probability +used for the mutation operator: ```cpp namespace gapp diff --git a/docs/fitness-functions.md b/docs/fitness-functions.md index 8ee1647a..dda3fa03 100644 --- a/docs/fitness-functions.md +++ b/docs/fitness-functions.md @@ -133,22 +133,29 @@ returned by `invoke`. ## Other fitness function properties -There are some other parameters of the fitness function that can -be specified, but these generally don't have to be changed from -their default values. However, more complex fitness functions might -have to set their values different from these defaults in some cases. +There are some additional parameters of the fitness function that can +be specified, but these typically don't have to be changed from +their default values. More complex fitness functions, however, might +have to set their values differently from the defaults in some cases. ### Dynamic fitness functions -By default, the fitness function is assumed to always return the -same fitness vector for a particular chromosome passed to it as an +The fitness functions are, by default, assumed to always return the +same fitness vector for a given chromosome passed to them as an argument. This assumption is used to prevent unnecessary fitness -function calls, but this optimization would cause incorrect fitness -vectors to be assigned to some solutions if the assumption is false. +function calls, but it would also cause potentially incorrect fitness +vectors to be assigned to some solutions if the assumption is not true. -For fitness functions where this assumption would not be true, the value -of the `dynamic` parameter in the constructor of `FitnessFunctionBase` -or `FitnessFunction` has to be set to `true`. +In order to prevent this, the fitness functions have a type parameter +associated with them, which can either be `Static` or `Dynamic`. The type +of a fitness function can be set in its constructor, with the default type +being `Static`. + +For fitness functions where this default behaviour would be incorrect, the +value of the `type` parameter in the constructor of the fitness function +should to be set to `Dynamic`. This will disable any kind of caching that +might be used in the GAs, and cause the solutions to be evaluated using +the fitness function every time it's needed. ### Variable chromosome lengths @@ -164,19 +171,48 @@ the initial population is generated instead of explicitly specified. // implementation of a dynamic fitness function class MyFitnessFunction : public FitnessFunction { - MyFitnessFunction() : FitnessFunction(/* dynamic = */ true) {} + MyFitnessFunction() : FitnessFunction(/* type = */ Type::Dynamic) {} FitnessVector invoke(const Chromosome& x) const override; }; ``` +## The number of objective function evaluations + +The number of times the fitness function is evaluated during a run of the GA +is determined by the number of candidates in the population, and the number +of generations. Naively, the number of fitness function calls during a run +would be: + +``` +N = population_size * generations +``` + +While there are cases where this will really be the number of fitness +function calls, such as when the fitness function is dynamic, the library +generally tries to minimize the number of calls to the fitness function +where possible, which means that the actual number will typically be +smaller than this. + +By default, only a simple method is used to achieve this, with minimal +overhead during the runs, but it is also possible to cache the fitnesses +of the candidate solutions during a run to further reduce the number of +fitness function calls. Doing this has a larger overhead though, so it's +only worth doing if the fitness function is relatively expensive to evaluate. + +This cache can be turned on using the `cache_size` method of the GAs: + +```cpp +GA.cache_size(2); // cache the last 2 generations +``` + ## Other concerns ### Numeric issues -The library in general only assumes that the fitness values returned +The library, in general, only assumes that the fitness values returned by the fitness function are valid numbers (i.e. no `NaN` values will -be returned by it). +be returned by the fitness function). Whether infinite fitness values are allowed or not depends on the selection method used in the GA. If the fitness function can return @@ -190,8 +226,9 @@ any issues. ### Thread safety -The candidate solutions in the population are evaluated concurrently -in each generation of a run. As a result of this, the implementation -of the `invoke` method in the derived fitness functions must be thread-safe. +The candidate solutions of a population are evaluated concurrently +in each generation of a run. This means that the implementation +of the `invoke` method in the derived fitness functions should either +be thread-safe. ------------------------------------------------------------------------------------------------ diff --git a/examples/4_fitness_functions.cpp b/examples/4_fitness_functions.cpp index ea4f4551..ce3a9bb6 100644 --- a/examples/4_fitness_functions.cpp +++ b/examples/4_fitness_functions.cpp @@ -32,7 +32,7 @@ class XSquareMulti : public FitnessFunction class XSquareDynamic : public FitnessFunction< RealGene, 1> { public: - XSquareDynamic() : FitnessFunction(/* dynamic = */ true) {} + XSquareDynamic() : FitnessFunction(Type::Dynamic) {} FitnessVector invoke(const Chromosome& x) const override { diff --git a/gapp.natvis b/gapp.natvis index 07b279ff..447ef1ae 100644 --- a/gapp.natvis +++ b/gapp.natvis @@ -2,7 +2,7 @@ - {{ Matrix<{"$T1",sb}, {"$T2",sb}>{{ size = {nrows_}x{ncols_} }} }} + {{ {{ size = {nrows_}x{ncols_} }} }} nrows_ ncols_ @@ -17,7 +17,7 @@ - {{ small_vector<{"$T1",sb}, {"$T2",sb}>{{ size = { last_ - first_ }, capacity = { last_alloc_ - first_ } }} }} + {{ {{ size = { last_ - first_ }, capacity = { last_alloc_ - first_ } }} }} last_ - first_ @@ -26,4 +26,28 @@ + + + {{ {{ size = { size_ }, capacity = { block_size * (blocks_.last_alloc_ - blocks_.first_) } }} }} + + + + size_ + (bool)(blocks_.first_[$i / block_size] & (block_type(1) << ($i % block_size))) + + + + + + + {{ {{ size = { size_ }, capacity = { capacity_ } }} }} + + + + size_ + *(buffer_ + (first_ + $i >= capacity_ ? first_ + $i - capacity_ : first_ + $i)) + + + + diff --git a/src/core/candidate.hpp b/src/core/candidate.hpp index 2ee3f458..69476b6b 100644 --- a/src/core/candidate.hpp +++ b/src/core/candidate.hpp @@ -202,4 +202,17 @@ namespace gapp } // namespace gapp +namespace std +{ + template + struct hash> + { + std::size_t operator()(const gapp::Candidate& candidate) const noexcept + { + return gapp::CandidateHasher{}(candidate); + } + }; + +} // namespace std + #endif // !GA_CORE_CANDIDATE_HPP \ No newline at end of file diff --git a/src/core/fitness_function.hpp b/src/core/fitness_function.hpp index e36924e0..0a38ac50 100644 --- a/src/core/fitness_function.hpp +++ b/src/core/fitness_function.hpp @@ -23,18 +23,32 @@ namespace gapp class FitnessFunctionInfo { public: + /** + * The list of potential fitness function types. + * A fitness function may either be static or dynamic. + * + * @var Type::Static The value representing a static fitness function. A fitness function + * is considered static if it always returns the same fitness vector for a particular + * candidate solution. + * @var Type::Dynamic The value representing a dynamic fitness function. A fitness function + * is considered to be dynamic if it may return different fitness vectors for the same + * candidate solution over multiple calls to the fitness function. + */ + enum class Type { Static = 0, Dynamic = 1 }; + /** * Create a fitness function. * * @param chrom_len The chromosome length that is expected by the fitness function, - * and will be used for the candidate solutions in the GA. \n - * Must be at least 1, and a value must be specified even if the chromosome lengths are variable, - * as it will still be used to generate the initial population. - * @param is_dynamic Should be true if the fitness vector returned for a chromosome will not - * always be the same for the same chromosome (eg. it changes over time or isn't deterministic). + * and will be used for the candidate solutions in the GA. + * Must be at least 1, and a value must be specified even if the chromosome length + * is variable, as it will still be used to generate the initial population. + * @param type The type of the fitness function. The value should be either Type::Static + * or Type::Dynamic, based on whether the fitness function always returns the same + * fitness vector for a solution (static) or not (dynamic). */ - constexpr FitnessFunctionInfo(Positive chrom_len, bool is_dynamic = false) noexcept : - chrom_len_(chrom_len), is_dynamic_(is_dynamic) + constexpr FitnessFunctionInfo(Positive chrom_len, Type type = Type::Static) noexcept : + chrom_len_(chrom_len), type_(type) {} /** @returns The chromosome length the fitness function expects. */ @@ -43,7 +57,7 @@ namespace gapp /** @returns True if the fitness function is dynamic. */ [[nodiscard]] - constexpr bool is_dynamic() const noexcept { return is_dynamic_; } + constexpr bool is_dynamic() const noexcept { return type_ == Type::Dynamic; } /** Destructor. */ virtual ~FitnessFunctionInfo() = default; @@ -57,7 +71,7 @@ namespace gapp private: Positive chrom_len_; - bool is_dynamic_ = false; + Type type_; }; /** @@ -65,8 +79,8 @@ namespace gapp * The fitness functions take a candidate solution (chromosome) as a parameter * and return a fitness vector after evaluating the chromosome. * - * This should be used as the base class for fitness functions if the chromosome length - * is not known at compile time. + * This should be used as the base class for fitness functions if the chromosome + * length is not known at compile time. * If the chromosome length is known at compile, use FitnessFunction as the base class instead. * * @tparam T The gene type expected by the fitness function. @@ -114,14 +128,17 @@ namespace gapp class FitnessFunction : public FitnessFunctionBase { public: + using Type = FitnessFunctionInfo::Type; + /** * Create a fitness function. * - * @param dynamic Should be true if the fitness vector returned for a chromosome will not - * always be the same for the same chromosome (eg. it changes over time or isn't deterministic). + * @param type The type of the fitness function. The value should be either Type::Static + * or Type::Dynamic, based on whether the fitness function always returns the same + * fitness vector for a solution (static) or not (dynamic). */ - constexpr FitnessFunction(bool dynamic = false) noexcept : - FitnessFunctionBase(ChromLen, dynamic) + constexpr FitnessFunction(Type type = Type::Static) noexcept : + FitnessFunctionBase(ChromLen, type) {} }; diff --git a/src/core/ga_base.decl.hpp b/src/core/ga_base.decl.hpp index cab29478..dc530d2a 100644 --- a/src/core/ga_base.decl.hpp +++ b/src/core/ga_base.decl.hpp @@ -10,6 +10,7 @@ #include "../encoding/gene_types.hpp" #include "../stop_condition/stop_condition.hpp" #include "../utility/bounded_value.hpp" +#include "../utility/cache.hpp" #include "../utility/type_traits.hpp" #include #include @@ -334,6 +335,30 @@ namespace gapp */ void repair_function(RepairCallable f); + /** + * Set the number of generations whose solutions will be cached by the %GA. + * This can be used to reduce the number of fitness function evaluations + * performed during the runs. + * + * If not specified, or if @p generations is specified as 0, the default + * behaviour is to not cache any solutions. + * The cache will also be disabled when using a dynamic fitness function, + * regardless of the number specified for @p generations. + * + * When using a cache, it is recommended to only cache a small number of + * generations (1 or 2), as larger values will typically have very little + * additional benefit. + * Using the cache should also be avoided for the real-encoded GA, since + * the cache hit rates will typically be very low due to the floating-point + * encoding used. + * + * The cache is not kept between runs, and setting a new size will also + * clear the current cache. + * + * @param generations The number of generations to cache. Specifying 0 as + * the value will disable the cache. + */ + void cache_size(size_t generations) noexcept; /** * @returns The pareto-optimal solutions found by the %GA. @@ -558,6 +583,9 @@ namespace gapp Population population_; Candidates solutions_; + detail::fifo_cache, FitnessVector> fitness_cache_; + size_t cached_generations_ = 0; + std::unique_ptr> fitness_function_; std::unique_ptr> crossover_; std::unique_ptr> mutation_; diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index 5604a6f5..40d204ff 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -227,6 +227,12 @@ namespace gapp repair_ = std::move(f); } + template + inline void GA::cache_size(size_t generations) noexcept + { + cached_generations_ = generations; + } + template inline Positive GA::findNumberOfObjectives() const { @@ -320,6 +326,8 @@ namespace gapp solutions_.clear(); population_.clear(); + fitness_cache_.reset(!fitness_function_->is_dynamic() * cached_generations_ * population_size_); + if constexpr (is_bounded) { bounds_ = std::move(bounds); } /* Derived GA. */ @@ -431,6 +439,7 @@ namespace gapp GAPP_ASSERT(isValidEvaluatedPopulation(population_)); GAPP_ASSERT(fitnessMatrixIsSynced()); + fitness_cache_.insert(population_.begin(), population_.end(), &Candidate::fitness); population_ = algorithm_->nextPopulation(*this, std::move(population_), std::move(children)); fitness_matrix_ = detail::toFitnessMatrix(population_); } @@ -452,14 +461,26 @@ namespace gapp /* If the fitness function is static, and the solution has already * been evaluted sometime earlier (in an earlier generation), there * is no point doing it again. */ - if (!sol.is_evaluated || fitness_function_->is_dynamic()) + if (!fitness_function_->is_dynamic() && sol.is_evaluated) return; + + if (cached_generations_) { - std::atomic_ref{ num_fitness_evals_ }.fetch_add(1, std::memory_order_release); - - sol.fitness = (*fitness_function_)(sol.chromosome); - sol.is_evaluated = true; + GAPP_ASSERT(!fitness_function_->is_dynamic()); + + const FitnessVector* fitness = fitness_cache_.get(sol); + if (fitness) + { + sol.fitness = *fitness; + sol.is_evaluated = true; + return; + } } + std::atomic_ref{ num_fitness_evals_ }.fetch_add(1, std::memory_order_release); + + sol.fitness = (*fitness_function_)(sol.chromosome); + sol.is_evaluated = true; + GAPP_ASSERT(hasValidFitness(sol)); } diff --git a/src/problems/benchmark_function.hpp b/src/problems/benchmark_function.hpp index 49f3f8fe..88a75148 100644 --- a/src/problems/benchmark_function.hpp +++ b/src/problems/benchmark_function.hpp @@ -100,19 +100,19 @@ namespace gapp::problems /* Single-objective, uniform bounds. */ BenchmarkFunction(std::string name, Bounds bounds, Chromosome optimum, double optimal_value) : - FitnessFunctionBase(optimum.size(), 1), + FitnessFunctionBase(optimum.size()), BenchmarkFunctionTraits(std::move(name), bounds, std::move(optimum), optimal_value) {} /* Multi-objective, uniform bounds. */ BenchmarkFunction(std::string name, Bounds bounds, Chromosome optimum, FitnessVector optimal_value) : - FitnessFunctionBase(optimum.size(), optimal_value.size()), + FitnessFunctionBase(optimum.size()), BenchmarkFunctionTraits(std::move(name), bounds, std::move(optimum), std::move(optimal_value)) {} /* General ctor, uniform bounds. */ BenchmarkFunction(std::string name, size_t nvars, size_t nobj, Bounds bounds) : - FitnessFunctionBase(nvars, nobj), + FitnessFunctionBase(nvars), BenchmarkFunctionTraits(std::move(name), nobj, nvars, bounds) {} diff --git a/src/utility/cache.hpp b/src/utility/cache.hpp new file mode 100644 index 00000000..272c7d4c --- /dev/null +++ b/src/utility/cache.hpp @@ -0,0 +1,209 @@ +/* Copyright (c) 2024 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GAPP_UTILITY_CACHE_HPP +#define GAPP_UTILITY_CACHE_HPP + +#include "circular_buffer.hpp" +#include "concepts.hpp" +#include "utility.hpp" +#include +#include +#include +#include +#include + +namespace gapp::detail +{ + template + class fifo_cache + { + public: + using key_type = Key; + using value_type = Value; + using reference = Value&; + using const_reference = const Value&; + using pointer = Value*; + using const_pointer = const Value*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + static_assert(detail::hashable && std::equality_comparable); + + //-----------------------------------// + // CONSTRUCTORS // + //-----------------------------------// + + constexpr fifo_cache() noexcept = default; + + constexpr fifo_cache(size_type capacity) : + cache_(capacity + 1), + order_(capacity) + {} + + constexpr fifo_cache(const fifo_cache& other) : + cache_(other.capacity() + 1), + order_(other.capacity()) + { + for (auto it : other.order_) + { + order_.push_back(cache_.insert(*it).first); + } + } + + constexpr fifo_cache(fifo_cache&& other) noexcept + { + swap(other); + } + + constexpr fifo_cache& operator=(fifo_cache other) noexcept + { + swap(other); + return *this; + } + + //-----------------------------------// + // CAPACITY // + //-----------------------------------// + + constexpr size_type size() const noexcept { return cache_.size(); } + constexpr size_type capacity() const noexcept { return order_.capacity(); } + + constexpr bool empty() const noexcept { return cache_.empty(); } + constexpr bool full() const noexcept { return size() == capacity(); } + + //-----------------------------------// + // LOOKUP // + //-----------------------------------// + + constexpr bool contains(const key_type& key) const + { + return cache_.contains(key); + } + + constexpr pointer get(const key_type& key) + { + if (empty()) return nullptr; + const auto it = cache_.find(key); + if (it == cache_.end()) return nullptr; + return std::addressof(it->second); + } + + constexpr const_pointer get(const key_type& key) const + { + if (empty()) return nullptr; + const auto it = cache_.find(key); + if (it == cache_.end()) return nullptr; + return std::addressof(it->second); + } + + //-----------------------------------// + // MODIFIERS // + //-----------------------------------// + + template + constexpr void insert(K&& key, V&& value) + { + GAPP_ASSERT(free_capacity() >= 1); + + if (capacity() == 0) [[unlikely]] return; + + const auto [it, inserted] = cache_.insert_or_assign(std::forward(key), std::forward(value)); + if (!inserted) return; + + if (order_.full()) cache_.erase(order_.front()); + order_.push_back(it); + } + + template + constexpr void try_insert(K&& key, Args&&... vargs) + { + GAPP_ASSERT(free_capacity() >= 1); + + if (capacity() == 0) [[unlikely]] return; + + const auto [it, inserted] = cache_.try_emplace(std::forward(key), std::forward(vargs)...); + if (!inserted) return; + + if (order_.full()) cache_.erase(order_.front()); + order_.push_back(it); + } + + template> F> + constexpr void insert(Iter first, Iter last, F&& f) + { + GAPP_ASSERT(free_capacity() >= 1); + + if (capacity() == 0) [[unlikely]] return; + + const size_type range_len = std::distance(first, last); + const size_type insert_count = std::min(capacity(), range_len); + first = std::prev(last, insert_count); + + for (; first != last; ++first) + { + const auto [it, inserted] = cache_.insert_or_assign(*first, std::invoke(f, *first)); + if (!inserted) continue; + + if (order_.full()) cache_.erase(order_.front()); + order_.push_back(it); + } + } + + constexpr void clear() noexcept + { + cache_.clear(); + order_.clear(); + } + + constexpr void reset(size_type new_capacity) + { + clear(); + cache_.reserve(new_capacity + 1); + order_.reset(new_capacity); + } + + constexpr void swap(fifo_cache& other) noexcept + { + cache_.swap(other.cache_); + order_.swap(other.order_); + } + + //-----------------------------------// + // OTHER // + //-----------------------------------// + + constexpr friend bool operator==(const fifo_cache& lhs, const fifo_cache& rhs) + { + if (lhs.size() != rhs.size()) return false; + + for (size_type i = 0; i < lhs.size(); i++) + { + if (*lhs.order_[i] != *rhs.order_[i]) return false; + } + return true; + } + + private: + using Iter = typename std::unordered_map::iterator; + + std::unordered_map cache_; + detail::circular_buffer order_; + + constexpr size_type free_capacity() const noexcept + { + GAPP_ASSERT(cache_.max_load_factor() == 1.0); + GAPP_ASSERT(cache_.size() <= cache_.bucket_count()); + + return cache_.bucket_count() - cache_.size(); + } + }; + + template + constexpr void swap(fifo_cache& lhs, fifo_cache& rhs) noexcept + { + lhs.swap(rhs); + } + +} // namespace gapp::detail + +#endif // !GAPP_UTILITY_CACHE_HPP diff --git a/src/utility/circular_buffer.hpp b/src/utility/circular_buffer.hpp new file mode 100644 index 00000000..f257d267 --- /dev/null +++ b/src/utility/circular_buffer.hpp @@ -0,0 +1,378 @@ +/* Copyright (c) 2024 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GAPP_UTILITY_CIRCULAR_BUFFER_HPP +#define GAPP_UTILITY_CIRCULAR_BUFFER_HPP + +#include "small_vector.hpp" +#include "iterators.hpp" +#include "scope_exit.hpp" +#include "utility.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gapp::detail +{ + template> + class circular_buffer + { + public: + using value_type = T; + using allocator_type = A; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + using iterator = detail::stable_iterator; + using const_iterator = detail::const_stable_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + static_assert(std::allocator_traits::is_always_equal::value); + + //-----------------------------------// + // CONSTRUCTORS // + //-----------------------------------// + + constexpr explicit circular_buffer(A alloc = A()) noexcept : + alloc_(std::move(alloc)) + {} + + constexpr explicit circular_buffer(size_type capacity, A alloc = A()) : + buffer_(detail::allocate(alloc, capacity).data), + capacity_(capacity), + alloc_(std::move(alloc)) + {} + + template + constexpr circular_buffer(size_type capacity, Iter first, Iter last, A alloc = A()) : + capacity_(capacity), + alloc_(std::move(alloc)) + { + buffer_ = detail::allocate(alloc_, capacity_).data; + detail::scope_exit guard{ [&] { detail::deallocate(alloc_, buffer_, capacity_); } }; + const size_type size = std::min(capacity, std::distance(first, last)); + detail::construct_range(alloc_, buffer_, buffer_ + size, std::prev(last, size)); + size_ = size; + guard.release(); + } + + constexpr circular_buffer(size_type capacity, std::initializer_list ilist, A alloc = A()) : + circular_buffer(capacity, ilist.begin(), ilist.end(), std::move(alloc)) + {} + + constexpr circular_buffer(const circular_buffer& other) : + circular_buffer(other.capacity(), other.begin(), other.end(), std::allocator_traits::select_on_container_copy_construction(other.alloc_)) + {} + + constexpr circular_buffer(circular_buffer&& other) noexcept + { + swap(other); + } + + //-----------------------------------// + // DESTRUCTOR // + //-----------------------------------// + + constexpr ~circular_buffer() noexcept + { + destroy(); + } + + //-----------------------------------// + // ASSIGNMENT // + //-----------------------------------// + + constexpr circular_buffer& operator=(circular_buffer other) noexcept + { + swap(other); + return *this; + } + + //-----------------------------------// + // ITERATORS // + //-----------------------------------// + + constexpr iterator begin() noexcept { return iterator(this, 0); } + constexpr iterator end() noexcept { return iterator(this, size()); } + + constexpr const_iterator begin() const noexcept { return const_iterator(this, 0); } + constexpr const_iterator end() const noexcept { return const_iterator(this, size()); } + + constexpr const_iterator cbegin() const noexcept { return begin(); } + constexpr const_iterator cend() const noexcept { return end(); } + + constexpr reverse_iterator rbegin() noexcept { return std::reverse_iterator(end()); } + constexpr reverse_iterator rend() noexcept { return std::reverse_iterator(begin()); } + + constexpr const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator(end()); } + constexpr const_reverse_iterator rend() const noexcept { return std::reverse_iterator(begin()); } + + constexpr const_reverse_iterator crbegin() const noexcept { return rbegin(); } + constexpr const_reverse_iterator crend() const noexcept { return rend(); } + + //-----------------------------------// + // CAPACITY // + //-----------------------------------// + + constexpr size_type size() const noexcept { return size_; } + constexpr size_type capacity() const noexcept { return capacity_; } + constexpr size_type max_size() const noexcept { return std::allocator_traits::max_size(alloc_); } + + constexpr bool empty() const noexcept { return size() == 0; } + constexpr bool full() const noexcept { return size() == capacity(); } + + //-----------------------------------// + // ELEMENT ACCESS // + //-----------------------------------// + + constexpr reference operator[](size_type pos) + { + GAPP_ASSERT(pos < size()); + return *pointer_to(pos); + } + + constexpr const_reference operator[](size_type pos) const + { + GAPP_ASSERT(pos < size()); + return *pointer_to(pos); + } + + constexpr reference at(size_type pos) + { + if (pos >= size()) GAPP_THROW(std::out_of_range, "Bad buffer index."); + return (*this)[pos]; + } + + constexpr const_reference at(size_type pos) const + { + if (pos >= size()) GAPP_THROW(std::out_of_range, "Bad buffer index."); + return (*this)[pos]; + } + + constexpr reference front() + { + GAPP_ASSERT(!empty()); + return buffer_[first_]; + } + + constexpr const_reference front() const + { + GAPP_ASSERT(!empty()); + return buffer_[first_]; + } + + constexpr reference back() + { + GAPP_ASSERT(!empty()); + return (*this)[size() - 1]; + } + + constexpr const_reference back() const + { + GAPP_ASSERT(!empty()); + return (*this)[size() - 1]; + } + + //-----------------------------------// + // MODIFIERS // + //-----------------------------------// + + constexpr void push_back(const value_type& value) + noexcept(std::is_nothrow_copy_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + emplace_back(value); + } + + constexpr void push_back(value_type&& value) + noexcept(std::is_nothrow_move_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + emplace_back(std::move(value)); + } + + constexpr void push_front(const value_type& value) + noexcept(std::is_nothrow_move_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + emplace_front(value); + } + + constexpr void push_front(value_type&& value) + noexcept(std::is_nothrow_move_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + emplace_front(std::move(value)); + } + + template + constexpr reference emplace_back(Args&&... args) + noexcept(std::is_nothrow_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + + if (full()) + { + pointer last = buffer_ + first_; + *last = T(std::forward(args)...); + first_ = next_idx(first_); + return *last; + } + + pointer last = pointer_to(size_); + detail::construct(alloc_, last, std::forward(args)...); + size_++; + return *last; + } + + template + constexpr reference emplace_front(Args&&... args) + noexcept(std::is_nothrow_constructible_v) + { + GAPP_ASSERT(capacity() > 0); + + const size_type new_first = prev_idx(first_); + + if (full()) + { + buffer_[new_first] = T(std::forward(args)...); + first_ = new_first; + } + else + { + detail::construct(alloc_, buffer_ + new_first, std::forward(args)...); + first_ = new_first; + size_++; + } + + return buffer_[first_]; + } + + constexpr void pop_front() noexcept + { + GAPP_ASSERT(!empty()); + detail::destroy(alloc_, std::addressof(front())); + first_ = next_idx(first_); + size_--; + } + + constexpr void pop_back() noexcept + { + GAPP_ASSERT(!empty()); + detail::destroy(alloc_, std::addressof(back())); + size_--; + } + + constexpr void set_capacity(size_type new_capacity) + { + if (new_capacity == capacity()) return; + + pointer new_buffer = detail::allocate(alloc_, new_capacity).data; + detail::scope_exit guard{ [&] { detail::deallocate(alloc_, buffer_, new_capacity); } }; + const size_type new_size = std::min(new_capacity, size()); + std::uninitialized_copy_n(detail::make_move_iterator_if_noexcept(begin()), new_size, new_buffer); + destroy(); + buffer_ = new_buffer; + size_ = new_size; + capacity_ = new_capacity; + guard.release(); + } + + constexpr void clear() noexcept + { + for (T& elem : *this) detail::destroy(alloc_, std::addressof(elem)); + first_ = 0; + size_ = 0; + } + + constexpr void reset(size_type new_capacity) + { + clear(); + if (new_capacity == capacity_) return; + if (buffer_) detail::deallocate(alloc_, buffer_, capacity_); + buffer_ = detail::allocate(alloc_, new_capacity).data; + capacity_ = new_capacity; + } + + constexpr void swap(circular_buffer& other) noexcept + { + using std::swap; + swap(buffer_, other.buffer_); + swap(first_, other.first_); + swap(size_, other.size_); + swap(capacity_, other.capacity_); + swap(alloc_, other.alloc_); + } + + //-----------------------------------// + // OTHER // + //-----------------------------------// + + constexpr allocator_type get_allocator() const noexcept(std::is_nothrow_copy_constructible_v) + { + return alloc_; + } + + constexpr friend bool operator==(const circular_buffer& lhs, const circular_buffer& rhs) noexcept + { + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); + } + + constexpr friend auto operator<=>(const circular_buffer& lhs, const circular_buffer& rhs) noexcept + { + return std::lexicographical_compare_three_way(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); + } + + private: + pointer buffer_ = nullptr; + size_type first_ = 0; + size_type size_ = 0; + size_type capacity_ = 0; + GAPP_NO_UNIQUE_ADDRESS A alloc_; + + constexpr size_type next_idx(size_type idx) const noexcept + { + return detail::next_mod(idx, capacity_); + } + + constexpr size_type prev_idx(size_type idx) const noexcept + { + return detail::prev_mod(idx, capacity_); + } + + constexpr pointer pointer_to(size_type pos) const noexcept + { + const size_type idx = first_ + pos; + return idx >= capacity_ ? (buffer_ + idx - capacity_) : (buffer_ + idx); + } + + constexpr void destroy() noexcept + { + if (!buffer_) return; + clear(); + detail::deallocate(alloc_, buffer_, capacity_); + } + }; + + template + constexpr void swap(circular_buffer& lhs, circular_buffer& rhs) noexcept + { + lhs.swap(rhs); + } + + template>> + circular_buffer(Iter, Iter, Alloc = Alloc()) -> circular_buffer, Alloc>; + +} // namespace gapp::detail + +#endif // !GAPP_UTILITY_CIRCULAR_BUFFER_HPP diff --git a/src/utility/iterators.hpp b/src/utility/iterators.hpp index 2bf520c8..bdfd3852 100644 --- a/src/utility/iterators.hpp +++ b/src/utility/iterators.hpp @@ -295,7 +295,7 @@ namespace gapp::detail * operator< * increment() function (equivalent to prefix operator++) * decrement() function (equivalent to prefix operator--) - * operator +=(n) + * operator+=(n) * operator-(it, it) */ template @@ -556,6 +556,15 @@ namespace gapp::detail T value_; }; + + template + constexpr auto make_move_iterator_if_noexcept(Iter it) noexcept + { + if constexpr (std::is_nothrow_move_constructible_v>) + return std::move_iterator(it); + else return it; + } + } // namespace gapp::detail #endif // !GA_UTILITY_ITERATORS_HPP \ No newline at end of file diff --git a/src/utility/utility.hpp b/src/utility/utility.hpp index f247937a..6084a2b9 100644 --- a/src/utility/utility.hpp +++ b/src/utility/utility.hpp @@ -186,12 +186,33 @@ namespace gapp::detail } template - constexpr void increment_mod(T& value, T mod) + constexpr T next_mod(T value, T mod) noexcept { GAPP_ASSERT(mod > 0); GAPP_ASSERT(0 <= value && value < mod); - value = (value + 1 == mod) ? T(0) : value + 1; + return (value + 1 == mod) ? T(0) : (value + 1); + } + + template + constexpr T prev_mod(T value, T mod) noexcept + { + GAPP_ASSERT(mod > 0); + GAPP_ASSERT(0 <= value && value < mod); + + return (value == 0) ? (mod - 1) : (value - 1); + } + + template + constexpr void increment_mod(T& value, T mod) noexcept + { + value = next_mod(value, mod); + } + + template + constexpr void decrement_mod(T& value, T mod) noexcept + { + value = prev_mod(value, mod); } } // namespace gapp::detail diff --git a/test/unit/algorithm.cpp b/test/unit/algorithm.cpp index fd6dce3e..3a0fbfdf 100644 --- a/test/unit/algorithm.cpp +++ b/test/unit/algorithm.cpp @@ -19,14 +19,45 @@ static constexpr auto always_false = [](auto) { return false; }; using namespace gapp; +TEST_CASE("next_mod", "[algorithm]") +{ + REQUIRE(detail::next_mod(0, 3) == 1); + REQUIRE(detail::next_mod(1, 3) == 2); + REQUIRE(detail::next_mod(2, 3) == 0); +} + +TEST_CASE("prev_mod", "[algorithm]") +{ + REQUIRE(detail::prev_mod(0, 3) == 2); + REQUIRE(detail::prev_mod(1, 3) == 0); + REQUIRE(detail::prev_mod(2, 3) == 1); +} + TEST_CASE("increment_mod", "[algorithm]") { int n = 0; - detail::increment_mod(n, 2); + detail::increment_mod(n, 3); + REQUIRE(n == 1); + + detail::increment_mod(n, 3); + REQUIRE(n == 2); + + detail::increment_mod(n, 3); + REQUIRE(n == 0); +} + +TEST_CASE("decrement_mod", "[algorithm]") +{ + int n = 0; + + detail::decrement_mod(n, 3); + REQUIRE(n == 2); + + detail::decrement_mod(n, 3); REQUIRE(n == 1); - detail::increment_mod(n, 2); + detail::decrement_mod(n, 3); REQUIRE(n == 0); } diff --git a/test/unit/cache.cpp b/test/unit/cache.cpp new file mode 100644 index 00000000..8677191b --- /dev/null +++ b/test/unit/cache.cpp @@ -0,0 +1,316 @@ +/* Copyright (c) 2024 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include +#include "utility/cache.hpp" +#include +#include + +using gapp::detail::fifo_cache; + + +TEST_CASE("constructor", "[fifo_cache]") +{ + fifo_cache cache1; + REQUIRE(cache1.capacity() == 0); + + fifo_cache cache2(4); + REQUIRE(cache2.capacity() == 4); + + fifo_cache cache3 = cache2; + REQUIRE(cache3.capacity() == 4); + + fifo_cache cache4 = std::move(cache2); + REQUIRE(cache4.capacity() == 4); + REQUIRE(cache2.empty()); +} + +TEST_CASE("copy_complex", "[fifo_cache]") +{ + fifo_cache cache1(4); + + cache1.insert(1, 2); + cache1.insert(3, 6); + cache1.insert(2, 4); + cache1.insert(4, 8); + + fifo_cache cache2 = cache1; + + REQUIRE(cache2.size() == 4); + REQUIRE(cache2.capacity() == 4); + + cache2.insert(5, 10); + + REQUIRE(cache2.get(1) == nullptr); + REQUIRE(*cache2.get(5) == 10); + + cache2.insert(6, 12); + + REQUIRE(cache2.get(3) == nullptr); + REQUIRE(*cache2.get(6) == 12); +} + +TEST_CASE("size/capacity", "[fifo_cache]") +{ + fifo_cache cache(5); + + REQUIRE(cache.size() == 0); + REQUIRE(cache.capacity() == 5); + + cache.insert(1, 2); + + REQUIRE(cache.size() == 1); + REQUIRE(cache.capacity() == 5); + + cache.insert(2, 4); + cache.insert(3, 6); + cache.insert(4, 8); + cache.insert(5, 10); + + REQUIRE(cache.size() == 5); + REQUIRE(cache.capacity() == 5); + + cache.insert(6, 12); + + REQUIRE(cache.size() == 5); + REQUIRE(cache.capacity() == 5); +} + +TEST_CASE("full/empty", "[fifo_cache]") +{ + fifo_cache cache(4); + + REQUIRE(cache.empty()); + REQUIRE(!cache.full()); + + cache.insert(1, 2); + + REQUIRE(!cache.empty()); + REQUIRE(!cache.full()); + + cache.insert(2, 4); + cache.insert(3, 6); + cache.insert(4, 8); + + REQUIRE(!cache.empty()); + REQUIRE(cache.full()); + + cache.insert(5, 10); + + REQUIRE(!cache.empty()); + REQUIRE(cache.full()); +} + +TEST_CASE("insert/get", "[fifo_cache]") +{ + fifo_cache cache(4); + + REQUIRE(cache.get(1) == nullptr); + + cache.insert(1, 2); + + REQUIRE(*cache.get(1) == 2); + REQUIRE(cache.get(2) == nullptr); + + cache.insert(1, -1); + + REQUIRE(*cache.get(1) == -1); + + cache.insert(2, 4); + cache.insert(3, 6); + cache.insert(4, 8); + + REQUIRE(*cache.get(1) == -1); + REQUIRE(*cache.get(3) == 6); + REQUIRE(*cache.get(4) == 8); + + cache.insert(5, 10); + + REQUIRE(*cache.get(5) == 10); + REQUIRE(cache.get(1) == nullptr); + + cache.insert(6, 12); + + REQUIRE(*cache.get(6) == 12); + REQUIRE(cache.get(2) == nullptr); + + + fifo_cache empty; + + REQUIRE(empty.empty()); + REQUIRE(empty.capacity() == 0); + + empty.insert(1, 1); + + REQUIRE(empty.empty()); +} + +TEST_CASE("try_insert", "[fifo_cache]") +{ + fifo_cache cache(4); + + REQUIRE(cache.get(1) == nullptr); + + cache.try_insert(1, 2); + + REQUIRE(*cache.get(1) == 2); + REQUIRE(cache.get(2) == nullptr); + + cache.try_insert(1, -1); + + REQUIRE(*cache.get(1) == 2); + + cache.try_insert(2, 4); + cache.try_insert(3, 6); + cache.try_insert(4, 8); + + REQUIRE(*cache.get(1) == 2); + REQUIRE(*cache.get(3) == 6); + REQUIRE(*cache.get(4) == 8); + + cache.try_insert(5, 10); + + REQUIRE(*cache.get(5) == 10); + REQUIRE(cache.get(1) == nullptr); + + cache.try_insert(6, 12); + + REQUIRE(*cache.get(6) == 12); + REQUIRE(cache.get(2) == nullptr); + + + fifo_cache empty; + + REQUIRE(empty.empty()); + REQUIRE(empty.capacity() == 0); + + empty.try_insert(1, 1); + + REQUIRE(empty.empty()); +} + +TEST_CASE("insert_range", "[fifo_cache]") +{ + const std::vector keys{ 1, 2, 3, 4 }; + + fifo_cache cache1(4); + cache1.insert(keys.begin(), keys.end(), [](int n) { return n * 2; }); + + REQUIRE(cache1.size() == 4); + REQUIRE(*cache1.get(1) == 2); + REQUIRE(*cache1.get(3) == 6); + + fifo_cache cache2(2); + cache2.insert(keys.begin(), keys.end(), [](int n) { return n * 2; }); + + REQUIRE(cache2.size() == 2); + REQUIRE(*cache2.get(3) == 6); + REQUIRE(*cache2.get(4) == 8); +} + +TEST_CASE("contains", "[fifo_cache]") +{ + fifo_cache cache(4); + + REQUIRE(!cache.contains(3)); + REQUIRE(!cache.contains(2)); + + cache.insert(3, 2); + + REQUIRE(cache.contains(3)); + REQUIRE(!cache.contains(2)); +} + +TEST_CASE("clear", "[fifo_cache]") +{ + fifo_cache cache(3); + + cache.insert(1, 2); + cache.insert(2, 4); + + REQUIRE(cache.size() == 2); + REQUIRE(cache.capacity() == 3); + + cache.clear(); + + REQUIRE(cache.empty()); + REQUIRE(cache.capacity() == 3); +} + +TEST_CASE("reset", "[fifo_cache]") +{ + const size_t new_capacity = GENERATE(2, 3, 5); + + fifo_cache cache(3); + + cache.insert(1, 2); + cache.insert(2, 4); + + REQUIRE(cache.size() == 2); + REQUIRE(cache.capacity() == 3); + + cache.reset(new_capacity); + + REQUIRE(cache.empty()); + REQUIRE(cache.capacity() == new_capacity); +} + +TEST_CASE("swap", "[fifo_cache]") +{ + using std::swap; + + fifo_cache cache1(4); + fifo_cache cache2(5); + + REQUIRE(cache1 == cache2); + + swap(cache1, cache2); + + REQUIRE(cache1 == cache2); + + cache1.insert(1, 2); + cache1.insert(2, 4); + + cache2.insert(1, 3); + + REQUIRE(cache1 != cache2); + + REQUIRE(cache1.size() == 2); + REQUIRE(cache1.capacity() == 5); + + REQUIRE(cache2.size() == 1); + REQUIRE(cache2.capacity() == 4); + + swap(cache1, cache2); + + REQUIRE(cache1.size() == 1); + REQUIRE(cache1.capacity() == 4); + + REQUIRE(cache2.size() == 2); + REQUIRE(cache2.capacity() == 5); + + REQUIRE(*cache1.get(1) == 3); +} + +TEST_CASE("comparison", "[fifo_cache]") +{ + fifo_cache cache1(3); + fifo_cache cache2(4); + + REQUIRE(cache1 == cache2); + + cache1.insert(1, 2); + cache2.insert(1, 2); + + REQUIRE(cache1 == cache2); + + cache1.insert(2, 4); + cache2.insert(3, 6); + + REQUIRE(cache1 != cache2); + + cache1.insert(3, 6); + cache2.insert(2, 4); + + REQUIRE(cache1 != cache2); +} diff --git a/test/unit/circular_buffer.cpp b/test/unit/circular_buffer.cpp new file mode 100644 index 00000000..fdb466e4 --- /dev/null +++ b/test/unit/circular_buffer.cpp @@ -0,0 +1,372 @@ +/* Copyright (c) 2024 Krisztián Rugási. Subject to the MIT License. */ + +#include +#include +#include "utility/circular_buffer.hpp" +#include +#include +#include + +using gapp::detail::circular_buffer; + + +TEST_CASE("constructor", "[circular_buffer]") +{ + const size_t capacity = GENERATE(1, 2, 3, 5, 100); + + circular_buffer buffer(capacity); + + REQUIRE(buffer.capacity() == capacity); + REQUIRE(buffer.size() == 0); + + circular_buffer buffer_copy = buffer; + + REQUIRE(buffer.capacity() == capacity); + REQUIRE(buffer.size() == 0); + + REQUIRE(buffer_copy.capacity() == capacity); + REQUIRE(buffer_copy.size() == 0); + + circular_buffer buffer_moved = std::move(buffer); + + REQUIRE(buffer.capacity() == 0); + REQUIRE(buffer.size() == 0); + + REQUIRE(buffer_moved.capacity() == capacity); + REQUIRE(buffer_moved.size() == 0); +} + +TEST_CASE("emplace_back", "[circular_buffer]") +{ + circular_buffer buffer(4); + + REQUIRE(buffer.capacity() == 4); + + buffer.emplace_back(1); + REQUIRE(buffer.size() == 1); + + buffer.emplace_back(2); + buffer.emplace_back(3); + REQUIRE(buffer.size() == 3); + + buffer.emplace_back(4); + REQUIRE(buffer.size() == 4); + + buffer.emplace_back(5); + REQUIRE(buffer.size() == 4); + + buffer.emplace_back(6); + REQUIRE(buffer.size() == 4); + + REQUIRE(buffer.capacity() == 4); +} + +TEST_CASE("empty/full", "[circular_buffer]") +{ + circular_buffer buffer(4); + + REQUIRE(buffer.empty()); + REQUIRE(!buffer.full()); + + buffer.emplace_back(1); + REQUIRE(!buffer.empty()); + REQUIRE(!buffer.full()); + + buffer.emplace_back(2); + buffer.emplace_back(3); + buffer.emplace_back(4); + REQUIRE(!buffer.empty()); + REQUIRE(buffer.full()); + + buffer.emplace_back(5); + REQUIRE(!buffer.empty()); + REQUIRE(buffer.full()); +} + +TEST_CASE("front/back", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.emplace_back(1); + REQUIRE(buffer.back() == 1); + REQUIRE(buffer.front() == 1); + + buffer.emplace_back(2); + REQUIRE(buffer.back() == 2); + REQUIRE(buffer.front() == 1); + + buffer.emplace_back(3); + buffer.emplace_back(4); + REQUIRE(buffer.back() == 4); + REQUIRE(buffer.front() == 1); + + buffer.emplace_back(5); + REQUIRE(buffer.back() == 5); + REQUIRE(buffer.front() == 2); + + buffer.emplace_back(6); + REQUIRE(buffer.back() == 6); + REQUIRE(buffer.front() == 3); +} + +TEST_CASE("element_access", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.emplace_back(1); + buffer.emplace_back(2); + buffer.emplace_back(3); + buffer.emplace_back(4); + + REQUIRE(buffer[0] == 1); + REQUIRE(buffer[1] == 2); + REQUIRE(buffer[2] == 3); + REQUIRE(buffer[3] == 4); + + buffer.emplace_back(5); + buffer.emplace_back(6); + + REQUIRE(buffer[0] == 3); + REQUIRE(buffer[1] == 4); + REQUIRE(buffer[2] == 5); + REQUIRE(buffer[3] == 6); + + buffer.emplace_back(7); + buffer.emplace_back(8); + buffer.emplace_back(9); + + REQUIRE(buffer[0] == 6); + REQUIRE(buffer[1] == 7); + REQUIRE(buffer[2] == 8); + REQUIRE(buffer[3] == 9); +} + +TEST_CASE("emplace_front", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.emplace_front(1); + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.front() == 1); + + buffer.emplace_front(2); + REQUIRE(buffer.size() == 2); + REQUIRE(buffer.front() == 2); + + buffer.emplace_front(3); + buffer.emplace_front(4); + REQUIRE(buffer.size() == 4); + REQUIRE(buffer.front() == 4); + REQUIRE(buffer.back() == 1); + + buffer.emplace_front(5); + REQUIRE(buffer.size() == 4); + REQUIRE(buffer.front() == 5); + REQUIRE(buffer.back() == 2); +} + +TEST_CASE("pop_front/back", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.emplace_back(1); + buffer.emplace_back(2); + + SECTION("pop_front") + { + buffer.pop_front(); + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.front() == 2); + REQUIRE(buffer.back() == 2); + + buffer.pop_front(); + REQUIRE(buffer.empty()); + } + + SECTION("pop_back") + { + buffer.pop_back(); + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.front() == 1); + REQUIRE(buffer.back() == 1); + + buffer.pop_back(); + REQUIRE(buffer.empty()); + } + + SECTION("pop_both") + { + buffer.pop_front(); + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.front() == 2); + REQUIRE(buffer.back() == 2); + + buffer.pop_back(); + REQUIRE(buffer.empty()); + } +} + +TEST_CASE("push_pop", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.push_back(1); + buffer.push_back(2); + buffer.pop_front(); + + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.front() == 2); + REQUIRE(buffer.back() == 2); + + buffer.push_back(3); + + REQUIRE(buffer.size() == 2); + REQUIRE(buffer.back() == 3); + + buffer.push_back(4); + buffer.pop_front(); + + REQUIRE(buffer.size() == 2); + REQUIRE(buffer.back() == 4); + + buffer.push_back(5); + buffer.push_back(6); + + REQUIRE(buffer.size() == 4); + REQUIRE(buffer.front() == 3); + REQUIRE(buffer.back() == 6); +} + +TEST_CASE("set_capacity", "[circular_buffer]") +{ + circular_buffer buffer(4, { 1, 2, 3 }); + + REQUIRE(buffer.size() == 3); + REQUIRE(buffer.capacity() == 4); + + buffer.set_capacity(4); + + REQUIRE(buffer.size() == 3); + REQUIRE(buffer.capacity() == 4); + + REQUIRE(buffer.front() == 1); + REQUIRE(buffer.back() == 3); + + buffer.set_capacity(10); + + REQUIRE(buffer.size() == 3); + REQUIRE(buffer.capacity() == 10); + + REQUIRE(buffer.front() == 1); + REQUIRE(buffer.back() == 3); + + buffer.set_capacity(1); + + REQUIRE(buffer.size() == 1); + REQUIRE(buffer.capacity() == 1); + + REQUIRE(buffer.front() == 1); +} + +TEST_CASE("clear", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.emplace_back(1); + buffer.emplace_back(2); + buffer.emplace_back(3); + + buffer.clear(); + + REQUIRE(buffer.empty()); + REQUIRE(buffer.capacity() == 4); + + buffer.emplace_front(1); + + REQUIRE(buffer.size() == 1); +} + +TEST_CASE("reset", "[circular_buffer]") +{ + const size_t new_capacity = GENERATE(1, 3, 5); + + circular_buffer buffer(4, { 1, 2, 3 }); + + REQUIRE(buffer.size() == 3); + REQUIRE(buffer.capacity() == 4); + + buffer.reset(new_capacity); + + REQUIRE(buffer.empty()); + REQUIRE(buffer.capacity() == new_capacity); +} + +TEST_CASE("comparisons", "[circular_buffer]") +{ + circular_buffer buffer1(4); + + REQUIRE(buffer1 == circular_buffer{}); + + buffer1.push_back(1); + buffer1.push_back(2); + buffer1.push_back(3); + buffer1.push_back(4); + + circular_buffer buffer2(4); + + buffer2.push_back(0); + buffer2.push_back(1); + buffer2.push_back(2); + buffer2.push_back(3); + + REQUIRE(buffer1 != buffer2); + + buffer2.push_back(4); + + REQUIRE(buffer1 == buffer2); + + REQUIRE(buffer1 != circular_buffer{}); +} + +TEST_CASE("iterators", "[circular_buffer]") +{ + circular_buffer buffer(4); + + buffer.push_back(0); + buffer.push_back(2); + buffer.push_back(1); + buffer.push_back(3); + buffer.push_back(0); + + REQUIRE(!std::is_sorted(buffer.cbegin(), buffer.cend())); + + std::sort(buffer.begin(), buffer.end()); + + REQUIRE(std::is_sorted(buffer.rbegin(), buffer.rend(), std::greater{})); +} + +TEST_CASE("swap", "[circular_buffer]") +{ + circular_buffer buffer1(4, { 1, 2, 3 }); + circular_buffer buffer2(5, { 4, 5 }); + + REQUIRE(buffer1 != buffer2); + + REQUIRE(buffer1.size() == 3); + REQUIRE(buffer1.capacity() == 4); + + REQUIRE(buffer2.size() == 2); + REQUIRE(buffer2.capacity() == 5); + + using std::swap; + swap(buffer1, buffer2); + + REQUIRE(buffer1.size() == 2); + REQUIRE(buffer1.capacity() == 5); + + REQUIRE(buffer2.size() == 3); + REQUIRE(buffer2.capacity() == 4); + + REQUIRE(buffer1.front() == 4); + REQUIRE(buffer2.front() == 1); +} diff --git a/test/unit/metrics.cpp b/test/unit/metrics.cpp index 7dd39374..0224e730 100644 --- a/test/unit/metrics.cpp +++ b/test/unit/metrics.cpp @@ -22,6 +22,7 @@ TEMPLATE_TEST_CASE("fitness_metrics", "[metrics]", FitnessMin, FitnessMax, Fitne using Metric = TestType; BinaryGA GA{ popsize }; + GA.track(Metric{}); GA.solve(DummyFitnessFunction{ 10, num_obj }, num_gen); @@ -40,6 +41,7 @@ TEMPLATE_TEST_CASE("fitness_metrics", "[metrics]", FitnessMin, FitnessMax, Fitne TEST_CASE("nadir_point_metric", "[metrics]") { BinaryGA GA{ popsize }; + GA.track(NadirPoint{}); GA.solve(DummyFitnessFunction{ 10, num_obj }, num_gen); @@ -55,6 +57,7 @@ TEST_CASE("nadir_point_metric", "[metrics]") TEST_CASE("hypervolume_metric", "[metrics]") { BinaryGA GA{ popsize }; + GA.track(Hypervolume{ FitnessVector(num_obj, -10.0) }); GA.solve(DummyFitnessFunction{ 10, num_obj }, num_gen); @@ -67,7 +70,8 @@ TEST_CASE("hypervolume_metric", "[metrics]") TEST_CASE("hypervolume_auto", "[metrics]") { BinaryGA GA{ popsize }; - GA.track(AutoHypervolume{ }); + + GA.track(AutoHypervolume{}); GA.solve(DummyFitnessFunction{ 10, num_obj }, num_gen); const auto& metric = GA.get_metric(); @@ -79,15 +83,16 @@ TEST_CASE("hypervolume_auto", "[metrics]") TEST_CASE("fitness_evaluations", "[metrics]") { BinaryGA GA{ popsize }; + GA.track(FitnessEvaluations{}); - GA.solve(DummyFitnessFunction{ 10, num_obj }, num_gen); + GA.solve(DummyFitnessFunction{ 10, num_obj, FitnessFunctionInfo::Type::Static }, num_gen); const auto& metric1 = GA.get_metric(); REQUIRE(metric1.size() == num_gen); REQUIRE(std::all_of(metric1.begin(), metric1.end(), detail::between(0_sz, popsize))); - GA.solve(DummyFitnessFunction{ 10, num_obj, true }, num_gen); + GA.solve(DummyFitnessFunction{ 10, num_obj, FitnessFunctionInfo::Type::Dynamic }, num_gen); const auto& metric2 = GA.get_metric(); diff --git a/test/unit/test_utils.hpp b/test/unit/test_utils.hpp index ee20dff3..73ee1e46 100644 --- a/test/unit/test_utils.hpp +++ b/test/unit/test_utils.hpp @@ -14,11 +14,19 @@ template class DummyFitnessFunction final : public FitnessFunctionBase { public: - explicit DummyFitnessFunction(size_t chrom_len, size_t nobj = 1, bool dynamic = false) : - FitnessFunctionBase(chrom_len, dynamic), nobj_(nobj) {} + using FitnessFunctionInfo::Type; + + explicit DummyFitnessFunction(size_t chrom_len, size_t nobj = 1, Type type = Type::Static) : + FitnessFunctionBase(chrom_len, type), nobj_(nobj) + {} + private: - FitnessVector invoke(const Chromosome&) const override { return FitnessVector(nobj_, 0.0); } // NOLINT(*return-braced-init-list) size_t nobj_; + + FitnessVector invoke(const Chromosome&) const override + { + return FitnessVector(nobj_, 0.0); // NOLINT(*return-braced-init-list) + } }; #endif // !GA_TEST_UTILS_HPP diff --git a/tools/install.sh b/tools/install.sh index 964213c8..09a7bab9 100644 --- a/tools/install.sh +++ b/tools/install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash echo -e "Installing gapp...\n"