Source code for fastga_he.gui.performances_viewer

# This file is part of FAST-OAD_CS23-HE : A framework for rapid Overall Aircraft Design of Hybrid
# Electric Aircraft.
# Copyright (C) 2022 ISAE-SUPAERO

import ipywidgets as widgets
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from IPython.display import clear_output, display

from plotly.subplots import make_subplots

from fastga_he.powertrain_builder.powertrain import PROMOTION_FROM_MISSION

MARKER_DICTIONARY = {
    "sizing:main_route:climb": "circle-open",
    "sizing:main_route:cruise": "square",
    "sizing:main_route:descent": "diamond",
    "sizing:main_route:reserve": "cross",
}
COLOR_DICTIONARY = {
    "sizing:main_route:climb": px.colors.qualitative.Prism[1],
    "sizing:main_route:cruise": px.colors.qualitative.Prism[2],
    "sizing:main_route:descent": px.colors.qualitative.Prism[3],
    "sizing:main_route:reserve": px.colors.qualitative.Prism[4],
}
COLOR_DICTIONARY_AXIS_2 = {
    "sizing:main_route:climb": px.colors.qualitative.Prism[5],
    "sizing:main_route:cruise": px.colors.qualitative.Prism[6],
    "sizing:main_route:descent": px.colors.qualitative.Prism[7],
    "sizing:main_route:reserve": px.colors.qualitative.Prism[8],
}


