diff --git a/HISTORY.md b/HISTORY.md
index 024f113a1..cc6fc7041 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,9 @@
* Make Callback flexible for MultiObjective Optimizers
([#289](https://github.com/mlpack/ensmallen/pull/289)).
+ * Add MOEA-D/DE Optimizer
+ ([#269](https://github.com/mlpack/ensmallen/pull/269)).
+
### 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/doc/function_types.md b/doc/function_types.md
index 4cf4dcc1b..28139cce2 100644
--- a/doc/function_types.md
+++ b/doc/function_types.md
@@ -888,6 +888,7 @@ front.
The following optimizers can be used with multi-objective functions:
- [NSGA2](#nsga2)
+- [MOEA/D-DE](#moead)
## Constrained functions
diff --git a/doc/optimizers.md b/doc/optimizers.md
index 2317a92c1..103ff855d 100644
--- a/doc/optimizers.md
+++ b/doc/optimizers.md
@@ -1563,6 +1563,66 @@ optimizer.Optimize(f, coordinates);
* [SGD in Wikipedia](https://en.wikipedia.org/wiki/Stochastic_gradient_descent)
* [Differentiable separable functions](#differentiable-separable-functions)
+## MOEA/D-DE
+*An optimizer for arbitrary multi-objective functions.*
+MOEA/D-DE (Multi Objective Evolutionary Algorithm based on Decomposition - Differential Evolution) is a multi
+objective optimization algorithm. It works by decomposing the problem into a number of scalar optimization
+subproblems which are solved simultaneously per generation. MOEA/D in itself is a framework, this particular
+algorithm uses Differential Crossover followed by Polynomial Mutation to create offsprings which are then
+decomposed to form a Single Objective Problem. A diversity preserving mechanism is also employed which encourages
+a varied set of solution.
+
+#### Constructors
+* `MOEAD()`
+* `MOEAD(`_`populationSize, maxGenerations, crossoverProb, neighborProb, neighborSize, distributionIndex, differentialWeight, maxReplace, epsilon, lowerBound, upperBound`_`)`
+
+#### Attributes
+
+| **type** | **name** | **description** | **default** |
+|----------|----------|-----------------|-------------|
+| `size_t` | **`populationSize`** | The number of candidates in the population. | `150` |
+| `size_t` | **`maxGenerations`** | The maximum number of generations allowed. | `300` |
+| `double` | **`crossoverProb`** | Probability that a crossover will occur. | `1.0` |
+| `double` | **`neighborProb`** | The probability of sampling from neighbor. | `0.9` |
+| `size_t` | **`neighborSize`** | The number of nearest-neighbours to consider per weight vector. | `20` |
+| `double` | **`distributionIndex`** | The crowding degree of the mutation. | `20` |
+| `double` | **`differentialWeight`** | Amplification factor of the differentiation. | `0.5` |
+| `size_t` | **`maxReplace`** | The limit of solutions allowed to be replaced by a child. | `2`|
+| `double` | **`epsilon`** | Handles numerical stability after weight initialization. | `1E-10`|
+| `double`, `arma::vec` | **`lowerBound`** | Lower bound of the coordinates on the coordinates of the whole population during the search process. | `0` |
+| `double`, `arma::vec` | **`upperBound`** | Lower bound of the coordinates on the coordinates of the whole population during the search process. | `1` |
+
+Attributes of the optimizer may also be changed via the member methods
+`PopulationSize()`, `MaxGenerations()`, `CrossoverRate()`, `NeighborProb()`, `NeighborSize()`, `DistributionIndex()`,
+`DifferentialWeight()`, `MaxReplace()`, `Epsilon()`, `LowerBound()` and `UpperBound()`.
+
+#### Examples:
+
+
+Click to collapse/expand example code.
+
+
+```c++
+SchafferFunctionN1 SCH;
+arma::vec lowerBound("-10 -10");
+arma::vec upperBound("10 10");
+MOEAD opt(150, 300, 1.0, 0.9, 20, 20, 0.5, 2, 1E-10, lowerBound, upperBound);
+typedef decltype(SCH.objectiveA) ObjectiveTypeA;
+typedef decltype(SCH.objectiveB) ObjectiveTypeB;
+arma::mat coords = SCH.GetInitialPoint();
+std::tuple objectives = SCH.GetObjectives();
+// obj will contain the minimum sum of objectiveA and objectiveB found on the best front.
+double obj = opt.Optimize(objectives, coords);
+// Now obtain the best front.
+arma::cube bestFront = opt.ParetoFront();
+```
+
+
+#### See also
+* [MOEA/D-DE Algorithm](https://ieeexplore.ieee.org/document/4633340)
+* [Multi-objective Functions in Wikipedia](https://en.wikipedia.org/wiki/Test_functions_for_optimization#Test_functions_for_multi-objective_optimization)
+* [Multi-objective functions](#multi-objective-functions)
+
## NSGA2
*An optimizer for arbitrary multi-objective functions.*
@@ -1619,7 +1679,7 @@ std::tuple objectives = SCH.GetObjectives();
// obj will contain the minimum sum of objectiveA and objectiveB found on the best front.
double obj = opt.Optimize(objectives, coords);
// Now obtain the best front.
-std::vector bestFront = opt.Front();
+arma::cube bestFront = opt.Front();
```
diff --git a/include/ensmallen.hpp b/include/ensmallen.hpp
index 75316514a..0f6520708 100644
--- a/include/ensmallen.hpp
+++ b/include/ensmallen.hpp
@@ -103,6 +103,7 @@
#include "ensmallen_bits/katyusha/katyusha.hpp"
#include "ensmallen_bits/lbfgs/lbfgs.hpp"
#include "ensmallen_bits/lookahead/lookahead.hpp"
+#include "ensmallen_bits/moead/moead.hpp"
#include "ensmallen_bits/nsga2/nsga2.hpp"
#include "ensmallen_bits/padam/padam.hpp"
#include "ensmallen_bits/parallel_sgd/parallel_sgd.hpp"
diff --git a/include/ensmallen_bits/moead/moead.hpp b/include/ensmallen_bits/moead/moead.hpp
new file mode 100644
index 000000000..9fa34cc09
--- /dev/null
+++ b/include/ensmallen_bits/moead/moead.hpp
@@ -0,0 +1,326 @@
+/**
+ * @file moead.hpp
+ * @author Utkarsh Rai
+ * @author Nanubala Gnana Sai
+ *
+ * MOEA/D, Multi Objective Evolutionary Algorithm based on Decompositon is a
+ * multi objective optimization algorithm. It employs evolutionary algorithms,
+ * to find better solutions by iterating on the previous solutions and
+ * decomposition approaches, to convert the multi objective problem to a single
+ * objective one, to find the best Pareto Front for the given problem.
+ *
+ * ensmallen is free software; you may redistribute it and/or modify it under
+ * the terms of the 3-clause BSD license. You should have received a copy of
+ * the 3-clause BSD license along with ensmallen. If not, see
+ * http://www.opensource.org/licenses/BSD-3-Clause for more information.
+ */
+
+#ifndef ENSMALLEN_MOEAD_MOEAD_HPP
+#define ENSMALLEN_MOEAD_MOEAD_HPP
+
+namespace ens {
+
+/**
+ * This class implements the MOEA/D algorithm with Differential Evolution
+ * crossover. Step numbers used in different parts of the implementation
+ * correspond to the step number used in the original algorithm by the author.
+ *
+ * For more information, see the following:
+ * @code
+ * @article{article,
+ * author = {Zhang, Qingfu and Li, Hui},
+ * year = {2008},
+ * pages = {712 - 731},
+ * title = {MOEA/D: A Multiobjective Evolutionary Algorithm Based on
+ * Decomposition},
+ * journal = {Evolutionary Computation, IEEE Transactions on},
+ *
+ * @article{4633340,
+ * author={H. {Li} and Q. {Zhang}},
+ * year={2009},
+ * pages={284-302},}
+ * title={Multiobjective Optimization Problems With Complicated Pareto Sets, MOEA/D and NSGA-II},
+ * journal={IEEE Transactions on Evolutionary Computation},
+ * @endcode
+ *
+ * MOEA/D can optimize arbitrary multi-objective functions. For more details,
+ * see the documentation on function types included with this distribution or
+ * on the ensmallen website.
+ */
+class MOEAD {
+ public:
+ /**
+ * Constructor for the MOEA/D optimizer.
+ *
+ * The default values provided here are not necessarily suitable for a
+ * given function. Therefore, it is highly recommended to adjust the
+ * parameters according to the problem.
+ *
+ * @param populationSize The number of elements in the population.
+ * @param maxGenerations The maximum number of generations allowed.
+ * @param crossoverProb The probability that a crossover will occur.
+ * @param neighborProb The probability of sampling from neighbor.
+ * @param neighborSize The number of nearest neighbours of weights
+ * to find.
+ * @param distributionIndex The crowding degree of the mutation.
+ * @param differentialWeight A parameter used in the mutation of candidate
+ * solutions controls amplification factor of the differentiation.
+ * @param maxReplace The limit of solutions allowed to be replaced by a child.
+ * @param epsilon Handle numerical stability after weight initialization.
+ * @param lowerBound The lower bound on each variable of a member
+ * of the variable space.
+ * @param upperBound The upper bound on each variable of a member
+ * of the variable space.
+ */
+ MOEAD(const size_t populationSize = 150,
+ const size_t maxGenerations = 300,
+ const double crossoverProb = 1.0,
+ const double neighborProb = 0.9,
+ const size_t neighborSize = 20,
+ const double distributionIndex = 20,
+ const double differentialWeight = 0.5,
+ const size_t maxReplace = 2,
+ const double epsilon = 1E-10,
+ const arma::vec& lowerBound = arma::zeros(1, 1),
+ const arma::vec& upperBound = arma::ones(1, 1));
+
+ /**
+ * Constructor for the MOEA/D optimizer. This constructor is provides an
+ * overload to use lowerBound and upperBound as doubles, in case all the
+ * variables in the problem have the same limits.
+ *
+ * The default values provided here are not necessarily suitable for a
+ * given function. Therefore, it is highly recommended to adjust the
+ * parameters according to the problem.
+ *
+ * @param populationSize The number of elements in the population.
+ * @param maxGenerations The maximum number of generations allowed.
+ * @param crossoverProb The probability that a crossover will occur.
+ * @param neighborProb The probability of sampling from neighbor.
+ * @param neighborSize The number of nearest neighbours of weights
+ * to find.
+ * @param distributionIndex The crowding degree of the mutation.
+ * @param differentialWeight A parameter used in the mutation of candidate
+ * solutions controls amplification factor of the differentiation.
+ * @param maxReplace The limit of solutions allowed to be replaced by a child.
+ * @param epsilon Handle numerical stability after weight initialization.
+ * @param lowerBound The lower bound on each variable of a member
+ * of the variable space.
+ * @param upperBound The upper bound on each variable of a member
+ * of the variable space.
+ */
+ MOEAD(const size_t populationSize = 150,
+ const size_t maxGenerations = 300,
+ const double crossoverProb = 1.0,
+ const double neighborProb = 0.9,
+ const size_t neighborSize = 20,
+ const double distributionIndex = 20,
+ const double differentialWeight = 0.5,
+ const size_t maxReplace = 2,
+ const double epsilon = 1E-10,
+ const double lowerBound = 0,
+ const double upperBound = 1);
+
+ /**
+ * Optimize a set of objectives. The initial population is generated
+ * using the initial point. The output is the best generated front.
+ *
+ * @tparam MatType The type of matrix used to store coordinates.
+ * @tparam ArbitraryFunctionType The type of objective function.
+ * @tparam CallbackTypes Types of callback function.
+ * @param objectives std::tuple of the objective functions.
+ * @param iterate The initial reference point for generating population.
+ * @param callbacks The callback functions.
+ */
+ template
+ typename MatType::elem_type Optimize(std::tuple& objectives,
+ MatType& iterate,
+ CallbackTypes&&... callbacks);
+
+ //! Retrieve population size.
+ size_t PopulationSize() const { return populationSize; }
+ //! Modify the population size.
+ size_t& PopulationSize() { return populationSize; }
+
+ //! Retrieve number of generations.
+ size_t MaxGenerations() const { return maxGenerations; }
+ //! Modify the number of generations.
+ size_t& MaxGenerations() { return maxGenerations; }
+
+ //! Retrieve crossover rate.
+ double CrossoverRate() const { return crossoverProb; }
+ //! Modify the crossover rate.
+ double& CrossoverRate() { return crossoverProb; }
+
+ //! Retrieve size of the weight neighbor.
+ size_t NeighborSize() const { return neighborSize; }
+ //! Modify the size of the weight neighbor.
+ size_t& NeighborSize() { return neighborSize; }
+
+ //! Retrieve value of the distribution index.
+ double DistributionIndex() const { return distributionIndex; }
+ //! Modify the value of the distribution index.
+ double& DistributionIndex() { return distributionIndex; }
+
+ //! Retrieve value of neighbor probability.
+ double NeighborProb() const { return neighborProb; }
+ //! Modify the value of neigbourhood probability.
+ double& NeighborProb() { return neighborProb; }
+
+ //! Retrieve value of scaling factor.
+ double DifferentialWeight() const { return differentialWeight; }
+ //! Modify the value of scaling factor.
+ double& DifferentialWeight() { return differentialWeight; }
+
+ //! Retrieve value of maxReplace.
+ size_t MaxReplace() const { return maxReplace; }
+ //! Modify value of maxReplace.
+ size_t& MaxReplace() { return maxReplace; }
+
+ //! Retrieve value of epsilon.
+ double Epsilon() const { return epsilon; }
+ //! Modify value of maxReplace.
+ double& Epsilon() { return epsilon; }
+
+ //! Retrieve value of lowerBound.
+ const arma::vec& LowerBound() const { return lowerBound; }
+ //! Modify value of lowerBound.
+ arma::vec& LowerBound() { return lowerBound; }
+
+ //! Retrieve value of upperBound.
+ const arma::vec& UpperBound() const { return upperBound; }
+ //! Modify value of upperBound.
+ arma::vec& UpperBound() { return upperBound; }
+
+ //! Retrieve the Pareto optimal points in variable space. This returns an empty cube
+ //! until `Optimize()` has been called.
+ const arma::cube& ParetoSet() const { return paretoSet; }
+
+ //! Retrieve the best front (the Pareto frontier). This returns an empty cube until
+ //! `Optimize()` has been called.
+ const arma::cube& ParetoFront() const { return paretoFront; }
+
+
+ private:
+ /**
+ * @brief Randomly selects two members from the population.
+ *
+ * @param subProblemIdx Index of the current subproblem.
+ * @param neighborSize A matrix containing indices of the neighbors.
+ * @return std::tuple The chosen pair of indices.
+ */
+ std::tuple Mating(size_t subProblemIdx,
+ const arma::umat& neighborSize,
+ bool sampleNeighbor);
+
+ /**
+ * Mutate the child formed by the crossover of two random members of the
+ * population. Uses polynomial mutation.
+ *
+ * @tparam MatType The type of matrix used to store coordinates.
+ * @param child The candidate to be mutated.
+ * @param mutationRate The probability of mutation.
+ * @param lowerBound The lower bound on each variable in the matrix.
+ * @param upperBound The upper bound on each variable in the matrix.
+ * @return The mutated child.
+ */
+ template
+ void Mutate(MatType& child,
+ double mutationRate,
+ const MatType& lowerBound,
+ const MatType& upperBound);
+
+ /**
+ * Decompose the multi objective problem to a single objective problem.
+ *
+ * @param subProblemWeight The Decomposition weight vector of the current subproblem.
+ * @param idealPoint The reference point z for a decomposition problem.
+ * @param candidateFitness The fitness value of the candidate.
+ * @return The real value obtained from the decomposed function.
+ */
+ template
+ ElemType DecomposeObjectives(const arma::Col& subProblemWeight,
+ const arma::Col& idealPoint,
+ const arma::Col& candidateFitness);
+
+ /**
+ * Evaluate objectives for the elite population.
+ *
+ * @tparam ArbitraryFunctionType std::tuple of multiple function types.
+ * @tparam MatType Type of matrix to optimize.
+ * @param population The elite population.
+ * @param objectives The set of objectives.
+ * @param calculatedObjectives Vector to store calculated objectives.
+ */
+ template
+ typename std::enable_if::type
+ EvaluateObjectives(
+ std::vector&,
+ std::tuple&,
+ std::vector >&);
+
+ template
+ typename std::enable_if::type
+ EvaluateObjectives(
+ std::vector& population,
+ std::tuple& objectives,
+ std::vector >&
+ calculatedObjectives);
+
+ //! Size of the population.
+ size_t populationSize;
+
+ //! Maximum number of generations before termination criteria is met.
+ size_t maxGenerations;
+
+ //! Probability of crossover between two members.
+ double crossoverProb;
+
+ //! The probability that two elements will be chosen from the neighbor.
+ double neighborProb;
+
+ //! Number of nearest neighbours of weights to consider.
+ size_t neighborSize;
+
+ //! The crowding degree of the mutation. Higher value produces a mutant
+ //! resembling its parent.
+ double distributionIndex;
+
+ //! Amplification factor for differentiation.
+ double differentialWeight;
+
+ //! Maximum number of childs which can replace the parent. Higher value
+ //! leads to a loss of diversity.
+ size_t maxReplace;
+
+ //! A small numeric value to be added to the weights after initialization.
+ //! Prevents zero value inside inited weights.
+ double epsilon;
+
+ //! Lower bound on each variable in the variable space.
+ arma::vec lowerBound;
+
+ //! Upper bound on each variable in the variable space.
+ arma::vec upperBound;
+
+ //! The set of all the Pareto optimal points.
+ //! Stored after Optimize() is called.
+ arma::cube paretoSet;
+
+ //! The set of all the Pareto optimal objective vectors.
+ //! Stored after Optimize() is called.
+ arma::cube paretoFront;
+};
+
+} // namespace ens
+
+// Include implementation.
+#include "moead_impl.hpp"
+
+#endif
diff --git a/include/ensmallen_bits/moead/moead_impl.hpp b/include/ensmallen_bits/moead/moead_impl.hpp
new file mode 100644
index 000000000..26d08aa49
--- /dev/null
+++ b/include/ensmallen_bits/moead/moead_impl.hpp
@@ -0,0 +1,429 @@
+/**
+ * @file moead_impl.hpp
+ * @author Utkarsh Rai
+ * @author Nanubala Gnana Sai
+ *
+ * Implementation of the MOEA/D-DE algorithm. Used for multi-objective
+ * optimization problems on arbitrary functions.
+ *
+ * ensmallen is free software; you may redistribute it and/or modify it under
+ * the terms of the 3-clause BSD license. You should have received a copy of
+ * the 3-clause BSD license along with ensmallen. If not, see
+ * http://www.opensource.org/licenses/BSD-3-Clause for more Information.
+ */
+
+#ifndef ENSMALLEN_MOEAD_MOEAD_IMPL_HPP
+#define ENSMALLEN_MOEAD_MOEAD_IMPL_HPP
+
+#include "moead.hpp"
+#include
+
+namespace ens {
+
+inline MOEAD::MOEAD(const size_t populationSize,
+ const size_t maxGenerations,
+ const double crossoverProb,
+ const double neighborProb,
+ const size_t neighborSize,
+ const double distributionIndex,
+ const double differentialWeight,
+ const size_t maxReplace,
+ const double epsilon,
+ const arma::vec& lowerBound,
+ const arma::vec& upperBound) :
+ populationSize(populationSize),
+ maxGenerations(maxGenerations),
+ crossoverProb(crossoverProb),
+ neighborProb(neighborProb),
+ neighborSize(neighborSize),
+ distributionIndex(distributionIndex),
+ differentialWeight(differentialWeight),
+ maxReplace(maxReplace),
+ epsilon(epsilon),
+ lowerBound(lowerBound),
+ upperBound(upperBound)
+ { /* Nothing to do here. */ }
+
+inline MOEAD::MOEAD(const size_t populationSize,
+ const size_t maxGenerations,
+ const double crossoverProb,
+ const double neighborProb,
+ const size_t neighborSize,
+ const double distributionIndex,
+ const double differentialWeight,
+ const size_t maxReplace,
+ const double epsilon,
+ const double lowerBound,
+ const double upperBound) :
+ populationSize(populationSize),
+ maxGenerations(maxGenerations),
+ crossoverProb(crossoverProb),
+ neighborProb(neighborProb),
+ neighborSize(neighborSize),
+ distributionIndex(distributionIndex),
+ differentialWeight(differentialWeight),
+ maxReplace(maxReplace),
+ epsilon(epsilon),
+ lowerBound(lowerBound * arma::ones(1, 1)),
+ upperBound(upperBound * arma::ones(1, 1))
+ { /* Nothing to do here. */ }
+
+//! Optimize the function.
+template
+typename MatType::elem_type MOEAD::Optimize(std::tuple& objectives,
+ MatType& iterateIn,
+ CallbackTypes&&... callbacks)
+{
+ // Population Size must be at least 3 for MOEA/D-DE to work.
+ if (populationSize < 3)
+ {
+ throw std::logic_error("MOEA/D-DE::Optimize(): population size should be at least"
+ " 3!");
+ }
+
+ // 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();
+
+ if (neighborSize < 2)
+ {
+ throw std::invalid_argument(
+ "neighborSize should be atleast 2, however "
+ + std::to_string(neighborSize) + " was detected."
+ );
+ }
+
+ if (neighborSize > populationSize - 1u)
+ {
+ std::ostringstream oss;
+ oss << "MOEAD::Optimize(): " << "neighborSize is " << neighborSize
+ << " but populationSize is " << populationSize << "(should be"
+ << " atleast " << (neighborSize + 1u) << ")" << std::endl;
+ throw std::logic_error(oss.str());
+ }
+
+ // 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);
+
+ // Check if upper bound is a vector of a single dimension.
+ if (upperBound.n_rows == 1)
+ upperBound = upperBound(0, 0) * arma::ones(iterate.n_rows, iterate.n_cols);
+
+ // Check the dimensions of lowerBound and upperBound.
+ assert(lowerBound.n_rows == iterate.n_rows && "The dimensions of "
+ "lowerBound are not the same as the dimensions of iterate.");
+ assert(upperBound.n_rows == iterate.n_rows && "The dimensions of "
+ "upperBound are not the same as the dimensions of iterate.");
+
+ const size_t numObjectives = sizeof...(ArbitraryFunctionType);
+ const size_t numVariables = iterate.n_rows;
+
+ //! Useful temporaries for float-like comparisons.
+ const BaseMatType castedLowerBound = arma::conv_to::from(lowerBound);
+ const BaseMatType castedUpperBound = arma::conv_to::from(upperBound);
+
+ // Controls early termination of the optimization process.
+ bool terminate = false;
+
+ // The weight matrix. Each vector represents a decomposition subproblem (M X N).
+ const BaseMatType weights = BaseMatType(numObjectives, populationSize,
+ arma::fill::randu) + epsilon;
+
+ // 1.1 Storing the indices of nearest neighbors of each weight vector.
+ arma::umat neighborIndices(neighborSize, populationSize);
+ for (size_t i = 0; i < populationSize; ++i)
+ {
+ // Cache the distance between weights[i] and other weights.
+ const arma::Row distances =
+ arma::sqrt(arma::sum(arma::square(weights.col(i) - weights.each_col())));
+ arma::uvec sortedIndices = arma::stable_sort_index(distances);
+ // Ignore distance from self.
+ neighborIndices.col(i) = sortedIndices(arma::span(1, neighborSize));
+ }
+
+ // 1.2 Random generation of the initial population.
+ std::vector population(populationSize);
+ for (BaseMatType& individual : population)
+ {
+ individual = arma::randu(
+ iterate.n_rows, iterate.n_cols) - 0.5 + iterate;
+
+ // Constrain all genes to be within bounds.
+ individual = arma::min(arma::max(individual, castedLowerBound), castedUpperBound);
+ }
+
+ Info << "MOEA/D-DE initialized successfully. Optimization started." << std::endl;
+
+ std::vector> populationFitness(populationSize);
+ std::fill(populationFitness.begin(), populationFitness.end(),
+ arma::Col(numObjectives, arma::fill::zeros));
+ EvaluateObjectives(population, objectives, populationFitness);
+
+ // 1.3 Initialize the ideal point z.
+ arma::Col idealPoint(numObjectives);
+ idealPoint.fill(std::numeric_limits::max());
+
+ for (const arma::Col& individualFitness : populationFitness)
+ idealPoint = arma::min(idealPoint, individualFitness);
+
+ terminate |= Callback::BeginOptimization(*this, objectives, iterate, callbacks...);
+
+ // 2 The main loop.
+ for (size_t generation = 1; generation <= maxGenerations && !terminate; ++generation)
+ {
+ // Shuffle indexes of subproblems.
+ const arma::uvec shuffle = arma::shuffle(
+ arma::linspace(0, populationSize - 1, populationSize));
+ for (size_t subProblemIdx : shuffle)
+ {
+ // 2.1 Randomly select two indices in neighborIndices[subProblemIdx] and use them
+ // to make a child.
+ size_t r1, r2, r3;
+ r1 = subProblemIdx;
+ // Randomly choose to sample from the population or the neighbors.
+ const bool sampleNeighbor = arma::randu() < neighborProb;
+ std::tie(r2, r3) =
+ Mating(subProblemIdx, neighborIndices, sampleNeighbor);
+
+ // 2.2 - 2.3 Reproduction and Repair: Differential Operator followed by
+ // Polynomial Mutation.
+ BaseMatType candidate(iterate.n_rows, iterate.n_cols);
+
+ for (size_t geneIdx = 0; geneIdx < numVariables; ++geneIdx)
+ {
+ if (arma::randu() < crossoverProb)
+ {
+ candidate(geneIdx) = population[r1](geneIdx) +
+ differentialWeight * (population[r2](geneIdx) -
+ population[r3](geneIdx));
+
+ // Boundary conditions.
+ if (candidate(geneIdx) < castedLowerBound(geneIdx))
+ {
+ candidate(geneIdx) = castedLowerBound(geneIdx) +
+ arma::randu() * (population[r1](geneIdx) - castedLowerBound(geneIdx));
+ }
+ if (candidate(geneIdx) > castedUpperBound(geneIdx))
+ {
+ candidate(geneIdx) = castedUpperBound(geneIdx) -
+ arma::randu() * (castedUpperBound(geneIdx) - population[r1](geneIdx));
+ }
+ }
+ else
+ candidate(geneIdx) = population[r1](geneIdx);
+ }
+
+ Mutate(candidate, 1.0 / static_cast(numVariables),
+ castedLowerBound, castedUpperBound);
+
+ arma::Col candidateFitness(numObjectives);
+ //! Creating temp vectors to pass to EvaluateObjectives.
+ std::vector candidateContainer { candidate };
+ std::vector> fitnessContainer { candidateFitness };
+ EvaluateObjectives(candidateContainer, objectives, fitnessContainer);
+ candidateFitness = std::move(fitnessContainer[0]);
+ //! Flush out the dummy containers.
+ fitnessContainer.clear();
+ candidateContainer.clear();
+
+ // 2.4 Update of ideal point.
+ idealPoint = arma::min(idealPoint, candidateFitness);
+
+ // 2.5 Update of the population.
+ size_t replaceCounter = 0;
+ const size_t sampleSize = sampleNeighbor ? neighborSize : populationSize;
+
+ const arma::uvec idxShuffle = arma::shuffle(
+ arma::linspace(0, sampleSize - 1, sampleSize));
+
+ for (size_t idx : idxShuffle)
+ {
+ // Preserve diversity by controlling replacement of neighbors
+ // by child solution.
+ if (replaceCounter >= maxReplace)
+ break;
+
+ const size_t pick = sampleNeighbor ?
+ neighborIndices(idx, subProblemIdx) : idx;
+
+ const ElemType candidateDecomposition = DecomposeObjectives(
+ weights.col(pick), idealPoint, candidateFitness);
+ const ElemType parentDecomposition = DecomposeObjectives(
+ weights.col(pick), idealPoint, populationFitness[pick]);
+
+ if (candidateDecomposition < parentDecomposition)
+ {
+ population[pick] = candidate;
+ populationFitness[pick] = candidateFitness;
+ ++replaceCounter;
+ }
+ }
+ } // End of pass over all subproblems.
+
+ // The final population itself is the best front.
+ const arma::uvec frontIndices = arma::shuffle(
+ arma::linspace(0, populationSize - 1, populationSize));
+
+ terminate |= Callback::GenerationalStepTaken(*this, objectives, iterate,
+ populationFitness, frontIndices, callbacks...);
+ } // End of pass over all the generations.
+
+ // Set the candidates from the Pareto Set as the output.
+ paretoSet.resize(population[0].n_rows, population[0].n_cols, population.size());
+
+ // The Pareto Front is stored, can be obtained via ParetoSet() getter.
+ for (size_t solutionIdx = 0; solutionIdx < population.size(); ++solutionIdx)
+ {
+ paretoSet.slice(solutionIdx) =
+ arma::conv_to::from(population[solutionIdx]);
+ }
+
+ EvaluateObjectives(population, objectives, populationFitness);
+ // Set the candidates from the Pareto Front as the output.
+ paretoFront.resize(populationFitness[0].n_rows, populationFitness[0].n_cols,
+ populationFitness.size());
+
+ // The Pareto Front is stored, can be obtained via ParetoFront() getter.
+ for (size_t solutionIdx = 0; solutionIdx < populationFitness.size(); ++solutionIdx)
+ {
+ paretoFront.slice(solutionIdx) =
+ arma::conv_to::from(populationFitness[solutionIdx]);
+ }
+
+ Callback::EndOptimization(*this, objectives, iterate, callbacks...);
+
+ ElemType performance = std::numeric_limits::max();
+
+ for (size_t geneIdx = 0; geneIdx < numObjectives; ++geneIdx)
+ {
+ if (arma::accu(populationFitness[geneIdx]) < performance)
+ performance = arma::accu(populationFitness[geneIdx]);
+ }
+
+ return performance;
+}
+
+//! Randomly chooses to select from parents or neighbors.
+inline std::tuple
+MOEAD::Mating(size_t subProblemIdx,
+ const arma::umat& neighborIndices,
+ bool sampleNeighbor)
+{
+ //! Indexes of two points from the sample space.
+ size_t pointA = sampleNeighbor
+ ? neighborIndices(
+ arma::randi(arma::distr_param(0, neighborSize - 1u)), subProblemIdx)
+ : arma::randi(arma::distr_param(0, populationSize - 1u));
+
+ size_t pointB = sampleNeighbor
+ ? neighborIndices(
+ arma::randi(arma::distr_param(0, neighborSize - 1u)), subProblemIdx)
+ : arma::randi(arma::distr_param(0, populationSize - 1u));
+
+ //! If the sampled points are equal, then modify one of them
+ //! within reasonable bounds.
+ if (pointA == pointB)
+ {
+ if (pointA == populationSize - 1u)
+ --pointA;
+ else
+ ++pointA;
+ }
+
+ return std::make_tuple(pointA, pointB);
+}
+
+//! Perform Polynomial mutation of the candidate.
+template
+inline void MOEAD::Mutate(MatType& candidate,
+ double mutationRate,
+ const MatType& lowerBound,
+ const MatType& upperBound)
+{
+ const size_t numVariables = candidate.n_rows;
+ for (size_t geneIdx = 0; geneIdx < numVariables; ++geneIdx)
+ {
+ // Should this gene be mutated?
+ if (arma::randu() > mutationRate)
+ continue;
+
+ const double geneRange = upperBound(geneIdx) - lowerBound(geneIdx);
+ // Normalised distance from the bounds.
+ const double lowerDelta = (candidate(geneIdx) - lowerBound(geneIdx)) / geneRange;
+ const double upperDelta = (upperBound(geneIdx) - candidate(geneIdx)) / geneRange;
+ const double mutationPower = 1. / (distributionIndex + 1.0);
+ const double rand = arma::randu();
+ double value, perturbationFactor;
+ if (rand < 0.5)
+ {
+ value = 2.0 * rand + (1.0 - 2.0 * rand) *
+ std::pow(upperDelta, distributionIndex + 1.0);
+ perturbationFactor = std::pow(value, mutationPower) - 1.0;
+ }
+ else
+ {
+ value = 2.0 * (1.0 - rand) + 2.0 *(rand - 0.5) *
+ std::pow(lowerDelta, distributionIndex + 1.0);
+ perturbationFactor = 1.0 - std::pow(value, mutationPower);
+ }
+
+ candidate(geneIdx) += perturbationFactor * geneRange;
+ }
+ //! Enforce bounds.
+ candidate = arma::min(arma::max(candidate, lowerBound), upperBound);
+}
+
+//! Calculate the output for single objective function using the Tchebycheff
+//! approach.
+template
+inline ElemType MOEAD::DecomposeObjectives(const arma::Col& subProblemWeight,
+ const arma::Col& idealPoint,
+ const arma::Col& candidateFitness)
+{
+ return arma::max(subProblemWeight % arma::abs(candidateFitness - idealPoint));
+}
+
+//! No objectives to evaluate.
+template
+typename std::enable_if::type
+MOEAD::EvaluateObjectives(
+ std::vector&,
+ std::tuple&,
+ std::vector >&)
+{
+ // Nothing to do here.
+}
+
+//! Evaluate the objectives for the entire population.
+template
+typename std::enable_if::type
+MOEAD::EvaluateObjectives(
+ std::vector& population,
+ std::tuple& objectives,
+ std::vector >& calculatedObjectives)
+{
+ for (size_t i = 0; i < population.size(); i++)
+ {
+ calculatedObjectives[i](I) = std::get(objectives).Evaluate(population[i]);
+ EvaluateObjectives(population, objectives,
+ calculatedObjectives);
+ }
+}
+
+} // namespace ens
+
+#endif
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6b55d4a4a..e17ec5a34 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -23,6 +23,7 @@ set(ENSMALLEN_TESTS_SOURCES
line_search_test.cpp
lookahead_test.cpp
lrsdp_test.cpp
+ moead_test.cpp
momentum_sgd_test.cpp
nesterov_momentum_sgd_test.cpp
nsga2_test.cpp
diff --git a/tests/callbacks_test.cpp b/tests/callbacks_test.cpp
index 90c1ddc0b..924278f38 100644
--- a/tests/callbacks_test.cpp
+++ b/tests/callbacks_test.cpp
@@ -399,6 +399,18 @@ TEST_CASE("NSGA2CallbacksFullFunctionTest", "[CallbackTest]")
true, true, false, false, false, true);
}
+/**
+ * Make sure we invoke all callbacks (MOEA/D-DE).
+ */
+TEST_CASE("MOEADCallbacksFullFunctionTest", "[CallbackTest]")
+{
+ arma::vec lowerBound = {-1000};
+ arma::vec upperBound = {1000};
+ MOEAD optimizer(150, 300, 1.0, 0.9, 20, 20, 0.5, 2, 1E-10, lowerBound, upperBound);
+ CallbacksFullMultiobjectiveFunctionTest(optimizer, false, false, false, false,
+ true, true, false, false, false, true);
+}
+
/**
* Make sure we invoke all callbacks (Lookahead).
*/
diff --git a/tests/moead_test.cpp b/tests/moead_test.cpp
new file mode 100644
index 000000000..8e89872b0
--- /dev/null
+++ b/tests/moead_test.cpp
@@ -0,0 +1,500 @@
+/**
+ * @file moead_test.cpp
+ * @author Sayan Goswami
+ * @author Utkarsh Rai
+ * @author Nanubala Gnana Sai
+ *
+ * ensmallen is free software; you may redistribute it and/or modify it under
+ * the terms of the 3-clause BSD license. You should have received a copy of
+ * the 3-clause BSD license along with ensmallen. If not, see
+ * http://www.opensource.org/licenses/BSD-3-Clause for more information.
+ */
+
+#include
+#include "catch.hpp"
+#include "test_function_tools.hpp"
+
+using namespace ens;
+using namespace ens::test;
+using namespace std;
+
+/**
+ * Checks if low <= value <= high. Used by MOEADFonsecaFlemingTest.
+ *
+ * @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].
+ */
+template
+bool IsInBounds(const ElemType& value, const ElemType& low, const ElemType& high)
+{
+ 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("MOEADSchafferN1DoubleTest", "[MOEADTest]")
+{
+ SchafferFunctionN1 SCH;
+ const double lowerBound = -1000;
+ const double upperBound = 1000;
+ const double expectedLowerBound = 0.0;
+ const double expectedUpperBound = 2.0;
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+
+ 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::mat coords = SCH.GetInitialPoint();
+ std::tuple objectives = SCH.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::cube paretoSet= opt.ParetoSet();
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ double val = arma::as_scalar(paretoSet.slice(solutionIdx));
+ 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 double.
+ */
+TEST_CASE("MOEADSchafferN1TestVectorDoubleBounds", "[MOEADTest]")
+{
+ // 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;
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+
+ typedef decltype(SCH.objectiveA) ObjectiveTypeA;
+ typedef decltype(SCH.objectiveB) ObjectiveTypeB;
+
+ bool success = false;
+ for (size_t trial = 0; trial < 3; ++trial)
+ {
+ arma::mat coords = SCH.GetInitialPoint();
+ std::tuple objectives = SCH.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::cube paretoSet = opt.ParetoSet();
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ double val = arma::as_scalar(paretoSet.slice(solutionIdx));
+ 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 double.
+ */
+TEST_CASE("MOEADFonsecaFlemingDoubleTest", "[MOEADTest]")
+{
+ FonsecaFlemingFunction FON;
+ const double lowerBound = -4;
+ const double upperBound = 4;
+ const double expectedLowerBound = -1.0 / sqrt(3);
+ const double expectedUpperBound = 1.0 / sqrt(3);
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+ typedef decltype(FON.objectiveA) ObjectiveTypeA;
+ typedef decltype(FON.objectiveB) ObjectiveTypeB;
+
+ arma::mat coords = FON.GetInitialPoint();
+ std::tuple objectives = FON.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::cube paretoSet = opt.ParetoSet();
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ const arma::mat solution = paretoSet.slice(solutionIdx);
+ double valX = arma::as_scalar(solution(0));
+ 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))
+ {
+ allInRange = false;
+ break;
+ }
+ }
+
+ REQUIRE(allInRange);
+}
+
+/**
+ * Optimize for the Fonseca Fleming function using NSGA-II optimizer.
+ * Tests for data of type double.
+ */
+TEST_CASE("MOEADFonsecaFlemingTestVectorDoubleBounds", "[MOEADTest]")
+{
+ FonsecaFlemingFunction FON;
+ const arma::vec lowerBound = {-4, -4, -4};
+ const arma::vec upperBound = {4, 4, 4};
+ const double expectedLowerBound = -1.0 / sqrt(3);
+ const double expectedUpperBound = 1.0 / sqrt(3);
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+ typedef decltype(FON.objectiveA) ObjectiveTypeA;
+ typedef decltype(FON.objectiveB) ObjectiveTypeB;
+
+ arma::mat coords = FON.GetInitialPoint();
+ std::tuple objectives = FON.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::cube paretoSet = opt.ParetoSet();
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ const arma::mat solution = paretoSet.slice(solutionIdx);
+ double valX = arma::as_scalar(solution(0));
+ 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))
+ {
+ allInRange = false;
+ break;
+ }
+ }
+
+ REQUIRE(allInRange);
+}
+
+/**
+ * Optimize for the Schaffer N.1 function using NSGA-II optimizer.
+ * Tests for data of type float.
+ */
+TEST_CASE("MOEADSchafferN1FloatTest", "[MOEADTest]")
+{
+ SchafferFunctionN1 SCH;
+ const double lowerBound = -1000;
+ const double upperBound = 1000;
+ const double expectedLowerBound = 0.0;
+ const double expectedUpperBound = 2.0;
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+
+ 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);
+ arma::fcube paretoSet = arma::conv_to::from(opt.ParetoSet());
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ float val = arma::as_scalar(paretoSet.slice(solutionIdx));
+ 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("MOEADSchafferN1TestVectorFloatBounds", "[MOEADTest]")
+{
+ // 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;
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+
+ 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);
+ arma::fcube paretoSet = arma::conv_to::from(opt.ParetoSet());
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ float val = arma::as_scalar(paretoSet.slice(solutionIdx));
+ 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("MOEADFonsecaFlemingFloatTest", "[MOEADTest]")
+{
+ FonsecaFlemingFunction FON;
+ const double lowerBound = -4;
+ const double upperBound = 4;
+ const float expectedLowerBound = -1.0 / sqrt(3);
+ const float expectedUpperBound = 1.0 / sqrt(3);
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+ typedef decltype(FON.objectiveA) ObjectiveTypeA;
+ typedef decltype(FON.objectiveB) ObjectiveTypeB;
+
+ arma::fmat coords = FON.GetInitialPoint();
+ std::tuple objectives = FON.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::fcube paretoSet = arma::conv_to::from(opt.ParetoSet());
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ const arma::fmat solution = paretoSet.slice(solutionIdx);
+ 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("MOEADFonsecaFlemingTestVectorFloatBounds", "[MOEADTest]")
+{
+ FonsecaFlemingFunction FON;
+ const arma::vec lowerBound = {-4, -4, -4};
+ const arma::vec upperBound = {4, 4, 4};
+ const float expectedLowerBound = -1.0 / sqrt(3);
+ const float expectedUpperBound = 1.0 / sqrt(3);
+
+ MOEAD opt(
+ 150, // Population size.
+ 300, // Max generations.
+ 1.0, // Crossover probability.
+ 0.9, // Probability of sampling from neighbor.
+ 20, // Neighborhood size.
+ 20, // Perturbation index.
+ 0.5, // Differential weight.
+ 2, // Max childrens to replace parents.
+ 1E-10, // epsilon.
+ lowerBound, // Lower bound.
+ upperBound // Upper bound.
+ );
+ typedef decltype(FON.objectiveA) ObjectiveTypeA;
+ typedef decltype(FON.objectiveB) ObjectiveTypeB;
+
+ arma::fmat coords = FON.GetInitialPoint();
+ std::tuple objectives = FON.GetObjectives();
+
+ opt.Optimize(objectives, coords);
+ arma::fcube paretoSet = arma::conv_to::from(opt.ParetoSet());
+
+ bool allInRange = true;
+
+ for (size_t solutionIdx = 0; solutionIdx < paretoSet.n_slices; ++solutionIdx)
+ {
+ const arma::fmat solution = paretoSet.slice(solutionIdx);
+ 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);
+}