diff --git a/tardis/visualization/tools/sdec_plot.py b/tardis/visualization/tools/sdec_plot.py index 484647fe11f..f633ceedf59 100644 --- a/tardis/visualization/tools/sdec_plot.py +++ b/tardis/visualization/tools/sdec_plot.py @@ -14,9 +14,20 @@ import matplotlib.colors as clr import plotly.graph_objects as go -from tardis.util.base import atomic_number2element_symbol +from tardis.util.base import ( + atomic_number2element_symbol, + element_symbol2atomic_number, + species_string_to_tuple, + species_tuple_to_string, + roman_to_int, + int_to_roman, +) from tardis.visualization import plot_util as pu +import logging + +logger = logging.getLogger(__name__) + class SDECData: """The data of simulation model used by Spectral element DEComposition (SDEC) Plot. @@ -110,18 +121,26 @@ def __init__( ) # & operator is quite faster than np.logical_and on pd.Series self.packets_df_line_interaction = self.packets_df.loc[line_mask].copy() - # Add columns for atomic number of last interaction in/out - self.packets_df_line_interaction["last_line_interaction_out_atom"] = ( + # Add columns for atomic number of last interaction out + self.packets_df_line_interaction["last_line_interaction_atom"] = ( self.lines_df["atomic_number"] .iloc[ self.packets_df_line_interaction["last_line_interaction_out_id"] ] .to_numpy() ) - self.packets_df_line_interaction["last_line_interaction_in_atom"] = ( + # Add columns for the species id of last interaction + # Species id is given by 100 * Z + X, where Z is atomic number and X is ion number + self.packets_df_line_interaction["last_line_interaction_species"] = ( self.lines_df["atomic_number"] .iloc[ - self.packets_df_line_interaction["last_line_interaction_in_id"] + self.packets_df_line_interaction["last_line_interaction_out_id"] + ] + .to_numpy() + * 100 + + self.lines_df["ion_number"] + .iloc[ + self.packets_df_line_interaction["last_line_interaction_out_id"] ] .to_numpy() ) @@ -424,6 +443,95 @@ def from_hdf(cls, hdf_fpath): ) ) + def _parse_species_list(self, species_list): + """ + Parse user requested species list and create list of species ids to be used. + + Parameters + ---------- + species_list : list of species to plot + List of species (e.g. Si II, Ca II, etc.) that the user wants to show as unique colours. + Species can be given as an ion (e.g. Si II), an element (e.g. Si), a range of ions + (e.g. Si I - V), or any combination of these (e.g. species_list = [Si II, Fe I-V, Ca]) + + """ + + if species_list is not None: + # check if there are any digits in the species list. If there are, then exit. + # species_list should only contain species in the Roman numeral + # format, e.g. Si II, and each ion must contain a space + if any(char.isdigit() for char in " ".join(species_list)) == True: + raise ValueError( + "All species must be in Roman numeral form, e.g. Si II" + ) + else: + full_species_list = [] + for species in species_list: + # check if a hyphen is present. If it is, then it indicates a + # range of ions. Add each ion in that range to the list as a new entry + if "-" in species: + # split the string on spaces. First thing in the list is then the element + element = species.split(" ")[0] + # Next thing is the ion range + # convert the requested ions into numerals + first_ion_numeral = roman_to_int( + species.split(" ")[-1].split("-")[0] + ) + second_ion_numeral = roman_to_int( + species.split(" ")[-1].split("-")[-1] + ) + # add each ion between the two requested into the species list + for ion_number in np.arange( + first_ion_numeral, second_ion_numeral + 1 + ): + full_species_list.append( + f"{element} {int_to_roman(ion_number)}" + ) + else: + # Otherwise it's either an element or ion so just add to the list + full_species_list.append(species) + + # full_species_list is now a list containing each individual species requested + # e.g. it parses species_list = [Si I - V] into species_list = [Si I, Si II, Si III, Si IV, Si V] + self._full_species_list = full_species_list + requested_species_ids = [] + keep_colour = [] + + # go through each of the requested species. Check whether it is + # an element or ion (ions have spaces). If it is an element, + # add all possible ions to the ions list. Otherwise just add + # the requested ion + for species in full_species_list: + if " " in species: + requested_species_ids.append( + [ + species_string_to_tuple(species)[0] * 100 + + species_string_to_tuple(species)[1] + ] + ) + else: + atomic_number = element_symbol2atomic_number(species) + requested_species_ids.append( + [ + atomic_number * 100 + ion_number + for ion_number in np.arange(atomic_number) + ] + ) + # add the atomic number to a list so you know that this element should + # have all species in the same colour, i.e. it was requested like + # species_list = [Si] + keep_colour.append(atomic_number) + requested_species_ids = [ + species_id + for temp_list in requested_species_ids + for species_id in temp_list + ] + + self._species_list = requested_species_ids + self._keep_colour = keep_colour + else: + self._species_list = None + def _calculate_plotting_data( self, packets_mode, packet_wvl_range, distance, nelements ): @@ -519,13 +627,13 @@ def _calculate_plotting_data( # Calculate luminosities to be shown in plot ( self.emission_luminosities_df, - self.emission_elements, + self.emission_species, ) = self._calculate_emission_luminosities( packets_mode=packets_mode, packet_wvl_range=packet_wvl_range ) ( self.absorption_luminosities_df, - self.absorption_elements, + self.absorption_species, ) = self._calculate_absorption_luminosities( packets_mode=packets_mode, packet_wvl_range=packet_wvl_range ) @@ -544,9 +652,81 @@ def _calculate_plotting_data( ascending=False ) - # If nelements is not included, the list of elements is just all elements - if nelements is None: - self.elements = np.array(list(self.total_luminosities_df.keys())) + # If nelements and species_list are not included, the list of elements is just all elements + if nelements is None and self._species_list is None: + self.species = np.array(list(self.total_luminosities_df.keys())) + elif self._species_list is not None: + # Compare the species present in the model to those in the requested list + # Mask out those that aren't in the model + mask = np.in1d( + np.array(list(sorted_list.keys())), self._species_list + ) + # If species_list is included then create a new column which is the sum + # of all other species i.e. those that aren't in the requested list + self.total_luminosities_df.insert( + loc=0, + column="other", + value=self.total_luminosities_df[sorted_list.keys()[~mask]].sum( + axis=1 + ), + ) + # Then drop all of the individual columns for species included in 'other' + self.total_luminosities_df.drop( + sorted_list.keys()[~mask], inplace=True, axis=1 + ) + # Repeat this for the emission and absorption dfs + # This will require creating a temporary list that includes 'noint' and 'escatter' + # packets, because you don't want them dropped or included in 'other' + temp = [species for species in self._species_list] + temp.append("noint") + temp.append("escatter") + mask = np.in1d( + np.array(list(self.emission_luminosities_df.keys())), temp + ) + # If species_list is included then create a new column which is the sum + # of all other species i.e. those that aren't in the requested list + self.emission_luminosities_df.insert( + loc=0, + column="other", + value=self.emission_luminosities_df[ + self.emission_luminosities_df.keys()[~mask] + ].sum(axis=1), + ) + # Need to add a new value to the mask array for the 'other' column just added + mask = np.insert(mask, 0, True) + # Then drop all of the individual columns for species included in 'other' + self.emission_luminosities_df.drop( + self.emission_luminosities_df.keys()[~mask], + inplace=True, + axis=1, + ) + + temp = [species for species in self._species_list] + mask = np.in1d( + np.array(list(self.absorption_luminosities_df.keys())), temp + ) + # If species_list is included then create a new column which is the sum + # of all other species i.e. those that aren't in the requested list + self.absorption_luminosities_df.insert( + loc=0, + column="other", + value=self.absorption_luminosities_df[ + self.absorption_luminosities_df.keys()[~mask] + ].sum(axis=1), + ) + # Need to add a new value to the mask array for the 'other' column just added + mask = np.insert(mask, 0, True) + # Then drop all of the individual columns for species included in 'other' + self.absorption_luminosities_df.drop( + self.absorption_luminosities_df.keys()[~mask], + inplace=True, + axis=1, + ) + + # Get the list of species in the model + # Index from 1: to avoid the 'other' column + self.species = np.sort(self.total_luminosities_df.keys()[1:]) + else: # If nelements is included then create a new column which is the sum # of all other elements, i.e. those that aren't in the top contributing nelements @@ -587,9 +767,9 @@ def _calculate_plotting_data( self.absorption_luminosities_df.drop( sorted_list.keys()[nelements:], inplace=True, axis=1 ) - + # Get the list of species in the model # Index from 1: to avoid the 'other' column - self.elements = np.sort(self.total_luminosities_df.keys()[1:]) + self.species = np.sort(self.total_luminosities_df.keys()[1:]) self.photosphere_luminosity = self._calculate_photosphere_luminosity( packets_mode=packets_mode @@ -722,15 +902,23 @@ def _calculate_emission_luminosities(self, packets_mode, packet_wvl_range): # Group packets_df by atomic number of elements with which packets # had their last emission (interaction out) - packets_df_grouped = ( - self.data[packets_mode] - .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] - .groupby(by="last_line_interaction_out_atom") - ) + # or if species_list is requested then group by species id + if self._species_list is None: + packets_df_grouped = ( + self.data[packets_mode] + .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] + .groupby(by="last_line_interaction_atom") + ) + else: + packets_df_grouped = ( + self.data[packets_mode] + .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] + .groupby(by="last_line_interaction_species") + ) - # Contribution of each element with which packets interacted ---------- - for atomic_number, group in packets_df_grouped: - # Histogram of specific element + # Contribution of each species with which packets interacted ---------- + for identifier, group in packets_df_grouped: + # Histogram of specific species hist_el = np.histogram( group["nus"], bins=self.plot_frequency_bins, @@ -748,14 +936,14 @@ def _calculate_emission_luminosities(self, packets_mode, packet_wvl_range): ) L_lambda_el = L_nu_el * self.plot_frequency / self.plot_wavelength - luminosities_df[atomic_number] = L_lambda_el.value + luminosities_df[identifier] = L_lambda_el.value - # Create an array of the elements with which packets interacted - emission_elements_present = np.array( + # Create an array of the species with which packets interacted + emission_species_present = np.array( list(packets_df_grouped.groups.keys()) ) - return luminosities_df, emission_elements_present + return luminosities_df, emission_species_present def _calculate_absorption_luminosities( self, packets_mode, packet_wvl_range @@ -803,13 +991,22 @@ def _calculate_absorption_luminosities( # Group packets_df by atomic number of elements with which packets # had their last absorption (interaction in) - packets_df_grouped = ( - self.data[packets_mode] - .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] - .groupby(by="last_line_interaction_in_atom") - ) - for atomic_number, group in packets_df_grouped: - # Histogram of specific element + # or if species_list is requested then group by species id + if self._species_list is None: + packets_df_grouped = ( + self.data[packets_mode] + .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] + .groupby(by="last_line_interaction_atom") + ) + else: + packets_df_grouped = ( + self.data[packets_mode] + .packets_df_line_interaction.loc[self.packet_nu_line_range_mask] + .groupby(by="last_line_interaction_species") + ) + + for identifier, group in packets_df_grouped: + # Histogram of specific species hist_el = np.histogram( group["last_line_interaction_in_nu"], bins=self.plot_frequency_bins, @@ -827,13 +1024,13 @@ def _calculate_absorption_luminosities( ) L_lambda_el = L_nu_el * self.plot_frequency / self.plot_wavelength - luminosities_df[atomic_number] = L_lambda_el.value + luminosities_df[identifier] = L_lambda_el.value - absorption_elements_present = np.array( + absorption_species_present = np.array( list(packets_df_grouped.groups.keys()) ) - return luminosities_df, absorption_elements_present + return luminosities_df, absorption_species_present def _calculate_photosphere_luminosity(self, packets_mode): """ @@ -873,6 +1070,7 @@ def generate_plot_mpl( figsize=(12, 7), cmapname="jet", nelements=None, + species_list=None, ): """ Generate Spectral element DEComposition (SDEC) Plot using matplotlib. @@ -906,12 +1104,27 @@ def generate_plot_mpl( largest contribution to total luminosity absorbed and emitted. Other elements are shown in silver. Default value is None, which displays all elements + species_list: list of strings or None + list of strings containing the names of species that should be included in the SDEC plots. + Must be given in Roman numeral format. Can include specific ions, a range of ions, + individual elements, or any combination of these: + e.g. ['Si II', 'Ca II', 'C', 'Fe I-V'] Returns ------- matplotlib.axes._subplots.AxesSubplot Axis on which SDEC Plot is created """ + + # If species_list and nelements requested, tell user that nelements is ignored + if species_list is not None and nelements is not None: + logger.info( + "Both nelements and species_list were requested. Species_list takes priority; nelements is ignored" + ) + + # Parse the requested species list + self._parse_species_list(species_list=species_list) + # Calculate data attributes required for plotting # and save them in instance itself self._calculate_plotting_data( @@ -926,9 +1139,15 @@ def generate_plot_mpl( else: self.ax = ax + # Get the labels in the color bar. This determines the number of unique colors + self._make_colorbar_labels() # Set colormap to be used in elements of emission and absorption plots - self.cmap = cm.get_cmap(cmapname, self.elements.size) + self.cmap = cm.get_cmap(cmapname, len(self._species_name)) + # Get the number of unqie colors + self._make_colorbar_colors() + self._show_colorbar_mpl() + # Plot emission and absorption components self._plot_emission_mpl() self._plot_absorption_mpl() @@ -950,8 +1169,6 @@ def generate_plot_mpl( label="Blackbody Photosphere", ) - self._show_colorbar_mpl() - # Set legends and labels self.ax.legend(fontsize=12) self.ax.set_xlabel(r"Wavelength $[\mathrm{\AA}]$", fontsize=12) @@ -1013,31 +1230,40 @@ def _plot_emission_mpl(self): label="Other elements", ) - elements_z = self.elements # Contribution from each element - for i, atomic_number in enumerate(elements_z): - # Add a try catch because elemets_z comes from the total contribution of absorption and emission. - # Therefore it's possible that something in elements_z is not in the emission df + for species_counter, identifier in enumerate(self.species): try: lower_level = upper_level upper_level = ( lower_level - + self.emission_luminosities_df[atomic_number].to_numpy() + + self.emission_luminosities_df[identifier].to_numpy() ) self.ax.fill_between( self.plot_wavelength, lower_level, upper_level, - color=self.cmap(i / len(self.elements)), + color=self._color_list[species_counter], cmap=self.cmap, linewidth=0, ) except: - print( - atomic_number2element_symbol(atomic_number) - + " is not in the emitted packets; skipping" - ) + # Add notifications that this species was not in the emission df + if self._species_list is None: + longer.info( + f"{atomic_number2element_symbol(identifier)}" + f" is not in the emitted packets; skipping" + ) + else: + # Get the ion number and atomic number for each species + ion_number = identifier % 100 + atomic_number = (identifier - ion_number) / 100 + + logger.info( + f"{atomic_number2element_symbol(atomic_number)}" + f"{int_to_roman(ion_number + 1)}" + f" is not in the emitted packets; skipping" + ) def _plot_absorption_mpl(self): """Plot absorption part of the SDEC Plot using matplotlib.""" @@ -1060,50 +1286,144 @@ def _plot_absorption_mpl(self): color="silver", ) - elements_z = self.elements - for i, atomic_number in enumerate(elements_z): - # Add a try catch because elemets_z comes from the total contribution of absorption and emission. - # Therefore it's possible that something in elements_z is not in the absorption df + for species_counter, identifier in enumerate(self.species): try: upper_level = lower_level lower_level = ( upper_level - - self.absorption_luminosities_df[atomic_number].to_numpy() + - self.absorption_luminosities_df[identifier].to_numpy() ) self.ax.fill_between( self.plot_wavelength, upper_level, lower_level, - color=self.cmap(i / len(elements_z)), + color=self._color_list[species_counter], cmap=self.cmap, linewidth=0, ) + except: - print( - atomic_number2element_symbol(atomic_number) - + " is not in the absorbed packets; skipping" - ) + # Add notifications that this species was not in the emission df + if self._species_list is None: + longer.info( + f"{atomic_number2element_symbol(identifier)}" + f" is not in the absorbed packets; skipping" + ) + else: + # Get the ion number and atomic number for each species + ion_number = identifier % 100 + atomic_number = (identifier - ion_number) / 100 + + logger.info( + f"{atomic_number2element_symbol(atomic_number)}" + f"{int_to_roman(ion_number + 1)}" + f" is not in the absorbed packets; skipping" + ) def _show_colorbar_mpl(self): """Show matplotlib colorbar with labels of elements mapped to colors.""" + color_values = [ - self.cmap(i / self.elements.size) for i in range(self.elements.size) + self.cmap(species_counter / len(self._species_name)) + for species_counter in range(len(self._species_name)) ] + custcmap = clr.ListedColormap(color_values) - norm = clr.Normalize(vmin=0, vmax=self.elements.size) + norm = clr.Normalize(vmin=0, vmax=len(self._species_name)) mappable = cm.ScalarMappable(norm=norm, cmap=custcmap) - mappable.set_array(np.linspace(1, self.elements.size + 1, 256)) + mappable.set_array(np.linspace(1, len(self._species_name) + 1, 256)) cbar = plt.colorbar(mappable, ax=self.ax) - bounds = np.arange(self.elements.size) + 0.5 + bounds = np.arange(len(self._species_name)) + 0.5 cbar.set_ticks(bounds) - elements_name = [ - atomic_number2element_symbol(atomic_num) - for atomic_num in self.elements - ] - cbar.set_ticklabels(elements_name) + cbar.set_ticklabels(self._species_name) + + def _make_colorbar_labels(self): + """Get the labels for the species in the colorbar.""" + if self._species_list is None: + # If species_list is none then the labels are just elements + species_name = [ + atomic_number2element_symbol(atomic_num) + for atomic_num in self.species + ] + else: + species_name = [] + for species in self.species: + # Go through each species requested + ion_number = species % 100 + atomic_number = (species - ion_number) / 100 + + ion_numeral = int_to_roman(ion_number + 1) + atomic_symbol = atomic_number2element_symbol(atomic_number) + + # if the element was requested, and not a specific ion, then + # add the element symbol to the label list + if (atomic_number in self._keep_colour) & ( + atomic_symbol not in species_name + ): + # compiling the label, and adding it to the list + label = f"{atomic_symbol}" + species_name.append(label) + elif atomic_number not in self._keep_colour: + # otherwise add the ion to the label list + label = f"{atomic_symbol} {ion_numeral}" + species_name.append(label) + + self._species_name = species_name + + def _make_colorbar_colors(self): + """Get the colours for the species to be plotted.""" + # the colours depends on the species present in the model and what's requested + # some species need to be shown in the same colour, so the exact colours have to be + # worked out + + color_list = [] + + # Colors for each element + # Create new variables to keep track of the last atomic number that was plotted + # This is used when plotting species in case an element was given in the list + # This is to ensure that all ions of that element are grouped together + # ii is to track the colour index + # e.g. if Si is given in species_list, this is to ensure Si I, Si II, etc. all have the same colour + color_counter = 0 + previous_atomic_number = 0 + for species_counter, identifier in enumerate(self.species): + if self._species_list is not None: + # Get the ion number and atomic number for each species + ion_number = identifier % 100 + atomic_number = (identifier - ion_number) / 100 + if previous_atomic_number == 0: + # If this is the first species being plotted, then take note of the atomic number + # don't update the colour index + color_counter = color_counter + previous_atomic_number = atomic_number + elif previous_atomic_number in self._keep_colour: + # If the atomic number is in the list of elements that should all be plotted in the same colour + # then don't update the colour index if this element has been plotted already + if previous_atomic_number == atomic_number: + color_counter = color_counter + previous_atomic_number = atomic_number + else: + # Otherwise, increase the colour counter by one, because this is a new element + color_counter = color_counter + 1 + previous_atomic_number = atomic_number + else: + # If this is just a normal species that was requested then increment the colour index + color_counter = color_counter + 1 + previous_atomic_number = atomic_number + # Calculate the colour of this species + color = self.cmap(color_counter / len(self._species_name)) + + else: + # If you're not using species list then this is just a fraction based on the total + # number of columns in the dataframe + color = self.cmap(species_counter / len(self.species)) + + color_list.append(color) + + self._color_list = color_list def generate_plot_ply( self, @@ -1116,6 +1436,7 @@ def generate_plot_ply( graph_height=600, cmapname="jet", nelements=None, + species_list=None, ): """ Generate interactive Spectral element DEComposition (SDEC) Plot using plotly. @@ -1149,12 +1470,26 @@ def generate_plot_ply( largest contribution to total luminosity absorbed and emitted. Other elements are shown in silver. Default value is None, which displays all elements - + species_list: list of strings or None + list of strings containing the names of species that should be included in the SDEC plots. + Must be given in Roman numeral format. Can include specific ions, a range of ions, + individual elements, or any combination of these: + e.g. ['Si II', 'Ca II', 'C', 'Fe I-V'] Returns ------- plotly.graph_objs._figure.Figure Figure object on which SDEC Plot is created """ + + # If species_list and nelements requested, tell user that nelements is ignored + if species_list is not None and nelements is not None: + logger.info( + "Both nelements and species_list were requested. Species_list takes priority; nelements is ignored" + ) + + # Parse the requested species list + self._parse_species_list(species_list=species_list) + # Calculate data attributes required for plotting # and save them in instance itself self._calculate_plotting_data( @@ -1169,9 +1504,14 @@ def generate_plot_ply( else: self.fig = fig + # Get the labels in the color bar. This determines the number of unique colors + self._make_colorbar_labels() # Set colormap to be used in elements of emission and absorption plots - self.cmap = cm.get_cmap(cmapname, self.elements.size) + self.cmap = cm.get_cmap(cmapname, len(self._species_name)) + # Get the number of unique colors + self._make_colorbar_colors() + # Plot absorption and emission components self._plot_emission_ply() self._plot_absorption_ply() @@ -1282,29 +1622,39 @@ def _plot_emission_ply(self): ) ) - elements_z = self.elements - for i, atomic_num in enumerate(elements_z): - # Add a try catch because elemets_z comes from the total contribution of absorption and emission. - # Therefore it's possible that something in elements_z is not in the emission df + # Contribution from each element + for species_counter, identifier in enumerate(self.species): try: self.fig.add_trace( go.Scatter( x=self.emission_luminosities_df.index, - y=self.emission_luminosities_df[atomic_num], + y=self.emission_luminosities_df[identifier], mode="none", - name=atomic_number2element_symbol(atomic_num), + name="none", fillcolor=self.to_rgb255_string( - self.cmap(i / len(self.elements)) + self._color_list[species_counter] ), stackgroup="emission", showlegend=False, ) ) except: - print( - atomic_number2element_symbol(atomic_num) - + " is not in the emitted packets; skipping" - ) + # Add notifications that this species was not in the emission df + if self._species_list is None: + longer.info( + f"{atomic_number2element_symbol(identifier)}" + f" is not in the emitted packets; skipping" + ) + else: + # Get the ion number and atomic number for each species + ion_number = identifier % 100 + atomic_number = (identifier - ion_number) / 100 + + logger.info( + f"{atomic_number2element_symbol(atomic_number)}" + f"{int_to_roman(ion_number + 1)}" + f" is not in the emitted packets; skipping" + ) def _plot_absorption_ply(self): """Plot absorption part of the SDEC Plot using plotly.""" @@ -1324,57 +1674,70 @@ def _plot_absorption_ply(self): ) ) - elements_z = self.elements - for i, atomic_num in enumerate(elements_z): - # Add a try catch because elemets_z comes from the total contribution of absorption and emission. - # Therefore it's possible that something in elements_z is not in the absorption df + for species_counter, identifier in enumerate(self.species): try: self.fig.add_trace( go.Scatter( x=self.absorption_luminosities_df.index, # to plot absorption luminosities along negative y-axis - y=self.absorption_luminosities_df[atomic_num] * -1, + y=self.absorption_luminosities_df[identifier] * -1, mode="none", - name=atomic_number2element_symbol(atomic_num), + name="none", fillcolor=self.to_rgb255_string( - self.cmap(i / len(self.elements)) + self._color_list[species_counter] ), stackgroup="absorption", showlegend=False, ) ) + except: - print( - atomic_number2element_symbol(atomic_num) - + " is not in the absorbed packets; skipping" - ) + # Add notifications that this species was not in the emission df + if self._species_list is None: + longer.info( + f"{atomic_number2element_symbol(identifier)}" + f" is not in the absorbed packets; skipping" + ) + else: + # Get the ion number and atomic number for each species + ion_number = identifier % 100 + atomic_number = (identifier - ion_number) / 100 + + logger.info( + f"{atomic_number2element_symbol(atomic_number)}" + f"{int_to_roman(ion_number + 1)}" + f" is not in the absorbed packets; skipping" + ) def _show_colorbar_ply(self): """Show plotly colorbar with labels of elements mapped to colors.""" # Interpolate [0, 1] range to create bins equal to number of elements - colorscale_bins = np.linspace(0, 1, num=self.elements.size + 1) + colorscale_bins = np.linspace(0, 1, num=len(self._species_name) + 1) # Create a categorical colorscale [a list of (reference point, color)] # by mapping same reference points (excluding 1st and last bin edge) # twice in a row (https://plotly.com/python/colorscales/#constructing-a-discrete-or-discontinuous-color-scale) categorical_colorscale = [] - for i in range(self.elements.size): - color = self.to_rgb255_string(self.cmap(colorscale_bins[i])) - categorical_colorscale.append((colorscale_bins[i], color)) - categorical_colorscale.append((colorscale_bins[i + 1], color)) + for species_counter in range(len(self._species_name)): + color = self.to_rgb255_string( + self.cmap(colorscale_bins[species_counter]) + ) + categorical_colorscale.append( + (colorscale_bins[species_counter], color) + ) + categorical_colorscale.append( + (colorscale_bins[species_counter + 1], color) + ) coloraxis_options = dict( colorscale=categorical_colorscale, showscale=True, cmin=0, - cmax=self.elements.size, + cmax=len(self._species_name), colorbar=dict( title="Elements", - tickvals=np.arange(0, self.elements.size) + 0.5, - ticktext=[ - atomic_number2element_symbol(atomic_num) - for atomic_num in self.elements - ], + tickvals=np.arange(0, len(self._species_name)) + 0.5, + ticktext=self._species_name, # to change length and position of colorbar len=0.75, yanchor="top",