*ARCHIVED* development moved to aircraft-studio.
1
# This file is part of Marius Peter's airfoil analysis package (this program).
2
#
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program. If not, see <https://www.gnu.org/licenses/>.
15
"""
16
The creator. module contains class definitions for coordinates
17
and various components we add to an (,,and).
18
19
Classes:
20
Airfoil: instantiated with class method to provide coordinates to heirs.
21
Spar: inherits from Airfoil.
22
Stringer: also inherits from Airfoil.
23
24
Functions:
25
(): generates a 2D plot of the airfoil & any components.
26
"""
27
28
import sys
29
import os.
30
import numpy as np
31
from math import sin, cos, atan
32
import bisect as bi
33
import matplotlib. as plt
34
35
36
class Airfoil:
37
"""This class represents a single NACA airfoil.
38
39
The coordinates are saved as two lists
40
for the x- and z-coordinates. coordinates start at
41
the leading edge, travel over the airfoil's upper edge,
42
then loop back to the leading edge via the lower edge.
43
44
This method was chosen for easier future exports
45
to 3D CAD packages like SolidWorks, which can import such
46
geometry as coordinates written in a CSV file.
47
"""
48
49
# Defaults
50
chord = 100
51
semi_span = 200
52
53
def __init__(self):
54
# mass and area
55
self. = float()
56
self. = float()
57
# Component material
58
self. = str()
59
# Coordinate
60
self. = []
61
self. = []
62
63
@classmethod
64
def (cls,,):
65
"""Create airfoil from its chord and semi-span."""
66
if chord > 20:
67
cls. = chord
68
else:
69
cls. = 20
70
print('Chord too small, using minimum value of 20.')
71
cls. = semi_span
72
return ()
73
74
def __str__(self):
75
return type(self).__name__
76
77
def (self,):
78
"""Generate surface geometry for a NACA airfoil.
79
80
The nested functions perform the required steps to generate geometry,
81
and can be called to solve the geometry y-coordinate for any 'x' input.
82
Equation coefficients were retrieved from Wikipedia..
83
84
Parameters:
85
naca_num: 4-digit NACA wing
86
87
Return:
88
None
89
"""
90
# Variables extracted from 'naca_num' argument passed to the function
91
self. = naca_num
92
m = int(str()[0]) / 100
93
p = int(str()[1]) / 10
94
t = int(str()[2:]) / 100
95
# x-coordinate of maximum camber
96
p_c = p * self.
97
98
def ():
99
"""
100
Returns camber z-coordinate from 1 'x' along the airfoil chord.
101
"""
102
z_c = float()
103
if 0 <= x < p_c:
104
z_c = (m / (p**2)) * (2 * p * (x / self.) -
105
(x / self.)**2)
106
elif p_c <= x <= self.:
107
z_c = (m /
108
((1 - p)**2)) * ((1 - 2 * p) + 2 * p *
109
(x / self.) - (x / self.)**2)
110
return(*self.)
111
112
def ():
113
"""Return thickness from 1 'x' along the airfoil chord."""
114
x = 0 if x < 0 else x
115
z_t = 5 * t * self. * (+0.2969 *
116
(x / self.)**0.5 - 0.1260 *
117
(x / self.)**1 - 0.3516 *
118
(x / self.)**2 + 0.2843 *
119
(x / self.)**3 - 0.1015 *
120
(x / self.)**4)
121
return z_t
122
123
def ():
124
dz_c = float()
125
if 0 <= x < p_c:
126
dz_c = ((2 * m) / p**2) * (p - x / self.)
127
elif p_c <= x <= self.:
128
dz_c = (2 * m) / ((1 - p)**2) * (p - x / self.)
129
130
theta = ()
131
return theta
132
133
def ():
134
x = x - () * (())
135
z = () + () * (())
136
return(,)
137
138
def ():
139
x = x + () * (())
140
z = () - () * (())
141
return(,)
142
143
# Densify x-coordinates 10 times for first 1/4 chord length
144
x_chord_25_percent = round(self./4)
145
146
x_chord = [i / 10 for i in range(*10)]
147
x_chord.(forinrange(,self.+1))
148
# Reversed list for our lower airfoil coordinate densification
149
x_chord_rev = [i for i in range(self.,,-1)]
150
extend = [i / 10 for i in range(*10,-1,-1)]
151
x_chord_rev.()
152
153
# Generate our airfoil geometry from previous sub-functions.
154
self. = []
155
self. = []
156
for x in x_chord:
157
self..()
158
self..(())
159
self..(()[0])
160
self..(()[1])
161
for x in x_chord_rev:
162
self..(()[0])
163
self..(()[1])
164
return None
165
166
def (self,):
167
self. = mass
168
169
def (self,round):
170
"""Print all the component's coordinates to the terminal."""
171
name = ' CREATOR DATA FOR {} '.(str(self).())
172
num_of_dashes = len()
173
print(*'-')
174
print()
175
for k, v in self.__dict__.():
176
if type() != list:
177
print('{}:\n'.(),)
178
print(*'-')
179
for k, v in self.__dict__.():
180
if type() == list:
181
print('{}:\n'.(),.(,round))
182
return None
183
184
def (self,,):
185
"""Save all the object's coordinates (must be full path)."""
186
file_name = '{}_{}.txt'.(str(self).(),)
187
full_path = os..(,)
188
try:
189
with open(,'w') as sys.:
190
self.(6)
191
# This line required to reset behavior of sys.stdout
192
sys. = sys.
193
print('Successfully wrote to file {}'.())
194
except IOError:
195
print(
196
'Unable to write {} to specified directory.\n'.(
197
),'Was the full path passed to the function?')
198
return None
199
200
201
class ():
202
"""Contains a single spar's location."""
203
def __init__(self):
204
super().__init__()
205
self. = []
206
self. = []
207
self. = float()
208
self. = []
209
self. = []
210
211
def (self,,):
212
"""Add a single spar at the % chord location given to function.
213
214
Parameters:
215
airfoil: gives the spar access to airfoil's coordinates.
216
x_loc_percent: spar's location as a % of total chord length.
217
218
Return:
219
None
220
"""
221
222
# Scaled spar location with regards to chord
223
loc = x_loc_percent * self.
224
# bi.bisect_left: returns index of first value in airfoil.x > loc
225
# This ensures that spar geom intersects with airfoil geom.
226
# Spar upper coordinates
227
spar_x = bi.(.,) - 1
228
x = [airfoil.[]]
229
z = [airfoil.[]]
230
# Spar lower coordinates
231
spar_x = bi.(.[::-1],)
232
x += [airfoil.[-]]
233
z += [airfoil.[-]]
234
self..()
235
self..()
236
return None
237
238
def (self,):
239
self. = spar_cap_area
240
return None
241
242
def (self,):
243
self. = len(self.) * mass
244
return None
245
246
def (self,):
247
"""Add webs to spars."""
248
for _ in range(len(self.)):
249
self..(self.[][0])
250
self..(self.[][1])
251
self..(self.[][0])
252
self..(self.[][1])
253
self. = thickness
254
return None
255
256
257
class ():
258
"""Contains the coordinates of all stringers."""
259
def __init__(self):
260
super().__init__()
261
self. = []
262
self. = []
263
self. = float()
264
self. = []
265
self. = []
266
self. = float()
267
268
def (self,,,,,
269
):
270
"""Add equally distributed stringers to four airfoil locations
271
(upper nose, lower nose, upper surface, lower surface).
272
273
Parameters:
274
airfoil_coord: packed airfoil coordinates
275
spar_coord: packed spar coordinates
276
stringer_u_1: upper nose number of stringers
277
stringer_u_2: upper surface number of stringers
278
stringer_l_1: lower nose number of stringers
279
stringer_l_2: lower surface number of stringers
280
281
Returns:
282
None
283
"""
284
285
# Find distance between leading edge and first upper stringer
286
interval = airfoil..[0][0] / (stringer_u_1 + 1)
287
# initialise first self.stringer_x at first interval
288
x = interval
289
# Add upper stringers from leading edge until first spar.
290
for _ in range(0,):
291
# Index of the first value of airfoil.x > x
292
i = bi.(.,)
293
self..(.[])
294
self..(.[])
295
x += interval
296
# Add upper stringers from first spar until last spar
297
# TODO: stringer placement if only one spar is created
298
interval = (airfoil..[-1][0] -
299
airfoil..[0][0]) / (stringer_u_2 + 1)
300
x = interval + airfoil..[0][0]
301
for _ in range(0,):
302
i = bi.(.,)
303
self..(.[])
304
self..(.[])
305
x += interval
306
307
# Find distance between leading edge and first lower stringer
308
interval = airfoil..[0][1] / (stringer_l_1 + 1)
309
x = interval
310
# Add lower stringers from leading edge until first spar.
311
for _ in range(0,):
312
i = bi.(.[::-1],)
313
self..(.[-])
314
self..(.[-])
315
x += interval
316
# Add lower stringers from first spar until last spar
317
interval = (airfoil..[-1][1] -
318
airfoil..[0][1]) / (stringer_l_2 + 1)
319
x = interval + airfoil..[0][1]
320
for _ in range(0,):
321
i = bi.(.[::-1],)
322
self..(.[-])
323
self..(.[-])
324
x += interval
325
return None
326
327
def (self,):
328
self. = area
329
return None
330
331
def (self,):
332
self. = len(self.) * mass + len(self.) * mass
333
return None
334
335
def (self,):
336
"""Add webs to stringers."""
337
for _ in range(len(self.)//2):
338
self..(self.[])
339
self..(self.[+1])
340
self..(self.[])
341
self..(self.[+1])
342
self. = thickness
343
return None
344
345
def (self,round):
346
super().(round)
347
print('Stringer Area:\n',.(self.,round))
348
return None
349
350
351
def (,:False):
352
"""This function plots the airfoil's + sub-components' geometry."""
353
fig, ax = plt.()
354
355
# Plot chord
356
x = [0, airfoil.]
357
y = [0, 0]
358
ax.(,,linewidth='1')
359
# Plot quarter chord
360
ax.(./4,
361
0,
362
'.',
363
color='g',
364
markersize=24,
365
label='Quarter-chord')
366
# Plot mean camber line
367
ax.(.,
368
.,
369
'-.',
370
color='r',
371
linewidth='2',
372
label='Mean camber line')
373
# Plot airfoil surfaces
374
ax.(.,.,color='b',linewidth='1')
375
376
# Plot spars
377
try:
378
for _ in range(len(..)):
379
x = (airfoil..[])
380
y = (airfoil..[])
381
ax.(,,'-',color='y',linewidth='4')
382
except AttributeError:
383
print('No spars to plot.')
384
# Plot stringers
385
try:
386
for _ in range(0,len(..)):
387
x = airfoil..[]
388
y = airfoil..[]
389
ax.(,,'.',color='y',markersize=12)
390
except AttributeError:
391
print('No stringers to plot.')
392
393
# Graph formatting
394
plot_bound = max(.)
395
ax.(title='NACA '+str(.)+' airfoil',
396
xlabel='X axis',
397
xlim=[-0.10*,1.10*],
398
ylabel='Z axis',
399
ylim=[-(1.10*/2),(1.10*/2)])
400
401
plt.(axis='both',linestyle=':',linewidth=1)
402
plt.().('equal',adjustable='box')
403
plt.().(bbox_to_anchor=(1,1),
404
bbox_transform=.().)
405
406
if view is True:
407
plt.()
408
else:
409
pass
410
return fig, ax
411
412
413
def ():
414
return None
415
416
417
if __name__ == '__main__':
418
()
419