Merge pull request #3 from Blendoit/evaluator

Evaluator

Commit
e66d67542100b0b4e7d405ce0b9dea4333559cde
Author
Blendoit <51464356+Blendoit@users.noreply.github.com>
Author date
Committer
GitHub <noreply@github.com>
Committer date
Changed files
__init__.py
index 5e916af5..dc031d04 100644..100644
@@ -13,5 +13,5 @@
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program. If not, see <https://www.gnu.org/licenses/>.
15 15 __author__ = "Marius Peter"
16 Removed: __version__ = "2.3"
17 Removed: __revision__ = "2.3.1"
16 Added: # __version__ = "2.3"
17 Added: # __revision__ = "2.3.1"
creator.py
index eac39184..634277eb 100644..100644
@@ -1,422 +1,447 @@
1 Removed: # This file is part of Marius Peter's airfoil analysis package (this program).
2 Removed: #
3 Removed: # This program is free software: you can redistribute it and/or modify
4 Removed: # it under the terms of the GNU General Public License as published by
5 Removed: # the Free Software Foundation, either version 3 of the License, or
6 Removed: # (at your option) any later version.
7 Removed: #
8 Removed: # This program is distributed in the hope that it will be useful,
9 Removed: # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 Removed: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 Removed: # GNU General Public License for more details.
12 Removed: #
13 Removed: # You should have received a copy of the GNU General Public License
14 Removed: # along with this program. If not, see <https://www.gnu.org/licenses/>.
15 Removed:
16 Removed: import sys
17 Removed: import os.path
18 Removed: import numpy as np
19 Removed: from math import sin, cos, tan, atan, sqrt, ceil
20 Removed: import bisect as bi
21 Removed: import matplotlib.pyplot as plt
22 Removed: import matplotlib as mpl
23 Removed: from mpl_toolkits.mplot3d import Axes3D
24 Removed:
25 Removed: # This variable is required for main.py constant wing dimensions
26 Removed: # to be passed to inheriting classes (Airfoil, Spar, Stringer, Rib).
27 Removed: # This way, we don't have to redeclare our coordinates as parameters for
28 Removed: # our spars, stringers and ribs. This makes for more elegant code.
29 Removed: global parent
30 Removed:
31 Removed:
32 Removed: class Coordinates:
33 Removed: """
34 Removed: All airfoil components need the following:
35 Removed:
36 Removed: Parameters:
37 Removed: * Component material
38 Removed: * Coordinates relative to the chord & semi-span.
39 Removed:
40 Removed: Methods:
41 Removed: * Print component coordinates
42 Removed: * Save component coordinates to file specified in main.py
43 Removed:
44 Removed: So, all component classes inherit from class Coordinates.
45 Removed: """
46 Removed:
47 Removed: def __init__(self, chord, semi_span):
48 Removed: # Global dimensions
49 Removed: self.chord = chord
50 Removed: if chord < 10:
51 Removed: self.chord = 10
52 Removed: self.semi_span = semi_span
53 Removed: # Component material
54 Removed: self.material = str()
55 Removed: # Upper coordinates
56 Removed: self.x_u = []
57 Removed: self.y_u = []
58 Removed: # Lower coordinates
59 Removed: self.x_l = []
60 Removed: self.y_l = []
61 Removed: # Coordinates x_u, y_u, x_l, y_l packed in single list
62 Removed: self.coord = []
63 Removed: # The airfoil components know the Coordinates instance's coords
64 Removed: global parent
65 Removed: parent = self
66 Removed:
67 Removed: def __str__(self):
68 Removed: return type(self).__name__
69 Removed:
70 Removed: def print_coord(self, round):
71 Removed: """
72 Removed: Print all the component's coordinates to the terminal.
73 Removed:
74 Removed: This function's output is piped to the 'save_coord' function below.
75 Removed: """
76 Removed: print('============================')
77 Removed: print('Component:', str(self))
78 Removed: print('Chord length:', self.chord)
79 Removed: print('Semi-span:', self.semi_span)
80 Removed: print('============================')
81 Removed: print('x_u the upper x-coordinates:\n', np.around(self.x_u, round))
82 Removed: print('y_u the upper y-coordinates:\n', np.around(self.y_u, round))
83 Removed: print('x_l the lower x-coordinates:\n', np.around(self.x_l, round))
84 Removed: print('y_l the lower y-coordinates:\n', np.around(self.y_l, round))
85 Removed: # print('\n')
86 Removed: return None
87 Removed:
88 Removed: def save_coord(self, save_dir_path, number):
89 Removed: """
90 Removed: Save all the object's coordinates (must be full path).
91 Removed: """
92 Removed:
93 Removed: file_name = '{}_{}.txt'.format(self, number)
94 Removed: full_path = os.path.join(save_dir_path, file_name)
95 Removed: # sys.stdout = open(full_path, 'w')
96 Removed: # self.print_coord(2)
97 Removed: with open(full_path, 'w') as sys.stdout:
98 Removed: self.print_coord(2)
99 Removed: # Following line required to reset value of sys.stdout
100 Removed: sys.stdout = sys.__stdout__
101 Removed: # It is cleaner to use this context guard to ensure file is closed
102 Removed:
103 Removed: return None
104 Removed:
105 Removed: def pack_coord(self):
106 Removed: self.coord.append(self.x_u)
107 Removed: self.coord.append(self.y_u)
108 Removed: self.coord.append(self.x_l)
109 Removed: self.coord.append(self.y_l)
110 Removed:
111 Removed:
112 Removed: class Airfoil(Coordinates):
113 Removed: """This class enables the creation of a single NACA airfoil."""
114 Removed:
115 Removed: def __init__(self):
116 Removed: global parent
117 Removed: # Run 'Coordinates' super class init method with same chord & 1/2 span.
118 Removed: super().__init__(parent.chord, parent.semi_span)
119 Removed: # NACA number
120 Removed: self.naca_num = int()
121 Removed: # Mean camber line
122 Removed: self.x_c = [] # Contains only integers from 0 to self.chord
123 Removed: self.y_c = [] # Contains floats
124 Removed: # Thickness
125 Removed: self.y_t = []
126 Removed: # dy_c / d_x
127 Removed: self.dy_c = []
128 Removed: # Theta
129 Removed: self.theta = []
130 Removed:
131 Removed: def add_naca(self, naca_num):
132 Removed: """
133 Removed: This function generates geometry for our chosen NACA airfoil shape.
134 Removed: The nested functions perform the required steps to generate geometry,
135 Removed: and can be called to solve the geometry y-coordinate for any 'x' input.
136 Removed: Equation coefficients were retrieved from Wikipedia.org.
137 Removed:
138 Removed: Parameters:
139 Removed: naca_num: 4-digit NACA wing
140 Removed:
141 Removed: Return:
142 Removed: None
143 Removed: """
144 Removed:
145 Removed: # Variables extracted from 'naca_num' argument passed to the function
146 Removed: self.naca_num = naca_num
147 Removed: m = int(str(naca_num)[0]) / 100
148 Removed: p = int(str(naca_num)[1]) / 10
149 Removed: t = int(str(naca_num)[2:]) / 100
150 Removed: # x-coordinate of maximum camber
151 Removed: p_c = p * self.chord
152 Removed:
153 Removed: def get_camber(x):
154 Removed: """
155 Removed: Returns 1 camber y-coordinate from 1 'x' along the airfoil chord.
156 Removed: """
157 Removed: x_c = x
158 Removed: y_c = float()
159 Removed: if 0 <= x < p_c:
160 Removed: y_c = (m / (p**2)) * (2 * p * (x / self.chord) -
161 Removed: (x / self.chord)**2)
162 Removed: elif p_c <= x <= self.chord:
163 Removed: y_c = (m /
164 Removed: ((1 - p)**2)) * ((1 - 2 * p) + 2 * p *
165 Removed: (x / self.chord) - (x / self.chord)**2)
166 Removed: else:
167 Removed: print('x-coordinate for camber is out of bounds. '
168 Removed: 'Check that 0 < x <= chord.')
169 Removed: return (x_c, y_c * self.chord)
170 Removed:
171 Removed: def get_thickness(x):
172 Removed: """
173 Removed: Returns thickness from 1 'x' along the airfoil chord.
174 Removed: """
175 Removed: y_t = float()
176 Removed: if 0 <= x <= self.chord:
177 Removed: y_t = 5 * t * self.chord * (0.2969 * sqrt(x / self.chord) -
178 Removed: 0.1260 *
179 Removed: (x / self.chord) - 0.3516 *
180 Removed: (x / self.chord)**2 + 0.2843 *
181 Removed: (x / self.chord)**3 - 0.1015 *
182 Removed: (x / self.chord)**4)
183 Removed: else:
184 Removed: print('x-coordinate for thickness is out of bounds. '
185 Removed: 'Check that 0 < x <= chord.')
186 Removed: return y_t
187 Removed:
188 Removed: def get_dy_c(x):
189 Removed: """
190 Removed: Returns dy_c/dx from 1 'x' along the airfoil chord.
191 Removed: """
192 Removed: dy_c = float()
193 Removed: if 0 <= x < p_c:
194 Removed: dy_c = ((2 * m) / p**2) * (p - x / self.chord)
195 Removed: elif p_c <= x <= self.chord:
196 Removed: dy_c = (2 * m) / ((1 - p)**2) * (p - x / self.chord)
197 Removed: return dy_c
198 Removed:
199 Removed: def get_theta(dy_c):
200 Removed: theta = atan(dy_c)
201 Removed: return theta
202 Removed:
203 Removed: def get_upper_coordinates(x):
204 Removed: x_u = float()
205 Removed: y_u = float()
206 Removed: if 0 <= x < self.chord:
207 Removed: x_u = x - self.y_t[x] * sin(self.theta[x])
208 Removed: y_u = self.y_c[x] + self.y_t[x] * cos(self.theta[x])
209 Removed: elif x == self.chord:
210 Removed: x_u = x - self.y_t[x] * sin(self.theta[x])
211 Removed: y_u = 0 # Make upper curve finish at y = 0
212 Removed: return (x_u, y_u)
213 Removed:
214 Removed: def get_lower_coordinates(x):
215 Removed: x_l = float()
216 Removed: y_l = float()
217 Removed: if 0 <= x < self.chord:
218 Removed: x_l = (x + self.y_t[x] * sin(self.theta[x]))
219 Removed: y_l = (self.y_c[x] - self.y_t[x] * cos(self.theta[x]))
220 Removed: elif x == self.chord:
221 Removed: x_l = (x + self.y_t[x] * sin(self.theta[x]))
222 Removed: y_l = 0 # Make lower curve finish at y = 0
223 Removed: return (x_l, y_l)
224 Removed:
225 Removed: # Generate all our wing geometries from previous sub-functions
226 Removed: for x in range(0, self.chord + 1):
227 Removed: self.x_c.append(get_camber(x)[0])
228 Removed: self.y_c.append(get_camber(x)[1])
229 Removed: self.y_t.append(get_thickness(x))
230 Removed: self.dy_c.append(get_dy_c(x))
231 Removed: self.theta.append(get_theta(self.dy_c[x]))
232 Removed: self.x_u.append(get_upper_coordinates(x)[0])
233 Removed: self.y_u.append(get_upper_coordinates(x)[1])
234 Removed: self.x_l.append(get_lower_coordinates(x)[0])
235 Removed: self.y_l.append(get_lower_coordinates(x)[1])
236 Removed:
237 Removed: super().pack_coord()
238 Removed: return None
239 Removed:
240 Removed:
241 Removed: class Spar(Coordinates):
242 Removed: """Contains a single spar's location."""
243 Removed: global parent
244 Removed:
245 Removed: def __init__(self):
246 Removed: super().__init__(parent.chord, parent.semi_span)
247 Removed:
248 Removed: def add(self, airfoil_coord, spar_x):
249 Removed: """
250 Removed: Add a single spar at the % chord location given to function.
251 Removed:
252 Removed: Parameters:
253 Removed: coordinates: provided by Airfoil.coordinates[x_u, y_u, x_l, y_l].
254 Removed: material: spar's material. Assumes homogeneous material.
255 Removed: spar_x: spar's location as a % of total chord length.
256 Removed:
257 Removed: Return:
258 Removed: None
259 Removed: """
260 Removed: # Airfoil surface coordinates
261 Removed: # unpacked from 'coordinates' (list of lists in 'Coordinates').
262 Removed: x_u = airfoil_coord[0]
263 Removed: y_u = airfoil_coord[1]
264 Removed: x_l = airfoil_coord[2]
265 Removed: y_l = airfoil_coord[3]
266 Removed: # Scaled spar location with regards to chord
267 Removed: loc = spar_x * self.chord
268 Removed: # bisect_left: returns index of first value in x_u > loc.
269 Removed: # This ensures that the spar coordinates intersect with airfoil surface.
270 Removed: spar_x_u = bi.bisect_left(x_u, loc) # index of spar's x_u
271 Removed: spar_x_l = bi.bisect_left(x_l, loc) # index of spar's x_l
272 Removed: # These x and y coordinates are assigned to the spar, NOT airfoil.
273 Removed: self.x_u.append(x_u[spar_x_u])
274 Removed: self.y_u.append(y_u[spar_x_u])
275 Removed: self.x_l.append(x_l[spar_x_l])
276 Removed: self.y_l.append(y_l[spar_x_l])
277 Removed:
278 Removed: super().pack_coord()
279 Removed: return None
280 Removed:
281 Removed:
282 Removed: class Stringer(Coordinates):
283 Removed: """Contains the coordinates of all stringers."""
284 Removed: global parent
285 Removed:
286 Removed: def __init__(self):
287 Removed: super().__init__(parent.chord, parent.semi_span)
288 Removed:
289 Removed: def add(self, airfoil_coord, spar_coord, stringer_u_1, stringer_u_2,
290 Removed: stringer_l_1, stringer_l_2):
291 Removed: """
292 Removed: Add equally distributed stringers to four airfoil locations
293 Removed: (upper nose, lower nose, upper surface, lower surface).
294 Removed:
295 Removed: Parameters:
296 Removed: stringer_u_1: upper nose number of stringers
297 Removed: stringer_u_2: upper surface number of stringers
298 Removed: stringer_l_1: lower nose number of stringers
299 Removed: stringer_l_2: lower surface number of stringers
300 Removed:
301 Removed: Returns:
302 Removed: None
303 Removed: """
304 Removed:
305 Removed: # Airfoil surface coordinates
306 Removed: # unpacked from 'coordinates' (list of lists in 'Coordinates').
307 Removed: airfoil_x_u = airfoil_coord[0]
308 Removed: airfoil_y_u = airfoil_coord[1]
309 Removed: airfoil_x_l = airfoil_coord[2]
310 Removed: airfoil_y_l = airfoil_coord[3]
311 Removed: # Spar coordinates
312 Removed: # unpacked from 'coordinates' (list of lists in 'Coordinates').
313 Removed: try:
314 Removed: spar_x_u = spar_coord[0]
315 Removed: spar_y_u = spar_coord[1]
316 Removed: spar_x_l = spar_coord[2]
317 Removed: spar_y_l = spar_coord[3]
318 Removed: except:
319 Removed: print('Unable to initialize stringers. Were spars created?')
320 Removed: # Find distance between leading edge and first upper stringer
321 Removed: interval = spar_x_u[0] / (stringer_u_1 + 1)
322 Removed: # initialise first self.stringer_x_u at first interval
323 Removed: x = interval
324 Removed: # Add upper stringers from leading edge until first spar.
325 Removed: for _ in range(0, stringer_u_1):
326 Removed: # Index of the first value of airfoil_x_u > x
327 Removed: index = bi.bisect_left(airfoil_x_u, x)
328 Removed: self.x_u.append(airfoil_x_u[index])
329 Removed: self.y_u.append(airfoil_y_u[index])
330 Removed: x += interval
331 Removed: # Add upper stringers from first spar until last spar
332 Removed: interval = (spar_x_u[-1] - spar_x_u[0]) / (stringer_u_2 + 1)
333 Removed: x = interval + spar_x_u[0]
334 Removed: for _ in range(0, stringer_u_2):
335 Removed: index = bi.bisect_left(airfoil_x_u, x)
336 Removed: self.x_u.append(airfoil_x_u[index])
337 Removed: self.y_u.append(airfoil_y_u[index])
338 Removed: x += interval
339 Removed:
340 Removed: # Find distance between leading edge and first lower stringer
341 Removed: interval = spar_x_l[0] / (stringer_l_1 + 1)
342 Removed: x = interval
343 Removed: # Add lower stringers from leading edge until first spar.
344 Removed: for _ in range(0, stringer_l_1):
345 Removed: index = bi.bisect_left(airfoil_x_l, x)
346 Removed: self.x_l.append(airfoil_x_l[index])
347 Removed: self.y_l.append(airfoil_y_l[index])
348 Removed: x += interval
349 Removed: # Add lower stringers from first spar until last spar
350 Removed: interval = (spar_x_l[-1] - spar_x_l[0]) / (stringer_l_2 + 1)
351 Removed: x = interval + spar_x_l[0]
352 Removed: for _ in range(0, stringer_l_2):
353 Removed: index = bi.bisect_left(airfoil_x_l, x)
354 Removed: self.x_l.append(airfoil_x_l[index])
355 Removed: self.y_l.append(airfoil_y_l[index])
356 Removed: x += interval
357 Removed:
358 Removed: super().pack_coord()
359 Removed: return None
360 Removed:
361 Removed:
362 Removed: def plot(airfoil, spar, stringer):
363 Removed: """This function plots the elements passed as arguments."""
364 Removed:
365 Removed: print('Plotting airfoil.')
366 Removed: # Plot chord
367 Removed: x_chord = [0, airfoil.chord]
368 Removed: y_chord = [0, 0]
369 Removed: plt.plot(x_chord, y_chord, linewidth='1')
370 Removed: # Plot mean camber line
371 Removed: plt.plot(airfoil.x_c,
372 Removed: airfoil.y_c,
373 Removed: '-.',
374 Removed: color='r',
375 Removed: linewidth='2',
376 Removed: label='mean camber line')
377 Removed: # Plot upper surface
378 Removed: plt.plot(airfoil.x_u, airfoil.y_u, '', color='b', linewidth='1')
379 Removed: # Plot lower surface
380 Removed: plt.plot(airfoil.x_l, airfoil.y_l, '', color='b', linewidth='1')
381 Removed:
382 Removed: # Plot spars
383 Removed: try:
384 Removed: for _ in range(0, len(spar.x_u)):
385 Removed: x = (spar.x_u[_], spar.x_l[_])
386 Removed: y = (spar.y_u[_], spar.y_l[_])
387 Removed: plt.plot(x, y, '.-', color='b')
388 Removed: plt.legend()
389 Removed: except:
390 Removed: print('Did not plot spars. Were they added?')
391 Removed:
392 Removed: # Plot stringers
393 Removed: try:
394 Removed: # Upper stringers
395 Removed: for _ in range(0, len(stringer.x_u)):
396 Removed: x = stringer.x_u[_]
397 Removed: y = stringer.y_u[_]
398 Removed: plt.plot(x, y, '.', color='y')
399 Removed: # Lower stringers
400 Removed: for _ in range(0, len(stringer.x_l)):
401 Removed: x = stringer.x_l[_]
402 Removed: y = stringer.y_l[_]
403 Removed: plt.plot(x, y, '.', color='y')
404 Removed: except:
405 Removed: print('Unable to plot stringers. Were they created?')
406 Removed:
407 Removed: # Graph formatting
408 Removed: plt.gcf().set_size_inches(9, 2.2)
409 Removed: plt.xlabel('X axis')
410 Removed: plt.ylabel('Y axis')
411 Removed: # plt.gcf().set_size_inches(self.chord, max(self.y_u) - min(self.y_l))
412 Removed: plt.grid(axis='both', linestyle=':', linewidth=1)
413 Removed: plt.show()
414 Removed: return None
415 Removed:
416 Removed:
417 Removed: def main():
418 Removed: return None
419 Removed:
420 Removed:
421 Removed: if __name__ == '__main__':
422 Removed: main()
1 Added: # This file is part of Marius Peter's airfoil analysis package (this program).
2 Added: #
3 Added: # This program is free software: you can redistribute it and/or modify
4 Added: # it under the terms of the GNU General Public License as published by
5 Added: # the Free Software Foundation, either version 3 of the License, or
6 Added: # (at your option) any later version.
7 Added: #
8 Added: # This program is distributed in the hope that it will be useful,
9 Added: # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 Added: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 Added: # GNU General Public License for more details.
12 Added: #
13 Added: # You should have received a copy of the GNU General Public License
14 Added: # along with this program. If not, see <https://www.gnu.org/licenses/>.
15 Added:
16 Added: import sys
17 Added: import os.path
18 Added: import numpy as np
19 Added: from math import sin, cos, tan, atan, sqrt, ceil
20 Added: import bisect as bi
21 Added: import matplotlib.pyplot as plt
22 Added:
23 Added: # This variable is required for main.py constant wing dimensions
24 Added: # to be passed to inheriting classes (Airfoil, Spar, Stringer, Rib).
25 Added: # This way, we don't have to redeclare our coordinates as parameters for
26 Added: # our spars, stringers and ribs. This makes for more elegant code.
27 Added: global parent
28 Added:
29 Added:
30 Added: class Coordinates:
31 Added: """
32 Added: All airfoil components need the following:
33 Added:
34 Added: Parameters:
35 Added: * Component material
36 Added: * Coordinates relative to the chord & semi-span.
37 Added:
38 Added: Methods:
39 Added: * Print component coordinates
40 Added: * Save component coordinates to file specified in main.py
41 Added:
42 Added: So, all component classes inherit from class Coordinates.
43 Added: """
44 Added:
45 Added: def __init__(self, chord, semi_span):
46 Added: # Global dimensions
47 Added: self.chord = chord
48 Added: if chord < 10:
49 Added: self.chord = 10
50 Added: self.semi_span = semi_span
51 Added: # mass and area
52 Added: self.mass = float()
53 Added: self.area = float()
54 Added: # Component material
55 Added: self.material = str()
56 Added: # Upper coordinates
57 Added: self.x_u = []
58 Added: self.z_u = []
59 Added: # Lower coordinates
60 Added: self.x_l = []
61 Added: self.z_l = []
62 Added: # Coordinates x_u, z_u, x_l, z_l packed in single list
63 Added: self.coord = []
64 Added:
65 Added: # The airfoil components know the Coordinates instance's coords
66 Added: global parent
67 Added: parent = self
68 Added:
69 Added: def __str__(self):
70 Added: return type(self).__name__
71 Added:
72 Added: def print_info(self, round):
73 Added: """
74 Added: Print all the component's coordinates to the terminal.
75 Added:
76 Added: This function's output is piped to the 'save_coord' function below.
77 Added: """
78 Added: print('============================')
79 Added: print('Component:', str(self))
80 Added: print('Chord length:', self.chord)
81 Added: print('Semi-span:', self.semi_span)
82 Added: print('Mass:', self.mass)
83 Added: print('============================')
84 Added: print('x_u the upper x-coordinates:\n', np.around(self.x_u, round))
85 Added: print('z_u the upper y-coordinates:\n', np.around(self.z_u, round))
86 Added: print('x_l the lower x-coordinates:\n', np.around(self.x_l, round))
87 Added: print('z_l the lower y-coordinates:\n', np.around(self.z_l, round))
88 Added: return None
89 Added:
90 Added: def save_info(self, save_dir_path, number):
91 Added: """
92 Added: Save all the object's coordinates (must be full path).
93 Added: """
94 Added:
95 Added: file_name = '{}_{}.txt'.format(self, number)
96 Added: full_path = os.path.join(save_dir_path, file_name)
97 Added: try:
98 Added: with open(full_path, 'w') as sys.stdout:
99 Added: self.print_info(2)
100 Added: # This line required to reset behavior of sys.stdout
101 Added: sys.stdout = sys.__stdout__
102 Added: print('Successfully wrote to file {}'.format(full_path))
103 Added: except:
104 Added: print('Unable to write {} to specified directory.\n'
105 Added: .format(file_name),
106 Added: 'Was the full path passed to the function?')
107 Added: # It is cleaner to use this context guard to ensure file is closed
108 Added: return None
109 Added:
110 Added: def pack_info(self):
111 Added: self.coord.append(self.x_u)
112 Added: self.coord.append(self.z_u)
113 Added: self.coord.append(self.x_l)
114 Added: self.coord.append(self.z_l)
115 Added: return None
116 Added:
117 Added:
118 Added: class Airfoil(Coordinates):
119 Added: """This class enables the creation of a single NACA airfoil."""
120 Added:
121 Added: def __init__(self):
122 Added: global parent
123 Added: # Run 'Coordinates' super class init method with same chord & 1/2 span.
124 Added: super().__init__(parent.chord, parent.semi_span)
125 Added: # NACA number
126 Added: self.naca_num = int()
127 Added: # Mean camber line
128 Added: self.x_c = [] # Contains only integers from 0 to self.chord
129 Added: self.y_c = [] # Contains floats
130 Added: # Thickness
131 Added: self.y_t = []
132 Added: # dy_c / d_x
133 Added: self.dy_c = []
134 Added: # Theta
135 Added: self.theta = []
136 Added:
137 Added: def add_naca(self, naca_num):
138 Added: """
139 Added: This function generates geometry for our chosen NACA airfoil shape.
140 Added: The nested functions perform the required steps to generate geometry,
141 Added: and can be called to solve the geometry y-coordinate for any 'x' input.
142 Added: Equation coefficients were retrieved from Wikipedia.org.
143 Added:
144 Added: Parameters:
145 Added: naca_num: 4-digit NACA wing
146 Added:
147 Added: Return:
148 Added: None
149 Added: """
150 Added:
151 Added: # Variables extracted from 'naca_num' argument passed to the function
152 Added: self.naca_num = naca_num
153 Added: m = int(str(naca_num)[0]) / 100
154 Added: p = int(str(naca_num)[1]) / 10
155 Added: t = int(str(naca_num)[2:]) / 100
156 Added: # x-coordinate of maximum camber
157 Added: p_c = p * self.chord
158 Added:
159 Added: def get_camber(x):
160 Added: """
161 Added: Returns 1 camber y-coordinate from 1 'x' along the airfoil chord.
162 Added: """
163 Added: x_c = x
164 Added: y_c = float()
165 Added: if 0 <= x < p_c:
166 Added: y_c = (m / (p**2)) * (2 * p * (x / self.chord) -
167 Added: (x / self.chord)**2)
168 Added: elif p_c <= x <= self.chord:
169 Added: y_c = (m /
170 Added: ((1 - p)**2)) * ((1 - 2 * p) + 2 * p *
171 Added: (x / self.chord) - (x / self.chord)**2)
172 Added: else:
173 Added: print('x-coordinate for camber is out of bounds. '
174 Added: 'Check that 0 < x <= chord.')
175 Added: return (x_c, y_c * self.chord)
176 Added:
177 Added: def get_thickness(x):
178 Added: """
179 Added: Returns thickness from 1 'x' along the airfoil chord.
180 Added: """
181 Added: y_t = float()
182 Added: if 0 <= x <= self.chord:
183 Added: y_t = 5 * t * self.chord * (0.2969 * sqrt(x / self.chord) -
184 Added: 0.1260 *
185 Added: (x / self.chord) - 0.3516 *
186 Added: (x / self.chord)**2 + 0.2843 *
187 Added: (x / self.chord)**3 - 0.1015 *
188 Added: (x / self.chord)**4)
189 Added: else:
190 Added: print('x-coordinate for thickness is out of bounds. '
191 Added: 'Check that 0 < x <= chord.')
192 Added: return y_t
193 Added:
194 Added: def get_dy_c(x):
195 Added: """
196 Added: Returns dy_c/dx from 1 'x' along the airfoil chord.
197 Added: """
198 Added: dy_c = float()
199 Added: if 0 <= x < p_c:
200 Added: dy_c = ((2 * m) / p**2) * (p - x / self.chord)
201 Added: elif p_c <= x <= self.chord:
202 Added: dy_c = (2 * m) / ((1 - p)**2) * (p - x / self.chord)
203 Added: return dy_c
204 Added:
205 Added: def get_theta(dy_c):
206 Added: theta = atan(dy_c)
207 Added: return theta
208 Added:
209 Added: def get_upper_coordinates(x):
210 Added: x_u = float()
211 Added: z_u = float()
212 Added: if 0 <= x < self.chord:
213 Added: x_u = x - self.y_t[x] * sin(self.theta[x])
214 Added: z_u = self.y_c[x] + self.y_t[x] * cos(self.theta[x])
215 Added: elif x == self.chord:
216 Added: x_u = x - self.y_t[x] * sin(self.theta[x])
217 Added: z_u = 0 # Make upper curve finish at y = 0
218 Added: return (x_u, z_u)
219 Added:
220 Added: def get_lower_coordinates(x):
221 Added: x_l = float()
222 Added: z_l = float()
223 Added: if 0 <= x < self.chord:
224 Added: x_l = (x + self.y_t[x] * sin(self.theta[x]))
225 Added: z_l = (self.y_c[x] - self.y_t[x] * cos(self.theta[x]))
226 Added: elif x == self.chord:
227 Added: x_l = (x + self.y_t[x] * sin(self.theta[x]))
228 Added: z_l = 0 # Make lower curve finish at y = 0
229 Added: return (x_l, z_l)
230 Added:
231 Added: # Generate all our wing geometries from previous sub-functions
232 Added: for x in range(0, self.chord + 1):
233 Added: self.x_c.append(get_camber(x)[0])
234 Added: self.y_c.append(get_camber(x)[1])
235 Added: self.y_t.append(get_thickness(x))
236 Added: self.dy_c.append(get_dy_c(x))
237 Added: self.theta.append(get_theta(self.dy_c[x]))
238 Added: self.x_u.append(get_upper_coordinates(x)[0])
239 Added: self.z_u.append(get_upper_coordinates(x)[1])
240 Added: self.x_l.append(get_lower_coordinates(x)[0])
241 Added: self.z_l.append(get_lower_coordinates(x)[1])
242 Added:
243 Added: super().pack_info()
244 Added: return None
245 Added:
246 Added: def add_mass(self, mass):
247 Added: self.mass = mass
248 Added:
249 Added:
250 Added: class Spar(Coordinates):
251 Added: """Contains a single spar's location."""
252 Added: global parent
253 Added:
254 Added: def __init__(self):
255 Added: super().__init__(parent.chord, parent.semi_span)
256 Added:
257 Added: def add_coord(self, airfoil_coord, spar_x):
258 Added: """
259 Added: Add a single spar at the % chord location given to function.
260 Added:
261 Added: Parameters:
262 Added: coordinates: provided by Airfoil.coordinates[x_u, z_u, x_l, z_l].
263 Added: material: spar's material. Assumes homogeneous material.
264 Added: spar_x: spar's location as a % of total chord length.
265 Added:
266 Added: Return:
267 Added: None
268 Added: """
269 Added: # Airfoil surface coordinates
270 Added: # unpacked from 'coordinates' (list of lists in 'Coordinates').
271 Added: x_u = airfoil_coord[0]
272 Added: z_u = airfoil_coord[1]
273 Added: x_l = airfoil_coord[2]
274 Added: z_l = airfoil_coord[3]
275 Added: # Scaled spar location with regards to chord
276 Added: loc = spar_x * self.chord
277 Added: # bisect_left: returns index of first value in x_u > loc.
278 Added: # This ensures that the spar coordinates intersect with airfoil surface.
279 Added: spar_x_u = bi.bisect_left(x_u, loc) # index of spar's x_u
280 Added: spar_x_l = bi.bisect_left(x_l, loc) # index of spar's x_l
281 Added: # These x and y coordinates are assigned to the spar, NOT airfoil.
282 Added: self.x_u.append(x_u[spar_x_u])
283 Added: self.z_u.append(z_u[spar_x_u])
284 Added: self.x_l.append(x_l[spar_x_l])
285 Added: self.z_l.append(z_l[spar_x_l])
286 Added:
287 Added: super().pack_info()
288 Added: return None
289 Added:
290 Added: def add_mass(self, mass):
291 Added: self.mass = len(self.x_u) * mass
292 Added:
293 Added:
294 Added: class Stringer(Coordinates):
295 Added: """Contains the coordinates of all stringers."""
296 Added: global parent
297 Added:
298 Added: def __init__(self):
299 Added: super().__init__(parent.chord, parent.semi_span)
300 Added: self.area = float()
301 Added:
302 Added: def add_coord(self, airfoil_coord, spar_coord,
303 Added: stringer_u_1, stringer_u_2, stringer_l_1, stringer_l_2):
304 Added: """
305 Added: Add equally distributed stringers to four airfoil locations
306 Added: (upper nose, lower nose, upper surface, lower surface).
307 Added:
308 Added: Parameters:
309 Added: stringer_u_1: upper nose number of stringers
310 Added: stringer_u_2: upper surface number of stringers
311 Added: stringer_l_1: lower nose number of stringers
312 Added: stringer_l_2: lower surface number of stringers
313 Added:
314 Added: Returns:
315 Added: None
316 Added: """
317 Added:
318 Added: # Airfoil surface coordinates
319 Added: # unpacked from 'coordinates' (list of lists in 'Coordinates').
320 Added: airfoil_x_u = airfoil_coord[0]
321 Added: airfoil_z_u = airfoil_coord[1]
322 Added: airfoil_x_l = airfoil_coord[2]
323 Added: airfoil_z_l = airfoil_coord[3]
324 Added: # Spar coordinates
325 Added: # unpacked from 'coordinates' (list of lists in 'Coordinates').
326 Added: try:
327 Added: spar_x_u = spar_coord[0]
328 Added: spar_z_u = spar_coord[1]
329 Added: spar_x_l = spar_coord[2]
330 Added: spar_z_l = spar_coord[3]
331 Added: except:
332 Added: print('Unable to initialize stringers. Were spars created?')
333 Added:
334 Added: # Find distance between leading edge and first upper stringer
335 Added: interval = spar_x_u[0] / (stringer_u_1 + 1)
336 Added: # initialise first self.stringer_x_u at first interval
337 Added: x = interval
338 Added: # Add upper stringers from leading edge until first spar.
339 Added: for _ in range(0, stringer_u_1):
340 Added: # Index of the first value of airfoil_x_u > x
341 Added: index = bi.bisect_left(airfoil_x_u, x)
342 Added: self.x_u.append(airfoil_x_u[index])
343 Added: self.z_u.append(airfoil_z_u[index])
344 Added: x += interval
345 Added: # Add upper stringers from first spar until last spar
346 Added: interval = (spar_x_u[-1] - spar_x_u[0]) / (stringer_u_2 + 1)
347 Added: x = interval + spar_x_u[0]
348 Added: for _ in range(0, stringer_u_2):
349 Added: index = bi.bisect_left(airfoil_x_u, x)
350 Added: self.x_u.append(airfoil_x_u[index])
351 Added: self.z_u.append(airfoil_z_u[index])
352 Added: x += interval
353 Added:
354 Added: # Find distance between leading edge and first lower stringer
355 Added: interval = spar_x_l[0] / (stringer_l_1 + 1)
356 Added: x = interval
357 Added: # Add lower stringers from leading edge until first spar.
358 Added: for _ in range(0, stringer_l_1):
359 Added: index = bi.bisect_left(airfoil_x_l, x)
360 Added: self.x_l.append(airfoil_x_l[index])
361 Added: self.z_l.append(airfoil_z_l[index])
362 Added: x += interval
363 Added: # Add lower stringers from first spar until last spar
364 Added: interval = (spar_x_l[-1] - spar_x_l[0]) / (stringer_l_2 + 1)
365 Added: x = interval + spar_x_l[0]
366 Added: for _ in range(0, stringer_l_2):
367 Added: index = bi.bisect_left(airfoil_x_l, x)
368 Added: self.x_l.append(airfoil_x_l[index])
369 Added: self.z_l.append(airfoil_z_l[index])
370 Added: x += interval
371 Added: super().pack_info()
372 Added: return None
373 Added:
374 Added: def add_area(self, area):
375 Added: self.area = area
376 Added: return None
377 Added:
378 Added: def add_mass(self, mass):
379 Added: self.mass = len(self.x_u) * mass + len(self.x_l) * mass
380 Added: return None
381 Added:
382 Added: def print_info(self, round):
383 Added: super().print_info(round)
384 Added: print('Stringer Area:\n', np.around(self.area, round))
385 Added: return None
386 Added:
387 Added:
388 Added: def plot(airfoil, spar, stringer):
389 Added: """This function plots the elements passed as arguments."""
390 Added:
391 Added: print('Plotting airfoil.')
392 Added: # Plot chord
393 Added: x_chord = [0, airfoil.chord]
394 Added: y_chord = [0, 0]
395 Added: plt.plot(x_chord, y_chord, linewidth='1')
396 Added: # Plot mean camber line
397 Added: plt.plot(airfoil.x_c,
398 Added: airfoil.y_c,
399 Added: '-.',
400 Added: color='r',
401 Added: linewidth='2')
402 Added: # label='mean camber line')
403 Added: # Plot upper surface
404 Added: plt.plot(airfoil.x_u, airfoil.z_u, '', color='b', linewidth='1')
405 Added: # Plot lower surface
406 Added: plt.plot(airfoil.x_l, airfoil.z_l, '', color='b', linewidth='1')
407 Added:
408 Added: # Plot spars
409 Added: try:
410 Added: for _ in range(0, len(spar.x_u)):
411 Added: x = (spar.x_u[_], spar.x_l[_])
412 Added: y = (spar.z_u[_], spar.z_l[_])
413 Added: plt.plot(x, y, '.-', color='b')
414 Added: # plt.legend()
415 Added: except:
416 Added: print('Did not plot spars. Were they added?')
417 Added:
418 Added: # Plot stringers
419 Added: try:
420 Added: # Upper stringers
421 Added: for _ in range(0, len(stringer.x_u)):
422 Added: x = stringer.x_u[_]
423 Added: y = stringer.z_u[_]
424 Added: plt.plot(x, y, '.', color='y')
425 Added: # Lower stringers
426 Added: for _ in range(0, len(stringer.x_l)):
427 Added: x = stringer.x_l[_]
428 Added: y = stringer.z_l[_]
429 Added: plt.plot(x, y, '.', color='y')
430 Added: except:
431 Added: print('Unable to plot stringers. Were they created?')
432 Added:
433 Added: # Graph formatting
434 Added: plt.gca().set_aspect('equal', adjustable='box')
435 Added: plt.xlabel('X axis')
436 Added: plt.ylabel('Z axis')
437 Added: plt.grid(axis='both', linestyle=':', linewidth=1)
438 Added: plt.show()
439 Added: return None
440 Added:
441 Added:
442 Added: def main():
443 Added: return None
444 Added:
445 Added:
446 Added: if __name__ == '__main__':
447 Added: main()
evaluator.py
index 6a87b7ef..69a589aa 100644..100644
@@ -13,4 +13,22 @@
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program. If not, see <https://www.gnu.org/licenses/>.
15 15
16 Removed: import creator
16 Added: # F_z =
17 Added:
18 Added:
19 Added: def get_centroid(airfoil):
20 Added: area = airfoil.stringer.area
21 Added: numerator = float()
22 Added: for _ in airfoil.stringer.x_u:
23 Added: numerator += _ * area
24 Added: for _ in airfoil.stringer.x_l:
25 Added: numerator += _ * area
26 Added: denominator
27 Added: # z_c =
28 Added:
29 Added:
30 Added: def get_total_mass(self, *component):
31 Added: total_mass = float()
32 Added: for _ in component:
33 Added: total_mass += _.mass
34 Added: return total_mass
main.py
index 5d2e90ab..ce69f6b9 100644..100644
@@ -15,7 +15,7 @@
15 15
16 16 import creator # Create geometry
17 17 import evaluator # Evaluate geometry
18 Removed: import generator # Iteratevely evaluate instances of geometry
18 Added: import generator # Iteratevely evaluate instances of geometry and optimize
19 19 import random
20 20
21 21 import time
@@ -24,43 +24,69 @@
24 24 CHORD_LENGTH = 100
25 25 SEMI_SPAN = 200
26 26
27 Added: # m=Mass
28 Added: AIRFOIL_MASS = 100 # lbs
29 Added: SPAR_MASS = 10 # lbs
30 Added: STRINGER_MASS = 5 # lbs
31 Added:
32 Added: # Area
33 Added: STRINGER_AREA = 0.1 # sqin
34 Added:
35 Added: # population information
27 36 POP_SIZE = 1
28 37 SAVE_PATH = 'C:/Users/blend/github/UCLA_MAE_154B/save'
29 38
30 39
31 40 def main():
41 Added: '''
42 Added: Create an airfoil;
43 Added: Evaluate an airfoil;
44 Added: Generate a population of airfoils & optimize.
45 Added: '''
46 Added:
32 47 # Create coordinate system specific to our airfoil dimensions.
48 Added: # TODO: imperial + metric unit setting
33 49 creator.Coordinates(CHORD_LENGTH, SEMI_SPAN)
34 50
35 51 # Interate through all wings in population.
36 52 for _ in range(1, POP_SIZE + 1):
53 Added:
37 54 # Create airfoil instance
38 55 af = creator.Airfoil()
39 Removed: # Define NACA airfoil coordinates
56 Added: # Define NACA airfoil coordinates and mass
40 57 af.add_naca(2412)
41 Removed: af.print_coord(2)
58 Added: af.add_mass(AIRFOIL_MASS)
59 Added: af.print_info(2)
42 60
43 61 # Create spar instance
44 62 af.spar = creator.Spar()
45 Removed: # Define the spar coordinates, stored in single spar object
46 Removed: af.spar.add(af.coord, 0.15)
47 Removed: af.spar.add(af.coord, 0.55)
48 Removed: af.spar.print_coord(2)
63 Added: # Define the spar coordinates and mass, stored in single spar object
64 Added: af.spar.add_coord(af.coord, 0.15)
65 Added: af.spar.add_coord(af.coord, 0.55)
66 Added: af.spar.add_mass(SPAR_MASS)
67 Added: af.spar.print_info(2)
49 68
50 69 # Create stringer instance
51 70 af.stringer = creator.Stringer()
52 Removed: # Define the stringer coordinates from their amount
53 Removed: af.stringer.add(af.coord, af.spar.coord, 4, 7, 5, 6)
54 Removed: # Print coordinates of af.stringer to terminal
55 Removed: af.stringer.print_coord(2)
71 Added: # Compute the stringer coordinates from their quantity in each zone
72 Added: af.stringer.add_coord(af.coord, af.spar.coord, 4, 7, 5, 6)
73 Added: af.stringer.add_area(STRINGER_AREA)
74 Added: af.stringer.add_mass(STRINGER_MASS)
75 Added: af.stringer.print_info(2)
56 76
57 77 # Plot components with matplotlib
58 78 creator.plot(af, af.spar, af.stringer)
59 79
60 Removed: # Save component coordinates
61 Removed: af.save_coord(SAVE_PATH, _)
62 Removed: af.spar.save_coord(SAVE_PATH, _)
63 Removed: af.stringer.save_coord(SAVE_PATH, _)
80 Added: # Save component info
81 Added: af.save_info(SAVE_PATH, _)
82 Added: af.spar.save_info(SAVE_PATH, _)
83 Added: af.stringer.save_info(SAVE_PATH, _)
84 Added:
85 Added: # Evaluate previously created airfoil(s).
86 Added: total_mass = evaluator.get_total_mass(af, af.spar, af.stringer)
87 Added:
88 Added: # Iteratively evaluate airfoils by defining genetic generations.
89 Added: # pass
64 90
65 91 # Print final execution time
66 92 print("--- %s seconds ---" % (time.time() - start_time))