C:\> Rostislav Persion's Projects

.:: Antenna Evolver ::.
Using simulated annealing algorithm to evolve an antenna


Program that uses simulated annealing to evolve optimized antennas



Simulated annealing is a computer algorithm for solving multi-variable problems using mutation, random walk algorithms and simulated natural selection. The alternative is the brute force method where you try every possible variable value. This however is a costly process and very slow compared to simulated annealing. Annealing is a term borrowed from metal working. Specifically heat treating. When you heat a piece of metal and then let it slowly cool down, the inner structures align themselves automatically to the most “relaxed” state, making the metal softer and easier to machine. When performing simulated annealing algorithm, you must randomly tweak variables and keep score of the overall system performance. If you come across a very good result, better than the previous best, you store all the input variables, as well as a numeric score for that trial. This process is then performed over and over until you feel the results are good enough. There is no way to tell if your result is perfect, but it will be very good. One problem of simulated annealing is that a program can get stuck with a solution (local maximum) while there is an even better solution (global maximum). When using simulated annealing it is good practice to use multiple threads or multiple generations. The reason for this is because the algorithm might get stuck with a very good result (local maximum), and the random walk algorithm doesn’t reach out far enough to find the better solution (global maximum). Having multiple threads or generations lets the program start fresh and take on a different path, perhaps getting close to the global maximum. I decided to generate antennas made out of bent wire, made out of three segments. Any more segments would require much more computing power.

My project is a proof of concept where I demonstrate how simulated annealing can be applied to antenna design evolution. I decided to not use simulated evolutionary algorithm because in that system there is a reproduction step. This is where two successful results are mated in hope that something better emerges. With antennas this is not a good idea. If you mate two antenna results together the performance will be far from optimal. You may as well generate a random antenna.

My strongest programming language is C#, but chose to use Python to write the simulation. This is mostly because there are antenna simulation modules available for Python that aren’t available for C#. There is also a C++ version of the library available for those who prefer that language. My simulation uses the PyNEC module to put together and test the radiation pattern of the antenna being evaluated. This module is actually a wrapper for the C++ NEC library. When evolving a system using simulated annealing, you must be able to evaluate each attempt. There should be a way to score a trial run in order to compare its result with previous trials. In my simulation program I am attempting to maximize the antenna gain and directionality. This means that I need to have a function that gives a score value based on the current mutated values in question. The way I decided to score my antennas is by storing the maximum gain value from my 360 degree polar gain array, and then divide the value by the average gain of the entire 360 of the antenna. This selection process promotes a final radiation pattern with a very pronounced primary lobe and smaller lobes around the rest of it. If you wanted to place more value on evolving just gain, you would just score the maximum gain value within the entire radiation pattern and leave out the average gain value.

In conclusion, since antenna design is a very complex process you can rely on already existing templates, or you can try every possible combination. There is an infinite amount of possible antennas though. Using simulated annealing you can continuously evolve an antenna and arrive at a good result in a short period of time.

The ground plane has been disabled and the antenna operates in free space.

This program using a Python module called, PyNEC. You can find the documentaton for it here...

http://tmolteno.github.io/necpp/libnecpp_8h.html#a9cf02620e0d0365cbdd24071a7ebeae9

NEC2 Documentation PDF for C++ (same function parameters as in Python)

You can get the PyNEC Python module here... https://pypi.org/project/PyNEC/

py -m pip install PyNEC==1.7.3.4

Python modules used in programs...

pip install matplotlib

pip install matplotlib.pyplot

pip install numpy

pip install mpl_toolkits

https://visualstudio.microsoft.com/visual-cpp-build-tools/

pip install PyNEC==1.7.3.4



You will also need this before installing PyNEC module...

https://visualstudio.microsoft.com/visual-cpp-build-tools/



Animated screen shot. ↑



Live view of evolution... ↑



Final optimal antenna radiation pattern...↑



Evolver output. Red star is the RF input node. The blue symbol is the tip of the antenna. ↑

PYTHON CODE: ANTENNA SIMULATION (OPTIMIZES BOTH GAIN AND DIRECTIONALITY):


Configuration variables in code...

antenna_thickness = Antenna thickness
max_length = Limit of antenna geometry
random_walk = Maximum random step size
mutations = How many times to mutate per generation
generations = How many generations to do
frequency = Antenna input frequency in MHz

DOWNLOAD PYTHON CODE #001

  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
