0%
0%

Using an evolutionary neural network to create an optimized broadband antenna for the ranges of 108MHz to 400MHz

Similar projects worth following
291 views
The goal for this project is to create a broadband antenna for voice communications on a military training jet. The antenna needs to have a VSWR of 2.5 or lower for the frequencies of 108MHz to 400MHz. Ideally, the antenna would be unturned. I will be using an evolutionary neural network to create an optimized antenna.

• Goal:
• Create a broadband antenna for voice communications on a military training jet.
• Background:
• What is VSWR?
• VSWR stands for Voltage Standing Wave Ratio. It is a measure of how much power is being reflected back to the source from the load. It typically written as a ratio like 2:1 or ∞:1 or as just the first number in that ratio (ie. 2:1 → 2). In a perfect scenario the VSWR would be 1 where 0% of the power is reflected back to the source and is all used by the load. The worst scenario is where 100% of the power is reflected back and this is the VSWR of a short-circuit.
• What are the required specifications?
• The VSWR has to be less than or equal to 2.5 for the frequencies of 108MHz to 162MHz and 225MHz to 400MHz but that essentially means that the antenna has to work for the whole range of 108-400.
• The antenna must fit in roughly 27cm tall and 75cm wide space. This makes low-frequency antenna design difficult because the wavelength for the lowest frequencies is 2.7m (though in practice lower because electromagnetic signals travel slower in copper/aluminum)
• Method:
• Software:
• Python due to the abundance of libraries for machine learning and antenna simulation.
• NEC using windows (4nec2) and python (necpp) front ends
• Google colab to run long-term simulations at a lower cost to my computer.
• Options considered:
• brute force/modify existing designs
• computer-optimized antenna design:
• machine-learning options (reinforcement, convolutional, evolutionary, RNN(language processing))
• So far a hybrid of modifying existing designs and computer-optimized seems to be the most efficient and likely to work method.
• Using a scimitar antenna design and modifying the parameters, such as scale (k), outside (a1), and inside (a2) curvature, and additional modifications such as vertical and horizontal stretches in conjunction with machine learning to optimize these variables.
• Generalized genetic algorithm
• An algorithm that allows for any antenna that fits the limitations of the space. In practice, this would mean a set/limited count of segments and putting a limit on the points that make up the geometry.

• Generalized Antenna Algorithm

The scimitar seemed unlikely to work well enough to meet the requirements for a few reasons. First, the antenna were larger than would fit in the space and still weren't even close to the goal. Also, scimitars are usually used for narrow high frequency ranges and are seemingly cant work well in lower ranges as I'd hoped. While there are a few options for existing designs to optimize (a U like design, multiple vertical wires, scimitar variants and trapezoidal shapes) I really wanted to see if a generalized antenna evolver would work.

I started by defining a rectangle that bounded where points could be, about 25cm tall and 60cm wide and defined a shape that would be on every antenna a 2cm by 1cm triangle where the antenna would be attached (with an alligator clip for testing but eventually soldered). Then, I defined an individual as a collection of points (a 2 by N matrix to represent the x and y position of N points) in the order that they would be connected by wires which would outline a shape. For example, an individual with a matrix of points like this:

[[ 0.          0.        ]
[ 0.01        0.01      ]
[ 0.1056655   0.2056655 ]
[ 0.262       0.32031084]
[ 0.262       0.4673137 ]
[ 0.262       0.60390589]
[ -.1347361   0.70804492]
[-0.01        0.01      ]
[ 0.          0.        ]]

would have an outline of:

Notice the small triangle at the bottom of the antenna, that's the preset shape that allows the antenna to be attached, every matrix has the points (0,0) and (0.01 , 0.01) at the beginning and a similar set of points at the end to define that shape. Then I just have to modify the testing code a bit to make it take in the new kind of individual and I can run the code as is. Just kidding. There's one big problem, my code cant really handle geometry errors. If two wires cross it breaks, if two wires are too close together it breaks, if the wires are in certain configurations it returns negative VSWRs (it breaks). Luckily, all the necpp functions return an int, 0 if it ran without errors, and some other number if there was an error (different numbers for different errors). I can find negative VSWRs because the real component of the impedance is negative. I use that result to check if there are any errors when calling a function and if there are, I give the broken antenna a high VSWR which should make it worse than any working antenna. I didn't have to do this for the scimitars because I set the limits on the individuals such that they would all give working shapes.

• Evolving Scimitars and Real World Comparisons

First I had to choose some parameters that would determine the specifics of the scimitar antenna. The 4 that I chose were a horizontal scaling coefficient, inner and outer curve values and scale. The code to create the scimitar geometry looked like this:

