Source code for mcalf.visualisation.classifications

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.gridspec import GridSpec

from mcalf.utils.plot import calculate_extent, class_cmap
from mcalf.utils.smooth import mask_classifications

__all__ = ['plot_classifications', 'bar', 'plot_class_map', 'init_class_data']


[docs]def plot_classifications(spectra, labels, nrows=None, ncols=None, nlines=20, style='original', cmap=None, show_labels=True, plot_settings={}, fig=None): """Plot spectra grouped by their labelled classification. Parameters ---------- spectra : ndarray, ndim=2 Two-dimensional array with dimensions [spectra, wavelengths]. labels : ndarray, ndim=1, length of `spectra` List of classifications for each spectrum in `spectra`. nrows : int, optional, default=None Number of rows. Defaults to rows of max width 3 axes. Special case: four plots will be in a 2x2 grid. Only one of `nrows` and `ncols` can be specified. ncols : int, optional, default=None Number of columns. Defaults to rows of max width 3 axes. Special case: four plots will be in a 2x2 grid. Only one of `nrows` and `ncols` can be specified. nlines : int, optional, default=20 Maximum number of lines per classification plot. style : str, optional, default='original' The named matplotlib colormap to extract a :class:`~matplotlib.colors.ListedColormap` from. Colours are selected from `vmin` to `vmax` at equidistant values in the range [0, 1]. The :class:`~matplotlib.colors.ListedColormap` produced will also show bad classifications and classifications out of range in grey. The default 'original' is a special case used since early versions of this code. It is a hardcoded list of 5 colours. When the number of classifications exceeds 5, ``style='viridis'`` will be used. cmap : callable, optional, default=None Function that returns a colour for each input from zero to num. classifications. This parameter overrides any cmap requested via the `style` parameter. Return value is passed to the `color` parameter of :func:`matplotlib.pyplot.axes.Axes.plot`. show_labels : bool, optional, default=True Whether to label the axes with the corresponding classifications. plot_settings : dict, optional, default={} Dictionary of keyword arguments to pass to :func:`matplotlib.pyplot.axes.Axes.plot`. fig : matplotlib.figure.Figure, optional, default=None Figure into which the classifications will be plotted. Defaults to the current figure. Returns ------- gs : matplotlib.gridspec.GridSpec The grid layout subplots are placed on within the figure. Examples -------- .. minigallery:: mcalf.visualisation.plot_classifications """ if fig is None: fig = plt.gcf() # Validate parameters for n, v in (('spectra', spectra), ('labels', labels)): if not isinstance(v, np.ndarray): raise TypeError(f'`{n}` must be a numpy.ndarray, got {type(v)}.') if not spectra.ndim == 2: raise TypeError('`spectra` must be a 2D array.') if not labels.ndim == 1 or not issubclass(labels.dtype.type, np.integer): raise TypeError('`labels` must be a 1D array of integers.') if len(spectra) != len(labels): raise ValueError('`spectra` and `labels` must be the same length along the first dimension.') if nrows is not None and ncols is not None: raise ValueError('Both `nrows` and `ncols` cannot be given together.') for n, v in (('nrows', nrows), ('ncols', ncols)): if v is not None and not isinstance(v, (int, np.integer)): raise TypeError(f'`{n}` must be an integer, got {type(v)}.') if not isinstance(nlines, (int, np.integer)) or nlines <= 0: raise TypeError('`nlines` must be a positive integer.') # Find and count unique classifications classifications = np.unique(labels) n = len(classifications) # number of subplots if n == 0: return None # Set `nrows` and `ncols` if ncols is None and nrows is None: if n == 1: ncols = 1 elif n == 2 or n == 4: ncols = 2 else: ncols = 3 nrows = int(np.ceil(n / ncols)) elif ncols is None: ncols = int(np.ceil(n / nrows)) elif nrows is None: nrows = int(np.ceil(n / ncols)) # Verify `nrows` and `ncols` if (nrows - 1) * ncols >= n: raise ValueError('`nrows` is larger than it needs to be.') if nrows * (ncols - 1) >= n: raise ValueError('`ncols` is larger than it needs to be.') gs = GridSpec(nrows, ncols, figure=fig, wspace=0) # Configure the color map if cmap is None: cmap = class_cmap(style, n) for i in range(n): ax = fig.add_subplot(gs[i]) c = classifications[i] lines = spectra[labels == c] if len(lines) > nlines: # crop if too big lines = lines[:nlines] # Whether to crop the y-axis to [0, 1] limit_y = False if np.nanmin(lines) > -1e-6 and np.nanmax(lines) < 1 + 1e-6: limit_y = True color = cmap(i) # extract the single color from the listed colormap for line in lines: ax.plot(line, color=color, **plot_settings) if limit_y: # if data within range [0, 1] ax.set_ylim(0, 1) ax.set_yticks([0, 1]) # only show that intensity is scaled [0, 1] ax.set_xticks([]) # no wavelengths plotted ax.margins(0) # no "gaps" at line ends for loc, spine in ax.spines.items(): if loc != 'left': spine.set_color('none') # don't draw spine if show_labels: ax.set_title(f'classification {str(c)}') return gs
[docs]def bar(class_map=None, vmin=None, vmax=None, reduce=True, style='original', cmap=None, ax=None, data=None): """Plot a bar chart of the classification abundances. Parameters ---------- class_map : numpy.ndarray[int], ndim=2 or 3 Array of classifications. If the array is three-dimensional, it is assumed that the first dimension is time, and a time average classification will be plotted. The time average is the most common positive (valid) classification at each pixel. vmin : int, optional, default=None Minimum classification integer to plot. Must be greater or equal to zero. Defaults to min positive integer in `class_map`. vmax : int, optional, default=None Maximum classification integer to plot. Must be greater than zero. Defaults to max positive integer in `class_map`. reduce : bool, optional, default=True Whether to perform the time average described in `class_map` info. style : str, optional, default='original' The named matplotlib colormap to extract a :class:`~matplotlib.colors.ListedColormap` from. Colours are selected from `vmin` to `vmax` at equidistant values in the range [0, 1]. The :class:`~matplotlib.colors.ListedColormap` produced will also show bad classifications and classifications out of range in grey. The default 'original' is a special case used since early versions of this code. It is a hardcoded list of 5 colours. When the number of classifications exceeds 5, ``style='viridis'`` will be used. cmap : str or matplotlib.colors.Colormap, optional, default=None Parameter to pass to matplotlib.axes.Axes.imshow. This parameter overrides any cmap requested via the `style` parameter. ax : matplotlib.axes.Axes, optional, default=None Axes into which the velocity map will be plotted. Defaults to the current axis of the current figure. data : dict, optional, default=None Dictionary of common classification plotting settings generated by :func:`init_class_data`. If present, all other parameters are ignored except and `ax`. Returns ------- b : matplotlib.container.BarContainer The object returned by :func:`matplotlib.axes.Axes.bar` after plotting abundances. See Also -------- mcalf.models.ModelBase.classify_spectra : Classify spectra. mcalf.utils.smooth.average_classification : Average a 3D array of classifications. Notes ----- Visualisation assumes that all integers between `vmin` and `vmax` are valid classifications, even if they do not appear in `class_map`. Examples -------- .. minigallery:: mcalf.visualisation.bar """ if ax is None: ax = plt.gca() if data is None: if class_map is None: # `class_map` must always be provided raise TypeError("bar() missing 1 required positional argument: 'class_map'") data = init_class_data(class_map, vmin=vmin, vmax=vmax, reduce=reduce, style=style, cmap=cmap) # Count for each classification d = data['class_map'].flatten() counts = np.array([len(d[d == i]) for i in data['classes']]) d = counts / len(d) * 100 # Convert to percentage b = ax.bar(data['classes'], d, color=data['cmap'](np.arange(len(data['classes'])))) ax.set(xlabel='classification', ylabel='abundance (%)', xticks=data['classes'], xticklabels=data['classes']) return b
[docs]def plot_class_map(class_map=None, vmin=None, vmax=None, resolution=None, offset=(0, 0), dimension='distance', style='original', cmap=None, show_colorbar=True, colorbar_settings=None, ax=None, data=None): """Plot a map of the classifications. Parameters ---------- class_map : numpy.ndarray[int], ndim=2 or 3 Array of classifications. If the array is three-dimensional, it is assumed that the first dimension is time, and a time average classification will be plotted. The time average is the most common positive (valid) classification at each pixel. vmin : int, optional, default=None Minimum classification integer to plot. Must be greater or equal to zero. Defaults to min positive integer in `class_map`. vmax : int, optional, default=None Maximum classification integer to plot. Must be greater than zero. Defaults to max positive integer in `class_map`. resolution : tuple[float] or astropy.units.quantity.Quantity, optional, default=None A 2-tuple (x, y) containing the length of each pixel in the x and y direction respectively. If a value has type :class:`astropy.units.quantity.Quantity`, its axis label will include its attached unit, otherwise the unit will default to Mm. If `resolution` is None, both axes will be ticked with the default pixel value with no axis labels. offset : tuple[float] or int, length=2, optional, default=(0, 0) Two offset values (x, y) for the x and y axis respectively. Number of pixels from the 0 pixel to the first pixel. Defaults to the first pixel being at 0 length units. For example, in a 1000 pixel wide dataset, setting offset to -500 would place the 0 Mm location at the centre. dimension : str or tuple[str] or list[str], length=2, optional, default='distance' If an `ax` (and `resolution`) is provided, use this string as the `dimension name` that appears before the ``(unit)`` in the axis label. A 2-tuple (x, y) or list [x, y] can instead be given to provide a different name for the x-axis and y-axis respectively. style : str, optional, default='original' The named matplotlib colormap to extract a :class:`~matplotlib.colors.ListedColormap` from. Colours are selected from `vmin` to `vmax` at equidistant values in the range [0, 1]. The :class:`~matplotlib.colors.ListedColormap` produced will also show bad classifications and classifications out of range in grey. The default 'original' is a special case used since early versions of this code. It is a hardcoded list of 5 colours. When the number of classifications exceeds 5, ``style='viridis'`` will be used. cmap : str or matplotlib.colors.Colormap, optional, default=None Parameter to pass to matplotlib.axes.Axes.imshow. This parameter overrides any cmap requested via the `style` parameter. show_colorbar : bool, optional, default=True Whether to draw a colorbar. colorbar_settings : dict, optional, default=None Dictionary of keyword arguments to pass to :func:`matplotlib.figure.Figure.colorbar`. Ignored if `show_colorbar` is False. ax : matplotlib.axes.Axes, optional, default=None Axes into which the velocity map will be plotted. Defaults to the current axis of the current figure. data : dict, optional, default=None Dictionary of common classification plotting settings generated by :func:`init_class_data`. If present, all other parameters are ignored except `show_colorbar` and `ax`. Returns ------- im : matplotlib.image.AxesImage The object returned by :func:`matplotlib.axes.Axes.imshow` after plotting `class_map`. See Also -------- mcalf.models.ModelBase.classify_spectra : Classify spectra. mcalf.utils.smooth.average_classification : Average a 3D array of classifications. Notes ----- Visualisation assumes that all integers between `vmin` and `vmax` are valid classifications, even if they do not appear in `class_map`. Examples -------- .. minigallery:: mcalf.visualisation.plot_class_map """ if ax is None: ax = plt.gca() if data is None: if class_map is None: # `class_map` must always be provided raise TypeError("plot_class_map() missing 1 required positional argument: 'class_map'") data = init_class_data(class_map, vmin=vmin, vmax=vmax, resolution=resolution, offset=offset, dimension=dimension, style=style, cmap=cmap, colorbar_settings=colorbar_settings, ax=ax) # Plot `class_map` (using special imshow `data` keyword) im = ax.imshow('class_map', cmap='cmap', vmin='plot_vmin', vmax='plot_vmax', origin='lower', extent='extent', interpolation='nearest', data=data) if show_colorbar: ax.get_figure().colorbar(im, **data['colorbar_settings']) return im
[docs]def init_class_data(class_map, vmin=None, vmax=None, reduce=True, resolution=None, offset=(0, 0), dimension='distance', style='original', cmap=None, colorbar_settings=None, ax=None): """Initialise dictionary of common classification plotting data. Parameters ---------- class_map : numpy.ndarray[int], ndim=2 or 3 Array of classifications. If `reduce` is True (default) and the array is three-dimensional, it is assumed that the first dimension is time, and a time average classification will be calculated. The time average is the most common positive (valid) classification at each pixel. vmin : int, optional, default=None Minimum classification integer to include. Must be greater or equal to zero. Defaults to min positive integer in `class_map`. Classifications below this value will be set to -1. vmax : int, optional, default=None Maximum classification integer to include. Must be greater than zero. Defaults to max positive integer in `class_map`. Classifications above this value will be set to -1. reduce : bool, optional, default=True Whether to perform the time average described in `class_map` info. resolution : tuple[float] or astropy.units.quantity.Quantity, optional, default=None A 2-tuple (x, y) containing the length of each pixel in the x and y direction respectively. If a value has type :class:`astropy.units.quantity.Quantity`, its axis label will include its attached unit, otherwise the unit will default to Mm. If `resolution` is None, both axes will be ticked with the default pixel value with no axis labels. offset : tuple[float] or int, length=2, optional, default=(0, 0) Two offset values (x, y) for the x and y axis respectively. Number of pixels from the 0 pixel to the first pixel. Defaults to the first pixel being at 0 length units. For example, in a 1000 pixel wide dataset, setting offset to -500 would place the 0 Mm location at the centre. dimension : str or tuple[str] or list[str], length=2, optional, default='distance' If an `ax` (and `resolution`) is provided, use this string as the `dimension name` that appears before the ``(unit)`` in the axis label. A 2-tuple (x, y) or list [x, y] can instead be given to provide a different name for the x-axis and y-axis respectively. style : str, optional, default='original' The named matplotlib colormap to extract a :class:`~matplotlib.colors.ListedColormap` from. Colours are selected from `vmin` to `vmax` at equidistant values in the range [0, 1]. The :class:`~matplotlib.colors.ListedColormap` produced will also show bad classifications and classifications out of range in grey. The default 'original' is a special case used since early versions of this code. It is a hardcoded list of 5 colours. When the number of classifications exceeds 5, ``style='viridis'`` will be used. cmap : str or matplotlib.colors.Colormap, optional, default=None Parameter to pass to matplotlib.axes.Axes.imshow. This parameter overrides any cmap requested via the `style` parameter. colorbar_settings : dict, optional, default=None Dictionary of keyword arguments to pass to :func:`matplotlib.figure.Figure.colorbar`. ax : matplotlib.axes.Axes, optional, default=None Axes into which the classification map will be plotted. Defaults to the current axis of the current figure. Returns ------- data : dict Common classification plotting settings. See Also -------- mcalf.visualisation.bar : Plot a bar chart of the classification abundances. mcalf.visualisation.plot_class_map : Plot a map of the classifications. mcalf.utils.smooth.mask_classifications : Mask 2D and 3D arrays of classifications. mcalf.utils.plot.calculate_extent : Calculate the extent from a particular data shape and resolution. mcalf.utils.plot.class_cmap : Create a listed colormap for a specific number of classifications. Examples -------- .. minigallery:: mcalf.visualisation.init_class_data """ # Mask and average classification map according to the classification range and shape class_map, vmin, vmax = mask_classifications(class_map, vmin, vmax, reduce=reduce) # Create a list of the classifications classes = np.arange(vmin, vmax + 1, dtype=int) # Configure the color map if cmap is None: cmap = class_cmap(style, len(classes)) # Calculate a specific extent if a resolution is specified extent = calculate_extent(class_map.shape, resolution, offset, ax=ax, dimension=dimension) # Create and update colorbar settings cbar_settings = {'ax': [ax], 'ticks': classes} if colorbar_settings is not None: cbar_settings.update(colorbar_settings) data = { 'class_map': class_map, 'vmin': vmin, 'vmax': vmax, 'plot_vmin': vmin - 0.5, 'plot_vmax': vmax + 0.5, 'classes': classes, 'extent': extent, 'cmap': cmap, 'colorbar_settings': cbar_settings, } return data