summaryrefslogtreecommitdiff
path: root/evaluator.py
diff options
context:
space:
mode:
Diffstat (limited to 'evaluator.py')
-rw-r--r--evaluator.py360
1 files changed, 360 insertions, 0 deletions
diff --git a/evaluator.py b/evaluator.py
new file mode 100644
index 0000000..0307079
--- /dev/null
+++ b/evaluator.py
@@ -0,0 +1,360 @@
+"""
+The evaluator.py module contains a single Evaluator class,
+which knows all the attributes of a specified Aircraft 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
+import concurrent.futures
+import logging
+
+logging.basicConfig(filename='log_eval.txt',
+ level=logging.DEBUG,
+ format='%(asctime)s - %(levelname)s - %(message)s')
+
+
+class Evaluator:
+ """Performs structural evaluations on aircrafts.
+ Individual aircrafts must claim an Evaluator object as parent."""
+ def __init__(self, name):
+ self.name = name
+ self.aircrafts = []
+ self.results = []
+
+ self.I_ = {'x': 0, 'z': 0, 'xz': 0}
+
+ def _get_lift_rectangular(aircraft, lift=50):
+ L_prime = [
+ 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 = [
+ 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(self, aircraft):
+ """Combination of rectangular and elliptical lift."""
+ F_z = [
+ self._get_lift_rectangular(aircraft) +
+ self._get_lift_elliptical(aircraft) / 2
+ for i in range(aircraft.wing.semi_span)
+ ]
+ 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(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_centroid(aircraft):
+ """Return the coordinates of the centroid."""
+ stringer_area = aircraft.stringer.area
+ cap_area = aircraft.spar.cap_area
+
+ caps_x = [value for spar in aircraft.spar.x for value in spar]
+ caps_z = [value for spar in aircraft.spar.z for value in spar]
+ stringers_x = aircraft.stringer.x
+ stringers_z = aircraft.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):
+ """Perform all analysis calculations and store in self.results."""
+ # with concurrent.futures.ProcessPoolExecutor() as executor:
+ # for aircraft in self.aircrafts:
+ # lift = executor.submit(self.get_lift_total(aircraft))
+ # drag = executor.submit(self.get_drag_total(aircraft))
+ # mass = executor.submit(self.get_mass_total(aircraft))
+ # thrust = executor.submit(self.get_thrust_total(aircraft))
+
+ # for aircraft in self.aircrafts:
+ # print(lift.result())
+ # print(drag.result())
+ # print(mass.result())
+ # print(thrust.result())
+
+ # for f in concurrent.futures.as_completed(l, d, m, t):
+ # print(f.result())
+
+ for aircraft in self.aircrafts:
+ # lift = self.get_lift_total(aircraft),
+ # drag = self.get_drag(aircraft.wing),
+ # centroid = self.get_centroid(aircraft.wing)
+ results = {"Lift": 400, "Drag": 20, "Centroid": [0.2, 4.5]}
+ self.results.append(results)
+ # results = {
+ # "Lift": self.get_lift_total(aircraft),
+ # "Drag": self.get_drag(aircraft),
+ # "Centroid": self.get_centroid(aircraft)
+ # }
+ return results
+
+ # 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, *aircrafts):
+ """Print the list of subcomponents."""
+ name = f" TREE FOR {[i.name for i in aircrafts]} IN {self.name} "
+ num_of_dashes = len(name)
+ print(num_of_dashes * '-')
+ print(name)
+ for aircraft in aircrafts:
+ 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,
+ *aircrafts,
+ save_path='/home/blendux/Projects/Aircraft_Studio/save'):
+ """Save the evaluator's tree to a file."""
+ for aircraft in aircrafts:
+ 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 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
+
+
+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
Copyright 2019--2024 Marius PETER