Source code for fret_tester.plot

"""Utilities for plotting results of the simulations"""
# Copyright 2017-2018 Lukas Schrangl
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
import math

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt


class _PlotterBase:
    """Base class implementing common plotting functionality"""
    truth_style = ":"
    """Line style for plotting the true values"""

    truth_color = "C1"
    """Line color for plotting  the true values"""

    scale = "log"
    """Axes scale"""

    cbar_width = 0.03
    """Width of the colorbar as a fraction of 1.0"""

    tick_formatter = None
    """Tick formatter"""

    def __init__(self, **kwargs):
        """Parameters
        ----------
        **kwargs
            Set any of the class attributes (:py:attr:`truth_style`, …)
        """
        self.time_unit = None

        for k, v in kwargs.items():
            setattr(self, k, v)

        if self.tick_formatter is None:
            if self.scale == "log":
                self.tick_formatter = mpl.ticker.LogFormatter()
            else:
                self.tick_formatter = mpl.ticker.ScalarFormatter()

    @property
    def time_unit(self):
        """String describing the time unit

        This is used in axis labels.
        """
        return self._time_unit

    @time_unit.setter
    def time_unit(self, t):
        self._time_unit = t
        self._time_init_label = " [{}]".format(t) if t else ""

    def make_axes(self, nrows, n, ax_or_subspec, fig, colorbar="on"):
        """Create matplotlib Axes objects for plotting

        Parameters
        ----------
        nrows : int
            Number of rows. Only has an effect if `ax_or_subspec` is a
            :py:class:`SubplotSpec` instance.
        n : int
            Number plotss. Only has an effect if `ax_or_subspec` is a
            :py:class:`SubplotSpec` instance.
        ax_or_subspec : list of matplotlib Axes or matplotlib SupblotSpec or None
            If this is a :py:class:`SubplotSpec` instance, create new Axes on
            a grid in `ax_or_subspec`. If this is a list of Axes, just use
            those. If `None`, create a :py:class:`SubplotSpec` that fills the
            whole figure.
        fig : matplotlib.figure.Figure
            Figure to create the Axes on. Only applicable if `ax_or_subspec`
            is a :py:class:`SubplotSpec` instance.
        colorbar : {"on", "empty", "off"}, optional
            If "on" or "empty", create addtional Axes (of :py:attr:`cbar_width`
            width) intended for a colorbar (if `ax_or_subspec` is a
            :py:class:`SubplotSpec` instance) or use the last element of
            `ax_or_subspec` as colorbar Axes (if it is a list of Axes).
            Defaults to "on".

        Returns
        -------
        ax : list of matplotlib.axes.Axes
            Axes for plots
        cbar_ax : matplotlib.axes.Axes or None
            Axes for colorbar (if `colorbar` is "on" or "empty") or None
            (if `colorbar` is "off").
        """
        ncols = math.ceil(n / nrows)

        if ax_or_subspec is None:
            ax_or_subspec = mpl.gridspec.GridSpec(1, 1, hspace=0)[0]

        if colorbar in ("on", "empty"):
            col_width_ratios = (((1 - self.cbar_width) / ncols,) * ncols +
                                (self.cbar_width,))
            n_tot = n + nrows - 1
            if isinstance(ax_or_subspec, mpl.gridspec.SubplotSpec):
                grid = mpl.gridspec.GridSpecFromSubplotSpec(
                    nrows, ncols+1, ax_or_subspec,
                    width_ratios=col_width_ratios)
                ax = [fig.add_subplot(grid[i])
                      for i in range(n_tot) if i % (ncols + 1) != ncols]
                cbar_ax = (fig.add_subplot(grid[:, -1]) if colorbar == "on"
                           else None)
            else:
                if len(ax_or_subspec) != n + 1:
                    raise ValueError(
                        "`ax_or_subspec` should contain {} entries".format(
                            n + 1))
                ax = ax_or_subspec[:-1]
                cbar_ax = ax_or_subspec[-1]
            return ax, cbar_ax
        if colorbar == "off":
            if isinstance(ax_or_subspec, mpl.gridspec.GridSpecBase):
                grid = mpl.gridspec.GridSpecFromSubplotSpec(
                    nrows, ncols, ax_or_subspec)
                ax = [fig.add_subplot(grid[i]) for i in range(n)]
            else:
                if len(ax_or_subspec) != n:
                    raise ValueError(
                        "`ax_or_subspec` should contain {} entries".format(n))
                ax = ax_or_subspec
            return ax, None
        raise ValueError("colorbar must be in ('on', 'off', 'empty').")


