-
Generalized Antenna Algorithm
10/21/2020 at 16:25 • 0 commentsThe 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
08/26/2020 at 16:08 • 0 commentsFirst 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
08/18/2020 at 01:12 • 0 commentsThe 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 the frequency card (fr_card) had to be modified to this:
handle_nec(necpp.nec_fr_card(nec, 0, n, freq, d))
where n is the number of frequencies tested and d is the distance between samplings
In order to get better results I moved the tip of the scimitar to (0,0) and the wide part of the scimitar off the ground plane. Given the size constraint of the space the antenna had to fit in I created a scaled (and slightly raised) version of the patented scimitar antenna and for an antenna that looked like this:
got this:
the orange line marks a VSWR of 2, which is the goal for the entire frequency range. The scale of the antenna is in meters so it is very small.
-
Generalized Scimitar Geometry
08/15/2020 at 19:13 • 0 commentsAfter 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
08/03/2020 at 04:06 • 0 commentsAfter 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.