diff options
| -rw-r--r-- | creator.py | 894 | ||||
| -rw-r--r-- | evaluator.py | 9 | ||||
| -rw-r--r-- | main.py | 12 | 
3 files changed, 461 insertions, 454 deletions
| @@ -1,447 +1,447 @@ -# 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 <https://www.gnu.org/licenses/>.
 -
 -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
 -
 -# 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 airfoil components need the following:
 -
 -    Parameters:
 -        * Component material
 -        * Coordinates relative to the chord & semi-span.
 -
 -    Methods:
 -        * 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
 -        self.chord = chord
 -        if chord < 10:
 -            self.chord = 10
 -        self.semi_span = semi_span
 -        # mass and area
 -        self.mass = float()
 -        self.area = float()
 -        # Component material
 -        self.material = str()
 -        # Upper coordinates
 -        self.x_u = []
 -        self.z_u = []
 -        # Lower coordinates
 -        self.x_l = []
 -        self.z_l = []
 -        # Coordinates x_u, z_u, x_l, z_l packed in single list
 -        self.coord = []
 -
 -        # The airfoil components know the Coordinates instance's coords
 -        global parent
 -        parent = self
 -
 -    def __str__(self):
 -        return type(self).__name__
 -
 -    def print_info(self, round):
 -        """
 -        Print all the component's coordinates to the terminal.
 -
 -        This function's output is piped to the 'save_coord' function below.
 -        """
 -        print('============================')
 -        print('Component:', str(self))
 -        print('Chord length:', self.chord)
 -        print('Semi-span:', self.semi_span)
 -        print('Mass:', self.mass)
 -        print('============================')
 -        print('x_u the upper x-coordinates:\n', np.around(self.x_u, round))
 -        print('z_u the upper y-coordinates:\n', np.around(self.z_u, round))
 -        print('x_l the lower x-coordinates:\n', np.around(self.x_l, round))
 -        print('z_l the lower y-coordinates:\n', np.around(self.z_l, round))
 -        return None
 -
 -    def save_info(self, save_dir_path, number):
 -        """
 -        Save all the object's coordinates (must be full path).
 -        """
 -
 -        file_name = '{}_{}.txt'.format(self, number)
 -        full_path = os.path.join(save_dir_path, file_name)
 -        try:
 -            with open(full_path, 'w') as sys.stdout:
 -                self.print_info(2)
 -                # This line required to reset behavior of sys.stdout
 -                sys.stdout = sys.__stdout__
 -                print('Successfully wrote to file {}'.format(full_path))
 -        except:
 -            print('Unable to write {} to specified directory.\n'
 -                  .format(file_name),
 -                  'Was the full path passed to the function?')
 -        # It is cleaner to use this context guard to ensure file is closed
 -        return None
 -
 -    def pack_info(self):
 -        self.coord.append(self.x_u)
 -        self.coord.append(self.z_u)
 -        self.coord.append(self.x_l)
 -        self.coord.append(self.z_l)
 -        return None
 -
 -
 -class Airfoil(Coordinates):
 -    """This class enables the creation of a single 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 = []  # Contains only integers from 0 to self.chord
 -        self.y_c = []  # Contains floats
 -        # Thickness
 -        self.y_t = []
 -        # dy_c / d_x
 -        self.dy_c = []
 -        # Theta
 -        self.theta = []
 -
 -    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.
 -        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 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()
 -            z_u = float()
 -            if 0 <= x < self.chord:
 -                x_u = x - self.y_t[x] * sin(self.theta[x])
 -                z_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])
 -                z_u = 0  # Make upper curve finish at y = 0
 -            return (x_u, z_u)
 -
 -        def get_lower_coordinates(x):
 -            x_l = float()
 -            z_l = float()
 -            if 0 <= x < self.chord:
 -                x_l = (x + self.y_t[x] * sin(self.theta[x]))
 -                z_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]))
 -                z_l = 0  # Make lower curve finish at y = 0
 -            return (x_l, z_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.z_u.append(get_upper_coordinates(x)[1])
 -            self.x_l.append(get_lower_coordinates(x)[0])
 -            self.z_l.append(get_lower_coordinates(x)[1])
 -
 -        super().pack_info()
 -        return None
 -
 -    def add_mass(self, mass):
 -        self.mass = mass
 -
 -
 -class Spar(Coordinates):
 -    """Contains a single spar's location."""
 -    global parent
 -
 -    def __init__(self):
 -        super().__init__(parent.chord, parent.semi_span)
 -
 -    def add_coord(self, airfoil_coord, spar_x):
 -        """
 -        Add a single spar at the % chord location given to function.
 -
 -        Parameters:
 -        coordinates: provided by Airfoil.coordinates[x_u, z_u, x_l, z_l].
 -        material: spar's material. Assumes homogeneous material.
 -        spar_x: spar's location as a % of total chord length.
 -
 -        Return:
 -        None
 -        """
 -        # Airfoil surface coordinates
 -        # unpacked from 'coordinates' (list of lists in 'Coordinates').
 -        x_u = airfoil_coord[0]
 -        z_u = airfoil_coord[1]
 -        x_l = airfoil_coord[2]
 -        z_l = airfoil_coord[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.z_u.append(z_u[spar_x_u])
 -        self.x_l.append(x_l[spar_x_l])
 -        self.z_l.append(z_l[spar_x_l])
 -
 -        super().pack_info()
 -        return None
 -
 -    def add_mass(self, mass):
 -        self.mass = len(self.x_u) * mass
 -
 -
 -class Stringer(Coordinates):
 -    """Contains the coordinates of all stringers."""
 -    global parent
 -
 -    def __init__(self):
 -        super().__init__(parent.chord, parent.semi_span)
 -        self.area = float()
 -
 -    def add_coord(self, airfoil_coord, spar_coord,
 -                  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:
 -        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
 -        """
 -
 -        # Airfoil surface coordinates
 -        # unpacked from 'coordinates' (list of lists in 'Coordinates').
 -        airfoil_x_u = airfoil_coord[0]
 -        airfoil_z_u = airfoil_coord[1]
 -        airfoil_x_l = airfoil_coord[2]
 -        airfoil_z_l = airfoil_coord[3]
 -        # Spar coordinates
 -        # unpacked from 'coordinates' (list of lists in 'Coordinates').
 -        try:
 -            spar_x_u = spar_coord[0]
 -            spar_z_u = spar_coord[1]
 -            spar_x_l = spar_coord[2]
 -            spar_z_l = spar_coord[3]
 -        except:
 -            print('Unable to initialize stringers. Were spars created?')
 -
 -        # Find distance between leading edge and first upper stringer
 -        interval = spar_x_u[0] / (stringer_u_1 + 1)
 -        # initialise first self.stringer_x_u at first interval
 -        x = interval
 -        # Add upper stringers from leading edge until first spar.
 -        for _ in range(0, stringer_u_1):
 -            # Index of the first value of airfoil_x_u > x
 -            index = bi.bisect_left(airfoil_x_u, x)
 -            self.x_u.append(airfoil_x_u[index])
 -            self.z_u.append(airfoil_z_u[index])
 -            x += interval
 -        # Add upper stringers from first spar until last spar
 -        interval = (spar_x_u[-1] - spar_x_u[0]) / (stringer_u_2 + 1)
 -        x = interval + spar_x_u[0]
 -        for _ in range(0, stringer_u_2):
 -            index = bi.bisect_left(airfoil_x_u, x)
 -            self.x_u.append(airfoil_x_u[index])
 -            self.z_u.append(airfoil_z_u[index])
 -            x += interval
 -
 -        # Find distance between leading edge and first lower stringer
 -        interval = spar_x_l[0] / (stringer_l_1 + 1)
 -        x = interval
 -        # Add lower stringers from leading edge until first spar.
 -        for _ in range(0, stringer_l_1):
 -            index = bi.bisect_left(airfoil_x_l, x)
 -            self.x_l.append(airfoil_x_l[index])
 -            self.z_l.append(airfoil_z_l[index])
 -            x += interval
 -        # Add lower stringers from first spar until last spar
 -        interval = (spar_x_l[-1] - spar_x_l[0]) / (stringer_l_2 + 1)
 -        x = interval + spar_x_l[0]
 -        for _ in range(0, stringer_l_2):
 -            index = bi.bisect_left(airfoil_x_l, x)
 -            self.x_l.append(airfoil_x_l[index])
 -            self.z_l.append(airfoil_z_l[index])
 -            x += interval
 -        super().pack_info()
 -        return None
 -
 -    def add_area(self, area):
 -        self.area = area
 -        return None
 -
 -    def add_mass(self, mass):
 -        self.mass = len(self.x_u) * mass + len(self.x_l) * mass
 -        return None
 -
 -    def print_info(self, round):
 -        super().print_info(round)
 -        print('Stringer Area:\n', np.around(self.area, round))
 -        return None
 -
 -
 -def plot(airfoil, spar, stringer):
 -    """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.z_u, '', color='b', linewidth='1')
 -    # Plot lower surface
 -    plt.plot(airfoil.x_l, airfoil.z_l, '', color='b', linewidth='1')
 -
 -    # Plot spars
 -    try:
 -        for _ in range(0, len(spar.x_u)):
 -            x = (spar.x_u[_], spar.x_l[_])
 -            y = (spar.z_u[_], spar.z_l[_])
 -            plt.plot(x, y, '.-', color='b')
 -            # plt.legend()
 -    except:
 -        print('Did not plot spars. Were they added?')
 -
 -    # Plot stringers
 -    try:
 -        # Upper stringers
 -        for _ in range(0, len(stringer.x_u)):
 -            x = stringer.x_u[_]
 -            y = stringer.z_u[_]
 -            plt.plot(x, y, '.', color='y')
 -        # Lower stringers
 -        for _ in range(0, len(stringer.x_l)):
 -            x = stringer.x_l[_]
 -            y = stringer.z_l[_]
 -            plt.plot(x, y, '.', color='y')
 -    except:
 -        print('Unable to plot stringers. Were they created?')
 -
 -    # Graph formatting
 -    plt.gca().set_aspect('equal', adjustable='box')
 -    plt.xlabel('X axis')
 -    plt.ylabel('Z axis')
 -    plt.grid(axis='both', linestyle=':', linewidth=1)
 -    plt.show()
 -    return None
 -
 -
 -def main():
 -    return None
 -
 -
 -if __name__ == '__main__':
 -    main()
 +# 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 <https://www.gnu.org/licenses/>. + +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 + +# 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 airfoil components need the following: + +    Parameters: +        * Component material +        * Coordinates relative to the chord & semi-span. + +    Methods: +        * 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 +        self.chord = chord +        if chord < 10: +            self.chord = 10 +        self.semi_span = semi_span +        # mass and area +        self.mass = float() +        self.area = float() +        # Component material +        self.material = str() +        # Upper coordinates +        self.x_u = [] +        self.z_u = [] +        # Lower coordinates +        self.x_l = [] +        self.z_l = [] +        # Coordinates x_u, z_u, x_l, z_l packed in single list +        self.coord = [] + +        # The airfoil components know the Coordinates instance's coords +        global parent +        parent = self + +    def __str__(self): +        return type(self).__name__ + +    def print_info(self, round): +        """ +        Print all the component's coordinates to the terminal. + +        This function's output is piped to the 'save_coord' function below. +        """ +        print('============================') +        print('Component:', str(self)) +        print('Chord length:', self.chord) +        print('Semi-span:', self.semi_span) +        print('Mass:', self.mass) +        print('============================') +        print('x_u the upper x-coordinates:\n', np.around(self.x_u, round)) +        print('z_u the upper y-coordinates:\n', np.around(self.z_u, round)) +        print('x_l the lower x-coordinates:\n', np.around(self.x_l, round)) +        print('z_l the lower y-coordinates:\n', np.around(self.z_l, round)) +        return None + +    def save_info(self, save_dir_path, number): +        """ +        Save all the object's coordinates (must be full path). +        """ + +        file_name = '{}_{}.txt'.format(self, number) +        full_path = os.path.join(save_dir_path, file_name) +        try: +            with open(full_path, 'w') as sys.stdout: +                self.print_info(2) +                # This line required to reset behavior of sys.stdout +                sys.stdout = sys.__stdout__ +                print('Successfully wrote to file {}'.format(full_path)) +        except: +            print('Unable to write {} to specified directory.\n' +                  .format(file_name), +                  'Was the full path passed to the function?') +        # It is cleaner to use this context guard to ensure file is closed +        return None + +    def pack_info(self): +        self.coord.append(self.x_u) +        self.coord.append(self.z_u) +        self.coord.append(self.x_l) +        self.coord.append(self.z_l) +        return None + + +class Airfoil(Coordinates): +    """This class enables the creation of a single 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 = []  # Contains only integers from 0 to self.chord +        self.y_c = []  # Contains floats +        # Thickness +        self.y_t = [] +        # dy_c / d_x +        self.dy_c = [] +        # Theta +        self.theta = [] + +    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. +        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 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() +            z_u = float() +            if 0 <= x < self.chord: +                x_u = x - self.y_t[x] * sin(self.theta[x]) +                z_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]) +                z_u = 0  # Make upper curve finish at y = 0 +            return (x_u, z_u) + +        def get_lower_coordinates(x): +            x_l = float() +            z_l = float() +            if 0 <= x < self.chord: +                x_l = (x + self.y_t[x] * sin(self.theta[x])) +                z_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])) +                z_l = 0  # Make lower curve finish at y = 0 +            return (x_l, z_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.z_u.append(get_upper_coordinates(x)[1]) +            self.x_l.append(get_lower_coordinates(x)[0]) +            self.z_l.append(get_lower_coordinates(x)[1]) + +        super().pack_info() +        return None + +    def add_mass(self, mass): +        self.mass = mass + + +class Spar(Coordinates): +    """Contains a single spar's location.""" +    global parent + +    def __init__(self): +        super().__init__(parent.chord, parent.semi_span) + +    def add_coord(self, airfoil_coord, spar_x): +        """ +        Add a single spar at the % chord location given to function. + +        Parameters: +        coordinates: provided by Airfoil.coordinates[x_u, z_u, x_l, z_l]. +        material: spar's material. Assumes homogeneous material. +        spar_x: spar's location as a % of total chord length. + +        Return: +        None +        """ +        # Airfoil surface coordinates +        # unpacked from 'coordinates' (list of lists in 'Coordinates'). +        x_u = airfoil_coord[0] +        z_u = airfoil_coord[1] +        x_l = airfoil_coord[2] +        z_l = airfoil_coord[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.z_u.append(z_u[spar_x_u]) +        self.x_l.append(x_l[spar_x_l]) +        self.z_l.append(z_l[spar_x_l]) + +        super().pack_info() +        return None + +    def add_mass(self, mass): +        self.mass = len(self.x_u) * mass + + +class Stringer(Coordinates): +    """Contains the coordinates of all stringers.""" +    global parent + +    def __init__(self): +        super().__init__(parent.chord, parent.semi_span) +        self.area = float() + +    def add_coord(self, airfoil_coord, spar_coord, +                  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: +        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 +        """ + +        # Airfoil surface coordinates +        # unpacked from 'coordinates' (list of lists in 'Coordinates'). +        airfoil_x_u = airfoil_coord[0] +        airfoil_z_u = airfoil_coord[1] +        airfoil_x_l = airfoil_coord[2] +        airfoil_z_l = airfoil_coord[3] +        # Spar coordinates +        # unpacked from 'coordinates' (list of lists in 'Coordinates'). +        try: +            spar_x_u = spar_coord[0] +            spar_z_u = spar_coord[1] +            spar_x_l = spar_coord[2] +            spar_z_l = spar_coord[3] +        except: +            print('Unable to initialize stringers. Were spars created?') + +        # Find distance between leading edge and first upper stringer +        interval = spar_x_u[0] / (stringer_u_1 + 1) +        # initialise first self.stringer_x_u at first interval +        x = interval +        # Add upper stringers from leading edge until first spar. +        for _ in range(0, stringer_u_1): +            # Index of the first value of airfoil_x_u > x +            index = bi.bisect_left(airfoil_x_u, x) +            self.x_u.append(airfoil_x_u[index]) +            self.z_u.append(airfoil_z_u[index]) +            x += interval +        # Add upper stringers from first spar until last spar +        interval = (spar_x_u[-1] - spar_x_u[0]) / (stringer_u_2 + 1) +        x = interval + spar_x_u[0] +        for _ in range(0, stringer_u_2): +            index = bi.bisect_left(airfoil_x_u, x) +            self.x_u.append(airfoil_x_u[index]) +            self.z_u.append(airfoil_z_u[index]) +            x += interval + +        # Find distance between leading edge and first lower stringer +        interval = spar_x_l[0] / (stringer_l_1 + 1) +        x = interval +        # Add lower stringers from leading edge until first spar. +        for _ in range(0, stringer_l_1): +            index = bi.bisect_left(airfoil_x_l, x) +            self.x_l.append(airfoil_x_l[index]) +            self.z_l.append(airfoil_z_l[index]) +            x += interval +        # Add lower stringers from first spar until last spar +        interval = (spar_x_l[-1] - spar_x_l[0]) / (stringer_l_2 + 1) +        x = interval + spar_x_l[0] +        for _ in range(0, stringer_l_2): +            index = bi.bisect_left(airfoil_x_l, x) +            self.x_l.append(airfoil_x_l[index]) +            self.z_l.append(airfoil_z_l[index]) +            x += interval +        super().pack_info() +        return None + +    def add_area(self, area): +        self.area = area +        return None + +    def add_mass(self, mass): +        self.mass = len(self.x_u) * mass + len(self.x_l) * mass +        return None + +    def print_info(self, round): +        super().print_info(round) +        print('Stringer Area:\n', np.around(self.area, round)) +        return None + + +def plot(airfoil, spar, stringer): +    """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.z_u, '', color='b', linewidth='1') +    # Plot lower surface +    plt.plot(airfoil.x_l, airfoil.z_l, '', color='b', linewidth='1') + +    # Plot spars +    try: +        for _ in range(0, len(spar.x_u)): +            x = (spar.x_u[_], spar.x_l[_]) +            y = (spar.z_u[_], spar.z_l[_]) +            plt.plot(x, y, '.-', color='b') +            # plt.legend() +    except: +        print('Did not plot spars. Were they added?') + +    # Plot stringers +    try: +        # Upper stringers +        for _ in range(0, len(stringer.x_u)): +            x = stringer.x_u[_] +            y = stringer.z_u[_] +            plt.plot(x, y, '.', color='y') +        # Lower stringers +        for _ in range(0, len(stringer.x_l)): +            x = stringer.x_l[_] +            y = stringer.z_l[_] +            plt.plot(x, y, '.', color='y') +    except: +        print('Unable to plot stringers. Were they created?') + +    # Graph formatting +    plt.gca().set_aspect('equal', adjustable='box') +    plt.xlabel('X axis') +    plt.ylabel('Z axis') +    plt.grid(axis='both', linestyle=':', linewidth=1) +    plt.show() +    return None + + +def main(): +    return None + + +if __name__ == '__main__': +    main() diff --git a/evaluator.py b/evaluator.py index 4a65a4e..69a589a 100644 --- a/evaluator.py +++ b/evaluator.py @@ -17,7 +17,14 @@  def get_centroid(airfoil): -    pass +    area = airfoil.stringer.area +    numerator = float() +    for _ in airfoil.stringer.x_u: +        numerator += _ * area +    for _ in airfoil.stringer.x_l: +        numerator += _ * area +    denominator +    # z_c =  def get_total_mass(self, *component): @@ -56,7 +56,7 @@ def main():          # Define NACA airfoil coordinates and mass          af.add_naca(2412)          af.add_mass(AIRFOIL_MASS) -        # af.print_info(2) +        af.print_info(2)          # Create spar instance          af.spar = creator.Spar() @@ -64,7 +64,7 @@ def main():          af.spar.add_coord(af.coord, 0.15)          af.spar.add_coord(af.coord, 0.55)          af.spar.add_mass(SPAR_MASS) -        # af.spar.print_info(2) +        af.spar.print_info(2)          # Create stringer instance          af.stringer = creator.Stringer() @@ -75,12 +75,12 @@ def main():          af.stringer.print_info(2)          # Plot components with matplotlib -        # creator.plot(af, af.spar, af.stringer) +        creator.plot(af, af.spar, af.stringer)          # Save component info -        # af.save_info(SAVE_PATH, _) -        # af.spar.save_info(SAVE_PATH, _) -        # af.stringer.save_info(SAVE_PATH, _) +        af.save_info(SAVE_PATH, _) +        af.spar.save_info(SAVE_PATH, _) +        af.stringer.save_info(SAVE_PATH, _)      # Evaluate previously created airfoil(s).      total_mass = evaluator.get_total_mass(af, af.spar, af.stringer) | 
