Close
0%
0%

Evolved Broadband Antenna

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

Similar projects worth following
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 electo-magnetic 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 longterm simulations at a lower cost to my computer. 
  • Options considered: 
    • bruteforce/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.

  • Evolving Scimitars and Real World Comparisons

    JTKLENKE08/26/2020 at 16:08 0 comments

    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 otehr optimization strategies. Firth 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

    JTKLENKE08/18/2020 at 01:12 0 comments

    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[0]-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...

    Read more »

  • Generalized Scimitar Geometry

    JTKLENKE08/15/2020 at 19:13 0 comments

    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

    JTKLENKE08/03/2020 at 04:06 0 comments

    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):
      sum = np.add(np.add(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. 

View all 4 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

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