diff options
Diffstat (limited to 'aircraftstudio')
| -rw-r--r-- | aircraftstudio/__init__.py | 1 | ||||
| -rw-r--r-- | aircraftstudio/creator/__init__.py | 4 | ||||
| -rw-r--r-- | aircraftstudio/creator/base.py | 112 | ||||
| -rw-r--r-- | aircraftstudio/creator/fuselage.py | 0 | ||||
| -rw-r--r-- | aircraftstudio/creator/propulsion.py | 0 | ||||
| -rw-r--r-- | aircraftstudio/creator/wing.py | 312 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/__init__.py | 1 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/dP.py | 18 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/drag.py | 18 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/evaluator.py | 195 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/inertia.py | 66 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/lift.py | 30 | ||||
| -rw-r--r-- | aircraftstudio/evaluator/mass.py | 8 | ||||
| -rw-r--r-- | aircraftstudio/generator/__init__.py | 1 | ||||
| -rw-r--r-- | aircraftstudio/generator/generator.py | 43 | 
15 files changed, 809 insertions, 0 deletions
| diff --git a/aircraftstudio/__init__.py b/aircraftstudio/__init__.py new file mode 100644 index 0000000..dda0cea --- /dev/null +++ b/aircraftstudio/__init__.py @@ -0,0 +1 @@ +from . import creator, evaluator, generator diff --git a/aircraftstudio/creator/__init__.py b/aircraftstudio/creator/__init__.py new file mode 100644 index 0000000..818c7b2 --- /dev/null +++ b/aircraftstudio/creator/__init__.py @@ -0,0 +1,4 @@ +from . import base +from . import fuselage +from . import propulsion +from . import wing diff --git a/aircraftstudio/creator/base.py b/aircraftstudio/creator/base.py new file mode 100644 index 0000000..92fe421 --- /dev/null +++ b/aircraftstudio/creator/base.py @@ -0,0 +1,112 @@ +"""The base.py module contains parent classes for components.""" + +import numpy as np +import os.path +import random +import logging + +from aircraftstudio import creator + +logging.basicConfig(filename='log_base.txt', +                    level=logging.DEBUG, +                    format='%(asctime)s - %(levelname)s - %(message)s') + + +class Aircraft: +    """This class tracks all sub-components and is fed to the evaluator.""" +    name = None +    fuselage = None +    propulsion = None +    wing = None +    properties = {} + +    naca = [2412, 3412, 2420] + +    def __init__(self): +        self.results = {} + +    def __str__(self): +        return self.name + +    @classmethod +    def from_default(cls): +        aircraft = Aircraft() +        aircraft.name = 'default_aircraft_' + str(random.randrange(1000, 9999)) +        airfoil = creator.wing.Airfoil(aircraft, 'default_airfoil') +        airfoil.add_naca(2412) +        soar1 = creator.wing.Spar(airfoil, 'default_spar_1', 0.30) +        soar2 = creator.wing.Spar(airfoil, 'default_spar_2', 0.60) +        stringer = creator.wing.Stringer(airfoil, 'default_stringer') +        return aircraft + +    @classmethod +    def from_random(cls): +        aircraft = Aircraft() +        aircraft.name = 'random_aircraft_' + str(random.randrange(1000, 9999)) +        airfoil = creator.wing.Airfoil(aircraft, 'random_airfoil') +        airfoil.add_naca(random.choice(cls.naca)) +        soar1 = creator.wing.Spar(airfoil, 'random_spar_1', +                                  random.randrange(20, 80) / 100) +        soar2 = creator.wing.Spar(airfoil, 'random_spar_2', +                                  random.randrange(20, 80) / 100) +        stringer = creator.wing.Stringer(airfoil, 'random_stringer', +                                         random.randint(1, 10), +                                         random.randint(1, 10), +                                         random.randint(1, 10), +                                         random.randint(1, 10)) +        return aircraft + + +class Component: +    """Basic component providing coordinates, tools and a component tree.""" +    def __init__(self, parent, name): +        self.parent = parent +        self.name = name +        self.x = np.array([]) +        self.z = np.array([]) +        self.y = np.array([]) +        self.material = None +        self.mass = float() +        self.properties = {} + +    def __str__(self): +        return self.name + +    def info_print(self, round): +        """Print all the component's coordinates to the terminal.""" +        name = f'    CREATOR DATA FOR {str(self).upper()}    ' +        num_of_dashes = len(name) +        print(num_of_dashes * '-') +        print(name) +        for k, v in self.__dict__.items(): +            if type(v) is not np.ndarray: +                print(f'{k}:\n', v) +                print(num_of_dashes * '-') +        for k, v in self.__dict__.items(): +            if type(v) is np.ndarray: +                print(f'{k}:\n', np.around(v, round)) +        return None + +    def info_save(self, +                  save_path='/home/blendux/Projects/Aircraft_Studio/save'): +        """Save all the object's coordinates (must be full path).""" +        file_name = f'{self.name}_info.txt' +        full_path = os.path.join(save_path, file_name) +        try: +            with open(full_path, 'w') as f: +                for k, v in self.__dict__.items(): +                    if type(v) is not np.ndarray: +                        f.write(f'{k}=\n') +                        f.write(str(v)) +                        f.write("\n") +                        # print(num_of_dashes * '-') +                for k, v in self.__dict__.items(): +                    if type(v) is np.ndarray: +                        f.write(f'{k}=\n') +                        f.write(str(v)) +                        f.write("\n") +            logging.debug(f'Successfully wrote to file {full_path}') +        except IOError: +            print(f'Unable to write {file_name} to specified directory.\n', +                  'Was the full path passed to the function?') +        return None diff --git a/aircraftstudio/creator/fuselage.py b/aircraftstudio/creator/fuselage.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aircraftstudio/creator/fuselage.py diff --git a/aircraftstudio/creator/propulsion.py b/aircraftstudio/creator/propulsion.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/aircraftstudio/creator/propulsion.py diff --git a/aircraftstudio/creator/wing.py b/aircraftstudio/creator/wing.py new file mode 100644 index 0000000..afe52fe --- /dev/null +++ b/aircraftstudio/creator/wing.py @@ -0,0 +1,312 @@ +""" +The wing.py module contains class definitions for 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 logging +import numpy as np +from math import sin, cos, atan +import bisect as bi +import matplotlib.pyplot as plt + +from aircraftstudio.creator import base +import resources.materials as mt + + +class Airfoil(base.Component): +    """This class represents a single NACA airfoil. + +    The coordinates are saved as two np.arrays +    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, +                 parent, +                 name, +                 chord=68, +                 semi_span=150, +                 material=mt.aluminium): +        super().__init__(parent, name) +        parent.wing = self +        if chord > 20: +            self.chord = chord +        else: +            self.chord = 20 +            logging.debug('Chord too small, using minimum value of 20.') +        parent +        self.semi_span = semi_span +        self.material = material +        self.spars = [] +        self.stringers = [] + +    def add_naca(self, naca_num=2412): +        """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 +        """ +        self.naca_num = naca_num +        # Variables extracted from naca_num argument passed to the function +        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_coord_u(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_coord_l(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)) +        # Generate our airfoil skin geometry from previous sub-functions +        self.x_c = np.array([]) +        self.z_c = np.array([]) +        # Upper surface and camber line +        for x in x_chord: +            self.x_c = np.append(self.x_c, x) +            self.z_c = np.append(self.z_c, get_camber(x)) +            self.x = np.append(self.x, get_coord_u(x)[0]) +            self.z = np.append(self.z, get_coord_u(x)[1]) +        # Lower surface +        for x in x_chord[::-1]: +            self.x = np.append(self.x, get_coord_l(x)[0]) +            self.z = np.append(self.z, get_coord_l(x)[1]) +        return None + + +class Spar(base.Component): +    """Contains a single spar's data.""" +    def __init__(self, parent, name, loc_percent=0.30, material=mt.aluminium): +        """Set spar location as percent of chord length.""" +        super().__init__(parent, name) +        parent.spars.append(self) +        self.material = material +        self.cap_area = float() +        # bi.bisect_left: returns index of first value in parent.x > loc +        # This ensures that spar geom intersects with airfoil geom. +        loc = loc_percent * parent.chord +        # Spar upper coordinates +        spar_u = bi.bisect_left(parent.x, loc) - 1 +        self.x = np.append(self.x, parent.x[spar_u]) +        self.z = np.append(self.z, parent.z[spar_u]) +        # Spar lower coordinates +        spar_l = bi.bisect_left(parent.x[::-1], loc) +        self.x = np.append(self.x, parent.x[-spar_l]) +        self.z = np.append(self.z, parent.z[-spar_l]) +        return None + +    def set_cap_area(self, cap_area): +        self.cap_area = cap_area +        return None + +    def set_mass(self, mass): +        self.mass = mass +        return None + + +class Stringer(base.Component): +    """Contains the coordinates of all stringers.""" +    def __init__(self, +                 parent, +                 name, +                 den_u_1=4, +                 den_u_2=4, +                 den_l_1=4, +                 den_l_2=4): +        """Add equally distributed stringers to four airfoil locations +        (upper nose, lower nose, upper surface, lower surface). + +        den_u_1: upper nose number of stringers +        den_u_2: upper surface number of stringers +        den_l_1: lower nose number of stringers +        den_l_2: lower surface number of stringers +        """ +        super().__init__(parent, name) +        parent.stringers = self +        self.x_start = [] +        self.x_end = [] +        self.z_start = [] +        self.z_end = [] +        self.diameter = float() +        self.area = float() + +        # Find distance between leading edge and first upper stringer +        # interval = self.parent.spars[0].x[0] / (den_u_1 + 1) +        interval = 2 +        # initialise first self.stringer_x at first interval +        x = interval +        # Add upper stringers from leading edge until first spar. +        for _ in range(0, den_u_1): +            # Index of the first value of airfoil.x > x +            i = bi.bisect_left(self.parent.x, x) +            self.x = np.append(self.x, self.parent.x[i]) +            self.z = np.append(self.z, self.parent.z[i]) +            x += interval +            # Add upper stringers from first spar until last spar +        interval = (self.parent.spars[-1].x[0] - +                    self.parent.spars[0].x[0]) / (den_u_2 + 1) +        x = interval + self.parent.spars[0].x[0] +        for _ in range(0, den_u_2): +            i = bi.bisect_left(self.parent.x, x) +            self.x = np.append(self.x, self.parent.x[i]) +            self.z = np.append(self.z, self.parent.z[i]) +            x += interval + +        # Find distance between leading edge and first lower stringer +        interval = self.parent.spars[0].x[1] / (den_l_1 + 1) +        x = interval +        # Add lower stringers from leading edge until first spar. +        for _ in range(0, den_l_1): +            i = bi.bisect_left(self.parent.x[::-1], x) +            self.x = np.append(self.x, self.parent.x[-i]) +            self.z = np.append(self.z, self.parent.z[-i]) +            x += interval +            # Add lower stringers from first spar until last spar +        interval = (self.parent.spars[-1].x[1] - +                    self.parent.spars[0].x[1]) / (den_l_2 + 1) +        x = interval + self.parent.spars[0].x[1] +        for _ in range(0, den_l_2): +            i = bi.bisect_left(self.parent.x[::-1], x) +            self.x = np.append(self.x, self.parent.x[-i]) +            self.z = np.append(self.z, self.parent.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=2): +        super().info_print(round) +        print('Stringer Area:\n', np.around(self.area, round)) +        return None + + +def plot_geom(airfoil): +    """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') + +    try:  # Plot spars +        for spar in airfoil.spars: +            x = (spar.x) +            y = (spar.z) +            ax.plot(x, y, '-', color='y', linewidth='4') +    except AttributeError: +        print('No spars to plot.') +    try:  # Plot stringers +        for i in range(len(airfoil.stringers.x)): +            x = airfoil.stringers.x[i] +            y = airfoil.stringers.z[i] +            ax.plot(x, y, '.', color='y', markersize=12) +    except AttributeError: +        print('No stringers to plot.') + +    ax.set(title='NACA ' + str(airfoil.naca_num) + ' airfoil', +           xlabel='X axis', +           ylabel='Z axis') + +    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) +    plt.show() +    return fig, ax diff --git a/aircraftstudio/evaluator/__init__.py b/aircraftstudio/evaluator/__init__.py new file mode 100644 index 0000000..a58168b --- /dev/null +++ b/aircraftstudio/evaluator/__init__.py @@ -0,0 +1 @@ +from .evaluator import * diff --git a/aircraftstudio/evaluator/dP.py b/aircraftstudio/evaluator/dP.py new file mode 100644 index 0000000..b6aaa3b --- /dev/null +++ b/aircraftstudio/evaluator/dP.py @@ -0,0 +1,18 @@ +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 diff --git a/aircraftstudio/evaluator/drag.py b/aircraftstudio/evaluator/drag.py new file mode 100644 index 0000000..73a26fc --- /dev/null +++ b/aircraftstudio/evaluator/drag.py @@ -0,0 +1,18 @@ +import random + +def get_drag(aircraft, drag): +    # Transform semi-span integer into list +    semi_span = [x for x in range(0, aircraft.wing.semi_span)] + +    # Drag increases after 80% of the semi_span +    cutoff = round(0.8 * aircraft.wing.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_drag_total(aircraft): +    """Get total drag force acting on the aircraft.""" +    return random.random() * 100 diff --git a/aircraftstudio/evaluator/evaluator.py b/aircraftstudio/evaluator/evaluator.py new file mode 100644 index 0000000..18bb692 --- /dev/null +++ b/aircraftstudio/evaluator/evaluator.py @@ -0,0 +1,195 @@ +""" +The evaluator.py module contains functions +that return calculated data for an aircraft. +Plotting aircraft components is also possible. +""" + +import os.path +import concurrent.futures +import matplotlib.pyplot as plt + +from . import drag, inertia, lift, mass + + +def analyze(aircraft): +    """Analyze a single aircraft.""" +    results = { +        'Lift': lift.get_lift_total(aircraft), +        'Drag': drag.get_drag_total(aircraft), +        'Mass': mass.get_mass_total(aircraft), +        'Centroid': inertia.get_centroid(aircraft) +    } +    aircraft.results = results +    return aircraft.name, results + + +def analyze_all(population): +    """Analyze all aircraft in a given population.""" +    # for aircraft in population.aircrafts: +    #     print(analyze(aircraft)) +    with concurrent.futures.ProcessPoolExecutor() as executor: +        results = executor.map(analyze, population.aircrafts) +        for result in results: +            print(result) +    return None + +    # 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) +    #     print("yayyyyy") +    #     return None + +    # print(f"Analysis results for {aircraft.name}:\n", results) +    # self.results = self.get_lift_total(aircraft) + +    # 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 tree_print(self, population): +        """Print the list of subcomponents.""" +        name = f"    TREE FOR {[i.name for i in population.aircraft]} IN {self.name}    " +        num_of_dashes = len(name) +        print(num_of_dashes * '-') +        print(name) +        for aircraft in population: +            print(".") +            print(f"`-- {aircraft}") +            print(f"    |--{aircraft.wing}") +            print(f"    |   |-- {aircraft.wing.stringers}") +            for spar in aircraft.wing.spars[:-1]: +                print(f"    |   |-- {spar}") +            print(f"    |   `-- {aircraft.wing.spars[-1]}") +            print(f"    |-- {aircraft.fuselage}") +            print(f"    `-- {aircraft.propulsion}") +        print(num_of_dashes * '-') +        return None + +    def tree_save(self, +                  population, +                  save_path='/home/blendux/Projects/Aircraft_Studio/save'): +        """Save the evaluator's tree to a file.""" +        for aircraft in population.aircraft: +            file_name = f"{aircraft.name}_tree.txt" +            full_path = os.path.join(save_path, file_name) +            with open(full_path, 'w') as f: +                try: +                    f.write(".\n") +                    f.write(f"`-- {aircraft}\n") +                    f.write(f"    |--{aircraft.wing}\n") +                    for spar in aircraft.wing.spars[:-1]: +                        f.write(f"    |   |-- {spar}\n") +                    f.write(f"    |   `-- {aircraft.wing.spars[-1]}\n") +                    f.write(f"    |-- {aircraft.fuselage}\n") +                    f.write(f"    `-- {aircraft.propulsion}\n") +                    logging.debug(f'Successfully wrote to file {full_path}') + +                except IOError: +                    print( +                        f'Unable to write {file_name} to specified directory.', +                        'Was the full path passed to the function?') +        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/aircraftstudio/evaluator/inertia.py b/aircraftstudio/evaluator/inertia.py new file mode 100644 index 0000000..f047766 --- /dev/null +++ b/aircraftstudio/evaluator/inertia.py @@ -0,0 +1,66 @@ +def get_centroid(aircraft): +    """Return the coordinates of the centroid.""" +    # stringer_area = aircraft.wing.stringers.area +    # cap_area = aircraft.wing.spars.cap_area + +    # TODO: Fix this +    # caps_x = [value for spar in aircraft.wing.spars.x for value in spar] +    # caps_z = [value for spar in aircraft.wing.spars.z for value in spar] +    # stringers_x = aircraft.wing.stringers.x +    # stringers_z = aircraft.wing.stringers.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) +    return (200, 420) + + +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) diff --git a/aircraftstudio/evaluator/lift.py b/aircraftstudio/evaluator/lift.py new file mode 100644 index 0000000..516d649 --- /dev/null +++ b/aircraftstudio/evaluator/lift.py @@ -0,0 +1,30 @@ +import numpy as np +from math import sqrt + + +def _get_lift_rectangular(aircraft, lift=50): +    L_prime = np.array([ +        lift / (aircraft.wing.semi_span * 2) +        for _ in range(aircraft.wing.semi_span) +    ]) +    return L_prime + + +def _get_lift_elliptical(aircraft, L_0=3.2): +    L_prime = np.array([ +        0.5 * L_0 / (aircraft.wing.semi_span * 2) * +        sqrt(1 - (y / aircraft.wing.semi_span)**2) +        for y in range(aircraft.wing.semi_span) +    ]) +    return L_prime + + +def get_lift_total(aircraft): +    """Combination of rectangular and elliptical lift.""" +    # F_z = self._get_lift_rectangular(aircraft) + self._get_lift_elliptical( +    #     aircraft) +    # F_z = self._get_lift_rectangular( +    #     aircraft) + self._get_lift_elliptical(aircraft) / 2 +    # F_z = [i + j for i, j in self._get_lift_rectangular] +    # return F_z +    return 400 diff --git a/aircraftstudio/evaluator/mass.py b/aircraftstudio/evaluator/mass.py new file mode 100644 index 0000000..7edb926 --- /dev/null +++ b/aircraftstudio/evaluator/mass.py @@ -0,0 +1,8 @@ +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_mass_total(aircraft): +    """Get the total aircraft mass.""" +    return 2000 diff --git a/aircraftstudio/generator/__init__.py b/aircraftstudio/generator/__init__.py new file mode 100644 index 0000000..4f233cb --- /dev/null +++ b/aircraftstudio/generator/__init__.py @@ -0,0 +1 @@ +from .generator import * diff --git a/aircraftstudio/generator/generator.py b/aircraftstudio/generator/generator.py new file mode 100644 index 0000000..898683a --- /dev/null +++ b/aircraftstudio/generator/generator.py @@ -0,0 +1,43 @@ +""" +The generator.py module contains classes describing genetic populations +and methods to generate default aircraft. +""" + +from aircraftstudio import creator + + +def default_fuselage(): +    pass + + +def default_propulsion(): +    pass + + +class Population(): +    """Represents a collection of aircraft.""" +    def __init__(self, size): +        self.aircrafts = [] +        for _ in range(size): +            self.aircrafts.append(creator.base.Aircraft.from_random()) +        self.size = size +        self.results = None +        self.gen_number = 0  # incremented for every generation + +    #TODO class methods for default and random population +    # def from_default(self, size): +    #     for i in range(size): +    #         self.aircrafts.append(creator.base.Aircraft.from_default()) + +    # def from_random(self, size): +    #     for i in range(size): +    #         self.aircrafts.append(creator.base.Aircraft.from_random()) + +    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)""" | 
