File size: 24,839 Bytes
7775b72
 
ae445de
 
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6c3b05
 
0d0f645
f6c3b05
0d0f645
f6c3b05
7775b72
 
 
 
 
0d0f645
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43da5b2
6e16c34
0d0f645
6e16c34
43da5b2
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70cbdc1
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c78869
 
 
 
 
 
 
fe5d3f2
7c78869
fe5d3f2
 
 
 
 
 
 
 
 
 
 
 
 
 
7775b72
06a4769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae445de
 
 
 
 
 
7775b72
 
ae445de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27849a1
a82f0ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61a0673
a82f0ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7775b72
 
6677a7e
3c2f31a
 
6677a7e
0d0f645
7775b72
3c2f31a
 
 
7775b72
3c2f31a
5676e8a
3c2f31a
7775b72
0d0f645
d1e7472
0d0f645
 
d1e7472
0d0f645
3c2f31a
d84e707
7775b72
3c2f31a
2b1d24e
7775b72
a82f0ee
27849a1
a82f0ee
 
 
 
3c2f31a
 
 
7225605
3c2f31a
a82f0ee
7775b72
 
 
 
 
 
 
 
 
 
 
 
 
 
d84e707
2b1d24e
a82f0ee
 
7775b72
 
 
 
 
 
1321038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7775b72
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
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()