View raw

1 """ 2 The wing.py module contains class definitions for and various components 3 we add to an airfoil (spars, stringers, and ribs). 4 5 Classes: 6 Airfoil: instantiated with class method to provide coordinates to heirs. 7 Spar: inherits from Airfoil. 8 Stringer: also inherits from Airfoil. 9 10 Functions: 11 plot_geom(airfoil): generates a 2D plot of the airfoil & any components. 12 """ 13 14 import logging 15 import numpy as np 16 from math import sin, cos, atan 17 import bisect as bi 18 import matplotlib.pyplot as plt 19 20 from aircraftstudio.creator import base 21 import resources.materials as mt 22 23 24 class Airfoil(base.Component): 25 """This class represents a single NACA airfoil. 26 27 The coordinates are saved as two np.arrays 28 for the x- and z-coordinates. The coordinates start at 29 the leading edge, travel over the airfoil's upper edge, 30 then loop back to the leading edge via the lower edge. 31 32 This method was chosen for easier future exports 33 to 3D CAD packages like SolidWorks, which can import such 34 geometry as coordinates written in a CSV file. 35 """ 36 def __init__(self, 37 parent, 38 name, 39 chord=68, 40 semi_span=150, 41 material=mt.aluminium): 42 super().__init__(parent, name) 43 parent.wing = self 44 if chord > 20: 45 self.chord = chord 46 else: 47 self.chord = 20 48 logging.debug('Chord too small, using minimum value of 20.') 49 parent 50 self.semi_span = semi_span 51 self.material = material 52 self.spars = [] 53 self.stringers = [] 54 55 def add_naca(self, naca_num=2412): 56 """Generate surface geometry for a NACA airfoil. 57 58 The nested functions perform the required steps to generate geometry, 59 and can be called to solve the geometry y-coordinate for any 'x' input. 60 Equation coefficients were retrieved from Wikipedia.org. 61 62 Parameters: 63 naca_num: 4-digit NACA wing 64 65 Return: 66 None 67 """ 68 self.naca_num = naca_num 69 # Variables extracted from naca_num argument passed to the function 70 m = int(str(naca_num)[0]) / 100 71 p = int(str(naca_num)[1]) / 10 72 t = int(str(naca_num)[2:]) / 100 73 # x-coordinate of maximum camber 74 p_c = p * self.chord 75 76 def get_camber(x): 77 """ 78 Returns camber z-coordinate from 1 'x' along the airfoil chord. 79 """ 80 z_c = float() 81 if 0 <= x < p_c: 82 z_c = (m / (p**2)) * (2 * p * (x / self.chord) - 83 (x / self.chord)**2) 84 elif p_c <= x <= self.chord: 85 z_c = (m / 86 ((1 - p)**2)) * ((1 - 2 * p) + 2 * p * 87 (x / self.chord) - (x / self.chord)**2) 88 return (z_c * self.chord) 89 90 def get_thickness(x): 91 """Return thickness from 1 'x' along the airfoil chord.""" 92 x = 0 if x < 0 else x 93 z_t = 5 * t * self.chord * (+0.2969 * 94 (x / self.chord)**0.5 - 0.1260 * 95 (x / self.chord)**1 - 0.3516 * 96 (x / self.chord)**2 + 0.2843 * 97 (x / self.chord)**3 - 0.1015 * 98 (x / self.chord)**4) 99 return z_t 100 101 def get_theta(x): 102 dz_c = float() 103 if 0 <= x < p_c: 104 dz_c = ((2 * m) / p**2) * (p - x / self.chord) 105 elif p_c <= x <= self.chord: 106 dz_c = (2 * m) / ((1 - p)**2) * (p - x / self.chord) 107 108 theta = atan(dz_c) 109 return theta 110 111 def get_coord_u(x): 112 x = x - get_thickness(x) * sin(get_theta(x)) 113 z = get_camber(x) + get_thickness(x) * cos(get_theta(x)) 114 return (x, z) 115 116 def get_coord_l(x): 117 x = x + get_thickness(x) * sin(get_theta(x)) 118 z = get_camber(x) - get_thickness(x) * cos(get_theta(x)) 119 return (x, z) 120 121 # Densify x-coordinates 10 times for first 1/4 chord length 122 x_chord_25_percent = round(self.chord / 4) 123 x_chord = [i / 10 for i in range(x_chord_25_percent * 10)] 124 x_chord.extend(i for i in range(x_chord_25_percent, self.chord + 1)) 125 # Generate our airfoil skin geometry from previous sub-functions 126 self.x_c = np.array([]) 127 self.z_c = np.array([]) 128 # Upper surface and camber line 129 for x in x_chord: 130 self.x_c = np.append(self.x_c, x) 131 self.z_c = np.append(self.z_c, get_camber(x)) 132 self.x = np.append(self.x, get_coord_u(x)[0]) 133 self.z = np.append(self.z, get_coord_u(x)[1]) 134 # Lower surface 135 for x in x_chord[::-1]: 136 self.x = np.append(self.x, get_coord_l(x)[0]) 137 self.z = np.append(self.z, get_coord_l(x)[1]) 138 return None 139 140 141 class Spar(base.Component): 142 """Contains a single spar's data.""" 143 def __init__(self, parent, name, loc_percent=0.30, material=mt.aluminium): 144 """Set spar location as percent of chord length.""" 145 super().__init__(parent, name) 146 parent.spars.append(self) 147 self.material = material 148 self.cap_area = float() 149 # bi.bisect_left: returns index of first value in parent.x > loc 150 # This ensures that spar geom intersects with airfoil geom. 151 loc = loc_percent * parent.chord 152 # Spar upper coordinates 153 spar_u = bi.bisect_left(parent.x, loc) - 1 154 self.x = np.append(self.x, parent.x[spar_u]) 155 self.z = np.append(self.z, parent.z[spar_u]) 156 # Spar lower coordinates 157 spar_l = bi.bisect_left(parent.x[::-1], loc) 158 self.x = np.append(self.x, parent.x[-spar_l]) 159 self.z = np.append(self.z, parent.z[-spar_l]) 160 return None 161 162 def set_cap_area(self, cap_area): 163 self.cap_area = cap_area 164 return None 165 166 def set_mass(self, mass): 167 self.mass = mass 168 return None 169 170 171 class Stringer(base.Component): 172 """Contains the coordinates of all stringers.""" 173 def __init__(self, 174 parent, 175 name, 176 den_u_1=4, 177 den_u_2=4, 178 den_l_1=4, 179 den_l_2=4): 180 """Add equally distributed stringers to four airfoil locations 181 (upper nose, lower nose, upper surface, lower surface). 182 183 den_u_1: upper nose number of stringers 184 den_u_2: upper surface number of stringers 185 den_l_1: lower nose number of stringers 186 den_l_2: lower surface number of stringers 187 """ 188 super().__init__(parent, name) 189 parent.stringers = self 190 self.x_start = [] 191 self.x_end = [] 192 self.z_start = [] 193 self.z_end = [] 194 self.diameter = float() 195 self.area = float() 196 197 # Find distance between leading edge and first upper stringer 198 # interval = self.parent.spars[0].x[0] / (den_u_1 + 1) 199 interval = 2 200 # initialise first self.stringer_x at first interval 201 x = interval 202 # Add upper stringers from leading edge until first spar. 203 for _ in range(0, den_u_1): 204 # Index of the first value of airfoil.x > x 205 i = bi.bisect_left(self.parent.x, x) 206 self.x = np.append(self.x, self.parent.x[i]) 207 self.z = np.append(self.z, self.parent.z[i]) 208 x += interval 209 # Add upper stringers from first spar until last spar 210 interval = (self.parent.spars[-1].x[0] - 211 self.parent.spars[0].x[0]) / (den_u_2 + 1) 212 x = interval + self.parent.spars[0].x[0] 213 for _ in range(0, den_u_2): 214 i = bi.bisect_left(self.parent.x, x) 215 self.x = np.append(self.x, self.parent.x[i]) 216 self.z = np.append(self.z, self.parent.z[i]) 217 x += interval 218 219 # Find distance between leading edge and first lower stringer 220 interval = self.parent.spars[0].x[1] / (den_l_1 + 1) 221 x = interval 222 # Add lower stringers from leading edge until first spar. 223 for _ in range(0, den_l_1): 224 i = bi.bisect_left(self.parent.x[::-1], x) 225 self.x = np.append(self.x, self.parent.x[-i]) 226 self.z = np.append(self.z, self.parent.z[-i]) 227 x += interval 228 # Add lower stringers from first spar until last spar 229 interval = (self.parent.spars[-1].x[1] - 230 self.parent.spars[0].x[1]) / (den_l_2 + 1) 231 x = interval + self.parent.spars[0].x[1] 232 for _ in range(0, den_l_2): 233 i = bi.bisect_left(self.parent.x[::-1], x) 234 self.x = np.append(self.x, self.parent.x[-i]) 235 self.z = np.append(self.z, self.parent.z[-i]) 236 x += interval 237 return None 238 239 def add_area(self, area): 240 self.area = area 241 return None 242 243 def add_mass(self, mass): 244 self.mass = len(self.x) * mass + len(self.x) * mass 245 return None 246 247 def add_webs(self, thickness): 248 """Add webs to stringers.""" 249 for _ in range(len(self.x) // 2): 250 self.x_start.append(self.x[_]) 251 self.x_end.append(self.x[_ + 1]) 252 self.z_start.append(self.z[_]) 253 self.z_end.append(self.z[_ + 1]) 254 self.thickness = thickness 255 return None 256 257 def info_print(self, round=2): 258 super().info_print(round) 259 print('Stringer Area:\n', np.around(self.area, round)) 260 return None 261 262 263 def plot_geom(airfoil): 264 """This function plots the airfoil's + sub-components' geometry.""" 265 fig, ax = plt.subplots() 266 267 # Plot chord 268 x = [0, airfoil.chord] 269 y = [0, 0] 270 ax.plot(x, y, linewidth='1') 271 # Plot quarter chord 272 ax.plot(airfoil.chord / 4, 273 0, 274 '.', 275 color='g', 276 markersize=24, 277 label='Quarter-chord') 278 # Plot mean camber line 279 ax.plot(airfoil.x_c, 280 airfoil.z_c, 281 '-.', 282 color='r', 283 linewidth='2', 284 label='Mean camber line') 285 # Plot airfoil surfaces 286 ax.plot(airfoil.x, airfoil.z, color='b', linewidth='1') 287 288 try: # Plot spars 289 for spar in airfoil.spars: 290 x = (spar.x) 291 y = (spar.z) 292 ax.plot(x, y, '-', color='y', linewidth='4') 293 except AttributeError: 294 print('No spars to plot.') 295 try: # Plot stringers 296 for i in range(len(airfoil.stringers.x)): 297 x = airfoil.stringers.x[i] 298 y = airfoil.stringers.z[i] 299 ax.plot(x, y, '.', color='y', markersize=12) 300 except AttributeError: 301 print('No stringers to plot.') 302 303 ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil', 304 xlabel='X axis', 305 ylabel='Z axis') 306 307 plt.grid(axis='both', linestyle=':', linewidth=1) 308 plt.gca().set_aspect('equal', adjustable='box') 309 plt.gca().legend(bbox_to_anchor=(1, 1), 310 bbox_transform=plt.gcf().transFigure) 311 plt.show() 312 return fig, ax 313