#pip install matplotlib
#pip install matplotlib.pyplot
#pip install numpy
#pip install mpl_toolkits
#https://visualstudio.microsoft.com/visual-cpp-build-tools/
#pip install PyNEC==1.7.3.4

from PyNEC import *
import numpy as np
import random
import time
from datetime import datetime
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt

#WELCOME
print("SIMULATED ANNEALING ANTENNA EVOLVER (MUTLIPLE GENERATIONS) - ROSTISLAV PERSION")
print()

#track best
antenna_thickness = 0.001
max_length = 1
random_walk = 0.1
mutations = 1000
generations = 10
frequency = 2400

#reset randomly best values
total_best_x1 = 0.0
total_best_y1 = 0.0
total_best_z1 = 0.001
total_best_x2 = 0.0
total_best_y2 = 0.0
total_best_z2 = 0.002
total_best_x3 = 0.0
total_best_y3 = 0.0
total_best_z3 = 0.003

total_best_gain = 0
total_bestscore = 0

#start graph
plt.ion()

#set up graph
fig = plt.figure(num='ROSTISLAV PERSION - ANTENNA EVOLVER')
ax = plt.subplot(111, polar=True)
ax.grid(True)

#start evolution
for j in range(1,generations + 1):

    #reset randomly best values
    bestgain = 0
    lowegain = 0
    bestscore = 0

    best_x1 = 0.0
    best_y1 = 0.0
    best_z1 = 0.001
    best_x2 = 0.0
    best_y2 = 0.0
    best_z2 = 0.002
    best_x3 = 0.0
    best_y3 = 0.0
    best_z3 = 0.003



    for i in range(0, mutations):

        #creation of a nec context
        context=nec_context()

        #get the associated geometry
        geo = context.get_geometry()

        #random number 1....
        rnd1 = random.uniform(-random_walk, random_walk)
        x1 = best_x1 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        y1 = best_y1 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        z1 = best_z1 + rnd1

        rnd1 = random.uniform(-random_walk, random_walk)
        x2 = best_x2 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        y2 = best_y2 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        z2 = best_z2 + rnd1

        rnd1 = random.uniform(-random_walk, random_walk)
        x3 = best_x3 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        y3 = best_y3 + rnd1
        rnd1 = random.uniform(-random_walk, random_walk)
        z3 = best_z3 + rnd1

        #node boundaries
        if x1 > max_length: x1 = max_length 
        if x1 < -max_length: x1 = -max_length 
        if y1 > max_length: y1 = max_length 
        if y1 < -max_length: y1 = -max_length 
        if z1 > max_length: z1 = max_length 
        if z1 < -max_length: z1 = -max_length 

        if x2 > max_length: x2 = max_length 
        if x2 < -max_length: x2 = -max_length 
        if y2 > max_length: y2 = max_length 
        if y2 < -max_length: y2 = -max_length 
        if z2 > max_length: z2 = max_length 
        if z2 < -max_length: z2 = -max_length 

        if x3 > max_length: x3 = max_length 
        if x3 < -max_length: x3 = -max_length 
        if y3 > max_length: y3 = max_length 
        if y3 < -max_length: y3 = -max_length 
        if z3 > max_length: z3 = max_length 
        if z3 < -max_length: z3 = -max_length 

        try:

            #add wires to the geometry
            geo.wire(1, 9, 0, 0, 0, x1, y1, z1, antenna_thickness, 1.0, 1.0)
            geo.wire(2, 9, x1, y1, z1, x2, y2, z2, antenna_thickness, 1.0, 1.0)
            geo.wire(3, 9, x2, y2, z2, x3, y3, z3, antenna_thickness, 1.0, 1.0)
            context.geometry_complete(0)

            #free space, no ground plane
            context.gn_card(-1, 0, 0, 0, 0, 0, 0, 0)
            #add a "ex" card to specify an excitation
            context.ex_card(0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0)
            #add a "fr" card to specify the frequency 
            context.fr_card(0, 1, frequency, 100.0e6) 
            #add a "rp" card to specify radiation pattern sampling parameters and to cause program execution
            context.rp_card(0, 91, 1, 0, 5, 0, 0, 0.0, 45.0, 4.0, 2.0, 1.0, 0.0)

            #get the radiation_pattern
            rp = context.get_radiation_pattern(0)

            # Gains are in decibels
            gains_db = rp.get_gain()
            gains = 10.0**(gains_db / 10.0)
            thetas = rp.get_theta_angles() * 3.1415 / 180.0
            phis = rp.get_phi_angles() * 3.1415 / 180.0

            #log the graph
            gains = np.sqrt(gains)

            #messing with numbers
            maxgain = (max(gains[ : , 0 ]))                                           
            score = max(gains[ : , 0 ]) / (sum(gains[ : , 0 ]) / len(gains[ : , 0 ]))                                 

            #check score
            if score > bestscore:
                bestscore = score
                bestgain = maxgain
                bestgain2 = (max(gains_db[ : , 0 ])) 

                best_x1 = x1
                best_y1 = y1
                best_z1 = z1

                best_x2 = x2
                best_y2 = y2
                best_z2 = z2

                best_x3 = x3
                best_y3 = y3
                best_z3 = z3

                best_thetas = thetas
                best_gains = gains
                best_gains2 = gains_db

                #update graph
                ax.clear()

                #draw previous global best, but only after the first evolution
                if j > 1:
                    ax.plot(total_best_thetas, total_best_gains[:,0], color='#AAAAAA', linewidth=3)
                ax.plot(best_thetas, best_gains[:,0], color='#000000', linewidth=3)
                ax.set_title("LAST BEST IN GENERATION: " + str(j) + ", BEST GENERATION SCORE: " + str(np.round(score, 3)) + ", BEST TOTAL SCORE: " + str(np.round(total_bestscore,3)), va='bottom', fontsize=8)
                
                # re-drawing the figure
                fig.canvas.draw()
                
                # to flush the GUI events
                fig.canvas.flush_events()
                #time.sleep(0.1)
                

        except Exception as e:
            #print("PASSIVE ERROR: " + str(e))
            pass

    #store global best
    if bestscore > total_bestscore:

        total_bestscore = bestscore
        total_best_gain = bestgain
        total_best_gain2 = bestgain2

        total_best_x1 = best_x1
        total_best_y1 = best_y1
        total_best_z1 = best_z1

        total_best_x2 = best_x2
        total_best_y2 = best_y2
        total_best_z2 = best_z2
        
        total_best_x3 = best_x3
        total_best_y3 = best_y3
        total_best_z3 = best_z3

        total_best_thetas = best_thetas
        total_best_gains = best_gains

    #output
    print("END OF EVOLUTION ITTERATION: " + str(j) + " OF " + str(generations))
    now = datetime.now()
    date_time = now.strftime("%m/%d/%Y, %H:%M:%S")
    print("BEST GAIN AT HIGH SCORE IN THIS GENERATION: " + str(maxgain) + " EVOLUTION SCORE: " + str(bestscore) + " TIME: " + date_time)
    print("TOTAL BEST SCORE: " + str(total_bestscore))
    print()

