From 50557a765e812beca9b9ef7d92af0828bd7bd0d4 Mon Sep 17 00:00:00 2001 From: Blendoit <51464356+Blendoit@users.noreply.github.com> Date: Thu, 6 Jun 2019 15:37:05 -0700 Subject: Add files via upload --- creator.py | 382 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 creator.py (limited to 'creator.py') diff --git a/creator.py b/creator.py new file mode 100644 index 0000000..59c8436 --- /dev/null +++ b/creator.py @@ -0,0 +1,382 @@ +# This file is part of Marius Peter's airfoil analysis package (this program). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os.path +import numpy as np +from math import sin, cos, tan, atan, sqrt, ceil +import bisect as bi +import matplotlib.pyplot as plt +import matplotlib as mpl +from mpl_toolkits.mplot3d import Axes3D + + +# This variable is required for main.py constant wing dimensions +# to be passed to inheriting classes (Airfoil, Spar, Stringer, Rib). +# This way, we don't have to redeclare our coordinates as parameters for +# our spars, stringers and ribs. This makes for more elegant code. +global parent + + +class Coordinates: + """ + All classes need coordinates relative to the chord & semi-span. + So, they all inherit from this class. + """ + + def __init__(self, chord, semi_span): + # Global dimensions + self.chord = chord + self.semi_span = semi_span + # Upper coordinates + self.x_u = [] + self.y_u = [] + # Lower coordinates + self.x_l = [] + self.y_l = [] + # Upper coordinates + self.x_u = [] + self.y_u = [] + # Lower coordinates + self.x_l = [] + self.y_l = [] + # Coordinates x_u, y_u, x_l, y_l packed in single list + self.coordinates = [] + global parent + parent = self + + def create(self, chord, semi_span): + self.chord = chord + self.semi_span = semi_span + + chord = self.chord + semi_span = self.semi_span + + +class Airfoil(Coordinates): + """This class enables the creation of a NACA airfoil.""" + + def __init__(self): + global parent + # Run 'Coordinates' super class init method with same chord & 1/2 span. + super().__init__(parent.chord, parent.semi_span) + # NACA number + self.naca_num = int() + # Mean camber line + self.x_c = [] + self.y_c = [] + # Thickness + self.y_t = [] + # dy_c / d_x + self.dy_c = [] + # Theta + self.theta = [] + + def naca(self, naca_num): + """ + This function generates geometry for our chosen NACA airfoil shape.\ + The nested functions perform the required steps to generate geometry,\ + and can be called to solve the geometry y-coordinate for any 'x' input.\ + Equation coefficients were retrieved from Wikipedia.org. + + Parameters: + naca_num: 4-digit NACA wing + chord: wing chord length, in any unit + + Return: + None + """ + + # Variables extracted from 'naca_num' argument passed to the function + self.naca_num = naca_num + m = int(str(naca_num)[0]) / 100 + p = int(str(naca_num)[1]) / 10 + t = int(str(naca_num)[2:]) / 100 + # Chord length. Should be higher than 10. + if self.chord < 10: + self.chord = 10 + # x-coordinate of maximum camber + p_c = p * self.chord + + def get_camber(x): + """ + Returns 1 camber y-coordinate from 1 'x' along the airfoil chord. + """ + x_c = x + y_c = float() + if 0 <= x < p_c: + y_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) + + 2 * p * (x / self.chord) + - (x / self.chord) ** 2) + else: + print('x-coordinate for camber is out of bounds. ' + 'Check that 0 < x <= chord.') + return (x_c, y_c * self.chord) + + def get_thickness(x): + """ + Returns thickness from 1 'x' along the airfoil chord. + """ + y_t = float() + if 0 <= x <= self.chord: + y_t = 5 * t * self.chord * (0.2969 * sqrt(x / self.chord) + - 0.1260 * (x / self.chord) + - 0.3516 * (x / self.chord) ** 2 + + 0.2843 * (x / self.chord) ** 3 + - 0.1015 * (x / self.chord) ** 4) + else: + print('x-coordinate for thickness is out of bounds. ' + 'Check that 0 < x <= chord.') + return y_t + + def get_dy_c(x): + """ + Returns dy_c/dx from 1 'x' along the airfoil chord. + """ + dy_c = float() + if 0 <= x < p_c: + dy_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) + return dy_c + + def get_theta(dy_c): + theta = atan(dy_c) + return theta + + def get_upper_coordinates(x): + x_u = float() + y_u = float() + if 0 <= x < self.chord: + x_u = x - self.y_t[x] * sin(self.theta[x]) + y_u = self.y_c[x] + self.y_t[x] * cos(self.theta[x]) + elif x == self.chord: + x_u = x - self.y_t[x] * sin(self.theta[x]) + y_u = 0 # Make upper curve finish at y = 0 + return(x_u, y_u) + + def get_lower_coordinates(x): + x_l = float() + y_l = float() + if 0 <= x < self.chord: + x_l = (x + self.y_t[x] * sin(self.theta[x])) + y_l = (self.y_c[x] - self.y_t[x] * cos(self.theta[x])) + elif x == self.chord: + x_l = (x + self.y_t[x] * sin(self.theta[x])) + y_l = 0 # Make lower curve finish at y = 0 + return(x_l, y_l) + + # Generate all our wing geometries from previous sub-functions + for x in range(0, self.chord + 1): + self.x_c.append(get_camber(x)[0]) + self.y_c.append(get_camber(x)[1]) + self.y_t.append(get_thickness(x)) + self.dy_c.append(get_dy_c(x)) + self.theta.append(get_theta(self.dy_c[x])) + self.x_u.append(get_upper_coordinates(x)[0]) + self.y_u.append(get_upper_coordinates(x)[1]) + self.x_l.append(get_lower_coordinates(x)[0]) + self.y_l.append(get_lower_coordinates(x)[1]) + + self.coordinates.append(self.x_u) + self.coordinates.append(self.y_u) + self.coordinates.append(self.x_l) + self.coordinates.append(self.x_l) + + return None + + def print_geometry(self, round): + """ + Print all the declared geometry to the terminal. + """ + # Print all our basic geometry, useful for debugging + print('Chord length') + print(self.chord) + print('x_c the x-coordinates of the mean camber line') + print(np.around(self.x_c, round)) + print('y_c the y-coordinates of the mean camber line') + print(np.around(self.y_c, round)) + print('y_t the y-coordinates of the airfoil thickness') + print(np.around(self.y_t, round)) + print('dy_c the derivative of y_c with respect to dx') + print(np.around(self.dy_c, round)) + print('theta is like an angle, idk') + print(np.around(self.theta, round)) + print('x_u the x-coordinates of the upper airfoil surface') + print(np.around(self.x_u, round)) + print('y_u the y-coordinates of the upper airfoil surface') + print(np.around(self.y_u, round)) + print('x_l the x-coordinates of the lower airfoil surface') + print(np.around(self.x_l, round)) + print('y_l the y-coordinates of lower airfoil surface') + print(np.around(self.y_l, round)) + return None + + def save_values(self, airfoil_number, save_dir_path): + """ + Save all the declared geometry to save_dir_path (must be full path). + """ + file_name = 'airfoil_%s' % airfoil_number + full_path = os.path.join(save_dir_path, file_name+'.txt') + file = open(full_path, 'w') + sys.stdout = file + self.print_geometry(4) + return None + + +class Spar(Coordinates): + """Contains a single spar's location and material.""" + global parent + + def __init__(self): + super().__init__(parent.chord, parent.semi_span) + # Spar material + self.spar_material = [] + + def add_spar(self, coordinates, material, spar_x): + """ + Add a single spar at the % chord location given to function. + + Parameters: + coordinates: provided by Airfoil.coordinates[x_u, y_u, x_l, y_l]. + 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 'Airfoil'). + x_u = coordinates[0] + y_u = coordinates[1] + x_l = coordinates[2] + y_l = coordinates[3] + # Scaled spar location with regards to chord + loc = spar_x * self.chord + # bisect_left: returns index of first value in x_u > loc. + # This 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.y_u.append(y_u[spar_x_u]) + self.x_l.append(x_l[spar_x_l]) + self.y_l.append(y_l[spar_x_l]) + self.spar_material = material + return None + + +class Stringer(): + """Contains the coordinates of stringer(s) location and material.""" + + def __init__(self): + # Stringer attributes + self.stringer_x_u = [] + self.stringer_y_u = [] + self.stringer_x_l = [] + self.stringer_y_l = [] + self.stringer_mat = [] + + def add_stringers(self, material, *density): + """ + Add stringers to the wing from their distribution density between spars. + First half of density[] concerns stringer distribution on + + Parameters: + material: stringer material + *density: + + """ + + # Find interval between leading edge and first upper stringer, + # from density parameter den_u_1. + interval = self.spar_x_u[0] / (den_u_1 * self.spar_x_u[0]) + # initialise first self.stringer_x_u at first interval. + x = interval + # Add upper stringers until first spar. + while x < self.spar_x_u[0]: + # Index of the first value of self.x_u > x + x_u = bi.bisect_left(self.x_u, x) + self.stringer_x_u.append(self.x_u[x_u]) + self.stringer_y_u.append(self.y_u[x_u]) + x += interval + + # Find interval between leading edge and first lower stringer, + # from density parameter den_l_1. + interval = self.spar_x_u[0] / (den_l_1 * self.spar_x_u[0]) + # initialise first self.stringer_x_l at first interval. + x = interval + # Add lower stringers until first spar. + while x < self.spar_x_l[0]: + # Index of the first value of self.x_l > x + x_u = bi.bisect_left(self.x_l, x) + self.stringer_x_l.append(self.x_l[x_u]) + self.stringer_y_l.append(self.y_l[x_u]) + x += interval + return None + + +def plot(airfoil, spar): + """This function plots the elements passed as arguments.""" + + print('Plotting airfoil.') + # Plot chord + x_chord = [0, airfoil.chord] + y_chord = [0, 0] + plt.plot(x_chord, y_chord, linewidth='1') + # Plot mean camber line + plt.plot(airfoil.x_c, airfoil.y_c, '-.', color='r', linewidth='2', + label='mean camber line') + # Plot upper surface + plt.plot(airfoil.x_u, airfoil.y_u, '', color='b', linewidth='1') + # Plot lower surface + plt.plot(airfoil.x_l, airfoil.y_l, '', color='b', linewidth='1') + # Plot spars + try: + for _ in range(0, len(spar.x_u)): + x = (spar.spar_x_u[_], spar.spar_x_l[_]) + y = (spar.spar_y_u[_], spar.spar_y_l[_]) + plt.plot(x, y, '.-', color='b', label='spar') + plt.legend() + except: + print('Did plot spars. Were they added?') + # Plot stringers + # if len(self.spar_x) != 0: + # for _ in range(0, len(self.stringer_x)): + # x = (self.stringer_x[_], self.stringer_x[_]) + # y = (self.stringer_y_u[_], self.stringer_y_l[_]) + # plt.scatter(x, y, color='y', linewidth='1', + # else: + # print('Unable to plot stringers. Were they created?') + # Graph formatting + plt.gcf().set_size_inches(9, 2.2) + plt.xlabel('X axis') + plt.ylabel('Y axis') + # plt.gcf().set_size_inches(self.chord, max(self.y_u) - min(self.y_l)) + plt.grid(axis='both', linestyle=':', linewidth=1) + plt.show() + return None + + +def main(): + return None + + +if __name__ == '__main__': + main() -- cgit v1.2.3