summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/creator.py416
-rw-r--r--tools/evaluator.py288
-rw-r--r--tools/generator.py64
3 files changed, 768 insertions, 0 deletions
diff --git a/tools/creator.py b/tools/creator.py
new file mode 100644
index 0000000..810df09
--- /dev/null
+++ b/tools/creator.py
@@ -0,0 +1,416 @@
+# This file is part of Marius Peter's airfoil analysis package (this program).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.py module contains class definitions for coordinates
+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 numpy as np
+from math import sin, cos, atan
+import bisect as bi
+import matplotlib.pyplot as plt
+
+
+class 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
+ 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.
+ """
+
+ # Defaults
+ chord = 100
+ semi_span = 200
+
+ def __init__(self):
+ # mass and area
+ self.mass = float()
+ self.area = float()
+ # Component material
+ self.material = str()
+ # Coordinates
+ self.x = []
+ self.z = []
+
+ @classmethod
+ def from_dimensions(cls, chord, semi_span):
+ """Create airfoil from its chord and semi-span."""
+ if chord > 20:
+ cls.chord = chord
+ else:
+ cls.chord = 20
+ print('Chord too small, using minimum value of 20.')
+ cls.semi_span = semi_span
+ return Airfoil()
+
+ 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
+ """
+ # Variables extracted from 'naca_num' argument passed to the function
+ self.naca_num = naca_num
+ 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_upper_coord(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_lower_coord(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))
+ # Reversed list for our lower airfoil coordinate densification
+ x_chord_rev = [i for i in range(self.chord, x_chord_25_percent, -1)]
+ extend = [i / 10 for i in range(x_chord_25_percent * 10, -1, -1)]
+ 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))
+ self.x.append(get_upper_coord(x)[0])
+ self.z.append(get_upper_coord(x)[1])
+ for x in x_chord_rev:
+ self.x.append(get_lower_coord(x)[0])
+ self.z.append(get_lower_coord(x)[1])
+ return None
+
+ def add_mass(self, mass):
+ self.mass = mass
+
+ def info_print(self, round):
+ """Print all the component's coordinates to the terminal."""
+ name = ' CREATOR DATA FOR {} '.format(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 = '{}_{}.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(Airfoil):
+ """Contains a single spar's location."""
+
+ def __init__(self):
+ super().__init__()
+ self.x_start = []
+ self.x_end = []
+ self.thickness = float()
+ self.z_start = []
+ self.z_end = []
+
+ def add_coord(self, airfoil, x_loc_percent):
+ """Add a single spar at the % chord location given to function.
+
+ Parameters:
+ airfoil: gives the spar access to airfoil's coordinates.
+ x_loc_percent: spar's location as a % of total chord length.
+
+ 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
+ # This ensures that spar geom intersects with airfoil geom.
+ # Spar upper coordinates
+ spar_x = bi.bisect_left(airfoil.x, loc) - 1
+ x = [airfoil.x[spar_x]]
+ z = [airfoil.z[spar_x]]
+ # Spar lower coordinates
+ spar_x = bi.bisect_left(airfoil.x[::-1], loc)
+ x += [airfoil.x[-spar_x]]
+ z += [airfoil.z[-spar_x]]
+ self.x.append(x)
+ self.z.append(z)
+ return None
+
+ def add_spar_caps(self, spar_cap_area):
+ self.cap_area = spar_cap_area
+ return None
+
+ def add_mass(self, mass):
+ self.mass = len(self.x) * mass
+ return None
+
+ def add_webs(self, thickness):
+ """Add webs to spars."""
+ for _ in range(len(self.x)):
+ self.x_start.append(self.x[_][0])
+ self.x_end.append(self.x[_][1])
+ self.z_start.append(self.z[_][0])
+ self.z_end.append(self.z[_][1])
+ self.thickness = thickness
+ return None
+
+
+class Stringer(Airfoil):
+ """Contains the coordinates of all stringers."""
+
+ def __init__(self):
+ super().__init__()
+ self.x_start = []
+ self.x_end = []
+ self.thickness = float()
+ self.z_start = []
+ self.z_end = []
+ self.area = float()
+
+ 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).
+
+ 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 = airfoil.spar.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, view: False):
+ """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 _ in range(len(airfoil.spar.x)):
+ x = (airfoil.spar.x[_])
+ y = (airfoil.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 = max(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)
+
+ if view == True:
+ plt.show()
+ else:
+ pass
+ return fig, ax
+
+
+def main():
+ return None
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/evaluator.py b/tools/evaluator.py
new file mode 100644
index 0000000..ad1f873
--- /dev/null
+++ b/tools/evaluator.py
@@ -0,0 +1,288 @@
+# This file is part of Marius Peter's airfoil analysis package (this program).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.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.
+"""
+
+import sys
+import os.path
+import numpy as np
+from math import sqrt
+import matplotlib.pyplot as plt
+
+
+class Evaluator:
+ """Performs structural evaluations for the airfoil passed as argument."""
+
+ def __init__(self, airfoil):
+ # Evaluator knows all geometrical info from evaluated airfoil
+ self.airfoil = airfoil
+ self.spar = airfoil.spar
+ self.stringer = airfoil.stringer
+ # Global dimensions
+ self.chord = airfoil.chord
+ self.semi_span = airfoil.semi_span
+ # Mass & spanwise distribution
+ self.mass_total = float(airfoil.mass
+ + airfoil.spar.mass
+ + airfoil.stringer.mass)
+ self.mass_dist = []
+ # Lift
+ self.lift_rectangular = []
+ self.lift_elliptical = []
+ self.lift_total = []
+ # Drag
+ self.drag = []
+ # centroid
+ self.centroid = []
+ # Inertia terms:
+ self.I_ = {'x': 0, 'z': 0, 'xz': 0}
+
+ def __str__(self):
+ return type(self).__name__
+
+ def info_print(self, round):
+ """Print all the component's evaluated data to the terminal."""
+ name = ' EVALUATOR DATA FOR {} '.format(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 = 'airfoil_{}_eval.txt'.format(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
+
+ # 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)]
+ 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)
+ ]
+ return L_prime
+
+ def get_lift_total(self):
+ F_z = [(self.lift_rectangular[_] + self.lift_elliptical[_]) / 2
+ for _ in range(len(self.lift_rectangular))]
+ 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)]
+ return F_z
+
+ def get_drag(self, drag):
+ # Transform semi-span integer into list
+ semi_span = [x for x in range(0, self.semi_span)]
+
+ # Drag increases after 80% of the semi_span
+ cutoff = round(0.8 * self.semi_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_centroid(self):
+ """Return the coordinates of the centroid."""
+ stringer_area = self.stringer.area
+ cap_area = self.spar.cap_area
+
+ caps_x = [value for spar in self.spar.x for value in spar]
+ caps_z = [value for spar in self.spar.z for value in spar]
+ stringers_x = self.stringer.x
+ stringers_z = self.stringer.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)
+
+ 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)
+
+ 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
+
+ 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)
+ 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/tools/generator.py b/tools/generator.py
new file mode 100644
index 0000000..0213828
--- /dev/null
+++ b/tools/generator.py
@@ -0,0 +1,64 @@
+# This file is part of Marius Peter's airfoil analysis package (this program).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.py module contains a single Population class,
+which represents a collection of randomized airfoils.
+"""
+
+from tools import creator
+
+
+def default_airfoil():
+ """Generate the default airfoil."""
+ airfoil = creator.Airfoil.from_dimensions(100, 200)
+ airfoil.add_naca(2412)
+ airfoil.add_mass(10)
+
+ airfoil.spar = creator.Spar()
+ airfoil.spar.add_coord(airfoil, 0.23)
+ airfoil.spar.add_coord(airfoil, 0.57)
+ airfoil.spar.add_spar_caps(0.3)
+ airfoil.spar.add_mass(10)
+ airfoil.spar.add_webs(0.4)
+
+ airfoil.stringer = creator.Stringer()
+ airfoil.stringer.add_coord(airfoil, 3, 6, 5, 4)
+ airfoil.stringer.add_area(0.1)
+ airfoil.stringer.add_mass(5)
+ airfoil.stringer.add_webs(0.1)
+
+ return airfoil
+
+
+class Population(creator.Airfoil):
+ """Collection of random airfoils."""
+
+ def __init__(self, size):
+ af = creator.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."""
+
+ 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