Source code for xpsi.PostProcessing._signalplot

from ._global_imports import *

try:
    import fgivenx
except ImportError:
    _warning('Cannot import fgivenx for conditional posterior contours.')
    fgivenx = None

from .. import Signal

[docs]class SignalPlot(object, metaclass=ABCMeta): """ Base class for a signal plot. Interact with a single :class:`~xpsi.Signal.Signal` instance to plot signals. :param str fig_dir: Directory to write to. :param str root_filename: Prepended to filename automatically generated figure filename. :param str cmap: Colormap name from :mod:`matplotlib` to use for :mod:`fgivenx` contours. It is advisable to choose a colormap that is darker at lower values (in tails of conditional posterior distribution) and lighter at higher values (at higher densities in conditional posterior distribution). """ __figtype__ = None # When you subclass, assign values to the following specials and then # do not change the values at runtime unless the methods behave # accordingly in response to such changes. This is because the methods # might simply assume the values and have not adapt to changes in those # values, which might require conditional statements to control the flow # of execution. It is best to consider these specials as hard-coded/ # invariant, and not runtime variables because they describe the nature # and distribution of information for a given plot type, instead of being # boolean execution switches or settings whose values are independent of the # flow of execution. __caching_targets__ = None __rows__ = None # of large panels (of size cls._panelsize) __columns__ = None # of large panels (of size cls._panelsize) __ax_rows__ = None # of all axes including colorbar axes __ax_columns__ = None # of all axes including colorbar axes __height_ratios__ = None # of all axes __width_ratios__ = None # of all axes __wspace__ = None # of all axes __hspace__ = None # of all axes
[docs] @classmethod @make_verbose('Declaring plot class settings','Settings declared') def declare_settings(cls, panelsize=(8,4), fscale=1.0, rasterized=True, tick_lengths=(8,4), tick_width=1.0, logspace_y=False, tqdm_kwargs={'disable': False}, scale_ymin=0.9, scale_ymax=1.1, ny=500, lines_on=False, add_label=True, write=True, extension='.pdf', dpi=300, write_kwargs=None, xticks=None, yticks=None): """ Configure subclass with attributes, *before instantiation*. These attributes can in principle be safely changed during the runtime. :param tuple panelsize: The approximate size, ``(width, height)``, in inches *per panel*. :param float fscale: Scale factor for the fontsize applied to the default. :param bool rasterized: Beware that if ``False``, white polygon edges may appear in figures. :param tuple(int, int) tick_lengths: The major and minor tick lengths for all axis objects. :param int tick_width: The width of every tick. :param bool logspace_y: Should the :mod:`fgivenx` y-values (for which the posterior mass is integrated over points at lower densities than the density at y) be logarithimcally or linearly spaced? Appropriate for logarithmic y-axes of signal panels. :param dict tqdm_kwargs: Progress bar keyword arguments. :param float scale_ymin: The minimum y-value to use, as a fraction of the minimum sample value. :param float scale_ymax: The maximum y-value to use, as a fraction of the minimum sample value. :param int ny: Number of y-values to compute the posterior mass for. :param bool lines_on: Render contour lines? :param bool add_label: Add contour colorbar label. :param bool write: Write figure to disk? :param str extension: File extension for writing. :param int dpi: Figure write resolution. :param dict write_kwargs: Additional writing keyword arguments. :param list(list(float)) xticks: Determine the x-tick values that will be labelled in each of the subplots (including color bars). Automatic ticks and labels are used for the specific subplot if providing None instead of a list of floats (e.g., xticks=[[0.1,0.5,1.0],None,None]). If xticks=None (default), automatic ticks and labels are used for all the subplots. :param list(list(float)) yticks: Determine the y-tick values that will be labelled in each of the subplots (including color bars). See param xticks for more details. """ if panelsize[1] >= panelsize[0]: yield ('Warning: plots are generally better rendered such ' 'that panel width is greater than panel height. Use the ' '``figsize`` argument to specify figure size per panel.') cls._panelsize = panelsize cls._fscale = fscale cls._fontsize = cls._fscale * 3.25 * min(panelsize[0], panelsize[1]) cls._rasterized = rasterized cls._tick_lengths = tick_lengths cls._tick_width = tick_width cls._logspace_y = logspace_y cls._tqdm_kwargs = tqdm_kwargs cls._scale_ymin = scale_ymin cls._scale_ymax = scale_ymax cls._ny = ny cls._lines_on = lines_on cls._add_label = add_label cls._write = write cls._ext = extension cls._dpi = int(dpi) if not write_kwargs: cls._write_kwargs = dict(bbox_inches='tight') cls._xticks = xticks cls._yticks = yticks cls._settings_declared = True yield
def __init__(self, fig_dir = './', root_filename = '', cmap = 'RdPu_r', **kwargs): try: type(self)._settings_declared except AttributeError: self.declare_settings() # defaults else: if not type(self)._settings_declared: self.declare_settings() # defaults # set fontsize globally for use in subclass initializer for axes # creation; note that __exit__ from context finalizes plotting # before another plot object is handled and also ensures fontsize # is set as the class fontsize in case needed rc('font',**{'size': self._fontsize}) # per plot fontsize settings self._fig_dir = fig_dir self._root_filename = root_filename self._cmap = cmap # will shadow class attributes if you really do want instance-specific # attributes for key, value in kwargs.items(): key = '_' + key if key[0] != '_' else key try: getattr(self, key) except AttributeError: pass else: if key[1] != '_': # try to protect specials setattr(self, key, value) self._axes = [] # in subclass init, append axes to automatically veneer @property def posterior(self): try: return self._posterior except AttributeError: return '' @posterior.setter def posterior(self, ID): if not isinstance(ID, _six.string_types): raise TypeError('Invalid type for ID.') else: self._posterior = ID @property def signal(self): return self._signal @signal.setter def signal(self, obj): if not isinstance(obj, Signal): raise TypeError('Invalid type for signal object.') else: self._signal = obj def __next__(self): """ Redirect for Python 3 compatibility. """ return next(self) def __enter__(self): """ Plots are a resource to be managed. """ rc('font',**{'size': self._fontsize}) # per plot fontsize settings return self def __exit__(self, exc_type, exc_value, traceback): if exc_type is not None: raise self.finalize() try: self._axes except AttributeError: # assume no axes to veneer pass else: try: iter(self._axes) except TypeError: # quietly do nothing pass else: iax = 0 for ax in self._axes: if self._xticks is None and self._yticks is None: self._veneer(ax) elif self._xticks is not None and self._yticks is None: self._veneer(ax,xticks=self._xticks[iax]) if self._xticks[iax] is not None: ax.set_xticklabels(self._xticks[iax]) elif self._yticks is not None and self._xticks is None: self._veneer(ax,yticks=self._yticks[iax]) if self._yticks[iax] is not None: ax.set_yticklabels(self._yticks[iax]) else: self._veneer(ax,xticks=self._xticks[iax],yticks=self._yticks[iax]) if self._yticks[iax] is not None: ax.set_yticklabels(self._yticks[iax]) if self._xticks[iax] is not None: ax.set_xticklabels(self._xticks[iax]) iax = iax+1 if self._write: self.savefig() # the following handles possible hanging current figure ref # if comment out this line, a figure and axis of default # matplotlib size seems to be created somewhere and output # in a notebook; need to figure ;) out why that happens plt.close()
[docs] @abstractmethod def execute(self, thetas, wrapper): """ Execute behaviours requiring a loop over samples. :param ndarray[n,m] thetas: Equally-weighted set of posterior samples. :param callable wrapper: Wrapper function that interleaves iterative calls `next(self)` with code to update the :class:`~.Signal.Signal` instance from a cache stored on disk. The wrapper returns a callback function, If such a cache is unavailable, the likelihood function is evaluated on-the-fly when the callback is called passing a sample (parameter vector). In this case, signals to be plotted are temporarily cached in the :class:`~.Signal.Signal` instance. The signature of the wrapper function is: `wrapper(self, delete_me=None, index=None)`, where `self` is this a subclass of :class:`~.SignalPlot`, `delete_me` is an optional attribute name or a list of attribute name in `self.__dict__` to be deleted before iterating over samples, and `index` is an optional integer to index a tuple returned from `next(self)`. The signature of the callback function, if you need to call it directly (instead of an external library such as fgivenx calling it): `callback(None, theta)`, where each sample/parameter vector is a row in `thetas`. """ pass
def __next__(self): """ Redirect for Python 3 compatibility. """ return next(self) #@abstractmethod #def __next__(self): # """ Execute next iteration. """ # pass
[docs] @abstractmethod def finalize(self): """ Execute instructions to finish plotting. Before this method is called, the likelihood object will be updated so that the true (injected) signals are cached. In the body of the subclass implementation of this method, one can plot the injected signals by accessing the attributes of the signal instance. """ pass
def _veneer(self, ax, xticks=None, yticks=None): """ These are globally applicable settings for aesthetics. """ if not isinstance(ax, _mpl.axes.Axes): return ax.tick_params(axis='both', which='major', colors='black', length=self._tick_lengths[0], width=self._tick_width, direction='out') ax.tick_params(axis='both', which='minor', colors='black', length=self._tick_lengths[1], width=self._tick_width, direction='out') for spine in ax.spines.values(): spine.set_linewidth(self._tick_width) if xticks: ax.set_xticks(xticks) if yticks: ax.set_yticks(yticks) def _add_contours(self, callback, thetas, x, ax, ax_cb, **kwargs): """ Plot contours with :mod:`fgivenx`. In order to increase control, we bypass direct use of some functions from :mod:`fgivenx.drivers`. Avoid :mod:`fgivenx` caching because it caching is implemented in :mod:`xpsi.PostProcessing` for derived signals, which are generally needed beyond a single application of :mod:`fgivenx`. We thus do not currently cache the masses computed by :mod:`fgivenx`, but this is acceptable in terms of the speed to replot the contours. .. todo:: Determine whether parallelistion is straighforwardly possible within :mod:`fgivenx` given that the callback is complicated object. Currently ``OpenMP`` threading is enabled by the likelihood object instead. """ if fgivenx is None: raise ImportError('Install fgivenx to plot contours.') tqdm_kwargs = kwargs.get('tqdm_kwargs', self._tqdm_kwargs) fsamps = fgivenx.samples.compute_samples(f=[callback], x=x, samples=[thetas], cache=None, parallel=False, tqdm_kwargs=tqdm_kwargs) ymin = fsamps[~_np.isnan(fsamps)].min(axis=None) ymin *= kwargs.get('scale_ymin', self._scale_ymin) ymax = fsamps[~_np.isnan(fsamps)].max(axis=None) ymax *= kwargs.get('scale_ymax', self._scale_ymax) ny = kwargs.get('ny', self._ny) logspace = kwargs.get('logspace_y', self._logspace_y) if not logspace: y = _np.linspace(ymin, ymax, ny) else: y = _np.logspace(_np.log10(ymin), _np.log10(ymax), ny, base=10.0) # posterior mass integration more robust under transformation _fsamps = fsamps if not logspace else _np.log10(fsamps) _y = y if not logspace else _np.log10(y) pmf = fgivenx.mass.compute_pmf(fsamps=_fsamps, y=_y, parallel=False, cache=None, tqdm_kwargs=tqdm_kwargs) cb = fgivenx.plot.plot(x=x, y=y, z=pmf, ax=ax, colors=cm.get_cmap(kwargs.get('cmap', self._cmap)), lines=kwargs.get('lines_on', self._lines_on), rasterize_contours=self._rasterized, smooth=False) # no additional smoothing with kernel cb = plt.colorbar(cb, cax=ax_cb, ticks=[1,2,3]) # ticks units = sigmas cb.ax.set_frame_on(True) cb.ax.yaxis.set_minor_locator(AutoMinorLocator()) if kwargs.get('add_label', self._add_label): cb.ax.set_yticklabels([r'$1\sigma$', r'$2\sigma$', r'$3\sigma$']) def _add_signal(self, ax, x, signal, axis=None, **kwargs): """ Add signal line as a 1D function. """ if not kwargs: kwargs.update(dict(color='k', ls='-', lw=0.05, alpha=1.0)) if axis is not None: signal = _np.sum(signal, axis=axis) ax.plot(x, signal, **kwargs) def _get_figure(self): """ Create and return figure handle. """ cls = type(self) # do not permit shadowing instance attributes self._fig = plt.figure(figsize = (self._panelsize[0] * cls.__columns__, self._panelsize[1] * cls.__rows__)) self._gs = gridspec.GridSpec(cls.__ax_rows__, cls.__ax_columns__, height_ratios = cls.__height_ratios__, width_ratios = cls.__width_ratios__) self._gs.update(wspace = cls.__wspace__ * self._fscale, hspace = cls.__hspace__ * self._fscale) return self._fig, self._gs @property def fig(self): """ Get the figure object. """ try: return self._fig except AttributeError: pass # nothing to return def _add_subplot(self, i, j): """ Add subplot and append a reference to the new subplot axes object. To work with a subgrid, you must implement it manually. See, e.g., :class:`~xpsi.PostProcessing._spectrum.SpectrumPlot`. """ subplot = self._fig.add_subplot(self._gs[i,j]) self._axes.append(subplot) return subplot
[docs] @make_verbose('Writing plot to disk', 'Written') def savefig(self): """ Write figure to file. """ fileroot = (self._root_filename + '__' if self._root_filename else '') fileroot += self.posterior.replace(' ', '_') filename = _os.path.join(self._fig_dir, fileroot + '__' + self.__figtype__ + self._ext) yield (self.__class__.__name__ + \ ' instance plot will be written to path %s' % filename) try: self._fig.savefig(filename, dpi=self._dpi, **self._write_kwargs) except TypeError: # quietly proceed self._fig.savefig(filename, dpi=self._dpi) yield