summaryrefslogtreecommitdiff
path: root/aircraftstudio
diff options
context:
space:
mode:
authorblendoit <blendoit@gmail.com>2019-11-01 18:12:34 -0700
committerblendoit <blendoit@gmail.com>2019-11-01 18:12:34 -0700
commit8b6f11119790c8c930734894a37d2a4aaa42462d (patch)
tree9d6b9013ad4522f9a5598f30b4d3a0fcd26810ac /aircraftstudio
parent5ab73817371c1b4fedbd98838d3cf28984d73004 (diff)
Start work on optimized multiprocessing random a/c gen. & eval.HEADmaster
Diffstat (limited to 'aircraftstudio')
-rw-r--r--aircraftstudio/__init__.py1
-rw-r--r--aircraftstudio/creator/__init__.py4
-rw-r--r--aircraftstudio/creator/base.py112
-rw-r--r--aircraftstudio/creator/fuselage.py0
-rw-r--r--aircraftstudio/creator/propulsion.py0
-rw-r--r--aircraftstudio/creator/wing.py312
-rw-r--r--aircraftstudio/evaluator/__init__.py1
-rw-r--r--aircraftstudio/evaluator/dP.py18
-rw-r--r--aircraftstudio/evaluator/drag.py18
-rw-r--r--aircraftstudio/evaluator/evaluator.py195
-rw-r--r--aircraftstudio/evaluator/inertia.py66
-rw-r--r--aircraftstudio/evaluator/lift.py30
-rw-r--r--aircraftstudio/evaluator/mass.py8
-rw-r--r--aircraftstudio/generator/__init__.py1
-rw-r--r--aircraftstudio/generator/generator.py43
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)"""
Copyright 2019--2024 Marius PETER