Source code for h_transport_materials.plotting

import matplotlib.pyplot as plt
import numpy as np
import pint
from h_transport_materials import (
    Property,
    PropertiesGroup,
    ureg,
    Solubility,
    Diffusivity,
    Permeability,
    RecombinationCoeff,
    DissociationCoeff,
)
from typing import Union
import warnings


[docs]def plot( prop: Union[Property, PropertiesGroup], T_bounds=(300, 1200), inverse_temperature=True, auto_label=True, show_datapoints=True, scatter_kwargs={}, colour_by="property", key_to_colour=None, **kwargs, ): """Plots a Property object on a temperature plot Args: prop (Property or PropertiesGroup): the property (or group of properties) to plot. T_bounds (tuple, optional): If the property doesn't have a temperature range, this range will be used. Defaults to (300, 1200). inverse_temperature (bool, optional): If True, the x axis will be the inverse temperature (in K^-1). Defaults to True. auto_label (bool, optional): If True, a label will be automatically generated from the isotope, author and year. Ignored if label is set in kwargs. Defaults to True. show_datapoints (bool, optional): If True, the experimental datapoints will be scattered too. Defaults to True. scatter_kwargs (dict, optional): other matplotlib.pyplot.scatter arguments. Defaults to {}. colour_by (str, optional): a property attribute to colour by (eg. "author", "isotope", "material"). Defaults to "property". key_to_colour (dict, optional): a dictionary with keys (eg. material) corresponding to colours. Defaults to None kwargs: other matplotlib.pyplot.plot arguments Returns: matplotlib.lines.Line2D: the Line2D artist """ if isinstance(prop, Property): return _plot_property( prop, T_bounds, inverse_temperature, auto_label, show_datapoints, scatter_kwargs, **kwargs, ) elif isinstance(prop, PropertiesGroup): group = prop if prop.units == "mixed units": raise ValueError("Cannot plot group with mixed units") # compute the prop to colour mapping if colour_by != "property": prop_to_color = get_prop_to_color( group, colour_by, key_to_colour, colour_cycle=plt.rcParams["axes.prop_cycle"].by_key()["color"], ) elif key_to_colour is not None: warnings.warn( UserWarning( "key_to_colour specified with colour_by='property' will be ignored" ) ) lines = [] for single_prop in group: # change colour from kwargs if need be current_kwargs = kwargs.copy() if colour_by != "property" and "color" not in kwargs: current_kwargs["color"] = prop_to_color[single_prop] # change the label based on colour_by current_kwargs["label"] = _label_from_colour_by(single_prop, colour_by) l = _plot_property( single_prop, T_bounds=T_bounds, inverse_temperature=inverse_temperature, auto_label=auto_label, show_datapoints=show_datapoints, scatter_kwargs=scatter_kwargs, **current_kwargs, ) lines.append(l) legend() return lines
def _plot_property( prop: Property, T_bounds=(300, 1200), inverse_temperature=True, auto_label=True, show_datapoints=True, scatter_kwargs={}, **kwargs, ): """Plots a Property object on a temperature plot Args: prop (Property): the property (or group of properties) to plot. T_bounds (tuple, optional): If the property doesn't have a temperature range, this range will be used. Defaults to (300, 1200). inverse_temperature (bool, optional): If True, the x axis will be the inverse temperature (in K^-1). Defaults to True. auto_label (bool, optional): If True, a label will be automatically generated from the isotope, author and year. Ignored if label is set in kwargs. Defaults to True. show_datapoints (bool, optional): If True, the experimental datapoints will be scattered too. Defaults to True. scatter_kwargs (dict, optional): other matplotlib.pyplot.scatter arguments. Defaults to {}. kwargs: other matplotlib.pyplot.plot arguments Returns: matplotlib.lines.Line2D: the Line2D artist """ if prop.range is None: range = T_bounds else: range = prop.range T = np.linspace(*range, num=50) if not isinstance(T, pint.Quantity): T *= ureg.K if inverse_temperature: plt.xlabel("1/T (K$^{-1}$)") x = (1 / T)[::-1] y = prop.value(T)[::-1] else: plt.xlabel("T (K)") x = T y = prop.value(T) if auto_label and "label" not in kwargs.keys(): label = "{} {} ({})".format(prop.isotope, prop.author.capitalize(), prop.year) kwargs["label"] = label (l,) = plt.plot(x, y, **kwargs) if show_datapoints and prop.data_T is not None: if inverse_temperature: scat_x = (1 / prop.data_T)[::-1] scat_y = prop.data_y[::-1] else: scat_x = prop.data_T scat_y = prop.data_y if "alpha" not in scatter_kwargs: scatter_kwargs["alpha"] = l.get_alpha() plt.scatter(scat_x, scat_y, color=l.get_color(), **scatter_kwargs) return l
[docs]def get_prop_to_color( group: PropertiesGroup, colour_by: str, key_to_colour: dict = None, colour_cycle: list = None, ): """Returns a dictionary mapping Property objects to a colour based on a property attribute Args: group (PropertiesGroup): a group of properties colour_by (str): a property attribute to colour by (eg. "author", "isotope", "material") key_to_colour (dict, optional): a dictionary with keys (eg. material) corresponding to colours. Defaults to None colour_cycle (list, optional): a list of colours used if colour_by is key_to_colour is not given Returns: dict: a dictionary mapping properties to colours """ all_keys = list(set([getattr(prop, colour_by) for prop in group])) if not key_to_colour: # if key_to_colour not specified, use colour_cycle assert colour_cycle is not None key_to_colour = { key: colour_cycle[i % len(colour_cycle)] for i, key in enumerate(all_keys) } prop_to_colour = {prop: key_to_colour[getattr(prop, colour_by)] for prop in group} return prop_to_colour
[docs]def plot_plotly( group_of_properties: PropertiesGroup, T_bounds=(300, 1200), colour_by="property", show_datapoints=True, line_kwargs={}, scatter_kwargs={}, ): """Creates a plotly graph for visualising properties. Args: group_of_properties (list): a group of properties T_bounds (tuple, optional): If the property doesn't have a temperature range, this range will be used. Defaults to (300, 1200). colour_by (str, optional): "property", "material", "isotope", "author". Defaults to "property". show_datapoints (bool, optional): If True, the experimental datapoints will be scattered too. Defaults to True. line_kwargs (dict, optional): the keywords args for `line`. Defaults to {}. scatter_kwargs (dict, optional): the keywords args for `marker`. Defaults to {}. Returns: go.Figure: the graph """ import plotly.graph_objects as go if colour_by != "property": colour_cycle = [ "#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A", "#19D3F3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52", ] prop_to_color = get_prop_to_color( group_of_properties, colour_by, colour_cycle=colour_cycle ) fig = go.Figure() for prop in group_of_properties: if colour_by != "property": line_kwargs["color"] = prop_to_color[prop] _plot_property_plotly( prop, fig, line_kwargs, scatter_kwargs=scatter_kwargs, T_bounds=T_bounds, show_datapoints=show_datapoints, ) _update_axes(fig, group_of_properties) return fig
def _plot_property_plotly( prop, fig, line_kwargs, scatter_kwargs={}, T_bounds=(300, 1200), show_datapoints=True, ): """Adds a property line and points to a current plotly figure Args: prop (_type_): _description_ fig (_type_): _description_ line_kwargs (dict): _description_ scatter_kwargs (dict, optional): _description_ T_bounds (tuple, optional): If the property doesn't have a temperature range, this range will be used. Defaults to (300, 1200). show_datapoints (bool, optional): If True, the experimental datapoints will be scattered too. Defaults to True. """ import plotly.graph_objects as go label = f"{prop.isotope} {prop.author.capitalize()} ({prop.year})" if prop.range is None: range = T_bounds else: range = prop.range T = np.linspace(*range, num=500) if not isinstance(T, pint.Quantity): T *= ureg.K fig.add_trace( go.Scatter( x=1 / T.magnitude, y=prop.value(T).magnitude, name=label, mode="lines", line=line_kwargs, text=[label] * len(T), customdata=T.magnitude, hovertemplate=_make_hovertemplate(prop), ) ) if prop.data_T is not None and show_datapoints: scatter_kwargs["color"] = fig.data[-1].line.color fig.add_trace( go.Scatter( x=1 / prop.data_T.magnitude, y=prop.data_y.magnitude, name=label, mode="markers", marker=scatter_kwargs, ) ) def _make_hovertemplate(prop): # TODO refactor this if isinstance(prop, Solubility): return ( "<b>%{text}</b><br><br>" + prop.material.name + "<br>" + "1/T: %{x:,.2e} K<sup>-1</sup><br>" + "T: %{customdata:.0f} K<br>" + "S: %{y:,.2e} " + f"{prop.units:~H}<br>" + f"S_0: {prop.pre_exp:.2e~H} <br>" + f"E_S : {prop.act_energy:.2f~H}" + "<extra></extra>" ) elif isinstance(prop, Diffusivity): return ( "<b>%{text}</b><br><br>" + prop.material.name + "<br>" + "1/T: %{x:,.2e} K<sup>-1</sup><br>" + "T: %{customdata:.0f} K<br>" + "D: %{y:,.2e} " + f"{prop.units:~H} <br>" + f"D_0: {prop.pre_exp:.2e~H}<br>" + f"E_D : {prop.act_energy:.2f~H}" + "<extra></extra>" ) else: return ( "<b>%{text}</b><br><br>" + prop.material.name + "<br>" + "1/T: %{x:,.2e} K<sup>-1</sup><br>" + "T: %{customdata:.0f} K<br>" + "value: %{y:,.2e} " + f"{prop.units:~H} <br>" + f"pre-exp: {prop.pre_exp:.2e~H}<br>" + f"act. energy : {prop.act_energy:.2f~H}" + "<extra></extra>" ) def _update_axes(fig, group_of_properties): if len(group_of_properties) == 0: return if isinstance(group_of_properties[0], Solubility): all_units = np.unique([f"{S.units:~H}" for S in group_of_properties]).tolist() if len(all_units) == 1: yticks_suffix = all_units[0].replace("particle", " H") title_units = f"({yticks_suffix})" else: # if the group contains mixed units, display nothing title_units = "(mixed units)" yticks_suffix = "" ylabel = f"Solubility {title_units}" elif isinstance(group_of_properties[0], Diffusivity): ylabel = "Diffusivity" yticks_suffix = f" {group_of_properties[0].units:~H}" elif isinstance(group_of_properties[0], Permeability): ylabel = f"Permeability {group_of_properties[0].units:~H}" yticks_suffix = "" elif isinstance(group_of_properties[0], RecombinationCoeff): ylabel = "Recombination coefficient" yticks_suffix = " m<sup>4</sup>/s" elif isinstance(group_of_properties[0], DissociationCoeff): ylabel = f"Dissociation coefficient {group_of_properties[0].units:~H}" yticks_suffix = "" xticks_suffix = " K<sup>-1</sup>" fig.update_yaxes( title_text=ylabel, type="log", tickformat=".0e", ticksuffix=yticks_suffix ) fig.update_xaxes(title_text="1/T", tickformat=".2e", ticksuffix=xticks_suffix) def _label_from_colour_by(prop, colour_by): """Returns a label for a property based on a colour_by setting Args: prop (htm.Property): the property to label colour_by (str): a property attribute to colour by (eg. "author", "isotope", "material") Returns: str: the property label """ if colour_by == "property": return f"{prop.isotope} {prop.author.capitalize()} ({prop.year})" elif colour_by == "material": return f"{prop.material}" elif colour_by == "author": return f"{prop.author.capitalize()}" elif colour_by == "isotope": return f"{prop.isotope}"
[docs]def legend(**kwargs): """Adds a legend to an existing plot. Should be used in combination with htm.plotting.plot(PropertiesGroup) """ all_lines = plt.gca().get_lines() all_labels = [l.get_label() for l in all_lines] unique_labels = np.unique(all_labels) unique_lines = [all_lines[all_labels.index(label)] for label in unique_labels] plt.gca().legend(unique_lines, unique_labels, **kwargs)