Source code for posydon.visualization.VH_diagram.Presenter

"""Data visualization in VH diagram."""


__authors__ = [
    "Maxime Rambosson <Maxime.Rambosson@etu.unige.ch>",
]


from .SimulationModel import SimulationModel
from .GraphVisualizer import CaseInfos, PointInfos, StateInfos, columnTYPE

from .MainWindow import MainWindow
from .PresenterMode import PresenterMode

from posydon.utils.common_functions import orbital_separation_from_period
from posydon.utils.common_functions import PATH_TO_POSYDON

from datetime import datetime
import os
import numpy as np


[docs]def to_megayears(nb): """Convert yr to Myr.""" return nb / 1000000
[docs]def combine_simplified_data(*, data_to, data_from): """Combine 2 dictionaries of SimplifiedInfo. data_to[].state_after take the value of data_from[].state_before. Parameters ---------- data_to : dictionary of SimplifiedInfo Informations who will be udpated. data_from : dictionary of SimplifiedInfo Informations who will be combined. """ for key in data_to.keys(): data_to[key].state_after = data_from[key].state_before
[docs]def to_simplified_data(row): """Convert dictionnary of SimplifiedInfo with values as `.state_before`. Parameters ---------- row : dictionnary Dictionnary of value to convert. Returns ------- dictionnary of SimplifiedInfo Dictionnary of SimplifiedInfo containing the value as SimplifiedInfo.state_before. """ data = {} for key in row.keys(): data[key] = SimplifiedInfo(row[key]) # Side effect data["state"] = SimplifiedInfo( row["event"] if (str(row["event"]) != "nan") else row["state"] ) return data
[docs]def file_exist(filename): """Check if file exists.""" return os.path.exists(filename)
[docs]def get_star_state_filename(state, *, suffix=""): """Get the name of the file illustring the state of one star. Parameters ---------- state : str State name of the star. Returns ------- str Name of the file illustrating this state. """ if state.count("_") < 2: return state + suffix parts = state.split("_") return parts[0] + "_" + suffix
[docs]def get_event_state_filename(left_star_state, event_state, right_star_state, *, suffix=""): """Get the name of the file illustrating the event with the 2 given star. Parameters ---------- left_star_state : str State's name of the left star. event_state : str State's name of the event. right_star_state : type State's name of the right star. Returns ------- str Name of the file illustrating this complete state. """ left_star_filename = get_star_state_filename(left_star_state) if left_star_filename[-1] != "_": left_star_filename += "_" return ( left_star_filename + event_state + "_" + get_star_state_filename(right_star_state) + suffix )
[docs]def equal_with_epsilon(nb_1, nb_2, epsilon): """Check if 2 numbers are equal up to epsilon. Parameters ---------- nb_1 : int, float, double Value 1 to compare. nb_2 : int, float, double Value 2 compare. epsilon : int, float, double Difference allowed. Returns ------- bool True if numbers are equal up to epsilon, False if not. """ return abs(nb_1 - nb_2) < epsilon
[docs]def get_max_distance(data): """Get max distance from a dictionary of SimplifiedInfo. Parameters ---------- data : dictionnary of SimplifiedInfo Dictionnary, full of SimplifiedInfo, containing all infos Returns ------- float Max distance of this row, accroding to before/after state """ if data["separation"].state_after is not None: if np.isnan(data["separation"].state_after) or ( data["separation"].state_before > data["separation"].state_after ): return ( data["separation"].state_before if (not np.isnan(data["separation"].state_before)) else 0 ) else: return data["separation"].state_after else: if np.isnan(data["separation"].state_before): return 0 else: return data["separation"].state_before
[docs]def calculate_separation(data): """For each row in dataframe, calculate the separation. Parameters ---------- data : pandas.DataFrame Complete dataframe without 'separation'. Returns ------- Array of float Array of calculated separations, len(array) == nb rows. """ if (("orbital_period" not in data) or ("S1_mass" not in data) or ("S2_mass" not in data)): return None separations = [] for index, row in data.iterrows(): separations.append( orbital_separation_from_period(row["orbital_period"], row["S1_mass"], row["S2_mass"])) return separations
[docs]class SimplifiedInfo: """Simple class to store an before/after info and simplfy dataframe. Parameters ---------- state_before : any value Value for the older state. state_after : any value Value for the younger state. Attributes ---------- state_before state_after """ def __init__(self, state_before=None, state_after=None): """Initialize a SimplifiedInfo instance.""" self.state_before = state_before self.state_after = state_after
[docs]class Presenter: """Charged to setup window and format data before calling display funcs.""" # PATH_TO_DRAWS = os.path.dirname(__file__) + "\\draws\\%s.png" PATH_TO_DRAWS = os.path.join( PATH_TO_POSYDON, "posydon", "visualization", "VH_diagram", "draws" ) PATH_TO_SCREENS = os.path.join(os.getcwd(), "screens") _formating_dict = { "binary_index": "", "state": "", "event": "", "time": r"$%.2f\, Mys$", "separation": "", "orbital_period": r"$p = %.2f\, d$", "eccentricity": r"$e = %.2f $", "V_sys": "", "rl_relative_overflow_1": "", "rl_relative_overflow_2": "", "lg_mtransfer_rate": "", "mass_transfer_case": "", "nearest_neighbour_distance": "", "S1_state": "", "S1_metallicity": "", "S1_mass": r"$m=%3.2f\, \mathrm{M}_\odot$", "S1_log_R": "", "S1_log_L": "", "S1_lg_mdot": "", "S1_lg_system_mdot": "", "S1_lg_wind_mdot": "", "S1_he_core_mass": "", "S1_he_core_radius": "", "S1_c_core_mass": "", "S1_c_core_radius": "", "S1_o_core_mass": "", "S1_o_core_radius": "", "S1_center_h1": "", "S1_center_he4": "", "S1_center_c12": "", "S1_center_n14": "", "S1_center_o16": "", "S1_surface_h1": "", "S1_surface_he4": "", "S1_surface_c12": "", "S1_surface_n14": "", "S1_surface_o16": "", "S1_log_LH": "", "S1_log_LHe": "", "S1_log_LZ": "", "S1_log_Lnuc": "", "S1_c12_c12": "", "S1_avg_c_in_c_core": "", "S1_surf_avg_omega": "", "S1_surf_avg_omega_div_omega_crit": "", "S1_total_moment_of_inertia": "", "S1_log_total_angular_momentum": "", "S1_spin": "", "S1_profile": "", "S2_state": "", "S2_metallicity": "", "S2_mass": r"$m=%3.2f\, \mathrm{M}_\odot$", "S2_log_R": "", "S2_log_L": "", "S2_lg_mdot": "", "S2_lg_system_mdot": "", "S2_lg_wind_mdot": "", "S2_he_core_mass": "", "S2_he_core_radius": "", "S2_c_core_mass": "", "S2_c_core_radius": "", "S2_o_core_mass": "", "S2_o_core_radius": "", "S2_center_h1": "", "S2_center_he4": "", "S2_center_c12": "", "S2_center_n14": "", "S2_center_o16": "", "S2_surface_h1": "", "S2_surface_he4": "", "S2_surface_c12": "", "S2_surface_n14": "", "S2_surface_o16": "", "S2_log_LH": "", "S2_log_LHe": "", "S2_log_LZ": "", "S2_log_Lnuc": "", "S2_c12_c12": "", "S2_avg_c_in_c_core": "", "S2_surf_avg_omega": "", "S2_surf_avg_omega_div_omega_crit": "", "S2_total_moment_of_inertia": "", "S2_log_total_angular_momentum": "", "S2_spin": "", "S2_profile": "", } def __init__(self, filename, path="./", binary=False): """Initialize a Presenter instance.""" self._model = SimulationModel(filename=filename, path=path, binary=binary) self._model.load_csv() self._main_window = MainWindow() self._current_index = None self._current_data = None self._visualizer = self._main_window.start_visualization() self._visualizer.detail_required().connect( self._set_visualisation_detailed) self._visualizer.reduce_required().connect( self._set_visualisation_reduced) self._visualizer.simplify_required().connect( self._set_visualisation_simplified) self._visualizer.diagram_required().connect( self._set_visualisation_diagram) self._visualizer.distance_representation_required().connect( self._set_distance_representation) self._visualizer.save_required().connect(self.screen) self._present_mode = PresenterMode.DETAILED self._visualizer.options().set_showed_mode(self._present_mode) # maybe change it by flags if more option self._distance_representation = True self._visualizer.options().set_distance_representation( self._distance_representation )
[docs] def screen(self): """Take a screen of the displayed window.""" os.makedirs(self.PATH_TO_SCREENS, exist_ok=True) filepath = os.path.join( self.PATH_TO_SCREENS, datetime.today().strftime("%Y-%m-%d-%H-%M-%S") + "_" + str(self._current_index) + ".png") self._visualizer().update() self._visualizer().repaint() self._visualizer().saveAsPicture(filepath) return filepath
def _prepare_corresponding_data(self, index): """Get binary data and calculate missing columns. Parameters ---------- index : int Binary index to load. """ if self._current_index == index: return self._current_data = self._model.get_by_binary_index(index) if "separation" not in self._current_data: separations = calculate_separation(self._current_data) if separations is not None: self._current_data["separation"] = separations self._current_index = index
[docs] def present(self, index, mode=PresenterMode.DETAILED): """Preset the binary.""" self._present_mode = mode self._visualizer.options().set_showed_mode(self._present_mode) self._prepare_corresponding_data(index) self._update_visualisation() self._main_window.show()
[docs] def close(self): """Close the window.""" self._main_window.close()
def _update_visualisation(self): """Update the display according to current_index and present_mode.""" if self._current_index is None: return self._visualizer().reset() if self._present_mode == PresenterMode.DETAILED: self._prepare_basic_columns() self._detailed_presentation(self._current_data) elif self._present_mode == PresenterMode.REDUCED: self._prepare_basic_columns() self._reduced_presentation(self._current_data) elif self._present_mode == PresenterMode.SIMPLIFIED: self._prepare_basic_columns() self._simplified_presentation(self._current_data) elif self._present_mode == PresenterMode.DIAGRAM: self._prepare_diagram_columns() self._digram_presentation(self._current_data) def _prepare_basic_columns(self): """Add columns for Detailled/Reduced/Simplified View.""" self._time_id = self._visualizer().add_column(columnTYPE.TIMELINE) self._visualizer().get_column(self._time_id).set_title("TIME") self._S1_id = self._visualizer().add_column(columnTYPE.CONNECTED) self._visualizer().get_column(self._S1_id).set_title("S1") self._event_id = self._visualizer().add_column(columnTYPE.CONNECTED) self._visualizer().get_column(self._event_id).set_title("EVENT/STATE") self._S2_id = self._visualizer().add_column(columnTYPE.CONNECTED) self._visualizer().get_column(self._S2_id).set_title("S2") def _prepare_diagram_columns(self): """Add columns for Diagram View.""" self._time_id = self._visualizer().add_column(columnTYPE.TIMELINE) self._visualizer().get_column(self._time_id).set_title("TIME") self._state_id = self._visualizer().add_column(columnTYPE.CONNECTED) self._visualizer().get_column(self._state_id).set_title("STATE") def _set_visualisation_detailed(self): self._present_mode = PresenterMode.DETAILED self._update_visualisation() def _set_visualisation_reduced(self): self._present_mode = PresenterMode.REDUCED self._update_visualisation() def _set_visualisation_simplified(self): self._present_mode = PresenterMode.SIMPLIFIED self._update_visualisation() def _set_visualisation_diagram(self): self._present_mode = PresenterMode.DIAGRAM self._update_visualisation() def _set_distance_representation(self, state): self._distance_representation = state self._update_visualisation() def _prepare_basic_data(self, row): """Prepare widget to display as basic (non-simplified) data. Parameters ---------- row : dictionary Dataframe's row needing to be formated. Returns ------- Array of Infos Array containing derivated struct of Infos to create correspondong widget. """ time_data = PointInfos( self._time_id, self._formating_dict["time"] % to_megayears(row["time"])) s1_data = CaseInfos( self._S1_id, centered_txt=row["S1_state"], bot_left_txt=self._formating_dict["S1_mass"] % row["S1_mass"], ) s1_data.border_width = 2 event_data = CaseInfos(self._event_id) event_data.border_width = 2 event_data.centered_text = (str(row["event"]) if (str(row["event"]) != "nan") else str(row["state"])) if "eccentricity" in row: event_data.bot_right_text = ( self._formating_dict["eccentricity"] % row["eccentricity"] ) if "orbital_period" in row: event_data.bot_left_text = ( self._formating_dict["orbital_period"] % row["orbital_period"] ) s2_data = CaseInfos( self._S2_id, centered_txt=row["S2_state"], bot_right_txt=self._formating_dict["S2_mass"] % row["S2_mass"], ) s2_data.border_width = 2 return [time_data, s1_data, event_data, s2_data] def _set_simplified_infos(self, dataset, name, infos, side=False): """Format the information 'data_name' for simplfied view. This is done according to whether it exists or not, and if there is a before/after. Parameters ---------- dataset : dictionary of SimplifiedInfo Dictionary containing all infos to format. name : str Name of the info to format. infos : type Struct to create coresponding widget. side : bool Indicate on which side display the information, False = Left """ if name not in dataset: return if dataset[name].state_after is not None: if not side: infos.bot_left_text = (self._formating_dict[name] % dataset[name].state_after) infos.top_left_text = (self._formating_dict[name] % dataset[name].state_before) else: infos.bot_right_text = (self._formating_dict[name] % dataset[name].state_after) infos.top_right_text = (self._formating_dict[name] % dataset[name].state_before) else: if not side: infos.bot_left_text = (self._formating_dict[name] % dataset[name].state_before) else: infos.bot_right_text = (self._formating_dict[name] % dataset[name].state_before) def _prepare_simplified_line(self, data): """Prepare widget informations to display as simplified data. Parameters ---------- data : dictionary of SimplifiedInfo Infos formated to be displayed in widgets. Returns ------- Array of Infos Array containing derivated struct of Infos to create corresponding widgets. """ time_data = PointInfos( self._time_id, self._formating_dict["time"] % to_megayears(data["time"].state_before)) s1_data = CaseInfos(self._S1_id) s1_data.border_width = 2 if data["S1_state"].state_after is not None: s1_data.centered_text = data["S1_state"].state_after else: s1_data.centered_text = data["S1_state"].state_before self._set_simplified_infos(data, "S1_mass", s1_data, False) event_data = CaseInfos(self._event_id) event_data.border_width = 2 if data["state"].state_after is not None: event_data.centered_text = data["state"].state_after else: event_data.centered_text = data["state"].state_before self._set_simplified_infos(data, "eccentricity", event_data, False) self._set_simplified_infos(data, "orbital_period", event_data, True) s2_data = CaseInfos(self._S2_id) s2_data.border_width = 2 if data["S2_state"].state_after is not None: s2_data.centered_text = data["S2_state"].state_after else: s2_data.centered_text = data["S2_state"].state_before self._set_simplified_infos(data, "S2_mass", s2_data, True) return [time_data, s1_data, event_data, s2_data] def _get_distance_representation(self, simplified_distance, max_distance): """Normalize 1 of the 2 distances in simplified_distance. Use the max_distance, according to whether it exists or not, if it's NaN or 0. Parameters ---------- simplified_distance : SimplifiedInfo Struct containing distances of the current state. max_distance : float Description of parameter `max_distance`. Returns ------- float Normalized current star distance. """ if max_distance == 0: return 0 if simplified_distance.state_after is not None and not np.isnan( simplified_distance.state_after ): if simplified_distance.state_after < 1: simplified_distance.state_after = 1 if max_distance < 1: return np.log(simplified_distance.state_after) return (np.log(simplified_distance.state_after) / np.log(max_distance)) else: if np.isnan(simplified_distance.state_before): return 0 elif simplified_distance.state_before < 1: simplified_distance.state_before = 1 if max_distance < 1: return np.log(simplified_distance.state_before) return (np.log(simplified_distance.state_before) / np.log(max_distance)) def _set_diagram_infos(self, dataset, data_name, diagram_infos): """Format the information 'data_name' for diagram view. This is done according to whether it exists or not, and if there is a before/after. Parameters ---------- dataset : dictionary of SimplifiedInfo Dictionary containing all infos to format. data_name : str Name of the info to format. diagram_infos : StateInfos Struct to create coresponding widget. """ if data_name not in dataset: diagram_infos.top_texts.append("") diagram_infos.bot_texts.append("") return diagram_infos.top_texts.append(self._formating_dict[data_name] % dataset[data_name].state_before) if dataset[data_name].state_after is not None: diagram_infos.bot_texts.append(self._formating_dict[data_name] % dataset[data_name].state_after) else: diagram_infos.bot_texts.append("") def _prepare_diagram_line(self, data, max_distance): """Prepare widget information to display basic (non-simplified) data. Parameters ---------- data : dictionary of SimplifiedInfo Infos formated to be displayed in widgets. max_distance : float Max distance between 2 star, used to set normalized distance for this state. Returns ------- Array of Infos Array containing derivated struct of Infos to create corresponding widgets. """ time_info = PointInfos( self._time_id, self._formating_dict["time"] % to_megayears( data["time"].state_before), ) state_info = StateInfos(self._state_id) if self._distance_representation and "separation" in data: state_info.distance = self._get_distance_representation( data["separation"], max_distance ) else: state_info.distance = 1 state_before_filename = os.path.join( self.PATH_TO_DRAWS, get_event_state_filename( data["S1_state"].state_before, data["state"].state_before, data["S2_state"].state_before, suffix=".png", ), ) state_after_filename = os.path.join( self.PATH_TO_DRAWS, get_event_state_filename( data["S1_state"].state_before, data["state"].state_before, data["S2_state"].state_before, suffix=".png", ), ) if (data["state"].state_after != "detached" and file_exist(state_after_filename)): state_info.event_filename = state_after_filename elif data["state"].state_before != "detached" and file_exist( state_before_filename ): state_info.event_filename = state_before_filename else: if data["S1_state"].state_after is not None: state_info.S1_filename = os.path.join( self.PATH_TO_DRAWS, get_star_state_filename( data["S1_state"].state_after, suffix=".png" ), ) else: state_info.S1_filename = os.path.join( self.PATH_TO_DRAWS, get_star_state_filename( data["S1_state"].state_before, suffix=".png" ), ) if data["S2_state"].state_after is not None: state_info.S2_filename = os.path.join( self.PATH_TO_DRAWS, get_star_state_filename( data["S2_state"].state_after, suffix=".png" ), ) else: state_info.S2_filename = os.path.join( self.PATH_TO_DRAWS, get_star_state_filename( data["S2_state"].state_before, suffix=".png" ), ) self._set_diagram_infos(data, "S1_mass", state_info) self._set_diagram_infos(data, "eccentricity", state_info) self._set_diagram_infos(data, "orbital_period", state_info) self._set_diagram_infos(data, "S2_mass", state_info) return [time_info, state_info] def _detailed_presentation(self, data): """Format and display data in the Detailled View. Parameters ---------- data : pandas.DataFrame Data to display in detailled view. """ for index, row in data.iterrows(): if row["event"] == "END": continue formatted_data = self._prepare_basic_data(row) self._visualizer().add_line(formatted_data) def _reduce_data(self, data): """Reduce the dataframe given by folowing hardcoded rules. If the row has same time (within 10,000 years) as the previous one, each info is combined with the previous info in a SimplifiedInfo struct giving a dictionary with same row's key, full of SimplifiedInfo, which are the simplified row. Parameters ---------- data : pandas.DataFrame Data to simplify. Returns ------- Array of dictionary of SimplifiedInfo Each dictionary hold same keys of 1 data's row, but with SimplifiedInfo as value. Array length =/= row's number in data because some infos are combined in SimplifiedInfo. """ reduced_data = [] for index, row in data.iterrows(): if row["event"] == "END" or row["event"] == "redirect": continue new_data = to_simplified_data(row) if not reduced_data: reduced_data.append(new_data) else: if ( equal_with_epsilon( reduced_data[-1]["time"].state_before, new_data["time"].state_before, 10000, ) and (reduced_data[-1]["state"].state_before != "detached") and (new_data["state"].state_before != "detached") ): # Need to combine combine_simplified_data( data_to=reduced_data[-1], data_from=new_data ) else: reduced_data.append(new_data) return reduced_data def _reduced_presentation(self, data): """Format and display data in the Reduced View. Parameters ---------- data : pandas.DataFrame Data to display in reduced view. """ reduced_data = self._reduce_data(data) for line_data in reduced_data: self._visualizer().add_line( self._prepare_simplified_line(line_data)) def _simplify_data(self, data): """Simplify the dataframe given by folowing hardcoded rules. If the row isn't interesting, each info is combined with the previous info in a SimplifiedInfo struct, giving a dictionary with same row's key, full of SimplifiedInfo, which are the simplified row. Parameters ---------- data : pandas.DataFrame Data to simplify. Returns ------- Array of dictionary of SimplifiedInfo Each dictionary hold same keys of 1 data's row, but with SimplifiedInfo as value. Array length =/= row's number in data because some infos are combined in SimplifiedInfo. """ simplified_data = [] for index, row in data.iterrows(): if row["event"] == "END" or row["event"] == "redirect": continue new_data = to_simplified_data(row) if not simplified_data: simplified_data.append(new_data) else: if ( equal_with_epsilon( simplified_data[-1]["time"].state_before, new_data["time"].state_before, 10000, ) and simplified_data[-1]["state"].state_before != "detached" and new_data["state"].state_before != "detached" ) or ( simplified_data[-1]["state"].state_before == "detached" and new_data["state"].state_before == "detached" ): # Need to combine combine_simplified_data( data_to=simplified_data[-1], data_from=new_data ) else: simplified_data.append(new_data) return simplified_data def _simplified_presentation(self, data): """Format and display data in the Simplified View. Parameters ---------- data : pandas.DataFrame Data to display in simplified view. """ simplified_data = self._simplify_data(data) for data in simplified_data: self._visualizer().add_line(self._prepare_simplified_line(data)) def _digram_presentation(self, data): """Format and display data in the Diagram View. Parameters ---------- data : pandas.DataFrame Data to display in diagram view. """ simplified_data = self._simplify_data(data) max_distance_data = max(simplified_data, key=get_max_distance) max_distance = get_max_distance(max_distance_data) for line_data in simplified_data: self._visualizer().add_line( self._prepare_diagram_line(line_data, max_distance) ) if ( simplified_data[-1]["state"].state_after == "disrupted" or simplified_data[-1]["state"].state_after == "merged" ): aditional_info = CaseInfos(self._state_id) aditional_info.centered_text = simplified_data[-1][ "state"].state_after aditional_info.connected = False self._visualizer().add_line([aditional_info])