Source code for fastoad.utils.postprocessing.analysis_and_plots

"""
Defines the analysis and plotting functions for postprocessing
"""
#  This file is part of FAST : A framework for rapid Overall Aircraft Design
#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
#  FAST 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/>.
import numpy as np
import plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from fastoad.io import VariableIO

COLS = plotly.colors.DEFAULT_PLOTLY_COLORS


[docs]def wing_geometry_plot( aircraft_file_path: str, name=None, fig=None, file_formatter=None ) -> go.FigureWidget: """ Returns a figure plot of the top view of the wing. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param name: name to give to the trace added to the figure :param fig: existing figure to which add the plot :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: wing plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() wing_kink_leading_edge_x = variables["data:geometry:wing:kink:leading_edge:x:local"].value[0] wing_tip_leading_edge_x = variables["data:geometry:wing:tip:leading_edge:x:local"].value[0] wing_root_y = variables["data:geometry:wing:root:y"].value[0] wing_kink_y = variables["data:geometry:wing:kink:y"].value[0] wing_tip_y = variables["data:geometry:wing:tip:y"].value[0] wing_root_chord = variables["data:geometry:wing:root:chord"].value[0] wing_kink_chord = variables["data:geometry:wing:kink:chord"].value[0] wing_tip_chord = variables["data:geometry:wing:tip:chord"].value[0] mean_aerodynamic_chord = variables["data:geometry:wing:MAC:length"].value[0] mac25_x_position = variables["data:geometry:wing:MAC:at25percent:x"].value[0] distance_root_mac_chords = variables["data:geometry:wing:MAC:leading_edge:x:local"].value[0] # pylint: disable=invalid-name # that's a common naming y = np.array( [0, wing_root_y, wing_kink_y, wing_tip_y, wing_tip_y, wing_kink_y, wing_root_y, 0, 0] ) # pylint: disable=invalid-name # that's a common naming y = np.concatenate((-y, y)) # pylint: disable=invalid-name # that's a common naming x = np.array( [ 0, 0, wing_kink_leading_edge_x, wing_tip_leading_edge_x, wing_tip_leading_edge_x + wing_tip_chord, wing_kink_leading_edge_x + wing_kink_chord, wing_root_chord, wing_root_chord, 0, ] ) x = x + mac25_x_position - 0.25 * mean_aerodynamic_chord - distance_root_mac_chords # pylint: disable=invalid-name # that's a common naming x = np.concatenate((x, x)) if fig is None: fig = go.Figure() scatter = go.Scatter(x=y, y=x, mode="lines+markers", name=name) fig.add_trace(scatter) fig.layout = go.Layout(yaxis=dict(scaleanchor="x", scaleratio=1)) fig = go.FigureWidget(fig) fig.update_layout( title_text="Wing Geometry", title_x=0.5, xaxis_title="y", yaxis_title="x", ) return fig
# pylint: disable-msg=too-many-locals
[docs]def aircraft_geometry_plot( aircraft_file_path: str, name=None, fig=None, file_formatter=None ) -> go.FigureWidget: """ Returns a figure plot of the top view of the wing. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param name: name to give to the trace added to the figure :param fig: existing figure to which add the plot :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: wing plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() # Wing parameters wing_kink_leading_edge_x = variables["data:geometry:wing:kink:leading_edge:x:local"].value[0] wing_tip_leading_edge_x = variables["data:geometry:wing:tip:leading_edge:x:local"].value[0] wing_root_y = variables["data:geometry:wing:root:y"].value[0] wing_kink_y = variables["data:geometry:wing:kink:y"].value[0] wing_tip_y = variables["data:geometry:wing:tip:y"].value[0] wing_root_chord = variables["data:geometry:wing:root:chord"].value[0] wing_kink_chord = variables["data:geometry:wing:kink:chord"].value[0] wing_tip_chord = variables["data:geometry:wing:tip:chord"].value[0] y_wing = np.array( [0, wing_root_y, wing_kink_y, wing_tip_y, wing_tip_y, wing_kink_y, wing_root_y, 0, 0] ) x_wing = np.array( [ 0, 0, wing_kink_leading_edge_x, wing_tip_leading_edge_x, wing_tip_leading_edge_x + wing_tip_chord, wing_kink_leading_edge_x + wing_kink_chord, wing_root_chord, wing_root_chord, 0, ] ) # Horizontal Tail parameters ht_root_chord = variables["data:geometry:horizontal_tail:root:chord"].value[0] ht_tip_chord = variables["data:geometry:horizontal_tail:tip:chord"].value[0] ht_span = variables["data:geometry:horizontal_tail:span"].value[0] ht_sweep_0 = variables["data:geometry:horizontal_tail:sweep_0"].value[0] ht_tip_leading_edge_x = ht_span / 2.0 * np.tan(ht_sweep_0 * np.pi / 180.0) y_ht = np.array([0, ht_span / 2.0, ht_span / 2.0, 0.0, 0.0]) x_ht = np.array( [0, ht_tip_leading_edge_x, ht_tip_leading_edge_x + ht_tip_chord, ht_root_chord, 0] ) # Fuselage parameters fuselage_max_width = variables["data:geometry:fuselage:maximum_width"].value[0] fuselage_length = variables["data:geometry:fuselage:length"].value[0] fuselage_front_length = variables["data:geometry:fuselage:front_length"].value[0] fuselage_rear_length = variables["data:geometry:fuselage:rear_length"].value[0] x_fuselage = np.array( [ 0.0, 0.0, fuselage_front_length, fuselage_length - fuselage_rear_length, fuselage_length, fuselage_length, ] ) y_fuselage = np.array( [ 0.0, fuselage_max_width / 4.0, fuselage_max_width / 2.0, fuselage_max_width / 2.0, fuselage_max_width / 4.0, 0.0, ] ) # CGs wing_25mac_x = variables["data:geometry:wing:MAC:at25percent:x"].value[0] wing_mac_length = variables["data:geometry:wing:MAC:length"].value[0] local_wing_mac_le_x = variables["data:geometry:wing:MAC:leading_edge:x:local"].value[0] local_ht_25mac_x = variables["data:geometry:horizontal_tail:MAC:at25percent:x:local"].value[0] ht_distance_from_wing = variables[ "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25" ].value[0] x_wing = x_wing + wing_25mac_x - 0.25 * wing_mac_length - local_wing_mac_le_x x_ht = x_ht + wing_25mac_x + ht_distance_from_wing - local_ht_25mac_x # pylint: disable=invalid-name # that's a common naming x = np.concatenate((x_fuselage, x_wing, x_ht)) # pylint: disable=invalid-name # that's a common naming y = np.concatenate((y_fuselage, y_wing, y_ht)) # pylint: disable=invalid-name # that's a common naming y = np.concatenate((-y, y)) # pylint: disable=invalid-name # that's a common naming x = np.concatenate((x, x)) if fig is None: fig = go.Figure() scatter = go.Scatter(x=y, y=x, mode="lines+markers", name=name) fig.add_trace(scatter) fig.layout = go.Layout(yaxis=dict(scaleanchor="x", scaleratio=1)) fig = go.FigureWidget(fig) fig.update_layout( title_text="Aircraft Geometry", title_x=0.5, xaxis_title="y", yaxis_title="x", ) return fig
[docs]def drag_polar_plot( aircraft_file_path: str, name=None, fig=None, file_formatter=None ) -> go.FigureWidget: """ Returns a figure plot of the aircraft drag polar. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param name: name to give to the trace added to the figure :param fig: existing figure to which add the plot :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: wing plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() # pylint: disable=invalid-name # that's a common naming cd = np.asarray(variables["data:aerodynamics:aircraft:cruise:CD"].value) # pylint: disable=invalid-name # that's a common naming cl = np.asarray(variables["data:aerodynamics:aircraft:cruise:CL"].value) # TODO: remove filtering one models provide proper bounds cd_short = cd[cd <= 2.0] cl_short = cl[cd <= 2.0] if fig is None: fig = go.Figure() scatter = go.Scatter(x=cd_short, y=cl_short, mode="lines+markers", name=name) fig.add_trace(scatter) fig = go.FigureWidget(fig) fig.update_layout( title_text="Drag Polar", title_x=0.5, xaxis_title="Cd", yaxis_title="Cl", ) return fig
[docs]def mass_breakdown_bar_plot( aircraft_file_path: str, name=None, fig=None, file_formatter=None ) -> go.FigureWidget: """ Returns a figure plot of the aircraft mass breakdown using bar plots. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param name: name to give to the trace added to the figure :param fig: existing figure to which add the plot :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: bar plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() systems = variables["data:weight:systems:mass"].value[0] furniture = variables["data:weight:furniture:mass"].value[0] crew = variables["data:weight:crew:mass"].value[0] airframe = variables["data:weight:airframe:mass"].value[0] propulsion = variables["data:weight:propulsion:mass"].value[0] # pylint: disable=invalid-name # that's a common naming MTOW = variables["data:weight:aircraft:MTOW"].value[0] # pylint: disable=invalid-name # that's a common naming OWE = variables["data:weight:aircraft:OWE"].value[0] payload = variables["data:weight:aircraft:payload"].value[0] fuel_mission = variables["data:mission:sizing:fuel"].value[0] if fig is None: fig = make_subplots( rows=1, cols=2, subplot_titles=("Maximum Take-Off Weight Breakdown", "Overall Weight Empty Breakdown"), ) # Same color for each aircraft configuration i = len(fig.data) weight_labels = ["MTOW", "OWE", "Fuel - Mission", "Payload"] weight_values = [MTOW, OWE, fuel_mission, payload] fig.add_trace( go.Bar(name="", x=weight_labels, y=weight_values, marker_color=COLS[i], showlegend=False), row=1, col=1, ) weight_labels = ["Airframe", "Propulsion", "Systems", "Furniture", "Crew"] weight_values = [airframe, propulsion, systems, furniture, crew] fig.add_trace( go.Bar(name=name, x=weight_labels, y=weight_values, marker_color=COLS[i]), row=1, col=2, ) fig.update_layout(yaxis_title="[kg]") return fig
[docs]def mass_breakdown_sun_plot(aircraft_file_path: str, file_formatter=None): """ Returns a figure sunburst plot of the mass breakdown. On the left a MTOW sunburst and on the right a OWE sunburst. Different designs can be superposed by providing an existing fig. Each design can be provided a name. :param aircraft_file_path: path of data file :param file_formatter: the formatter that defines the format of data file. If not provided, default format will be assumed. :return: sunburst plot figure """ variables = VariableIO(aircraft_file_path, file_formatter).read() systems = variables["data:weight:systems:mass"].value[0] C11 = variables["data:weight:systems:power:auxiliary_power_unit:mass"].value[0] C12 = variables["data:weight:systems:power:electric_systems:mass"].value[0] C13 = variables["data:weight:systems:power:hydraulic_systems:mass"].value[0] C21 = variables["data:weight:systems:life_support:insulation:mass"].value[0] C22 = variables["data:weight:systems:life_support:air_conditioning:mass"].value[0] C23 = variables["data:weight:systems:life_support:de-icing:mass"].value[0] C24 = variables["data:weight:systems:life_support:cabin_lighting:mass"].value[0] C25 = variables["data:weight:systems:life_support:seats_crew_accommodation:mass"].value[0] C26 = variables["data:weight:systems:life_support:oxygen:mass"].value[0] C27 = variables["data:weight:systems:life_support:safety_equipment:mass"].value[0] C3 = variables["data:weight:systems:navigation:mass"].value[0] C4 = variables["data:weight:systems:transmission:mass"].value[0] C51 = variables["data:weight:systems:operational:radar:mass"].value[0] C52 = variables["data:weight:systems:operational:cargo_hold:mass"].value[0] C6 = variables["data:weight:systems:flight_kit:mass"].value[0] furniture = variables["data:weight:furniture:mass"].value[0] D2 = variables["data:weight:furniture:passenger_seats:mass"].value[0] D3 = variables["data:weight:furniture:food_water:mass"].value[0] D4 = variables["data:weight:furniture:security_kit:mass"].value[0] D5 = variables["data:weight:furniture:toilets:mass"].value[0] crew = variables["data:weight:crew:mass"].value[0] airframe = variables["data:weight:airframe:mass"].value[0] wing = variables["data:weight:airframe:wing:mass"].value[0] fuselage = variables["data:weight:airframe:fuselage:mass"].value[0] h_tail = variables["data:weight:airframe:horizontal_tail:mass"].value[0] v_tail = variables["data:weight:airframe:vertical_tail:mass"].value[0] control_surface = variables["data:weight:airframe:flight_controls:mass"].value[0] landing_gear_1 = variables["data:weight:airframe:landing_gear:main:mass"].value[0] landing_gear_2 = variables["data:weight:airframe:landing_gear:front:mass"].value[0] engine_pylon = variables["data:weight:airframe:pylon:mass"].value[0] paint = variables["data:weight:airframe:paint:mass"].value[0] propulsion = variables["data:weight:propulsion:mass"].value[0] B1 = variables["data:weight:propulsion:engine:mass"].value[0] B2 = variables["data:weight:propulsion:fuel_lines:mass"].value[0] B3 = variables["data:weight:propulsion:unconsumables:mass"].value[0] MTOW = variables["data:weight:aircraft:MTOW"].value[0] OWE = variables["data:weight:aircraft:OWE"].value[0] payload = variables["data:weight:aircraft:payload"].value[0] fuel_mission = variables["data:mission:sizing:fuel"].value[0] # TODO: Deal with this in a more generic manner ? if round(MTOW, 6) == round(OWE + payload + fuel_mission, 6): MTOW = OWE + payload + fuel_mission fig = make_subplots(1, 2, specs=[[{"type": "domain"}, {"type": "domain"}]],) fig.add_trace( go.Sunburst( labels=[ "MTOW" + "<br>" + str(int(MTOW)) + " [kg]", "payload" + "<br>" + str(int(payload)) + " [kg] (" + str(round(payload / MTOW * 100, 1)) + "%)", "fuel_mission" + "<br>" + str(int(fuel_mission)) + " [kg] (" + str(round(fuel_mission / MTOW * 100, 1)) + "%)", "OWE" + "<br>" + str(int(OWE)) + " [kg] (" + str(round(OWE / MTOW * 100, 1)) + "%)", ], parents=[ "", "MTOW" + "<br>" + str(int(MTOW)) + " [kg]", "MTOW" + "<br>" + str(int(MTOW)) + " [kg]", "MTOW" + "<br>" + str(int(MTOW)) + " [kg]", ], values=[MTOW, payload, fuel_mission, OWE], branchvalues="total", ), 1, 1, ) airframe_str = ( "airframe" + "<br>" + str(int(airframe)) + " [kg] (" + str(round(airframe / OWE * 100, 1)) + "%)" ) propulsion_str = ( "propulsion" + "<br>" + str(int(propulsion)) + " [kg] (" + str(round(propulsion / MTOW * 100, 1)) + "%)" ) systems_str = ( "systems" + "<br>" + str(int(systems)) + " [kg] (" + str(round(systems / MTOW * 100, 1)) + "%)" ) furniture_str = ( "furniture" + "<br>" + str(int(furniture)) + " [kg] (" + str(round(furniture / MTOW * 100, 1)) + "%)" ) crew_str = ( "crew" + "<br>" + str(int(crew)) + " [kg] (" + str(round(crew / MTOW * 100, 1)) + "%)" ) fig.add_trace( go.Sunburst( labels=[ "OWE" + "<br>" + str(int(OWE)) + " [kg]", airframe_str, propulsion_str, systems_str, furniture_str, crew_str, "wing", "fuselage", "horizontal_tail", "vertical_tail", "flight_controls", "landing_gear_main", "landing_gear_front", "pylon", "paint", "engine", "fuel_lines", "unconsumables", "auxiliary_power_unit", "electric_systems", "hydraulic_systems", "insulation", "air_conditioning", "de-icing", "cabin_lighting", "seats_crew_accommodation", "oxygen", "safety_equipment", "navigation", "transmission", "radar", "cargo_hold", "flight_kit", # "cargo", "passenger_seats", "food_water", "security_kit", "toilets", "passenger_seats", "food_water", "security_kit", "toilets", ], parents=[ "", "OWE" + "<br>" + str(int(OWE)) + " [kg]", "OWE" + "<br>" + str(int(OWE)) + " [kg]", "OWE" + "<br>" + str(int(OWE)) + " [kg]", "OWE" + "<br>" + str(int(OWE)) + " [kg]", "OWE" + "<br>" + str(int(OWE)) + " [kg]", airframe_str, airframe_str, airframe_str, airframe_str, airframe_str, airframe_str, airframe_str, airframe_str, airframe_str, propulsion_str, propulsion_str, propulsion_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, systems_str, # "furniture", "furniture", "furniture", "furniture", "furniture", furniture_str, furniture_str, furniture_str, furniture_str, ], values=[ OWE, airframe, propulsion, systems, furniture, crew, wing, fuselage, h_tail, v_tail, control_surface, landing_gear_1, landing_gear_2, engine_pylon, paint, B1, B2, B3, C11, C12, C13, C21, C22, C23, C24, C25, C26, C27, C3, C4, C51, C52, C6, D2, D3, D4, D5, ], branchvalues="total", ), 1, 2, ) fig.update_layout(title_text="Mass Breakdown", title_x=0.5) return fig