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