diff --git a/HISTORY.md b/HISTORY.md index a596432ad..6bbbd0d4b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,9 @@ * CheckArbitraryFunctionTypeAPI extended for MOO support ([#283](https://github.com/mlpack/ensmallen/pull/283)). + * Refactor NSGA2 + ([#263](https://github.com/mlpack/ensmallen/pull/263)). + ### ensmallen 2.16.1: "Severely Dented Can Of Polyurethane" ###### 2021-03-02 * Fix test compilation issue when `ENS_USE_OPENMP` is set diff --git a/include/ensmallen_bits/nsga2/nsga2.hpp b/include/ensmallen_bits/nsga2/nsga2.hpp index ffaaaaab9..5a315152a 100644 --- a/include/ensmallen_bits/nsga2/nsga2.hpp +++ b/include/ensmallen_bits/nsga2/nsga2.hpp @@ -188,7 +188,7 @@ class NSGA2 { typename std::enable_if::type EvaluateObjectives(std::vector&, std::tuple&, - std::vector >&); + std::vector >&); template::type EvaluateObjectives(std::vector& population, std::tuple& objectives, - std::vector >& calculatedObjectives); + std::vector >& + calculatedObjectives); /** * Reproduce candidates from the elite population to generate a new @@ -281,11 +282,15 @@ class NSGA2 { * Assigns crowding distance metric for sorting. * * @param front The previously generated Pareto fronts. - * @param objectives The set of objectives. - * @param crowdingDistance The previously calculated objectives. + * @param calculatedObjectives The previously calculated objectives. + * @param crowdingDistance The crowding distance for each individual in + * the population. */ - void CrowdingDistanceAssignment(const std::vector& front, - std::vector& crowdingDistance); + template + void CrowdingDistanceAssignment( + const std::vector& front, + std::vector>& calculatedObjectives, + std::vector& crowdingDistance); /** * The operator used in the crowding distance based sorting. @@ -299,13 +304,15 @@ class NSGA2 { * @param idxQ The index of the second cadidate from the elite population * being sorted. * @param ranks The previously calculated ranks. - * @param crowdingDistance The previously calculated objectives. + * @param crowdingDistance The crowding distance for each individual in + * the population. * @return true if the first candidate is preferred, otherwise, false. */ + template bool CrowdingOperator(size_t idxP, size_t idxQ, const std::vector& ranks, - const std::vector& crowdingDistance); + const std::vector& crowdingDistance); //! The number of objectives being optimised for. size_t numObjectives; diff --git a/include/ensmallen_bits/nsga2/nsga2_impl.hpp b/include/ensmallen_bits/nsga2/nsga2_impl.hpp index 9b083be3c..2c36758c5 100644 --- a/include/ensmallen_bits/nsga2/nsga2_impl.hpp +++ b/include/ensmallen_bits/nsga2/nsga2_impl.hpp @@ -27,6 +27,8 @@ inline NSGA2::NSGA2(const size_t populationSize, const double epsilon, const arma::vec& lowerBound, const arma::vec& upperBound) : + numObjectives(0), + numVariables(0), populationSize(populationSize), maxGenerations(maxGenerations), crossoverProb(crossoverProb), @@ -45,6 +47,8 @@ inline NSGA2::NSGA2(const size_t populationSize, const double epsilon, const double lowerBound, const double upperBound) : + numObjectives(0), + numVariables(0), populationSize(populationSize), maxGenerations(maxGenerations), crossoverProb(crossoverProb), @@ -61,7 +65,7 @@ template typename MatType::elem_type NSGA2::Optimize( std::tuple& objectives, - MatType& iterate, + MatType& iterateIn, CallbackTypes&&... callbacks) { // Make sure for evolution to work at least four candidates are present. @@ -71,6 +75,17 @@ typename MatType::elem_type NSGA2::Optimize( " least 4, and, a multiple of 4!"); } + // Convenience typedefs. + typedef typename MatType::elem_type ElemType; + typedef typename MatTypeTraits::BaseMatType BaseMatType; + + BaseMatType& iterate = (BaseMatType&) iterateIn; + + // Make sure that we have the methods that we need. Long name... + traits::CheckArbitraryFunctionTypeAPI(); + RequireDenseFloatingPointType(); + // Check if lower bound is a vector of a single dimension. if (lowerBound.n_rows == 1) lowerBound = lowerBound(0, 0) * arma::ones(iterate.n_rows, iterate.n_cols); @@ -85,9 +100,6 @@ typename MatType::elem_type NSGA2::Optimize( assert(upperBound.n_rows == iterate.n_rows && "The dimensions of " "upperBound are not the same as the dimensions of iterate."); - // Convenience typedefs. - typedef typename MatType::elem_type ElemType; - numObjectives = sizeof...(ArbitraryFunctionType); numVariables = iterate.n_rows; @@ -98,13 +110,14 @@ typename MatType::elem_type NSGA2::Optimize( // Population size reserved to 2 * populationSize + 1 to accommodate // for the size of intermediate candidate population. - std::vector population; + std::vector population; population.reserve(2 * populationSize + 1); // Pareto fronts, initialized during non-dominated sorting. + // Stores indices of population belonging to a certain front. std::vector > fronts; // Initialised in CrowdingDistanceAssignment. - std::vector crowdingDistance; + std::vector crowdingDistance; // Initialised during non-dominated sorting. std::vector ranks; @@ -115,8 +128,17 @@ typename MatType::elem_type NSGA2::Optimize( // starting point. for (size_t i = 0; i < populationSize; i++) { - population.push_back(arma::randu(iterate.n_rows, + population.push_back(arma::randu(iterate.n_rows, iterate.n_cols) - 0.5 + iterate); + + // Constrain all genes to be between bounds. + for (size_t geneIdx = 0; geneIdx < numVariables; geneIdx++) + { + if (population[i](geneIdx) < lowerBound(geneIdx)) + population[i](geneIdx) = lowerBound(geneIdx); + else if (population[i](geneIdx) > upperBound(geneIdx)) + population[i](geneIdx) = upperBound(geneIdx); + } } Info << "NSGA2 initialized successfully. Optimization started." << std::endl; @@ -146,56 +168,63 @@ typename MatType::elem_type NSGA2::Optimize( // Perform fast non dominated sort on P_t ∪ G_t. ranks.resize(population.size()); - FastNonDominatedSort(fronts, ranks, calculatedObjectives); + FastNonDominatedSort(fronts, ranks, calculatedObjectives); // Perform crowding distance assignment. crowdingDistance.resize(population.size()); - + std::fill(crowdingDistance.begin(), crowdingDistance.end(), 0.); for (size_t fNum = 0; fNum < fronts.size(); fNum++) { - CrowdingDistanceAssignment(fronts[fNum], crowdingDistance); + CrowdingDistanceAssignment( + fronts[fNum], calculatedObjectives, crowdingDistance); } // Sort based on crowding distance. std::sort(population.begin(), population.end(), - [this, ranks, crowdingDistance, population](MatType candidateP, - MatType candidateQ) - { - size_t idxP, idxQ; - for (size_t i = 0; i < population.size(); i++) - { - if (arma::approx_equal(population[i], candidateP, "absdiff", epsilon)) - idxP = i; - - if (arma::approx_equal(population[i], candidateQ, "absdiff", epsilon)) - idxQ = i; - } - - return CrowdingOperator(idxP, idxQ, ranks, crowdingDistance); - } + [this, ranks, crowdingDistance, population] + (BaseMatType candidateP, BaseMatType candidateQ) + { + size_t idxP{}, idxQ{}; + for (size_t i = 0; i < population.size(); i++) + { + if (arma::approx_equal(population[i], candidateP, "absdiff", epsilon)) + idxP = i; + + if (arma::approx_equal(population[i], candidateQ, "absdiff", epsilon)) + idxQ = i; + } + + return CrowdingOperator(idxP, idxQ, ranks, crowdingDistance); + } ); // Yield a new population P_{t+1} of size populationSize. + // Discards unfit population from the R_{t} to yield P_{t+1}. population.resize(populationSize); } // Set the candidates from the best front as the output. - std::vector front; + std::vector front; for (size_t f: fronts[0]) front.push_back(population[f]); + bestFront.resize(front.size()); // bestFront is stored, can be obtained by the Front() getter. - bestFront = front; + std::transform(front.begin(), front.end(), bestFront.begin(), + [&](const BaseMatType& individual) + { + return arma::conv_to::from(individual); + }); // Assign iterate to first element of the best front. - iterate = bestFront[0]; + iterate = front[0]; Callback::EndOptimization(*this, objectives, iterate, callbacks...); ElemType performance = std::numeric_limits::max(); - for(arma::Col objective: calculatedObjectives) + for (const arma::Col& objective: calculatedObjectives) if (arma::accu(objective) < performance) performance = arma::accu(objective); @@ -210,7 +239,7 @@ typename std::enable_if::type NSGA2::EvaluateObjectives( std::vector&, std::tuple&, - std::vector >&) + std::vector >&) { // Nothing to do here. } @@ -223,7 +252,7 @@ typename std::enable_if::type NSGA2::EvaluateObjectives( std::vector& population, std::tuple& objectives, - std::vector >& calculatedObjectives) + std::vector >& calculatedObjectives) { for (size_t i = 0; i < populationSize; i++) { @@ -344,7 +373,7 @@ inline void NSGA2::FastNonDominatedSort( size_t i = 0; - while (fronts[i].size() > 0) + while (!fronts[i].empty()) { std::vector nextFront; @@ -365,6 +394,8 @@ inline void NSGA2::FastNonDominatedSort( i++; fronts.push_back(nextFront); } + // Remove the empty final set. + fronts.pop_back(); } //! Check if a candidate Pareto dominates another candidate. @@ -393,37 +424,59 @@ inline bool NSGA2::Dominates( } //! Assign crowding distance to the population. -inline void NSGA2::CrowdingDistanceAssignment(const std::vector& front, - std::vector& crowdingDistance) +template +inline void NSGA2::CrowdingDistanceAssignment( + const std::vector& front, + std::vector>& calculatedObjectives, + std::vector& crowdingDistance) { - if (front.size() > 0) - { - for (size_t elem: front) - crowdingDistance[elem] = 0; + // Convenience typedefs. + typedef typename MatType::elem_type ElemType; - size_t fSize = front.size(); + size_t fSize = front.size(); + // Stores the sorted indices of the fronts. + arma::uvec sortedIdx = arma::regspace(0, 1, fSize - 1); - for (size_t m = 0; m < numObjectives; m++) - { - crowdingDistance[front[0]] = std::numeric_limits::max(); - crowdingDistance[front[fSize - 1]] = std::numeric_limits::max(); + for (size_t m = 0; m < numObjectives; m++) + { + // Cache fValues of individuals for current objective. + arma::Col fValues(fSize); + std::transform(front.begin(), front.end(), fValues.begin(), + [&](const size_t& individual) + { + return calculatedObjectives[individual](m); + }); - for (size_t i = 1; i < fSize - 1 ; i++) - { - crowdingDistance[front[i]] += (crowdingDistance[front[i - 1]] - - crowdingDistance[front[i + 1]]) / - (std::numeric_limits::max() - - std::numeric_limits::min()); - } + // Sort front indices by ascending fValues for current objective. + std::sort(sortedIdx.begin(), sortedIdx.end(), + [&](const size_t& frontIdxA, const size_t& frontIdxB) + { + return (fValues(frontIdxA) < fValues(frontIdxB)); + }); + + crowdingDistance[front[sortedIdx(0)]] = + std::numeric_limits::max(); + crowdingDistance[front[sortedIdx(fSize - 1)]] = + std::numeric_limits::max(); + ElemType minFval = fValues(sortedIdx(0)); + ElemType maxFval = fValues(sortedIdx(fSize - 1)); + ElemType scale = + std::abs(maxFval - minFval) == 0. ? 1. : std::abs(maxFval - minFval); + + for (size_t i = 1; i < fSize - 1; i++) + { + crowdingDistance[front[sortedIdx(i)]] += + (fValues(sortedIdx(i + 1)) - fValues(sortedIdx(i - 1))) / scale; } } } //! Comparator for crowding distance based sorting. +template inline bool NSGA2::CrowdingOperator(size_t idxP, size_t idxQ, const std::vector& ranks, - const std::vector& crowdingDistance) + const std::vector& crowdingDistance) { if (ranks[idxP] < ranks[idxQ]) return true; diff --git a/tests/nsga2_test.cpp b/tests/nsga2_test.cpp index e1737b1af..b0e301b44 100644 --- a/tests/nsga2_test.cpp +++ b/tests/nsga2_test.cpp @@ -22,24 +22,30 @@ using namespace std; * @param value The value being checked. * @param low The lower bound. * @param high The upper bound. + * @tparam The type of elements in the population set. * @return true if value lies in the range [low, high]. * @return false if value does not lie in the range [low, high]. */ -bool IsInBounds(const double& value, const double& low, const double& high) +template +bool IsInBounds(const ElemType& value, const ElemType& low, const ElemType& high) { - return !(value < low) && !(high < value); + ElemType roundoff = 0.1; + return !(value < (low - roundoff)) && !((high + roundoff) < value); } /** * Optimize for the Schaffer N.1 function using NSGA-II optimizer. + * Tests for data of type double. */ -TEST_CASE("NSGA2SchafferN1Test", "[NSGA2Test]") +TEST_CASE("NSGA2SchafferN1DoubleTest", "[NSGA2Test]") { SchafferFunctionN1 SCH; const double lowerBound = -1000; const double upperBound = 1000; + const double expectedLowerBound = 0.0; + const double expectedUpperBound = 2.0; - NSGA2 opt(20, 5000, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); + NSGA2 opt(20, 300, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); typedef decltype(SCH.objectiveA) ObjectiveTypeA; typedef decltype(SCH.objectiveB) ObjectiveTypeB; @@ -60,7 +66,7 @@ TEST_CASE("NSGA2SchafferN1Test", "[NSGA2Test]") { double val = arma::as_scalar(solution); - if (val < 0.0 || val > 2.0) + if (!IsInBounds(val, expectedLowerBound, expectedUpperBound)) { allInRange = false; break; @@ -79,15 +85,18 @@ TEST_CASE("NSGA2SchafferN1Test", "[NSGA2Test]") /** * Optimize for the Schaffer N.1 function using NSGA-II optimizer. + * Tests for data of type double. */ -TEST_CASE("NSGA2SchafferN1TestVectorBounds", "[NSGA2Test]") +TEST_CASE("NSGA2SchafferN1TestVectorDoubleBounds", "[NSGA2Test]") { // This test can be a little flaky, so we try it a few times. SchafferFunctionN1 SCH; const arma::vec lowerBound = {-1000}; const arma::vec upperBound = {1000}; + const double expectedLowerBound = 0.0; + const double expectedUpperBound = 2.0; - NSGA2 opt(20, 5000, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); + NSGA2 opt(20, 300, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); typedef decltype(SCH.objectiveA) ObjectiveTypeA; typedef decltype(SCH.objectiveB) ObjectiveTypeB; @@ -107,7 +116,7 @@ TEST_CASE("NSGA2SchafferN1TestVectorBounds", "[NSGA2Test]") { double val = arma::as_scalar(solution); - if (val < 0.0 || val > 2.0) + if (!IsInBounds(val, expectedLowerBound, expectedUpperBound)) { allInRange = false; break; @@ -126,8 +135,9 @@ TEST_CASE("NSGA2SchafferN1TestVectorBounds", "[NSGA2Test]") /** * Optimize for the Fonseca Fleming function using NSGA-II optimizer. + * Tests for data of type double. */ -TEST_CASE("NSGA2FonsecaFlemingTest", "[NSGA2Test]") +TEST_CASE("NSGA2FonsecaFlemingDoubleTest", "[NSGA2Test]") { FonsecaFlemingFunction FON; const double lowerBound = -4; @@ -137,7 +147,7 @@ TEST_CASE("NSGA2FonsecaFlemingTest", "[NSGA2Test]") const double expectedLowerBound = -1.0 / sqrt(3); const double expectedUpperBound = 1.0 / sqrt(3); - NSGA2 opt(20, 4000, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); + NSGA2 opt(20, 300, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); typedef decltype(FON.objectiveA) ObjectiveTypeA; typedef decltype(FON.objectiveB) ObjectiveTypeB; @@ -157,21 +167,23 @@ TEST_CASE("NSGA2FonsecaFlemingTest", "[NSGA2Test]") double valY = arma::as_scalar(solution(1)); double valZ = arma::as_scalar(solution(2)); - if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || - !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || - !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) + if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) { allInRange = false; break; } } + REQUIRE(allInRange); } /** * Optimize for the Fonseca Fleming function using NSGA-II optimizer. + * Tests for data of type double. */ -TEST_CASE("NSGA2FonsecaFlemingTestVectorBounds", "[NSGA2Test]") +TEST_CASE("NSGA2FonsecaFlemingTestVectorDoubleBounds", "[NSGA2Test]") { FonsecaFlemingFunction FON; const arma::vec lowerBound = {-4, -4, -4}; @@ -181,7 +193,7 @@ TEST_CASE("NSGA2FonsecaFlemingTestVectorBounds", "[NSGA2Test]") const double expectedLowerBound = -1.0 / sqrt(3); const double expectedUpperBound = 1.0 / sqrt(3); - NSGA2 opt(20, 4000, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); + NSGA2 opt(20, 300, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); typedef decltype(FON.objectiveA) ObjectiveTypeA; typedef decltype(FON.objectiveB) ObjectiveTypeB; @@ -201,13 +213,226 @@ TEST_CASE("NSGA2FonsecaFlemingTestVectorBounds", "[NSGA2Test]") double valY = arma::as_scalar(solution(1)); double valZ = arma::as_scalar(solution(2)); - if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || - !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || - !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) + if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) { allInRange = false; break; } } + REQUIRE(allInRange); } + +/** + * @brief Convert the individuals from pareto front to requested data type. + * + * @tparam MatType The type to be casted to. + * @param bestFront Vector containing individuals from the pareto front. + * @return std::vector The casted individuals. + */ +template +std::vector castFront(const std::vector& bestFront) +{ + std::vector castedFront(bestFront.size()); + std::transform(bestFront.begin(), bestFront.end(), castedFront.begin(), + [&](const arma::mat& individual) + { + return arma::conv_to::from(individual); + }); + + return castedFront; +} + +/** + * Optimize for the Schaffer N.1 function using NSGA-II optimizer. + * Tests for data of type float. + */ +TEST_CASE("NSGA2SchafferN1FloatTest", "[NSGA2Test]") +{ + SchafferFunctionN1 SCH; + const double lowerBound = -1000; + const double upperBound = 1000; + const double expectedLowerBound = 0.0; + const double expectedUpperBound = 2.0; + + NSGA2 opt(20, 300, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); + + typedef decltype(SCH.objectiveA) ObjectiveTypeA; + typedef decltype(SCH.objectiveB) ObjectiveTypeB; + + // We allow a few trials in case of poor convergence. + bool success = false; + for (size_t trial = 0; trial < 3; ++trial) + { + arma::fmat coords = SCH.GetInitialPoint(); + std::tuple objectives = SCH.GetObjectives(); + + opt.Optimize(objectives, coords); + std::vector bestFront = castFront(opt.Front()); + + bool allInRange = true; + + for (arma::fmat solution: bestFront) + { + float val = arma::as_scalar(solution); + + if (!IsInBounds(val, expectedLowerBound, expectedUpperBound)) + { + allInRange = false; + break; + } + } + + if (allInRange) + { + success = true; + break; + } + } + + REQUIRE(success == true); +} + +/** + * Optimize for the Schaffer N.1 function using NSGA-II optimizer. + * Tests for data of type float. + */ +TEST_CASE("NSGA2SchafferN1TestVectorFloatBounds", "[NSGA2Test]") +{ + // This test can be a little flaky, so we try it a few times. + SchafferFunctionN1 SCH; + const arma::vec lowerBound = {-1000}; + const arma::vec upperBound = {1000}; + const double expectedLowerBound = 0.0; + const double expectedUpperBound = 2.0; + + NSGA2 opt(20, 300, 0.5, 0.5, 1e-3, 1e-6, lowerBound, upperBound); + + typedef decltype(SCH.objectiveA) ObjectiveTypeA; + typedef decltype(SCH.objectiveB) ObjectiveTypeB; + + bool success = false; + for (size_t trial = 0; trial < 3; ++trial) + { + arma::fmat coords = SCH.GetInitialPoint(); + std::tuple objectives = SCH.GetObjectives(); + + opt.Optimize(objectives, coords); + std::vector bestFront = castFront(opt.Front()); + + bool allInRange = true; + + for (arma::fmat solution: bestFront) + { + float val = arma::as_scalar(solution); + + if (!IsInBounds(val, expectedLowerBound, expectedUpperBound)) + { + allInRange = false; + break; + } + } + + if (allInRange) + { + success = true; + break; + } + } + + REQUIRE(success == true); +} + +/** + * Optimize for the Fonseca Fleming function using NSGA-II optimizer. + * Tests for data of type float. + */ +TEST_CASE("NSGA2FonsecaFlemingFloatTest", "[NSGA2Test]") +{ + FonsecaFlemingFunction FON; + const double lowerBound = -4; + const double upperBound = 4; + const double tolerance = 1e-6; + const double strength = 1e-4; + const float expectedLowerBound = -1.0 / sqrt(3); + const float expectedUpperBound = 1.0 / sqrt(3); + + NSGA2 opt(20, 300, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); + + typedef decltype(FON.objectiveA) ObjectiveTypeA; + typedef decltype(FON.objectiveB) ObjectiveTypeB; + + arma::fmat coords = FON.GetInitialPoint(); + std::tuple objectives = FON.GetObjectives(); + + opt.Optimize(objectives, coords); + std::vector bestFront = castFront(opt.Front()); + + bool allInRange = true; + + for (size_t i = 0; i < bestFront.size(); i++) + { + const arma::fmat solution = bestFront[i]; + float valX = arma::as_scalar(solution(0)); + float valY = arma::as_scalar(solution(1)); + float valZ = arma::as_scalar(solution(2)); + + if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) + { + allInRange = false; + break; + } + } + + REQUIRE(allInRange); +} + +/** + * Optimize for the Fonseca Fleming function using NSGA-II optimizer. + * Tests for data of type float. + */ +TEST_CASE("NSGA2FonsecaFlemingTestVectorFloatBounds", "[NSGA2Test]") +{ + FonsecaFlemingFunction FON; + const arma::vec lowerBound = {-4, -4, -4}; + const arma::vec upperBound = {4, 4, 4}; + const double tolerance = 1e-6; + const double strength = 1e-4; + const float expectedLowerBound = -1.0 / sqrt(3); + const float expectedUpperBound = 1.0 / sqrt(3); + + NSGA2 opt(20, 300, 0.6, 0.3, strength, tolerance, lowerBound, upperBound); + + typedef decltype(FON.objectiveA) ObjectiveTypeA; + typedef decltype(FON.objectiveB) ObjectiveTypeB; + + arma::fmat coords = FON.GetInitialPoint(); + std::tuple objectives = FON.GetObjectives(); + + opt.Optimize(objectives, coords); + std::vector bestFront = castFront(opt.Front()); + + bool allInRange = true; + + for (size_t i = 0; i < bestFront.size(); i++) + { + const arma::fmat solution = bestFront[i]; + float valX = arma::as_scalar(solution(0)); + float valY = arma::as_scalar(solution(1)); + float valZ = arma::as_scalar(solution(2)); + + if (!IsInBounds(valX, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valY, expectedLowerBound, expectedUpperBound) || + !IsInBounds(valZ, expectedLowerBound, expectedUpperBound)) + { + allInRange = false; + break; + } + } + + REQUIRE(allInRange); +} \ No newline at end of file