""" 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 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 x in range(aircraft.wing.semi_span) # ] return lift 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_0 def get_lift_total(self, aircraft): F_z = 100 # F_z = [(self.get_lift_rectangular() + self.get_lift_elliptical() / 2 # for _ in range(len(aircraft.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(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.""" for aircraft in self.aircrafts: 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.wing), # "Centroid": self.get_centroid(aircraft.wing) # } 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): """Print a list of subcomponents.""" name = f" COMPONENT TREE FOR {[_.name for _ in self.aircrafts]} " num_of_dashes = len(name) print(num_of_dashes * '-') print(name) for aircraft in self.aircrafts: print(".") print(f"`-- {aircraft}") print(f" |--{aircraft.wing}") 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, save_path='/home/blendux/Projects/Aircraft_Studio/save'): """Save the evaluator's tree to a file.""" file_name = f"{self.name}_tree.txt" full_path = os.path.join(save_path, file_name) with open(full_path, 'w') as f: try: for aircraft in self.aircrafts: 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.\n', 'Was the full path passed to the function?') return None try: with open(full_path, 'w') as 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 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