summaryrefslogtreecommitdiff
path: root/creator/wing.py
diff options
context:
space:
mode:
authorblendoit <blendoit@gmail.com>2019-09-30 18:42:34 -0700
committerblendoit <blendoit@gmail.com>2019-09-30 18:42:34 -0700
commit588c34a3d595fcad5e93b8d4893f1098ce64d046 (patch)
tree0afc8ab9588845080b46c31ce62b725d9de3f0a8 /creator/wing.py
First commit!
Changed coordinate lists into numpy arrays.
Diffstat (limited to 'creator/wing.py')
-rw-r--r--creator/wing.py375
1 files changed, 375 insertions, 0 deletions
diff --git a/creator/wing.py b/creator/wing.py
new file mode 100644
index 0000000..4988cb5
--- /dev/null
+++ b/creator/wing.py
@@ -0,0 +1,375 @@
+"""
+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 sys
+import os.path
+import logging
+import numpy as np
+from math import sin, cos, atan
+import bisect as bi
+import matplotlib.pyplot as plt
+
+logging.basicConfig(filename='log.txt',
+ level=logging.DEBUG,
+ format='%(asctime)s - %(levelname)s - %(message)s')
+
+
+class Component:
+ """Basic component providing coordinates and tools."""
+
+ # TODO: define defaults in separate module
+ def __init__(self):
+ self.x = np.array([])
+ self.z = np.array([])
+ self.material = str()
+ self.mass = float()
+
+ def set_material(self, material):
+ """Set the component bulk material."""
+ self.material = material
+
+ 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) != list:
+ print('{}:\n'.format(k), v)
+ print(num_of_dashes * '-')
+ for k, v in self.__dict__.items():
+ if type(v) == list:
+ print('{}:\n'.format(k), np.around(v, round))
+ return None
+
+ def info_save(self, save_path, number):
+ """Save all the object's coordinates (must be full path)."""
+ file_name = f'{str(self).lower()}_{number}.txt'
+ full_path = os.path.join(save_path, file_name)
+ try:
+ with open(full_path, 'w') as sys.stdout:
+ self.info_print(6)
+ # This line required to reset behavior of sys.stdout
+ sys.stdout = sys.__stdout__
+ 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
+
+
+class Airfoil(Component):
+ """This class represents a single NACA airfoil.
+
+ The coordinates are saved as two lists
+ for the x- and z-coordinates. The coordinates start at
+ the leading edge, travel over the airfoil's upper edge,
+ then loop back to the leading edge via the lower edge.
+
+ This method was chosen for easier future exports
+ to 3D CAD packages like SolidWorks, which can import such
+ geometry as coordinates written in a CSV file.
+ """
+
+ # TODO: default values in separate module
+ def __init__(self, chord, semi_span, material):
+ super().__init__()
+ # self.x = np.array([])
+ # self.z = np.array([])
+ # self.chord = chord
+ """Create airfoil from its chord and semi-span."""
+ self.chord = chord if chord > 20 else 20
+ if chord <= 20:
+ logging.debug('Chord too small, using minimum value of 20.')
+ self.semi_span = semi_span
+ self.material = material
+ self.naca_num = int()
+
+ def __str__(self):
+ return type(self).__name__
+
+ def add_naca(self, naca_num):
+ """Generate surface geometry for a NACA airfoil.
+
+ The nested functions perform the required steps to generate geometry,
+ and can be called to solve the geometry y-coordinate for any 'x' input.
+ Equation coefficients were retrieved from Wikipedia.org.
+
+ Parameters:
+ naca_num: 4-digit NACA wing
+
+ Return:
+ None
+ """
+ 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(Component):
+ """Contains a single spar's data."""
+ def __init__(self, airfoil, loc_percent, material):
+ """Set spar location as percent of chord length."""
+ super().__init__()
+ super().set_material(material)
+ self.cap_area = float()
+ loc = loc_percent * airfoil.chord
+ # bi.bisect_left: returns index of first value in airfoil.x > loc
+ # This ensures that spar geom intersects with airfoil geom.
+ # Spar upper coordinates
+ spar_u = bi.bisect_left(airfoil.x, loc) - 1
+ self.x = np.append(self.x, airfoil.x[spar_u])
+ self.z = np.append(self.z, airfoil.z[spar_u])
+ # Spar lower coordinates
+ spar_l = bi.bisect_left(airfoil.x[::-1], loc)
+ self.x = np.append(self.x, airfoil.x[-spar_l])
+ self.z = np.append(self.z, airfoil.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(Component):
+ """Contains the coordinates of all stringers."""
+ def __init__(self):
+ super().__init__()
+ self.x_start = []
+ self.x_end = []
+ self.z_start = []
+ self.z_end = []
+ self.diameter = float()
+ self.area = float()
+
+ def add_coord(self, airfoil, spars, stringer_u_1, stringer_u_2,
+ stringer_l_1, stringer_l_2):
+ """Add equally distributed stringers to four airfoil locations
+ (upper nose, lower nose, upper surface, lower surface).
+
+ Parameters:
+ airfoil_coord: packed airfoil coordinates
+ spar_coord: packed spar coordinates
+ stringer_u_1: upper nose number of stringers
+ stringer_u_2: upper surface number of stringers
+ stringer_l_1: lower nose number of stringers
+ stringer_l_2: lower surface number of stringers
+
+ Returns:
+ None
+ """
+
+ # Find distance between leading edge and first upper stringer
+ interval = spars.x[0][0] / (stringer_u_1 + 1)
+ # initialise first self.stringer_x at first interval
+ x = interval
+ # Add upper stringers from leading edge until first spar.
+ for _ in range(0, stringer_u_1):
+ # Index of the first value of airfoil.x > x
+ i = bi.bisect_left(airfoil.x, x)
+ self.x.append(airfoil.x[i])
+ self.z.append(airfoil.z[i])
+ x += interval
+ # Add upper stringers from first spar until last spar
+ # TODO: stringer placement if only one spar is created
+ interval = (airfoil.spar.x[-1][0] -
+ airfoil.spar.x[0][0]) / (stringer_u_2 + 1)
+ x = interval + airfoil.spar.x[0][0]
+ for _ in range(0, stringer_u_2):
+ i = bi.bisect_left(airfoil.x, x)
+ self.x.append(airfoil.x[i])
+ self.z.append(airfoil.z[i])
+ x += interval
+
+ # Find distance between leading edge and first lower stringer
+ interval = airfoil.spar.x[0][1] / (stringer_l_1 + 1)
+ x = interval
+ # Add lower stringers from leading edge until first spar.
+ for _ in range(0, stringer_l_1):
+ i = bi.bisect_left(airfoil.x[::-1], x)
+ self.x.append(airfoil.x[-i])
+ self.z.append(airfoil.z[-i])
+ x += interval
+ # Add lower stringers from first spar until last spar
+ interval = (airfoil.spar.x[-1][1] -
+ airfoil.spar.x[0][1]) / (stringer_l_2 + 1)
+ x = interval + airfoil.spar.x[0][1]
+ for _ in range(0, stringer_l_2):
+ i = bi.bisect_left(airfoil.x[::-1], x)
+ self.x.append(airfoil.x[-i])
+ self.z.append(airfoil.z[-i])
+ x += interval
+ return None
+
+ def add_area(self, area):
+ self.area = area
+ return None
+
+ def add_mass(self, mass):
+ self.mass = len(self.x) * mass + len(self.x) * mass
+ return None
+
+ def add_webs(self, thickness):
+ """Add webs to stringers."""
+ for _ in range(len(self.x) // 2):
+ self.x_start.append(self.x[_])
+ self.x_end.append(self.x[_ + 1])
+ self.z_start.append(self.z[_])
+ self.z_end.append(self.z[_ + 1])
+ self.thickness = thickness
+ return None
+
+ def info_print(self, round):
+ super().info_print(round)
+ print('Stringer Area:\n', np.around(self.area, round))
+ return None
+
+
+def plot_geom(airfoil, spars, stringers):
+ """This function plots the airfoil's + sub-components' geometry."""
+ fig, ax = plt.subplots()
+
+ # Plot chord
+ x = [0, airfoil.chord]
+ y = [0, 0]
+ ax.plot(x, y, linewidth='1')
+ # Plot quarter chord
+ ax.plot(airfoil.chord / 4,
+ 0,
+ '.',
+ color='g',
+ markersize=24,
+ label='Quarter-chord')
+ # Plot mean camber line
+ ax.plot(airfoil.x_c,
+ airfoil.z_c,
+ '-.',
+ color='r',
+ linewidth='2',
+ label='Mean camber line')
+ # Plot airfoil surfaces
+ ax.plot(airfoil.x, airfoil.z, color='b', linewidth='1')
+
+ # Plot spars
+ try:
+ for spar in spars:
+ x = (spar.x)
+ y = (spar.z)
+ ax.plot(x, y, '-', color='y', linewidth='4')
+ except AttributeError:
+ print('No spars to plot.')
+ # Plot stringers
+ try:
+ for _ in range(0, len(airfoil.stringer.x)):
+ x = airfoil.stringer.x[_]
+ y = airfoil.stringer.z[_]
+ ax.plot(x, y, '.', color='y', markersize=12)
+ except AttributeError:
+ print('No stringers to plot.')
+
+ # Graph formatting
+ # plot_bound = np.amax(airfoil.x)
+ ax.set(
+ title='NACA ' + str(airfoil.naca_num) + ' airfoil',
+ xlabel='X axis',
+ # xlim=[-0.10 * plot_bound, 1.10 * plot_bound],
+ ylabel='Z axis')
+ # ylim=[-(1.10 * plot_bound / 2), (1.10 * plot_bound / 2)])
+
+ plt.grid(axis='both', linestyle=':', linewidth=1)
+ plt.gca().set_aspect('equal', adjustable='box')
+ plt.gca().legend(bbox_to_anchor=(1, 1),
+ bbox_transform=plt.gcf().transFigure)
+ plt.show()
+ return fig, ax
+
+
+def main():
+ return None
+
+
+if __name__ == '__main__':
+ main()
Copyright 2019--2024 Marius PETER