commence work on centroid

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