[docs]class Plotter1D(_PlotterBase): """Class for producing 1D plots These are typically plots of lifetimes vs. p-values with fixed ratio between the two lifetimes. """ pval_scale = "log" """Scale for the p-value axis""" significance = 1e-2 """Significance threshold""" pval_range = (1e-3, 1.) """p-value axis range""" significance_opts = dict(color="gray", alpha=0.4, ls="None", lw=0.001) """Options for plotting the region of significance""" def __init__(self, **kwargs): """Parameters ---------- **kwargs Set any of the class attributes (:py:attr:`truth_style`, …) """ super().__init__(**kwargs) if not hasattr(self, "pval_tick_formatter"): if self.pval_scale == "log": self.pval_tick_formatter = mpl.ticker.LogFormatterSciNotation() else: self.pval_tick_formatter = mpl.ticker.ScalarFormatter()
[docs] def plot(self, test_times, p_vals, truth=None, ax=None): """Draw a single 1D plot This is typically for plots of lifetimes vs. p-values with fixed ratio between the two lifetimes. Parameters ---------- test_times : list of lists of float test_times[0] specifies the times the first lifetime :math:`\tau_1` was tested against. test_times[1] specify the times the second lifetime :math:`\tau_2` was tested against. p_vals : list of float p-values corresponding to `test_times` truth : tuple of float or None, optional If not `None`, draw lines at these time coordinates to mark the true lifetime combination (if known). Defaults to `None`. ax : matplotlib.axes.Axes or None: Axes to draw on. Use ``matplotlib.pyplot.gca()`` if `None`. Defaults to `None`. Returns ------- ax : matplotlib.axes.Axes Axes for test_times[1] axt : matplotlib.axes.Axes Axes for test_times[0] """ if ax is None: ax = plt.gca() if self.significance > min(self.pval_range): t = mpl.transforms.blended_transform_factory(ax.transAxes, ax.transData) r = mpl.patches.Rectangle((0, min(self.pval_range)), 1, self.significance - min(self.pval_range), transform=t, **self.significance_opts) ax.add_patch(r) axt = ax.twiny() axt.plot(test_times[0], p_vals) axt.set_ylim(*self.pval_range) ax.set_xlim(np.min(test_times[1]), np.max(test_times[1])) axt.set_xlim(np.min(test_times[0]), np.max(test_times[0])) axt.set_xscale(self.scale) ax.set_xscale(self.scale) ax.set_yscale(self.pval_scale) if truth is not None: t = mpl.transforms.blended_transform_factory(axt.transData, axt.transAxes) li = mpl.lines.Line2D([truth[0], truth[0]], [0, 1], transform=t, color=self.truth_color, linestyle=self.truth_style) axt.add_line(li) for a in (ax.xaxis, axt.xaxis): a.set_major_formatter(self.tick_formatter) a.set_minor_formatter(self.tick_formatter) ax.yaxis.set_major_formatter(self.pval_tick_formatter) ax.yaxis.set_minor_formatter(self.pval_tick_formatter) axt.set_xlabel("$\\tau_1$" + self._time_init_label) ax.set_xlabel("$\\tau_2$" + self._time_init_label) ax.set_ylabel("$p$-value") return ax, axt
[docs] def plot_series(self, test_times, p_vals, truths=None, ax_or_subspec=None, fig=None, nrows=1, empty_cbar=False): """Draw a series of 1D plots Use :py:meth:`plot` to draw a series of plots on a grid. Parameters ---------- test_times : list Each entry will be passed to :py:meth:`plot` as `test_times` parameter. p_vals : list Each entry will be passed to :py:meth:`plot` as `p_vals` parameter. truths : list or None, optional Each entry will be passed to :py:meth:`plot` as `truth` parameter. Defaults to None. ax_or_subspec : list of matplotlib Axes or matplotlib SupblotSpec or None If this is a :py:class:`SubplotSpec` instance, create new Axes on a grid in `ax_or_subspec`. If this is a list of Axes, just use those. If `None`, create a :py:class:`SubplotSpec` that fills the whole figure. fig : matplotlib.figure.Figure or None Only effective if `ax_or_subspec` is a :py:class:`SubplotSpec` instance. If `None`, use ``matplotlib.pyplot.gcf()``. Defaults to `None`. nrows : int, optional Number of rows. Defaults to 1. empty_cbar : bool, optional Whether to allocate an (emtpy) grid entry for a colorbar. This is useful if the plot series is paired with a 2D series (with a color bar) in the same figure. Defaults to False. Returns ------- ax : list of matplotlib.axes.Axes Axes for ``tt[1]``, where ``tt`` stands for each entry of `test_times`. axt : list of matplotlib.axes.Axes Axes for ``tt[0]``, where ``tt`` stands for each entry of `test_times`. """ n = len(test_times) ncols = math.ceil(n / nrows) if truths is None: truths = [None] * n empty_cbar = "empty" if empty_cbar else "off" if fig is None: fig = plt.gcf() ax, _ = self.make_axes(nrows, n, ax_or_subspec, fig, empty_cbar) axt = [] for a, tt, p, tr in zip(ax, test_times, p_vals, truths): _, tw = self.plot(tt, p, tr, a) axt.append(tw) for i, a in enumerate(ax): if i % ncols == ncols - 1: a.tick_params("y", which="both", labelright=True, right=True, labelleft=False, left=False) a.yaxis.set_label_position("right") else: a.tick_params("y", which="both", labelright=False, right=True, labelleft=False, left=False) a.yaxis.label.set_visible(False) for a in axt[ncols:]: a.xaxis.label.set_visible(False) a.tick_params("x", which="both", labeltop=False, labelbottom=False) for a in ax[:ncols*(nrows-1)]: a.xaxis.label.set_visible(False) a.tick_params("x", which="both", labeltop=False, labelbottom=False) return ax, axt
[docs]class Plotter2D(_PlotterBase): """Class for producing 2D plots These are typically plots of where the two lifetimes were tested independently, producing a 2D plot with the p-values color-coded. """ norm = mpl.colors.LogNorm() """p-value color map normalization. Logarithmic by default.""" pval_range = (1e-2, 1.) """p-value range""" cmap = "binary" """p-value color map""" def __init__(self, **kwargs): super().__init__(**kwargs)
[docs] def plot(self, test_times, p_vals, truth=None, ax=None): """Draw a single 2D plot These is typically for plots of where the two lifetimes were tested independently, producing a 2D plot with the p-values color-coded. Parameters ---------- test_times : list of lists of float test_times[0] specifies the times the first lifetime :math:`\tau_1` was tested against. test_times[1] specify the times the second lifetime :math:`\tau_2` was tested against. p_vals : array_like, dtype(float), shape(len(test_times[0], len(test_times[1])) p-values corresponding to `test_times` truth : tuple of float or None, optional If not `None`, draw lines at these time coordinates to mark the true lifetime combination (if known). Defaults to `None`. ax : matplotlib.axes.Axes or None: Axes to draw on. Use ``matplotlib.pyplot.gca()`` if `None`. Defaults to `None`. Returns ------- ax : matplotlib.axes.Axes Axes for test_times[1] axt : matplotlib.axes.Axes Axes for test_times[0] """ if ax is None: ax = plt.gca() m = ax.pcolormesh(test_times[0], test_times[1], p_vals.T, cmap=self.cmap, norm=self.norm, vmin=self.pval_range[0], vmax=self.pval_range[1]) # ax.contour(test_times[0], test_times[1], p_vals.T, [1e-6, 1e-3], # linewidths=0.1, colors="k", linestyles=[":", "--"]) if truth is not None: t1 = mpl.transforms.blended_transform_factory(ax.transData, ax.transAxes) l1 = mpl.lines.Line2D([truth[0], truth[0]], [0, 1], transform=t1, color=self.truth_color, linestyle=self.truth_style) ax.add_line(l1) t2 = mpl.transforms.blended_transform_factory(ax.transAxes, ax.transData) l2 = mpl.lines.Line2D([0, 1], [truth[1], truth[1]], transform=t2, color=self.truth_color, linestyle=self.truth_style) ax.add_line(l2) ax.set_xscale(self.scale) ax.set_yscale(self.scale) for a in (ax.xaxis, ax.yaxis): a.set_major_formatter(self.tick_formatter) a.set_minor_formatter(self.tick_formatter) ax.set_xlabel("$\\tau_1$" + self._time_init_label) ax.set_ylabel("$\\tau_2$" + self._time_init_label) return m
[docs] def plot_series(self, test_times, p_vals, truths=None, ax_or_subspec=None, fig=None, nrows=1): """Draw a series of 2D plots Use :py:meth:`plot` to draw a series of plots on a grid. Parameters ---------- test_times : list Each entry will be passed to :py:meth:`plot` as `test_times` parameter. p_vals : list Each entry will be passed to :py:meth:`plot` as `p_vals` parameter. truths : list or None, optional Each entry will be passed to :py:meth:`plot` as `truth` parameter. Defaults to None. ax_or_subspec : list of matplotlib Axes or matplotlib SupblotSpec or None If this is a :py:class:`SubplotSpec` instance, create new Axes on a grid in `ax_or_subspec`. If this is a list of Axes, just use those. If `None`, create a :py:class:`SubplotSpec` that fills the whole figure. fig : matplotlib.figure.Figure or None Only effective if `ax_or_subspec` is a :py:class:`SubplotSpec` instance. If `None`, use ``matplotlib.pyplot.gcf()``. Defaults to `None`. nrows : int, optional Number of rows. Defaults to 1. Returns ------- ax : list of matplotlib.axes.Axes Axes for p-value plots. colorbar_ax : matplotlib.axes.Axes Axes for colorbar. fig : matplotlib.figure.Figure or None Only effective if `ax_or_subspec` is a :py:class:`SubplotSpec` instance. If `None`, use ``matplotlib.pyplot.gcf()``. Defaults to `None`. nrows : int, optional Number of rows. Defaults to 1. Returns ------- ax : list of matplotlib.axes.Axes Axes for p-value plots. colorbar_ax : matplotlib.axes.Axes Axes for colorbar. """ n = len(test_times) ncols = math.ceil(n / nrows) if truths is None: truths = [None] * n if fig is None: fig = plt.gcf() ax, colorbar_ax = self.make_axes(nrows, n, ax_or_subspec, fig, "on") for a, tt, p, tr in zip(ax, test_times, p_vals, truths): m = self.plot(tt, p, tr, a) for a in (a2 for i, a2 in enumerate(ax) if i % ncols != 0): a.tick_params("y", which="both", labelright=False, labelleft=False) a.yaxis.label.set_visible(False) for a in ax[:ncols*(nrows-1)]: a.xaxis.label.set_visible(False) a.tick_params("x", which="both", labeltop=False, labelbottom=False) fig.colorbar(m, cax=colorbar_ax) colorbar_ax.set_ylabel(r"$p$-value") return ax, colorbar_ax