Multiobjective optimization using differential evolution#

In the introduction, two multiobjective optimization problems were described and the Pareto fronts of the problems were found by sampling the space and using a non-dominated sorting algorithm that finds a set of non-dominated solutions within the sampled points. However, the sorting algorithm can find it harder to find accurate fronts in the case of problems with complex functions. In this section, the non-dominated sorting differential evolution (NSDE) algorithm is introduced and used to solve the same two optimization problems. This algorithm uses the non-dominated sorting method and applies it to each population of the differential evolution algorithm. That along with other heuristics such as crowding functions makes it more likely that the algorithm finds a more accurate Pareto front for a problem than just using the non-dominated sorting method.

NOTE: The NSDE algorithm used in this section has been implemented using pymoode which is an extension for pymoo and contains multiobjective differential evolution methods. pymoode must be installed before running the code of this section. To install it, activate your conda environment and use the command pip install pymoode==0.3.0rc3. Make sure that you use this exact command since other versions pymoode have certains bugs that prevent installation.

The block of code below imports the required packages for this section

import numpy as np
import matplotlib.pyplot as plt
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
from pymoode.algorithms import NSDE
from pymoode.survival import RankAndCrowding
from pymoo.util.nds.non_dominated_sorting import NonDominatedSorting

Branin-Currin optimization problem#

The Branin-Currin optimization problem was described in the previous section. The block of code below defines the two objective functions of the problem.

# Defining the objective functions
def branin(x):

    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)

    x1 = 15*x[:,0] - 5
    x2 = 15*x[:,1]

    b = 5.1 / (4*np.pi**2)
    c = 5 / np.pi
    t = 1 / (8*np.pi)

    y = (1/51.95)*((x2 - b*x1**2 + c*x1 - 6)**2 + 10*(1-t)*np.cos(x1) + 10 - 44.81)

    if dim == 1:
        y = y.reshape(-1)

    return y

def currin(x):

    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)

    x1 = x[:,0]
    x2 = x[:,1]
    
    factor = 1 - np.exp(-1/(2*x2))
    num = 2300*x1**3 + 1900*x1**2 + 2092*x1 + 60
    den = 100*x1**3 + 500*x1**2 + 4*x1 + 20
    y = factor*num/den
    
    if dim == 1:
        y = y.reshape(-1)

    return y

The blocks of code below defines the Problem class for the optimization problem. The optimization problem is solved using the NSDE algorithm that is given in pymoode. The number of generations and function evaluations required by the algorithm are also reported.

# Defining the problem class for pymoo - we are evaluating two objective functions in this case
class BraninCurrin(Problem):

    def __init__(self):
        super().__init__(n_var=2, n_obj=2, n_constr=0, xl=np.array([1e-6, 1e-6]), xu=np.array([1, 1]))

    def _evaluate(self, x, out, *args, **kwargs):

        out["F"] = np.column_stack((branin(x), currin(x)))
problem = BraninCurrin()
algorithm = NSDE(pop_size=100, CR=0.9, survival=RankAndCrowding(crowding_func="pcd"),save_history = True) 
res_nsde = minimize(problem, algorithm, verbose=False)
print("Number of generations:", len(res_nsde.history))
print("Number of function evaluations:", res_nsde.algorithm.evaluator.n_eval)
Number of generations: 95
Number of function evaluations: 9500

To visualize the convergence of the algorithm, the set of non-dominated points found at each generation of NSDE are plotted at specific iterations of the algorithm. The plots show that the set of non-dominated points converges to the Pareto front of the problem as the iterations progress.

niter = len(res_nsde.history) # Number of iterations
iterations = np.arange(0,20,5)
iterations = np.concatenate((iterations, np.arange(20,niter,20)))