def scimitar (scale,a1,a2,a,segment,h):
A = np.zeros((2*segment,2))
for i in range(0,segment):
t = np.pi/(segment-1)*i
x=scale*np.exp(a1*t)*np.cos(t)
y=scale*np.exp(a1*t)*np.sin(t)
A.itemset((i,0),a*(x-scale))
A.itemset((i,1),y)
A.itemset((segment-1,1),A[segment-1,1]+h)
for i in range(1,segment):
t = np.pi/(segment-1)*(segment-i)
x=scale*np.exp(a2*t)*np.cos(t)
y=scale*np.exp(a2*t)*np.sin(t)
A.itemset((i+segment-1,0),a*(x-scale))
A.itemset((i+segment-1,1),y)
A.itemset((segment,1),A[segment,1]+h)
A.itemset((2*segment-1,0),0)
A.itemset((2*segment-1,1),0)
return A

where scale, a1, a2, and a were to be optimized though the evolutionary algorithm and segment and h were to be held constant at 15 and .001 respectively. I set boundary conditions for the antenna so that they would be a reasonable size (Not good ones though because the output antenna ended up being about a meter long). Next, I modified the existing code to test the scimitar geometry and output its score as a sum of all of the VSWRs in the chosen range to optimize. Essentially, you would be minimizing the average over a range. After running it for 500 generations with a population of 500 the end result looked like this:

which gave a VSWR graph that looked like this:

where the y-axis is 0-50 and the x-axis is 118 to 400MHz. After that I tried a few other optimization strategies. First, putting a 3 times weight on all frequencies higher than 250MHz which yielded this antenna:

and gave a VSWR graph of:

(same axis). Next I wanted to see if real versions of the antenna would give similar VSWR plots as the simulated ones. After drawing out the scimitar shapes on some paper, gluing tinfoil to that paper and cutting out the shapes, I tested the geometries. Side note: tinfoil gives the same VSWR values as a metal plate of the same shape, antennas are just made out of .032" aluminum so they don't melt. Outlines of wire also (supposedly). give the same VSWR as a filled in sheet. Anyways, the VSWR sweeps of the physical antennas were completely different from the simulated ones. The actual sweep of the weighted looked like this:

the x-axis is 0-1400MHz the shape is sorta similar in the range 118-400 but the values are considerably lower in the real world. This is sorta ok because if the shape of the graph is similar then an optimized version will still be optimized in the real world and just have a better VSWR.

• Necpp And Testing Scimitars

The first thing I did was to find an example and test it in google colab. The example came from the PyPi Website. It simulates a vertical pole and calculates the impedance for the frequency 34.5 MHz.

!pip install necpp
import necpp

def handle_nec(result):
if (result != 0):
print (necpp.nec_error_message())

def impedance(frequency, z0, height):

nec = necpp.nec_create()
handle_nec(necpp.nec_wire(nec, 1, 17, 0, 0, z0, 0, 0, z0+height, 0.1, 1, 1))
handle_nec(necpp.nec_geometry_complete(nec, 0))
handle_nec(necpp.nec_gn_card(nec, 1, 0, 0, 0, 0, 0, 0, 0))
handle_nec(necpp.nec_fr_card(nec, 0, 1, frequency, 0))
handle_nec(necpp.nec_ex_card(nec, 0, 0, 1, 0, 1.0, 0, 0, 0, 0, 0))
handle_nec(necpp.nec_rp_card(nec, 0, 90, 1, 0,5,0,0, 0, 90, 1, 0, 0, 0))
result_index = 0

z = complex(necpp.nec_impedance_real(nec,result_index),
necpp.nec_impedance_imag(nec,result_index))

necpp.nec_delete(nec)
return z

z = impedance(frequency = 34.5, z0 = 0.5, height = 4.0)
print ("Impedance \t(%6.1f,%+6.1fI) Ohms" % (z.real, z.imag))

After getting that to work I started trying to get multiple wires and connections to work with the goal of eventually putting together a scimitar geometry. At first I was getting a lot of strange errors and the lack of good documentation and examples made debugging really difficult. The error was "index 2 is out of bounds for axis 0 with size 2" or sometime just the program crashing. Eventually I figured out the problem was with the segment count so I brute forced my way to a segment count that worked for the shape I was trying. Later I found out that the segment count has to be high enough so that the largest segment of a wire has to be smaller than 20% of the smallest wavelength. Basically, divide the speed of light by the highest frequency, divide that by 5 and make sure the wire has enough segments so that they are all smaller than that value. From there I could calculate the impedance for a specific frequency for any geometry I wanted, the final code looked like this:

def impedance(freq,pts):
nec = necpp.nec_create()
for a in range(0,pts.shape-1):
y1=pts.item((a,0))
z1=pts.item((a,1))
y2=pts.item((a+1,0))
z2=pts.item((a+1,1))
handle_nec(necpp.nec_wire(nec, a+1, segmentCount(y1,z1,y2,z2), 0, y1, z1, 0, y2, z2, 0.001, 1, 1))