[docs] class PerformancesViewer: """ A class for displaying the performances of all the elements of the power train along with the performances at aircraft level. Vastly inspired by the MissionViewer from fast-oad-core. """ def __init__( self, power_train_data_file_path: str, mission_data_file_path: str = "NeFinisPasPar.csv!", plot_height: int = None, plot_width: int = None, ): if power_train_data_file_path.endswith(".csv") and not mission_data_file_path.endswith( ".csv" ): power_train_data = pd.read_csv(power_train_data_file_path, index_col=0) # Remove the taxi power train data because they are not stored in the mission data # either power_train_data = power_train_data.drop([0]).iloc[:-1] # We readjust the index power_train_data = power_train_data.set_index(np.arange(len(power_train_data.index))) all_data = power_train_data elif power_train_data_file_path.endswith(".csv") and mission_data_file_path.endswith( ".csv" ): columns_to_drop = [] for mission_variable_name in list(PROMOTION_FROM_MISSION.keys()): columns_to_drop.append( mission_variable_name + " [" + PROMOTION_FROM_MISSION[mission_variable_name] + "]" ) # Read the two CSV and concatenate them so that all data can be displayed against all # data power_train_data = pd.read_csv(power_train_data_file_path, index_col=0) # Remove the taxi power train data because they are not stored in the mission data # either power_train_data = power_train_data.drop([0]).iloc[:-1] # We readjust the index power_train_data = power_train_data.set_index(np.arange(len(power_train_data.index))) power_train_data = power_train_data.drop(columns_to_drop, axis=1) mission_data = pd.read_csv(mission_data_file_path, index_col=0) all_data = pd.concat([power_train_data, mission_data], axis=1) else: raise TypeError("Unknown type for mission and power train data, please use .csv") # The figure displayed self._fig = None # The x selector self._x_widget = None # The y selector self._y_widget = None # The y2 selector self._y2_widget = None # The button to ensure same axis self._axis_ensurer = None self.plot_height = plot_height self.plot_width = plot_width self.data = all_data # Used to store the minimum values display in the graph so that we can readjust the axis self.y_min = np.inf self.y_max = -np.inf self._initialize_widgets() def _initialize_widgets(self): """ Initializes the widgets for selecting x and y """ key = list(self.data) keys = self.data[key].keys() output = widgets.Output() def show_plots(change=None): with output: clear_output(wait=True) # Reset axis_range self.y_min = np.inf self.y_max = -np.inf x_name = self._x_widget.value y_name = self._y_widget.value y2_name = self._y2_widget.value if y2_name == "None": fig = go.Figure() else: fig = make_subplots(specs=[[{"secondary_y": True}]]) # If both mission and pt data are present, we change the color according to the # phase to make it more readable if "name" in self.data.columns: for name in list(set(self.data["name"].to_list())): # pylint: disable=invalid-name # that's a common naming x = self.data.loc[self.data["name"] == name, x_name] # pylint: disable=invalid-name # that's a common naming y = self.data.loc[self.data["name"] == name, y_name] scatter = go.Scatter( x=x, y=y, mode="markers", marker={ "color": COLOR_DICTIONARY[name], "symbol": MARKER_DICTIONARY[name], "size": 8, }, name=name, legendgroup="Primary axis", legendgrouptitle_text="primary_axis", ) self.readjust_axis(y) if y2_name != "None": y_2 = self.data.loc[self.data["name"] == name, y2_name] scatter_2 = go.Scatter( x=x, y=y_2, mode="markers", marker={ "color": COLOR_DICTIONARY_AXIS_2[name], "symbol": MARKER_DICTIONARY[name], "size": 8, }, name=name, legendgroup="Secondary axis", legendgrouptitle_text="secondary_axis", ) self.readjust_axis(y_2) fig.add_trace(scatter, secondary_y=False) fig.add_trace(scatter_2, secondary_y=True) else: fig.add_trace(scatter) else: # pylint: disable=invalid-name # that's a common naming x = self.data[x_name] # pylint: disable=invalid-name # that's a common naming y = self.data[y_name] scatter = go.Scatter( x=x, y=y, mode="markers", legendgroup="Primary axis", legendgrouptitle_text="primary_axis", ) self.readjust_axis(y) if y2_name != "None": y_2 = self.data[y2_name] scatter_2 = go.Scatter( x=x, y=y_2, mode="markers", legendgroup="Secondary axis", legendgrouptitle_text="secondary_axis", ) self.readjust_axis(y_2) fig.add_trace(scatter, secondary_y=False) fig.add_trace(scatter_2, secondary_y=True) else: fig.add_trace(scatter) if y2_name != "None": fig.update_layout( title_text="Power train performances on the mission", title_x=0.5, showlegend=True, ) fig.update_xaxes(title_text=x_name) fig.update_yaxes(title_text=y_name, secondary_y=False) fig.update_yaxes(title_text=y2_name, secondary_y=True) if self._axis_ensurer.value: fig.update_yaxes( range=[0.95 * self.y_min, 1.05 * self.y_max], secondary_y=False ) fig.update_yaxes( range=[0.95 * self.y_min, 1.05 * self.y_max], secondary_y=True ) else: fig.update_layout( title_text="Power train performances on the mission", title_x=0.5, xaxis_title=x_name, yaxis_title=y_name, showlegend=True, ) if self.plot_height: fig.update_layout(height=self.plot_height) if self.plot_width: fig.update_layout(width=self.plot_width) fig = go.FigureWidget(fig) display(fig) # Check if time is in column name to put it as the x axis by default if "time" in keys: index_x = self.data.columns.get_loc("time") else: index_x = 2 self._x_widget = widgets.Dropdown(value=keys[index_x], options=keys) self._x_widget.observe(show_plots, "value") self._y_widget = widgets.Dropdown(value=keys[1], options=keys) self._y_widget.observe(show_plots, "value") self._y2_widget = widgets.Dropdown(value="None", options=list(keys) + ["None"]) self._y2_widget.observe(show_plots, "value") self._axis_ensurer = widgets.ToggleButton( value=False, description="Ensure same axis", disabled=False, button_style="success", tooltip="Check me to ensure that the primary and secondary axis will have the same y " "range, useful for comparing efficiencies for instance", icon="check", ) self._axis_ensurer.observe(show_plots, "value") show_plots() toolbar = widgets.HBox( [ widgets.Label(value="x:"), self._x_widget, widgets.Label(value="y:"), self._y_widget, widgets.Label(value="y2:"), self._y2_widget, self._axis_ensurer, ] ) display(toolbar, output)
[docs] def readjust_axis(self, y: pd.Series): """Readjusts the range of data plotted when adding a new scatter to the graph.""" self.y_min = min(self.y_min, y.min()) self.y_max = max(self.y_max, y.max())