start packagification

Commit
cab1c2b9470ddc9099c4458daf2388d30ac12ca6
Author
Marius Peter <blendoit@gmail.com>
Author date
Committer
Marius Peter <blendoit@gmail.com>
Committer date
Changed files
__init__.py
index dc031d04..00000000 100644..000000
@@ -1,17 +0,0 @@
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: __author__ = "Marius Peter"
16 Removed: # __version__ = "2.3"
17 Removed: # __revision__ = "2.3.1"
creator.py
index 810df094..00000000 100644..000000
@@ -1,416 +0,0 @@
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: """
17 Removed: The creator.py module contains class definitions for coordinates
18 Removed: and various components we add to an airfoil (spars, stringers, and ribs).
19 Removed:
20 Removed: Classes:
21 Removed: Airfoil: instantiated with class method to provide coordinates to heirs.
22 Removed: Spar: inherits from Airfoil.
23 Removed: Stringer: also inherits from Airfoil.
24 Removed:
25 Removed: Functions:
26 Removed: plot_geom(airfoil): generates a 2D plot of the airfoil & any components.
27 Removed: """
28 Removed:
29 Removed: import sys
30 Removed: import os.path
31 Removed: import numpy as np
32 Removed: from math import sin, cos, atan
33 Removed: import bisect as bi
34 Removed: import matplotlib.pyplot as plt
35 Removed:
36 Removed:
37 Removed: class Airfoil:
38 Removed: """This class represents a single NACA airfoil.
39 Removed:
40 Removed: Please note: the coordinates are saved as two lists
41 Removed: for the x- and z-coordinates. The coordinates start at
42 Removed: the leading edge, travel over the airfoil's upper edge,
43 Removed: then loop back to the leading edge via the lower edge.
44 Removed:
45 Removed: This method was chosen for easier future exports
46 Removed: to 3D CAD packages like SolidWorks, which can import such
47 Removed: geometry as coordinates written in a CSV file.
48 Removed: """
49 Removed:
50 Removed: # Defaults
51 Removed: chord = 100
52 Removed: semi_span = 200
53 Removed:
54 Removed: def __init__(self):
55 Removed: # mass and area
56 Removed: self.mass = float()
57 Removed: self.area = float()
58 Removed: # Component material
59 Removed: self.material = str()
60 Removed: # Coordinates
61 Removed: self.x = []
62 Removed: self.z = []
63 Removed:
64 Removed: @classmethod
65 Removed: def from_dimensions(cls, chord, semi_span):
66 Removed: """Create airfoil from its chord and semi-span."""
67 Removed: if chord > 20:
68 Removed: cls.chord = chord
69 Removed: else:
70 Removed: cls.chord = 20
71 Removed: print('Chord too small, using minimum value of 20.')
72 Removed: cls.semi_span = semi_span
73 Removed: return Airfoil()
74 Removed:
75 Removed: def __str__(self):
76 Removed: return type(self).__name__
77 Removed:
78 Removed: def add_naca(self, naca_num):
79 Removed: """Generate surface geometry for a NACA airfoil.
80 Removed:
81 Removed: The nested functions perform the required steps to generate geometry,
82 Removed: and can be called to solve the geometry y-coordinate for any 'x' input.
83 Removed: Equation coefficients were retrieved from Wikipedia.org.
84 Removed:
85 Removed: Parameters:
86 Removed: naca_num: 4-digit NACA wing
87 Removed:
88 Removed: Return:
89 Removed: None
90 Removed: """
91 Removed: # Variables extracted from 'naca_num' argument passed to the function
92 Removed: self.naca_num = naca_num
93 Removed: m = int(str(naca_num)[0]) / 100
94 Removed: p = int(str(naca_num)[1]) / 10
95 Removed: t = int(str(naca_num)[2:]) / 100
96 Removed: # x-coordinate of maximum camber
97 Removed: p_c = p * self.chord
98 Removed:
99 Removed: def get_camber(x):
100 Removed: """
101 Removed: Returns camber z-coordinate from 1 'x' along the airfoil chord.
102 Removed: """
103 Removed: z_c = float()
104 Removed: if 0 <= x < p_c:
105 Removed: z_c = (m / (p ** 2)) * (2 * p * (x / self.chord)
106 Removed: - (x / self.chord) ** 2)
107 Removed: elif p_c <= x <= self.chord:
108 Removed: z_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p)
109 Removed: + 2 * p * (x / self.chord)
110 Removed: - (x / self.chord) ** 2)
111 Removed: return (z_c * self.chord)
112 Removed:
113 Removed: def get_thickness(x):
114 Removed: """Return thickness from 1 'x' along the airfoil chord."""
115 Removed: x = 0 if x < 0 else x
116 Removed: z_t = 5 * t * self.chord * (
117 Removed: + 0.2969 * (x / self.chord) ** 0.5
118 Removed: - 0.1260 * (x / self.chord) ** 1
119 Removed: - 0.3516 * (x / self.chord) ** 2
120 Removed: + 0.2843 * (x / self.chord) ** 3
121 Removed: - 0.1015 * (x / self.chord) ** 4)
122 Removed: return z_t
123 Removed:
124 Removed: def get_theta(x):
125 Removed: dz_c = float()
126 Removed: if 0 <= x < p_c:
127 Removed: dz_c = ((2 * m) / p ** 2) * (p - x / self.chord)
128 Removed: elif p_c <= x <= self.chord:
129 Removed: dz_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord)
130 Removed: theta = atan(dz_c)
131 Removed: return theta
132 Removed:
133 Removed: def get_upper_coord(x):
134 Removed: x = x - get_thickness(x) * sin(get_theta(x))
135 Removed: z = get_camber(x) + get_thickness(x) * cos(get_theta(x))
136 Removed: return (x, z)
137 Removed:
138 Removed: def get_lower_coord(x):
139 Removed: x = x + get_thickness(x) * sin(get_theta(x))
140 Removed: z = get_camber(x) - get_thickness(x) * cos(get_theta(x))
141 Removed: return (x, z)
142 Removed:
143 Removed: # Densify x-coordinates 10 times for first 1/4 chord length
144 Removed: x_chord_25_percent = round(self.chord / 4)
145 Removed:
146 Removed: x_chord = [i / 10 for i in range(x_chord_25_percent * 10)]
147 Removed: x_chord.extend(i for i in range(x_chord_25_percent, self.chord + 1))
148 Removed: # Reversed list for our lower airfoil coordinate densification
149 Removed: x_chord_rev = [i for i in range(self.chord, x_chord_25_percent, -1)]
150 Removed: extend = [i / 10 for i in range(x_chord_25_percent * 10, -1, -1)]
151 Removed: x_chord_rev.extend(extend)
152 Removed:
153 Removed: # Generate our airfoil geometry from previous sub-functions.
154 Removed: self.x_c = []
155 Removed: self.z_c = []
156 Removed: for x in x_chord:
157 Removed: self.x_c.append(x)
158 Removed: self.z_c.append(get_camber(x))
159 Removed: self.x.append(get_upper_coord(x)[0])
160 Removed: self.z.append(get_upper_coord(x)[1])
161 Removed: for x in x_chord_rev:
162 Removed: self.x.append(get_lower_coord(x)[0])
163 Removed: self.z.append(get_lower_coord(x)[1])
164 Removed: return None
165 Removed:
166 Removed: def add_mass(self, mass):
167 Removed: self.mass = mass
168 Removed:
169 Removed: def info_print(self, round):
170 Removed: """Print all the component's coordinates to the terminal."""
171 Removed: name = ' CREATOR DATA FOR {} '.format(str(self).upper())
172 Removed: num_of_dashes = len(name)
173 Removed: print(num_of_dashes * '-')
174 Removed: print(name)
175 Removed: for k, v in self.__dict__.items():
176 Removed: if type(v) != list:
177 Removed: print('{}:\n'.format(k), v)
178 Removed: print(num_of_dashes * '-')
179 Removed: for k, v in self.__dict__.items():
180 Removed: if type(v) == list:
181 Removed: print('{}:\n'.format(k), np.around(v, round))
182 Removed: return None
183 Removed:
184 Removed: def info_save(self, save_path, number):
185 Removed: """Save all the object's coordinates (must be full path)."""
186 Removed: file_name = '{}_{}.txt'.format(str(self).lower(), number)
187 Removed: full_path = os.path.join(save_path, file_name)
188 Removed: try:
189 Removed: with open(full_path, 'w') as sys.stdout:
190 Removed: self.info_print(6)
191 Removed: # This line required to reset behavior of sys.stdout
192 Removed: sys.stdout = sys.__stdout__
193 Removed: print('Successfully wrote to file {}'.format(full_path))
194 Removed: except IOError:
195 Removed: print('Unable to write {} to specified directory.\n'
196 Removed: .format(file_name),
197 Removed: 'Was the full path passed to the function?')
198 Removed: return None
199 Removed:
200 Removed:
201 Removed: class Spar(Airfoil):
202 Removed: """Contains a single spar's location."""
203 Removed:
204 Removed: def __init__(self):
205 Removed: super().__init__()
206 Removed: self.x_start = []
207 Removed: self.x_end = []
208 Removed: self.thickness = float()
209 Removed: self.z_start = []
210 Removed: self.z_end = []
211 Removed:
212 Removed: def add_coord(self, airfoil, x_loc_percent):
213 Removed: """Add a single spar at the % chord location given to function.
214 Removed:
215 Removed: Parameters:
216 Removed: airfoil: gives the spar access to airfoil's coordinates.
217 Removed: x_loc_percent: spar's location as a % of total chord length.
218 Removed:
219 Removed: Return:
220 Removed: None
221 Removed: """
222 Removed:
223 Removed: # Scaled spar location with regards to chord
224 Removed: loc = x_loc_percent * self.chord
225 Removed: # bi.bisect_left: returns index of first value in airfoil.x > loc
226 Removed: # This ensures that spar geom intersects with airfoil geom.
227 Removed: # Spar upper coordinates
228 Removed: spar_x = bi.bisect_left(airfoil.x, loc) - 1
229 Removed: x = [airfoil.x[spar_x]]
230 Removed: z = [airfoil.z[spar_x]]
231 Removed: # Spar lower coordinates
232 Removed: spar_x = bi.bisect_left(airfoil.x[::-1], loc)
233 Removed: x += [airfoil.x[-spar_x]]
234 Removed: z += [airfoil.z[-spar_x]]
235 Removed: self.x.append(x)
236 Removed: self.z.append(z)
237 Removed: return None
238 Removed:
239 Removed: def add_spar_caps(self, spar_cap_area):
240 Removed: self.cap_area = spar_cap_area
241 Removed: return None
242 Removed:
243 Removed: def add_mass(self, mass):
244 Removed: self.mass = len(self.x) * mass
245 Removed: return None
246 Removed:
247 Removed: def add_webs(self, thickness):
248 Removed: """Add webs to spars."""
249 Removed: for _ in range(len(self.x)):
250 Removed: self.x_start.append(self.x[_][0])
251 Removed: self.x_end.append(self.x[_][1])
252 Removed: self.z_start.append(self.z[_][0])
253 Removed: self.z_end.append(self.z[_][1])
254 Removed: self.thickness = thickness
255 Removed: return None
256 Removed:
257 Removed:
258 Removed: class Stringer(Airfoil):
259 Removed: """Contains the coordinates of all stringers."""
260 Removed:
261 Removed: def __init__(self):
262 Removed: super().__init__()
263 Removed: self.x_start = []
264 Removed: self.x_end = []
265 Removed: self.thickness = float()
266 Removed: self.z_start = []
267 Removed: self.z_end = []
268 Removed: self.area = float()
269 Removed:
270 Removed: def add_coord(self, airfoil,
271 Removed: stringer_u_1, stringer_u_2,
272 Removed: stringer_l_1, stringer_l_2):
273 Removed: """Add equally distributed stringers to four airfoil locations
274 Removed: (upper nose, lower nose, upper surface, lower surface).
275 Removed:
276 Removed: Parameters:
277 Removed: airfoil_coord: packed airfoil coordinates
278 Removed: spar_coord: packed spar coordinates
279 Removed: stringer_u_1: upper nose number of stringers
280 Removed: stringer_u_2: upper surface number of stringers
281 Removed: stringer_l_1: lower nose number of stringers
282 Removed: stringer_l_2: lower surface number of stringers
283 Removed:
284 Removed: Returns:
285 Removed: None
286 Removed: """
287 Removed:
288 Removed: # Find distance between leading edge and first upper stringer
289 Removed: interval = airfoil.spar.x[0][0] / (stringer_u_1 + 1)
290 Removed: # initialise first self.stringer_x at first interval
291 Removed: x = interval
292 Removed: # Add upper stringers from leading edge until first spar.
293 Removed: for _ in range(0, stringer_u_1):
294 Removed: # Index of the first value of airfoil.x > x
295 Removed: i = bi.bisect_left(airfoil.x, x)
296 Removed: self.x.append(airfoil.x[i])
297 Removed: self.z.append(airfoil.z[i])
298 Removed: x += interval
299 Removed: # Add upper stringers from first spar until last spar
300 Removed: # TODO: stringer placement if only one spar is created
301 Removed: interval = (airfoil.spar.x[-1][0]
302 Removed: - airfoil.spar.x[0][0]) / (stringer_u_2 + 1)
303 Removed: x = interval + airfoil.spar.x[0][0]
304 Removed: for _ in range(0, stringer_u_2):
305 Removed: i = bi.bisect_left(airfoil.x, x)
306 Removed: self.x.append(airfoil.x[i])
307 Removed: self.z.append(airfoil.z[i])
308 Removed: x += interval
309 Removed:
310 Removed: # Find distance between leading edge and first lower stringer
311 Removed: interval = airfoil.spar.x[0][1] / (stringer_l_1 + 1)
312 Removed: x = interval
313 Removed: # Add lower stringers from leading edge until first spar.
314 Removed: for _ in range(0, stringer_l_1):
315 Removed: i = bi.bisect_left(airfoil.x[::-1], x)
316 Removed: self.x.append(airfoil.x[-i])
317 Removed: self.z.append(airfoil.z[-i])
318 Removed: x += interval
319 Removed: # Add lower stringers from first spar until last spar
320 Removed: interval = (airfoil.spar.x[-1][1]
321 Removed: - airfoil.spar.x[0][1]) / (stringer_l_2 + 1)
322 Removed: x = interval + airfoil.spar.x[0][1]
323 Removed: for _ in range(0, stringer_l_2):
324 Removed: i = bi.bisect_left(airfoil.x[::-1], x)
325 Removed: self.x.append(airfoil.x[-i])
326 Removed: self.z.append(airfoil.z[-i])
327 Removed: x += interval
328 Removed: return None
329 Removed:
330 Removed: def add_area(self, area):
331 Removed: self.area = area
332 Removed: return None
333 Removed:
334 Removed: def add_mass(self, mass):
335 Removed: self.mass = len(self.x) * mass + len(self.x) * mass
336 Removed: return None
337 Removed:
338 Removed: def add_webs(self, thickness):
339 Removed: """Add webs to stringers."""
340 Removed: for _ in range(len(self.x) // 2):
341 Removed: self.x_start.append(self.x[_])
342 Removed: self.x_end.append(self.x[_ + 1])
343 Removed: self.z_start.append(self.z[_])
344 Removed: self.z_end.append(self.z[_ + 1])
345 Removed: self.thickness = thickness
346 Removed: return None
347 Removed:
348 Removed: def info_print(self, round):
349 Removed: super().info_print(round)
350 Removed: print('Stringer Area:\n', np.around(self.area, round))
351 Removed: return None
352 Removed:
353 Removed:
354 Removed: def plot_geom(airfoil, view: False):
355 Removed: """This function plots the airfoil's + sub-components' geometry."""
356 Removed: fig, ax = plt.subplots()
357 Removed:
358 Removed: # Plot chord
359 Removed: x = [0, airfoil.chord]
360 Removed: y = [0, 0]
361 Removed: ax.plot(x, y, linewidth='1')
362 Removed: # Plot quarter chord
363 Removed: ax.plot(airfoil.chord / 4, 0,
364 Removed: '.', color='g', markersize=24,
365 Removed: label='Quarter-chord')
366 Removed: # Plot mean camber line
367 Removed: ax.plot(airfoil.x_c, airfoil.z_c,
368 Removed: '-.', color='r', linewidth='2',
369 Removed: label='Mean camber line')
370 Removed: # Plot airfoil surfaces
371 Removed: ax.plot(airfoil.x, airfoil.z,
372 Removed: color='b', linewidth='1')
373 Removed:
374 Removed: # Plot spars
375 Removed: try:
376 Removed: for _ in range(len(airfoil.spar.x)):
377 Removed: x = (airfoil.spar.x[_])
378 Removed: y = (airfoil.spar.z[_])
379 Removed: ax.plot(x, y, '-', color='y', linewidth='4')
380 Removed: except AttributeError:
381 Removed: print('No spars to plot.')
382 Removed: # Plot stringers
383 Removed: try:
384 Removed: for _ in range(0, len(airfoil.stringer.x)):
385 Removed: x = airfoil.stringer.x[_]
386 Removed: y = airfoil.stringer.z[_]
387 Removed: ax.plot(x, y, '.', color='y', markersize=12)
388 Removed: except AttributeError:
389 Removed: print('No stringers to plot.')
390 Removed:
391 Removed: # Graph formatting
392 Removed: plot_bound = max(airfoil.x)
393 Removed: ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil',
394 Removed: xlabel='X axis',
395 Removed: xlim=[- 0.10 * plot_bound, 1.10 * plot_bound],
396 Removed: ylabel='Z axis',
397 Removed: ylim=[- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)])
398 Removed:
399 Removed: plt.grid(axis='both', linestyle=':', linewidth=1)
400 Removed: plt.gca().set_aspect('equal', adjustable='box')
401 Removed: plt.gca().legend(bbox_to_anchor=(1, 1),
402 Removed: bbox_transform=plt.gcf().transFigure)
403 Removed:
404 Removed: if view == True:
405 Removed: plt.show()
406 Removed: else:
407 Removed: pass
408 Removed: return fig, ax
409 Removed:
410 Removed:
411 Removed: def main():
412 Removed: return None
413 Removed:
414 Removed:
415 Removed: if __name__ == '__main__':
416 Removed: main()
evaluator.py
index ad1f8732..00000000 100644..000000
@@ -1,288 +0,0 @@
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: The evaluator.py module contains a single Evaluator class,
17 Removed: which knows all the attributes of a specified Airfoil instance,
18 Removed: and contains functions to analyse the airfoil's geometrical
19 Removed: & structural properties.
20 Removed: """
21 Removed:
22 Removed: import sys
23 Removed: import os.path
24 Removed: import numpy as np
25 Removed: from math import sqrt
26 Removed: import matplotlib.pyplot as plt
27 Removed:
28 Removed:
29 Removed: class Evaluator:
30 Removed: """Performs structural evaluations for the airfoil passed as argument."""
31 Removed:
32 Removed: def __init__(self, airfoil):
33 Removed: # Evaluator knows all geometrical info from evaluated airfoil
34 Removed: self.airfoil = airfoil
35 Removed: self.spar = airfoil.spar
36 Removed: self.stringer = airfoil.stringer
37 Removed: # Global dimensions
38 Removed: self.chord = airfoil.chord
39 Removed: self.semi_span = airfoil.semi_span
40 Removed: # Mass & spanwise distribution
41 Removed: self.mass_total = float(airfoil.mass
42 Removed: + airfoil.spar.mass
43 Removed: + airfoil.stringer.mass)
44 Removed: self.mass_dist = []
45 Removed: # Lift
46 Removed: self.lift_rectangular = []
47 Removed: self.lift_elliptical = []
48 Removed: self.lift_total = []
49 Removed: # Drag
50 Removed: self.drag = []
51 Removed: # centroid
52 Removed: self.centroid = []
53 Removed: # Inertia terms:
54 Removed: self.I_ = {'x': 0, 'z': 0, 'xz': 0}
55 Removed:
56 Removed: def __str__(self):
57 Removed: return type(self).__name__
58 Removed:
59 Removed: def info_print(self, round):
60 Removed: """Print all the component's evaluated data to the terminal."""
61 Removed: name = ' EVALUATOR DATA FOR {} '.format(str(self).upper())
62 Removed: num_of_dashes = len(name)
63 Removed: print(num_of_dashes * '-')
64 Removed: print(name)
65 Removed: for k, v in self.__dict__.items():
66 Removed: if type(v) != list:
67 Removed: print('{}:\n'.format(k), v)
68 Removed: print(num_of_dashes * '-')
69 Removed: for k, v in self.__dict__.items():
70 Removed: if type(v) == list:
71 Removed: print('{}:\n'.format(k), np.around(v, round))
72 Removed: return None
73 Removed:
74 Removed: def info_save(self, save_path, number):
75 Removed: """Save all the object's coordinates (must be full path)."""
76 Removed: file_name = 'airfoil_{}_eval.txt'.format(number)
77 Removed: full_path = os.path.join(save_path, file_name)
78 Removed: try:
79 Removed: with open(full_path, 'w') as sys.stdout:
80 Removed: self.info_print(6)
81 Removed: # This line required to reset behavior of sys.stdout
82 Removed: sys.stdout = sys.__stdout__
83 Removed: print('Successfully wrote to file {}'.format(full_path))
84 Removed: except IOError:
85 Removed: print(
86 Removed: 'Unable to write {} to specified directory.\n'.format(
87 Removed: file_name), 'Was the full path passed to the function?')
88 Removed: return None
89 Removed:
90 Removed: # All these functions take integer arguments and return lists.
91 Removed:
92 Removed: def get_lift_rectangular(self, lift):
93 Removed: L_prime = [lift / (self.semi_span * 2) for x in range(self.semi_span)]
94 Removed: return L_prime
95 Removed:
96 Removed: def get_lift_elliptical(self, L_0):
97 Removed: L_prime = [
98 Removed: L_0 / (self.semi_span * 2) * sqrt(1 - (y / self.semi_span)**2)
99 Removed: for y in range(self.semi_span)
100 Removed: ]
101 Removed: return L_prime
102 Removed:
103 Removed: def get_lift_total(self):
104 Removed: F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2
105 Removed: for _ in range(len(self.lift_rectangular))]
106 Removed: return F_z
107 Removed:
108 Removed: def get_mass_distribution(self, total_mass):
109 Removed: F_z = [total_mass / self.semi_span for x in range(0, self.semi_span)]
110 Removed: return F_z
111 Removed:
112 Removed: def get_drag(self, drag):
113 Removed: # Transform semi-span integer into list
114 Removed: semi_span = [x for x in range(0, self.semi_span)]
115 Removed:
116 Removed: # Drag increases after 80% of the semi_span
117 Removed: cutoff = round(0.8 * self.semi_span)
118 Removed:
119 Removed: # Drag increases by 25% after 80% of the semi_span
120 Removed: F_x = [drag for x in semi_span[0:cutoff]]
121 Removed: F_x.extend([1.25 * drag for x in semi_span[cutoff:]])
122 Removed: return F_x
123 Removed:
124 Removed: def get_centroid(self):
125 Removed: """Return the coordinates of the centroid."""
126 Removed: stringer_area = self.stringer.area
127 Removed: cap_area = self.spar.cap_area
128 Removed:
129 Removed: caps_x = [value for spar in self.spar.x for value in spar]
130 Removed: caps_z = [value for spar in self.spar.z for value in spar]
131 Removed: stringers_x = self.stringer.x
132 Removed: stringers_z = self.stringer.z
133 Removed:
134 Removed: denominator = float(len(caps_x) * cap_area
135 Removed: + len(stringers_x) * stringer_area)
136 Removed:
137 Removed: centroid_x = float(sum([x * cap_area for x in caps_x])
138 Removed: + sum([x * stringer_area for x in stringers_x]))
139 Removed: centroid_x = centroid_x / denominator
140 Removed:
141 Removed: centroid_z = float(sum([z * cap_area for z in caps_z])
142 Removed: + sum([z * stringer_area for z in stringers_z]))
143 Removed: centroid_z = centroid_z / denominator
144 Removed:
145 Removed: return (centroid_x, centroid_z)
146 Removed:
147 Removed: def get_inertia_terms(self):
148 Removed: """Obtain all inertia terms."""
149 Removed: stringer_area = self.stringer.area
150 Removed: cap_area = self.spar.cap_area
151 Removed:
152 Removed: # Adds upper and lower components' coordinates to list
153 Removed: x_stringers = self.stringer.x
154 Removed: z_stringers = self.stringer.z
155 Removed: x_spars = self.spar.x[:][0] + self.spar.x[:][1]
156 Removed: z_spars = self.spar.z[:][0] + self.spar.z[:][1]
157 Removed: stringer_count = range(len(x_stringers))
158 Removed: spar_count = range(len(self.spar.x))
159 Removed:
160 Removed: # I_x is the sum of the contributions of the spar caps and stringers
161 Removed: # TODO: replace list indices with dictionary value
162 Removed: I_x = sum([cap_area * (z_spars[i] - self.centroid[1])**2
163 Removed: for i in spar_count])
164 Removed: I_x += sum([stringer_area * (z_stringers[i] - self.centroid[1])**2
165 Removed: for i in stringer_count])
166 Removed:
167 Removed: I_z = sum([cap_area * (x_spars[i] - self.centroid[0])**2
168 Removed: for i in spar_count])
169 Removed: I_z += sum([stringer_area * (x_stringers[i] - self.centroid[0])**2
170 Removed: for i in stringer_count])
171 Removed:
172 Removed: I_xz = sum([cap_area * (x_spars[i] - self.centroid[0])
173 Removed: * (z_spars[i] - self.centroid[1])
174 Removed: for i in spar_count])
175 Removed: I_xz += sum([stringer_area * (x_stringers[i] - self.centroid[0])
176 Removed: * (z_stringers[i] - self.centroid[1])
177 Removed: for i in stringer_count])
178 Removed: return (I_x, I_z, I_xz)
179 Removed:
180 Removed: def get_dx(self, component):
181 Removed: return [x - self.centroid[0] for x in component.x_start]
182 Removed:
183 Removed: def get_dz(self, component):
184 Removed: return [x - self.centroid[1] for x in component.x_start]
185 Removed:
186 Removed: def get_dP(self, xDist, zDist, V_x, V_z, area):
187 Removed: I_x = self.I_['x']
188 Removed: I_z = self.I_['z']
189 Removed: I_xz = self.I_['xz']
190 Removed: denom = float(I_x * I_z - I_xz ** 2)
191 Removed: z = float()
192 Removed: for _ in range(len(xDist)):
193 Removed: z += float(-area * xDist[_] * (I_x * V_x - I_xz * V_z)
194 Removed: / denom
195 Removed: - area * zDist[_] * (I_z * V_z - I_xz * V_x)
196 Removed: / denom)
197 Removed: return z
198 Removed:
199 Removed: def analysis(self, V_x, V_z):
200 Removed: """Perform all analysis calculations and store in class instance."""
201 Removed: self.drag = self.get_drag(10)
202 Removed: self.lift_rectangular = self.get_lift_rectangular(13.7)
203 Removed: self.lift_elliptical = self.get_lift_elliptical(15)
204 Removed: self.lift_total = self.get_lift_total()
205 Removed: self.mass_dist = self.get_mass_distribution(self.mass_total)
206 Removed: self.centroid = self.get_centroid()
207 Removed: self.I_['x'] = self.get_inertia_terms()[0]
208 Removed: self.I_['z'] = self.get_inertia_terms()[1]
209 Removed: self.I_['xz'] = self.get_inertia_terms()[2]
210 Removed: spar_dx = self.get_dx(self.spar)
211 Removed: spar_dz = self.get_dz(self.spar)
212 Removed: self.spar.dP_x = self.get_dP(spar_dx, spar_dz,
213 Removed: V_x, 0, self.spar.cap_area)
214 Removed: self.spar.dP_z = self.get_dP(spar_dx, spar_dz,
215 Removed: 0, V_z, self.spar.cap_area)
216 Removed: return None
217 Removed:
218 Removed:
219 Removed: def plot_geom(evaluator):
220 Removed: """This function plots analysis results over the airfoil's geometry."""
221 Removed: # Plot chord
222 Removed: x_chord = [0, evaluator.chord]
223 Removed: y_chord = [0, 0]
224 Removed: plt.plot(x_chord, y_chord, linewidth='1')
225 Removed: # Plot quarter chord
226 Removed: plt.plot(evaluator.chord / 4, 0,
227 Removed: '.', color='g', markersize=24, label='Quarter-chord')
228 Removed: # Plot airfoil surfaces
229 Removed: x = [0.98 * x for x in evaluator.airfoil.x]
230 Removed: y = [0.98 * z for z in evaluator.airfoil.z]
231 Removed: plt.fill(x, y, color='w', linewidth='1', fill=False)
232 Removed: x = [1.02 * x for x in evaluator.airfoil.x]
233 Removed: y = [1.02 * z for z in evaluator.airfoil.z]
234 Removed: plt.fill(x, y, color='b', linewidth='1', fill=False)
235 Removed:
236 Removed: # Plot spars
237 Removed: try:
238 Removed: for _ in range(len(evaluator.spar.x)):
239 Removed: x = (evaluator.spar.x[_])
240 Removed: y = (evaluator.spar.z[_])
241 Removed: plt.plot(x, y, '-', color='b')
242 Removed: except AttributeError:
243 Removed: print('No spars to plot.')
244 Removed: # Plot stringers
245 Removed: try:
246 Removed: for _ in range(0, len(evaluator.stringer.x)):
247 Removed: x = evaluator.stringer.x[_]
248 Removed: y = evaluator.stringer.z[_]
249 Removed: plt.plot(x, y, '.', color='y', markersize=12)
250 Removed: except AttributeError:
251 Removed: print('No stringers to plot.')
252 Removed:
253 Removed: # Plot centroid
254 Removed: x = evaluator.centroid[0]
255 Removed: y = evaluator.centroid[1]
256 Removed: plt.plot(x, y, '.', color='r', markersize=24, label='centroid')
257 Removed:
258 Removed: # Graph formatting
259 Removed: plt.xlabel('X axis')
260 Removed: plt.ylabel('Z axis')
261 Removed:
262 Removed: plot_bound = max(evaluator.airfoil.x)
263 Removed: plt.xlim(-0.10 * plot_bound, 1.10 * plot_bound)
264 Removed: plt.ylim(-(1.10 * plot_bound / 2), (1.10 * plot_bound / 2))
265 Removed: plt.gca().set_aspect('equal', adjustable='box')
266 Removed: plt.gca().legend()
267 Removed: plt.grid(axis='both', linestyle=':', linewidth=1)
268 Removed: plt.show()
269 Removed: return None
270 Removed:
271 Removed:
272 Removed: def plot_lift(evaluator):
273 Removed: x = range(evaluator.semi_span)
274 Removed: y_1 = evaluator.lift_rectangular
275 Removed: y_2 = evaluator.lift_elliptical
276 Removed: y_3 = evaluator.lift_total
277 Removed: plt.plot(x, y_1, '.', color='b', markersize=4, label='Rectangular lift')
278 Removed: plt.plot(x, y_2, '.', color='g', markersize=4, label='Elliptical lift')
279 Removed: plt.plot(x, y_3, '.', color='r', markersize=4, label='Total lift')
280 Removed:
281 Removed: # Graph formatting
282 Removed: plt.xlabel('Semi-span location')
283 Removed: plt.ylabel('Lift')
284 Removed:
285 Removed: plt.gca().legend()
286 Removed: plt.grid(axis='both', linestyle=':', linewidth=1)
287 Removed: plt.show()
288 Removed: return None
example_airfoil.py
index cf279d5b..f09ea7f3 100644..100644
@@ -1,13 +1,14 @@
1 1 """This example illustrates the usage of creator, evaluator and generator.
2 2
3 Added: All the steps of airfoil creation & evaluation are detailed here;
4 Added: however, the generator.py module contains certain presets (default airfoils).
5 Added:
3 6 Create an airfoil;
4 7 Evaluate an airfoil;
5 8 Generate a population of airfoils & optimize.
6 9 """
7 10
8 Removed: import creator # Create geometry
9 Removed: import evaluator # Evaluate geometry
10 Removed: import generator # Iteratevely evaluate instances of geometry and optimize
11 Added: from tools import creator, evaluator, generator
11 12
12 13 import time
13 14 start_time = time.time()
@@ -36,10 +37,9 @@
36 37 NOSE_TOP_STRINGERS = 3
37 38 NOSE_BOTTOM_STRINGERS = 5
38 39
39 Removed: # population information & save path
40 Removed: POP_SIZE = 1
41 40 SAVE_PATH = 'C:/Users/blend/github/UCLA_MAE_154B/save'
42 41
42 Added:
43 43 # Create airfoil instance
44 44 af = creator.Airfoil.from_dimensions(CHORD_LENGTH, SEMI_SPAN)
45 45 af.add_naca(NACA_NUM)
@@ -49,10 +49,10 @@
49 49
50 50 # Create spar instance
51 51 af.spar = creator.Spar()
52 Removed: # Define the spar coordinates and mass, stored in single spar object
52 Added: # All spar coordinates are stored in single Spar object
53 53 af.spar.add_coord(af, 0.23)
54 54 af.spar.add_coord(af, 0.57)
55 Removed: # Automatically adds spar caps for each spar defined previously
55 Added: # Automatically adds spar caps for each spar previously defined
56 56 af.spar.add_spar_caps(SPAR_CAP_AREA)
57 57 af.spar.add_mass(SPAR_MASS)
58 58 af.spar.add_webs(SPAR_THICKNESS)
generator.py
index 6a2865dd..00000000 100644..000000
@@ -1,64 +0,0 @@
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: The generator.py module contains a single Population class,
17 Removed: which represents a collection of randomized airfoils.
18 Removed: """
19 Removed:
20 Removed: import creator
21 Removed:
22 Removed:
23 Removed: def default_airfoil():
24 Removed: """Generate the default airfoil."""
25 Removed: airfoil = creator.Airfoil.from_dimensions(100, 200)
26 Removed: airfoil.add_naca(2412)
27 Removed: airfoil.add_mass(10)
28 Removed:
29 Removed: airfoil.spar = creator.Spar()
30 Removed: airfoil.spar.add_coord(airfoil, 0.23)
31 Removed: airfoil.spar.add_coord(airfoil, 0.57)
32 Removed: airfoil.spar.add_spar_caps(0.3)
33 Removed: airfoil.spar.add_mass(10)
34 Removed: airfoil.spar.add_webs(0.4)
35 Removed:
36 Removed: airfoil.stringer = creator.Stringer()
37 Removed: airfoil.stringer.add_coord(airfoil, 3, 6, 5, 4)
38 Removed: airfoil.stringer.add_area(0.1)
39 Removed: airfoil.stringer.add_mass(5)
40 Removed: airfoil.stringer.add_webs(0.1)
41 Removed:
42 Removed: return airfoil
43 Removed:
44 Removed:
45 Removed: class Population(creator.Airfoil):
46 Removed: """Collection of random airfoils."""
47 Removed:
48 Removed: def __init__(self, size):
49 Removed: af = creator.Airfoil
50 Removed: # print(af)
51 Removed: self.size = size
52 Removed: self.gen_number = 0 # incremented for every generation
53 Removed:
54 Removed: def mutate(self, prob_mt):
55 Removed: """Randomly mutate the genes of prob_mt % of the population."""
56 Removed:
57 Removed: def crossover(self, prob_cx):
58 Removed: """Combine the genes of prob_cx % of the population."""
59 Removed:
60 Removed: def reproduce(self, prob_rp):
61 Removed: """Pass on the genes of the fittest prob_rp % of the population."""
62 Removed:
63 Removed: def fitness():
64 Removed: """Rate the fitness of an individual on a relative scale (0-100)"""
tools/creator.py
index 00000000..810df094 000000..100644
@@ -0,0 +1,416 @@
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: """
17 Added: The creator.py module contains class definitions for coordinates
18 Added: and various components we add to an airfoil (spars, stringers, and ribs).
19 Added:
20 Added: Classes:
21 Added: Airfoil: instantiated with class method to provide coordinates to heirs.
22 Added: Spar: inherits from Airfoil.
23 Added: Stringer: also inherits from Airfoil.
24 Added:
25 Added: Functions:
26 Added: plot_geom(airfoil): generates a 2D plot of the airfoil & any components.
27 Added: """
28 Added:
29 Added: import sys
30 Added: import os.path
31 Added: import numpy as np
32 Added: from math import sin, cos, atan
33 Added: import bisect as bi
34 Added: import matplotlib.pyplot as plt
35 Added:
36 Added:
37 Added: class Airfoil:
38 Added: """This class represents a single NACA airfoil.
39 Added:
40 Added: Please note: the coordinates are saved as two lists
41 Added: for the x- and z-coordinates. The coordinates start at
42 Added: the leading edge, travel over the airfoil's upper edge,
43 Added: then loop back to the leading edge via the lower edge.
44 Added:
45 Added: This method was chosen for easier future exports
46 Added: to 3D CAD packages like SolidWorks, which can import such
47 Added: geometry as coordinates written in a CSV file.
48 Added: """
49 Added:
50 Added: # Defaults
51 Added: chord = 100
52 Added: semi_span = 200
53 Added:
54 Added: def __init__(self):
55 Added: # mass and area
56 Added: self.mass = float()
57 Added: self.area = float()
58 Added: # Component material
59 Added: self.material = str()
60 Added: # Coordinates
61 Added: self.x = []
62 Added: self.z = []
63 Added:
64 Added: @classmethod
65 Added: def from_dimensions(cls, chord, semi_span):
66 Added: """Create airfoil from its chord and semi-span."""
67 Added: if chord > 20:
68 Added: cls.chord = chord
69 Added: else:
70 Added: cls.chord = 20
71 Added: print('Chord too small, using minimum value of 20.')
72 Added: cls.semi_span = semi_span
73 Added: return Airfoil()
74 Added:
75 Added: def __str__(self):
76 Added: return type(self).__name__
77 Added:
78 Added: def add_naca(self, naca_num):
79 Added: """Generate surface geometry for a NACA airfoil.
80 Added:
81 Added: The nested functions perform the required steps to generate geometry,
82 Added: and can be called to solve the geometry y-coordinate for any 'x' input.
83 Added: Equation coefficients were retrieved from Wikipedia.org.
84 Added:
85 Added: Parameters:
86 Added: naca_num: 4-digit NACA wing
87 Added:
88 Added: Return:
89 Added: None
90 Added: """
91 Added: # Variables extracted from 'naca_num' argument passed to the function
92 Added: self.naca_num = naca_num
93 Added: m = int(str(naca_num)[0]) / 100
94 Added: p = int(str(naca_num)[1]) / 10
95 Added: t = int(str(naca_num)[2:]) / 100
96 Added: # x-coordinate of maximum camber
97 Added: p_c = p * self.chord
98 Added:
99 Added: def get_camber(x):
100 Added: """
101 Added: Returns camber z-coordinate from 1 'x' along the airfoil chord.
102 Added: """
103 Added: z_c = float()
104 Added: if 0 <= x < p_c:
105 Added: z_c = (m / (p ** 2)) * (2 * p * (x / self.chord)
106 Added: - (x / self.chord) ** 2)
107 Added: elif p_c <= x <= self.chord:
108 Added: z_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p)
109 Added: + 2 * p * (x / self.chord)
110 Added: - (x / self.chord) ** 2)
111 Added: return (z_c * self.chord)
112 Added:
113 Added: def get_thickness(x):
114 Added: """Return thickness from 1 'x' along the airfoil chord."""
115 Added: x = 0 if x < 0 else x
116 Added: z_t = 5 * t * self.chord * (
117 Added: + 0.2969 * (x / self.chord) ** 0.5
118 Added: - 0.1260 * (x / self.chord) ** 1
119 Added: - 0.3516 * (x / self.chord) ** 2
120 Added: + 0.2843 * (x / self.chord) ** 3
121 Added: - 0.1015 * (x / self.chord) ** 4)
122 Added: return z_t
123 Added:
124 Added: def get_theta(x):
125 Added: dz_c = float()
126 Added: if 0 <= x < p_c:
127 Added: dz_c = ((2 * m) / p ** 2) * (p - x / self.chord)
128 Added: elif p_c <= x <= self.chord:
129 Added: dz_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord)
130 Added: theta = atan(dz_c)
131 Added: return theta
132 Added:
133 Added: def get_upper_coord(x):
134 Added: x = x - get_thickness(x) * sin(get_theta(x))
135 Added: z = get_camber(x) + get_thickness(x) * cos(get_theta(x))
136 Added: return (x, z)
137 Added:
138 Added: def get_lower_coord(x):
139 Added: x = x + get_thickness(x) * sin(get_theta(x))
140 Added: z = get_camber(x) - get_thickness(x) * cos(get_theta(x))
141 Added: return (x, z)
142 Added:
143 Added: # Densify x-coordinates 10 times for first 1/4 chord length
144 Added: x_chord_25_percent = round(self.chord / 4)
145 Added:
146 Added: x_chord = [i / 10 for i in range(x_chord_25_percent * 10)]
147 Added: x_chord.extend(i for i in range(x_chord_25_percent, self.chord + 1))
148 Added: # Reversed list for our lower airfoil coordinate densification
149 Added: x_chord_rev = [i for i in range(self.chord, x_chord_25_percent, -1)]
150 Added: extend = [i / 10 for i in range(x_chord_25_percent * 10, -1, -1)]
151 Added: x_chord_rev.extend(extend)
152 Added:
153 Added: # Generate our airfoil geometry from previous sub-functions.
154 Added: self.x_c = []
155 Added: self.z_c = []
156 Added: for x in x_chord:
157 Added: self.x_c.append(x)
158 Added: self.z_c.append(get_camber(x))
159 Added: self.x.append(get_upper_coord(x)[0])
160 Added: self.z.append(get_upper_coord(x)[1])
161 Added: for x in x_chord_rev:
162 Added: self.x.append(get_lower_coord(x)[0])
163 Added: self.z.append(get_lower_coord(x)[1])
164 Added: return None
165 Added:
166 Added: def add_mass(self, mass):
167 Added: self.mass = mass
168 Added:
169 Added: def info_print(self, round):
170 Added: """Print all the component's coordinates to the terminal."""
171 Added: name = ' CREATOR DATA FOR {} '.format(str(self).upper())
172 Added: num_of_dashes = len(name)
173 Added: print(num_of_dashes * '-')
174 Added: print(name)
175 Added: for k, v in self.__dict__.items():
176 Added: if type(v) != list:
177 Added: print('{}:\n'.format(k), v)
178 Added: print(num_of_dashes * '-')
179 Added: for k, v in self.__dict__.items():
180 Added: if type(v) == list:
181 Added: print('{}:\n'.format(k), np.around(v, round))
182 Added: return None
183 Added:
184 Added: def info_save(self, save_path, number):
185 Added: """Save all the object's coordinates (must be full path)."""
186 Added: file_name = '{}_{}.txt'.format(str(self).lower(), number)
187 Added: full_path = os.path.join(save_path, file_name)
188 Added: try:
189 Added: with open(full_path, 'w') as sys.stdout:
190 Added: self.info_print(6)
191 Added: # This line required to reset behavior of sys.stdout
192 Added: sys.stdout = sys.__stdout__
193 Added: print('Successfully wrote to file {}'.format(full_path))
194 Added: except IOError:
195 Added: print('Unable to write {} to specified directory.\n'
196 Added: .format(file_name),
197 Added: 'Was the full path passed to the function?')
198 Added: return None
199 Added:
200 Added:
201 Added: class Spar(Airfoil):
202 Added: """Contains a single spar's location."""
203 Added:
204 Added: def __init__(self):
205 Added: super().__init__()
206 Added: self.x_start = []
207 Added: self.x_end = []
208 Added: self.thickness = float()
209 Added: self.z_start = []
210 Added: self.z_end = []
211 Added:
212 Added: def add_coord(self, airfoil, x_loc_percent):
213 Added: """Add a single spar at the % chord location given to function.
214 Added:
215 Added: Parameters:
216 Added: airfoil: gives the spar access to airfoil's coordinates.
217 Added: x_loc_percent: spar's location as a % of total chord length.
218 Added:
219 Added: Return:
220 Added: None
221 Added: """
222 Added:
223 Added: # Scaled spar location with regards to chord
224 Added: loc = x_loc_percent * self.chord
225 Added: # bi.bisect_left: returns index of first value in airfoil.x > loc
226 Added: # This ensures that spar geom intersects with airfoil geom.
227 Added: # Spar upper coordinates
228 Added: spar_x = bi.bisect_left(airfoil.x, loc) - 1
229 Added: x = [airfoil.x[spar_x]]
230 Added: z = [airfoil.z[spar_x]]
231 Added: # Spar lower coordinates
232 Added: spar_x = bi.bisect_left(airfoil.x[::-1], loc)
233 Added: x += [airfoil.x[-spar_x]]
234 Added: z += [airfoil.z[-spar_x]]
235 Added: self.x.append(x)
236 Added: self.z.append(z)
237 Added: return None
238 Added:
239 Added: def add_spar_caps(self, spar_cap_area):
240 Added: self.cap_area = spar_cap_area
241 Added: return None
242 Added:
243 Added: def add_mass(self, mass):
244 Added: self.mass = len(self.x) * mass
245 Added: return None
246 Added:
247 Added: def add_webs(self, thickness):
248 Added: """Add webs to spars."""
249 Added: for _ in range(len(self.x)):
250 Added: self.x_start.append(self.x[_][0])
251 Added: self.x_end.append(self.x[_][1])
252 Added: self.z_start.append(self.z[_][0])
253 Added: self.z_end.append(self.z[_][1])
254 Added: self.thickness = thickness
255 Added: return None
256 Added:
257 Added:
258 Added: class Stringer(Airfoil):
259 Added: """Contains the coordinates of all stringers."""
260 Added:
261 Added: def __init__(self):
262 Added: super().__init__()
263 Added: self.x_start = []
264 Added: self.x_end = []
265 Added: self.thickness = float()
266 Added: self.z_start = []
267 Added: self.z_end = []
268 Added: self.area = float()
269 Added:
270 Added: def add_coord(self, airfoil,
271 Added: stringer_u_1, stringer_u_2,
272 Added: stringer_l_1, stringer_l_2):
273 Added: """Add equally distributed stringers to four airfoil locations
274 Added: (upper nose, lower nose, upper surface, lower surface).
275 Added:
276 Added: Parameters:
277 Added: airfoil_coord: packed airfoil coordinates
278 Added: spar_coord: packed spar coordinates
279 Added: stringer_u_1: upper nose number of stringers
280 Added: stringer_u_2: upper surface number of stringers
281 Added: stringer_l_1: lower nose number of stringers
282 Added: stringer_l_2: lower surface number of stringers
283 Added:
284 Added: Returns:
285 Added: None
286 Added: """
287 Added:
288 Added: # Find distance between leading edge and first upper stringer
289 Added: interval = airfoil.spar.x[0][0] / (stringer_u_1 + 1)
290 Added: # initialise first self.stringer_x at first interval
291 Added: x = interval
292 Added: # Add upper stringers from leading edge until first spar.
293 Added: for _ in range(0, stringer_u_1):
294 Added: # Index of the first value of airfoil.x > x
295 Added: i = bi.bisect_left(airfoil.x, x)
296 Added: self.x.append(airfoil.x[i])
297 Added: self.z.append(airfoil.z[i])
298 Added: x += interval
299 Added: # Add upper stringers from first spar until last spar
300 Added: # TODO: stringer placement if only one spar is created
301 Added: interval = (airfoil.spar.x[-1][0]
302 Added: - airfoil.spar.x[0][0]) / (stringer_u_2 + 1)
303 Added: x = interval + airfoil.spar.x[0][0]
304 Added: for _ in range(0, stringer_u_2):
305 Added: i = bi.bisect_left(airfoil.x, x)
306 Added: self.x.append(airfoil.x[i])
307 Added: self.z.append(airfoil.z[i])
308 Added: x += interval
309 Added:
310 Added: # Find distance between leading edge and first lower stringer
311 Added: interval = airfoil.spar.x[0][1] / (stringer_l_1 + 1)
312 Added: x = interval
313 Added: # Add lower stringers from leading edge until first spar.
314 Added: for _ in range(0, stringer_l_1):
315 Added: i = bi.bisect_left(airfoil.x[::-1], x)
316 Added: self.x.append(airfoil.x[-i])
317 Added: self.z.append(airfoil.z[-i])
318 Added: x += interval
319 Added: # Add lower stringers from first spar until last spar
320 Added: interval = (airfoil.spar.x[-1][1]
321 Added: - airfoil.spar.x[0][1]) / (stringer_l_2 + 1)
322 Added: x = interval + airfoil.spar.x[0][1]
323 Added: for _ in range(0, stringer_l_2):
324 Added: i = bi.bisect_left(airfoil.x[::-1], x)
325 Added: self.x.append(airfoil.x[-i])
326 Added: self.z.append(airfoil.z[-i])
327 Added: x += interval
328 Added: return None
329 Added:
330 Added: def add_area(self, area):
331 Added: self.area = area
332 Added: return None
333 Added:
334 Added: def add_mass(self, mass):
335 Added: self.mass = len(self.x) * mass + len(self.x) * mass
336 Added: return None
337 Added:
338 Added: def add_webs(self, thickness):
339 Added: """Add webs to stringers."""
340 Added: for _ in range(len(self.x) // 2):
341 Added: self.x_start.append(self.x[_])
342 Added: self.x_end.append(self.x[_ + 1])
343 Added: self.z_start.append(self.z[_])
344 Added: self.z_end.append(self.z[_ + 1])
345 Added: self.thickness = thickness
346 Added: return None
347 Added:
348 Added: def info_print(self, round):
349 Added: super().info_print(round)
350 Added: print('Stringer Area:\n', np.around(self.area, round))
351 Added: return None
352 Added:
353 Added:
354 Added: def plot_geom(airfoil, view: False):
355 Added: """This function plots the airfoil's + sub-components' geometry."""
356 Added: fig, ax = plt.subplots()
357 Added:
358 Added: # Plot chord
359 Added: x = [0, airfoil.chord]
360 Added: y = [0, 0]
361 Added: ax.plot(x, y, linewidth='1')
362 Added: # Plot quarter chord
363 Added: ax.plot(airfoil.chord / 4, 0,
364 Added: '.', color='g', markersize=24,
365 Added: label='Quarter-chord')
366 Added: # Plot mean camber line
367 Added: ax.plot(airfoil.x_c, airfoil.z_c,
368 Added: '-.', color='r', linewidth='2',
369 Added: label='Mean camber line')
370 Added: # Plot airfoil surfaces
371 Added: ax.plot(airfoil.x, airfoil.z,
372 Added: color='b', linewidth='1')
373 Added:
374 Added: # Plot spars
375 Added: try:
376 Added: for _ in range(len(airfoil.spar.x)):
377 Added: x = (airfoil.spar.x[_])
378 Added: y = (airfoil.spar.z[_])
379 Added: ax.plot(x, y, '-', color='y', linewidth='4')
380 Added: except AttributeError:
381 Added: print('No spars to plot.')
382 Added: # Plot stringers
383 Added: try:
384 Added: for _ in range(0, len(airfoil.stringer.x)):
385 Added: x = airfoil.stringer.x[_]
386 Added: y = airfoil.stringer.z[_]
387 Added: ax.plot(x, y, '.', color='y', markersize=12)
388 Added: except AttributeError:
389 Added: print('No stringers to plot.')
390 Added:
391 Added: # Graph formatting
392 Added: plot_bound = max(airfoil.x)
393 Added: ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil',
394 Added: xlabel='X axis',
395 Added: xlim=[- 0.10 * plot_bound, 1.10 * plot_bound],
396 Added: ylabel='Z axis',
397 Added: ylim=[- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)])
398 Added:
399 Added: plt.grid(axis='both', linestyle=':', linewidth=1)
400 Added: plt.gca().set_aspect('equal', adjustable='box')
401 Added: plt.gca().legend(bbox_to_anchor=(1, 1),
402 Added: bbox_transform=plt.gcf().transFigure)
403 Added:
404 Added: if view == True:
405 Added: plt.show()
406 Added: else:
407 Added: pass
408 Added: return fig, ax
409 Added:
410 Added:
411 Added: def main():
412 Added: return None
413 Added:
414 Added:
415 Added: if __name__ == '__main__':
416 Added: main()
tools/evaluator.py
index 00000000..ad1f8732 000000..100644
@@ -0,0 +1,288 @@
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: The evaluator.py module contains a single Evaluator class,
17 Added: which knows all the attributes of a specified Airfoil instance,
18 Added: and contains functions to analyse the airfoil's geometrical
19 Added: & structural properties.
20 Added: """
21 Added:
22 Added: import sys
23 Added: import os.path
24 Added: import numpy as np
25 Added: from math import sqrt
26 Added: import matplotlib.pyplot as plt
27 Added:
28 Added:
29 Added: class Evaluator:
30 Added: """Performs structural evaluations for the airfoil passed as argument."""
31 Added:
32 Added: def __init__(self, airfoil):
33 Added: # Evaluator knows all geometrical info from evaluated airfoil
34 Added: self.airfoil = airfoil
35 Added: self.spar = airfoil.spar
36 Added: self.stringer = airfoil.stringer
37 Added: # Global dimensions
38 Added: self.chord = airfoil.chord
39 Added: self.semi_span = airfoil.semi_span
40 Added: # Mass & spanwise distribution
41 Added: self.mass_total = float(airfoil.mass
42 Added: + airfoil.spar.mass
43 Added: + airfoil.stringer.mass)
44 Added: self.mass_dist = []
45 Added: # Lift
46 Added: self.lift_rectangular = []
47 Added: self.lift_elliptical = []
48 Added: self.lift_total = []
49 Added: # Drag
50 Added: self.drag = []
51 Added: # centroid
52 Added: self.centroid = []
53 Added: # Inertia terms:
54 Added: self.I_ = {'x': 0, 'z': 0, 'xz': 0}
55 Added:
56 Added: def __str__(self):
57 Added: return type(self).__name__
58 Added:
59 Added: def info_print(self, round):
60 Added: """Print all the component's evaluated data to the terminal."""
61 Added: name = ' EVALUATOR DATA FOR {} '.format(str(self).upper())
62 Added: num_of_dashes = len(name)
63 Added: print(num_of_dashes * '-')
64 Added: print(name)
65 Added: for k, v in self.__dict__.items():
66 Added: if type(v) != list:
67 Added: print('{}:\n'.format(k), v)
68 Added: print(num_of_dashes * '-')
69 Added: for k, v in self.__dict__.items():
70 Added: if type(v) == list:
71 Added: print('{}:\n'.format(k), np.around(v, round))
72 Added: return None
73 Added:
74 Added: def info_save(self, save_path, number):
75 Added: """Save all the object's coordinates (must be full path)."""
76 Added: file_name = 'airfoil_{}_eval.txt'.format(number)
77 Added: full_path = os.path.join(save_path, file_name)
78 Added: try:
79 Added: with open(full_path, 'w') as sys.stdout:
80 Added: self.info_print(6)
81 Added: # This line required to reset behavior of sys.stdout
82 Added: sys.stdout = sys.__stdout__
83 Added: print('Successfully wrote to file {}'.format(full_path))
84 Added: except IOError:
85 Added: print(
86 Added: 'Unable to write {} to specified directory.\n'.format(
87 Added: file_name), 'Was the full path passed to the function?')
88 Added: return None
89 Added:
90 Added: # All these functions take integer arguments and return lists.
91 Added:
92 Added: def get_lift_rectangular(self, lift):
93 Added: L_prime = [lift / (self.semi_span * 2) for x in range(self.semi_span)]
94 Added: return L_prime
95 Added:
96 Added: def get_lift_elliptical(self, L_0):
97 Added: L_prime = [
98 Added: L_0 / (self.semi_span * 2) * sqrt(1 - (y / self.semi_span)**2)
99 Added: for y in range(self.semi_span)
100 Added: ]
101 Added: return L_prime
102 Added:
103 Added: def get_lift_total(self):
104 Added: F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2
105 Added: for _ in range(len(self.lift_rectangular))]
106 Added: return F_z
107 Added:
108 Added: def get_mass_distribution(self, total_mass):
109 Added: F_z = [total_mass / self.semi_span for x in range(0, self.semi_span)]
110 Added: return F_z
111 Added:
112 Added: def get_drag(self, drag):
113 Added: # Transform semi-span integer into list
114 Added: semi_span = [x for x in range(0, self.semi_span)]
115 Added:
116 Added: # Drag increases after 80% of the semi_span
117 Added: cutoff = round(0.8 * self.semi_span)
118 Added:
119 Added: # Drag increases by 25% after 80% of the semi_span
120 Added: F_x = [drag for x in semi_span[0:cutoff]]
121 Added: F_x.extend([1.25 * drag for x in semi_span[cutoff:]])
122 Added: return F_x
123 Added:
124 Added: def get_centroid(self):
125 Added: """Return the coordinates of the centroid."""
126 Added: stringer_area = self.stringer.area
127 Added: cap_area = self.spar.cap_area
128 Added:
129 Added: caps_x = [value for spar in self.spar.x for value in spar]
130 Added: caps_z = [value for spar in self.spar.z for value in spar]
131 Added: stringers_x = self.stringer.x
132 Added: stringers_z = self.stringer.z
133 Added:
134 Added: denominator = float(len(caps_x) * cap_area
135 Added: + len(stringers_x) * stringer_area)
136 Added:
137 Added: centroid_x = float(sum([x * cap_area for x in caps_x])
138 Added: + sum([x * stringer_area for x in stringers_x]))
139 Added: centroid_x = centroid_x / denominator
140 Added:
141 Added: centroid_z = float(sum([z * cap_area for z in caps_z])
142 Added: + sum([z * stringer_area for z in stringers_z]))
143 Added: centroid_z = centroid_z / denominator
144 Added:
145 Added: return (centroid_x, centroid_z)
146 Added:
147 Added: def get_inertia_terms(self):
148 Added: """Obtain all inertia terms."""
149 Added: stringer_area = self.stringer.area
150 Added: cap_area = self.spar.cap_area
151 Added:
152 Added: # Adds upper and lower components' coordinates to list
153 Added: x_stringers = self.stringer.x
154 Added: z_stringers = self.stringer.z
155 Added: x_spars = self.spar.x[:][0] + self.spar.x[:][1]
156 Added: z_spars = self.spar.z[:][0] + self.spar.z[:][1]
157 Added: stringer_count = range(len(x_stringers))
158 Added: spar_count = range(len(self.spar.x))
159 Added:
160 Added: # I_x is the sum of the contributions of the spar caps and stringers
161 Added: # TODO: replace list indices with dictionary value
162 Added: I_x = sum([cap_area * (z_spars[i] - self.centroid[1])**2
163 Added: for i in spar_count])
164 Added: I_x += sum([stringer_area * (z_stringers[i] - self.centroid[1])**2
165 Added: for i in stringer_count])
166 Added:
167 Added: I_z = sum([cap_area * (x_spars[i] - self.centroid[0])**2
168 Added: for i in spar_count])
169 Added: I_z += sum([stringer_area * (x_stringers[i] - self.centroid[0])**2
170 Added: for i in stringer_count])
171 Added:
172 Added: I_xz = sum([cap_area * (x_spars[i] - self.centroid[0])
173 Added: * (z_spars[i] - self.centroid[1])
174 Added: for i in spar_count])
175 Added: I_xz += sum([stringer_area * (x_stringers[i] - self.centroid[0])
176 Added: * (z_stringers[i] - self.centroid[1])
177 Added: for i in stringer_count])
178 Added: return (I_x, I_z, I_xz)
179 Added:
180 Added: def get_dx(self, component):
181 Added: return [x - self.centroid[0] for x in component.x_start]
182 Added:
183 Added: def get_dz(self, component):
184 Added: return [x - self.centroid[1] for x in component.x_start]
185 Added:
186 Added: def get_dP(self, xDist, zDist, V_x, V_z, area):
187 Added: I_x = self.I_['x']
188 Added: I_z = self.I_['z']
189 Added: I_xz = self.I_['xz']
190 Added: denom = float(I_x * I_z - I_xz ** 2)
191 Added: z = float()
192 Added: for _ in range(len(xDist)):
193 Added: z += float(-area * xDist[_] * (I_x * V_x - I_xz * V_z)
194 Added: / denom
195 Added: - area * zDist[_] * (I_z * V_z - I_xz * V_x)
196 Added: / denom)
197 Added: return z
198 Added:
199 Added: def analysis(self, V_x, V_z):
200 Added: """Perform all analysis calculations and store in class instance."""
201 Added: self.drag = self.get_drag(10)
202 Added: self.lift_rectangular = self.get_lift_rectangular(13.7)
203 Added: self.lift_elliptical = self.get_lift_elliptical(15)
204 Added: self.lift_total = self.get_lift_total()
205 Added: self.mass_dist = self.get_mass_distribution(self.mass_total)
206 Added: self.centroid = self.get_centroid()
207 Added: self.I_['x'] = self.get_inertia_terms()[0]
208 Added: self.I_['z'] = self.get_inertia_terms()[1]
209 Added: self.I_['xz'] = self.get_inertia_terms()[2]
210 Added: spar_dx = self.get_dx(self.spar)
211 Added: spar_dz = self.get_dz(self.spar)
212 Added: self.spar.dP_x = self.get_dP(spar_dx, spar_dz,
213 Added: V_x, 0, self.spar.cap_area)
214 Added: self.spar.dP_z = self.get_dP(spar_dx, spar_dz,
215 Added: 0, V_z, self.spar.cap_area)
216 Added: return None
217 Added:
218 Added:
219 Added: def plot_geom(evaluator):
220 Added: """This function plots analysis results over the airfoil's geometry."""
221 Added: # Plot chord
222 Added: x_chord = [0, evaluator.chord]
223 Added: y_chord = [0, 0]
224 Added: plt.plot(x_chord, y_chord, linewidth='1')
225 Added: # Plot quarter chord
226 Added: plt.plot(evaluator.chord / 4, 0,
227 Added: '.', color='g', markersize=24, label='Quarter-chord')
228 Added: # Plot airfoil surfaces
229 Added: x = [0.98 * x for x in evaluator.airfoil.x]
230 Added: y = [0.98 * z for z in evaluator.airfoil.z]
231 Added: plt.fill(x, y, color='w', linewidth='1', fill=False)
232 Added: x = [1.02 * x for x in evaluator.airfoil.x]
233 Added: y = [1.02 * z for z in evaluator.airfoil.z]
234 Added: plt.fill(x, y, color='b', linewidth='1', fill=False)
235 Added:
236 Added: # Plot spars
237 Added: try:
238 Added: for _ in range(len(evaluator.spar.x)):
239 Added: x = (evaluator.spar.x[_])
240 Added: y = (evaluator.spar.z[_])
241 Added: plt.plot(x, y, '-', color='b')
242 Added: except AttributeError:
243 Added: print('No spars to plot.')
244 Added: # Plot stringers
245 Added: try:
246 Added: for _ in range(0, len(evaluator.stringer.x)):
247 Added: x = evaluator.stringer.x[_]
248 Added: y = evaluator.stringer.z[_]
249 Added: plt.plot(x, y, '.', color='y', markersize=12)
250 Added: except AttributeError:
251 Added: print('No stringers to plot.')
252 Added:
253 Added: # Plot centroid
254 Added: x = evaluator.centroid[0]
255 Added: y = evaluator.centroid[1]
256 Added: plt.plot(x, y, '.', color='r', markersize=24, label='centroid')
257 Added:
258 Added: # Graph formatting
259 Added: plt.xlabel('X axis')
260 Added: plt.ylabel('Z axis')
261 Added:
262 Added: plot_bound = max(evaluator.airfoil.x)
263 Added: plt.xlim(-0.10 * plot_bound, 1.10 * plot_bound)
264 Added: plt.ylim(-(1.10 * plot_bound / 2), (1.10 * plot_bound / 2))
265 Added: plt.gca().set_aspect('equal', adjustable='box')
266 Added: plt.gca().legend()
267 Added: plt.grid(axis='both', linestyle=':', linewidth=1)
268 Added: plt.show()
269 Added: return None
270 Added:
271 Added:
272 Added: def plot_lift(evaluator):
273 Added: x = range(evaluator.semi_span)
274 Added: y_1 = evaluator.lift_rectangular
275 Added: y_2 = evaluator.lift_elliptical
276 Added: y_3 = evaluator.lift_total
277 Added: plt.plot(x, y_1, '.', color='b', markersize=4, label='Rectangular lift')
278 Added: plt.plot(x, y_2, '.', color='g', markersize=4, label='Elliptical lift')
279 Added: plt.plot(x, y_3, '.', color='r', markersize=4, label='Total lift')
280 Added:
281 Added: # Graph formatting
282 Added: plt.xlabel('Semi-span location')
283 Added: plt.ylabel('Lift')
284 Added:
285 Added: plt.gca().legend()
286 Added: plt.grid(axis='both', linestyle=':', linewidth=1)
287 Added: plt.show()
288 Added: return None
tools/generator.py
index 00000000..02138283 000000..100644
@@ -0,0 +1,64 @@
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: The generator.py module contains a single Population class,
17 Added: which represents a collection of randomized airfoils.
18 Added: """
19 Added:
20 Added: from tools import creator
21 Added:
22 Added:
23 Added: def default_airfoil():
24 Added: """Generate the default airfoil."""
25 Added: airfoil = creator.Airfoil.from_dimensions(100, 200)
26 Added: airfoil.add_naca(2412)
27 Added: airfoil.add_mass(10)
28 Added:
29 Added: airfoil.spar = creator.Spar()
30 Added: airfoil.spar.add_coord(airfoil, 0.23)
31 Added: airfoil.spar.add_coord(airfoil, 0.57)
32 Added: airfoil.spar.add_spar_caps(0.3)
33 Added: airfoil.spar.add_mass(10)
34 Added: airfoil.spar.add_webs(0.4)
35 Added:
36 Added: airfoil.stringer = creator.Stringer()
37 Added: airfoil.stringer.add_coord(airfoil, 3, 6, 5, 4)
38 Added: airfoil.stringer.add_area(0.1)
39 Added: airfoil.stringer.add_mass(5)
40 Added: airfoil.stringer.add_webs(0.1)
41 Added:
42 Added: return airfoil
43 Added:
44 Added:
45 Added: class Population(creator.Airfoil):
46 Added: """Collection of random airfoils."""
47 Added:
48 Added: def __init__(self, size):
49 Added: af = creator.Airfoil
50 Added: # print(af)
51 Added: self.size = size
52 Added: self.gen_number = 0 # incremented for every generation
53 Added:
54 Added: def mutate(self, prob_mt):
55 Added: """Randomly mutate the genes of prob_mt % of the population."""
56 Added:
57 Added: def crossover(self, prob_cx):
58 Added: """Combine the genes of prob_cx % of the population."""
59 Added:
60 Added: def reproduce(self, prob_rp):
61 Added: """Pass on the genes of the fittest prob_rp % of the population."""
62 Added:
63 Added: def fitness():
64 Added: """Rate the fitness of an individual on a relative scale (0-100)"""