# Plotting the particle evolution
for itr in iterations:
    fig, ax = plt.subplots(1,1,figsize = (8,6))
    ax.scatter(res_nsde.history[itr].pop.get("F")[::5, 0], res_nsde.history[itr].pop.get("F")[::5, 1], color = "blue")
    ax.set_title("Iteration %s" % itr)
    ax.grid()
    ax.set_xlabel("$f_1$", fontsize = 14)
    ax.set_ylabel("$f_2$", fontsize = 14)
../_images/0d3eb756e7bdd2f46c1ddd474ac38778112b1fc67977bf8ed38d49f48ec73d63.png ../_images/72b53bfc51e36ed159705c95f3d1505e1638546179335300c3ad91773bceebc6.png ../_images/0897cb5b1ed6a2b2e254c86378b038e46b84754ccb33a9a2e03acf02f8697b33.png ../_images/65334e57f9e8cf26fb9d92e4b8d19e9083b5e09d1c2e07745530606f410e1fa8.png ../_images/ea1c804ee3e31a716da100bff9b221a109cdbabb9a9f55eb712b1a7d79358b07.png ../_images/6ec07065ecee0f76bb7c0cdd339aad635c9bd9784c53678dd088bfe28cba7d92.png ../_images/455f0f7b911c3755869186ea828c00459d3bc7e6993c4fe16525119c3ff633bb.png ../_images/49bb92fdb39ab422f398d8eb523808ec126e8712500fadb72c0d8fa95b31f022.png

The block of code below plots the final Pareto front obtained for the problem and comapares it to the one found using non-dominated sorting alone. The Pareto front obtained using differential evolution is similar to the one obtained using non-dominated sorting, however, the known frontier of the problem is known to be smooth and this smoothness is better captured by the NSDE solution as compared to the non-dominated sorting solution.

# Generating a grid of points
num_points = 100

# Defining x and y values
x = np.linspace(1e-6,1,num_points)
y = np.linspace(1e-6,1,num_points)

# Creating a mesh
X, Y = np.meshgrid(x, y)

# Finding the front through non-dominated sorting
z1 = branin(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
z2 =  currin(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
nds = NonDominatedSorting()
F = np.column_stack((z1,z2))
pareto = nds.do(F, n_stop_if_ranked=50)
Z_pareto = F[pareto[0]]

# Plotting final Pareto frontier obtained
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(Z_pareto[1:,0], Z_pareto[1:,1], color="red", label="Non-dominated sorting")
ax.scatter(res_nsde.F[::5, 0], res_nsde.F[::5, 1], color="blue", label="NSDE")
ax.set_ylabel("$f_2$", fontsize = 14)
ax.set_xlabel("$f_1$", fontsize = 14)
ax.legend(fontsize = 14)
ax.grid()
../_images/3560b3aced538ca266782d8cca32eb75f16544d83d193c77b359d55991907d67.png

Constrained optimization problem#

The constrained optimization problem was described in the previous section. The code block below defines the objective and constraint functions for the problem.

# Defining the objective functions
def f1(x):
    
    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)

    y = 4*x[:,0]**2 + 4*x[:,1]**2
    
    return y
    
def f2(x):

    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)

    y = (x[:,0]-5)**2 + (x[:,1]-5)**2

    return y

def g1(x):
    
    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)
    
    g = (x[:,0]-5)**2 + x[:,1]**2 - 25

    return g

def g2(x):

    dim = x.ndim
    if dim == 1:
        x = x.reshape(1,-1)
    
    g = 7.7 - ((x[:,0]-8)**2 + (x[:,1]+3)**2)

    return g

# Defining the problem class for pymoo - we are evaluating two objective and two constraint functions in this case
class ConstrainedProblem(Problem):

    def __init__(self):
        super().__init__(n_var=2, n_obj=2, n_ieq_constr=2, vtype=float)

        self.xl = np.array([-20.0, -20.0])
        self.xu = np.array([20.0, 20.0])

    def _evaluate(self, x, out, *args, **kwargs):

        out["F"] = np.column_stack([f1(x), f2(x)])
        out["G"] = np.column_stack([g1(x), g2(x)])

