diff options
-rw-r--r-- | creator.py | 77 | ||||
-rw-r--r-- | evaluator.py | 142 | ||||
-rw-r--r-- | generator.py | 20 | ||||
-rw-r--r-- | main.py | 33 |
4 files changed, 142 insertions, 130 deletions
@@ -12,8 +12,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. - - +""" +The 'creator' module contains class definitions for coordinates +and various components we add to an airfoil (spars, stringers, and ribs.) + +Classes: + Coordinates: always instantiated first, but never assigned to object. + Airfoil: inherits from Coordinates & automatically aware of airfoil size. + Spar: also inherits from Coordinates. + Stringer: also inherits from Coordinates. + +Functions: + plot_geom(airfoil): generates a 2D plot of the airfoil & any components. +""" import sys import os.path import numpy as np @@ -30,19 +41,19 @@ global parent class Coordinates: - ''' + """ All airfoil components need the following: Parameters: - * Component material - * Coordinates relative to the chord & semi-span + Component material + Coordinates relative to the chord & semi-span Methods: - * Print component coordinates - * Save component coordinates to file specified in main.py + Print component coordinates + Save component coordinates to file specified in main.py So, all component classes inherit from class Coordinates. - ''' + """ def __init__(self, chord, semi_span): # Global dimensions @@ -65,11 +76,11 @@ class Coordinates: return type(self).__name__ def info_print(self, round): - ''' + """ Print all the component's coordinates to the terminal. This function's output is piped to the 'save_coord' function below. - ''' + """ name = ' CREATOR DATA ' num_of_dashes = len(name) @@ -85,10 +96,9 @@ class Coordinates: return None def info_save(self, save_path, number): - ''' + """ Save all the object's coordinates (must be full path). - ''' - + """ file_name = '{}_{}.txt'.format(str(self).lower(), number) full_path = os.path.join(save_path, file_name) try: @@ -105,8 +115,8 @@ class Coordinates: class Airfoil(Coordinates): - ''' - This class enables the creation of a single NACA airfoil. + """ + This class represents a single NACA airfoil. Please note: the coordinates are saved as two lists for the x- and z-coordinates. The coordinates start at @@ -116,7 +126,7 @@ class Airfoil(Coordinates): 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 @@ -129,7 +139,7 @@ class Airfoil(Coordinates): self.z_c = [] def add_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. @@ -140,8 +150,7 @@ class Airfoil(Coordinates): Return: None - ''' - + """ # Variables extracted from 'naca_num' argument passed to the function self.naca_num = naca_num m = int(str(naca_num)[0]) / 100 @@ -151,9 +160,9 @@ class Airfoil(Coordinates): p_c = p * self.chord def get_camber(x): - ''' + """ Returns camber z-coordinate from 1 'x' along the airfoil chord. - ''' + """ z_c = float() if 0 <= x < p_c: z_c = (m / (p ** 2)) * (2 * p * (x / self.chord) @@ -165,8 +174,7 @@ class Airfoil(Coordinates): 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 z_t = 5 * t * self.chord * ( + 0.2969 * sqrt(x / self.chord) @@ -227,7 +235,7 @@ class Airfoil(Coordinates): class Spar(Coordinates): - '''Contains a single spar's location.''' + """Contains a single spar's location.""" global parent def __init__(self): @@ -243,7 +251,7 @@ class Spar(Coordinates): self.dP_z = float() def add_coord(self, airfoil, x_loc_percent): - ''' + """ Add a single spar at the % chord location given to function. Parameters: @@ -252,8 +260,7 @@ class Spar(Coordinates): Return: None - ''' - + """ # Scaled spar location with regards to chord loc = x_loc_percent * self.chord # bi.bisect_left: returns index of first value in airfoil.x > loc @@ -279,8 +286,7 @@ class Spar(Coordinates): return None def add_webs(self, thickness): - '''Add webs to spars.''' - + """Add webs to spars.""" for _ in range(len(self.x)): self.x_start.append(self.x[_][0]) self.x_end.append(self.x[_][1]) @@ -291,7 +297,7 @@ class Spar(Coordinates): class Stringer(Coordinates): - '''Contains the coordinates of all stringers.''' + """Contains the coordinates of all stringers.""" global parent def __init__(self): @@ -310,7 +316,7 @@ class Stringer(Coordinates): def add_coord(self, airfoil, stringer_u_1, stringer_u_2, stringer_l_1, stringer_l_2): - ''' + """ Add equally distributed stringers to four airfoil locations (upper nose, lower nose, upper surface, lower surface). @@ -324,8 +330,7 @@ class Stringer(Coordinates): Returns: None - ''' - + """ # Find distance between leading edge and first upper stringer interval = airfoil.spar.x[0][0] / (stringer_u_1 + 1) # initialise first self.stringer_x at first interval @@ -377,8 +382,7 @@ class Stringer(Coordinates): return None def add_webs(self, thickness): - '''Add webs to stringers.''' - + """Add webs to stringers.""" for _ in range(len(self.x) // 2): self.x_start.append(self.x[_]) self.x_end.append(self.x[_ + 1]) @@ -394,8 +398,7 @@ class Stringer(Coordinates): def plot_geom(airfoil): - '''This function plots the airfoil's + sub-components' geometry.''' - + """This function plots the airfoil's + sub-components' geometry.""" # Plot chord x_chord = [0, airfoil.chord] y_chord = [0, 0] diff --git a/evaluator.py b/evaluator.py index 05900e9..44ed434 100644 --- a/evaluator.py +++ b/evaluator.py @@ -12,7 +12,12 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. - +""" +The 'evaluator' module contains a single Evaluator class, +which knows all the attributes of a specified Airfoil instance, +and contains functions to analyse the airfoil's geometrical +& structural properties. +""" import sys import os.path @@ -22,7 +27,7 @@ import matplotlib.pyplot as plt class Evaluator: - '''Performs structural evaluations for the airfoil passed as argument.''' + """Performs structural evaluations for the airfoil passed as argument.""" def __init__(self, airfoil): # Evaluator knows all geometrical info from evaluated airfoil @@ -32,8 +37,7 @@ class Evaluator: # Global dimensions self.chord = airfoil.chord self.semi_span = airfoil.semi_span - - # mass and area + # Mass & spanwise distribution self.mass_total = float(airfoil.mass + airfoil.spar.mass + airfoil.stringer.mass) @@ -50,18 +54,14 @@ class Evaluator: # centroid self.centroid = [] # Inertia terms: - # I_x = self.I_[0] - # I_z = self.I_[1] - # I_xz = self.I_[2] - self.I_ = [] + self.I_ = {'x': 0, 'z': 0, 'xz': 0} def info_print(self, round): - ''' + """ Print all the component's evaluated data to the terminal. This function's output is piped to the 'save_data' function below. - ''' - + """ name = ' EVALUATOR DATA ' num_of_dashes = len(name) @@ -73,9 +73,9 @@ class Evaluator: print('Total airfoil mass:', self.mass_total) print('Centroid location:\n', np.around(self.centroid, 3)) print('Inertia terms:') - print('I_x:\n', np.around(self.I_[0], 3)) - print('I_z:\n', np.around(self.I_[1], 3)) - print('I_xz:\n', np.around(self.I_[2], 3)) + print('I_x:\n', np.around(self.I_['x'], 3)) + print('I_z:\n', np.around(self.I_['z'], 3)) + print('I_xz:\n', np.around(self.I_['xz'], 3)) print('Spar dP_x:\n', self.spar.dP_x) print('Spar dP_z:\n', self.spar.dP_z) print(num_of_dashes * '-') @@ -91,8 +91,7 @@ class Evaluator: return None def info_save(self, save_path, number): - '''Save all the object's coordinates (must be full path).''' - + """Save all the object's coordinates (must be full path).""" file_name = 'airfoil_{}_eval.txt'.format(number) full_path = os.path.join(save_path, file_name) try: @@ -102,22 +101,22 @@ class Evaluator: sys.stdout = sys.__stdout__ print('Successfully wrote to file {}'.format(full_path)) except IOError: - print('Unable to write {} to specified directory.\n' - .format(file_name), - 'Was the full path passed to the function?') + print( + 'Unable to write {} to specified directory.\n'.format( + file_name), 'Was the full path passed to the function?') return None # All these functions take integer arguments and return lists. def get_lift_rectangular(self, lift): - L_prime = [lift / (self.semi_span * 2) - for x in range(self.semi_span)] + L_prime = [lift / (self.semi_span * 2) for x in range(self.semi_span)] return L_prime def get_lift_elliptical(self, L_0): - L_prime = [L_0 / (self.semi_span * 2) - * sqrt(1 - (y / self.semi_span) ** 2) - for y in range(self.semi_span)] + L_prime = [ + L_0 / (self.semi_span * 2) * sqrt(1 - (y / self.semi_span)**2) + for y in range(self.semi_span) + ] return L_prime def get_lift_total(self): @@ -126,8 +125,7 @@ class Evaluator: return F_z def get_mass_distribution(self, total_mass): - F_z = [total_mass / self.semi_span - for x in range(0, self.semi_span)] + F_z = [total_mass / self.semi_span for x in range(0, self.semi_span)] return F_z def get_drag(self, drag): @@ -143,8 +141,7 @@ class Evaluator: return F_x def get_centroid(self): - '''Return the coordinates of the centroid.''' - + """Return the coordinates of the centroid.""" stringer_area = self.stringer.area cap_area = self.spar.cap_area @@ -163,11 +160,11 @@ class Evaluator: centroid_z = float(sum([z * cap_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.''' + return (centroid_x, centroid_z) + def get_inertia_terms(self): + """Obtain all inertia terms.""" stringer_area = self.stringer.area cap_area = self.spar.cap_area @@ -180,72 +177,75 @@ class Evaluator: spar_count = range(len(self.spar.x)) # I_x is the sum of the contributions of the spar caps and stringers - I_x = (sum([cap_area * (z_spars[i] - self.centroid[1]) ** 2 + # TODO: replace list indices with dictionary value + I_x = sum([cap_area * (z_spars[i] - self.centroid[1])**2 + for i in spar_count]) + I_x += sum([stringer_area * (z_stringers[i] - self.centroid[1])**2 + for i in stringer_count]) + + I_z = sum([cap_area * (x_spars[i] - self.centroid[0])**2 + for i in spar_count]) + I_z += sum([stringer_area * (x_stringers[i] - self.centroid[0])**2 + for i in stringer_count]) + + I_xz = sum([cap_area * (x_spars[i] - self.centroid[0]) + * (z_spars[i] - self.centroid[1]) for i in spar_count]) - + sum([stringer_area * (z_stringers[i] - self.centroid[1]) ** 2 - for i in stringer_count])) + I_xz += sum([stringer_area * (x_stringers[i] - self.centroid[0]) + * (z_stringers[i] - self.centroid[1]) + for i in stringer_count]) + return (I_x, I_z, I_xz) - I_z = (sum([cap_area * (x_spars[i] - self.centroid[0]) ** 2 - for i in spar_count]) - + sum([stringer_area * (x_stringers[i] - self.centroid[0]) ** 2 - for i in stringer_count])) - - I_xz = (sum([cap_area * (x_spars[i] - self.centroid[0]) - * (z_spars[i] - self.centroid[1]) - for i in spar_count]) - + sum([stringer_area * (x_stringers[i] - self.centroid[0]) - * (z_stringers[i] - self.centroid[1]) - for i in stringer_count])) + def get_dx(self, component): + return [x - self.centroid[0] for x in component.x_start] - return(I_x, I_z, I_xz) + def get_dz(self, component): + return [x - self.centroid[1] for x in component.x_start] def analysis(self, V_x, V_z): - '''Perform all analysis calculations and store in class instance.''' + """Perform all analysis calculations and store in class instance.""" - def get_dp(xDist, zDist, V_x, V_z, I_x, I_z, I_xz, area): + def get_dP(xDist, zDist, V_x, V_z, I_x, I_z, I_xz, area): denom = float(I_x * I_z - I_xz ** 2) z = float() for _ in range(len(xDist)): - z += float(- area * xDist[_] * (I_x * V_x - I_xz * V_z) + z += float(-area * xDist[_] * (I_x * V_x - I_xz * V_z) / denom - area * zDist[_] * (I_z * V_z - I_xz * V_x) / denom) return z - def get_dx(component): - return [x - self.centroid[0] for x in component.x_start] - - def get_dz(component): - return [x - self.centroid[1] for x in component.x_start] - self.drag = self.get_drag(10) - self.lift_rectangular = self.get_lift_rectangular(13.7) self.lift_elliptical = self.get_lift_elliptical(15) self.lift_total = self.get_lift_total() - self.mass_dist = self.get_mass_distribution(self.mass_total) self.centroid = self.get_centroid() - self.I_ = self.get_inertia_terms() - self.spar.dP_x = get_dp(get_dx(self.spar), get_dz(self.spar), V_x, 0, - self.I_[0], self.I_[1], self.I_[2], + self.I_['x'] = self.get_inertia_terms()[0] + self.I_['z'] = self.get_inertia_terms()[1] + self.I_['xz'] = self.get_inertia_terms()[2] + spar_dx = self.get_dx(self.spar) + spar_dz = self.get_dz(self.spar) + self.spar.dP_x = get_dP(spar_dx, spar_dz, + V_x, 0, + self.I_['x'], self.I_['z'], self.I_['xz'], self.spar.cap_area) - self.spar.dP_z = get_dp(get_dx(self.spar), get_dz(self.spar), 0, V_z, - self.I_[0], self.I_[1], self.I_[2], + self.spar.dP_z = get_dP(spar_dx, spar_dz, + 0, V_z, + self.I_['x'], self.I_['z'], self.I_['xz'], self.spar.cap_area) return None def plot_geom(evaluator): - '''This function plots analysis results over the airfoil's geometry.''' - + """This function plots analysis results over the airfoil's geometry.""" # Plot chord x_chord = [0, evaluator.chord] y_chord = [0, 0] plt.plot(x_chord, y_chord, linewidth='1') # Plot quarter chord - plt.plot(evaluator.chord / 4, 0, '.', color='g', - markersize=24, label='Quarter-chord') + plt.plot(evaluator.chord / 4, 0, + '.', color='g', markersize=24, label='Quarter-chord') # Plot airfoil surfaces x = [0.98 * x for x in evaluator.x] y = [0.98 * z for z in evaluator.z] @@ -281,8 +281,8 @@ def plot_geom(evaluator): plt.ylabel('Z axis') 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.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') plt.gca().legend() plt.grid(axis='both', linestyle=':', linewidth=1) @@ -295,10 +295,8 @@ def plot_lift(evaluator): y_1 = evaluator.lift_rectangular y_2 = evaluator.lift_elliptical y_3 = evaluator.lift_total - plt.plot(x, y_1, '.', color='b', markersize=4, - label='Rectangular lift') - plt.plot(x, y_2, '.', color='g', markersize=4, - label='Elliptical lift') + plt.plot(x, y_1, '.', color='b', markersize=4, label='Rectangular lift') + plt.plot(x, y_2, '.', color='g', markersize=4, label='Elliptical lift') plt.plot(x, y_3, '.', color='r', markersize=4, label='Total lift') # Graph formatting diff --git a/generator.py b/generator.py index 7ad2cf3..0e8da8b 100644 --- a/generator.py +++ b/generator.py @@ -12,25 +12,31 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. +""" +The 'generator' module contains a single Population class, +which represents a collection of randomized airfoils. +""" -import creator +import creator as cr -class Population: - '''Collection of random airfoils.''' +class Population(cr.Airfoil, cr.Spar, cr.Stringer): + """Collection of random airfoils.""" def __init__(self, size): + af = cr.Airfoil + # print(af) self.size = size self.gen_number = 0 # incremented for every generation def mutate(self, prob_mt): - '''Randomly mutate the genes of prob_mt % of the population.''' + """Randomly mutate the genes of prob_mt % of the population.""" def crossover(self, prob_cx): - '''Combine the genes of prob_cx % of the population.''' + """Combine the genes of prob_cx % of the population.""" def reproduce(self, prob_rp): - '''Pass on the genes of the fittest prob_rp % of the population.''' + """Pass on the genes of the fittest prob_rp % of the population.""" def fitness(): - '''Rate the fitness of an individual on a relative scale (0-100)''' + """Rate the fitness of an individual on a relative scale (0-100)""" @@ -52,12 +52,11 @@ SAVE_PATH = 'C:/Users/blend/github/UCLA_MAE_154B/save' def main(): - ''' + """ Create an airfoil; Evaluate an airfoil; Generate a population of airfoils & optimize. - ''' - + """ # Create coordinate system specific to our airfoil dimensions. # TODO: imperial + metric unit setting creator.Coordinates(CHORD_LENGTH, SEMI_SPAN) @@ -70,8 +69,8 @@ def main(): # Define NACA airfoil coordinates and mass af.add_naca(NACA_NUM) af.add_mass(AIRFOIL_MASS) - af.info_print(2) - af.info_save(SAVE_PATH, _) + # af.info_print(2) + # af.info_save(SAVE_PATH, _) # Create spar instance af.spar = creator.Spar() @@ -82,8 +81,8 @@ def main(): af.spar.add_spar_caps(SPAR_CAP_AREA) af.spar.add_mass(SPAR_MASS) af.spar.add_webs(SPAR_THICKNESS) - af.spar.info_print(2) - af.spar.info_save(SAVE_PATH, _) + # af.spar.info_print(2) + # af.spar.info_save(SAVE_PATH, _) # Create stringer instance af.stringer = creator.Stringer() @@ -96,20 +95,26 @@ def main(): af.stringer.add_area(STRINGER_AREA) af.stringer.add_mass(STRINGER_MASS) af.stringer.add_webs(SKIN_THICKNESS) - af.stringer.info_print(2) - af.stringer.info_save(SAVE_PATH, _) - + # af.stringer.info_print(2) + # 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) # The analysis is performed in the evaluator.py module. eval.analysis(1, 1) - eval.info_print(2) - eval.info_save(SAVE_PATH, _) + # eval.info_print(2) + # eval.info_save(SAVE_PATH, _) evaluator.plot_geom(eval) - evaluator.plot_lift(eval) + # evaluator.plot_lift(eval) + + pop = generator.Population(10) + + # print(help(creator)) + # print(help(evaluator)) + # print(help(generator)) # Print final execution time print("--- %s seconds ---" % (time.time() - start_time)) |