Documentation | |
License |
- 💬 Introduction
- 🔆 What is new on version 3.0
- 🔆 What is new on version 2.0
- 💻 Installation
- ⚡ Usage
- 📚 Tutorial and full documentation
- ✒️ License and Citing
- 👷 TODO
- ✏️ Contributing
BRKGA-MP-IPR provides a very easy-to-use framework for the Multi-Parent Biased Random-Key Genetic Algorithm with Implict Path Relink (BRKGA-MP-IPR). Assuming that your have a decoder to your problem, we can setup, run, and extract the value of the best solution in less than 5 commands (obvisiously, you may need few other lines fo code to do a proper test).
We provide a very thorough and easy-to-follow tutorial on using BRKGA effectively. We strongly recommend you read the tutorial to make your code works at best within BRKGA. Check out the tutorial and API documentation: https://ceandrade.github.io/brkga_mp_ipr_cpp
This C++ version provides a fast prototyping API using C++20 standards and
libraries. All code was developed as a header-only library, and have no
external dependencies other than those included in the package. So, you just
need to copy/check out the files and point your compiler's header path to
BRKGA-MP-IPR folder (-I
on GCC and Clang).
This framework can use multiple threads of modern CPUs, by setting a single parameter (assuming that your decoder is thread-safe). This leverage the parallel decoding nature that BRKGAs offer, compared to other (meta) heuristic frameworks.
The code can be compiled using > GCC 9.4 and > Clang 10.0, and it is very probable that it can be compiled using other modern C++ compilers, such as Intel and Microsoft compilers. To use multiple threads, your compiler must support OpenMP. The current version has been long developed, and it is a very mature code used in production in several companies. However, it lacks of a proper unit and coverage testing. Such tests are in the TODO list.
If C++ is not suitable to you, we may find useful the Julia version of this framework. We are also developing a Python version which is in its earlier stages (no IPR yet). However, both Julia and Python versions only support single-objective optimization at this moment. We have no timeline to implement multi-objective optimization in such platforms. Also, at this moment, we have no plans to implement the BRKGA-MP-IPR in other languages such as Java or C#. But if you want to do so, you are must welcome. But please, keep the API as close as possible to the C++ API (or Julia API in case you decide go C), and use the best coding and documentation practices of your chosen language/framework.
If you are not familiar with how BRKGA works, take a look on Multi-Parent BRKGA.
On version 2.0, we claimed that BRKGA-MP-IPR was a very easy-to-use framework. But, few people told me this statement was not even true. The main complaining was that while the features were very nice, tightening them together was hard, even using the provided examples.
Now, BRKGA-MP-IPR supplies a method called run()
. It implements the entire
pipeline using all framework features in a chain-like way, similar to the
detailed examples. The user may call in this way:
//...
auto [brkga_params, control_params] =
BRKGA::readConfiguration(config_file);
MyDecoder my_decoder;
BRKGA::BRKGA_MP_IPR<MyDecoder> algorithm(
my_decoder, BRKGA::Sense::MINIMIZE, seed, num_chromosomes, brkga_params
);
// algorithm.initialize(); // No need anymore :-)
auto final_status = algorithm.run(control_params, &cout);
cout << "Final algorithm status\n" << final_status;
//...
where control_params
is an instance of the new class ControlParams
(explained further), and an optional stream for logging
(in this example, cout
).
run()
returns an AlgorithmStatus
object with information about the
optimization like total time, iteration counting, and more (check the full
documentation for that).
So, users need no more write fine control loops unless they need/want. Just set some control parameters (and some other callbacks, described below, if you like), and you are good to go!
Supporting run()
, we have three new methods:
-
setStoppingCriteria()
: while methodrun()
sets automatically maximum time or maximum stalled iterations (without improvement in the best solution) as standard stopping criteria, the user can add to these other criteria using this method. For example, in a minimization problem, we may want to stop at the value within a distance from a lower bound or when we reach a given number of iterations:fitness_t lower_bound = compute_lower_bound(); unsigned max_iterations = 100; algorithm.setStoppingCriteria( [&](const AlgorithmStatus& status) { return status.best_fitness <= lower_bound * 1.1; // 10% from the lower bound || status.current_iteration == max_iterations; } );
In this case, the stop criteria become:
- Is maximum time reached OR
- Is maximum stalled iterations reached OR
status.best_fitness <= lower_bound * 1.1
ORstatus.current_iteration == max_iterations
.
Note that BRKGA-MP-IPR always tests against time and stalled iterations to avoid hanging up. However, this behavior can be changed by modifying the maximum time and maximum stalled iterations in the control parameters.
📝 Note If you are using implicit path relinking (IPR), which is very timing consuming, we STRONGLY RECOMMEND TO SET A MAXIMUM TIME since this is the core stopping criteria on IPR. If you really mean to have no maximum time and/or maximum stalled iterations set, we recommend to use the following code:
// After reading your parameters, e.g., // auto [brkga_params, control_params] = readConfiguration("config.conf"); // You can set the time to max... control_params.maximum_running_time = std::chrono::seconds::max(); // ... and/or the stalled iterations to max. control_params.stall_offset = numeric_limits<unsigned>::max();
-
addNewSolutionObserver()
: This method adds a callback that is triggered when an overall improved solution is found by the algorithm. It also adds an additional stop point if the users finds it useful by returntrue
. This is very useful for tracking the evolution of the algorithm, for instance:algorithm.addNewSolutionObserver( [](const AlgorithmStatus& status) { std::cout << "> Iter: " << status.current_iteration << " | solution: " << status.best_fitness << " | time: " << status.current_time << std::endl; return false; // Dont' stop the optimization. } );
-
setShakingMethod()
: This method adds a custom shaking procedure defined by the user. Please, refer to its documentation for more details.
Less important but still relevant: previously, one must call initialize()
before any method that manipulated the population. Also, since initialize()
(re)decodes the population, we have to measure its running time too. Now,
the user do not need to call initialize()
anymore!!.
initialize()
is called on the need by its fellow methods internally. This
leads to fewer error-prone codes.
Although this is part of API enhancement, it deserves special attention. Now,
we include all BRKGA and IPR parameters into BrkgaParams
, and all (external)
control parameters into ControlParams
(which was renamed from
ExternalControlParams
). In doing so, we have a consistent set that can be
fully loaded from configuration files.
Not all parameters are required, and those not are set to their default values.
The new reading function readConfiguration()
will emit a warning when
no-required parameters are not set.
If you are using IPR, we STRONGLY RECOMMEND TO SET A MAXIMUM TIME since this is the core stopping criteria on IPR. |
The code has been modernized using C++20 facilities like concepts and ranges. Therefore, your compiler must support C++20 now.
One notable change was substituting the custom code in randInt()
for a
standard library uniform distribution
utility.
The old code was used when a custom Mersenne Twister RNG code was used (from
the original BRKGA implementation). The inclusion of the Mersenne Twister in
the standard library allows us to use all default utilities. Ad hoc tests show
that the standard library utilities are faster than the old custom code.
However, the speed-up is marginal when considering the full application of
BRKGA. But, when we accumulate hundreds or thousands of calls daily, the time
savings can be considerable in a full year of operation (which usually
translates into energy savings).
In version 2.0, BRKGA-MP-IPR also deals with multiple objectives in a lexicographical or priority dominance order. Differing from classical non-dominance order (using Pareto frontiers), the lexicographical order defines a strict preference order among the objective functions. This leads us to a partial ordering of the values of the solutions (composed of several values, each one from one objective function). So, we have the following definition (abusing a little bit of notation).
📝 Definition |
---|
Let |
For instance, let's assume we have three minimizing objective functions and four solutions described in the following table:
Solution | |||
---|---|---|---|
A | 50 | 30 | 30 |
B | 30 | 55 | 40 |
C | 30 | 20 | 50 |
D | 30 | 20 | 25 |
Note that Solution B is better than Solution A because
If you really want an algorithm to produce a non-dominated set of solutions (Pareto frontier), this is NOT the right algorithm for you. We recommend taking a look at the NSGA-II and MOAB. |
One of the nice additions to BRKGA-MP-IPR 2.0 is the capability of performing the mating in parallel. Such capability speeds up the algorithm substantially, mainly for large chromosomes and large populations. However, when performing parallel mating, we have some points regarding reproducibility described in Section Multi-thread mating in the tutorial.
Due to the inclusion of multi-objective optimization capabilities, the user now must
define a type fitness_t
, and his/her decoder must return such a type.
The user can do things like this:
Please, check for details in Sections "TL;DR - Multi objective" and "Using BRKGA-MP-IPR on multi-objective mode" from the tutorial.
In the previous versions, setInitialPopulation()
fills up only the first
population, discarding additional warm solutions if the population is full.
Now, setInitialPopulation()
fills up all populations.
More details here.
injectChromosome()
does not accept initial fitness value anymore. Now, injectChromosome()
triggers the decoder in all cases (and therefore, time must be measured on its call).
More details here.
BRKGA-MP-IPR now uses some features of C++17 standards. Therefore, you must change your building tools to support that.
BrkgaParams::mutants_percentage
should be of typedouble
notunsigned
(issue #1). Thanks @sohaibafifi.- Fix the shorter method call for
BRKGA_MP_IPR::pathRelinking
(pull #1). Thanks @afkummer.
BRKGA-MP-IPR is a header-only framework, which means that you only need to download the header files and tell your compiler to include the path to where the files were downloaded.
Quick example (unix): assume we are in an empty folder. So, we copy/clone BRKGA-IPR-MP first:
$ git clone https://github.com/ceandrade/brkga_mp_ipr_cpp
Cloning into 'brkga_mp_ipr_cpp'...
remote: Enumerating objects: 118, done.
remote: Counting objects: 100% (118/118), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 118 (delta 24), reused 0 (delta 0)
Receiving objects: 100% (118/118), 3.50 MiB | 3.66 MiB/s, done.
Resolving deltas: 100% (24/24), done.
Let's write a test.cpp
with the following code:
#include "brkga_mp_ipr.hpp"
#include <iostream>
int main() {
std::cout << "Testing sense: " << BRKGA::Sense::MINIMIZE;
return 0;
}
Then, let's compile and see it works:
$ g++ --version
g++ (MacPorts gcc12 12.3.0_0+stdlib_flag) 12.3.0
$ g++ -std=c++20 -Ibrkga_mp_ipr/brkga_mp_ipr test.cpp -o test
$ ./test
Testing sense: MINIMIZE
Note the Git clones the whole repository that contains the library code, documents, and examples. All the examples were built using GNU/Make and GCC toolchain. However, the code is standard C++, and we can quickly adapt it to other toolchains such as Clang, Microsoft, or Intel toolchains.
The best way to keep it short is to look in the on examples on
the git repo.
Let's start solving the traditional single-objective
Traveling Salesman Problem (TSP).
First, we must ensure that
BRKGA::fitness_t
has the right single-object type. Let's take a look at the trimmed version of
fitness_type.hpp
.
This is a trimmed copy:
#include <limits>
#include <tuple>
namespace BRKGA {
using fitness_t = double;
//...
} // end namespace BRKGA_MP_IPR
Here, fitness_t
defines the type of the objective function value. In the vast
majority of the cases, double
suffices. Let's take a look into the main call
main_minimal.cpp.
This is a trimmed copy:
#include "tsp/tsp_instance.hpp"
#include "decoders/tsp_decoder.hpp"
#include "brkga_mp_ipr.hpp"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
int main(int argc, char* argv[]) {
if(argc < 4) {
cerr
<< "Usage: " << argv[0]
<< " <seed> <config-file> <maximum-running-time>"
<< " <tsp-instance-file>"
<< endl;
return 1;
}
try {
////////////////////////////////////////
// Read command-line arguments and the instance
////////////////////////////////////////
const unsigned seed = stoi(argv[1]);
const string config_file = argv[2];
const string instance_file = argv[4];
const unsigned num_threads = 4;
cout << "Reading data..." << endl;
auto instance = TSP_Instance(instance_file);
////////////////////////////////////////
// Read algorithm parameters
////////////////////////////////////////
cout << "Reading parameters..." << endl;
auto [brkga_params, control_params] =
BRKGA::readConfiguration(config_file);
// Overwrite the maximum time from the config file.
control_params.maximum_running_time = chrono::seconds {stoi(argv[3])};
////////////////////////////////////////
// Build the BRKGA data structures
////////////////////////////////////////
cout << "Building BRKGA data and initializing..." << endl;
TSP_Decoder decoder(instance);
BRKGA::BRKGA_MP_IPR<TSP_Decoder> algorithm(
decoder, BRKGA::Sense::MINIMIZE, seed,
instance.num_nodes, brkga_params, num_threads
);
////////////////////////////////////////
// Find good solutions / evolve
////////////////////////////////////////
cout << "Running for " << control_params.maximum_running_time << "..."
<< endl;
const auto final_status = algorithm.run(control_params, &cout);
cout
<< "\nAlgorithm status: " << final_status
<< "\n\nBest cost: " << final_status.best_fitness
<< endl;
}
catch(exception& e) {
cerr
<< "\n" << string(40, '*') << "\n"
<< "Exception Occurred: " << e.what()
<< "\n" << string(40, '*')
<< endl;
return 1;
}
return 0;
}
You can identify the following basic steps:
-
Create a data structure to hold your input data. This object should be passed to the decoder object/functor (example
tsp/tsp_instance.hpp
); -
Certify that BRKGA::fitness_t has the correct type;
-
Implement a decoder object/functor. This function translates a chromosome (array of numbers in the interval [0, 1]) to a solution for your problem. The decoder must return the solution value or cost to be used as fitness by BRKGA (example
decoders/tsp_decoder.hpp
); -
Load the instance and other relevant data;
-
Read the algorithm parameters using
BRKGA::readConfiguration()
; or create aBRKGA::BrkgaParams
andBRKGA::ControlParams
objects by hand; -
Create an
BRKGA::BRKGA_MP_IPR
algorithm object; -
Call
BRKGA::BRKGA_MP_IPR::run()
to optimize.
main_minimal.cpp
provides a very minimal example to understand the necessary steps to use the
BRKGA-MP-IPR framework. However,
main_complete.cpp
provides a full-featured code, handy for scientific use, such as
experimentation and paper writing. This code allows fine-grained control of
the optimization, shows several features of BRKGA-MP-IPR such as the resets,
chromosome injection, and others. It also logs
all optimization steps, creating outputs easy to be parsed. You should use
this code for serious business and experimentation.
These are the basic steps, but I do recommend the full reading of the guide.
Again, if you really want an algorithm to produce a non-dominated set of solutions (Pareto frontier), this is NOT the right algorithm for you. We recommend taking a look at the NSGA-II and MOAB. |
To use BRKGA-MP-IPR in the multi-objective mode, we first must set
BRKGA::fitness_t
according to the number of objectives we want. In the
repo example,
we consider the TSP with two objectives: first, we must minimize the total tour
length, and second, the size of the largest edge in the tour. For that, we must
change the file
fitness_type.hpp
to reflect such a thing. In this example, we use the standard std::tuple
:
#include <limits>
#include <tuple>
namespace BRKGA {
using fitness_t = std::tuple<double, double>;
//...
} // end namespace BRKGA_MP_IPR
In this case, the first component of the tuple holds the tour length, and the
second contains the largest edge. On Section
Using BRKGA-MP on multi-objective mode,
we talk with more details about multi-objective problems. Just keep in mind,
although you could use any type for your fitness_t
, you should prefer to use
std::tuple
.
The remaining code is almost identical to the single-objective. The only differences are in computing the largest edge, and printing such information on the main call. All the steps described briefly in the previous section are also used here.
We provide a very thorough and easy-to-follow tutorial on using BRKGA effectively. We strongly recommend you read the tutorial to make your code works at best within BRKGA.
Check out the tutorial and API documentation: https://ceandrade.github.io/brkga_mp_ipr_cpp
BRKGA-MP-IPR uses a permissive BSD-like license and it can be used as it pleases you. And since this framework is also part of an academic effort, we kindly ask you to remember to cite the originating paper of this work. Indeed, Clause 4 estipulates that "all publications, softwares, or any other materials mentioning features or use of this software (as a whole package or any parts of it) and/or the data used to test it must cite the following article explicitly":
C.E. Andrade. R.F. Toso, J.F. Gonçalves, M.G.C. Resende. The Multi-Parent Biased Random-key Genetic Algorithm with Implicit Path Relinking. European Journal of Operational Research, volume 289, number 1, pages 17–30, 2021. DOI 10.1016/j.ejor.2019.11.037
If you are using the multi-objective version, you must also cite this paper:
C.E. Andrade, L.S. Pessoa, S. Stawiarski. The Physical Cell Identity Assignment Problem: a Multi-objective Optimization Approach. IEEE Transactions on Evolutionary Computation, to appear, 2022. DOI: 10.1109/TEVC.2022.3185927.
You may also consider to cite the following papers from people that helped to find bugs and develop new features for BRKGA-MP-IPR 2.0:
A.F. Kummer N., L.S. Buriol, O.C.B. de Araujo. A biased random key genetic algorithm applied to the VRPTW with skill requirements and synchronization constraints. Proceedings of the 2020 Genetic and Evolutionary Computation Conference (GECCO '20), pages 717-724, 2020. DOI: 10.1145/3377930.3390209.
You can download all references for this 📂 Bibtex, or this 📂 RIS files.
Check it out the full license.
- Alberto Kummer, 2021 (parallel mating).
- Daniele Ferone, 2023 (bug fix on IPR).
CI and tests side:
- Implement unit tests and certify coverage;
- Configure Travis-Ci correctly, such that we can run tests on Mac OSX and Windows too.