View raw

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 evaluator.py module contains a single Evaluator class, 17 which knows all the attributes of a specified Airfoil instance, 18 and contains functions to analyse the airfoil's geometrical 19 & structural properties. 20 """ 21 22 import sys 23 import os.path 24 import numpy as np 25 from math import sqrt 26 import matplotlib.pyplot as plt 27 28 29 class Evaluator: 30 """Performs structural evaluations for the airfoil passed as argument.""" 31 32 def __init__(self, airfoil): 33 # Evaluator knows all geometrical info from evaluated airfoil 34 self.airfoil = airfoil 35 self.spar = airfoil.spar 36 self.stringer = airfoil.stringer 37 # Global dimensions 38 self.chord = airfoil.chord 39 self.semi_span = airfoil.semi_span 40 # Mass & spanwise distribution 41 self.mass_total = float(airfoil.mass 42 + airfoil.spar.mass 43 + airfoil.stringer.mass) 44 self.mass_dist = [] 45 # Lift 46 self.lift_rectangular = [] 47 self.lift_elliptical = [] 48 self.lift_total = [] 49 # Drag 50 self.drag = [] 51 # centroid 52 self.centroid = [] 53 # Inertia terms: 54 self.I_ = {'x': 0, 'z': 0, 'xz': 0} 55 56 def __str__(self): 57 return type(self).__name__ 58 59 def info_print(self, round): 60 """Print all the component's evaluated data to the terminal.""" 61 name = ' EVALUATOR DATA FOR {} '.format(str(self).upper()) 62 num_of_dashes = len(name) 63 print(num_of_dashes * '-') 64 print(name) 65 for k, v in self.__dict__.items(): 66 if type(v) != list: 67 print('{}:\n'.format(k), v) 68 print(num_of_dashes * '-') 69 for k, v in self.__dict__.items(): 70 if type(v) == list: 71 print('{}:\n'.format(k), np.around(v, round)) 72 return None 73 74 def info_save(self, save_path, number): 75 """Save all the object's coordinates (must be full path).""" 76 file_name = 'airfoil_{}_eval.txt'.format(number) 77 full_path = os.path.join(save_path, file_name) 78 try: 79 with open(full_path, 'w') as sys.stdout: 80 self.info_print(6) 81 # This line required to reset behavior of sys.stdout 82 sys.stdout = sys.__stdout__ 83 print('Successfully wrote to file {}'.format(full_path)) 84 except IOError: 85 print( 86 'Unable to write {} to specified directory.\n'.format( 87 file_name), 'Was the full path passed to the function?') 88 return None 89 90 # All these functions take integer arguments and return lists. 91 92 def get_lift_rectangular(self, lift): 93 L_prime = [lift / (self.semi_span * 2) for x in range(self.semi_span)] 94 return L_prime 95 96 def get_lift_elliptical(self, L_0): 97 L_prime = [ 98 L_0 / (self.semi_span * 2) * sqrt(1 - (y / self.semi_span)**2) 99 for y in range(self.semi_span) 100 ] 101 return L_prime 102 103 def get_lift_total(self): 104 F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2 105 for _ in range(len(self.lift_rectangular))] 106 return F_z 107 108 def get_mass_distribution(self, total_mass): 109 F_z = [total_mass / self.semi_span for x in range(0, self.semi_span)] 110 return F_z 111 112 def get_drag(self, drag): 113 # Transform semi-span integer into list 114 semi_span = [x for x in range(0, self.semi_span)] 115 116 # Drag increases after 80% of the semi_span 117 cutoff = round(0.8 * self.semi_span) 118 119 # Drag increases by 25% after 80% of the semi_span 120 F_x = [drag for x in semi_span[0:cutoff]] 121 F_x.extend([1.25 * drag for x in semi_span[cutoff:]]) 122 return F_x 123 124 def get_centroid(self): 125 """Return the coordinates of the centroid.""" 126 stringer_area = self.stringer.area 127 cap_area = self.spar.cap_area 128 129 caps_x = [value for spar in self.spar.x for value in spar] 130 caps_z = [value for spar in self.spar.z for value in spar] 131 stringers_x = self.stringer.x 132 stringers_z = self.stringer.z 133 134 denominator = float(len(caps_x) * cap_area 135 + len(stringers_x) * stringer_area) 136 137 centroid_x = float(sum([x * cap_area for x in caps_x]) 138 + sum([x * stringer_area for x in stringers_x])) 139 centroid_x = centroid_x / denominator 140 141 centroid_z = float(sum([z * cap_area for z in caps_z]) 142 + sum([z * stringer_area for z in stringers_z])) 143 centroid_z = centroid_z / denominator 144 145 return (centroid_x, centroid_z) 146 147 def get_inertia_terms(self): 148 """Obtain all inertia terms.""" 149 stringer_area = self.stringer.area 150 cap_area = self.spar.cap_area 151 152 # Adds upper and lower components' coordinates to list 153 x_stringers = self.stringer.x 154 z_stringers = self.stringer.z 155 x_spars = self.spar.x[:][0] + self.spar.x[:][1] 156 z_spars = self.spar.z[:][0] + self.spar.z[:][1] 157 stringer_count = range(len(x_stringers)) 158 spar_count = range(len(self.spar.x)) 159 160 # I_x is the sum of the contributions of the spar caps and stringers 161 # TODO: replace list indices with dictionary value 162 I_x = sum([cap_area * (z_spars[i] - self.centroid[1])**2 163 for i in spar_count]) 164 I_x += sum([stringer_area * (z_stringers[i] - self.centroid[1])**2 165 for i in stringer_count]) 166 167 I_z = sum([cap_area * (x_spars[i] - self.centroid[0])**2 168 for i in spar_count]) 169 I_z += sum([stringer_area * (x_stringers[i] - self.centroid[0])**2 170 for i in stringer_count]) 171 172 I_xz = sum([cap_area * (x_spars[i] - self.centroid[0]) 173 * (z_spars[i] - self.centroid[1]) 174 for i in spar_count]) 175 I_xz += sum([stringer_area * (x_stringers[i] - self.centroid[0]) 176 * (z_stringers[i] - self.centroid[1]) 177 for i in stringer_count]) 178 return (I_x, I_z, I_xz) 179 180 def get_dx(self, component): 181 return [x - self.centroid[0] for x in component.x_start] 182 183 def get_dz(self, component): 184 return [x - self.centroid[1] for x in component.x_start] 185 186 def get_dP(self, xDist, zDist, V_x, V_z, area): 187 I_x = self.I_['x'] 188 I_z = self.I_['z'] 189 I_xz = self.I_['xz'] 190 denom = float(I_x * I_z - I_xz ** 2) 191 z = float() 192 for _ in range(len(xDist)): 193 z += float(-area * xDist[_] * (I_x * V_x - I_xz * V_z) 194 / denom 195 - area * zDist[_] * (I_z * V_z - I_xz * V_x) 196 / denom) 197 return z 198 199 def analysis(self, V_x, V_z): 200 """Perform all analysis calculations and store in class instance.""" 201 self.drag = self.get_drag(10) 202 self.lift_rectangular = self.get_lift_rectangular(13.7) 203 self.lift_elliptical = self.get_lift_elliptical(15) 204 self.lift_total = self.get_lift_total() 205 self.mass_dist = self.get_mass_distribution(self.mass_total) 206 self.centroid = self.get_centroid() 207 self.I_['x'] = self.get_inertia_terms()[0] 208 self.I_['z'] = self.get_inertia_terms()[1] 209 self.I_['xz'] = self.get_inertia_terms()[2] 210 spar_dx = self.get_dx(self.spar) 211 spar_dz = self.get_dz(self.spar) 212 self.spar.dP_x = self.get_dP(spar_dx, spar_dz, 213 V_x, 0, self.spar.cap_area) 214 self.spar.dP_z = self.get_dP(spar_dx, spar_dz, 215 0, V_z, self.spar.cap_area) 216 return None 217 218 219 def plot_geom(evaluator): 220 """This function plots analysis results over the airfoil's geometry.""" 221 # Plot chord 222 x_chord = [0, evaluator.chord] 223 y_chord = [0, 0] 224 plt.plot(x_chord, y_chord, linewidth='1') 225 # Plot quarter chord 226 plt.plot(evaluator.chord / 4, 0, 227 '.', color='g', markersize=24, label='Quarter-chord') 228 # Plot airfoil surfaces 229 x = [0.98 * x for x in evaluator.airfoil.x] 230 y = [0.98 * z for z in evaluator.airfoil.z] 231 plt.fill(x, y, color='w', linewidth='1', fill=False) 232 x = [1.02 * x for x in evaluator.airfoil.x] 233 y = [1.02 * z for z in evaluator.airfoil.z] 234 plt.fill(x, y, color='b', linewidth='1', fill=False) 235 236 # Plot spars 237 try: 238 for _ in range(len(evaluator.spar.x)): 239 x = (evaluator.spar.x[_]) 240 y = (evaluator.spar.z[_]) 241 plt.plot(x, y, '-', color='b') 242 except AttributeError: 243 print('No spars to plot.') 244 # Plot stringers 245 try: 246 for _ in range(0, len(evaluator.stringer.x)): 247 x = evaluator.stringer.x[_] 248 y = evaluator.stringer.z[_] 249 plt.plot(x, y, '.', color='y', markersize=12) 250 except AttributeError: 251 print('No stringers to plot.') 252 253 # Plot centroid 254 x = evaluator.centroid[0] 255 y = evaluator.centroid[1] 256 plt.plot(x, y, '.', color='r', markersize=24, label='centroid') 257 258 # Graph formatting 259 plt.xlabel('X axis') 260 plt.ylabel('Z axis') 261 262 plot_bound = max(evaluator.airfoil.x) 263 plt.xlim(-0.10 * plot_bound, 1.10 * plot_bound) 264 plt.ylim(-(1.10 * plot_bound / 2), (1.10 * plot_bound / 2)) 265 plt.gca().set_aspect('equal', adjustable='box') 266 plt.gca().legend() 267 plt.grid(axis='both', linestyle=':', linewidth=1) 268 plt.show() 269 return None 270 271 272 def plot_lift(evaluator): 273 x = range(evaluator.semi_span) 274 y_1 = evaluator.lift_rectangular 275 y_2 = evaluator.lift_elliptical 276 y_3 = evaluator.lift_total 277 plt.plot(x, y_1, '.', color='b', markersize=4, label='Rectangular lift') 278 plt.plot(x, y_2, '.', color='g', markersize=4, label='Elliptical lift') 279 plt.plot(x, y_3, '.', color='r', markersize=4, label='Total lift') 280 281 # Graph formatting 282 plt.xlabel('Semi-span location') 283 plt.ylabel('Lift') 284 285 plt.gca().legend() 286 plt.grid(axis='both', linestyle=':', linewidth=1) 287 plt.show() 288 return None 289