handle_nec(necpp.nec_geometry_complete(nec, 1))
handle_nec(necpp.nec_gn_card(nec, 1, 0, 0, 0, 0, 0, 0, 0))
handle_nec(necpp.nec_fr_card(nec, 0, 1, freq, 0))
handle_nec(necpp.nec_ex_card(nec, 0, 1, 1, 0, 1.0, 0, 0, 0, 0, 0))
handle_nec(necpp.nec_ld_card(nec, 5, 1, 0, 0, 58001000, 0, 0))
handle_nec(necpp.nec_xq_card(nec, 0))
result_index = 0
z = complex(necpp.nec_impedance_real(nec,result_index),
necpp.nec_impedance_imag(nec,result_index))

necpp.nec_delete(nec)
return z

where this would return the real and imaginary components of impedance for a certain frequency (freq) for a given list of points (pts). Impedance could then be turned into VSWR through the simple little equation (1+(sqrt((R-Z)^2+j^2)/(sqrt((R+Z)^2+j^2)))/(1-((sqrt((R-Z)^2+j^2)/(sqrt((R+Z)^2+j^2)))) where R is the real component j is the imaginary component and Z is the source impedance found more clearly here. Next, I used plt.plot to more easily visualize where the wires were in place of plt.scatter. Then, I decided to start graphing the VSWR for multiple frequencies. Originally, I called the impedance functions for every frequency in the range but this was slow because the program had to rebuild the geometry and environment each time it tested a frequency. I learned that you can run and access multiple frequencies at once and modified the impedance code so that result_index would be looped through and all the frequencies would be added to a list which would then be returrned:

for result_index in range(0,n):
z = complex(necpp.nec_impedance_real(nec,result_index),
necpp.nec_impedance_imag(nec,result_index))
A.append(z)
necpp.nec_delete(nec)
return A

and...

• Generalized Scimitar Geometry

After researching existing antennas, I figured that I would try to recreate some of them and see if I could get similar results using the windows version of 4nec2, the python version necpp and a real world sheet of metal. The chosen antenna design was a scimitar antenna like was used in the Apollo program. I wanted to get a list of points on the path of the scimtar that I could later feed into necpp. Using the patent I parameterized the two curves (k e^(at) cos t, k e^(at) sin t) and wrote some code to generate and plot the points.

def scimitar (scale,a1,a2,segment):
A = np.zeros((2*segment,2))
for i in range(0,segment):
t = np.pi/(segment-1)*i
x=scale*np.exp(a1*t)*np.cos(t)
y=scale*np.exp(a1*t)*np.sin(t)
A.itemset((i,0),x)
A.itemset((i,1),y)
for i in range(0,segment):
t = np.pi/(segment-1)*i
x=scale*np.exp(a2*t)*np.cos(t)
y=scale*np.exp(a2*t)*np.sin(t)
A.itemset((i+segment,0),x)
A.itemset((i+segment,1),y)
return A

pts=scimitar(1,.35,.08,20)
plt.scatter(*zip(*pts))
plt.show()

and ended up with a result like this:

• Evolutionary Program Testing

After watching an introductory lecture about some basic machine learning algorithms (linear regression, neural networks, RNN and reinforcement learning) as well as doing some research on evolutionary neural networks. I coded a simple evolution inspired AI, the basic process is to create a random assortment of matrices (which will eventually represent wires on the antenna based on the matrix value of 1 or 0):

#create random starting matrices
for j in range(0,populationCount):
plate = np.random.randint(2,size=(rows,columns))
Population.append( plate )

Then, after scoring these "individuals" according to a currently arbitrary test (will later be their VSWR) then discarding the bottom half of the population and taking attributes from the top half to refill the population along with a mutation chance (a random chance to flip some number of the matrices' bits).

The way that I am currently selecting attributes from the top half is to average 3 random matrices from the surviving half and rounding up to 1 or down to 0, but this might change later on. Then some random mutations are added according to a mutation chance and the process restarts with the new population.

def newGen():
for plate in range(0,int(populationCount/2)):
a = np.random.randint(0,int(populationCount/2))
b = np.random.randint(0,int(populationCount/2))
c = np.random.randint(0,int(populationCount/2))
new = scramble(Population[a],Population[b],Population[c])
Population.append(new)

def scramble(a,b,c):
avg = np.round(sum/3).astype(int)
if(np.random.randint(0,mutationChance)==1):
for i in range(0,np.random.randint(1,10)):
avg[np.random.randint(0,rows),np.random.randint(0,columns)] = (avg[np.random.randint(0,rows),np.random.randint(0,columns)]+1)%2
return avg

This process repeats for the specified number of generations and the best individual is returned along with their final score.

So far the results seem good but the testing function I have isn't affected by local groups but rather an individual entry in the matrix. This is unlike what the antenna simulation will be so I will likely have to modify the method of selecting traits from the successful individuals.

Share

Does this project spark your interest?

Become a member to follow this project and never miss any updates 