diff --git a/include/ioh/problem/bbob/many_affine.hpp b/include/ioh/problem/bbob/many_affine.hpp index ef55f1cde..b082c6944 100644 --- a/include/ioh/problem/bbob/many_affine.hpp +++ b/include/ioh/problem/bbob/many_affine.hpp @@ -95,6 +95,10 @@ namespace ioh::problem::bbob auto result = 0.0; for (int fi = 0; fi < 24; fi++) { + // A BBOB instance with zero weight contributes nothing to the weighted + // sum, so its evaluation can be skipped. + if (weights_[fi] == 0.0) + continue; // compute xopt shifted x std::vector x0 = x; for (size_t i = 0; i < x.size(); i++) diff --git a/tests/cpp/problem/test_bbob_affine.cpp b/tests/cpp/problem/test_bbob_affine.cpp index 5130f9a6f..7149cadc1 100644 --- a/tests/cpp/problem/test_bbob_affine.cpp +++ b/tests/cpp/problem/test_bbob_affine.cpp @@ -1,5 +1,9 @@ #include "../utils.hpp" +#include +#include +#include + #include "ioh/problem/bbob/many_affine.hpp" @@ -12,4 +16,86 @@ TEST_F(BaseTest, TestManyAffine) EXPECT_NEAR(affine(x0), 3.521076347, 1e-8); //TODO: test based on indiviudal problems -} \ No newline at end of file +} + +namespace +{ + //! True when two doubles have the same bit pattern, which is stricter than ==. + bool bitwise_equal(const double a, const double b) { return std::memcmp(&a, &b, sizeof(double)) == 0; } + + //! Reconstruct the original ManyAffine evaluation by summing all 24 BBOB instances. + //! This serves as a reference for the optimized implementation. + double evaluate_all_instances(ioh::problem::bbob::ManyAffine &affine, const std::vector &x) + { + auto problems = affine.get_problems(); + const auto weights = affine.get_weights(); + const auto scale_factors = affine.get_scale_factors(); + const auto xopt = affine.optimum().x; + + auto result = 0.0; + for (int fi = 0; fi < 24; fi++) + { + std::vector x0 = x; + for (size_t i = 0; i < x.size(); i++) + x0[i] = x[i] + problems[fi]->optimum().x[i] - xopt[i]; + + double f0 = (*problems[fi])(x0)-problems[fi]->optimum().y; + f0 = std::min(std::max(f0, 1e-12), 1e20); + f0 = (std::log10(f0) + 8) / scale_factors[fi]; + f0 = f0 * weights[fi]; + result += f0; + } + return pow(10, (10 * result - 8)); + } +} // namespace + +//! Skipping zero-weight BBOB instances must not change any objective value. +//! The reference implementation evaluates all 24 BBOB instances and is compared +//! with the optimized implementation using bitwise equality. +TEST_F(BaseTest, TestManyAffineSkipsZeroWeights) +{ + using namespace ioh::problem::bbob; + + for (const int n_variables : {2, 5, 10}) + { + for (int instance = 1; instance <= 10; instance++) + { + ManyAffine affine(instance, n_variables); + + const auto weights = affine.get_weights(); + const auto n_zero = std::count(weights.begin(), weights.end(), 0.0); + + // Ensure the test exercises the optimization target. + EXPECT_GT(n_zero, 0) << "instance " << instance << " in " << n_variables << "D has no zero weight"; + + // The optimum is computed inside the constructor, so it is covered too. + EXPECT_TRUE(bitwise_equal(affine.optimum().y, evaluate_all_instances(affine, affine.optimum().x))) + << "optimum of instance " << instance << " in " << n_variables << "D"; + + std::mt19937 gen(static_cast(instance * 100 + n_variables)); + std::uniform_real_distribution dis(-5.0, 5.0); + + std::vector> points{ + std::vector(static_cast(n_variables), 0.0), + std::vector(static_cast(n_variables), -5.0), + std::vector(static_cast(n_variables), 5.0), + }; + for (int k = 0; k < 10; k++) + { + std::vector x(static_cast(n_variables)); + for (auto &xi : x) + xi = dis(gen); + points.push_back(x); + } + + for (const auto &x : points) + { + const double expected = evaluate_all_instances(affine, x); + const double got = affine(x); + EXPECT_TRUE(bitwise_equal(expected, got)) + << "instance " << instance << " in " << n_variables << "D at x = " << format_vector(x) + << ": expected " << expected << " got " << got; + } + } + } +}