summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--creator.py185
-rw-r--r--evaluator.py65
-rw-r--r--generator.py8
-rw-r--r--main.py27
4 files changed, 122 insertions, 163 deletions
diff --git a/creator.py b/creator.py
index 3e36036..eaf4d68 100644
--- a/creator.py
+++ b/creator.py
@@ -12,19 +12,20 @@
#
# 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.)
+The creator.py 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.
+ 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 numpy as np
@@ -33,32 +34,25 @@ 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:
+class Airfoil:
"""
- All airfoil components need the following:
-
- Parameters:
- Component material
- Coordinates relative to the chord & semi-span
+ This class represents a single NACA airfoil.
- Methods:
- Print component coordinates
- Save component coordinates to file specified in main.py
+ Please note: 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.
- So, all component classes inherit from class 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, chord, semi_span):
- # Global dimensions
- self.chord = chord if chord > 40 else 40
- self.semi_span = semi_span
+ # Defaults
+ chord = 100
+ semi_span = 200
+
+ def __init__(self):
# mass and area
self.mass = float()
self.area = float()
@@ -68,79 +62,18 @@ class Coordinates:
self.x = []
self.z = []
- # The airfoil components know the Coordinates instance's coords
- global parent
- parent = self
+ @classmethod
+ def from_dimensions(cls, chord, semi_span):
+ cls.chord = chord
+ cls.semi_span = semi_span
+ return Airfoil()
def __str__(self):
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)
-
- print(num_of_dashes * '-')
- print(name)
- print('Component:', str(self))
- print('Chord length:', self.chord)
- print('Semi-span:', self.semi_span)
- print('Mass:', self.mass)
- print(num_of_dashes * '-')
- print('x-coordinates:\n', np.around(self.x, round))
- print('z-coordinates:\n', np.around(self.z, round))
- 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:
- 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__
- 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?')
- return None
-
-
-class Airfoil(Coordinates):
- """
- 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
- 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):
- 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 = []
- self.z_c = []
-
def add_naca(self, naca_num):
"""
- This function generates geometry for our chosen NACA airfoil shape.
+ This function generates geometry for a NACA number passed as argument.
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.
@@ -214,6 +147,8 @@ class Airfoil(Coordinates):
x_chord_rev.extend(extend)
# Generate our airfoil geometry from previous sub-functions.
+ self.x_c = []
+ self.z_c = []
for x in x_chord:
self.x_c.append(x)
self.z_c.append(get_camber(x))
@@ -228,27 +163,56 @@ class Airfoil(Coordinates):
self.mass = mass
def info_print(self, round):
- super().info_print(round)
- print('x_c the camber x-coordinates:\n', np.around(self.x_c, round))
- print('z_c the camber z-coordinates:\n', np.around(self.z_c, 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)
+
+ print(num_of_dashes * '-')
+ print(name)
+ print('Component:', str(self))
+ print('Chord length:', self.chord)
+ print('Semi-span:', self.semi_span)
+ print('Mass:', self.mass)
+ print(num_of_dashes * '-')
+ print('x-coordinates:\n', np.around(self.x, round))
+ print('z-coordinates:\n', np.around(self.z, round))
+ 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:
+ 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__
+ 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?')
return None
-class Spar(Coordinates):
+class Spar(Airfoil):
"""Contains a single spar's location."""
- global parent
def __init__(self):
- super().__init__(parent.chord, parent.semi_span)
+ super().__init__()
self.x_start = []
self.x_end = []
self.thickness = float()
self.z_start = []
self.z_end = []
- self.dx = float()
- self.dz = float()
- self.dP_x = float()
- self.dP_z = float()
def add_coord(self, airfoil, x_loc_percent):
"""
@@ -261,6 +225,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
@@ -296,22 +261,17 @@ class Spar(Coordinates):
return None
-class Stringer(Coordinates):
+class Stringer(Airfoil):
"""Contains the coordinates of all stringers."""
- global parent
def __init__(self):
- super().__init__(parent.chord, parent.semi_span)
+ super().__init__()
self.x_start = []
self.x_end = []
self.thickness = float()
self.z_start = []
self.z_end = []
self.area = float()
- # self.dx = float()
- # self.dz = float()
- # self.dP_x = float()
- # self.dP_z = float()
def add_coord(self, airfoil,
stringer_u_1, stringer_u_2,
@@ -331,6 +291,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
@@ -399,6 +360,7 @@ class Stringer(Coordinates):
def plot_geom(airfoil):
"""This function plots the airfoil's + sub-components' geometry."""
+
# Plot chord
x_chord = [0, airfoil.chord]
y_chord = [0, 0]
@@ -432,7 +394,6 @@ def plot_geom(airfoil):
# Graph formatting
plt.xlabel('X axis')
plt.ylabel('Z axis')
-
plot_bound = max(airfoil.x)
plt.xlim(- 0.10 * plot_bound, 1.10 * plot_bound)
plt.ylim(- (1.10 * plot_bound / 2), (1.10 * plot_bound / 2))
diff --git a/evaluator.py b/evaluator.py
index 2bef652..cab2e9b 100644
--- a/evaluator.py
+++ b/evaluator.py
@@ -13,7 +13,7 @@
# 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,
+The evaluator.py 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.
@@ -42,9 +42,6 @@ class Evaluator:
+ airfoil.spar.mass
+ airfoil.stringer.mass)
self.mass_dist = []
- # Coordinates
- self.x = airfoil.x
- self.z = airfoil.z
# Lift
self.lift_rectangular = []
self.lift_elliptical = []
@@ -65,29 +62,33 @@ class Evaluator:
name = ' EVALUATOR DATA '
num_of_dashes = len(name)
- print(num_of_dashes * '-')
- print(name)
- print('Evaluating:', self.airfoil)
- print('Chord length:', self.chord)
- print('Semi-span:', self.semi_span)
- 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_['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 * '-')
- print('Rectangular lift along semi-span:\n',
- np.around(self.lift_rectangular, round))
- print('Elliptical lift along semi-span:\n',
- np.around(self.lift_elliptical, round))
- print('Combined lift along semi-span:\n',
- np.around(self.lift_total, round))
- print('Distribution of mass along semi-span:\n',
- np.around(self.mass_dist, round))
- print('Drag along semi-span:\n', np.around(self.drag, round))
+ try:
+ print(num_of_dashes * '-')
+ print(name)
+ print('Evaluating:', self.airfoil)
+ print('Chord length:', self.chord)
+ print('Semi-span:', self.semi_span)
+ 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_['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 * '-')
+ print('Rectangular lift along semi-span:\n',
+ np.around(self.lift_rectangular, round))
+ print('Elliptical lift along semi-span:\n',
+ np.around(self.lift_elliptical, round))
+ print('Combined lift along semi-span:\n',
+ np.around(self.lift_total, round))
+ print('Distribution of mass along semi-span:\n',
+ np.around(self.mass_dist, round))
+ print('Drag along semi-span:\n', np.around(self.drag, round))
+ except AttributeError:
+ print(num_of_dashes * '-')
+ print('Cannot print full evaluation. Was the airfoil analyzed?')
return None
def info_save(self, save_path, number):
@@ -246,11 +247,11 @@ def plot_geom(evaluator):
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]
+ 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.x]
- y = [1.02 * z for z in evaluator.z]
+ 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
@@ -279,7 +280,7 @@ def plot_geom(evaluator):
plt.xlabel('X axis')
plt.ylabel('Z axis')
- plot_bound = max(evaluator.x)
+ 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')
diff --git a/generator.py b/generator.py
index 0e8da8b..6c9d03c 100644
--- a/generator.py
+++ b/generator.py
@@ -13,18 +13,18 @@
# 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,
+The generator.py module contains a single Population class,
which represents a collection of randomized airfoils.
"""
-import creator as cr
+import creator
-class Population(cr.Airfoil, cr.Spar, cr.Stringer):
+class Population(creator.Airfoil):
"""Collection of random airfoils."""
def __init__(self, size):
- af = cr.Airfoil
+ af = creator.Airfoil
# print(af)
self.size = size
self.gen_number = 0 # incremented for every generation
diff --git a/main.py b/main.py
index f816c4b..1392218 100644
--- a/main.py
+++ b/main.py
@@ -57,20 +57,17 @@ def main():
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)
# Interate through all wings in population, creating and evaluating them.
for _ in range(1, POP_SIZE + 1):
# Create airfoil instance
- af = creator.Airfoil()
+ af = creator.Airfoil.from_dimensions(CHORD_LENGTH, SEMI_SPAN)
# 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()
@@ -81,8 +78,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()
@@ -95,24 +92,24 @@ 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(creator))
# print(help(evaluator))
# print(help(generator))
Copyright 2019--2024 Marius PETER