diff options
-rw-r--r-- | creator.py | 217 | ||||
-rw-r--r-- | evaluator.py | 95 | ||||
-rw-r--r-- | main.py | 26 |
3 files changed, 174 insertions, 164 deletions
@@ -53,14 +53,9 @@ class Coordinates: self.area = float() # Component material self.material = str() - # Upper coordinates - self.x_u = [] - self.z_u = [] - # Lower coordinates - self.x_l = [] - self.z_l = [] - # Coordinates x_u, z_u, x_l, z_l packed in single list - self.coord = [] + # Coordinates + self.x = [] + self.z = [] # The airfoil components know the Coordinates instance's coords global parent @@ -85,10 +80,8 @@ class Coordinates: print('Semi-span:', self.semi_span) print('Mass:', self.mass) print(num_of_dashes * '-') - print('x_u the upper x-coordinates:\n', np.around(self.x_u, round)) - print('z_u the upper z-coordinates:\n', np.around(self.z_u, round)) - print('x_l the lower x-coordinates:\n', np.around(self.x_l, round)) - print('z_l the lower z-coordinates:\n', np.around(self.z_l, round)) + print('x-coordinates:\n', np.around(self.x, round)) + print('z-coordinates:\n', np.around(self.z, round)) return None def info_save(self, save_path, number): @@ -112,7 +105,18 @@ class Coordinates: class Airfoil(Coordinates): - '''This class enables the creation of a single NACA airfoil.''' + ''' + This class enables the creation of a single NACA airfoil. + + Please note: the coordinates are saved as two lists + for the x- and z-coordinates. The coordinates start at + the leading edge, travel over the airfoil's upper edge, + then loop back to the leading edge via the lower edge. + + This method was chosen for easier future exports + to 3D CAD packages like SolidWorks, which can import such + geometry as coordinates written in a CSV file. + ''' def __init__(self): global parent @@ -122,7 +126,7 @@ class Airfoil(Coordinates): self.naca_num = int() # Mean camber line self.x_c = [] - self.y_c = [] + self.z_c = [] def add_naca(self, naca_num): ''' @@ -148,22 +152,22 @@ class Airfoil(Coordinates): def get_camber(x): ''' - Returns camber y-coordinate from 1 'x' along the airfoil chord. + Returns camber z-coordinate from 1 'x' along the airfoil chord. ''' - y_c = float() + z_c = float() if 0 <= x < p_c: - y_c = (m / (p ** 2)) * (2 * p * (x / self.chord) + z_c = (m / (p ** 2)) * (2 * p * (x / self.chord) - (x / self.chord) ** 2) elif p_c <= x <= self.chord: - y_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p) + z_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p) + 2 * p * (x / self.chord) - (x / self.chord) ** 2) - return (y_c * self.chord) + return (z_c * self.chord) def get_thickness(x): - ''' - Returns thickness from 1 'x' along the airfoil chord. - ''' + '''Returns thickness from 1 'x' along the airfoil chord.''' + + x = 0 if x < 0 else x y_t = 5 * t * self.chord * ( + 0.2969 * sqrt(x / self.chord) - 0.1260 * (x / self.chord) @@ -173,37 +177,43 @@ class Airfoil(Coordinates): return y_t def get_theta(x): - dy_c = float() + dz_c = float() if 0 <= x < p_c: - dy_c = ((2 * m) / p ** 2) * (p - x / self.chord) + dz_c = ((2 * m) / p ** 2) * (p - x / self.chord) elif p_c <= x <= self.chord: - dy_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord) - theta = atan(dy_c) + dz_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord) + theta = atan(dz_c) return theta def get_upper_coord(x): - x_u = x - get_thickness(x) * sin(get_theta(x)) - z_u = get_camber(x) + get_thickness(x) * cos(get_theta(x)) - return (x_u, z_u) + x = x - get_thickness(x) * sin(get_theta(x)) + z = get_camber(x) + get_thickness(x) * cos(get_theta(x)) + return (x, z) def get_lower_coord(x): - x_l = x + get_thickness(x) * sin(get_theta(x)) - z_l = get_camber(x) - get_thickness(x) * cos(get_theta(x)) - return (x_l, z_l) + x = x + get_thickness(x) * sin(get_theta(x)) + z = get_camber(x) - get_thickness(x) * cos(get_theta(x)) + return (x, z) # Densify x-coordinates 10 times for first 1/4 chord length x_chord_25_percent = round(self.chord / 4) - x_chord = [x / 10 for x in range(x_chord_25_percent * 10)] - x_chord.extend([x for x in range(x_chord_25_percent, self.chord + 1)]) + + x_chord = [i / 10 for i in range(x_chord_25_percent * 10)] + x_chord.extend(i for i in range(x_chord_25_percent, self.chord + 1)) + # Reversed list for our lower airfoil coordinate densification + x_chord_rev = [i for i in range(self.chord, x_chord_25_percent, -1)] + extend = [i / 10 for i in range(x_chord_25_percent * 10, -1, -1)] + x_chord_rev.extend(extend) # Generate our airfoil geometry from previous sub-functions. for x in x_chord: self.x_c.append(x) - self.y_c.append(get_camber(x)) - self.x_u.append(get_upper_coord(x)[0]) - self.z_u.append(get_upper_coord(x)[1]) - self.x_l.append(get_lower_coord(x)[0]) - self.z_l.append(get_lower_coord(x)[1]) + self.z_c.append(get_camber(x)) + self.x.append(get_upper_coord(x)[0]) + self.z.append(get_upper_coord(x)[1]) + for x in x_chord_rev: + self.x.append(get_lower_coord(x)[0]) + self.z.append(get_lower_coord(x)[1]) return None def add_mass(self, mass): @@ -211,8 +221,8 @@ class Airfoil(Coordinates): def info_print(self, round): super().info_print(round) - print('x_c the camber x-coordinates:\n', np.around(self.x_u, round)) - print('z_c the camber z-coordinates:\n', np.around(self.x_u, round)) + print('x_c the camber x-coordinates:\n', np.around(self.x, round)) + print('z_c the camber z-coordinates:\n', np.around(self.x, round)) return None @@ -223,35 +233,33 @@ class Spar(Coordinates): def __init__(self): super().__init__(parent.chord, parent.semi_span) - def add_coord(self, airfoil, spar_x): + def add_coord(self, airfoil, x_loc_percent): ''' Add a single spar at the % chord location given to function. Parameters: - coordinates: provided by Airfoil.coordinates[x_u, z_u, x_l, z_l]. + coordinates: provided by Airfoil.coordinates[x, z, x, z]. material: spar's material. Assumes homogeneous material. spar_x: spar's location as a % of total chord length. Return: None ''' - # Airfoil surface coordinates - # unpacked from 'coordinates' (list of lists in 'Coordinates'). - x_u = airfoil.x_u - z_u = airfoil.z_u - x_l = airfoil.x_l - z_l = airfoil.z_l + # Scaled spar location with regards to chord - loc = spar_x * self.chord - # bisect_left: returns index of first value in x_u > loc. - # Ensures that the spar coordinates intersect with airfoil surface. - spar_x_u = bi.bisect_left(x_u, loc) # index of spar's x_u - spar_x_l = bi.bisect_left(x_l, loc) # index of spar's x_l - # These x and y coordinates are assigned to the spar, NOT airfoil. - self.x_u.append(x_u[spar_x_u]) - self.z_u.append(z_u[spar_x_u]) - self.x_l.append(x_l[spar_x_l]) - self.z_l.append(z_l[spar_x_l]) + loc = x_loc_percent * self.chord + # bi.bisect_left: returns index of first value in airfoil.x > loc + # This ensures that spar geom intersects with airfoil geom. + # Spar upper coordinates + spar_x = bi.bisect_left(airfoil.x, loc) - 1 + x = [airfoil.x[spar_x]] + z = [airfoil.z[spar_x]] + # Spar lower coordinates + spar_x = bi.bisect_left(airfoil.x[::-1], loc) - 1 + x += [airfoil.x[-spar_x]] + z += [airfoil.z[-spar_x]] + self.x.append(x) + self.z.append(z) return None def add_spar_caps(self, spar_cap_area): @@ -259,7 +267,7 @@ class Spar(Coordinates): return None def add_mass(self, mass): - self.mass = len(self.x_u) * mass + self.mass = len(self.x) * mass return None @@ -291,44 +299,44 @@ class Stringer(Coordinates): ''' # Find distance between leading edge and first upper stringer - interval = airfoil.spar.x_u[0] / (stringer_u_1 + 1) - # initialise first self.stringer_x_u at first interval + interval = airfoil.spar.x[0][0] / (stringer_u_1 + 1) + # initialise first self.stringer_x at first interval x = interval # Add upper stringers from leading edge until first spar. for _ in range(0, stringer_u_1): - # Index of the first value of airfoil_x_u > x - index = bi.bisect_left(airfoil.x_u, x) - self.x_u.append(airfoil.x_u[index]) - self.z_u.append(airfoil.z_u[index]) + # Index of the first value of airfoil.x > x + i = bi.bisect_left(airfoil.x, x) + self.x.append(airfoil.x[i]) + self.z.append(airfoil.z[i]) x += interval # Add upper stringers from first spar until last spar # TODO: stringer placement if only one spar is created - interval = (airfoil.spar.x_u[-1] - - airfoil.spar.x_u[0]) / (stringer_u_2 + 1) - x = interval + airfoil.spar.x_u[0] + interval = (airfoil.spar.x[-1][0] + - airfoil.spar.x[0][0]) / (stringer_u_2 + 1) + x = interval + airfoil.spar.x[0][0] for _ in range(0, stringer_u_2): - index = bi.bisect_left(airfoil.x_u, x) - self.x_u.append(airfoil.x_u[index]) - self.z_u.append(airfoil.z_u[index]) + i = bi.bisect_left(airfoil.x, x) + self.x.append(airfoil.x[i]) + self.z.append(airfoil.z[i]) x += interval # Find distance between leading edge and first lower stringer - interval = airfoil.spar.x_l[0] / (stringer_l_1 + 1) + interval = airfoil.spar.x[0][1] / (stringer_l_1 + 1) x = interval # Add lower stringers from leading edge until first spar. for _ in range(0, stringer_l_1): - index = bi.bisect_left(airfoil.x_l, x) - self.x_l.append(airfoil.x_l[index]) - self.z_l.append(airfoil.z_l[index]) + i = bi.bisect_left(airfoil.x[::-1], x) + self.x.append(airfoil.x[-i]) + self.z.append(airfoil.z[-i]) x += interval # Add lower stringers from first spar until last spar - interval = (airfoil.spar.x_l[-1] - - airfoil.spar.x_l[0]) / (stringer_l_2 + 1) - x = interval + airfoil.spar.x_l[0] + interval = (airfoil.spar.x[-1][1] + - airfoil.spar.x[0][1]) / (stringer_l_2 + 1) + x = interval + airfoil.spar.x[0][1] for _ in range(0, stringer_l_2): - index = bi.bisect_left(airfoil.x_l, x) - self.x_l.append(airfoil.x_l[index]) - self.z_l.append(airfoil.z_l[index]) + i = bi.bisect_left(airfoil.x[::-1], x) + self.x.append(airfoil.x[-i]) + self.z.append(airfoil.z[-i]) x += interval return None @@ -337,7 +345,7 @@ class Stringer(Coordinates): return None def add_mass(self, mass): - self.mass = len(self.x_u) * mass + len(self.x_l) * mass + self.mass = len(self.x) * mass + len(self.x) * mass return None def info_print(self, round): @@ -357,38 +365,33 @@ def plot_geom(airfoil): plt.plot(airfoil.chord / 4, 0, '.', color='g', markersize=24, label='Quarter-chord') # Plot mean camber line - plt.plot(airfoil.x_c, airfoil.y_c, - '-.', color='r', linewidth='2', + plt.plot(airfoil.x_c, airfoil.z_c, '-.', color='r', linewidth='2', label='Mean camber line') - # Plot upper surface - plt.plot(airfoil.x_u, airfoil.z_u, - '', color='b', linewidth='1') - # Plot lower surface - plt.plot(airfoil.x_l, airfoil.z_l, - '', color='b', linewidth='1') + # Plot airfoil surfaces + plt.fill(airfoil.x, airfoil.z, color='b', linewidth='1', fill=False) # Plot spars - for _ in range(0, len(airfoil.spar.x_u)): - x = (airfoil.spar.x_u[_], airfoil.spar.x_l[_]) - y = (airfoil.spar.z_u[_], airfoil.spar.z_l[_]) - plt.plot(x, y, '.-', color='b') - - # Plot upper stringers - for _ in range(0, len(airfoil.stringer.x_u)): - x = airfoil.stringer.x_u[_] - y = airfoil.stringer.z_u[_] - plt.plot(x, y, '.', color='y', markersize=12) - # Plot lower stringers - for _ in range(0, len(airfoil.stringer.x_l)): - x = airfoil.stringer.x_l[_] - y = airfoil.stringer.z_l[_] - plt.plot(x, y, '.', color='y', markersize=12) + try: + for _ in range(len(airfoil.spar.x)): + x = (airfoil.spar.x[_]) + y = (airfoil.spar.z[_]) + plt.plot(x, y, '-', color='b') + except AttributeError: + print('No spars to plot.') + # Plot stringers + try: + for _ in range(0, len(airfoil.stringer.x)): + x = airfoil.stringer.x[_] + y = airfoil.stringer.z[_] + plt.plot(x, y, '.', color='y', markersize=12) + except AttributeError: + print('No stringers to plot.') # Graph formatting plt.xlabel('X axis') plt.ylabel('Z axis') - plot_bound = airfoil.x_u[-1] + plot_bound = max(airfoil.x) plt.xlim(- 0.10 * plot_bound, 1.10 * plot_bound) plt.ylim(- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)) plt.gca().set_aspect('equal', adjustable='box') diff --git a/evaluator.py b/evaluator.py index 5710522..046d4dd 100644 --- a/evaluator.py +++ b/evaluator.py @@ -38,17 +38,17 @@ class Evaluator: + airfoil.spar.mass + airfoil.stringer.mass) self.mass_dist = [] - # Upper coordinates - self.x_u = airfoil.x_u - self.z_u = airfoil.z_u - # Lower coordinates - self.x_l = airfoil.x_l - self.z_l = airfoil.z_l + # Coordinates + self.x = airfoil.x + self.z = airfoil.z + # Lift self.lift_rectangular = [] self.lift_elliptical = [] self.lift_total = [] # Drag self.drag = [] + # centroid + self.centroid = [] # Inertia terms: # I_x = self.I_[0] # I_z = self.I_[1] @@ -137,20 +137,26 @@ class Evaluator: def get_centroid(self): '''Return the coordinates of the centroid.''' + stringer_area = self.stringer.area caps_area = self.spar.cap_area - x_spars = self.spar.x_u + self.spar.x_l - x_stringers = self.stringer.x_u + self.stringer.x_l - z_stringers = self.stringer.z_u + self.stringer.z_l - denom = float(len(x_spars) * caps_area - + len(x_stringers) * stringer_area) + caps_x = [value for spar in self.spar.x for value in spar] + caps_z = [value for spar in self.spar.z for value in spar] + stringers_x = self.stringer.x + stringers_z = self.stringer.z + + denominator = float(len(caps_x) * caps_area + + len(stringers_x) * stringer_area) - x_ctr = (sum([i * caps_area for i in self.spar.x_u]) - + sum([i * stringer_area for i in x_stringers])) / denom - z_ctr = (sum([i * caps_area for i in self.spar.z_u]) - + sum([i * stringer_area for i in z_stringers])) / denom - return(x_ctr, z_ctr) + centroid_x = float(sum([x * caps_area for x in caps_x]) + + sum([x * stringer_area for x in stringers_x])) + centroid_x = centroid_x / denominator + + centroid_z = float(sum([z * caps_area for z in caps_z]) + + sum([z * stringer_area for z in stringers_z])) + centroid_z = centroid_z / denominator + return(centroid_x, centroid_z) def get_inertia_terms(self): '''Obtain all inertia terms.''' @@ -159,12 +165,12 @@ class Evaluator: caps_area = self.spar.cap_area # Adds upper and lower components' coordinates to list - x_stringers = self.stringer.x_u + self.stringer.x_l - z_stringers = self.stringer.z_u + self.stringer.z_l - x_spars = self.spar.x_u + self.spar.x_l - z_spars = self.spar.z_u + self.spar.z_l + x_stringers = self.stringer.x + z_stringers = self.stringer.z + x_spars = self.spar.x[:][0] + self.spar.x[:][1] + z_spars = self.spar.z[:][0] + self.spar.z[:][1] stringer_count = range(len(x_stringers)) - spar_count = range(len(self.spar.x_u)) + spar_count = range(len(self.spar.x)) # I_x is the sum of the contributions of the spar caps and stringers I_x = (sum([caps_area * (z_spars[i] - self.centroid[1]) ** 2 @@ -209,31 +215,32 @@ def plot_geom(evaluator): y_chord = [0, 0] plt.plot(x_chord, y_chord, linewidth='1') # Plot quarter chord - q = evaluator.chord / 4 - plt.plot(q, 0, '.', color='g', markersize=24, label='Quarter-chord') - # Plot upper surface - plt.plot(evaluator.x_u, evaluator.z_u, - '', color='b', linewidth='1') - # Plot lower surface - plt.plot(evaluator.x_l, evaluator.z_l, - '', color='b', linewidth='1') + plt.plot(evaluator.chord / 4, 0, '.', color='g', + markersize=24, label='Quarter-chord') + # Plot airfoil surfaces + plt.fill(evaluator.x, evaluator.z, color='b', linewidth='1', fill=False) # Plot spars - for _ in range(0, len(evaluator.spar.x_u)): - x = (evaluator.spar.x_u[_], evaluator.spar.x_l[_]) - y = (evaluator.spar.z_u[_], evaluator.spar.z_l[_]) - plt.plot(x, y, '.-', color='b') - + try: + for _ in range(len(evaluator.spar.x)): + x = (evaluator.spar.x[_]) + y = (evaluator.spar.z[_]) + plt.plot(x, y, '-', color='b') + except AttributeError: + print('No spars to plot.') # Plot upper stringers - for _ in range(0, len(evaluator.stringer.x_u)): - x = evaluator.stringer.x_u[_] - y = evaluator.stringer.z_u[_] - plt.plot(x, y, '.', color='y', markersize=12) - # Plot lower stringers - for _ in range(0, len(evaluator.stringer.x_l)): - x = evaluator.stringer.x_l[_] - y = evaluator.stringer.z_l[_] - plt.plot(x, y, '.', color='y', markersize=12) + try: + for _ in range(0, len(evaluator.stringer.x)): + x = evaluator.stringer.x[_] + y = evaluator.stringer.z[_] + plt.plot(x, y, '.', color='y', markersize=12) + except AttributeError: + print('No stringers to plot.') + # # Plot lower stringers + # for _ in range(0, len(evaluator.stringer.x)): + # x = evaluator.stringer.x[_] + # y = evaluator.stringer.z[_] + # plt.plot(x, y, '.', color='y', markersize=12) # Plot centroid x = evaluator.centroid[0] @@ -244,7 +251,7 @@ def plot_geom(evaluator): plt.xlabel('X axis') plt.ylabel('Z axis') - plot_bound = evaluator.x_u[-1] + plot_bound = max(evaluator.x) plt.xlim(- 0.10 * plot_bound, 1.10 * plot_bound) plt.ylim(- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)) plt.gca().set_aspect('equal', adjustable='box') @@ -24,8 +24,8 @@ start_time = time.time() # Airfoil dimensions NACA_NUM = 2412 -CHORD_LENGTH = 40 -SEMI_SPAN = 50 +CHORD_LENGTH = 101 +SEMI_SPAN = 40 # Airfoil thickness T_UPPER = 0.1 @@ -37,14 +37,14 @@ SPAR_MASS = 10 # lbs STRINGER_MASS = 5 # lbs # Area -SPAR_CAP_AREA = 0.3 # sqin +SPAR_CAP_AREA = 0.0 # sqin STRINGER_AREA = 0.1 # sqin # Amount of stringers -TOP_STRINGERS = 0 -BOTTOM_STRINGERS = 18 -NOSE_TOP_STRINGERS = 0 -NOSE_BOTTOM_STRINGERS = 5 +TOP_STRINGERS = 3 +BOTTOM_STRINGERS = 4 +NOSE_TOP_STRINGERS = 3 +NOSE_BOTTOM_STRINGERS = 6 # population information & save path POP_SIZE = 1 @@ -71,18 +71,18 @@ def main(): af.add_naca(NACA_NUM) af.add_mass(AIRFOIL_MASS) # af.info_print(2) - # af.info_save(SAVE_PATH, _) + af.info_save(SAVE_PATH, _) # Create spar instance af.spar = creator.Spar() # Define the spar coordinates and mass, stored in single spar object - af.spar.add_coord(af, 0.15) - af.spar.add_coord(af, 0.55) + af.spar.add_coord(af, 0.20) + af.spar.add_coord(af, 0.65) # Automatically adds spar caps for all spars previously defined af.spar.add_spar_caps(SPAR_CAP_AREA) af.spar.add_mass(SPAR_MASS) # af.spar.info_print(2) - # af.spar.info_save(SAVE_PATH, _) + af.spar.info_save(SAVE_PATH, _) # Create stringer instance af.stringer = creator.Stringer() @@ -95,10 +95,10 @@ def main(): af.stringer.add_area(STRINGER_AREA) af.stringer.add_mass(STRINGER_MASS) # af.stringer.info_print(2) - # af.stringer.info_save(SAVE_PATH, _) + af.stringer.info_save(SAVE_PATH, _) # Plot components with matplotlib - # creator.plot_geom(af) + creator.plot_geom(af) # Evaluator object contains airfoil analysis results. eval = evaluator.Evaluator(af) |