The blocks of code below defines the Problem class for the optimization problem. The optimization problem is solved using the NSDE algorithm that is given in pymoode. The number of generations and function evaluations required by the algorithm are also reported. The convergence of the process is shown by plotting the non-dominated solutions obtained at every iteration of the algorithm. This shows the evolution of the Pareto front as the iterations progress.

problem = ConstrainedProblem()
nsde = NSDE(pop_size=200, CR=0.8, survival=RankAndCrowding(crowding_func="pcd"), save_history = True)
res_nsde = minimize(problem, nsde, verbose=False)
print("Number of generations:", len(res_nsde.history))
print("Number of function evaluations:", res_nsde.algorithm.evaluator.n_eval)
Number of generations: 78
Number of function evaluations: 15600
niter = len(res_nsde.history) # Number of iterations
iterations = np.arange(0,20,5)
iterations = np.concatenate((iterations, np.arange(20,niter,20)))

# Plotting the particle evolution
for itr in iterations:
    fig, ax = plt.subplots(1,1,figsize = (8,6))
    ax.scatter(res_nsde.history[itr].pop.get("F")[::5, 0], res_nsde.history[itr].pop.get("F")[::5, 1], color = "blue")
    ax.set_title("Iteration %s" % itr)
    ax.grid()
    ax.set_xlabel("$f_1$", fontsize = 14)
    ax.set_ylabel("$f_2$", fontsize = 14)
../_images/f27d380b40b4279b4d5285ee72f5e6bae377a0be44477cfd7561feecfc2fcd84.png ../_images/5b829068bd91c009618a0abde4c9aeb1f57e083ecf24dc002dff068bea7e8362.png ../_images/b525782d574a58b138d95753b3b96052586d264549fb09c62193a622788db0ee.png ../_images/13b187dc570365f32da4062e03ab8c616baa0eef9a454c7b905b13ccc210a112.png ../_images/b7ec6b469a9e05af45084f0296d805f34fbedb56c5cb68e762c534a5a71da6f4.png ../_images/c4b84da8936641670f02b58a36b9e8d980580b33be9dcc08fad4574ae6e3e2ed.png ../_images/81117d498e446d9748745a6e4f4c5e73fec65825aacce34c8a8b642c04635841.png
# Generating a grid of points
num_points = 100

# Defining x and y values
x = np.linspace(-20,20,num_points)
y = np.linspace(-20,20,num_points)

# Creating a mesh
X, Y = np.meshgrid(x, y)

# Finding the front through non-dominated sorting
z1 = f1(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
z2 = f2(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
const1 = g1(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
const2 = g2(np.hstack((X.reshape(-1,1),Y.reshape(-1,1))))
z1 = z1[(const1<0) & (const2<0)]
z2 = z2[(const1<0) & (const2<0)]
nds = NonDominatedSorting()
F = np.column_stack((z1,z2))
pareto = nds.do(F, n_stop_if_ranked=50)
Z_pareto = F[pareto[0]]

# Plotting final Pareto frontier obtained
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(Z_pareto[:,0], Z_pareto[:,1], color="red", label="Non-dominated sorting")
ax.scatter(res_nsde.F[::5, 0], res_nsde.F[::5, 1], color="blue", label="NSDE")
ax.set_ylabel("$f_2$", fontsize = 14)
ax.set_xlabel("$f_1$", fontsize = 14)
ax.legend(fontsize = 14)
ax.grid()
../_images/2baed32e00006e169be5c5b8b3a195b19f1057195281ba7f19de1019b2d93159.png

In this case, the Pareto front obtained using the non-dominated sorting algorithm and the NSDE algorithm are identical to each other. This means that when the functions of the problem are simple enough, both direct sorting of the solutions and directly solving the optimization problems will yield similar results. The Pareto front for this problem is a smooth convex curve.