1. Overall Architecture of software applications
MH-Builder already implements some heuristics and metaheuristics. And it is quite easy and fast to use them. Moreover, the architecture of MH-builder allows to extend the set of (meta)heuristics. This is summarized in figure [img-archi_tutorial]
In the case of programming a new problem. Two classes must be implemented:
-
The Problem class is responsible for describing the problem. This class reads the data stored in a file to collect the information of a particular instance.
-
Associated with the Problem class, The Evaluation, a subclass of core.Eval, is in charge of calculating the evaluation of a solution of the problem concerned.
These first two classes are usually developed from scratch. Two additional classes are also needed but their programming is often facilitated by the existing classes of the MH-Builder framework. In some cases, there are fully provided.
-
the Solution class represents a solution of the problem
-
the (meta)Heuristic class is the implementation of the algorithm. Depending on the complexity of the algorithm, this part is decomposed into different classes. For example, for the local search algorithm, there are classes dedicated to the neighbor operator, other classes for the neighborhood, etc.
Once these classes are available, an executable can be implemented.
2. Illustration with the No-Wait flowshop Problem
2.1. Required classes
Within the MH-Builder framework, you will find the following classes:
-
The FSPSol class specifies the "Solution" class which is shared by the FlowShop and the No-Wait FlowShop Problem"
-
The NoWaitFSP class specifies the "Problem" class (No-Wait Flow shop) which is a specialization of the "FlowShop" Problem (FSP class).
-
The NoWaitFSPEval.h specifies the "Evaluation" class
src/representation/permutation/problems/fsp/FSP.h (embeds the solution and problem class)
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Asuncion Gomez and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_FSP_H
#define MH_BUILDER_FSP_H
#include "representation/permutation/PermutationSolution.h"
#include <fstream>
namespace representation::permutation::problems::fsp {
/*! Define type for a FSP solution */
template<typename FIT>
class FSPSol : public representation::permutation::PermutationSolution<FIT> {
public:
/**
* Construct a fsp solution of size n
* @param n size of permutation (number of jobs)
*/
explicit FSPSol(unsigned long long int n = 0) : representation::permutation::PermutationSolution<FIT>(n) {}
};
/**
* Class representing a FSP (Flowshop Problem) instance
*/
class FSP {
public:
/**
* Load a FSP instance
* @param instanceFile a data files
*/
explicit FSP(const std::string &instanceFile) {
readInstance(instanceFile);
}
/**
* Read an instance file
* @param instanceFile a data files
*/
void readInstance(const std::string &instanceFile) {
std::string line, tmp;
std::ifstream input(instanceFile);
if (!input)
throw std::runtime_error("Error: unable to open benchmark file");
input >> N >> M >> seed;
due_date.resize(N);
processing_time.resize(M);
for (unsigned long long int i = 0; i < M; i++) {
processing_time[i].resize(N);
}
total_job_processing_time.resize(N);
std::fill(total_job_processing_time.begin(), total_job_processing_time.end(), 0);
int num_job;
total_processing_time = 0;
for (unsigned long long int i = 0; i < N; i++) {
input >> num_job >> due_date[i];
for (unsigned long long int j = 0; j < M; j++) {
input >> processing_time[j][i];
total_processing_time += processing_time[j][i];
total_job_processing_time[i] += processing_time[j][i];
}
}
input.close();
}
/**
* Get the number of jobs
* @return the number of jobs
*/
unsigned long long int getN() {
return N;
}
/**
* Get the number of machines
* @return the number of machines
*/
unsigned long long int getM() {
return M;
}
/**
* Get the initial seed
* @return the initial seed
*/
unsigned long long int getSeed() {
return seed;
}
/**
* Get the processing time matrix
* @return the matrix of the the processing time
*/
std::vector<std::vector<unsigned long long int>> getProcessingTimeMatrix() {
return processing_time;
}
/**
* Get the total processing time
* @return the total processing time
*/
unsigned long long int getTotalProcessingTime() const {
return total_processing_time;
}
/**
* Get the total processing time for job i
* @return the total processing time for job i
*/
unsigned long long int getTotalProcessingTime(unsigned long long int i) const {
return total_job_processing_time[i];
}
/**
* Get the due date of the job i
* @return the due date of the job i
*/
double getDueDate(unsigned long long int i) const {
return due_date[i];
}
/**
* Get the task time of the job j at the machine i
* @return the task time of the job j at the machine i
*/
unsigned long long int getProcessingTime(unsigned long long int i, unsigned long long int j) {
return processing_time[i][j];
}
protected:
/*! Number of jobs */
unsigned long long int N;
/*! Number of machines */
unsigned long long int M;
/*! Seed */
unsigned long long int seed;
/*! Due date vector*/
std::vector<unsigned long long int> due_date;
/*! Processing time matrix */
std::vector<std::vector<unsigned long long int> > processing_time;
/*! Total job processing time */
std::vector<unsigned long long int> total_job_processing_time;
/*! Total processing time */
unsigned long long int total_processing_time;
};
}
#endif //MH_BUILDER_FSP_H
src/representation/permutation/problems/fsp/NoWaitFSP.h
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Olivier Caron and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_NOWAITFSP_H
#define MH_BUILDER_NOWAITFSP_H
namespace representation::permutation::problems::fsp {
#include "representation/permutation/problems/fsp/FSP.h"
#include <algorithm>
/**
* Class representing a No-Wait FlowShop Problem) instance
*/
class NoWaitFSP : public representation::permutation::problems::fsp::FSP {
public:
explicit NoWaitFSP(const std::string &instanceFile) : FSP(instanceFile) {
this->initDelay();
}
/**
* initialize the delay matrix d (Bertolissi 2000)
* d[i][j] corresponds to the delay between the start of job i and the start of following job j
* on the same first machine
*/
void initDelay() {
d.resize(this->N);
for (unsigned long long int i = 0; i < this->N; i++) {
d[i].resize(this->N);
for (unsigned long long int j = 0; j < this->N; j++) {
if (i != j) {
this->d[i][j] = this->processing_time[0][i];
int maxi = 0;
for (unsigned long long int r = 0; r < this->M; r++) {
int nb = 0;
for (unsigned long long int h = 1; h <= r; h++)
nb = nb + this->processing_time[h][i];
for (unsigned long long int h = 0; h < r; h++)
nb = nb - this->processing_time[h][j];
maxi = std::max(maxi, nb);
}
this->d[i][j] += maxi;
}
}
}
}
/**
*
* @param i job i
* @param j job j following i
* @return the delay between the start of job i and the start of following job j on the first machine
*/
unsigned long long int
delay(const unsigned long long int i, const unsigned long long int j) const {
return this->d[i][j];
}
protected:
/*! delay matrix d (Bertolissi 2000)
* d[i][j] corresponds to the delay between the start of job i and the start of following job j
* on the same first machine*/
std::vector<std::vector<unsigned long long int>> d;
};
}
#endif //MH_BUILDER_NOWAITFSP_H
src/representation/permutation/problems/fsp/NoWaitFSPEval.h
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Olivier Caron and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_NOWAITFSPEVAL_H
#define MH_BUILDER_NOWAITFSPEVAL_H
#include "core/Eval.h"
#include "NoWaitFSP.h"
namespace representation::permutation::problems::fsp {
/**
* Class representing an evaluator for Flowshop problem
*/
template<typename FSPSol>
class NoWaitFSPEval : public core::Eval<FSPSol> {
public:
/**
** Constructor of the evaluation
** @param _fsp data for evaluator
*/
explicit NoWaitFSPEval(NoWaitFSP &_fsp) : fsp(_fsp) {
}
/**
* Evaluate a NWFSP solution
* @param sol a solution
*/
void operator()(FSPSol &sol) override {
/*! fitness solution */
auto &fit = sol.fitness();
auto objv = fit.objectives();
/** single objective : makespan **/
objv.resize(1);
objv[0] = makespan(sol);
fit.objectives(objv);
this->eval_counter++;
}
virtual unsigned long long int makespan(const FSPSol &_fs) const {
return completionTime(_fs.size() - 1, _fs);
}
protected:
/*! FSP data */
NoWaitFSP fsp;
virtual unsigned long long int completionTime(const unsigned long long int pos, const FSPSol &_fs) const {
if (pos == 0) return fsp.getTotalProcessingTime(_fs[0]);
else {
int time = 0;
for (unsigned long long int k = 1; k <= pos; k++) time += fsp.delay(_fs[k - 1], _fs[k]);
return time + fsp.getTotalProcessingTime(_fs[pos]);
}
}
};
}
#endif //MH_BUILDER_NOWAITFSPEVAL_H
2.2. Programming the executable
The source program src-bin/nwfsp.cpp generates an executable for the No-Wait FlowShop problem.
2.2.1. Instanciation of the base classes
The first part consists in creating the necessary objects:
1
2
3
4
5
6
7
8
9
10
/**************************************************************************/
/* Part 1 : creation of required objects */
/**************************************************************************/
typedef core::fitness::FitnessMin<double, double> FIT; (1)
typedef FSPSol<FIT> SOL;
typedef NoWaitFSPEval<SOL> EVAL;
NoWaitFSP fsp_instance("../../instances/fsp/020_05_01.txt"); // the problem object (2)
EVAL eval(fsp_instance); // the evaluation object, requires the problem instance
SOL sol(fsp_instance.getN()); // <4> creation of the solution object
1 | type definitions for clarity (lines 4-6) |
2 | instanciations of the three classes that represent the problem, the evaluation and the solution (lines 8-10) |
2.2.2. run an heuristic
Let us illustrate our point with the NoWait-FlowShop Problem with the NEH algorithm.
src/representation/permutation/problems/fsp/algo/NEH.h
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Lucien Mousin and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_NEH_H
#define MH_BUILDER_NEH_H
#include "core/Eval.h"
#include "core/Algorithm.h"
#include "representation/permutation/problems/fsp/FSP.h"
#include "representation/permutation/algorithm/BestInsertion.h"
namespace representation::permutation::problems::fsp::algo {
template<typename SOL>
class NEH : core::Algorithm<SOL> {
public:
typedef typename SOL::FITNESS FIT;
explicit NEH(FSP &_instance, core::Eval<SOL> &_eval) : instance(_instance), eval(_eval) {}
void operator()(SOL &sol) override {
if (!this->criterion->operator()()) return; // stop criterion test
SOL best, tmp;
std::vector<unsigned long long int> indexes;
std::vector<unsigned long long int> fitness;
// init
for (unsigned long long int i = 0; i < instance.getN(); i++) {
fitness.push_back(instance.getTotalProcessingTime(i));
indexes.push_back(i);
}
std::sort(indexes.begin(), indexes.end(), [&](int i, int j) {
return fitness[i] < fitness[j];
});
// first member
best.push_back(indexes[0]);
// for each other members
for (unsigned long long int i = 1; i < instance.getN(); i++)
// insert at the best position
representation::permutation::algorithm::BestInsertion<SOL>::bestInsert(best, indexes[i],
eval);
eval(best);
sol = best;
};
void operator()(SOL &sol, FSP &_instance) {
instance = _instance;
operator()(sol);
}
protected:
FSP &instance;
core::Eval<SOL> &eval;
};
}
#endif //MH_BUILDER_NEH_H
/**************************************************************************/
/* Part 2 : first resolution of the problem */
/* by using the heuristics NEH */
/**************************************************************************/
algo::NEH<SOL> neh(fsp_instance, eval); // (1)
neh(sol); (2)
std::cout << "Solution provided by the NEH heuristic" << std::endl; // display results
std::cout << sol << std::endl;
std::cout << "Number of evaluations: " << eval.getEvalCounter() << std::endl ;
1 | creation of the NEH object, requires the problem and eval objects |
2 | execution of the NEH algorithm with a starting solution, sol is updated. |
2.2.3. run a meta-heuristic
Let us now illustrate the NoWait-FlowShop Problem using the hillClimbing localsearch metaheuristic algorithm.
file location: src/opt/singleSolution/localsearch/HillClimbing.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/****************************************************************************/
/* Part 3 : optimisation of the problem */
/* by using the hill climbing metaheuristics */
/****************************************************************************/
representation::permutation::neighborhood::neighbor::ShiftNeighbor<SOL> shiftNeighbor; (1)
opt::singleSolution::neighborhood::RandomNeighborhood<SOL> randomNeighborhood(shiftNeighbor);
opt::singleSolution::neighborhood::explorer::BestImprNeighborhoodExplorer<SOL> best(randomNeighborhood, eval);
opt::singleSolution::localsearch::HillClimbing<SOL> hillclimbing(best, eval);
/******************************************************************************/
/* Part 4 : running metaheuristics */
/******************************************************************************/
std::cout << "RUNNING HillClimbing LOCAL SEARCH" << std::endl;
hillclimbing(sol); (2)
std::cout << "Solution provided by the HillClimbing metaheuristic" << std::endl; // display results
std::cout << sol << std::endl;
std::cout << "Number of evaluations: " << eval.getEvalCounter() << std::endl ;
1 | configuration and creation of the hillclimbing algorithm (lines 5-8). MH-Builder provides various neighbor operators and neighborhood explorers. All provided metaheuristics are highly customizable. |
2 | execution of the hillclimbing algorithm starting from the solution of NEH, sol is updated. |
3. MH-Builder components
3.1. Stop Criterion
3.1.1. Protocol
Any algorithm implemented by a subclass of core::Algorithm can be controlled by a stop criterion.
Using stop criterion is quite simple as showned below:
AStopCriterion<SOL> c ; (1)
c.init() ; (2)
while (c()) { (3)
do_something() ; // specific to the algorithm
c.update(); (4)
}
1 | instanciation of a specific stop criterion |
2 | initialisation step |
3 | a stop criterion implements the C++ operator (). If this operator () returns true, the algorithm continues. |
4 | the update method allows to update inner values, required for the stop criterion. |
As a resume, a stop criterion subclass of core::Criterion have to implement three methods: init(), () and update()
Any algorithm that needs the use of stop criterion have to call these methods
AStopCriterion<SOL> c ;
AnAlgorithm<SOL> algo ;
algo.setCriterion(c) ; (1)
algo.init() ; (2)
algo() ; (3)
1 | set the stop criterion for an algorithm |
2 | the init method of the algorithm calls the init method of the stop criterion |
3 | the implementation of the algorithm is ensured by the C++ operator (), which calls methods () and update() of the stop criterion. |
3.1.2. Available stop criteria
The MH-Builder framework already provides different versions of stop criteria as illustrated below
Available stop criteria are the following:
- DummyCriterion
-
default stop criterion for algorithms, the execution continuation test always returns true
- EvalCriterion
-
the execution is stopped when a given number of evaluations has been performed
- IterCriterion
-
the execution is stopped when a given number of iterations has been performed
- TimeCriterion
-
the execution is stopped after a given elapsed time
- KeyboardHitCriterion
-
the execution is stopped when a keyboard ked is pressed (only under linux platforms)
- SimpleCoolingSchedule, CoolingSchedule
-
stop criterion for Simulated Annealing algorithm
- CriterionOr, CriterionAnd
-
allows you to combine of two stop criteria
3.1.3. C++ tutorial for stop criteria
The source program src-bin/nwfsp.cpp related to the No-Wait FlowShop problem includes a section of code to illustrate the use of stop criteria.
The following section of code allows to instanciate required classes
1
2
3
4
5
6
7
8
9
NoWaitFSP fsp_instanceSC("../../instances/fsp/020_05_01.txt"); // the problem object
EVAL evalSC(fsp_instanceSC); // the evaluation object, requires the problem instance
SOL solSC(fsp_instance.getN()); // creation of the solution object
opt::singleSolution::neighborhood::explorer::BestImprNeighborhoodExplorer<SOL> bestSC(randomNeighborhood, evalSC);
opt::singleSolution::localsearch::HillClimbing<SOL> hillclimbingSC(bestSC, evalSC);
opt::criterion::EvalCriterion<SOL> stop500evaluations(500,evalSC) ; (1)
opt::criterion::TimeCriterion<SOL> stopMilliSeconds(3000) ;
1 | after creation of objects related to the problem and the selected metaheuristic (lines 1-5), lines 7 and 8 creates two stop criteria. |
1
2
3
hillclimbingSC.setCriterion(stop500evaluations); (1)
std::cout << "Start running HillClimbing LOCAL SEARCH with max evaluation : 500" << std::endl;
hillclimbingSC(solSC); (2)
1 | the algorithm sets the first stop criteria |
2 | the execution stops after 500 evaluations |
1
2
3
hillclimbingSC.setCriterion(stopMilliSeconds); (1)
std::cout << "Continue the execution of HillClimbing LOCAL SEARCH during 3 seconds" << std::endl;
hillclimbingSC(solSC); (2)
1 | Now the algorithm update with the second stop criteria |
2 | the execution continues during 3 seconds |
You can run the NWFSP executable of the MH-Builder framework for better understanding
3.2. Local search components
Algorithms based on local search are well-known metaheuristics that involve different types of components interacting together. For each type of component, MH-Builder offers a modular, scalable architecture that allows the construction of different variants. In the following subsections, we describe these different types of components and how they are programmed in C++.
3.2.1. Neighbor operators
A neighbor operator is the first basic element of local search algorithms. Starting from a solution, the operator performs a move that produces a neighboring solution. Depending on the type of operator, there is a limited number of neighboring solutions.
The MH-Builder framework provides different neighbors as illustrated below
a Neighbor is characterized by its name. Two main public operations are available : do_move and do_move_back. The flag attribute takes the value true after a do_move and false after a do_move_back. During a do_move the previous solution and its fitness are stored in order to share do_move_back operation for all Neighbor subclasses.
An IndexNeighbor , a subclass of Neighbor manages the key and maxKey attributes. Starting from a solution, there are (maxKey - 1) neighboring solutions ranging from 0 to (maxKey - 1). To obtain one of these solutions, the programmer must define the key, then invoke the do_move method, as illustrated below
ShiftNeighbor<SOL> shiftNeighbor ;
shiftNeighbor.init(sol) ; (1)
SOL solTmp=sol ; (2)
for (auto i =0; i< shiftNeighbor.getMaxKey(); i++) { (3)
shiftNeighbor.setKey(i) ;
shiftNeighbor.do_move(solTmp) ; (4)
std::cout << solTmp << std::endl ; // display the result of move
solTmp = sol ; // redo a copy
}
1 | the init method sets the value of maxKey, which depends on the given solution. |
2 | make a copy of the solution |
3 | explore all the neighboring solutions |
4 | solTmp is the k+1st neighboring solution of sol |
Programming new neigbhor operators is fairly straightforward. You need to create a new class in the Neighbor hierarchy and implement at least the init and move methods.
click on the following line to see a complete example (the Shift Neighbor)
src/representation/permutation/neighborhood/neighbor/ShiftNeighbor.h
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Lucien Mousin and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_SHIFT_NEIGHBOR_H
#define MH_BUILDER_SHIFT_NEIGHBOR_H
#include "opt/singleSolution/neighborhood/neighbor/IndexNeighbor.h"
namespace representation::permutation::neighborhood::neighbor {
/**
* Class representing a classic shift neighbor:
* "the shift operator consists in choosing two positions i and j,
* insert the character located at position j into position i,
* and then shift all characters formerly located between i (included) and j (excluded)
* by one step to the right if i < j, or one step to the left otherwise."
* @tparam SOL Solution Type
*/
template<typename SOL>
class ShiftNeighbor : public opt::singleSolution::neighborhood::neighbor::IndexNeighbor<SOL> {
public:
/**
* constructor, set the name of the neighbor
*/
ShiftNeighbor() { this->name="ShiftNeighbor" ; }
/**
* Initialize the maximum key according to the size of the solution
* @param sol the solution
*/
void init(const SOL &sol) override {
this->maxKey = (sol.size() - 1) * (sol.size() - 1);
}
protected:
/**
* Apply a shift on a solution
* @param sol the solution
*/
void move(SOL &sol) override {
unsigned long long int first, second ;
if (this->key <= sol.size() - 2) {
first = 0;
second = this->key + 1;
} else {
first = (this->key - 1) / (sol.size() - 2);
second = (this->key - 1) % (sol.size() - 2);
if (second >= first - 1)
second += 2;
}
sol.move(first, second);
}
};
}
#endif //MH_BUILDER_SHIFT_NEIGHBOR_H
3.2.2. Neighborhood components
An instance of Neighborhood allows to iterate over the various neighbors of a solution. A neighborhood is always linked to a particular neighbor operator (see subsection Neighbor operators ). It is possible to change the linked neighbor operator at any time (subpart of the adaptive dimension of the MH-Builder framework).
The IndexNeighborhood class allows you to iterate over all the neighbors of a solution in an established order. RandomNeighborhood allows to obtain the same neighbors but in a random order.
The C++ code showned below illustrates the iteration of the neighborhood from an initial solution.
1
2
3
4
5
6
7
8
9
10
SOL solOrigin ; (1)
ShiftNeighbor<SOL> shiftNeighbor ;
IndexNeighborhood<SOL> indexNeighborhood(shiftNeighbor) ;
indexNeighborhood.init(solOrigin) ;
while (indexNeighborhood.hasNextNeighbor()) { (2)
SOL current = solOrigin; (3)
indexNeighborhood(current) ; (4)
std::cout << current << std::endl ; // display the result of move
indexNeighborhood.next() ;
}
1 | lines 1-3 creates the required objects. Here, the shift move operator is choosed. |
2 | the iteration of all neighbors of the initial solution |
3 | make a copy of the solution |
4 | the C++ operator () do a move according to the associated neighbor operator. |
3.2.3. Neighborhood Explorer components
An instance of NeighborhoodExplorer allows you to explore a neighborhood based on a neigborhood. The MH-Builder framework already provides various explorers, which are described below. The inheritance hierarchy makes it easily extends the framework by adding new explorers.
The C++ code showned below illustrates the ease of use of a neighborhood explorer:
1
2
3
4
5
6
SOL sol ; (1)
ShiftNeighbor<SOL> shiftNeighbor ;
IndexNeighborhood<SOL> indexNeighborhood(shiftNeighbor) ;
BestImprNeighborhoodExplorer bestImprovedExplorer(indexNeighborhood, eval)
indexNeighborhood.init(sol) ;
bestImprovedExplorer(sol) ; (2)
1 | lines 1-4 creates the required objects. |
2 | the C++ operator (sol) perform a neighborhood exploration. sol now contains the new solution which is the best solution of the neighborhood that improves on the initial solution |
Here is below the inheritance hierarchy for neighborhood explorers
The available explorers are the following:
-
BestImprNeighborhoodExplorer : explores the entire neighborhood to find the best improved solution WorstImprNeighborhoodExplorer : explores the entire neighborhood to find the worst-case solution that improves on the initial solution
-
FirstImprNeighborhoodExplorer : explores the neighborhood and stops exploring as soon as an improved solution is found
Develop new explorers is an easy task. The developer simply needs to create a subclass of NeighborhoodExplorer and override one or both of the following boolean methods searchCriterion and replacementCriterion
Let us illustrate this point with the FirstImprNeighborhoodExplorer class:
src/opt/singleSolution/neighborhood/explorer/FirstImprNeighborhoodExplorer.h
/***************************************************************************************
* MH-Builder, a framework for designing adaptive metaheuristics *
* for single and multi-objective optimization. *
* (c) 2019 University of Lille, CNRS *
* *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at *
* your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
* for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************
Authors: Lucien Mousin and additional contributors (see Authors)
****************************************************************************************/
#ifndef MH_BUILDER_FIRSTIMPRNEIGHBORHOODEXPLORER_H
#define MH_BUILDER_FIRSTIMPRNEIGHBORHOODEXPLORER_H
#include "core/Eval.h"
#include "opt/singleSolution/neighborhood/explorer/NeighborhoodExplorer.h"
#include "opt/singleSolution/neighborhood/Neighborhood.h"
namespace opt::singleSolution::neighborhood::explorer {
/**
* Class representing a first improvement exploration strategy for Neighborhood
* @tparam SOL
*/
template<typename SOL>
class FirstImprNeighborhoodExplorer : public NeighborhoodExplorer<SOL> {
public:
/**
* Constructor for a first improvement neighborhood explorer
* @param _neighborhood the neighborhood operator
* @param _eval the evaluation function
*/
explicit FirstImprNeighborhoodExplorer(Neighborhood<SOL> &_neighborhood, core::Eval<SOL> &_eval)
: NeighborhoodExplorer<SOL>(_neighborhood, _eval) {}
/**
* Test if the search continue (if not improved neighbor has been found)
* @param sol the solution
* @param _current the current solution
* @param _selected the selected solution
* @return true if sol do not have improved neighbor
*/
bool searchCriterion(const SOL &_sol, const SOL &_current, const SOL &_selected) override {
return NeighborhoodExplorer<SOL>::searchCriterion(_sol, _current, _selected) && _selected <= _sol;
}
/**
* Test if current neighbor is better than selected
* @param sol the solution
* @param _current the current solution
* @param _selected the selected solution
* @return true if the current neighbor is better than than out solution
*/
bool replacementCriterion([[maybe_unused]] const SOL &sol , const SOL &_current, const SOL &_selected) override {
return _current > _selected;
};
};
}
#endif //MH_BUILDER_FIRSTIMPRNEIGHBORHOODEXPLORER_H
3.3. Genetic Algorithm
This subsection describes the genetic algorithm part of the MH-Builder framework. Like other metaheuristics implemented in the MH-Builder framework, the proposed software architecture offers the following advantages:
-
a Genetic Algorithm (GA) composed of modular components.
-
a set of easily extensible components
-
each component can be dynamically exchanged or updated
The general framework of the GA is illustrated below:
The Genetic Algorithm class is the central part of all genetic algorithms. It contains and manages the different boxes. Boxes are either:
-
a selection operator (subclass of Selection)
-
two reproduction operators : one derived from the abstract Crossover class and one from the Mutation class. At runtime, these two operators are executed according to a given probability.
-
a replacement operator (subclass of Replacement)
All these elements, more detailed in the following subsections, are C++ templates where the template parameter is a subclass of the core::Archive.
3.3.1. The GeneticAlgorithm class
At the instanciation stage, the components that make up the genetic algorithm are defined. Using the getter/setter methods, these constituents can be accessed or updated. The algorithm is executed using the C++ operator ()(Population &sol). The sol parameter is a given population of individuals that evolves with each generation of the process.
Here is the C++ code for the general algorithm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* The operator that runs the genetic algorithm
* @param POPULATION population
*/
void operator()(POPULATION &sol) override {
this->init(sol);
while (this->criterion->operator()()) { (1)
this->numberOfGenerations++;
this->selection->operator()(sol); (2)
this->crossover->operator()(this->selection->getSelectedPopulation()); (3)
this->mutation->operator()(this->crossover->getOffspring()); (4)
this->replacement->operator()(sol,this->mutation->getOffspring()) ; (5)
this->criterion->update(); (6)
this->checkpoint->operator()(sol); (7)
}
}
1 | The process stops according to a given stopping criterion |
2 | the selection step extracts individuals from the initial population |
3 | the crossover step is then performed with the population resulting from the selection step |
4 | then the mutation step. The reproduction operators (crossover and mutation) store their result in the population called offspring |
5 | the last step of a generation is the replacement step, which evolves the population sol <6-7> the stop criterion and checkpoints are updated (these elements are managed by core::Algorithm super class) |
3.3.2. The selection step
Defining a new operator for the selection step is straightforward. It involves creating a subclass of Selection and implementing the C++ operator()(const POPULATION& _pop) : void. This method performs a selection of selectSize individuals extracted from the pop parameter. The UML diagram described above illustrates 3 different selection strategies (these strategies are included in the MH-Builder framework).
3.3.3. the reproduction steps
At each process generation, crossover and mutation operators are executed according to a probability. This probability is managed by the abstract class Reproduction . The population resulting from one of these operatorsis stored in the variable Reproduction.offspring.
Developping a new crossover operator involves creating a subclass of Crossover and implementing the method operator()(const INDIVIDUAL& ind1, const INDIVIDUAL& ind2) : pair<INDIVIDUAL , INDIVIDUAL>. This method generates a pair of children according to the two given individuals.
Developping a new mutation operator involves creating a new neighbor operator (see subsection Neighbor operators). Neighbors already implemented and used in local search algorithms are available for genetic algorithms.
3.3.4. The replacement step
The final step in the generation process of a genetic algorithm consists is to update the initial population with the offspring from the last generation. Here again, several strategies are possible.
Developing a new replacement strategy involves creating a subclass of Replacement and implementing the method operator()(POPULATION& _parent_pop, const POPULATION offspring)
3.3.5. The NSGA-II Genetic Algorithm
In MH-Builder, the genetic algorithm framework has been extended to support the NSGA-II algorithm. Three new classes are added as shown in the following figure:
New classes are:
-
the NSGA2_Solution class: this is a decorator class for all types of solution. This class manages both rank and crowding distance (see the NSGA-II definition for a more detailed explanation). These data are calculated by two static methods: fastNonDominatedSort and crowdingDistanceAssignment.
-
the NSGA2_Eval class: a decorator class for any class that evaluates a solution
-
the NSGA2_Replacement class, which is a specific implementation of the abstract Replacement class. According to the NSGA_II algorithm, the new population is calculated as a function of its rank and the crowding distance.
-
The NSGA2_Algorithm class, which is a subclass of _GeneticAlgorithm. The selection subcomponent must be a binary tournament, and the replacement subcomponent corresponds to the NSGA2_Replacement class described above. The other components (crossover, mutation) can be freely chosen.
As a consequence, the NSGA2 algorithm slightly differs from the general one. Here is the C++ code for the NSGA2 algorithm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* The operator that runs the nsga-ii genetic algorithm
* @param POPULATION population
*/
void operator()(POPULATION &sol) override {
this->init(sol);
for (auto &i: sol) {
eval(i);
}
auto fronts = NSGA2_Solution<SOL>::fastNonDominatedSort(sol); (1)
for (auto &f: fronts) NSGA2_Solution<SOL>::crowdingDistanceAssignment(sol, f); (2)
while (this->criterion->operator()()) {
this->numberOfGenerations++;
this->selection->operator()(sol); (3)
this->crossover->operator()(this->selection->getSelectedPopulation());
this->mutation->operator()(this->crossover->getOffspring());
this->replacement->operator()(sol, this->mutation->getOffspring()); (4)
this->criterion->update();
this->checkpoint->operator()(sol);
}
}
1 | Pareto fronts are the result of the method, the rank of all solutions of the archive pop is set t. |
2 | the crowding distance is set for all solutions of the archive sol |
3 | the selection is a binary tournament |
4 | the replacement variable is an instance of the NSGA2_Replacement class |
3.3.6. Illustrations
The MH-Builder GIT project contains several executables that illustrate the use of genetic algorithms for different problems.
- oneMaxGA
-
the onemax problem (bitstring solution) with a single objective
-
C++ source : src-bin/oneMaxGeneticAlgorithm.cpp
-
- tspGA
-
a TSP problem (permutation solution) with a single objective
-
C++ source : src-bin/tspGeneticAlgorithm.cpp
-
- tspNSGA2BiObjective
-
a TSP problem with 2 objectives solved with the NSGA2 algorithm
-
C++ source : src-bin/tspNSGA2BiObjective.cpp
-
- bistringNSGA2BiObjective
-
a 2-objective bistring problem solved with the NSGA2 algorithm
-
C++ source : _src-bin/bitstringNSGA2BiObjective.cpp
-