From cab1c2b9470ddc9099c4458daf2388d30ac12ca6 Mon Sep 17 00:00:00 2001 From: Marius Peter Date: Thu, 4 Jul 2019 11:00:26 -0700 Subject: start packagification --- __init__.py | 17 --- creator.py | 416 ----------------------------------------------------- evaluator.py | 288 ------------------------------------- example_airfoil.py | 14 +- generator.py | 64 --------- tools/creator.py | 416 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/evaluator.py | 288 +++++++++++++++++++++++++++++++++++++ tools/generator.py | 64 +++++++++ 8 files changed, 775 insertions(+), 792 deletions(-) delete mode 100644 __init__.py delete mode 100644 creator.py delete mode 100644 evaluator.py delete mode 100644 generator.py create mode 100644 tools/creator.py create mode 100644 tools/evaluator.py create mode 100644 tools/generator.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index dc031d0..0000000 --- a/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 . -__author__ = "Marius Peter" -# __version__ = "2.3" -# __revision__ = "2.3.1" diff --git a/creator.py b/creator.py deleted file mode 100644 index 810df09..0000000 --- a/creator.py +++ /dev/null @@ -1,416 +0,0 @@ -# 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 . - -""" -The creator.py module contains class definitions for coordinates -and various components we add to an airfoil (spars, stringers, and ribs). - -Classes: - Airfoil: instantiated with class method to provide coordinates to heirs. - Spar: inherits from Airfoil. - Stringer: also inherits from Airfoil. - -Functions: - plot_geom(airfoil): generates a 2D plot of the airfoil & any components. -""" - -import sys -import os.path -import numpy as np -from math import sin, cos, atan -import bisect as bi -import matplotlib.pyplot as plt - - -class 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 - 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. - """ - - # Defaults - chord = 100 - semi_span = 200 - - def __init__(self): - # mass and area - self.mass = float() - self.area = float() - # Component material - self.material = str() - # Coordinates - self.x = [] - self.z = [] - - @classmethod - def from_dimensions(cls, chord, semi_span): - """Create airfoil from its chord and semi-span.""" - if chord > 20: - cls.chord = chord - else: - cls.chord = 20 - print('Chord too small, using minimum value of 20.') - cls.semi_span = semi_span - return Airfoil() - - def __str__(self): - return type(self).__name__ - - def add_naca(self, naca_num): - """Generate surface geometry for a NACA airfoil. - - 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 - - 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 - # x-coordinate of maximum camber - 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) - - (x / self.chord) ** 2) - elif p_c <= x <= self.chord: - z_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p) - + 2 * p * (x / self.chord) - - (x / self.chord) ** 2) - return (z_c * self.chord) - - def get_thickness(x): - """Return thickness from 1 'x' along the airfoil chord.""" - x = 0 if x < 0 else x - z_t = 5 * t * self.chord * ( - + 0.2969 * (x / self.chord) ** 0.5 - - 0.1260 * (x / self.chord) ** 1 - - 0.3516 * (x / self.chord) ** 2 - + 0.2843 * (x / self.chord) ** 3 - - 0.1015 * (x / self.chord) ** 4) - return z_t - - def get_theta(x): - dz_c = float() - if 0 <= x < p_c: - dz_c = ((2 * m) / p ** 2) * (p - x / self.chord) - elif p_c <= x <= self.chord: - dz_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord) - theta = atan(dz_c) - return theta - - def get_upper_coord(x): - 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 = 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 = [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. - self.x_c = [] - self.z_c = [] - for x in x_chord: - self.x_c.append(x) - 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): - self.mass = mass - - def info_print(self, round): - """Print all the component's coordinates to the terminal.""" - name = ' CREATOR DATA FOR {} '.format(str(self).upper()) - num_of_dashes = len(name) - print(num_of_dashes * '-') - print(name) - for k, v in self.__dict__.items(): - if type(v) != list: - print('{}:\n'.format(k), v) - print(num_of_dashes * '-') - for k, v in self.__dict__.items(): - if type(v) == list: - print('{}:\n'.format(k), np.around(v, round)) - 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: - with open(full_path, 'w') as sys.stdout: - self.info_print(6) - # This line required to reset behavior of sys.stdout - 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?') - return None - - -class Spar(Airfoil): - """Contains a single spar's location.""" - - def __init__(self): - super().__init__() - self.x_start = [] - self.x_end = [] - self.thickness = float() - self.z_start = [] - self.z_end = [] - - def add_coord(self, airfoil, x_loc_percent): - """Add a single spar at the % chord location given to function. - - Parameters: - airfoil: gives the spar access to airfoil's coordinates. - x_loc_percent: spar's location as a % of total chord length. - - 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 - # 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) - 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): - self.cap_area = spar_cap_area - return None - - def add_mass(self, mass): - self.mass = len(self.x) * mass - return None - - def add_webs(self, thickness): - """Add webs to spars.""" - for _ in range(len(self.x)): - self.x_start.append(self.x[_][0]) - self.x_end.append(self.x[_][1]) - self.z_start.append(self.z[_][0]) - self.z_end.append(self.z[_][1]) - self.thickness = thickness - return None - - -class Stringer(Airfoil): - """Contains the coordinates of all stringers.""" - - def __init__(self): - super().__init__() - self.x_start = [] - self.x_end = [] - self.thickness = float() - self.z_start = [] - self.z_end = [] - self.area = float() - - 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). - - Parameters: - airfoil_coord: packed airfoil coordinates - spar_coord: packed spar coordinates - stringer_u_1: upper nose number of stringers - stringer_u_2: upper surface number of stringers - stringer_l_1: lower nose number of stringers - stringer_l_2: lower surface number of stringers - - 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 - 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 > 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[-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): - 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[0][1] / (stringer_l_1 + 1) - x = interval - # Add lower stringers from leading edge until first spar. - for _ in range(0, stringer_l_1): - 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[-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): - i = bi.bisect_left(airfoil.x[::-1], x) - self.x.append(airfoil.x[-i]) - self.z.append(airfoil.z[-i]) - x += interval - return None - - def add_area(self, area): - self.area = area - return None - - def add_mass(self, mass): - self.mass = len(self.x) * mass + len(self.x) * mass - return None - - def add_webs(self, thickness): - """Add webs to stringers.""" - for _ in range(len(self.x) // 2): - self.x_start.append(self.x[_]) - self.x_end.append(self.x[_ + 1]) - self.z_start.append(self.z[_]) - self.z_end.append(self.z[_ + 1]) - self.thickness = thickness - return None - - def info_print(self, round): - super().info_print(round) - print('Stringer Area:\n', np.around(self.area, round)) - return None - - -def plot_geom(airfoil, view: False): - """This function plots the airfoil's + sub-components' geometry.""" - fig, ax = plt.subplots() - - # Plot chord - x = [0, airfoil.chord] - y = [0, 0] - ax.plot(x, y, linewidth='1') - # Plot quarter chord - ax.plot(airfoil.chord / 4, 0, - '.', color='g', markersize=24, - label='Quarter-chord') - # Plot mean camber line - ax.plot(airfoil.x_c, airfoil.z_c, - '-.', color='r', linewidth='2', - label='Mean camber line') - # Plot airfoil surfaces - ax.plot(airfoil.x, airfoil.z, - color='b', linewidth='1') - - # Plot spars - try: - for _ in range(len(airfoil.spar.x)): - x = (airfoil.spar.x[_]) - y = (airfoil.spar.z[_]) - ax.plot(x, y, '-', color='y', linewidth='4') - 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[_] - ax.plot(x, y, '.', color='y', markersize=12) - except AttributeError: - print('No stringers to plot.') - - # Graph formatting - plot_bound = max(airfoil.x) - ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil', - xlabel='X axis', - xlim=[- 0.10 * plot_bound, 1.10 * plot_bound], - ylabel='Z axis', - ylim=[- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)]) - - plt.grid(axis='both', linestyle=':', linewidth=1) - plt.gca().set_aspect('equal', adjustable='box') - plt.gca().legend(bbox_to_anchor=(1, 1), - bbox_transform=plt.gcf().transFigure) - - if view == True: - plt.show() - else: - pass - return fig, ax - - -def main(): - return None - - -if __name__ == '__main__': - main() diff --git a/evaluator.py b/evaluator.py deleted file mode 100644 index ad1f873..0000000 --- a/evaluator.py +++ /dev/null @@ -1,288 +0,0 @@ -# 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 . -""" -The evaluator.py 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 -import numpy as np -from math import sqrt -import matplotlib.pyplot as plt - - -class Evaluator: - """Performs structural evaluations for the airfoil passed as argument.""" - - def __init__(self, airfoil): - # Evaluator knows all geometrical info from evaluated airfoil - self.airfoil = airfoil - self.spar = airfoil.spar - self.stringer = airfoil.stringer - # Global dimensions - self.chord = airfoil.chord - self.semi_span = airfoil.semi_span - # Mass & spanwise distribution - self.mass_total = float(airfoil.mass - + airfoil.spar.mass - + airfoil.stringer.mass) - self.mass_dist = [] - # Lift - self.lift_rectangular = [] - self.lift_elliptical = [] - self.lift_total = [] - # Drag - self.drag = [] - # centroid - self.centroid = [] - # Inertia terms: - self.I_ = {'x': 0, 'z': 0, 'xz': 0} - - def __str__(self): - return type(self).__name__ - - def info_print(self, round): - """Print all the component's evaluated data to the terminal.""" - name = ' EVALUATOR DATA FOR {} '.format(str(self).upper()) - num_of_dashes = len(name) - print(num_of_dashes * '-') - print(name) - for k, v in self.__dict__.items(): - if type(v) != list: - print('{}:\n'.format(k), v) - print(num_of_dashes * '-') - for k, v in self.__dict__.items(): - if type(v) == list: - print('{}:\n'.format(k), np.around(v, round)) - return None - - def info_save(self, save_path, number): - """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: - with open(full_path, 'w') as sys.stdout: - self.info_print(6) - # This line required to reset behavior of sys.stdout - 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?') - 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)] - 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) - ] - return L_prime - - def get_lift_total(self): - F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2 - for _ in range(len(self.lift_rectangular))] - 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)] - return F_z - - def get_drag(self, drag): - # Transform semi-span integer into list - semi_span = [x for x in range(0, self.semi_span)] - - # Drag increases after 80% of the semi_span - cutoff = round(0.8 * self.semi_span) - - # Drag increases by 25% after 80% of the semi_span - F_x = [drag for x in semi_span[0:cutoff]] - F_x.extend([1.25 * drag for x in semi_span[cutoff:]]) - return F_x - - def get_centroid(self): - """Return the coordinates of the centroid.""" - stringer_area = self.stringer.area - cap_area = self.spar.cap_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) * cap_area - + len(stringers_x) * stringer_area) - - centroid_x = float(sum([x * cap_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 * 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.""" - stringer_area = self.stringer.area - cap_area = self.spar.cap_area - - # Adds upper and lower components' coordinates to list - 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)) - - # I_x is the sum of the contributions of the spar caps and stringers - # 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]) - 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) - - def get_dx(self, component): - return [x - self.centroid[0] for x in component.x_start] - - def get_dz(self, component): - return [x - self.centroid[1] for x in component.x_start] - - def get_dP(self, xDist, zDist, V_x, V_z, area): - I_x = self.I_['x'] - I_z = self.I_['z'] - I_xz = self.I_['xz'] - 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) - / denom - - area * zDist[_] * (I_z * V_z - I_xz * V_x) - / denom) - return z - - def analysis(self, V_x, V_z): - """Perform all analysis calculations and store in class instance.""" - 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_['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 = self.get_dP(spar_dx, spar_dz, - V_x, 0, self.spar.cap_area) - self.spar.dP_z = self.get_dP(spar_dx, spar_dz, - 0, V_z, self.spar.cap_area) - return None - - -def plot_geom(evaluator): - """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') - # Plot airfoil surfaces - x = [0.98 * x for x in evaluator.airfoil.x] - y = [0.98 * z for z in evaluator.airfoil.z] - plt.fill(x, y, color='w', linewidth='1', fill=False) - x = [1.02 * x for x in evaluator.airfoil.x] - y = [1.02 * z for z in evaluator.airfoil.z] - plt.fill(x, y, color='b', linewidth='1', fill=False) - - # Plot spars - 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 stringers - 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 centroid - x = evaluator.centroid[0] - y = evaluator.centroid[1] - plt.plot(x, y, '.', color='r', markersize=24, label='centroid') - - # Graph formatting - plt.xlabel('X axis') - plt.ylabel('Z axis') - - plot_bound = max(evaluator.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') - plt.gca().legend() - plt.grid(axis='both', linestyle=':', linewidth=1) - plt.show() - return None - - -def plot_lift(evaluator): - x = range(evaluator.semi_span) - 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_3, '.', color='r', markersize=4, label='Total lift') - - # Graph formatting - plt.xlabel('Semi-span location') - plt.ylabel('Lift') - - plt.gca().legend() - plt.grid(axis='both', linestyle=':', linewidth=1) - plt.show() - return None diff --git a/example_airfoil.py b/example_airfoil.py index cf279d5..f09ea7f 100644 --- a/example_airfoil.py +++ b/example_airfoil.py @@ -1,13 +1,14 @@ """This example illustrates the usage of creator, evaluator and generator. +All the steps of airfoil creation & evaluation are detailed here; +however, the generator.py module contains certain presets (default airfoils). + Create an airfoil; Evaluate an airfoil; Generate a population of airfoils & optimize. """ -import creator # Create geometry -import evaluator # Evaluate geometry -import generator # Iteratevely evaluate instances of geometry and optimize +from tools import creator, evaluator, generator import time start_time = time.time() @@ -36,10 +37,9 @@ BOTTOM_STRINGERS = 4 NOSE_TOP_STRINGERS = 3 NOSE_BOTTOM_STRINGERS = 5 -# population information & save path -POP_SIZE = 1 SAVE_PATH = 'C:/Users/blend/github/UCLA_MAE_154B/save' + # Create airfoil instance af = creator.Airfoil.from_dimensions(CHORD_LENGTH, SEMI_SPAN) af.add_naca(NACA_NUM) @@ -49,10 +49,10 @@ af.info_save(SAVE_PATH, 'foo_name') # Create spar instance af.spar = creator.Spar() -# Define the spar coordinates and mass, stored in single spar object +# All spar coordinates are stored in single Spar object af.spar.add_coord(af, 0.23) af.spar.add_coord(af, 0.57) -# Automatically adds spar caps for each spar defined previously +# Automatically adds spar caps for each spar previously defined af.spar.add_spar_caps(SPAR_CAP_AREA) af.spar.add_mass(SPAR_MASS) af.spar.add_webs(SPAR_THICKNESS) diff --git a/generator.py b/generator.py deleted file mode 100644 index 6a2865d..0000000 --- a/generator.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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 . -""" -The generator.py module contains a single Population class, -which represents a collection of randomized airfoils. -""" - -import creator - - -def default_airfoil(): - """Generate the default airfoil.""" - airfoil = creator.Airfoil.from_dimensions(100, 200) - airfoil.add_naca(2412) - airfoil.add_mass(10) - - airfoil.spar = creator.Spar() - airfoil.spar.add_coord(airfoil, 0.23) - airfoil.spar.add_coord(airfoil, 0.57) - airfoil.spar.add_spar_caps(0.3) - airfoil.spar.add_mass(10) - airfoil.spar.add_webs(0.4) - - airfoil.stringer = creator.Stringer() - airfoil.stringer.add_coord(airfoil, 3, 6, 5, 4) - airfoil.stringer.add_area(0.1) - airfoil.stringer.add_mass(5) - airfoil.stringer.add_webs(0.1) - - return airfoil - - -class Population(creator.Airfoil): - """Collection of random airfoils.""" - - def __init__(self, size): - af = creator.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.""" - - def crossover(self, prob_cx): - """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.""" - - def fitness(): - """Rate the fitness of an individual on a relative scale (0-100)""" diff --git a/tools/creator.py b/tools/creator.py new file mode 100644 index 0000000..810df09 --- /dev/null +++ b/tools/creator.py @@ -0,0 +1,416 @@ +# 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 . + +""" +The creator.py module contains class definitions for coordinates +and various components we add to an airfoil (spars, stringers, and ribs). + +Classes: + Airfoil: instantiated with class method to provide coordinates to heirs. + Spar: inherits from Airfoil. + Stringer: also inherits from Airfoil. + +Functions: + plot_geom(airfoil): generates a 2D plot of the airfoil & any components. +""" + +import sys +import os.path +import numpy as np +from math import sin, cos, atan +import bisect as bi +import matplotlib.pyplot as plt + + +class 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 + 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. + """ + + # Defaults + chord = 100 + semi_span = 200 + + def __init__(self): + # mass and area + self.mass = float() + self.area = float() + # Component material + self.material = str() + # Coordinates + self.x = [] + self.z = [] + + @classmethod + def from_dimensions(cls, chord, semi_span): + """Create airfoil from its chord and semi-span.""" + if chord > 20: + cls.chord = chord + else: + cls.chord = 20 + print('Chord too small, using minimum value of 20.') + cls.semi_span = semi_span + return Airfoil() + + def __str__(self): + return type(self).__name__ + + def add_naca(self, naca_num): + """Generate surface geometry for a NACA airfoil. + + 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 + + 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 + # x-coordinate of maximum camber + 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) + - (x / self.chord) ** 2) + elif p_c <= x <= self.chord: + z_c = (m / ((1 - p) ** 2)) * ((1 - 2 * p) + + 2 * p * (x / self.chord) + - (x / self.chord) ** 2) + return (z_c * self.chord) + + def get_thickness(x): + """Return thickness from 1 'x' along the airfoil chord.""" + x = 0 if x < 0 else x + z_t = 5 * t * self.chord * ( + + 0.2969 * (x / self.chord) ** 0.5 + - 0.1260 * (x / self.chord) ** 1 + - 0.3516 * (x / self.chord) ** 2 + + 0.2843 * (x / self.chord) ** 3 + - 0.1015 * (x / self.chord) ** 4) + return z_t + + def get_theta(x): + dz_c = float() + if 0 <= x < p_c: + dz_c = ((2 * m) / p ** 2) * (p - x / self.chord) + elif p_c <= x <= self.chord: + dz_c = (2 * m) / ((1 - p) ** 2) * (p - x / self.chord) + theta = atan(dz_c) + return theta + + def get_upper_coord(x): + 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 = 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 = [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. + self.x_c = [] + self.z_c = [] + for x in x_chord: + self.x_c.append(x) + 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): + self.mass = mass + + def info_print(self, round): + """Print all the component's coordinates to the terminal.""" + name = ' CREATOR DATA FOR {} '.format(str(self).upper()) + num_of_dashes = len(name) + print(num_of_dashes * '-') + print(name) + for k, v in self.__dict__.items(): + if type(v) != list: + print('{}:\n'.format(k), v) + print(num_of_dashes * '-') + for k, v in self.__dict__.items(): + if type(v) == list: + print('{}:\n'.format(k), np.around(v, round)) + 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: + with open(full_path, 'w') as sys.stdout: + self.info_print(6) + # This line required to reset behavior of sys.stdout + 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?') + return None + + +class Spar(Airfoil): + """Contains a single spar's location.""" + + def __init__(self): + super().__init__() + self.x_start = [] + self.x_end = [] + self.thickness = float() + self.z_start = [] + self.z_end = [] + + def add_coord(self, airfoil, x_loc_percent): + """Add a single spar at the % chord location given to function. + + Parameters: + airfoil: gives the spar access to airfoil's coordinates. + x_loc_percent: spar's location as a % of total chord length. + + 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 + # 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) + 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): + self.cap_area = spar_cap_area + return None + + def add_mass(self, mass): + self.mass = len(self.x) * mass + return None + + def add_webs(self, thickness): + """Add webs to spars.""" + for _ in range(len(self.x)): + self.x_start.append(self.x[_][0]) + self.x_end.append(self.x[_][1]) + self.z_start.append(self.z[_][0]) + self.z_end.append(self.z[_][1]) + self.thickness = thickness + return None + + +class Stringer(Airfoil): + """Contains the coordinates of all stringers.""" + + def __init__(self): + super().__init__() + self.x_start = [] + self.x_end = [] + self.thickness = float() + self.z_start = [] + self.z_end = [] + self.area = float() + + 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). + + Parameters: + airfoil_coord: packed airfoil coordinates + spar_coord: packed spar coordinates + stringer_u_1: upper nose number of stringers + stringer_u_2: upper surface number of stringers + stringer_l_1: lower nose number of stringers + stringer_l_2: lower surface number of stringers + + 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 + 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 > 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[-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): + 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[0][1] / (stringer_l_1 + 1) + x = interval + # Add lower stringers from leading edge until first spar. + for _ in range(0, stringer_l_1): + 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[-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): + i = bi.bisect_left(airfoil.x[::-1], x) + self.x.append(airfoil.x[-i]) + self.z.append(airfoil.z[-i]) + x += interval + return None + + def add_area(self, area): + self.area = area + return None + + def add_mass(self, mass): + self.mass = len(self.x) * mass + len(self.x) * mass + return None + + def add_webs(self, thickness): + """Add webs to stringers.""" + for _ in range(len(self.x) // 2): + self.x_start.append(self.x[_]) + self.x_end.append(self.x[_ + 1]) + self.z_start.append(self.z[_]) + self.z_end.append(self.z[_ + 1]) + self.thickness = thickness + return None + + def info_print(self, round): + super().info_print(round) + print('Stringer Area:\n', np.around(self.area, round)) + return None + + +def plot_geom(airfoil, view: False): + """This function plots the airfoil's + sub-components' geometry.""" + fig, ax = plt.subplots() + + # Plot chord + x = [0, airfoil.chord] + y = [0, 0] + ax.plot(x, y, linewidth='1') + # Plot quarter chord + ax.plot(airfoil.chord / 4, 0, + '.', color='g', markersize=24, + label='Quarter-chord') + # Plot mean camber line + ax.plot(airfoil.x_c, airfoil.z_c, + '-.', color='r', linewidth='2', + label='Mean camber line') + # Plot airfoil surfaces + ax.plot(airfoil.x, airfoil.z, + color='b', linewidth='1') + + # Plot spars + try: + for _ in range(len(airfoil.spar.x)): + x = (airfoil.spar.x[_]) + y = (airfoil.spar.z[_]) + ax.plot(x, y, '-', color='y', linewidth='4') + 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[_] + ax.plot(x, y, '.', color='y', markersize=12) + except AttributeError: + print('No stringers to plot.') + + # Graph formatting + plot_bound = max(airfoil.x) + ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil', + xlabel='X axis', + xlim=[- 0.10 * plot_bound, 1.10 * plot_bound], + ylabel='Z axis', + ylim=[- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2)]) + + plt.grid(axis='both', linestyle=':', linewidth=1) + plt.gca().set_aspect('equal', adjustable='box') + plt.gca().legend(bbox_to_anchor=(1, 1), + bbox_transform=plt.gcf().transFigure) + + if view == True: + plt.show() + else: + pass + return fig, ax + + +def main(): + return None + + +if __name__ == '__main__': + main() diff --git a/tools/evaluator.py b/tools/evaluator.py new file mode 100644 index 0000000..ad1f873 --- /dev/null +++ b/tools/evaluator.py @@ -0,0 +1,288 @@ +# 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 . +""" +The evaluator.py 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 +import numpy as np +from math import sqrt +import matplotlib.pyplot as plt + + +class Evaluator: + """Performs structural evaluations for the airfoil passed as argument.""" + + def __init__(self, airfoil): + # Evaluator knows all geometrical info from evaluated airfoil + self.airfoil = airfoil + self.spar = airfoil.spar + self.stringer = airfoil.stringer + # Global dimensions + self.chord = airfoil.chord + self.semi_span = airfoil.semi_span + # Mass & spanwise distribution + self.mass_total = float(airfoil.mass + + airfoil.spar.mass + + airfoil.stringer.mass) + self.mass_dist = [] + # Lift + self.lift_rectangular = [] + self.lift_elliptical = [] + self.lift_total = [] + # Drag + self.drag = [] + # centroid + self.centroid = [] + # Inertia terms: + self.I_ = {'x': 0, 'z': 0, 'xz': 0} + + def __str__(self): + return type(self).__name__ + + def info_print(self, round): + """Print all the component's evaluated data to the terminal.""" + name = ' EVALUATOR DATA FOR {} '.format(str(self).upper()) + num_of_dashes = len(name) + print(num_of_dashes * '-') + print(name) + for k, v in self.__dict__.items(): + if type(v) != list: + print('{}:\n'.format(k), v) + print(num_of_dashes * '-') + for k, v in self.__dict__.items(): + if type(v) == list: + print('{}:\n'.format(k), np.around(v, round)) + return None + + def info_save(self, save_path, number): + """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: + with open(full_path, 'w') as sys.stdout: + self.info_print(6) + # This line required to reset behavior of sys.stdout + 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?') + 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)] + 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) + ] + return L_prime + + def get_lift_total(self): + F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2 + for _ in range(len(self.lift_rectangular))] + 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)] + return F_z + + def get_drag(self, drag): + # Transform semi-span integer into list + semi_span = [x for x in range(0, self.semi_span)] + + # Drag increases after 80% of the semi_span + cutoff = round(0.8 * self.semi_span) + + # Drag increases by 25% after 80% of the semi_span + F_x = [drag for x in semi_span[0:cutoff]] + F_x.extend([1.25 * drag for x in semi_span[cutoff:]]) + return F_x + + def get_centroid(self): + """Return the coordinates of the centroid.""" + stringer_area = self.stringer.area + cap_area = self.spar.cap_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) * cap_area + + len(stringers_x) * stringer_area) + + centroid_x = float(sum([x * cap_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 * 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.""" + stringer_area = self.stringer.area + cap_area = self.spar.cap_area + + # Adds upper and lower components' coordinates to list + 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)) + + # I_x is the sum of the contributions of the spar caps and stringers + # 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]) + 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) + + def get_dx(self, component): + return [x - self.centroid[0] for x in component.x_start] + + def get_dz(self, component): + return [x - self.centroid[1] for x in component.x_start] + + def get_dP(self, xDist, zDist, V_x, V_z, area): + I_x = self.I_['x'] + I_z = self.I_['z'] + I_xz = self.I_['xz'] + 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) + / denom + - area * zDist[_] * (I_z * V_z - I_xz * V_x) + / denom) + return z + + def analysis(self, V_x, V_z): + """Perform all analysis calculations and store in class instance.""" + 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_['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 = self.get_dP(spar_dx, spar_dz, + V_x, 0, self.spar.cap_area) + self.spar.dP_z = self.get_dP(spar_dx, spar_dz, + 0, V_z, self.spar.cap_area) + return None + + +def plot_geom(evaluator): + """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') + # Plot airfoil surfaces + x = [0.98 * x for x in evaluator.airfoil.x] + y = [0.98 * z for z in evaluator.airfoil.z] + plt.fill(x, y, color='w', linewidth='1', fill=False) + x = [1.02 * x for x in evaluator.airfoil.x] + y = [1.02 * z for z in evaluator.airfoil.z] + plt.fill(x, y, color='b', linewidth='1', fill=False) + + # Plot spars + 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 stringers + 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 centroid + x = evaluator.centroid[0] + y = evaluator.centroid[1] + plt.plot(x, y, '.', color='r', markersize=24, label='centroid') + + # Graph formatting + plt.xlabel('X axis') + plt.ylabel('Z axis') + + plot_bound = max(evaluator.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') + plt.gca().legend() + plt.grid(axis='both', linestyle=':', linewidth=1) + plt.show() + return None + + +def plot_lift(evaluator): + x = range(evaluator.semi_span) + 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_3, '.', color='r', markersize=4, label='Total lift') + + # Graph formatting + plt.xlabel('Semi-span location') + plt.ylabel('Lift') + + plt.gca().legend() + plt.grid(axis='both', linestyle=':', linewidth=1) + plt.show() + return None diff --git a/tools/generator.py b/tools/generator.py new file mode 100644 index 0000000..0213828 --- /dev/null +++ b/tools/generator.py @@ -0,0 +1,64 @@ +# 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 . +""" +The generator.py module contains a single Population class, +which represents a collection of randomized airfoils. +""" + +from tools import creator + + +def default_airfoil(): + """Generate the default airfoil.""" + airfoil = creator.Airfoil.from_dimensions(100, 200) + airfoil.add_naca(2412) + airfoil.add_mass(10) + + airfoil.spar = creator.Spar() + airfoil.spar.add_coord(airfoil, 0.23) + airfoil.spar.add_coord(airfoil, 0.57) + airfoil.spar.add_spar_caps(0.3) + airfoil.spar.add_mass(10) + airfoil.spar.add_webs(0.4) + + airfoil.stringer = creator.Stringer() + airfoil.stringer.add_coord(airfoil, 3, 6, 5, 4) + airfoil.stringer.add_area(0.1) + airfoil.stringer.add_mass(5) + airfoil.stringer.add_webs(0.1) + + return airfoil + + +class Population(creator.Airfoil): + """Collection of random airfoils.""" + + def __init__(self, size): + af = creator.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.""" + + def crossover(self, prob_cx): + """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.""" + + def fitness(): + """Rate the fitness of an individual on a relative scale (0-100)""" -- cgit v1.2.3