This is a short guide to writing new functions for the MOEADr package. This package provides a component-based framework for developing (and applying) Multiobjective Evolutionary Algorithms based on Decomposition (MOEA/D)1.
The modular implementation provided in this package provides control over the following aspects of the algorithm:
This document describes how to write functions implementing new variants for any of these modules. A general description of the algorithm and the component-based interpretation behind the MOEADr package is available in our paper2
Functions should be preferably defined in the form verb_object (e.g., generate_weights or evaluate_population)
Please try to follow the policy one function, one file, same name (very short functions for general use can be exceptions - in this case they should be placed, e.g., in the utils.R
file.
When passing variables between functions, there are three main rules that should be observed:
Unless absolutely impossible, functions should always receive all variables used in the function body via formal arguments, plus whatever other variables may be used downstream using the ...
input.
If it is absolutely necessary, a function can eventually access variables from parent frames using, for instance, parent.frame()$variable_name
, but this is not encouraged. It is definitely not kosher for functions to directly modify variables in the parent environment by any means except by formal, explicit outputs. Previous (development) versions of the MOEADr
package used to allow it, but the resulting confusion (particularly when writing unit tests or debugging) heavily outweighted the benefits.
Functions should, with few exceptions, be able to handle any number of “useless” variables that may be passed to them (using the ...
formal argument).
Documentation should be complete. Please use roxygen2
-style documentation in all functions. This version of the package uses roxygen2
version 6.0.1 (which means some degree of markdown support and other conveniences).
Also, please make liberal use of in-code comments to clarify any non-trivial operation.
To discover the available decomposition strategies, use get_decomposition_methods()
. Decomposition functions are called from within generate_weights()
.
?moead
and ?decomposition_sld
to get details on the structure of decomp). Any other required inputs should be explicitly declared in the function definition.decomp$name
).get_decomposition_methods()
won’t be able to correctly list the method).Check decomposition_sld.R for a good example of decomposition routine (e.g., to use as a template).
To discover the available decomposition strategies, use get_scalarization_methods()
. Scalarization functions are called from within scalarize_values()
.
?moead
and ?scalarization_pbi
to get details on the structure of aggfun). Any other required inputs (e.g., W, Y, etc.) should be explicitly declared in the function definition.get_scalarization_methods()
won’t be able to correctly list the method.Check scalarize_pbi.R for a good example of decomposition routine (e.g., to use as a template).
The strategy for defining the neighborhood structure in the MOEADr package is essentially the same (use Euclidean distances and use the neighbors$T
nearest subproblems as a neighborhood). The only difference is the space in which the distances are calculated, which has implications in the need for re-calculating the neighborhood structure. The neighborhoods are defined using an efficient C implementation of the k-nearest-neighbors algorithm available in function FNN::get.knn
, which is the only reason why package MOEADr
lists FNN
in its Imports field (see DESCRIPTION).
The neighborhood assignment function is define_neighborhood()
, which is called directly from the main function moead()
.
?moead
and ?define_neighborhood
to get details on the structure of aggfun). Any other required inputs should be explicitly declared in the function definition.neighbors$T - 1
elements are the neighbor indices). This is a N x T matrix.1 / N
.define_neighborhood
. Other possibilities (e.g., to deal with adaptive weights, which would require periodic recalculation) can, at least in principle, use the same strategy. However, if an alternative assignment method becomes too different from the one currently implemented, it may be better to break the options and use the one function, one file, same name policy. In this case, the current options should be moved to independent functions starting with a common preffix (as is the case with other modules, e.g., decomposition).Check define_neighborhood.R for the current neighborhood assignment alternatives (e.g., to use as a template).
To discover the available variation operators, use get_variation_operators()
. Variation methods are called from within perform_variation()
.
?moead
and ?perform_variation
to get details on the structure of variation). Any other required inputs should be explicitly declared in the function definition.X
containing the (modified) points, or a list object containing the matrix X
as well as a counter nfe
, containing the number of additional function evaluations performed by that operator..variation_localsearch()
. Local search methods should follow the naming convention ls_XYZ, and available methods are discovered using get_localsearch_methods()
. See ?variation_localsearch
and the Variation section of ?moead
for details.get_variation_operators()
and get_localsearch_methods()
won’t be able to correctly list the method.Check variation_sbx.R for a good example of a non-local search variation operator, and variation_localsearch.R and ls_dvls.R for local search methods (e.g., to use as a template).
To discover the available decomposition strategies, use get_update_methods()
. Update functions are called from within update_population()
.
?moead
and ?update_population
to get details on the structure of update). Any other required inputs should be explicitly declared in the function definition.get_update_methods()
won’t be able to correctly list the method.Check update_standard.R for a good example of update routine (e.g., to use as a template).
To discover the available constraint handling strategies, use get_constraint_methods()
. Constraint handling methods are called from within order_neighborhood()
.
?moead
to get details on the structure of constraint). Any other required inputs should be explicitly declared in the function definition.get_constraint_methods()
won’t be able to correctly list the method.Check constraint_penalty.R for a good example of constraint handling routine (e.g., to use as a template).
The strategies for objective scaling currently available in the MOEADr package are essentially “none” (i.e., no scaling) and “simple” (simple linear scaling to the interval [0,1]
).The scaling function is scale_objectives()
.
?moead
and ?scale_objectives
to get details on the structure of scaling). Any other required inputs should be explicitly declared in the function definition.0
s and a vector of 1
s, if any scaling different from “none” is used).scale_objectives()
. Other possibilities can, at least in principle, use the same strategy. However, if an alternative assignment method becomes too different from the one currently implemented, it may be better to break the options and use the one function, one file, same name policy. In this case, the current options should be moved to independent functions starting with a common preffix (as is the case with other modules, e.g., decomposition).To discover the available termination criteria, use get_stop_criteria()
. Termination methods are called from within check_stop_criteria()
.
?moead
and ?get_stop_criteria
to get details on the structure of stopcrit). Any other required inputs should be explicitly declared in the function definition.TRUE
) or not (FALSE
).get_stop_criteria()
won’t be able to correctly list the method.Check stop_maxiter.R for a good example of constraint handling routine (e.g., to use as a template).