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).