diff options
-rw-r--r-- | creator.py | 185 | ||||
-rw-r--r-- | evaluator.py | 65 | ||||
-rw-r--r-- | generator.py | 8 | ||||
-rw-r--r-- | main.py | 27 |
4 files changed, 122 insertions, 163 deletions
@@ -12,19 +12,20 @@ # # 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.) +The creator.py 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. + 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 @@ -33,32 +34,25 @@ import bisect as bi import matplotlib.pyplot as plt -# 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: +class Airfoil: """ - All airfoil components need the following: - - Parameters: - Component material - Coordinates relative to the chord & semi-span + This class represents a single NACA airfoil. - Methods: - Print component coordinates - Save component coordinates to file specified in main.py + 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. - So, all component classes inherit from class 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, chord, semi_span): - # Global dimensions - self.chord = chord if chord > 40 else 40 - self.semi_span = semi_span + # Defaults + chord = 100 + semi_span = 200 + + def __init__(self): # mass and area self.mass = float() self.area = float() @@ -68,79 +62,18 @@ class Coordinates: self.x = [] self.z = [] - # The airfoil components know the Coordinates instance's coords - global parent - parent = self + @classmethod + def from_dimensions(cls, chord, semi_span): + cls.chord = chord + cls.semi_span = semi_span + return Airfoil() def __str__(self): 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) - - print(num_of_dashes * '-') - print(name) - print('Component:', str(self)) - print('Chord length:', self.chord) - print('Semi-span:', self.semi_span) - print('Mass:', self.mass) - print(num_of_dashes * '-') - 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): - """ - 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 Airfoil(Coordinates): - """ - 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. - """ - - 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.z_c = [] - def add_naca(self, naca_num): """ - This function generates geometry for our chosen NACA airfoil shape. + This function generates geometry for a NACA number passed as argument. 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. @@ -214,6 +147,8 @@ class Airfoil(Coordinates): 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)) @@ -228,27 +163,56 @@ class Airfoil(Coordinates): self.mass = mass def info_print(self, round): - super().info_print(round) - print('x_c the camber x-coordinates:\n', np.around(self.x_c, round)) - print('z_c the camber z-coordinates:\n', np.around(self.z_c, 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) + + print(num_of_dashes * '-') + print(name) + print('Component:', str(self)) + print('Chord length:', self.chord) + print('Semi-span:', self.semi_span) + print('Mass:', self.mass) + print(num_of_dashes * '-') + 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): + """ + 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(Coordinates): +class Spar(Airfoil): """Contains a single spar's location.""" - global parent def __init__(self): - super().__init__(parent.chord, parent.semi_span) + super().__init__() self.x_start = [] self.x_end = [] self.thickness = float() self.z_start = [] self.z_end = [] - self.dx = float() - self.dz = float() - self.dP_x = float() - self.dP_z = float() def add_coord(self, airfoil, x_loc_percent): """ @@ -261,6 +225,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 @@ -296,22 +261,17 @@ class Spar(Coordinates): return None -class Stringer(Coordinates): +class Stringer(Airfoil): """Contains the coordinates of all stringers.""" - global parent def __init__(self): - super().__init__(parent.chord, parent.semi_span) + super().__init__() self.x_start = [] self.x_end = [] self.thickness = float() self.z_start = [] self.z_end = [] self.area = float() - # self.dx = float() - # self.dz = float() - # self.dP_x = float() - # self.dP_z = float() def add_coord(self, airfoil, stringer_u_1, stringer_u_2, @@ -331,6 +291,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 @@ -399,6 +360,7 @@ class Stringer(Coordinates): def plot_geom(airfoil): """This function plots the airfoil's + sub-components' geometry.""" + # Plot chord x_chord = [0, airfoil.chord] y_chord = [0, 0] @@ -432,7 +394,6 @@ def plot_geom(airfoil): # Graph formatting plt.xlabel('X axis') plt.ylabel('Z axis') - 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)) diff --git a/evaluator.py b/evaluator.py index 2bef652..cab2e9b 100644 --- a/evaluator.py +++ b/evaluator.py @@ -13,7 +13,7 @@ # 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, +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. @@ -42,9 +42,6 @@ class Evaluator: + airfoil.spar.mass + airfoil.stringer.mass) self.mass_dist = [] - # Coordinates - self.x = airfoil.x - self.z = airfoil.z # Lift self.lift_rectangular = [] self.lift_elliptical = [] @@ -65,29 +62,33 @@ class Evaluator: name = ' EVALUATOR DATA ' num_of_dashes = len(name) - print(num_of_dashes * '-') - print(name) - print('Evaluating:', self.airfoil) - print('Chord length:', self.chord) - print('Semi-span:', self.semi_span) - 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_['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 * '-') - print('Rectangular lift along semi-span:\n', - np.around(self.lift_rectangular, round)) - print('Elliptical lift along semi-span:\n', - np.around(self.lift_elliptical, round)) - print('Combined lift along semi-span:\n', - np.around(self.lift_total, round)) - print('Distribution of mass along semi-span:\n', - np.around(self.mass_dist, round)) - print('Drag along semi-span:\n', np.around(self.drag, round)) + try: + print(num_of_dashes * '-') + print(name) + print('Evaluating:', self.airfoil) + print('Chord length:', self.chord) + print('Semi-span:', self.semi_span) + 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_['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 * '-') + print('Rectangular lift along semi-span:\n', + np.around(self.lift_rectangular, round)) + print('Elliptical lift along semi-span:\n', + np.around(self.lift_elliptical, round)) + print('Combined lift along semi-span:\n', + np.around(self.lift_total, round)) + print('Distribution of mass along semi-span:\n', + np.around(self.mass_dist, round)) + print('Drag along semi-span:\n', np.around(self.drag, round)) + except AttributeError: + print(num_of_dashes * '-') + print('Cannot print full evaluation. Was the airfoil analyzed?') return None def info_save(self, save_path, number): @@ -246,11 +247,11 @@ def plot_geom(evaluator): 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] + 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.x] - y = [1.02 * z for z in evaluator.z] + 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 @@ -279,7 +280,7 @@ def plot_geom(evaluator): plt.xlabel('X axis') plt.ylabel('Z axis') - plot_bound = max(evaluator.x) + 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') diff --git a/generator.py b/generator.py index 0e8da8b..6c9d03c 100644 --- a/generator.py +++ b/generator.py @@ -13,18 +13,18 @@ # 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, +The generator.py module contains a single Population class, which represents a collection of randomized airfoils. """ -import creator as cr +import creator -class Population(cr.Airfoil, cr.Spar, cr.Stringer): +class Population(creator.Airfoil): """Collection of random airfoils.""" def __init__(self, size): - af = cr.Airfoil + af = creator.Airfoil # print(af) self.size = size self.gen_number = 0 # incremented for every generation @@ -57,20 +57,17 @@ def main(): 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) # Interate through all wings in population, creating and evaluating them. for _ in range(1, POP_SIZE + 1): # Create airfoil instance - af = creator.Airfoil() + af = creator.Airfoil.from_dimensions(CHORD_LENGTH, SEMI_SPAN) # 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() @@ -81,8 +78,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() @@ -95,24 +92,24 @@ 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(creator)) # print(help(evaluator)) # print(help(generator)) |