Source code for h_transport_materials.plotting

import matplotlib.pyplot as plt
import numpy as np
from numpy.typing import ArrayLike
import pint
from h_transport_materials import Property, PropertiesGroup, ureg
import math
import matplotlib as mpl
from typing import Union


[docs]def plot( prop: Union[Property, PropertiesGroup], T_bounds=(300, 1200), inverse_temperature=True, auto_label=True, **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. kwargs: other matplotlib.pyplot.plot arguments Returns: matplotlib.lines.Line2D: the Line2D artist """ if isinstance(prop, Property): 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 return plt.plot(x, y, **kwargs) elif isinstance(prop, PropertiesGroup): if prop.units == "mixed units": raise ValueError("Cannot plot group with mixed units") for prop2 in prop: plot( prop2, T_bounds=T_bounds, inverse_temperature=inverse_temperature, auto_label=auto_label, **kwargs )
[docs]def line_labels( ax=None, min_label_distance: float or str = "auto", alpha: float = 1.0, **text_kwargs ): """Adapted from matplotx""" ax = ax or plt.gca() logy = ax.get_yscale() == "log" logx = ax.get_xscale() == "log" if min_label_distance == "auto": # Make sure that the distance is alpha * fontsize. This needs to be translated # into axes units. fig = plt.gcf() fig_height_inches = fig.get_size_inches()[1] ax = plt.gca() ax_pos = ax.get_position() ax_height = ax_pos.y1 - ax_pos.y0 ax_height_inches = ax_height * fig_height_inches ylim = ax.get_ylim() if logy: ax_height_ylim = math.log10(ylim[1]) - math.log10(ylim[0]) else: ax_height_ylim = ylim[1] - ylim[0] # 1 pt = 1/72 in fontsize = mpl.rcParams["font.size"] assert fontsize is not None min_label_distance_inches = fontsize / 72 * alpha min_label_distance = ( min_label_distance_inches / ax_height_inches * ax_height_ylim ) # find all Line2D objects with a valid label and valid data lines = [ child for child in ax.get_children() # https://stackoverflow.com/q/64358117/353337 if ( isinstance(child, mpl.lines.Line2D) and child.get_label()[0] != "_" and not np.all(np.isnan(child.get_ydata())) ) ] if len(lines) == 0: return # Add "legend" entries. # Get last non-nan y-value. targets, targets_x = [], [] for line in lines: ydata = line.get_ydata() xdata = line.get_xdata() targets.append(ydata[~np.isnan(ydata)][-1]) targets_x.append(xdata[~np.isnan(xdata)][-1]) if logy: targets = [math.log10(t) for t in targets] if logx: targets_x = [math.log10(t) for t in targets_x] # Sometimes, the max value if beyond ymax. It'd be cool if in this case we could put # the label above the graph (instead of the to the right), but for now let's just # cap the target y. ymax = ax.get_ylim()[1] targets = [min(target, ymax) for target in targets] targets = _move_min_distance(targets, min_label_distance) if logy: targets = [10**t for t in targets] # if logx: # targets_x = [10**t for t in targets_x] labels = [line.get_label() for line in lines] colors = [line.get_color() for line in lines] # Leave the labels some space to breathe. If they are too close to the # lines, they can get visually merged. # <https://twitter.com/EdwardTufte/status/1416035189843714050> # Don't forget to transform to axis coordinates first. This makes sure the # https://stackoverflow.com/a/40475221/353337 # axis_to_data = ax.transAxes + ax.transData.inverted() # xpos = axis_to_data.transform([1.03, 1.0])[0] for label, xpos, ypos, color in zip(labels, targets_x, targets, colors): plt.text( xpos, ypos, label, verticalalignment="center", color=color, **text_kwargs )
def _move_min_distance(targets: ArrayLike, min_distance: float) -> np.ndarray: """Move the targets such that they are close to their original positions, but keep min_distance apart. https://math.stackexchange.com/a/3705240/36678 """ # sort targets idx = np.argsort(targets) targets = np.sort(targets) n = len(targets) x0_min = targets[0] - n * min_distance A = np.tril(np.ones([n, n])) b = targets - (x0_min + np.arange(n) * min_distance) # import scipy.optimize # out, _ = scipy.optimize.nnls(A, b) out = nnls(A, b) sol = np.cumsum(out) + x0_min + np.arange(n) * min_distance # reorder idx2 = np.argsort(idx) return sol[idx2] def nnls(A, b, eps: float = 1.0e-10, max_steps: int = 100): # non-negative least-squares after # <https://en.wikipedia.org/wiki/Non-negative_least_squares> A = np.asarray(A) b = np.asarray(b) AtA = A.T @ A Atb = A.T @ b m, n = A.shape assert m == b.shape[0] mask = np.zeros(n, dtype=bool) x = np.zeros(n) w = Atb s = np.zeros(n) k = 0 while sum(mask) != n and max(w) > eps: if k >= max_steps: break mask[np.argmax(w)] = True s[mask] = np.linalg.lstsq(AtA[mask][:, mask], Atb[mask], rcond=None)[0] s[~mask] = 0.0 while np.min(s[mask]) <= 0: alpha = np.min(x[mask] / (x[mask] - s[mask])) x += alpha * (s - x) mask[np.abs(x) < eps] = False s[mask] = np.linalg.lstsq(AtA[mask][:, mask], Atb[mask], rcond=None)[0] s[~mask] = 0.0 x = s.copy() w = Atb - AtA @ x k += 1 return x