From d5976b140149557cf143085bb9358137944569d3 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 22 Dec 2017 14:00:09 -0500 Subject: [PATCH] Minor modifications --- .DS_Store | Bin 6148 -> 6148 bytes .gitignore | 2 + pymoo/__init__.py | 1 + pymoo/algorithms/NSGAII.py | 50 +++++++++++- pymoo/algorithms/NSGAIII.py | 33 +++++--- pymoo/algorithms/genetic_algorithm.py | 67 ++++++++-------- pymoo/configuration.py | 2 +- pymoo/model/algorithm.py | 72 ++++++++++++++---- pymoo/model/crossover.py | 48 +++++++++++- pymoo/model/{measure.py => performance.py} | 11 +-- pymoo/model/population.py | 27 ++----- pymoo/model/random.py | 2 - pymoo/model/sampling.py | 33 ++++++++ pymoo/model/selection.py | 46 +++++++++++ pymoo/model/survival.py | 33 ++++++-- .../real_simulated_binary_crossover.py | 2 +- .../operators/sampling/bin_random_sampling.py | 8 +- .../sampling/real_random_sampling.py | 15 ++-- .../selection/tournament_selection.py | 70 ++++++++++++++--- pymoo/operators/survival/rank_and_crowding.py | 55 +++++-------- .../survival/reference_line_survival.py | 2 - pymoo/performance/hypervolume.py | 8 +- pymoo/performance/igd.py | 8 +- pymoo/postprocess/run_analyse.py | 8 +- pymoo/postprocess/run_plot_front.py | 4 +- pymoo/postprocess/run_plot_func.py | 32 ++++++++ pymoo/problems/__init__.py | 1 + pymoo/rand/my_random_generator.py | 9 ++- pymoo/run_experiment.py | 53 ++++++++----- pymoo/run_nsga.py | 54 ++++++------- pymoo/tests/non_dominated_rank_test.py | 22 +++--- pymoo/util/dominator.py | 17 +++-- pymoo/util/misc.py | 10 ++- 33 files changed, 567 insertions(+), 238 deletions(-) rename pymoo/model/{measure.py => performance.py} (60%) create mode 100644 pymoo/model/sampling.py create mode 100644 pymoo/model/selection.py create mode 100644 pymoo/postprocess/run_plot_func.py diff --git a/.DS_Store b/.DS_Store index b2211999685f4c4c9210704c4ab6f462cca2a363..9c3c4665842fbaadfee32b8ed54908d4691e66a2 100644 GIT binary patch delta 31 ncmZoMXfc@J&&azmU^gQp?_?gP*PFSR|FTRh5Z=tr@s}R}qMQm) delta 44 zcmZoMXfc@J&&aniU^gQp-(()9*Zj;3DGd1x$tlIjIZ65XIh#K-F|uxE=lIJH04x0s A#Q*>R diff --git a/.gitignore b/.gitignore index 35d3f7d60..69af6e480 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/pymoo/__init__.py b/pymoo/__init__.py index e69de29bb..380c76db2 100644 --- a/pymoo/__init__.py +++ b/pymoo/__init__.py @@ -0,0 +1 @@ +__all__ = ["algorithms", "model", "operators", "performance", "postprocess", "problems", "rand", "util"] \ No newline at end of file diff --git a/pymoo/algorithms/NSGAII.py b/pymoo/algorithms/NSGAII.py index 0dab653f4..c32b6d784 100644 --- a/pymoo/algorithms/NSGAII.py +++ b/pymoo/algorithms/NSGAII.py @@ -1,4 +1,5 @@ from pymoo.algorithms.genetic_algorithm import GeneticAlgorithm +from pymoo.model import random from pymoo.operators.crossover.bin_uniform_crossover import BinaryUniformCrossover from pymoo.operators.crossover.real_simulated_binary_crossover import SimulatedBinaryCrossover from pymoo.operators.mutation.bin_bitflip_mutation import BinaryBitflipMutation @@ -7,16 +8,17 @@ from pymoo.operators.sampling.real_random_sampling import RealRandomSampling from pymoo.operators.selection.tournament_selection import TournamentSelection from pymoo.operators.survival.rank_and_crowding import RankAndCrowdingSurvival +from pymoo.util.dominator import Dominator class NSGAII(GeneticAlgorithm): - def __init__(self, var_type, pop_size=100, verbose=1): + if var_type == "real": super().__init__( pop_size=pop_size, sampling=RealRandomSampling(), - selection=TournamentSelection(), + selection=TournamentSelection(f_comp=comp_by_rank_and_crowding), crossover=SimulatedBinaryCrossover(), mutation=PolynomialMutation(), survival=RankAndCrowdingSurvival(), @@ -25,10 +27,52 @@ def __init__(self, var_type, pop_size=100, verbose=1): elif var_type == "binary": super().__init__( sampling=BinaryRandomSampling(), - selection=TournamentSelection(), + selection=TournamentSelection(f_comp=comp_by_rank_and_crowding), crossover=BinaryUniformCrossover(), mutation=BinaryBitflipMutation(), survival=RankAndCrowdingSurvival(), verbose=verbose, eliminate_duplicates=True ) + + +def comp_by_rank_and_crowding(pop, indices, data): + if len(indices) != 2: + raise ValueError("Only implemented for binary tournament!") + + first = indices[0] + second = indices[1] + + if data.rank[first] < data.rank[second]: + return first + elif data.rank[second] < data.rank[first]: + return second + else: + if data.crowding[first] > data.crowding[second]: + return first + elif data.crowding[second] > data.crowding[first]: + return second + else: + return indices[random.randint(0, 2)] + + +def comp_by_dom_and_crowding(pop, indices, data): + if len(indices) != 2: + raise ValueError("Only implemented for binary tournament!") + + first = indices[0] + second = indices[1] + + rel = Dominator.get_relation(pop.F[first, :], pop.F[second, :]) + + if rel == 1: + return first + elif rel == -1: + return second + else: + if data.crowding[first] > data.crowding[second]: + return first + elif data.crowding[second] > data.crowding[first]: + return second + else: + return indices[random.randint(0, 2)] diff --git a/pymoo/algorithms/NSGAIII.py b/pymoo/algorithms/NSGAIII.py index 4bd880645..d05c60c36 100644 --- a/pymoo/algorithms/NSGAIII.py +++ b/pymoo/algorithms/NSGAIII.py @@ -1,25 +1,40 @@ import numpy as np from pymoo.algorithms.genetic_algorithm import GeneticAlgorithm +from pymoo.operators.crossover.bin_uniform_crossover import BinaryUniformCrossover from pymoo.operators.crossover.real_simulated_binary_crossover import SimulatedBinaryCrossover +from pymoo.operators.mutation.bin_bitflip_mutation import BinaryBitflipMutation from pymoo.operators.mutation.real_polynomial_mutation import PolynomialMutation +from pymoo.operators.sampling.bin_random_sampling import BinaryRandomSampling from pymoo.operators.sampling.real_random_sampling import RealRandomSampling from pymoo.operators.selection.tournament_selection import TournamentSelection from pymoo.operators.survival.reference_line_survival import ReferenceLineSurvival class NSGAIII(GeneticAlgorithm): - def __init__(self, pop_size=100, verbose=1): + def __init__(self, var_type, pop_size=100, verbose=1): self.ref_lines = self.create_ref_lines() - super().__init__( - pop_size=pop_size, - sampling=RealRandomSampling(), - selection=TournamentSelection(), - crossover=SimulatedBinaryCrossover(), - mutation=PolynomialMutation(), - survival=ReferenceLineSurvival(self.ref_lines), - verbose=verbose) + if var_type == "real": + super().__init__( + pop_size=pop_size, + sampling=RealRandomSampling(), + selection=TournamentSelection(), + crossover=SimulatedBinaryCrossover(), + mutation=PolynomialMutation(), + survival=ReferenceLineSurvival(self.ref_lines), + verbose=verbose + ) + elif var_type == "binary": + super().__init__( + sampling=BinaryRandomSampling(), + selection=TournamentSelection(), + crossover=BinaryUniformCrossover(), + mutation=BinaryBitflipMutation(), + survival=ReferenceLineSurvival(self.ref_lines), + verbose=verbose, + eliminate_duplicates=True + ) def create_ref_lines(self): n_refs = 30 diff --git a/pymoo/algorithms/genetic_algorithm.py b/pymoo/algorithms/genetic_algorithm.py index f5ecd63fb..a78cc1e2a 100644 --- a/pymoo/algorithms/genetic_algorithm.py +++ b/pymoo/algorithms/genetic_algorithm.py @@ -15,22 +15,31 @@ class GeneticAlgorithm(Algorithm): Attributes ---------- + pop_size: int The population size to be used for the genetic algorithm. + sampling : class The sampling implementation to create the initial population. + selection : class A class to select the parents for the crossover + crossover : class The crossover to be performed on parents. + mutation : class The mutation that will be performed for each child after the crossover + survival : class This class selects the individuals to survive for the next generation + eliminate_duplicates : bool If this flag is set no duplicates are allowed in the population (mostly likely only used for binary or discrete) + verbose : int If larger than zero output is provided. (verbose=1 means some output, verbose=2 details for debugging) + callback : func A callback function can be passed that is executed every generation. The parameters for the function are the algorithm itself, the number of evaluations so far and the current population. @@ -62,19 +71,13 @@ def __init__(self, self.verbose = verbose self.callback = callback - def _initialize(self): - pass - def _solve(self, problem, evaluator): - # initialize stuff before the initial population is created - self._initialize() - # create the population according to the factoring strategy pop = Population() - pop.X = self.sampling.sample(problem, self.pop_size) + pop.X = self.sampling.sample(problem, self.pop_size, self) pop.F, pop.G = evaluator.eval(problem, pop.X) - pop = self.survival.do(pop, self.pop_size) + pop = self.survival.do(pop, self.pop_size, self) # setup initial generation n_gen = 0 @@ -86,46 +89,38 @@ def _solve(self, problem, evaluator): self._do_each_generation(n_gen, evaluator, pop) n_gen += 1 - # create the offspring generation - offsprings = self._get_offsprings(problem, evaluator, pop) + # initialize selection and offspring methods + off = Population() + off.X = np.full((self.pop_size, problem.n_var), np.inf) + self.selection.set_population(pop, self) + + n_off = 0 + n_parents = self.crossover.n_parents + n_children = self.crossover.n_children + + while n_off < self.pop_size: + parents = self.selection.next(n_parents) + X = self.crossover.do(problem, pop.X[parents, :], self) + + off.X[n_off:min(n_off + n_children, self.pop_size)] = X + n_off = n_off + X.shape[0] + + off.X = self.mutation.do(problem, off.X) + off.F, off.G = evaluator.eval(problem, off.X) # merge the population - pop.merge(offsprings) + pop.merge(off) # eliminate all duplicates in the population if self.eliminate_duplicates: pop.filter(unique_rows(pop.X)) # truncate the population - pop = self.survival.do(pop, self.pop_size) + pop = self.survival.do(pop, self.pop_size, self) self._do_each_generation(n_gen, evaluator, pop) - return pop.X, pop.F, pop.G - def _get_offsprings(self, problem, evaluator, pop): - - # initialize selection and offspring methods - off = Population() - off.X = np.zeros((self.pop_size, problem.n_var)) - self.selection._initialize(pop) - - n_off = 0 - n_parents = self.crossover.n_parents - n_children = self.crossover.n_children - - while n_off < self.pop_size: - parents = self.selection.next(n_parents) - X = self.crossover.do(problem, pop.X[parents, :]) - - off.X[n_off:min(n_off + n_children, self.pop_size)] = X - n_off = n_off + len(X) - - off.X = self.mutation.do(problem, off.X) - off.F, off.G = evaluator.eval(problem, off.X) - - return off - def _do_each_generation(self, n_gen, evaluator, pop): if self.verbose > 0: print('gen = %d' % (n_gen + 1)) diff --git a/pymoo/configuration.py b/pymoo/configuration.py index 7ec2e9f54..2634b7c23 100644 --- a/pymoo/configuration.py +++ b/pymoo/configuration.py @@ -3,7 +3,7 @@ class Configuration: - EPS = 1e-20 + EPS = 1e-30 BENCHMARK_DIR = '/Users/julesy/benchmark/' rand = MyRandomGenerator() #rand = NumpyRandomGenerator() diff --git a/pymoo/model/algorithm.py b/pymoo/model/algorithm.py index 5c63d45d0..c33288af0 100644 --- a/pymoo/model/algorithm.py +++ b/pymoo/model/algorithm.py @@ -1,15 +1,57 @@ from abc import abstractmethod -import numpy - from pymoo.model import random from pymoo.model.evaluator import Evaluator +from pymoo.util.misc import calc_constraint_violation from pymoo.util.non_dominated_rank import NonDominatedRank class Algorithm: + """ + + This class represents the abstract class for any algorithm to be implemented. Most importantly it + provides the solve method that is used to optimize a given problem. + + """ def solve(self, problem, evaluator, seed=1, return_only_feasible=True, return_only_non_dominated=True): + """ + + Solve a given problem by a given evaluator. The evaluator determines the termination condition and + can either have a maximum budget, hypervolume or whatever. The problem can be any problem the algorithm + is able to solve. + + Parameters + ---------- + + problem: class + Problem to be solved by the algorithm + + evaluator: class + object that evaluates and saves the number of evaluations and determines the stopping condition + + seed: int + Random seed for this run. Before the algorithm starts this seed is set. + + return_only_feasible: + If true, only feasible solutions are returned. + + return_only_non_dominated + If true, only the non dominated solutions are returned. Otherwise, it might be - dependend on the + algorithm - the final population + + Returns + ------- + X: matrix + Design space + + F: matrix + Objective space + + G: matrix + Constraint space + + """ # set the random seed random.seed(seed) @@ -18,24 +60,24 @@ def solve(self, problem, evaluator, seed=1, return_only_feasible=True, return_on evaluator = Evaluator(evaluator) # call the algorithm to solve the problem - x, f, g = self._solve(problem, evaluator) + X, F, G = self._solve(problem, evaluator) if return_only_feasible: - if g is not None and g.shape[0] == len(f) and g.shape[1] > 0: - b = numpy.array(numpy.where(numpy.sum(g, axis=1) <= 0))[0] - x = x[b, :] - f = f[b, :] - if g is not None: - g = g[b, :] + if G is not None and G.shape[0] == len(F) and G.shape[1] > 0: + cv = calc_constraint_violation(G) + X = X[cv, :] + F = F[cv, :] + if G is not None: + G = G[cv, :] if return_only_non_dominated: - idx_non_dom = NonDominatedRank.calc_as_fronts(f,g)[0] - x = x[idx_non_dom, :] - f = f[idx_non_dom, :] - if g is not None: - g = g[idx_non_dom, :] + idx_non_dom = NonDominatedRank.calc_as_fronts(F,G)[0] + X = X[idx_non_dom, :] + F = F[idx_non_dom, :] + if G is not None: + G = G[idx_non_dom, :] - return x, f, g + return X, F, G @abstractmethod def _solve(self, problem, evaluator): diff --git a/pymoo/model/crossover.py b/pymoo/model/crossover.py index c5cfae2b8..85136412f 100644 --- a/pymoo/model/crossover.py +++ b/pymoo/model/crossover.py @@ -4,15 +4,57 @@ class Crossover: + """ + + This class is the base class for a crossover. In general any crossover can be used. + It is up to the algorithm composer to take care of not using a crossover for real values in a binary problem. + Each crossover needs to define the number of parents that are needed and also the number of children + that are composed. + + """ + def __init__(self, n_parents, n_children): self.n_parents = n_parents self.n_children = n_children - def do(self, problem, parents): + def do(self, problem, parents, data=None): + """ + + This method executes the crossover on the parents. This class wraps the implementation of the class + that implements the crossover. + + + Parameters + ---------- + problem: class + The problem to be solved. Provides information such as lower and upper bounds or feasibility + conditions for custom crossovers. + + parents: matrix + The parents as a matrix. Each row is a parent and the columns are equal to the parameter length. + + data: + Any additional data that might be necessary to perform the crossover. E.g. constants of an algorithm. + + Returns + ------- + children: matrix + The children as a matrix. n_children rows and the number of columns is equal to the variable + length of the problem. + + """ n_var = parents.shape[1] + + if n_var != problem.n_var: + raise ValueError('Exception during crossover: Variable length is not equal to the defined one in problem.') + + if self.n_parents != parents.shape[0]: + raise ValueError('Exception during crossover: Number of parents differs from defined at crossover.') + children = np.full((self.n_children, n_var), np.inf) - return self._do(problem, parents, children) + return self._do(problem, parents, children, data) + @abstractmethod - def _do(self, problem, parents, children): + def _do(self, problem, parents, children, data=None): pass diff --git a/pymoo/model/measure.py b/pymoo/model/performance.py similarity index 60% rename from pymoo/model/measure.py rename to pymoo/model/performance.py index 45afd1eae..d398b0474 100644 --- a/pymoo/model/measure.py +++ b/pymoo/model/performance.py @@ -3,7 +3,7 @@ import numpy as np -class Measure: +class Performance: def __init__(self): self.default_if_empty = 0.0 @@ -18,15 +18,10 @@ def calc(self, F): if F.shape[1] == 0: return self.default_if_empty - return self.calc_(F) + return self._calc(F) @abc.abstractmethod - def calc_(self, F): - """ - Calcuates a measure on the objective space - :param F: ndarray - :return: scalar value that represents the measure - """ + def _calc(self, F): return diff --git a/pymoo/model/population.py b/pymoo/model/population.py index 0247b9dfa..ad0ac5603 100644 --- a/pymoo/model/population.py +++ b/pymoo/model/population.py @@ -1,11 +1,11 @@ import numpy as np -class Population: - def __init__(self): - self.X = None # design variables - self.F = None # objective values - self.G = None # constraint violations as vectors +class Population: + def __init__(self, X=None, F=None, G=None): + self.X = X # design variables + self.F = F # objective values + self.G = G # constraint violations as vectors def merge(self, other): self.X = np.concatenate([self.X, other.X]) @@ -23,17 +23,6 @@ def filter(self, v): if self.X is None: return 0 else: - - X = np.zeros((len(v), self.X.shape[1])) - F = np.zeros((len(v), self.F.shape[1])) - G = np.zeros((len(v), self.G.shape[1])) - - for i, o in enumerate(v): - X[i,:] = self.X[o,:] - F[i, :] = self.F[o, :] - G[i, :] = self.G[o, :] - - self.X = X - self.F = F - self.G = G - + self.X = self.X[v] + self.F = self.F[v] + self.G = self.G[v] diff --git a/pymoo/model/random.py b/pymoo/model/random.py index 64462ad70..c59c87eaf 100644 --- a/pymoo/model/random.py +++ b/pymoo/model/random.py @@ -1,5 +1,3 @@ -import time - from pymoo.configuration import Configuration diff --git a/pymoo/model/sampling.py b/pymoo/model/sampling.py new file mode 100644 index 000000000..cf463687c --- /dev/null +++ b/pymoo/model/sampling.py @@ -0,0 +1,33 @@ +from abc import abstractmethod + + +class Sampling: + """ + + This abstract class represents any sampling strategy that can be used + to create an initial population or just an initial point + + """ + + @abstractmethod + def sample(self, problem, n_samples, data=None): + """ + Sample new points according to the problem. + + + Parameters + ---------- + problem: class + The problem to which points should be sampled. (lower and upper bounds, discrete, binary, ...) + + n_samples: int + Number of points that should be created + + data: class + Any additional data that might be necessary. e.g. constants of the algorithm, ... + + Returns + ------- + + """ + pass diff --git a/pymoo/model/selection.py b/pymoo/model/selection.py new file mode 100644 index 000000000..8dc8cadaa --- /dev/null +++ b/pymoo/model/selection.py @@ -0,0 +1,46 @@ +from abc import abstractmethod + + +class Selection: + """ + + This class represents the selection of individuals for the mating. + + """ + + @abstractmethod + def set_population(self, pop, data): + """ + + Set the population to be selected from. + + Parameters + ---------- + pop: class + An object that stores the current population to be selected from. + data: class + Any other additional data that might be needed for the selection procedure. + + """ + pass + + @abstractmethod + def next(self, n_select): + """ + + Choose from the population new individuals to be selected. + + Parameters + ---------- + n_select: int + How many individuals that should be selected. + + Returns + ------- + + select: vector + A vector that contains the result indices of selected individuals. + + """ + + pass diff --git a/pymoo/model/survival.py b/pymoo/model/survival.py index cfd1416b8..8664a42f4 100644 --- a/pymoo/model/survival.py +++ b/pymoo/model/survival.py @@ -2,13 +2,36 @@ class Survival: + """ - def __init__(self): - pass + The class represents the survival selection during the evolution. Only the fittest can survive. + + """ + + def do(self, pop, n_survive, data): + """ + + The whole population is provided and the number of individuals to survive. If the number of survivers + is smaller than the populations a survival selection is done. Otherwise, the elements might only + be sorted by a specific criteria. This depends on the survival implementation. + + Parameters + ---------- + pop: class + The population. + n_survive: int + number of individuals that should survive. + data: class + Any additional data that might be needed to choose what individuals survive. + + Returns + ------- + pop: class + The population that has survived. - def do(self, pop, n_survive): - return self._do(pop, n_survive) + """ + return self._do(pop, n_survive, data) @abstractmethod - def _do(self, pop, n_survive): + def _do(self, pop, n_survive, data): pass diff --git a/pymoo/operators/crossover/real_simulated_binary_crossover.py b/pymoo/operators/crossover/real_simulated_binary_crossover.py index c59b4af1f..0ac5edb60 100644 --- a/pymoo/operators/crossover/real_simulated_binary_crossover.py +++ b/pymoo/operators/crossover/real_simulated_binary_crossover.py @@ -9,7 +9,7 @@ def __init__(self, eta_xover=15, p_xover=0.9): self.p_xover = p_xover self.eta_c = eta_xover - def _do(self, p, parents, children): + def _do(self, p, parents, children, data=None): n = p.n_var diff --git a/pymoo/operators/sampling/bin_random_sampling.py b/pymoo/operators/sampling/bin_random_sampling.py index 313c9b65b..61696c869 100644 --- a/pymoo/operators/sampling/bin_random_sampling.py +++ b/pymoo/operators/sampling/bin_random_sampling.py @@ -2,5 +2,9 @@ class BinaryRandomSampling: - def sample(self, p, n): - return random.randint(0, 1, size=(n, p.n_var)) + """ + Randomly sample a binary representation of 0's and 1's. + """ + + def sample(self, problem, n_samples, data): + return random.randint(0, 1, size=(n_samples, problem.n_var)) diff --git a/pymoo/operators/sampling/real_random_sampling.py b/pymoo/operators/sampling/real_random_sampling.py index 3b7116f9d..6d6851b1e 100644 --- a/pymoo/operators/sampling/real_random_sampling.py +++ b/pymoo/operators/sampling/real_random_sampling.py @@ -1,10 +1,15 @@ from pymoo.model import random +from pymoo.model.sampling import Sampling -class RealRandomSampling: - def sample(self, p, n): - m = len(p.xl) - val = random.random(size=(n, m)) +class RealRandomSampling(Sampling): + """ + Randomly sample points in the real space by considering the lower and upper bounds of the problem. + """ + + def sample(self, problem, n_samples, data): + m = len(problem.xl) + val = random.random(size=(n_samples, m)) for i in range(m): - val[:, i] = val[:, i] * (p.xu[i] - p.xl[i]) + p.xl[i] + val[:, i] = val[:, i] * (problem.xu[i] - problem.xl[i]) + problem.xl[i] return val diff --git a/pymoo/operators/selection/tournament_selection.py b/pymoo/operators/selection/tournament_selection.py index 7c6c40f53..c31bc82f6 100644 --- a/pymoo/operators/selection/tournament_selection.py +++ b/pymoo/operators/selection/tournament_selection.py @@ -1,25 +1,74 @@ import numpy as np from pymoo.model import random +from pymoo.model.selection import Selection -class TournamentSelection: - def __init__(self, - func=None, - pressure=2): +class TournamentSelection(Selection): + """ + The Tournament selection is used to simulated a tournament between individuals. The pressure balances + greedy the genetic algorithm will be. + + + """ + + def __init__(self, f_comp=None, pressure=2): + """ + + Parameters + ---------- + f_comp: func + The function to compare two individuals. It has the shape: comp(pop, indices, data) and returns the winner. + If the function is None it is assumed the population is sorted by a criterium and only indices are compared. + + pressure: int + The selection pressure to be applied. Default it is a binary tournament. + """ + + # selection pressure to be applied self.pressure = pressure - self.func = func - if self.func is None: - self.func = self.select_by_min_index + self.f_comp = f_comp + if self.f_comp is None: + self.f_comp = self.select_by_min_index - def _initialize(self, pop): + # attributes that will be set during the optimization + self.pop = None + self.perm = None + self.counter = None + self.data = None + + def set_population(self, pop, data): + """ + + Parameters + ---------- + pop: class + The population to be selected from. + data: class + Any additional data that might be needed for the selection. + + """ self.pop = pop + self.data = data self.perm = random.perm(self.pop.size()) self.counter = 0 def next(self, n_selected): + """ + + Parameters + ---------- + n_selected: int + Number of individuals to be selected. + + Returns + ------- + v: vector + Selected indices of individuals as a integer vector. + + """ selected = np.zeros(n_selected, dtype=np.int) @@ -28,10 +77,11 @@ def next(self, n_selected): if self.counter + self.pressure >= self.pop.size(): self.perm = random.perm(self.pop.size()) self.counter = 0 - selected[i] = self.func(self.pop, self.perm[self.counter:self.counter + self.pressure]) + selected[i] = self.f_comp(self.pop, self.perm[self.counter:self.counter + self.pressure], self.data) self.counter = self.counter + self.pressure return selected - def select_by_min_index(self, pop, indices): + # select simply by minimum index and assume sorting of the population according selection criteria + def select_by_min_index(self, pop, indices, data): return np.min(indices) diff --git a/pymoo/operators/survival/rank_and_crowding.py b/pymoo/operators/survival/rank_and_crowding.py index ebb037fdd..cc83d7c71 100644 --- a/pymoo/operators/survival/rank_and_crowding.py +++ b/pymoo/operators/survival/rank_and_crowding.py @@ -1,37 +1,36 @@ import numpy as np -from pymoo.configuration import Configuration from pymoo.model.survival import Survival +from pymoo.util.misc import normalize from pymoo.util.non_dominated_rank import NonDominatedRank class RankAndCrowdingSurvival(Survival): + def _do(self, pop, size, data): - def _do(self, pop, size): - # calculate rank and crowding and sort accordingly - rank, crowding = RankAndCrowdingSurvival.calc_rank_and_crowding(pop) - sorted_idx = sorted(range(pop.size()), key=lambda x: (rank[x], -crowding[x])) + fronts = NonDominatedRank.calc_as_fronts(pop.F, pop.G) + data.rank = NonDominatedRank.calc_from_fronts(fronts) + data.crowding = np.zeros(pop.F.shape[0]) + + for front in fronts: + cd_of_front = RankAndCrowdingSurvival.calc_crowding_distance(pop.F[front, :]) + data.crowding[front] = cd_of_front + + sorted_idx = sorted(range(pop.size()), key=lambda x: (data.rank[x], -data.crowding[x])) # now truncate the population sorted_idx = sorted_idx[:size] pop.filter(sorted_idx) + data.rank = data.rank[sorted_idx] + data.crowding = data.crowding[sorted_idx] return pop @staticmethod - def calc_rank_and_crowding(pop): - fronts = NonDominatedRank.calc_as_fronts_naive(pop.F, pop.G) - rank = NonDominatedRank.calc_from_fronts(fronts) - cd = np.zeros(pop.size()) - for front in fronts: - cd[front] = RankAndCrowdingSurvival.crowding_distance(pop.F[front, :]) - - return rank, cd - - @staticmethod - def crowding_distance(front): + def calc_crowding_distance(F, F_min=None, F_max=None): - n = front.shape[0] + n = F.shape[0] + m = F.shape[1] if n == 0: return [] @@ -40,35 +39,23 @@ def crowding_distance(front): else: cd = np.zeros(n) - n_obj = front.shape[1] - - f_min = np.min(front, axis=0) - f_max = np.max(front, axis=0) + N = normalize(F, F_min, F_max) # for each objective - for j in range(n_obj): + for j in range(m): # sort by its objective - sorted_idx = sorted(list(range(n)), key=lambda x: front[x, j]) + sorted_idx = np.argsort(N[:, j]) # set the corner points to infinity cd[sorted_idx[0]] = np.inf cd[sorted_idx[-1]] = np.inf - # calculate the normalization - avoid division by 0 - norm = f_max[j] - f_min[j] if f_min[j] != f_max[j] else Configuration.EPS - # add up the crowding measure for all points in between for i in range(1, n - 1): if np.isinf(cd[sorted_idx[i]]): continue else: - last = front[sorted_idx[i - 1], j] - current = front[sorted_idx[i], j] - next = front[sorted_idx[i + 1], j] - # this eliminates duplicates since they have a crowding distance of 0. - if last != current: - cd[sorted_idx[i]] += (next - last) / norm - - cd = [val / n_obj for val in cd] + cd[sorted_idx[i]] += N[sorted_idx[i + 1], j] - N[sorted_idx[i - 1], j] + return cd diff --git a/pymoo/operators/survival/reference_line_survival.py b/pymoo/operators/survival/reference_line_survival.py index b0edb6277..41cc24c0e 100644 --- a/pymoo/operators/survival/reference_line_survival.py +++ b/pymoo/operators/survival/reference_line_survival.py @@ -80,8 +80,6 @@ def _do(self, pop, size): def calc_perpendicular_dist_matrix(N, ref_lines): - """ - """ n = np.tile(ref_lines, (len(N), 1)) p = np.repeat(N, len(ref_lines), axis=0) a = np.zeros((len(p), N.shape[1])) diff --git a/pymoo/performance/hypervolume.py b/pymoo/performance/hypervolume.py index a537ea707..02ca2ce00 100644 --- a/pymoo/performance/hypervolume.py +++ b/pymoo/performance/hypervolume.py @@ -1,14 +1,14 @@ import numpy as np from pygmo.core import hypervolume -from pymoo.model.measure import Measure +from pymoo.model.performance import Performance -class Hypervolume(Measure): +class Hypervolume(Performance): def __init__(self, reference_point): - Measure.__init__(self) + Performance.__init__(self) self.reference_point = reference_point - def calc_(self, F): + def _calc(self, F): if F.shape[1] == 0: return 0.0 diff --git a/pymoo/performance/igd.py b/pymoo/performance/igd.py index e11f9438c..e6d2488e9 100644 --- a/pymoo/performance/igd.py +++ b/pymoo/performance/igd.py @@ -1,14 +1,14 @@ import numpy as np from scipy.spatial.distance import cdist -from pymoo.model.measure import Measure +from pymoo.model.performance import Performance -class IGD(Measure): +class IGD(Performance): def __init__(self, true_front): - Measure.__init__(self) + Performance.__init__(self) self.true_front = true_front - def calc_(self, F): + def _calc(self, F): v = cdist(self.true_front, F) return np.mean(np.min(v, axis=1)) diff --git a/pymoo/postprocess/run_analyse.py b/pymoo/postprocess/run_analyse.py index 2618ef096..c751a71e4 100644 --- a/pymoo/postprocess/run_analyse.py +++ b/pymoo/postprocess/run_analyse.py @@ -13,6 +13,7 @@ from pymoo.problems.ZDT.zdt2 import ZDT2 from pymoo.problems.ZDT.zdt3 import ZDT3 from pymoo.problems.ZDT.zdt4 import ZDT4 +from pymoo.problems.ZDT.zdt6 import ZDT6 from pymoo.performance.hypervolume import Hypervolume from pymoo.performance.igd import IGD @@ -108,7 +109,7 @@ def load(folder): continue - print(reference_point) + #print(reference_point) entry['igd'] = IGD(true_front).calc(pop) entry['hv'] = Hypervolume(reference_point).calc(pop) @@ -154,13 +155,14 @@ def load_files(folder, regex, columns=[], split_char="_"): if __name__ == '__main__': df = load(Configuration.BENCHMARK_DIR + "standard") + df = df.sort_values(['algorithm', 'problem']) with pd.option_context('display.max_rows', None): print(df) # print(df[(df.problem == 'ZDT3') & (df.igd > 0.03)]) with pd.option_context('display.max_rows', None): - f = {'igd': ['median', 'min', 'mean', 'max', 'std']} + f = {'hv': ['median', 'min', 'mean', 'max', 'std']} print(df.groupby(['problem', 'algorithm']).agg(f)) problems = ["ZDT1", "ZDT2", "ZDT3", "ZDT4", "ZDT6"] @@ -171,7 +173,7 @@ def load_files(folder, regex, columns=[], split_char="_"): F = [] for algorithm in data.algorithm.unique(): - F.append(np.array(data[data.algorithm == algorithm].igd.tolist())) + F.append(np.array(data[data.algorithm == algorithm].hv.tolist())) create_plot("%s.html" % problem, "Measure %s" % problem, F, chart_type="box", labels=data.algorithm.unique()) diff --git a/pymoo/postprocess/run_plot_front.py b/pymoo/postprocess/run_plot_front.py index 0b2cbbbac..daef8ef76 100644 --- a/pymoo/postprocess/run_plot_front.py +++ b/pymoo/postprocess/run_plot_front.py @@ -20,8 +20,8 @@ def parse_result_name(str): plots = [] -fn1 = Configuration.BENCHMARK_DIR + "standard/" + "pynsganew_ZDT3_1.out" -fn2 = Configuration.BENCHMARK_DIR + "standard/" + "cnsga_ZDT3_1.out" +fn1 = Configuration.BENCHMARK_DIR + "standard/" + "pynsga2_ZDT3_2.out" +fn2 = Configuration.BENCHMARK_DIR + "standard/" + "cnsga-rank_ZDT3_11.out" plot = 1 compare = 1 diff --git a/pymoo/postprocess/run_plot_func.py b/pymoo/postprocess/run_plot_func.py new file mode 100644 index 000000000..50ffb1f75 --- /dev/null +++ b/pymoo/postprocess/run_plot_func.py @@ -0,0 +1,32 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import cm +from matplotlib.ticker import LinearLocator, FormatStrFormatter +from mpl_toolkits.mplot3d import Axes3D + +from pymoo.problems.rastrigin import Rastrigin + +p = Rastrigin(2, 10) + +X = np.arange(-5, 5, 0.25) +Y = np.arange(-5, 5, 0.25) +X, Y = np.meshgrid(X, Y) + +F = np.zeros((len(X), len(Y))) +for i in range(len(X)): + for j in range(len(Y)): + F[i, j], _ = p.evaluate(np.array([X[i, j], Y[i, j]])) + +fig = plt.figure() +ax = fig.gca(projection='3d') + +# Plot the surface. +surf = ax.plot_surface(X, Y, F, cmap=cm.coolwarm, linewidth=0, antialiased=False) + +ax.zaxis.set_major_locator(LinearLocator(10)) +ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f')) + +# Add a color bar which maps values to colors. +fig.colorbar(surf, shrink=0.5, aspect=5) + +plt.show() diff --git a/pymoo/problems/__init__.py b/pymoo/problems/__init__.py index e69de29bb..60230c370 100644 --- a/pymoo/problems/__init__.py +++ b/pymoo/problems/__init__.py @@ -0,0 +1 @@ +__all__ = ["DTLZ", "ZDT"] \ No newline at end of file diff --git a/pymoo/rand/my_random_generator.py b/pymoo/rand/my_random_generator.py index c11f70bd8..c88d8a7ff 100644 --- a/pymoo/rand/my_random_generator.py +++ b/pymoo/rand/my_random_generator.py @@ -11,9 +11,10 @@ def __init__(self): self.jrand = 0 def seed(self, s): - while s >= 1: - s = s / 10.0 - self._seed = s + import random + random.seed(s) + self._seed = random.random() + self.__warmup_random() def rand(self, size=None): @@ -47,7 +48,7 @@ def perm(self, n): def randint(self, low, high, size=None): val = self.rand(size=size) - return (low + (val * (high - low + 1))).astype(np.int) + return (low + (val * (high - low))).astype(np.int) def __warmup_random(self): self.oldrand[54] = self._seed diff --git a/pymoo/run_experiment.py b/pymoo/run_experiment.py index 6103efed2..3c7ac9f90 100644 --- a/pymoo/run_experiment.py +++ b/pymoo/run_experiment.py @@ -1,33 +1,37 @@ import os +import random import sys -import time - -import numpy as np - sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from pymoo.algorithms.NSGAII import NSGAII + + +import time + +import numpy as np + from pymoo.model.evaluator import Evaluator from pymoo.problems.ZDT.zdt1 import ZDT1 from pymoo.problems.ZDT.zdt2 import ZDT2 from pymoo.problems.ZDT.zdt3 import ZDT3 from pymoo.problems.ZDT.zdt4 import ZDT4 from pymoo.problems.ZDT.zdt6 import ZDT6 -from pymoo.util.misc import save_hist n_runs = 20 -#output = os.path.join('..', '..', '..', 'benchmark', 'standard') -output = os.path.join('.') +output = os.path.join('..', '..', '..', 'benchmark', 'standard') +if not os.path.exists(output): + output = os.path.join('.') + def get_params(): - for algorithm in [NSGAII("real")]: + for algorithm in [NSGAII("real", verbose=0)]: for problem in [ZDT1(), ZDT2(), ZDT3(), ZDT4(), ZDT6()]: - for run in range(1, n_runs+1): + for run in range(1, n_runs + 1): yield (algorithm, problem, run) -if __name__ == '__main__': +if __name__ == '__main__': params = list(get_params()) @@ -37,15 +41,24 @@ def get_params(): print("Please provide a parameter settings value.") exit(1) - i = int(sys.argv[1]) - 1 - algorithm, problem, run = params[i] + start = int(sys.argv[1]) - 1 + end = start+1 + if len(sys.argv) == 3: + end = int(sys.argv[2]) - 1 + + print("Run for parameter settings: %s - %s" % (start, end)) + + for i in range(start,end): + + algorithm, problem, run = params[i] - eval = Evaluator(20000) - start_time = time.time() - X, F, G = algorithm.solve(problem, evaluator=eval, seed=run) - print("--- %s seconds ---" % (time.time() - start_time)) + eval = Evaluator(20000) + start_time = time.time() + X, F, G = algorithm.solve(problem, evaluator=eval, seed=run) + print("--- %s seconds ---" % (time.time() - start_time)) - # save the result as a test - fname = os.path.join(output, 'pynsganew_' + problem.__class__.__name__ + '_%s' % run) - np.savetxt(fname + ".out", F) - save_hist(fname + ".hist", eval.data) + # save the result as a test + fname = os.path.join(output, 'pynsga2-dom-myrand_' + problem.__class__.__name__ + '_%s' % run) + np.savetxt(fname + ".out", F) + print(fname + ".out") + #save_hist(fname + ".hist", eval.data) diff --git a/pymoo/run_nsga.py b/pymoo/run_nsga.py index 6be3b1c45..08f877132 100644 --- a/pymoo/run_nsga.py +++ b/pymoo/run_nsga.py @@ -5,47 +5,49 @@ import numpy as np from pymoo.algorithms.NSGAII import NSGAII +from pymoo.algorithms.NSGAIII import NSGAIII +from pymoo.model import random from pymoo.model.evaluator import Evaluator from pymoo.problems.ZDT.zdt1 import ZDT1 +from pymoo.problems.ZDT.zdt3 import ZDT3 from pymoo.util.misc import save_hist if __name__ == '__main__': - problem = ZDT1() + problem = ZDT3() + run = 20 - for run in range(1,11): + start_time = time.time() + # run the algorithm + nsga = NSGAII("real", verbose=True) + eval = Evaluator(20000) + X, F, G = nsga.solve(problem, evaluator=eval, seed=run) + print("--- %s seconds ---" % (time.time() - start_time)) + print(F) - start_time = time.time() - # run the algorithm - nsga = NSGAII("real", verbose=True) - eval = Evaluator(20000) - X, F, G = nsga.solve(problem, evaluator=eval, seed=12345) - print("--- %s seconds ---" % (time.time() - start_time)) + # save the result as a test + fname = os.path.join('..', '..', '..', 'benchmark', 'standard', + 'pynsga2_' + problem.__class__.__name__ + '_%s' % run) + np.savetxt(fname + ".out", F) + save_hist(fname + ".hist", eval.data) + # save the whole history - # save the result as a test - fname = os.path.join('..', '..', '..', 'benchmark', 'standard', - 'pynsganew_' + problem.__class__.__name__ + '_%s' % run) - np.savetxt(fname + ".out", F) - save_hist(fname + ".hist", eval.data) + plot = False + if plot: + plt.scatter(F[:, 0], F[:, 1]) - # save the whole history + for l in nsga.ref_lines[1:-1]: + x = np.linspace(0,1,100) + y = x / l[1] * l[0] + plt.plot(x,y) - plot = False - if plot: - plt.scatter(F[:, 0], F[:, 1]) - - for l in nsga.ref_lines[1:-1]: - x = np.linspace(0,1,100) - y = x / l[1] * l[0] - plt.plot(x,y) - - plt.ylim(0,1) - plt.xlim(0,1) - plt.show() + plt.ylim(0,1) + plt.xlim(0,1) + plt.show() # r = np.array([1.01, 1.01]) diff --git a/pymoo/tests/non_dominated_rank_test.py b/pymoo/tests/non_dominated_rank_test.py index 6aa2d6ee5..a0f93b6a0 100644 --- a/pymoo/tests/non_dominated_rank_test.py +++ b/pymoo/tests/non_dominated_rank_test.py @@ -1,23 +1,21 @@ import unittest import numpy as np -from pymoo.util.non_dominated_rank import NonDominatedRank +from pymoo.operators.survival.rank_and_crowding import RankAndCrowdingSurvival -class NonDominatedRankTest(unittest.TestCase): - def test_one_dimensional_rank(self): - pass - #i1 = make_individual(np.array([0,0,0]), np.array([1])) - #i2 = make_individual(np.array([1,1,1]), np.array([0])) - #i3 = make_individual(np.array([2,2,2]), np.array([0])) - #i4 = make_individual(np.array([2,2,2]), np.array([0])) - #i5 = make_individual(np.array([3,3,4]), np.array([0])) - #pop = [i1, i2, i3, i4, i5] +class NonDominatedRankTest(unittest.TestCase): + def test_crowding_distance(self): + F = np.array([[0.31, 6.10], [0.22, 7.09], [0.79, 3.97], [0.27, 6.93]]) + cd = RankAndCrowdingSurvival.calc_crowding_distance(F, F_min=np.array([0.1, 0]), F_max=np.array([1, 60])) + self.assertTrue(np.all(np.round(cd, decimals=2) == np.array([0.63, np.inf, np.inf, 0.12]))) - #self.assertTrue(np.array_equal(np.array([3, 0, 1, 1, 2]), NonDominatedRank.calc(pop))) - #self.assertTrue(np.array_equal(np.array([3, 0, 1, 1, 2]), NonDominatedRank.calc_from_fronts(NonDominatedRank.calc_as_fronts_pygmo(pop)))) + def test_crowding_distance_degenerated(self): + F = np.array([[0.0, 6.10], [0.0, 7.09], [0.0, 7.09]]) + cd = RankAndCrowdingSurvival.calc_crowding_distance(F) + print(cd) if __name__ == '__main__': unittest.main() diff --git a/pymoo/util/dominator.py b/pymoo/util/dominator.py index 10096f1fa..d6219696e 100644 --- a/pymoo/util/dominator.py +++ b/pymoo/util/dominator.py @@ -1,8 +1,9 @@ import numpy as np +from pymoo.util.misc import calc_constraint_violation -class Dominator: +class Dominator: @staticmethod def get_relation(a, b, cva=None, cvb=None): @@ -42,19 +43,23 @@ def calc_domination_matrix_loop(F, G): def calc_domination_matrix(F, G): if G is None: - constr = np.zeros((F.shape[0],F.shape[0])) + constr = np.zeros((F.shape[0], F.shape[0])) else: # consider the constraint violation - CV = np.sum(G * (G > 0).astype(np.float), axis=1) + CV = calc_constraint_violation(G) constr = (CV[:, None] < CV) * 1 + (CV[:, None] > CV) * -1 # look at the obj for dom - smaller = np.any(F[:, np.newaxis] < F, axis=2) - larger = np.any(F[:, np.newaxis] > F, axis=2) + n = F.shape[0] + L = np.repeat(F, n, axis=0) + R = np.tile(F, (n, 1)) + + smaller = np.reshape(np.any(L < R, axis=1), (n, n)) + larger = np.reshape(np.any(L > R, axis=1), (n, n)) + dom = np.logical_and(smaller, np.logical_not(larger)) * 1 \ + np.logical_and(larger, np.logical_not(smaller)) * -1 # if cv equal then look at dom M = constr + (constr == 0) * dom return M - diff --git a/pymoo/util/misc.py b/pymoo/util/misc.py index 185e16fb2..f4f621407 100644 --- a/pymoo/util/misc.py +++ b/pymoo/util/misc.py @@ -1,6 +1,6 @@ import numpy as np - +from pymoo.configuration import Configuration def denormalize(x, x_min, x_max): @@ -13,13 +13,19 @@ def normalize(x, x_min=None, x_max=None, return_bounds=False): if x_max is None: x_max = np.max(x, axis=0) - res = (x - x_min) / (x_max - x_min) + denom = x_max - x_min + denom = denom + (denom == 0) * Configuration.EPS + res = (x - x_min) / denom if not return_bounds: return res else: return res, x_min, x_max +def calc_constraint_violation(G): + return np.sum(G * (G > 0).astype(np.float), axis=1) + + # returns only the unique rows from a given matrix X def unique_rows(X): y = np.ascontiguousarray(X).view(np.dtype((np.void, X.dtype.itemsize * X.shape[1])))