Source code for rotsim2d.visual.functions

"""Visualization procedures."""
# * Imports
from typing import Optional, Tuple, Dict, Sequence, List, Mapping, Callable, Union
from copy import deepcopy
import re
from pathlib import Path
import subprocess as subp
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.gridspec as grd
import matplotlib.cm as cm
from rotsim2d.utils import find_index
import rotsim2d.dressedleaf as dl
import rotsim2d.symbolic.functions as sym
from rotsim2d.visual.latexprinter import latex
import molspecutils.molecule as mol

# * Matplotlib visualization
[docs]def make_extent(t1s, t2s, scale=1.0): """Return extents for imshow or contour(f). Parameters ---------- t1s: ndarray Time/frequency vertical axis (pump, w1). t2s: ndarray Time/frequency horizontal axis (probe, w3). Returns ------- list of float """ t1s = t1s*scale t2s = t2s*scale dt1 = (t1s[1]-t1s[0])/2 dt2 = (t2s[1]-t2s[0])/2 return [t2s[0]-dt2, t2s[-1]+dt2, t1s[0]-dt1, t1s[-1]+dt1]
[docs]def plot2d_im(freqs, spec2d, spec_linear=None, scale='symlog', line=True, pthresh=100.0, absmax=None, fig_kwargs={}): """2D imshow plot with decent defaults.""" # pylint: disable=too-many-locals,too-many-arguments,dangerous-default-value extent = make_extent(freqs[0], freqs[1]) cmap = cm.get_cmap('RdBu').reversed() fig = plt.figure(**fig_kwargs) if spec_linear is not None: gs = grd.GridSpec(nrows=2, ncols=2, height_ratios=[1, 6], width_ratios=[20, 1], figure=fig) ax1d = plt.subplot(gs[0, 0]) ax2d = plt.subplot(gs[1, 0]) axcbar = plt.subplot(gs[1, 1]) else: gs = grd.GridSpec(nrows=1, ncols=2, width_ratios=[20, 1], figure=fig) ax2d = plt.subplot(gs[0]) axcbar = plt.subplot(gs[1]) if absmax is None: absmax = np.max(np.abs(spec2d)) linthresh = absmax/pthresh if scale == 'symlog': cset = ax2d.imshow(spec2d, cmap=cmap, aspect='auto', extent=extent, clim=(-absmax, absmax), origin='lower', norm=colors.SymLogNorm(linthresh=linthresh, vmax=absmax, vmin=-absmax)) elif scale == 'linear': cset = ax2d.imshow(spec2d, cmap=cmap, aspect='auto', extent=extent, clim=(-absmax, absmax), origin='lower') ax2d.set(xlabel=r'Probe (cm$^{-1}$)', ylabel=r'Pump (cm$^{-1}$)') if line: p = (ax2d.get_xlim()[1]+ax2d.get_xlim()[0])/2 ax2d.axline((p, p), slope=1.0, color='gray', alpha=0.5, zorder=1) axcbar = fig.colorbar(cset, ax=ax2d, cax=axcbar) if spec_linear is not None: ax1d.plot(spec_linear[0], spec_linear[1]) ax1d.set( xlim=(spec_linear[0][0], spec_linear[0][-1]), xticklabels=[], ) fig.set_constrained_layout_pads(wspace=0.01, hspace=0.01, h_pad=0.01, w_pad=0.01) if spec_linear is not None: return {'ax2d': ax2d, 'axcbar': axcbar, 'ax1d': ax1d, 'fig': fig} else: return {'ax2d': ax2d, 'axcbar': axcbar, 'ax1d': None, 'fig': fig}
def plot1d_animation(coords: Tuple[np.ndarray, np.ndarray], spec2d: np.ndarray, ylim: Optional[Tuple[float, float]]=None, subplots_kwargs: Mapping={}, update_title: Callable=None, update_data_limits: bool=False): """Prepare Figure and callbacks for `FuncAnimation` for 1D spectra. The returned objects can be used as:: >> fanim = FuncAnimation(fig, update, frames, init, blit=True) Parameters ---------- freqs : tuple of ndarray Array of animation coordinates and array of frequencies. spec2d : ndarray 2D array of data to plot, first dimension is the animated coordinate. ylim : tuple of float Y limits, taken from data otherwise. fig_kwargs : dict Keyword arguments for `plt.figure`. update_title : Callable Function called to update the title. update_data_limits : bool Recalculate data limits (`absmax`) for each frame. Returns ------- fig : matplotlib.figure.Figure init : function Initialization closure function for FuncAnimation. update : function Update closure function for FuncAnimation. frames : ndarray NumPy array of frame numbers. """ fig, ax = plt.subplots(**subplots_kwargs) line, = ax.plot([], []) title = ax.text(0.5, 0.99, "", ha="center", va="top", transform=ax.transAxes) ax.set_xlim(np.min(coords[1]), np.max(coords[1])) if ylim: ymin, ymax = ylim else: ymin, ymax = np.min(spec2d), np.max(spec2d) ax.set_ylim(ymin, ymax) fig.set_constrained_layout_pads(wspace=0.01, hspace=0.01, h_pad=0.01, w_pad=0.01) def init(): return [line, title] def update(i): line.set_data(coords[1], spec2d[i]) if update_title is not None: update_title(title, i) if update_data_limits: ymin, ymax = np.min(spec2d[i]), np.max(spec2d[i]) ax.set_ylim(ymin, ymax) return [line, title] return fig, init, update, np.arange(len(coords[0])) def plot2d_animation(freqs: Tuple[np.ndarray, ...], spec3d: np.ndarray, absmax: Optional[float]=None, fig_kwargs: Mapping={}, update_title: Callable=None, update_data_limits: bool=False): """Prepare Figure and callbacks for `FuncAnimation` for 2D spectra. The returned objects can be used as:: >> fanim = FuncAnimation(fig, update, frames, init, blit=True) Parameters ---------- freqs : tuple of ndarray Array of pump frequencies, array of waiting times and array of probe frequencies. spec3d : ndarray 3D array of data to plot, second dimension is the animated time. absmax : float Data limits for colorbar, taken from data otherwise. fig_kwargs : dict Keyword arguments for `plt.figure`. update_title : Callable Function called to update the title. update_data_limits : bool Recalculate data limits (`absmax`) for each frame. Returns ------- fig : matplotlib.figure.Figure init : function Initialization closure function for FuncAnimation. update : function Update closure function for FuncAnimation. frames : ndarray NumPy array of frame numbers. """ extent = make_extent(freqs[0], freqs[2]) ts2 = freqs[1] cmap = cm.get_cmap('RdBu').reversed() fig = plt.figure(**fig_kwargs) gs = fig.add_gridspec(nrows=1, ncols=2, width_ratios=[20, 1], figure=fig) ax2d = fig.add_subplot(gs[0]) axcbar = fig.add_subplot(gs[1]) if absmax is None: absmax = np.max(np.abs(spec3d)) cset = ax2d.imshow(np.zeros((spec3d.shape[0], spec3d.shape[2])), cmap=cmap, aspect='auto', extent=extent, clim=(-absmax, absmax), origin='lower') ax2d.set(xlabel=r'Probe (cm$^{-1}$)', ylabel=r'Pump (cm$^{-1}$)') title = ax2d.text(0.5, 0.99, "title", ha='center', va='top', transform=ax2d.transAxes) axcbar = fig.colorbar(cset, ax=ax2d, cax=axcbar) fig.set_constrained_layout_pads(wspace=0.01, hspace=0.01, h_pad=0.01, w_pad=0.01) def init(): return [cset, title] def update(i): cset.set_data(spec3d[:, i, :]) if update_title is not None: update_title(title, i) if update_data_limits: absmax = np.max(np.abs(spec3d[:, i, :])) cset.set_clim(-absmax, absmax) return [cset, title] return fig, init, update, np.arange(spec3d.shape[1])
[docs]def plot2d_scatter(pl, fig_dict=None, line=True, vminmax=None, fig_kwargs={}, scatter_kwargs={}): # pylint: disable=dangerous-default-value,too-many-arguments if fig_dict: fig = fig_dict['fig'] ax = fig_dict['ax'] axcbar = fig_dict['axcbar'] else: fig = plt.figure(**fig_kwargs) gs = grd.GridSpec(1, 2, width_ratios=[20, 1], figure=fig) ax = plt.subplot(gs[0]) axcbar = plt.subplot(gs[1]) if vminmax: vmin = -np.abs(vminmax) else: vmin = -np.max(np.abs(pl.intensities)) skwargs = dict(s=10.0, cmap='seismic') skwargs.update(scatter_kwargs) sc = ax.scatter(pl.probes, pl.pumps, c=pl.intensities, vmin=vmin, vmax=-vmin, **skwargs) if not fig_dict: fig.colorbar(sc, ax=ax, cax=axcbar) if line: ax.axline((pl.probes[0], pl.probes[0]), slope=1.0, color='gray', alpha=0.5, zorder=0) fig.set_constrained_layout_pads(wspace=0.01, hspace=0.01, h_pad=0.01, w_pad=0.01) return {'ax': ax, 'axcbar': axcbar, 'fig': fig, 'sc': sc}
def plot1d_probe(pump_wn, freqs, spec2d, fig_dict=None, **plot_kwargs): """Plot probe spectrum at `pump_wn`.""" ipump = find_index(freqs[0], pump_wn) if fig_dict is None: fig, ax = plt.subplots() else: fig, ax = fig_dict['fig'], fig_dict['ax'] ax.plot(freqs[1], spec2d[ipump], **plot_kwargs) fig.set_constrained_layout_pads(wspace=0.01, hspace=0.01, h_pad=0.01, w_pad=0.01) return dict(fig=fig, ax=ax) # * LaTeX double-sided Feynmann diagrams LATEX_PRE = r"""\documentclass[tikz,border=3.14mm]{standalone} \usetikzlibrary{matrix,fit,positioning} \newcommand{\textoverline}[1]{$\overline{\mbox{#1}}$} \medmuskip=0mu \tikzset{ mymat/.style={ matrix of math nodes, left delimiter=|,right delimiter=|, align=center, column sep=-\pgflinewidth }, mymats/.style={ mymat, nodes={draw,fill=#1} } } \begin{document} \begin{tikzpicture}[font=\sffamily, every left delimiter/.style={xshift=.4em}, every right delimiter/.style={xshift=-.4em}] """ LATEX_POST = r"""\end{tikzpicture} \end{document}""" dj_to_letter = {-2: "O", -1: "P", 0: "Q", 1: "R", 2: "S"} def tikz_abstract_format(dnu: int, dj: int): if dnu==0 and dj==0: return "0" return str(dnu)+dj_to_letter[dj] def trans_label2latex(label: str) -> str: """Write ``label`` with overline instead of parentheses.""" return re.sub(r'\(([A-Z0-9]{1,2})\)', r'\\ensuremath{{}\\mkern1mu\\overline{\\mkern-1mu\\mbox{\1}}}', label) def R_label2latex(label: str) -> str: lat_label = r'\ensuremath{\mbox{R}_' + label[1] if len(label) == 3: lat_label += r'^\ast' return lat_label + '}' def tikz_diagram(leaf, index=1, first=True, direction=False, trans_label=None, R_label=False, abstract=None, hspace="2cm") -> str: """Return TikZ double-sided Feynmann diagram. With `abstract` ketbras are labeled relative to some (nu, j) ground state. Parameters ---------- leaf : rotsim2d.pathways.KetBra KetBra leaf instance to render. index : str, optional Internal Tikz index of the current diagram. first : bool, optional First diagram in the document. direction : bool, optional Add phase-matching direction label. trans_label : {'proper', 'degenerate'}, optional Add transition label, either unambiguous or degenerate one. R_label: bool, optional Add R_i, i=1,...,8, label. abstract : tuple of int, optional Tuple of ground state quantum numbers, (nu, j). hspace : str Horizontal space between pathways in LaTeX dimensions. """ diag_rows = [] for i, kb in enumerate(leaf.ketbras()): if not abstract: diag_rows.append(r"$|{:d},{:d}\rangle\langle {:d},{:d}|$\\".format(kb.ket.nu, kb.ket.j, kb.bra.nu, kb.bra.j)) else: asl = dl.abstract_state_label gstate = mol.DiatomState(*abstract) if i==0: diag_rows.append(r"$|0\rangle\langle 0|$\\") else: diag_rows.append(r"$|{:s}\rangle\langle {:s}|$\\".format( asl(kb.ket, gstate), asl(kb.bra, gstate))) diag_rows.reverse() diag_arrows = [] ints = leaf.interactions() nints = len(ints) for i, li in enumerate(ints): if li.side == -1: # label_side = "right" arr_side = "east" xshift = "4mm" else: # label_side = "left" arr_side = "west" xshift = "-4mm" if li.side == -1 and li.sign == -1: arr = "stealth-" yshift = "-4mm" elif li.side == -1 and li.sign == 1: arr = "-stealth" yshift = "4mm" elif li.side == 1 and li.sign == 1: arr = "stealth-" yshift = "-4mm" elif li.side == 1 and li.sign == -1: arr = "-stealth" yshift = "4mm" if li.readout: arr = arr + ",dashed" # if li.readout: # klabel = r"$\vec{\mu}$" # else: # klabel = r"$\vec{{k}}_{:d}$".format(i+1) arrow = r"\draw[{arr:s}] (mat{index:d}-{ri:d}-1.south -| mat{index:d}.{arr_side:s}) -- ++({xshift:s},{yshift:s});".format(arr=arr, ri=nints-i, arr_side=arr_side, xshift=xshift, yshift=yshift, index=index) diag_arrows.append(arrow) notes = [] if direction: if leaf.is_SI(): notes.append(r"$S_{\mathrm{I}}$") elif leaf.is_SII(): notes.append(r"$S_{\mathrm{II}}$") elif leaf.is_SIII(): notes.append(r"$S_{\mathrm{III}}$") if trans_label: if trans_label == "proper": notes.append(trans_label2latex(dl.Pathway(leaf).trans_label)) elif trans_label == "degenerate": notes.append(dl.Pathway(leaf).trans_label_deg) if R_label: notes.append(R_label2latex(leaf.R_label())) if notes: notes = ' {' + ', '.join(notes) + '};' note = r"\node[below] (mat{index:d}label) at (mat{index:d}.south)".format(index=index) +\ notes else: note = '' if first: header = r"""\matrix[mymat] at (0,0) (mat{index:d})""".format(index=index) else: header = r"""\matrix[mymat,right={hspace:s} of mat{prev_index:d}] (mat{index:d})""".format( prev_index=index-1, index=index, hspace=hspace) ret = header + """ {{ {rows:s} }}; {arrows:s} {note:s} """.format(rows="\n".join(diag_rows), arrows="\n".join(diag_arrows), note=note) return ret
[docs]def tikz_diagrams(tree, direction=False, trans_label=None, R_label=False, abstract=None, hspace="2cm"): """Return tikz code for series of double-sided Feynmann diagrams. Parameters ---------- tree : rotsim2d.pathways.KetBra Root of KetBra excitation tree. Generate pathway diagrams for all leaves. direction : bool, optional Add phase-matching direction label. trans_label : {'proper', 'degenerate'}, optional Add transition label, either unambiguous or degenerate one. R_label: bool, optional Add R_i, i=1,...,8, label. abstract : tuple of int, optional Tuple of ground state quantum numbers, (nu, j). hspace : str Horizontal space between pathways in LaTeX dimensions. Returns ------- str Tikz code to for diagrams. Needs to be placed in a LaTeX document to compile. Examples -------- Create KetBras, filter and render diagrams: >>> import rotsim2d.pathways as pw >>> import rotsim2d.visual as vis >>> kbs = pw.gen_pathways([5], meths=[pw.only_SI], rotor='linear') >>> vis.latex_compile('diagrams.tex', vis.latex_document( vis.tikz_diagrams(kbs, abstract=(0, 5)))) """ leaves = tree.leaves diagrams = [tikz_diagram(leaves[0], direction=direction, trans_label=trans_label, R_label=R_label, abstract=abstract)] for i, l in enumerate(leaves[1:], start=2): diagrams.append(tikz_diagram(l, index=i, first=False, direction=direction, trans_label=trans_label, R_label=R_label, abstract=abstract, hspace=hspace)) return "\n".join(diagrams)
[docs]def latex_document(fragment: str) -> str: """Wrap provided fragment in a LaTeX document.""" return LATEX_PRE + fragment + LATEX_POST
[docs]def latex_compile(path: Union[str, Path], doc: str): """Save `doc` to `path` and compile it.""" doc_path = Path(path) doc_dir = doc_path.parent doc_dir.mkdir(parents=True, exist_ok=True) doc_path.write_text(doc) subp.run(['pdflatex', doc_path.name], cwd=doc_dir, check=True)
# * LaTeX table of coefficients def classified_table_prep(classified: Dict[sym.RFactor, dl.Pathway], highj: bool=False) -> List[List]: """Convert dict of R-factors and pathways to a table of coefficients. Returns a list of rows. First element of a row is a list of `trans_label`s, remaining elements of a row are `c` coefficients. """ def labels(l: Sequence[dl.Pathway], highj: bool=False): l1 = [pw.trans_label for pw in l] if highj: l1 = list(set([re.sub(r'[)(0-9]', '', x) for x in l1])) l1.sort() l1 = [r'\textbf{{{:s}}}'.format(v) for v in l1] else: l1.sort(key=lambda x: re.sub(r'[)(0-9]', '', x)) l1 = [trans_label2latex(v) for v in l1] return l1 # represent pathways with transition labels classified = {k: labels(v, highj) for k, v in classified.items()} table = [] for k, v in classified.items(): if highj: table.append([v] + list(k.tuple[1:])) else: table.append([v] + list(k.tuple)) return table def merge_tables(dtable: Mapping[str, List[List]]) -> List[List]: """Merge different tables of coefficients into one. Duplicate `trans_label`s are distinguished by subscripted `dtable` keys. """ dtable = deepcopy(dtable) table = [] keys = list(dtable.keys()) # uniquify labels uniquify = set() for row in dtable[keys[0]]: labels, coeffs = row[0], row[1:] for il in range(len(labels)): # loop over other tables, rows, find labels to uniquify for fkey in keys[1:]: for frow in dtable[fkey]: flabels, fcoeffs = frow[0], frow[1:] for ifl in range(len(flabels)): if labels[il] == flabels[ifl] and coeffs != fcoeffs: uniquify.add(labels[il]) # actually uniquify the labels for label in uniquify: for key in keys: for row in dtable[key]: labels = row[0] for il in range(len(labels)): if labels[il] == label: row[0][il] = labels[il]+r'\textsubscript{{{:s}}}'.format(key) # now that labels are either unique or mean the same thing, merge the tables while len(dtable[keys[0]]): row = dtable[keys[0]].pop() labels, coeffs = set(row[0]), row[1:] for fkey in keys[1:]: for ifrow in range(len(dtable[fkey])): if coeffs == dtable[fkey][ifrow][1:]: frow = dtable[fkey].pop(ifrow) labels.update(frow[0]) break labels = list(labels) labels.sort() table.append([labels] + coeffs) for key in keys: assert len(dtable[key]) == 0 return table def classified_table_render(table: List[List]) -> str: """Render table of coefficients as LaTeX table body.""" str_table = [' & '.join([', '.join(x[0])] + [latex(xx, mode='inline') for xx in x[1:]]) for x in table] str_table.sort() return '\\\\\n'.join(str_table) def classified_table(classified: Dict[sym.RFactor, dl.Pathway], highj: bool=False) -> str: """Format dict of R-factors and pathways as a table of coefficients. Uses custom LaTeX printer from `.latexprinter` module. With `highj` True don't print the first coefficient. """ table = classified_table_prep(classified, highj) return classified_table_render(table)