TroglodyteDerivations's picture
Updated lines 654-669 with: LaTeX formulation vis-a-vis proper delimiters ‘$$'
1321038 verified
import numpy as np
import math
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from io import BytesIO
from PIL import Image
import gradio as gr
from mpl_toolkits.mplot3d import Axes3D
class Objective:
def Evaluate(self, p):
return -5.0*np.exp(-0.5*((p[0]+2.2)**2/0.4+(p[1]-4.3)**2/0.4)) + -2.0*np.exp(-0.5*((p[0]-2.2)**2/0.4+(p[1]+4.3)**2/0.4))
# Create an instance of the Objective class
obj = Objective()
# Evaluate the fitness of a position
position = np.array([-2.2, 4.3])
fitness = obj.Evaluate(position)
print(f"The fitness of the position {position} is {fitness}")
class Bounds:
def __init__(self, lower, upper, enforce="clip"):
self.lower = np.array(lower)
self.upper = np.array(upper)
self.enforce = enforce.lower()
def Upper(self):
return self.upper
def Lower(self):
return self.lower
def Limits(self, pos):
npart, ndim = pos.shape
for i in range(npart):
for j in range(ndim):
if pos[i, j] < self.lower[j]:
if self.enforce == "clip":
pos[i, j] = self.lower[j]
elif self.enforce == "resample":
pos[i, j] = self.lower[j] + np.random.random() * (self.upper[j] - self.lower[j])
elif pos[i, j] > self.upper[j]:
if self.enforce == "clip":
pos[i, j] = self.upper[j]
elif self.enforce == "resample":
pos[i, j] = self.lower[j] + np.random.random() * (self.upper[j] - self.lower[j])
pos[i] = self.Validate(pos[i])
return pos
def Validate(self, pos):
return pos
# Define the bounds
lower_bounds = [-6, -6]
upper_bounds = [6, 6]
# Create an instance of the Bounds class
bounds = Bounds(lower_bounds, upper_bounds, enforce="clip")
# Define a set of positions
positions = np.array([[15, 15], [-15, -15], [5, 15], [15, 5]])
# Enforce the bounds on the positions
valid_positions = bounds.Limits(positions)
print(f"Valid positions: {valid_positions}")
# Define the bounds
lower_bounds = [-6, -6]
upper_bounds = [6, 6]
# Create an instance of the Bounds class
bounds = Bounds(lower_bounds, upper_bounds, enforce="resample")
# Define a set of positions
positions = np.array([[15, 15], [-15, -15], [5, 15], [15, 5]])
# Enforce the bounds on the positions
valid_positions = bounds.Limits(positions)
print(f"Valid positions: {valid_positions}")
class QuasiRandomInitializer:
def __init__(self, npart=10, ndim=2, bounds=None, k=1, jitter=0.0):
self.npart = npart
self.ndim = ndim
self.bounds = bounds
self.k = k
self.jitter = jitter
self.primes = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197,
199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313,
317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439,
443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571,
577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659
]
def Halton(self, i, b):
f = 1.0
r = 0
while i > 0:
f = f / b
r = r + f * (i % b)
i = math.floor(i / b)
return r
def InitializeSwarm(self):
self.swarm = np.zeros((self.npart, self.ndim))
lo = np.zeros(self.ndim)
hi = np.ones(self.ndim)
if self.bounds is not None:
lo = self.bounds.Lower()
hi = self.bounds.Upper()
for i in range(self.npart):
for j in range(self.ndim):
h = self.Halton(i + self.k, self.primes[j % len(self.primes)])
q = self.jitter * (np.random.random() - 0.5)
self.swarm[i, j] = lo[j] + (hi[j] - lo[j]) * h + q
if self.bounds is not None:
self.swarm = self.bounds.Limits(self.swarm)
return self.swarm
# Define the bounds
lower_bounds = [-6, -6]
upper_bounds = [6, 6]
bounds = Bounds(lower_bounds, upper_bounds, enforce="clip")
# Create an instance of the QuasiRandomInitializer class
init = QuasiRandomInitializer(npart=50, ndim=2, bounds=bounds)
# Initialize the swarm
swarm_positions = init.InitializeSwarm()
print(f"Initial swarm positions: {swarm_positions}")
# Define the bounds
lower_bounds = [-6, -6]
upper_bounds = [6, 6]
bounds = Bounds(lower_bounds, upper_bounds, enforce="resample")
# Create an instance of the QuasiRandomInitializer class
init = QuasiRandomInitializer(npart=50, ndim=2, bounds=bounds)
# Initialize the swarm
swarm_positions = init.InitializeSwarm()
print(f"Initial swarm positions: {swarm_positions}")
class GWO:
def __init__(self, obj, eta=2.0, npart=10, ndim=2, max_iter=200,tol=None,init=None,done=None,bounds=None):
self.obj = obj
self.npart = npart
self.ndim = ndim
self.max_iter = max_iter
self.init = init
self.done = done
self.bounds = bounds
self.tol = tol
self.eta = eta
self.initialized = False
def Initialize(self):
"""Set up the swarm"""
self.initialized = True
self.iterations = 0
self.pos = self.init.InitializeSwarm() # initial swarm positions
self.vpos= np.zeros(self.npart)
for i in range(self.npart):
self.vpos[i] = self.obj.Evaluate(self.pos[i])
# Initialize the list to store positions at each iteration
self.all_positions = []
self.all_positions.append(self.pos.copy()) # Store the initial positions
# Swarm bests
self.gidx = []
self.gbest = []
self.gpos = []
self.giter = []
idx = np.argmin(self.vpos)
self.gidx.append(idx)
self.gbest.append(self.vpos[idx])
self.gpos.append(self.pos[idx].copy())
self.giter.append(0)
# 1st, 2nd, and 3rd best positions
idx = np.argsort(self.vpos)
self.alpha = self.pos[idx[0]].copy()
self.valpha= self.vpos[idx[0]]
self.beta = self.pos[idx[1]].copy()
self.vbeta = self.vpos[idx[1]]
self.delta = self.pos[idx[2]].copy()
self.vdelta= self.vpos[idx[2]]
# *** Gradio app method optimize created [leveraged vis-a-vis optimize function on the outside of the underlying anatomy of GWO class] ***
def optimize(self):
"""
Run a full optimization and return the best positions and fitness values.
This method is designed to be used with Gradio.
"""
# Initialize the swarm
self.Initialize()
# Lists to store the best positions and fitness values at each step
best_positions = []
best_fitness = []
# Main loop
while not self.Done():
self.Step() # Perform an optimization step
# Update best_positions and best_fitness with the current best values
best_positions.append(self.gbest[-1])
best_fitness.append(self.gpos[-1])
# Convert the list of best positions to a NumPy array
best_positions_array = np.array(best_positions)
np.save('best_positions_array', best_positions_array)
# Print the best positions and fitness found
print("Best Positions:", best_positions)
print("Best Fitness:", best_fitness)
# Return the best positions and fitness after the optimization
return best_positions, best_fitness
def Step(self):
"""Do one swarm step"""
# a from eta ... zero (default eta is 2)
a = self.eta - self.eta*(self.iterations/self.max_iter)
# Update everyone
for i in range(self.npart):
A = 2*a*np.random.random(self.ndim) - a
C = 2*np.random.random(self.ndim)
Dalpha = np.abs(C*self.alpha - self.pos[i])
X1 = self.alpha - A*Dalpha
A = 2*a*np.random.random(self.ndim) - a
C = 2*np.random.random(self.ndim)
Dbeta = np.abs(C*self.beta - self.pos[i])
X2 = self.beta - A*Dbeta
A = 2*a*np.random.random(self.ndim) - a
C = 2*np.random.random(self.ndim)
Ddelta = np.abs(C*self.delta - self.pos[i])
X3 = self.delta - A*Ddelta
self.pos[i,:] = (X1+X2+X3) / 3.0
# Keep in bounds
if (self.bounds != None):
self.pos = self.bounds.Limits(self.pos)
# Get objective function values and check for new leaders
for i in range(self.npart):
self.vpos[i] = self.obj.Evaluate(self.pos[i])
# new alpha?
if (self.vpos[i] < self.valpha):
self.vdelta = self.vbeta
self.delta = self.beta.copy()
self.vbeta = self.valpha
self.beta = self.alpha.copy()
self.valpha = self.vpos[i]
self.alpha = self.pos[i].copy()
# new beta?
if (self.vpos[i] > self.valpha) and (self.vpos[i] < self.vbeta):
self.vdelta = self.vbeta
self.delta = self.beta.copy()
self.vbeta = self.vpos[i]
self.beta = self.pos[i].copy()
# new delta?
if (self.vpos[i] > self.valpha) and (self.vpos[i] < self.vbeta) and (self.vpos[i] < self.vdelta):
self.vdelta = self.vpos[i]
self.delta = self.pos[i].copy()
# is alpha new swarm best?
if (self.valpha < self.gbest[-1]):
self.gidx.append(i)
self.gbest.append(self.valpha)
np.save('best_fitness.npy', np.array(self.gbest))
self.gpos.append(self.alpha.copy())
np.save('best_positions.npy', np.array(self.gpos))
# Save the positions at the current iteration
self.all_positions.append(self.pos.copy())
np.save('all_positions.npy', np.array(self.all_positions), allow_pickle=True)
self.giter.append(self.iterations)
self.iterations += 1
def Done(self):
"""Check if we are done"""
if (self.done == None):
if (self.tol == None):
return (self.iterations == self.max_iter)
else:
return (self.gbest[-1] < self.tol) or (self.iterations == self.max_iter)
else:
return self.done.Done(self.gbest,
gpos=self.gpos,
pos=self.pos,
max_iter=self.max_iter,
iteration=self.iterations)
def Evaluate(self, pos):
p = np.zeros(self.npart)
for i in range(self.npart):
p[i] = self.obj.Evaluate(pos[i])
return p
def Results(self):
if (not self.initialized):
return None
return {
"npart": self.npart,
"ndim": self.ndim,
"max_iter": self.max_iter,
"iterations": self.iterations,
"tol": self.tol,
"eta": self.eta,
"gbest": self.gbest,
"giter": self.giter,
"gpos": self.gpos,
"gidx": self.gidx,
"pos": self.pos,
"vpos": self.vpos
}
def plot_contour_and_wolves(self, wolf_positions):
# Ensure wolf_positions is a 2D array
best_positions_1D = np.load('best_positions.npy')
wolf_positions = best_positions_1D
# Define the objective function
def objective_function(x, y):
return -5.0*np.exp(-0.5*((x+2.2)**2/0.4+(y-4.3)**2/0.4)) + -2.0*np.exp(-0.5*((x-2.2)**2/0.4+(y+4.3)**2/0.4))
# Determine the search space boundaries based on the wolf positions
x_min, x_max = wolf_positions[:, 0].min() - 1, wolf_positions[:, 0].max() + 1
y_min, y_max = wolf_positions[:, 1].min() - 1, wolf_positions[:, 1].max() + 1
# Generate a grid of points within the determined search space
x = np.linspace(x_min, x_max, 100)
y = np.linspace(y_min, y_max, 100)
X, Y = np.meshgrid(x, y)
# Evaluate the objective function on the grid
Z = objective_function(X, Y)
# Plot the contour map
plt.figure(figsize=(10, 8))
contour = plt.contour(X, Y, Z, levels=20, cmap='magma')
plt.colorbar(contour)
# Plot the wolf positions
plt.plot(wolf_positions[:, 0], wolf_positions[:, 1], 'ro', markersize=5, label='Wolves')
# Set plot title and labels
plt.title('Contour Map of Wolves Oscillating Over Search Space')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
# Set the x and y limits of the plot based on the determined search space
plt.xlim(x_min, x_max)
plt.ylim(y_min, y_max)
# Convert the plot to a PIL Image and return it
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
plt.close() # Close the figure to free up memory
return Image.open(buf)
#def Dispersion(self):
#"""Calculate the dispersion of the swarm"""
#x, y = self.gpos[:, 0], self.gpos[:, 1]
#dx = x.max() - x.min()
#dy = y.max() - y.min()
#return (dx + dy) / 2.0
#def Dispersion(self):
# """Calculate the dispersion of the swarm"""
#dispersion = np.std(self.gpos[:, 0]) + np.std(self.gpos[:, 1])
#return dispersion
def Dispersion(self):
"""Calculate the dispersion of the swarm"""
# Ensure self.gpos is a NumPy array
if not isinstance(self.gpos, np.ndarray):
self.gpos = np.array(self.gpos)
# Now self.gpos should be a NumPy array, so we can calculate the dispersion
x, y = self.gpos[:, 0], self.gpos[:, 1]
dx = x.max() - x.min()
dy = y.max() - y.min()
return (dx + dy) / 2.0
def plot_dispersion_heatmap(self, x_range, y_range, resolution=100):
# Create a grid of points within the specified range
x = np.linspace(*x_range, resolution)
y = np.linspace(*y_range, resolution)
X, Y = np.meshgrid(x, y)
positions = np.vstack([X.ravel(), Y.ravel()]).T
# Calculate the dispersion for each position in the grid
dispersion_values = np.array([self.Dispersion() for _ in positions])
Z = dispersion_values.reshape(X.shape)
# Plot the dispersion heatmap
plt.figure(figsize=(10, 8))
plt.pcolormesh(X, Y, Z, cmap='viridis')
plt.colorbar(label='Dispersion')
# Set plot title and labels
plt.title('Dispersion Heatmap')
plt.xlabel('x')
plt.ylabel('y')
# Convert the plot to a PIL Image and return it
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
plt.close() # Close the figure to free up memory
return Image.open(buf)
def plot_dispersion(self):
"""Plot the dispersion over time"""
# Assuming self.giter stores the iteration number at which each best position was found
# and self.gbest stores the corresponding best fitness values
dispersion_values = [self.Dispersion() for _ in range(self.max_iter)]
plt.figure(figsize=(10, 6))
plt.plot(range(self.max_iter), dispersion_values, label='Dispersion')
plt.xlabel('Iteration')
plt.ylabel('Dispersion')
plt.title('Evolution of Dispersion')
plt.legend()
plt.show()
# Convert the plot to a PIL Image and return it
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
plt.close() # Close the figure to free up memory
return Image.open(buf)
#def plot_dispersion_heatmap(self, x_range, y_range, grid_size=100):
#"""Plot a heatmap of dispersion over a grid of positions"""
#x_values = np.linspace(x_range[0], x_range[1], grid_size)
#y_values = np.linspace(y_range[0], y_range[1], grid_size)
#X, Y = np.meshgrid(x_values, y_values)
#positions = np.vstack([X.ravel(), Y.ravel()]).T
# Calculate dispersion for each position in the grid
#dispersion_values = np.array([self.Dispersion(pos) for pos in positions])
#Z = dispersion_values.reshape(X.shape)
#plt.figure(figsize=(10, 8))
#plt.imshow(Z, extent=(x_range[0], x_range[1], y_range[0], y_range[1]), origin='lower', cmap='viridis')
#plt.colorbar(label='Dispersion')
#plt.title('Heatmap of Dispersion')
#plt.xlabel('X')
#plt.ylabel('Y')
#plt.show()
# Convert the plot to a PIL Image and return it
#buf = BytesIO()
#plt.savefig(buf, format='png')
#buf.seek(0)
#plt.close() # Close the figure to free up memory
#return Image.open(buf)
def plot_positions(self, iterations=[0, 2, 8, 10]):
"""Plot the positions of the particles over the specified iterations"""
# Load the all_positions data from the .npy file
all_positions = np.load('all_positions.npy', allow_pickle=True)
# Create a figure with the correct number of subplots
num_iterations = len(iterations)
fig, axs = plt.subplots(num_iterations, figsize=(6, 4 * num_iterations))
# If there is only one subplot, make it an array to simplify the loop
if num_iterations == 1:
axs = [axs]
# Iterate over the subplots and the specified iterations to plot
for i, ax in enumerate(axs):
# Plot the particles' positions at the specified iteration
iteration = iterations[i]
if iteration < len(all_positions):
positions = all_positions[iteration]
ax.scatter(positions[:, 0], positions[:, 1], c='r', alpha=0.5)
# Set plot title and labels
ax.set_title(f'Iteration {iteration}')
ax.set_xlabel('X')
ax.set_ylabel('Y')
else:
ax.axis('off') # Hide the subplot if the iteration is out of range
plt.tight_layout()
# Save the plot to a BytesIO object
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
image = Image.open(buf)
plt.close()
return image
def plot_3d_meshgrid_contour(self, obj):
"""Plot the 3D meshgrid with a 2D contour on the base"""
# Define the range for the x and y axis
x_range = np.linspace(self.bounds.Lower()[0], self.bounds.Upper()[0], 100)
y_range = np.linspace(self.bounds.Lower()[1], self.bounds.Upper()[1], 100)
# Create a grid of points
X, Y = np.meshgrid(x_range, y_range)
Z = np.zeros_like(X)
# Evaluate the objective function on the grid
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Z[i, j] = obj.Evaluate(np.array([X[i, j], Y[i, j]]))
# Create a 3D plot with subplots for each rotation
fig, axs = plt.subplots(2, 2, figsize=(14, 10), subplot_kw={'projection': '3d'})
plt.subplots_adjust(wspace=0.1, hspace=0.1)
# Define the rotations for each subplot
rotations = [(30, 45), (30, 135), (30, -45), (30, -135)]
for i, ax in enumerate(axs.flatten()):
# Plot the meshgrid
ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.5)
# Plot the contour
ax.contour(X, Y, Z, levels=20, colors='k', alpha=0.5)
# Plot the best positions found by the algorithm
best_positions = np.array(self.gpos)
ax.plot(best_positions[:, 0], best_positions[:, 1], self.gbest, 'g*', markersize=4)
# Set labels and title
ax.set_xlabel('X Position')
ax.set_ylabel('Y Position')
ax.set_zlabel('Z Position')
ax.set_title(f'GWO Optimization Process in 3D - Rotation {i+1}')
# Set the limits of the plot to match the bounds
ax.set_xlim(self.bounds.Lower()[0], self.bounds.Upper()[0])
ax.set_ylim(self.bounds.Lower()[1], self.bounds.Upper()[1])
ax.set_zlim(np.min(Z), np.max(Z))
# Change the background color to light blue
ax.set_facecolor('lightblue')
# Apply the rotation
ax.view_init(*rotations[i])
# Show the plot
plt.show()
# Save the plot to a BytesIO object
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
image = Image.open(buf)
plt.close()
return image
def optimize(npart, ndim, max_iter):
# Initialize the GWO algorithm with the provided parameters
gwo = GWO(obj=obj, npart=npart, ndim=ndim, max_iter=max_iter, init=init, bounds=bounds)
best_positions, best_fitness= gwo.optimize()
# Convert best_fitness and best_positions to NumPy arrays if necessary
best_fitness_npy = np.array(best_fitness)
best_positions_npy = np.array(best_positions)
# Calculate dispersion
dispersion = gwo.Dispersion()
dispersion_text = f"Dispersion: {dispersion}"
# Load best_positions_loaded_array
best_positions_array = np.load('best_positions_array.npy')
# Plot the contour_and_wolves
plot_contour_and_wolves = gwo.plot_contour_and_wolves(best_positions_array)
# Plot the dispersion over time
dispersion_plot = gwo.plot_dispersion()
# Plot the dispersion heatmap
dispersion_heatmap_plot = gwo.plot_dispersion_heatmap(x_range=(-6,6), y_range=(-6,6))
# Plot the plot_positions
plot_positions = gwo.plot_positions(iterations=[0, 2, 8, 10])
# Plot the plot_3d_meshgrid_contour
plot_3d_meshgrid_contour = gwo.plot_3d_meshgrid_contour(obj=obj)
# Format the output strings
best_fitness_text = f"Best Fitness: {best_fitness_npy}"
best_positions_text = f"Best Positions: {best_positions_npy}"
# Return the images and text
return plot_contour_and_wolves, dispersion_plot, dispersion_heatmap_plot, plot_positions, plot_3d_meshgrid_contour, best_fitness_text, best_positions_text, best_fitness_npy, best_positions_npy, dispersion_text
# Define the Gradio interface
iface = gr.Interface(
fn=optimize, # Pass the optimize function object
inputs=[
gr.components.Slider(10, 50, 50, step=1, label="Number of Wolves [Default = 50 Wolves]"),
gr.components.Slider(2, 2, 2, step=1, label="Two-Dimensional Search Space"),
gr.components.Slider(100, 200, 200, step=1, label="Maximum Iterations [Default = 200 Epochs]"),
],
outputs=[
gr.components.Image(type="pil", label="Contour Map of Wolves Oscillating Over Search Space"),
gr.components.Image(type="pil", label="Dispersion Plot"),
gr.components.Image(type="pil", label="Dispersion Heatmap Plot"),
gr.components.Image(type="pil", label="Best Positions Plot"),
gr.components.Image(type="pil", label="Plot 3D Meshgrid Contour"),
gr.components.Textbox(label="Dispersion Values"),
gr.components.Textbox(label="Best Positions"),
gr.components.Textbox(label="Best Fitness")
],
title="Grey Wolf Optimizer",
description=r"""
## Grey Wolf Optimizer
The Grey Wolf Optimizer (GWO) is a population-based metaheuristic optimization algorithm inspired by the social behavior of grey wolves in nature.
The objective function to be optimized is given by the following formula:
```math
f(p) = -5.0 \cdot \exp \left( -0.5 \cdot \left( \frac{(x+2.2)^2}{0.4} + \frac{(y-4.3)^2}{0.4} \right) \right) + -2.0 \cdot \exp \left( -0.5 \cdot \left( \frac{(x-2.2)^2}{0.4} + \frac{(y+4.3)^2}{0.4} \right) \right)
```
Or in a more readable form:
$$
f(p) = -5.0 \cdot \exp \left( -0.5 \cdot \left( \frac{(x+2.2)^2}{0.4} + \frac{(y-4.3)^2}{0.4} \right) \right) + -2.0 \cdot \exp \left( -0.5 \cdot \left( \frac{(x-2.2)^2}{0.4} + \frac{(y+4.3)^2}{0.4} \right) \right)
$$
""",
article="## Grey Wolf Optimizer\nThe Grey Wolf Optimizer (GWO) is a population-based metaheuristic optimization algorithm inspired by the social behavior of grey wolves in nature."
)
# Launch the Gradio interface
iface.launch()