summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--creator.py77
-rw-r--r--evaluator.py142
-rw-r--r--generator.py20
-rw-r--r--main.py33
4 files changed, 142 insertions, 130 deletions
diff --git a/creator.py b/creator.py
index 3a36bd8..3e36036 100644
--- a/creator.py
+++ b/creator.py
@@ -12,8 +12,19 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-
+"""
+The 'creator' module contains class definitions for coordinates
+and various components we add to an airfoil (spars, stringers, and ribs.)
+
+Classes:
+ Coordinates: always instantiated first, but never assigned to object.
+ Airfoil: inherits from Coordinates & automatically aware of airfoil size.
+ Spar: also inherits from Coordinates.
+ Stringer: also inherits from Coordinates.
+
+Functions:
+ plot_geom(airfoil): generates a 2D plot of the airfoil & any components.
+"""
import sys
import os.path
import numpy as np
@@ -30,19 +41,19 @@ global parent
class Coordinates:
- '''
+ """
All airfoil components need the following:
Parameters:
- * Component material
- * Coordinates relative to the chord & semi-span
+ Component material
+ Coordinates relative to the chord & semi-span
Methods:
- * Print component coordinates
- * Save component coordinates to file specified in main.py
+ 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
@@ -65,11 +76,11 @@ class Coordinates:
return type(self).__name__
def info_print(self, round):
- '''
+ """
Print all the component's coordinates to the terminal.
This function's output is piped to the 'save_coord' function below.
- '''
+ """
name = ' CREATOR DATA '
num_of_dashes = len(name)
@@ -85,10 +96,9 @@ class Coordinates:
return None
def info_save(self, save_path, number):
- '''
+ """
Save all the object's coordinates (must be full path).
- '''
-
+ """
file_name = '{}_{}.txt'.format(str(self).lower(), number)
full_path = os.path.join(save_path, file_name)
try:
@@ -105,8 +115,8 @@ class Coordinates:
class Airfoil(Coordinates):
- '''
- This class enables the creation of a single NACA airfoil.
+ """
+ This class represents a single NACA airfoil.
Please note: the coordinates are saved as two lists
for the x- and z-coordinates. The coordinates start at
@@ -116,7 +126,7 @@ class Airfoil(Coordinates):
This method was chosen for easier future exports
to 3D CAD packages like SolidWorks, which can import such
geometry as coordinates written in a CSV file.
- '''
+ """
def __init__(self):
global parent
@@ -129,7 +139,7 @@ class Airfoil(Coordinates):
self.z_c = []
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.
@@ -140,8 +150,7 @@ class Airfoil(Coordinates):
Return:
None
- '''
-
+ """
# Variables extracted from 'naca_num' argument passed to the function
self.naca_num = naca_num
m = int(str(naca_num)[0]) / 100
@@ -151,9 +160,9 @@ class Airfoil(Coordinates):
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)
@@ -165,8 +174,7 @@ class Airfoil(Coordinates):
return (z_c * self.chord)
def get_thickness(x):
- '''Returns thickness from 1 'x' along the airfoil chord.'''
-
+ """Returns thickness from 1 'x' along the airfoil chord."""
x = 0 if x < 0 else x
z_t = 5 * t * self.chord * (
+ 0.2969 * sqrt(x / self.chord)
@@ -227,7 +235,7 @@ class Airfoil(Coordinates):
class Spar(Coordinates):
- '''Contains a single spar's location.'''
+ """Contains a single spar's location."""
global parent
def __init__(self):
@@ -243,7 +251,7 @@ class Spar(Coordinates):
self.dP_z = float()
def add_coord(self, airfoil, x_loc_percent):
- '''
+ """
Add a single spar at the % chord location given to function.
Parameters:
@@ -252,8 +260,7 @@ class Spar(Coordinates):
Return:
None
- '''
-
+ """
# Scaled spar location with regards to chord
loc = x_loc_percent * self.chord
# bi.bisect_left: returns index of first value in airfoil.x > loc
@@ -279,8 +286,7 @@ class Spar(Coordinates):
return None
def add_webs(self, thickness):
- '''Add webs to spars.'''
-
+ """Add webs to spars."""
for _ in range(len(self.x)):
self.x_start.append(self.x[_][0])
self.x_end.append(self.x[_][1])
@@ -291,7 +297,7 @@ class Spar(Coordinates):
class Stringer(Coordinates):
- '''Contains the coordinates of all stringers.'''
+ """Contains the coordinates of all stringers."""
global parent
def __init__(self):
@@ -310,7 +316,7 @@ class Stringer(Coordinates):
def add_coord(self, airfoil,
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).
@@ -324,8 +330,7 @@ class Stringer(Coordinates):
Returns:
None
- '''
-
+ """
# Find distance between leading edge and first upper stringer
interval = airfoil.spar.x[0][0] / (stringer_u_1 + 1)
# initialise first self.stringer_x at first interval
@@ -377,8 +382,7 @@ class Stringer(Coordinates):
return None
def add_webs(self, thickness):
- '''Add webs to stringers.'''
-
+ """Add webs to stringers."""
for _ in range(len(self.x) // 2):
self.x_start.append(self.x[_])
self.x_end.append(self.x[_ + 1])
@@ -394,8 +398,7 @@ class Stringer(Coordinates):
def plot_geom(airfoil):
- '''This function plots the airfoil's + sub-components' geometry.'''
-
+ """This function plots the airfoil's + sub-components' geometry."""
# Plot chord
x_chord = [0, airfoil.chord]
y_chord = [0, 0]
diff --git a/evaluator.py b/evaluator.py
index 05900e9..44ed434 100644
--- a/evaluator.py
+++ b/evaluator.py
@@ -12,7 +12,12 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-
+"""
+The 'evaluator' module contains a single Evaluator class,
+which knows all the attributes of a specified Airfoil instance,
+and contains functions to analyse the airfoil's geometrical
+& structural properties.
+"""
import sys
import os.path
@@ -22,7 +27,7 @@ import matplotlib.pyplot as plt
class Evaluator:
- '''Performs structural evaluations for the airfoil passed as argument.'''
+ """Performs structural evaluations for the airfoil passed as argument."""
def __init__(self, airfoil):
# Evaluator knows all geometrical info from evaluated airfoil
@@ -32,8 +37,7 @@ class Evaluator:
# Global dimensions
self.chord = airfoil.chord
self.semi_span = airfoil.semi_span
-
- # mass and area
+ # Mass & spanwise distribution
self.mass_total = float(airfoil.mass
+ airfoil.spar.mass
+ airfoil.stringer.mass)
@@ -50,18 +54,14 @@ class Evaluator:
# centroid
self.centroid = []
# Inertia terms:
- # I_x = self.I_[0]
- # I_z = self.I_[1]
- # I_xz = self.I_[2]
- self.I_ = []
+ self.I_ = {'x': 0, 'z': 0, 'xz': 0}
def info_print(self, round):
- '''
+ """
Print all the component's evaluated data to the terminal.
This function's output is piped to the 'save_data' function below.
- '''
-
+ """
name = ' EVALUATOR DATA '
num_of_dashes = len(name)
@@ -73,9 +73,9 @@ class Evaluator:
print('Total airfoil mass:', self.mass_total)
print('Centroid location:\n', np.around(self.centroid, 3))
print('Inertia terms:')
- print('I_x:\n', np.around(self.I_[0], 3))
- print('I_z:\n', np.around(self.I_[1], 3))
- print('I_xz:\n', np.around(self.I_[2], 3))
+ print('I_x:\n', np.around(self.I_['x'], 3))
+ print('I_z:\n', np.around(self.I_['z'], 3))
+ print('I_xz:\n', np.around(self.I_['xz'], 3))
print('Spar dP_x:\n', self.spar.dP_x)
print('Spar dP_z:\n', self.spar.dP_z)
print(num_of_dashes * '-')
@@ -91,8 +91,7 @@ class Evaluator:
return None
def info_save(self, save_path, number):
- '''Save all the object's coordinates (must be full path).'''
-
+ """Save all the object's coordinates (must be full path)."""
file_name = 'airfoil_{}_eval.txt'.format(number)
full_path = os.path.join(save_path, file_name)
try:
@@ -102,22 +101,22 @@ class Evaluator:
sys.stdout = sys.__stdout__
print('Successfully wrote to file {}'.format(full_path))
except IOError:
- print('Unable to write {} to specified directory.\n'
- .format(file_name),
- 'Was the full path passed to the function?')
+ print(
+ 'Unable to write {} to specified directory.\n'.format(
+ file_name), 'Was the full path passed to the function?')
return None
# All these functions take integer arguments and return lists.
def get_lift_rectangular(self, lift):
- L_prime = [lift / (self.semi_span * 2)
- for x in range(self.semi_span)]
+ L_prime = [lift / (self.semi_span * 2) for x in range(self.semi_span)]
return L_prime
def get_lift_elliptical(self, L_0):
- L_prime = [L_0 / (self.semi_span * 2)
- * sqrt(1 - (y / self.semi_span) ** 2)
- for y in range(self.semi_span)]
+ L_prime = [
+ L_0 / (self.semi_span * 2) * sqrt(1 - (y / self.semi_span)**2)
+ for y in range(self.semi_span)
+ ]
return L_prime
def get_lift_total(self):
@@ -126,8 +125,7 @@ class Evaluator:
return F_z
def get_mass_distribution(self, total_mass):
- F_z = [total_mass / self.semi_span
- for x in range(0, self.semi_span)]
+ F_z = [total_mass / self.semi_span for x in range(0, self.semi_span)]
return F_z
def get_drag(self, drag):
@@ -143,8 +141,7 @@ class Evaluator:
return F_x
def get_centroid(self):
- '''Return the coordinates of the centroid.'''
-
+ """Return the coordinates of the centroid."""
stringer_area = self.stringer.area
cap_area = self.spar.cap_area
@@ -163,11 +160,11 @@ class Evaluator:
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)
- def get_inertia_terms(self):
- '''Obtain all inertia terms.'''
+ return (centroid_x, centroid_z)
+ def get_inertia_terms(self):
+ """Obtain all inertia terms."""
stringer_area = self.stringer.area
cap_area = self.spar.cap_area
@@ -180,72 +177,75 @@ class Evaluator:
spar_count = range(len(self.spar.x))
# I_x is the sum of the contributions of the spar caps and stringers
- I_x = (sum([cap_area * (z_spars[i] - self.centroid[1]) ** 2
+ # 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])
- + sum([stringer_area * (z_stringers[i] - self.centroid[1]) ** 2
- for i in stringer_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)
- I_z = (sum([cap_area * (x_spars[i] - self.centroid[0]) ** 2
- for i in spar_count])
- + 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])
- + sum([stringer_area * (x_stringers[i] - self.centroid[0])
- * (z_stringers[i] - self.centroid[1])
- for i in stringer_count]))
+ def get_dx(self, component):
+ return [x - self.centroid[0] for x in component.x_start]
- return(I_x, I_z, I_xz)
+ def get_dz(self, component):
+ return [x - self.centroid[1] for x in component.x_start]
def analysis(self, V_x, V_z):
- '''Perform all analysis calculations and store in class instance.'''
+ """Perform all analysis calculations and store in class instance."""
- def get_dp(xDist, zDist, V_x, V_z, I_x, I_z, I_xz, area):
+ def get_dP(xDist, zDist, V_x, V_z, I_x, I_z, I_xz, area):
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)
+ 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
- def get_dx(component):
- return [x - self.centroid[0] for x in component.x_start]
-
- def get_dz(component):
- return [x - self.centroid[1] for x in component.x_start]
-
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_ = self.get_inertia_terms()
- self.spar.dP_x = get_dp(get_dx(self.spar), get_dz(self.spar), V_x, 0,
- self.I_[0], self.I_[1], self.I_[2],
+ 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 = get_dP(spar_dx, spar_dz,
+ V_x, 0,
+ self.I_['x'], self.I_['z'], self.I_['xz'],
self.spar.cap_area)
- self.spar.dP_z = get_dp(get_dx(self.spar), get_dz(self.spar), 0, V_z,
- self.I_[0], self.I_[1], self.I_[2],
+ self.spar.dP_z = get_dP(spar_dx, spar_dz,
+ 0, V_z,
+ self.I_['x'], self.I_['z'], self.I_['xz'],
self.spar.cap_area)
return None
def plot_geom(evaluator):
- '''This function plots analysis results over the airfoil's geometry.'''
-
+ """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')
+ plt.plot(evaluator.chord / 4, 0,
+ '.', color='g', markersize=24, label='Quarter-chord')
# Plot airfoil surfaces
x = [0.98 * x for x in evaluator.x]
y = [0.98 * z for z in evaluator.z]
@@ -281,8 +281,8 @@ def plot_geom(evaluator):
plt.ylabel('Z axis')
plot_bound = max(evaluator.x)
- plt.xlim(- 0.10 * plot_bound, 1.10 * plot_bound)
- plt.ylim(- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2))
+ 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)
@@ -295,10 +295,8 @@ def plot_lift(evaluator):
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_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
diff --git a/generator.py b/generator.py
index 7ad2cf3..0e8da8b 100644
--- a/generator.py
+++ b/generator.py
@@ -12,25 +12,31 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
+"""
+The 'generator' module contains a single Population class,
+which represents a collection of randomized airfoils.
+"""
-import creator
+import creator as cr
-class Population:
- '''Collection of random airfoils.'''
+class Population(cr.Airfoil, cr.Spar, cr.Stringer):
+ """Collection of random airfoils."""
def __init__(self, size):
+ af = cr.Airfoil
+ # print(af)
self.size = size
self.gen_number = 0 # incremented for every generation
def mutate(self, prob_mt):
- '''Randomly mutate the genes of prob_mt % of the population.'''
+ """Randomly mutate the genes of prob_mt % of the population."""
def crossover(self, prob_cx):
- '''Combine the genes of prob_cx % of the population.'''
+ """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.'''
+ """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)'''
+ """Rate the fitness of an individual on a relative scale (0-100)"""
diff --git a/main.py b/main.py
index abbd339..ec70894 100644
--- a/main.py
+++ b/main.py
@@ -52,12 +52,11 @@ SAVE_PATH = 'C:/Users/blend/github/UCLA_MAE_154B/save'
def main():
- '''
+ """
Create an airfoil;
Evaluate an airfoil;
Generate a population of airfoils & optimize.
- '''
-
+ """
# Create coordinate system specific to our airfoil dimensions.
# TODO: imperial + metric unit setting
creator.Coordinates(CHORD_LENGTH, SEMI_SPAN)
@@ -70,8 +69,8 @@ def main():
# Define NACA airfoil coordinates and mass
af.add_naca(NACA_NUM)
af.add_mass(AIRFOIL_MASS)
- af.info_print(2)
- af.info_save(SAVE_PATH, _)
+ # af.info_print(2)
+ # af.info_save(SAVE_PATH, _)
# Create spar instance
af.spar = creator.Spar()
@@ -82,8 +81,8 @@ def main():
af.spar.add_spar_caps(SPAR_CAP_AREA)
af.spar.add_mass(SPAR_MASS)
af.spar.add_webs(SPAR_THICKNESS)
- af.spar.info_print(2)
- af.spar.info_save(SAVE_PATH, _)
+ # af.spar.info_print(2)
+ # af.spar.info_save(SAVE_PATH, _)
# Create stringer instance
af.stringer = creator.Stringer()
@@ -96,20 +95,26 @@ def main():
af.stringer.add_area(STRINGER_AREA)
af.stringer.add_mass(STRINGER_MASS)
af.stringer.add_webs(SKIN_THICKNESS)
- af.stringer.info_print(2)
- af.stringer.info_save(SAVE_PATH, _)
-
+ # af.stringer.info_print(2)
+ # af.stringer.info_save(SAVE_PATH, _)
+#
# Plot components with matplotlib
- creator.plot_geom(af)
+ # creator.plot_geom(af)
# Evaluator object contains airfoil analysis results.
eval = evaluator.Evaluator(af)
# The analysis is performed in the evaluator.py module.
eval.analysis(1, 1)
- eval.info_print(2)
- eval.info_save(SAVE_PATH, _)
+ # eval.info_print(2)
+ # eval.info_save(SAVE_PATH, _)
evaluator.plot_geom(eval)
- evaluator.plot_lift(eval)
+ # evaluator.plot_lift(eval)
+
+ pop = generator.Population(10)
+
+ # print(help(creator))
+ # print(help(evaluator))
+ # print(help(generator))
# Print final execution time
print("--- %s seconds ---" % (time.time() - start_time))
Copyright 2019--2024 Marius PETER