scieee AI-readable full text Open interactive document viewer

jMetalPy: a Python Framework for Multi-Objective Optimization with Metaheuristics

Benítez Hidalgo, Antonio; Nebro, Antonio J.; García Nieto, José Manuel; Oregi, Izaskun; Ser, Javier del

Abstract

This paper describes jMetalPy, an object-oriented Python-based framework for multi-objective optimization with metaheuristic techniques. Building upon our experiences with the well-known jMetal framework, we have developed a new multi-objective optimization software platform aiming not only at replicating the former one in a di erent programming language, but also at taking advantage of the full feature set of Python, including its facilities for fast prototyping and the large amount of available libraries for data processing, data analysis, data visualization, and high-performance computing. As a result, jMetalPy provides an environment for solving multi-objective optimization problems focused not only on traditional metaheuristics, but also on techniques supporting preference articulation and dynamic problems, along with a rich set of features related to the automatic generation of statistical data from the results generated, as well as the real-time and interactive visualization of the Pareto front approximations produced by the algorithms. jMetalPy o ers additionally support for parallel computing in multicore and cluster systems. We include some use cases to explore the main features of jMetalPy and to illustrate how to work with it.

Full text

jMetalPy: a Python Framework for Multi-Objective Optimization with Metaheuristics Antonio Ben´ ıtez-Hidalgoa, Antonio J. Nebroa, Jos´ e Garc´ ıa-Nietoa, Izaskun Oregib, Javier Del Serb,c,d aDepartamento de Lenguajes y Ciencias de la Computaci´on, Ada Byron Research Building, University of M´alaga, 29071 M´alaga, Spain bTECNALIA, 48160 Derio, Spain cUniversity of the Basque Country (UPV/EHU), 48013 Bilbao, Spain dBasque Center for Applied Mathematics (BCAM), 48009 Bilbao, Spain Abstract This paper describes jMetalPy, an object-oriented Python-based framework for multi-objective optimization with metaheuristic techniques. Building upon our experiences with the well-known jMetal framework, we have developed a new multi-objective optimization software platform aiming not only at replicating the former one in a different programming language, but also at taking advantage of the full feature set of Python, including its facilities for fast prototyping and the large amount of available libraries for data processing, data analysis, data visualization, and high-performance computing. As a result, jMetalPy provides an environment for solving multi-objective optimization problems focused not only on traditional metaheuristics, but also on techniques supporting preference articulation and dynamic problems, along with a rich set of features related to the automatic generation of statistical data from the results generated, as well as the real-time and interactive visualization of the Pareto front approximations produced by the algorithms. jMetalPy offers additionally support for parallel computing in multicore and cluster systems. We include some use cases to explore the main features of jMetalPy and to illustrate how to work with it. Keywords: Multi-Objective Optimization, Metaheuristics, Software Framework, Python, Statistical Analysis, Visualization 1. Introduction Multi-objective optimization problems are widely found in many disciplines [1, 2], including engineering, economics, logistics, transportation or energy, among others. They are characterized by having two or more conflicting objective functions that have to be maximized or minimized at the same time, with their optimum composed by a set of trade-offsolutions known as Pareto optimal set. Besides having several objectives, other factors can make this family of optimization problems particularly difficult to tackle and solve with exact techniques, such as deceptiveness, epistasis, NP-hard complexity, or high dimensionality [3]. As a consequence, the most popular techniques to deal with complex multi-objective optimization problems are metaheuristics [4], a family of non-exact algorithms including evolutionary algorithms and swarm intelligence methods (e.g. ant colony optimization or particle swarm optimization). An important factor that has ignited the widespread adoption of metaheuristics is the availability of software tools easing their implementation, execution and deployment in practical setups. In the context of multi-objective optimization, one of the most acknowledged frameworks is jMetal [5], a project started in 2006 that has been continuously evolving since then, including a full redesign from scratch in 2015 [6]. jMetal is implemented in Java under the MIT licence, and its source code is Email addresses: [email protected] (Antonio Ben´ ıtez-Hidalgo), [email protected] (Antonio J. Nebro), [email protected] (Jos´ e Garc´ ıa-Nieto), [email protected] (Izaskun Oregi), [email protected] (Javier Del Ser) publicly available in GitHub1. In this paper, we present jMetalPy, a new multi-objective optimization framework written in Python. Our motivation for developing jMetalPy stems from our past experience with jMetal and from the fact that nowadays Python has become a very prominent programming language with a plethora of interesting features, which enables fast prototyping fueled by its large ecosystem of libraries for numerical and scientific computing (NumPy [7], Scipy [8]), data analysis (Pandas), machine learning (Scikit-learn [9]), visualization (Matplotlib [10], Holoviews [11], Plotly [12]), large-scale processing (Dask [13], PySpark [14]) and so forth. Our goal is not only to rewrite jMetal in Python, but to focus mainly on aspects where Python can help fill the gaps not covered by Java. In particular, we place our interest in the analysis of results provided by the optimization algorithms, real-time and interactive visualization, preference articulation for supporting decision making, and solving dynamic problems. Furthermore, since Python can be thought of as a more agile programming environment for prototyping new multi-objective solvers, jMetalPy also incorporates a full suite of statistical significance tests and related tools for the sake of a principled comparison among multi-objective metaheuristics. jMetalPy has been developed by Computer Science engineers and scientists to support research in multi-objective optimization with metaheuristics, and to utilize the provided algorithms for solving real-word problems. Following the same 1jMetal: https://github.com/jMetal/jMetal. As of April 18, 2019, the papers about jMetal had accumulated more than 1280 citations (source: Google Scholar) Preprint submitted to journal (under review) April 18, 2019 open source philosophy as in jMetal, jMetalPy is released under the MIT license. The project is in continuous development, with its source code hosted in GitHub2, where the last stable and current development versions can be freely obtained. The main features of jMetalPy are summarized as follows: •jMetalPy is implemented in Python (version 3.6+), and its object-oriented architecture makes it flexible and extensible. •It provides a set of classical multi-objective metaheuristics (NSGA-II [15], GDE3 [16], SMPSO [17], OMOPSO [18], MOEA/D [19]) and standard families of problems for benchmarking (ZDT, DTLZ, WFG [2], and LZ09 [20]). •Dynamic multi-objective optimization is supported, including the implementation of dynamic versions of NSGA-II and SMPSO, as well as the FDA [21] problem family. •Reference point based preference articulation algorithms, such as SMPSO/RP [22] and versions of NSGA-II and GDE3, are also provided. •It implements quality indicators for multi-objective optimization, such as Hypervolume [23], Additive Epsilon [24] and Inverted Generational Distance [25]. •It provides visualization components to display the Pareto front approximations when solving problems with two objectives (scatter plot), three objectives (scatter plot 3D), and many-objective problems (parallel coordinates graph and a tailored version of Chord diagrams). •Support for comparative studies, including a wide number of statistical tests and utilities (e.g. non-parametric test, posthoc tests, boxplots, CD plot), including the automatic generation of L A T EX tables (mean, standard deviation, median, interquartile range) and figures in different formats. •jMetalPy can cooperatively work alongside with jMetal. The latter can be used to run algorithms and compute the quality indicators, while the post-processing data analysis can be carried out with jMetalPy. •Parallel computing is supported based on Apache Spark [26] and Dask [13]. This includes an evaluator component that can be used by generational metaheuristics to evaluate solutions in parallel with Spark (synchronous parallelism), as well as a parallel version of NSGA-II based on Dask (asynchronous parallelism). •Supporting documentation. A website3is maintained with user manuals and API specification for developers. This site also contains a series of Jupyter notebooks4with use cases and examples of experiments and visualizations. 2jMetalPy: https://github.com/jMetal/jMetalPy 3jMetalPy documentation: https://jmetalpy.readthedocs.io 4Jupyter: https://jupyter.org Our purpose of this paper is to describe jMetalPy, and to illustrate how it can be used by members of the community interested in experimenting with metaheuristics for solving multiobjective optimization problems. To this end, we include some implementation use cases based on NSGA-II to explore the main variants considered in jMetalPy, from standard versions (generational and steady state), to dynamic, reference-point based, parallel and distributed flavors of this solver. A experimental use case is also described to exemplify how the statistical tests and visualization tools included in jMetalPy can be used for post-processing and analyzing the obtained results in depth. For background concepts and formal definitions of multi-objective optimization, we refer to our previous work in [5]. The remaining of this paper is organized as follows. In Section 2, a review of relevant related algorithmic software platforms is conducted to give an insight and rationale of the main differences and contribution of jMetalPy. Section 3 delves into the jMetalPy architecture and its main components. Section 4 explains a use case of implementation. Visualization facilities are described in Section 5, while a use case of experimentation with statistical procedures is explained in Section 6. Finally, Section 7 presents the conclusions and outlines further related work planned for the near future. 2. Related Works In the last two decades, a number of software frameworks devoted to the implementation of multi-objective metaheuristics has been contributed to the community, such as ECJ [33], EvA [34], JCLEC-MO [35], jMetal [5, 6], MOEA Framework [36], and Opt4J [37], which are written in Java; ParadisEOMOEO [38], and PISA [39], developed in C/C++; and PlatEMO [40], implemented in Matlab. They all have in common the inclusion of representative algorithms from the the state of the art, benchmark problems and quality indicators for performance assessment. As has been mentioned in the introduction, there is a growing interest within the scientific community in software frameworks implemented in Python, since this language offers a large ecosystem of libraries, most of them devoted to data analysis, data processing and visualization. When it comes to optimization algorithms, a set of representative Python frameworks is listed in Table 1, where they are analyzed according to their algorithmic domains, maintenance status, Python version and licensing, as well as the featured variants, post-processing facilities and algorithms they currently offer. With the exception of the Inspyred framework, they are all active projects (i.e., their public source code have been updated at least one time within the last six months) and work out-of-the-box with a simple pip command. All of these frameworks support Python 3.x. DEAP and Inspyred are not centered in multi-objective optimization, and they include a shorter number of implemented algorithms. Pagmo/PyGMO, Platypus and Pymoo offer a higher number of features and algorithmic variants, including methods for statistical post-processing and visualization of results. In particular, Pagmo/PyGMO contains implementations of a number of single/multi-objective algorithms, including hybrid vari2 Table 1: Most popular optimization frameworks written in Python. Name Status Python License Parallel Dynamic Decision Post-processing Algorithms version processing optimization making facilities DEAP 1.2.2 [27] Active ≥2.7 LGPL-3.0 XStatistics GA, GP, CMA-ES, NSGA-II, SPEA2, MO-CMA-ES Geatpy 1.1.5 [28] Active ≥3.5 MIT GA, MOEA Inspyred 1.0.1 [29] Inactive ≥2.6 MIT GA, ES, PSO, ACO, SA, PAES, NSGA-II PyGMO 2.10 [30] Active 3.x GPL-3.0 XVisualization, statistics GA, DE, PSO, SA, ABC, IHS, MC, CMA-ES, NSGA-II, MOEA/D Platypus 1.0.3 [31] Active 3.6 GPL-3.0 XVisualization, statistics CMA-ES, NSGA-II, NSGA-III, GDE3, IBEA, MOEA/D, OMOPSO, EpsMOEA, SPEA2 Pymoo 0.2.4 [32] Active 3.6 Apache 2.0 XVisualization, statistics GA, DE, NSGA-II, NSGA-III, U-NSGA-III, reference point (R-NSGA-III) jMetalPy 1.0.0 Active ≥3.6 MIT X X X Visualization, statistics GA, EA, NSGA-II, NSGA-III, SMPSO, GDE3, OMOPSO, MOEA/D, reference point (G-NSGA-II, SMPSO/RP, G-GDE3), dynamic (NSGA-II, SMPSO, GDE3) ants, with statistical methods for racing algorithms, quality indicators and fitness landscape analysis. Platypus supports parallel processing in solution evaluation phase, whereas Pymoo is rather focused on offering methods for preference articulation based on reference points. The jMetalPy framework we proposed in this paper is also an active open source project, which is focused mainly on multiobjective optimization (although a number of single-objective algorithms are included) providing an increasing number of algorithms and modern methods for statistical post-processing and visualization of results. It offers algorithmic variants with methods for parallel processing and preference articulation based on reference points to provide decision making support. Moreover, jMetalPy incorporates algorithms and mechanisms for dynamic problem optimization, which is an additional feature not present in the other related frameworks. In this way, the proposed framework attempts at covering as many enhancing features in optimization as possible to support experimentation and decision making in both research and industry communities. Besides these features, an important design goal in jMetalPy has been to make the code easy to understand (in particular, the implementation of the algorithms), to reuse and to extend, as is illustrated in the next two sections. 3. Architecture of jMetalPy The architecture of jMetalPy has an object-oriented design to make it flexible and extensible (see Figure 1). The core classes define the basic functionality of jMetalPy: an Algorithm solves a Problem by using some Operator entities which manipulate a set of Solution objects. We detail these classes next. 3.1. Core Architecture Class Algorithm contains a list of solutions (i.e. population in Evolutionary Algorithms or swarm in Swarm Intelligence techniques) and a run() method that implements the behavior of a generic metaheuristic (for the sake of simplicity, full details of the codes are omitted): 1class Algorithm(ABC): def __init__(self): 3self.evaluations = 0 self.solutions = List[] 5self.observable = DefaultObservable() 7def run(self): self.solutions = self.create_initial_solutions() 9self.solutions = self.evaluate(self.solutions) self.init_progress() 11 while not self.stopping_condition_is_met(): self.step() 13 self.update_progress() In the above code we note the steps of creating the initial set of solutions, their evaluation, and the main loop of the algorithm, which performs a number of steps until a stopping condition is met. The initialization of state variables of an algorithm and their update at the end of each step are carried out in the init progress() and update progress() methods, respectively. In order to allow the communication of the status of an algorithm while running we have adopted the observer pattern [41], so that any algorithm is an observable entity which notifies to registered observers some information specified in advance (e.g., the current evaluation number, running time, or the current solution list), typically in the update progress() method. In this way we provide a structured method, for example, to display in real-time the current Pareto front approximation or to store it in a file. A problem is responsible of creating and evaluating solutions, and it is characterized by its number of decision variables, objectives and constraints. In case of the number of constraints be greater than 0, it is assumed that the evaluate() method also assesses whether the constraints are fulfilled. Subclasses of Problem include additional information depending of the assumed solution encoding; thus, a FloatProblem (for numerical optimization) or an IntegerProblem (for combinatorial optimization) requires the specification of the lower and upper bounds of the decision variables. Operators such as Mutation,Crossover, and Selection, have an execute(source) method which, given a source object, produces a result. Mutations operate on a solution and return a new one resulting from modifying the original one. On the contrary, crossover operators take a list of solutions (namely, the parents) and produce another list of solutions (correspondingly, the offspring). Selection operators usually receive a list of solutions 3 Manage 1 Solve 1 Create/Evaluate * Manipulate * Use * interface Algorithm + evaluations: int = 0 + solutions: List[Solution] = list() + observable = DefaultObservable() +init progress() +step() +update progress() + run() + get result() interface Solution + number of objectives: int + number of variables: int interface Problem + number of objectives: int + number of variables: int + number of constraints: int +create solution() +evaluate(solution: Solution) interface Operator +execute(solution: Solution) Figure 1: UML class diagram of jMetalPy. and returns one of them or a sublist of them. The Solution class is a key component in jMetalPy because it is used to represent the available solution encodings, which are linked to the problem type and the operators that can be used to solve it. Every solution is composed by a list of variables, a list of objective values, and a set of attributes implemented as a dictionary of key-value pairs. Attributes can be used to assign, for example, a rank to the solutions of population or a constraint violation degree. Depending on the type of the variables, we have subclasses of Solution such as FloatSolution, IntegerSolution,BinarySolution or PermutationSolution. 3.2. Classes for Dynamic Optimization jMetalPy supports dealing with dynamic optimization problems, i.e., problems that change over time. For this purpose, it contains two abstract classes named DynamicProblem and DynamicAlgorithm. A dynamic algorithm is defined as an algorithm with a restarting method, which is called whenever a change in the problem being solved is detected. The code of the DynamicAlgorithm class is as follows: 1class DynamicAlgorithm(Algorithm, ABC): 3@abstractmethod def restart(self) -> None: 5pass The DynamicProblem class extends Problem with methods to query whether the problem has changed whatsoever, and to clear that status: 1class DynamicProblem(Problem, Observer, ABC): 3@abstractmethod def the_problem_has_changed(self) -> bool: 5pass 7@abstractmethod def clear_changed(self) -> None: 9pass It is worth mentioning that a dynamic problem is also an observer entity according to the observer pattern. The underlying idea is that in jMetalPy it is assumed that changes in a dynamic problem are produced by external entities, i.e, observable objects where the problem is registered. 4. Implementation Use Case: NSGA-II and Variants With the aim of illustrating the basic usages of jMetalPy, in this section we describe the implementation of the well-known NSGA-II algorithm [15], as well as some of its variants (steadystate, dynamic, with preference articulation, parallel, and distributed). NSGA-II is a genetic algorithm, which is a subclass of Evolutionary Algorithms. In jMetalPy we include an abstract class for the latter, and a default implementation for the former. An Evolutionary Algorithm is a metaheuristic where the step() method consists of applying a sequence of selection, reproduction, and replacement methods, as illustrated in the code snippet below: 1class EvolutionaryAlgorithm(Algorithm, ABC): def __init__(self, 3problem: Problem, population_size: int, 5offspring_size: int): super(EvolutionaryAlgorithm, self).__init__() 7self.problem = problem self.population_size = population_size 9self.offspring_size = offspring_size 11 @abstractmethod def selection(self, population): 13 pass 15 @abstractmethod def reproduction(self, population): 17 pass 19 @abstractmethod def replacement(self, population, offspring): 21 pass 23 def init_progress(self): self.evaluations = self.population_size 25 def step(self): 27 mating_pool = self.selection(self.solutions) offspring = self.reproduction(mating_pool) 29 offspring = self.evaluate(offspring) self.solutions = self.replacement(self.solutions, offspring) 31 def update_progress(self): 33 self.evaluations += self.offspring_size On every step, the selection operator is used (line 27) to retrieve the mating pool from the solution list (the population) of the algorithm. Solutions of the mating pool are taken for reproduction (line 28), which yields a new list of solutions called offspring. Solutions of this offspring population must be evaluated (line 29), and thereafter a replacement strategy is applied to update the population (line 30). We can observe that the evaluation counter is initialized and updated in the init progress() (line 23) and update progress (line 32), respectively. 4 The EvolutionaryAlgorithm class is very generic. We provide a complete implementation of a Genetic Algorithm, which is an evolutionary algorithm where the reproduction is composed by combining a crossover and mutation operator. We partially illustrate this implementation next: 1class GeneticAlgorithm(EvolutionaryAlgorithm): def __init__(self, 3problem: Problem[Solution], population_size: int, 5offspring_population_size: int, mutation: Mutation, 7crossover: Crossover, selection: Selection, 9termination_criterion: TerminationCriterion, population_generator=RandomGenerator(), 11 population_evaluator=SequentialEvaluator()): ... 13 def create_initial_solutions(self): 15 return [self.population_generator.new(self.problem) for _in range(self.population_size)] 17 def evaluate(self, solutions): 19 return self.population_evaluator.evaluate(solutions, self.problem) 21 def stopping_condition_is_met(self): return self.termination_criterion.is_met 23 def selection(self, population: List[Solution]): 25 # select solutions to get the mating pool 27 def reproduction(self, mating_pool): # apply crossover and mutation 29 def replacement(self, population, offspring): 31 # combine the population and offspring populations There are some interesting features to point out here. First, the initial solution list is created from a Generator object (line 14), which, given a problem, returns a number of new solutions according to some strategy implemented in the generator; by default, a RandomGenerator() is chosen to produce a number of solutions uniformly drawn at random from the value range specified for the decision variables. Second, an Evaluator object is used to evaluate all produced solutions (line 19); the default one evaluates the solutions sequentially. Third, a TerminationCriterion object is used to check the stopping condition (line 21), which allows deciding among several stopping criteria when configured. The provided implementations include: stopping after making a maximum number of evaluations, computing for a maximum time, a key has been pressed, or the current population achieves a minimum level of quality according to some indicator. Fourth, the reproduction method applies the crossover and mutation operators over the mating pool to generate the offspring population. Finally, the replacement method combines the population and the offspring population to produce a new population. Departing from the implemented GeneticAlgorithm class, we are ready to implement the standard NSGA-II algorithm and some variants, which will be described in the next subsections. Computing times will be reported when running the algorithm to solve the ZDT1 benchmark problem [42] on a MacBook Pro with macOS Mojave, 2.2 GHz Intel Core i7 processor (Turbo boost up to 3.4GHz), 16 GB 1600 MHz DDR3 RAM, Python 3.6.7 :: Anaconda. 4.1. Standard Generational NSGA-II NSGA-II is a generational genetic algorithm, so the population and the offspring population have the same size. Its main feature is the use of a non-dominated sorting for ranking the solutions in a population to foster convergence, and a crowding distance density estimator to promote diversity [15]. These mechanisms are applied in the replacement method, as shown in the following snippet: 1class NSGAII(GeneticAlgorithm): def __init__(self, 3problem: Problem, population_size, 5offspring_size, mutation: Mutation, 7crossover: Crossover, selection: Selection, 9termination_criterion: TerminationCriterion, population_generator=RandomGenerator(), 11 population_evaluator=SequentialEvaluator() dominance_comparator=DominanceComparator()): 13 ... def replacement(self, population, offspring): 15 join_population = population + offspring 17 return RankingAndCrowdingDistanceSelection( self.population_size, self.dominance_comparator).execute( join_population) No more code is needed. To configure and run the algorithm we include some examples, such as the following code: # Standard generational NSGAII runner 2problem = ZDT1() 4max_evaluations = 25000 algorithm = NSGAII( 6problem=problem, population_size=100, 8offspring_population_size=100, mutation=PolynomialMutation(...), 10 crossover=SBXCrossover(...), selection=BinaryTournamentSelection(...), 12 termination_criterion=StoppingByEvaluations(max=max_evaluations), dominance_comparator=DominanceComparator() 14 ) 16 progress_bar = ProgressBarObserver(max=max_evals) algorithm.observable.register(observer=progress_bar) 18 real_time = VisualizerObserver() 20 algorithm.observable.register(observer=real_time) 22 algorithm.run() front = algorithm.get_result() 24 # Save results to file 26 print_function_values_to_file(front, ‘FUN’) print_variables_to_file(front, ‘VAR’) This code snippet depicts a standard configuration of NSGAII to solve the ZDT1 benchmark problem. Note that we can define a dominance comparator (line 13), which by default is the one used in the standard implementation of NSGA-II. Figure 2: Screenshot of jMetalPy running a NSGA-II for the ZDT1 benchmark problem showing the progress and the Pareto front approximation. 5 0.0 0.2 0.4 0.6 0.8 1.0 x 0.0 0.2 0.4 0.6 0.8 1.0 y NSGAII-ZDT1 Pareto front approximation 0.0 0.2 0.4 0.6 0.8 1.0 x 0.0 0.2 0.4 0.6 0.8 1.0 y ssNSGAII-ZDT1 Pareto front approximation 0.0 0.2 0.4 0.6 0.8 1.0 x 0.0 0.2 0.4 0.6 0.8 1.0 y gNSGAII-ZDT1 Pareto front approximation Figure 3: Pareto front approximations when solving the ZDT1 produced by the standard NSGA-II algorithm (left), a steady-state version (center), and G-NSGA-II (using the reference point [ f1,f2]=[0.5,0.5], shown in red). As commented previously, any algorithm is an observable entity, so observers can register into it. In this code, we register a progress bar observer (shows a bar in the terminal indicating the progress of the algorithm) and a visualizer observer (shows a graph plotting the current population, i.e., the current Pareto front approximation). A screen capture of NSGA-II running in included in Figure 2. The computing time of NSGA-II with this configuration in our target laptop is around 9.2 seconds. 4.2. Steady-State NSGA-II A steady-state version of NSGA-II can be configured by resorting to the same code, but just setting the offspring population size to one. This version yielded a better performance in terms of the produced Pareto front approximation compared with the standard NSGA-II as reported in a previous study [43], but at a cost of a higher computing time, which raises up to 190 seconds. An example of Pareto front approximation found by this version of NSGA-II when solving the ZDT1 benchmark problem is shown in Figure 3-center. As expected given the literature, it compares favorably against the one generated by the standard NSGA-II (Figure 3-left). 4.3. NSGA-II with Preference Articulation The NSGA-II implementation in jMetalPy can be easily extended to incorporate a preference articulation scheme. Concretely, we have developed a g-dominance based comparator considering the g-dominance concept described in [44], where a region of interest can be delimited by defining a reference point. If we desire to focus the search in the interest region delimited by the reference point, say e.g. [ f1,f2]=[0.5,0.5], we can configure NSGA-II with this comparator as follows: 1reference_point = [0.5, 0.5] algorithm = NSGAII( 3... dominance_comparator=GDominanceComparator(reference_point) 5) The resulting front is show in Figure 3-right. 4.4. Dynamic NSGA-II The approach adopted in jMetalPy to provide support for dynamic problem solving is as follows: First, we have developed a TimeCounter class (which is an Observable entity) which, given a delay, increments continuously a counter and notifies the registered observers the new counter values; second, we need to define an instance of DynamicProblem, which must implement the methods for checking whether the problem has changed and to clear the changed state. As DynamicProblem inherits from Observer, instances of this class can register in a TimeCounter object. Finally, it is required to extend DynamicAlgorithm with a class defining the restart() method that will be called when the algorithm detects a change in a dynamic problem. The following code snippet shows the implementation of the DynamicNSGAII class: 1class DynamicNSGAII(NSGAII, DynamicAlgorithm): def __init__(self, ...): 3... self.completed_iterations = 0 5 def restart(self) -> None 7# restart strategy 9def update_progress(self): if self.problem.the_problem_has_changed(): 11 self.restart() self.evaluator.evaluate(self.solutions, problem) 13 self.problem.clear_changed() self.evaluations += self.offspring_size 15 def stopping_condition_is_met(self): 17 if self.termination_criterion.is_met: self.restart() 19 self.evaluator.evaluate(self.solutions, problem) self.init_progress() 21 self.completed_iterations += 1 As shown above, at the end of each iteration a check is made about a change in the problem. If a change has occurred, the restart method is invoked which, depending on the implemented strategy, will remove some solutions from the population and new ones will be created to replace them. The resulting population will be evaluated and the clear changed() method of the problem object will be called. As opposed to the standard NSGA-II, the stopping condition method is not invoked to halt the algorithm, but instead to notify registered observers (e.g., a visualizer) that a new resulting population has been produced. Then, the algorithm starts again by invoking the restart() and init progress() methods. It is worth noting that most of the code of the original NSGA-II implementation is reused and only some methods need to be rewritten. To illustrate the implementation a dynamic problem, we next show code of the FDA abstract class, which is the base class of the five problems composing the FDA benchmark: 1class FDA(DynamicProblem, FloatProblem, ABC): def __init__(self): 3super(FDA, self).__init__() 6 self.tau_T = 5 5self.nT = 10 self.time = 1.0 7self.problem_modified = False 9def update(self, *args, **kwargs): counter = kwargs[’COUNTER’] 11 self.time = (1.0 / self.nT) * floor(counter * 1.0 / self.tau_T) self.problem_modified = True 13 def the_problem_has_changed(self) -> bool: 15 return self.problem_modified 17 def clear_changed(self) -> None: self.problem_modified = False The key point in this class is the update() method which, when invoked by an observable entity (e.g., an instance of the aforementioned TimeCounter class), sets the problem modified flag to True. We can observe that this flag can be queried and reset. The code presented next shows how to configure and run the dynamic NSGA-II algorithm: # Dynamic NSGAII runner 2problem = FDA2() time_counter = TimeCounter(delay=1)) 4time_counter.observable.register(problem) time_counter.start() 6 algorithm = DynamicNSGAII( 8... termination_criterion=StoppingByEvaluations(max= 10 max_evals) ) 12 algorithm.run() After creating the instances of the FDA2 benchmark problem [21] and the time counter class, the former is registered in the latter, which runs in a concurrent thread. The dynamic NSGA-II is set with stopping condition which returns a Pareto front approximation every 25,000 function evaluations. An example of running of the dynamic NSGA-II algorithm when solving the FDA2 problem is shown in Figure 4. 0.0 0.2 0.4 0.6 0.8 1.0 0 0.0 0.2 0.4 0.6 0.8 1.0 1 FDA2 Pareto front approximation Figure 4: Pareto front approximations when solving the dynamic FDA2 problem produced by the dynamic version of NSGA-II. 4.5. Parallel NSGA-II with Apache Spark In order to evaluate a population, NSGA-II (and in general, any generational algorithms in jMetalPy) can use an evaluator object. The default evaluator runs in a sequential fashion but, should the evaluate method of the problem be thread-safe, solutions can be evaluated in parallel. jMetalPy includes an evaluator based on Apache Spark, so the solutions can be evaluated in a variety of parallel systems (multicores, clusters) following the scheme presented in [45]. This evaluator can be used as exemplified next: # NSGAII runner using the Spark evaluator 2algorithm = NSGAII( ... 4evaluator=SparkEvaluator() ) The resulting parallel NSGA-II algoritm combines parallel with sequential phases, so speed improvements cannot be expected to scale linearly. A pilot test on our target laptop indicates speedup factors in the order of 2.7. However, what is interesting to note here is that no changes are required in NSGAII, which has the same behavior as its sequential version, so the obtained time reductions are for free. 4.6. Distributed NSGA-II with Dask The last variant of NSGA-II we present in this paper is a distributed version based on an asynchronous parallel model implemented with Dask [13], a parallel and distributed Python system including a broad set of parallel programming models, including asynchronous parallelism using futures. The distributed NSGA-II adopts a parallel scheme studied in [43]. The scheme is based on a steady-state NSGA-II and the use of Dask’s futures, in such a way that whenever a new solution has to evaluated, a task is created and submitted to Dask, which returns a future. When a task is completed, its corresponding future returns an evaluated solution, which is inserted into the offspring population. Then, a new solution is produced after performing the replacement, selection, and reproduction stages, to be sent again for evaluation. This way, all the processors/cores of the target cluster will be busy most of the time. Preliminary results on our target multicore laptop indicate that speedups around 5.45 can obtained with the 8 cores of the system where simulations were performed. We will discuss on this lack of scalability and other aspects of this use case in the next subsection. 4.7. Discussion In this section we have presented five different versions of NSGA-II , most of them (except for the distributed variant) requiring minor changes on the base class implementing NSGAII. Not all algorithms can be adapted in the same way, but some of the variations of NSGA-II can be implemented in a straightforward manner. Thus, we include in jMetalPy examples of dynamic, preference-based, and parallel versions of some of the included algorithms, such as SMPSO, GDE3, and OMOPSO. We would like to again stress on the readability of the codes, by virtue of which all the steps of the algorithms can be clearly identified. Some users may find the class hierarchy EvolutionaryAlgorithm →GeneticAlgorithm →NSGAII cumbersome, and prefer to have all the code of NSGA-II in a single class. However, this alternative design approach would hinder the flexibility of the current implementation, and would require to replicate most of the code when developing algorithmic variants. In the case of parallel algorithms, an exhaustive performance assessment is beyond the scope of this paper. The reported 7 speedups are not remarkable due to the Turbo Boost feature of the processor of the laptop used for performing the experiments, but they give an idea of the time reductions that can be achieved when using a modern multicore computer. 5. Visualization An advantage of using Python (instead of Java) is its power related to visualization features thanks to the availability of graphic plotting libraries, such as: Matplotlib, Holoviews or Plotly. jMetalPy harnesses these libraries to include three types of visualization charts: static, interactive and streaming. Table 2 summarizes these implementations. Static charts can be shown in the screen, stored in a file, or included in a Jupyter notebook (typically used at the end of the execution of an algorithm). Similarly, interactive charts are generated when an algorithm returns a Pareto front approximation but, unlike the static ones, the user can manipulate them interactively. There are two kinds of interactive charts: those that produce an HTML page including a chart (allowing to apply actions such as zooming, selecting part of the graph, or clicking in a point to see its objective values are allowed) and charts such as the Chord diagram that allows hovering the mouse over the chart and visualizing relationships among objective values. Finally, streaming charts depict graphs in real time, during the execution of the algorithms (and they can also be included in a Jupyter notebook); this can be useful to observe the evolution of the current Pareto front approximation produced by the algorithm. Table 2: Main visualizations included in jMetalPy. Name Type Backend Description Plot Static Matplotlib 2D, 3D, p-coords Interactive Plotly 2D, 3D, p-coords Streaming plot Streaming Matplotlib 2D, 3D Streaming HoloViews 2D, 3D (for Jupyter) Chord plot Interactive Matplotlib For statistical purposes Box plot Interactive Matplotlib For statistical purposes CD plot Static Matplotlib Demsar’s critical distance plot Posterior plot Static Matplotlib Bayesian posterior analysis Figure 5 shows three examples of interactive plots based on Plotly. The target problem is DTLZ1 [46], which is solved with the SMPSO algorithm when the problem is defined with 2, 3 and 5 objectives. For any problem with more than 3 objectives, a parallel coordinates graph is generated. An example of Chord diagram for a problem with 5 objectives is shown in Figure 6; each depicted chord represents a solution of the obtained Pareto front, and ties together its objective values. When hovering over a sector box of a certain objective fi, this chart only renders those solutions whose fivalues fall within the value support of this objective delimited by the extremes of the sector box. Finally, the outer partitioned torus of the chart represents a histogram of the values covered in the obtained Pareto front for every objective. Figure 5: Examples of interactive plots produced when using SMPSO to solve the DTLZ1 problem with 2 (top), 3 (middle), and 5 (bottom) objectives. 8 Figure 6: Example of Chord diagram for the front obtained by SMPSO when solving a problem with 5 objectives. 6. Experimental Use Case In previous sections, we have shown examples of Pareto front approximations produced by some of the metaheuristics included in jMetalPy. In this section, we describe how our framework can be used to carry out rigorous experimental studies based on comparing a number of algorithms to determine which of them presents the best overall performance. 6.1. Experimentation Methodology An experimental comparison requires a number of steps: 1. Determine the algorithms to be compared and the benchmark problems to be used. 2. Run a number of independent runs per algorithm-problem configuration and get the produced fronts. 3. Apply quality indicators to the fronts (e.g., Hypervolume, Epsilon, etc.). 4. Apply a number of statistical test to assess the statistical significance of the performance differences found among the algorithms considered in the benchmark. The first three steps can be done with jMetalPy, but also with jMetal or even manually (e.g., running algorithms using a script). The point where jMetalPy stands out is the fourth one, as it contains a large amount of statistical features to provide the user with a broad set of tools to analyze the results generated by a comparative study. All these functionalities have been programmed from scratch and embedded into the core of jMetalPy. Specifically, the statistical tests included in jMetalPy are listed next: •A diverse set of non-parametric null hypothesis significance tests, namely, the Wilcoxon rank sum test, Sign test, Friedman test, Friedman aligned rank test and Quade test. These tests have been traditionally used by the community to shed light on their comparative performance by inspecting a statistic computed from their scores. •Bayesian tests (sign test and signed rank test), which have been recently postulated to overcome the shortcomings of null hypothesis significance testing for performance assessment [47]. These tests are complemented by a posterior plot in barycentric coordinates to compare pairs of algorithms under a Bayesian approach by also accounting for possible statistical ties. •Posthoc tests to compare among multiple algorithms, either one-vs-all (Bonferroni-Dunn, Holland, Finner, and Hochberg) or all-vs-all (Li, Holm, Shaffer). The results of these tests are displayed by default in the screen and most of them can be exported to L A T EX tables. Furthermore, boxplot diagrams can be also generated. Finally, L A T EX tables containing means and medians (and their corresponding standard deviation and interquartile range dispersion measures, respectively) are automatically generated. 6.2. Implementation Details jMetalPy has a laboratory module containing utilities for defining experiments, which require three lists: the algorithms to be compared (which must be properly configured), the benchmark problems to be solved, and the quality indicators to be applied for performance assessment. Additional parameters are the number of independent runs and the output directory. Once the experiment is executed, a summary in the form of a CSV file is generated. This file contains all the information of the quality indicator values, for each configuration and run. Each line of this file has the following schema: Algorithm, Problem, Indicator, ExecutionId, IndicatorValue. An example of its contents follows: 1Algorithm,Problem,Indicator,ExecutionId,IndicatorValue NSGAII,ZDT1,EP,0,0.015705992620067832 3NSGAII,ZDT1,EP,1,0.012832504015918067 NSGAII,ZDT1,EP,2,0.01071189935186434 5... MOCell,ZDT6,IGD+,22,0.0047265135903854704 7MOCell,ZDT6,IGD+,23,0.004496215669027173 MOCell,ZDT6,IGD+,24,0.005483899232523609 where we can see the header with the column names, followed by four lines corresponding to the values of the Epsilon indicator of three runs of the NSGA-II algorithm when solving the ZDT1 problem. The end of the file shows the value of the IGD+indicator for three runs of MOCell when solving the ZDT6 problem. The file contains as many lines as the product of the numbers of algorithms, problems, quality indicators, and independent runs. The summary file is the input of all the statistical tests, so that they can be applied to any valid file having the proper format. This is particularly interesting to combine jMetal and jMetalPy. The last versions of jMetal generates a summary file after running a set of algorithms in an experimental study, so then we can take advantage of the features of jMetal (providing 9