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