#END OF generations RESULTS
print("END OF EVOLUTION SIMULATION.")
print()
print("EVOLUTION COUNT: " + str(generations))
print("EVOLUTION SCORE: " + str(total_bestscore))
print("BEST GAIN FOUND: " + str(total_best_gain))
print()
print("BEST X0: 0")
print("BEST Y0: 0")
print("BEST Z0: 0")
print()
print("BEST X1: " + str(total_best_x1))
print("BEST Y1: " + str(total_best_y1))
print("BEST Z1: " + str(total_best_z1))
print()
print("BEST X2: " + str(total_best_x2))
print("BEST Y2: " + str(total_best_y2))
print("BEST Z2: " + str(total_best_z2))
print()
print("BEST X3: " + str(total_best_x3))
print("BEST Y3: " + str(total_best_y3))
print("BEST Z3: " + str(total_best_z3))
print()

#Final Graph
ax.clear()
ax.plot(total_best_thetas, total_best_gains[:,0], color='#FF0000', linewidth=3)
ax.set_title("45 DEGREE ELEVATION... BEST TOTAL SCORE: " + str(np.round(total_bestscore,3)) + ", BEST MAX GAIN: " + str(np.round(total_best_gain2,3)), va='bottom', fontsize=8)
fig.canvas.draw()
fig.canvas.flush_events()

#PLOT ANTENNA WIRES
plt.ioff()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
xpoints = np.array([0,total_best_x1,total_best_x2,total_best_x3])
ypoints = np.array([0,total_best_y1,total_best_y2,total_best_y3])
zpoints = np.array([0,total_best_z1,total_best_z2,total_best_z3])
ax.plot(xpoints, ypoints, zpoints)
ax.scatter(0, 0, 0, c='red', marker='*', s=800)
ax.scatter(total_best_x3, total_best_y3, total_best_z3, c='blue', marker='1', s=300)
plt.show()