Source code for pymoo.algorithms.moo.age

try:
    import numba
    from numba import jit
except:
    raise Exception("Please install numba to use AGEMOEA: pip install numba")

import numpy as np

from pymoo.algorithms.base.genetic import GeneticAlgorithm
from pymoo.algorithms.moo.nsga2 import binary_tournament
from pymoo.core.survival import Survival
from pymoo.docs import parse_doc_string
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PM
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.operators.selection.tournament import TournamentSelection
from pymoo.termination.default import DefaultMultiObjectiveTermination
from pymoo.util.display.multi import MultiObjectiveOutput
from pymoo.util.misc import has_feasible
from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting


# =========================================================================================================
# Implementation
# =========================================================================================================

[docs] class AGEMOEA(GeneticAlgorithm): def __init__(self, pop_size=100, sampling=FloatRandomSampling(), selection=TournamentSelection(func_comp=binary_tournament), crossover=SBX(eta=15, prob=0.9), mutation=PM(eta=20), eliminate_duplicates=True, n_offsprings=None, output=MultiObjectiveOutput(), **kwargs): """ Adapted from: Panichella, A. (2019). An adaptive evolutionary algorithm based on non-euclidean geometry for many-objective optimization. GECCO 2019 - Proceedings of the 2019 Genetic and Evolutionary Computation Conference, July, 595–603. https://doi.org/10.1145/3321707.3321839 Parameters ---------- pop_size : {pop_size} sampling : {sampling} selection : {selection} crossover : {crossover} mutation : {mutation} eliminate_duplicates : {eliminate_duplicates} n_offsprings : {n_offsprings} """ super().__init__(pop_size=pop_size, sampling=sampling, selection=selection, crossover=crossover, mutation=mutation, survival=AGEMOEASurvival(), eliminate_duplicates=eliminate_duplicates, n_offsprings=n_offsprings, output=output, advance_after_initial_infill=True, **kwargs) self.default_termination = DefaultMultiObjectiveTermination() self.tournament_type = 'comp_by_rank_and_crowding' def _set_optimum(self, **kwargs): if not has_feasible(self.pop): self.opt = self.pop[[np.argmin(self.pop.get("CV"))]] else: self.opt = self.pop[self.pop.get("rank") == 0]
# --------------------------------------------------------------------------------------------------------- # Survival Selection # --------------------------------------------------------------------------------------------------------- class AGEMOEASurvival(Survival): def __init__(self) -> None: super().__init__(filter_infeasible=True) self.nds = NonDominatedSorting() def _do(self, problem, pop, *args, n_survive=None, **kwargs): # get the objective values F = pop.get("F") N = n_survive # Non-dominated sorting fronts = self.nds.do(F, n_stop_if_ranked=N) # get max int value max_val = np.iinfo(int).max # initialize population ranks with max int value front_no = np.full(F.shape[0], max_val, dtype=int) # assign the rank to each individual for i, fr in enumerate(fronts): front_no[fr] = i pop.set("rank", front_no) # get the index of the front to be sorted and cut max_f_no = np.max(front_no[front_no != max_val]) # keep fronts that have lower rank than the front to cut selected: np.ndarray = front_no < max_f_no n_ind, _ = F.shape # crowding distance is positive and has to be maximized crowd_dist = np.zeros(n_ind) # get the first front for normalization front1 = F[front_no == 0, :] # follows from the definition of the ideal point but with current non dominated solutions ideal_point = np.min(front1, axis=0) # Calculate the crowding distance of the first front as well as p and the normalization constants crowd_dist[front_no == 0], p, normalization = self.survival_score(front1, ideal_point) for i in range(1, max_f_no): # skip first front since it is normalized by survival_score front = F[front_no == i, :] m, _ = front.shape front = front / normalization crowd_dist[front_no == i] = 1. / self.minkowski_distances(front, ideal_point[None, :], p=p).squeeze() # Select the solutions in the last front based on their crowding distances last = np.arange(selected.shape[0])[front_no == max_f_no] rank = np.argsort(crowd_dist[last])[::-1] selected[last[rank[: N - np.sum(selected)]]] = True pop.set("crowding", crowd_dist) # return selected solutions, number of selected should be equal to population size return pop[selected] def survival_score(self, front, ideal_point): front = np.round(front, 12, out=front) m, n = front.shape crowd_dist = np.zeros(m) if m < n: p = 1 normalization = np.max(front, axis=0) return crowd_dist, p, normalization # shift the ideal point to the origin front = front - ideal_point # Detect the extreme points and normalize the front extreme = find_corner_solutions(front) front, normalization = normalize(front, extreme) # set the distance for the extreme solutions crowd_dist[extreme] = np.inf selected = np.full(m, False) selected[extreme] = True p = self.compute_geometry(front, extreme, n) nn = np.linalg.norm(front, p, axis=1) # Replace very small norms with 1 to prevent division by zero nn[nn < 1e-8] = 1 distances = self.pairwise_distances(front, p) distances[distances < 1e-8] = 1e-8 # Replace very small entries to prevent division by zero distances = distances / (nn[:, None]) neighbors = 2 remaining = np.arange(m) remaining = list(remaining[~selected]) for i in range(m - np.sum(selected)): mg = np.meshgrid(np.arange(selected.shape[0])[selected], remaining, copy=False, sparse=False) D_mg = distances[tuple(mg)] # avoid Numpy's future deprecation of array special indexing if D_mg.shape[1] > 1: # equivalent to mink(distances(remaining, selected),neighbors,2); in Matlab maxim = np.argpartition(D_mg, neighbors - 1, axis=1)[:, :neighbors] tmp = np.sum(np.take_along_axis(D_mg, maxim, axis=1), axis=1) index: int = np.argmax(tmp) d = tmp[index] else: index = D_mg[:, 0].argmax() d = D_mg[index, 0] best = remaining.pop(index) selected[best] = True crowd_dist[best] = d return crowd_dist, p, normalization @staticmethod def compute_geometry(front, extreme, n): # approximate p(norm) d = point_2_line_distance(front, np.zeros(n), np.ones(n)) d[extreme] = np.inf index = np.argmin(d) p = np.log(n) / np.log(1.0 / np.mean(front[index, :])) if np.isnan(p) or p <= 0.1: p = 1.0 elif p > 20: p = 20.0 # avoid numpy underflow return p @staticmethod #@jit(nopython=True, fastmath=True) def pairwise_distances(front, p): m = np.shape(front)[0] distances = np.zeros((m, m)) for i in range(m): distances[i] = np.sum(np.abs(front[i] - front) ** p, 1) ** (1 / p) return distances @staticmethod @jit(nopython=True, fastmath=True) def minkowski_distances(A, B, p): m1 = np.shape(A)[0] m2 = np.shape(B)[0] distances = np.zeros((m1, m2)) for i in range(m1): for j in range(m2): distances[i][j] = np.sum(np.abs(A[i] - B[j]) ** p) ** (1 / p) return distances @jit(nopython=True, fastmath=True) def find_corner_solutions(front): """Return the indexes of the extreme points""" m, n = front.shape if m <= n: return np.arange(m) # let's define the axes of the n-dimensional spaces W = 1e-6 + np.eye(n) r = W.shape[0] indexes = np.zeros(n, dtype=numba.intp) selected = np.zeros(m, dtype=numba.boolean) for i in range(r): dists = point_2_line_distance(front, np.zeros(n), W[i, :]) dists[selected] = np.inf # prevent already selected to be reselected index = np.argmin(dists) indexes[i] = index selected[index] = True return indexes @jit(nopython=True, fastmath=True) def point_2_line_distance(P, A, B): d = np.zeros(P.shape[0], dtype=numba.float64) for i in range(P.shape[0]): pa = P[i] - A ba = B - A t = np.dot(pa, ba) / np.dot(ba, ba) d[i] = np.sum((pa - t * ba) ** 2) return d # ========================================================================================================= # Normalization # ========================================================================================================= def normalize(front, extreme): m, n = front.shape if len(extreme) != len(np.unique(extreme, axis=0)): normalization = np.max(front, axis=0) front = front / normalization return front, normalization # Calculate the intercepts of the hyperplane constructed by the extreme # points and the axes try: hyperplane = np.linalg.solve(front[extreme], np.ones(n)) if any(np.isnan(hyperplane)) or any(np.isinf(hyperplane)) or any(hyperplane <= 0): normalization = np.max(front, axis=0) else: normalization = 1. / hyperplane if any(np.isnan(normalization)) or any(np.isinf(normalization)): normalization = np.max(front, axis=0) except np.linalg.LinAlgError: normalization = np.max(front, axis=0) normalization[normalization == 0.0] = 1.0 # Normalization front = front / normalization return front, normalization parse_doc_string(AGEMOEA.__init__)