Source code for dysh.plot.scanplot

"""
Plot a spectrum using matplotlib
"""

import warnings
from copy import deepcopy

import astropy.units as u
import numpy as np
from matplotlib.text import OffsetFrom
from matplotlib.ticker import AutoLocator, MaxNLocator

from . import PlotBase

_KMS = u.km / u.s


[docs] class ScanPlot(PlotBase): r""" The ScanPlot class is for simple plotting of a `~scan.Scan` or `~scan.ScanBlock` using matplotlib functions. Plots attributes are modified using keywords (\*\*kwargs) described below SpectrumPlot will attempt to make smart default choices for the plot if no additional keywords are given. Parameters ---------- scanblock_or_scan : `~spectra.scan.Scan` or `~spectra.scan.ScanBlock` The scan or scanblock to plot. **kwargs : dict Plot attribute keyword arguments, see below. Other Parameters ---------------- spectral_unit : str The units to use on the frequency axis. Can be 'MHz' or 'GHz'. """ def __init__(self, scanblock_or_scan, **kwargs): super().__init__() self._scanblock_or_scan = scanblock_or_scan self._plot_kwargs.update(kwargs) self._axis2 = None # self._title = self._plot_kwargs["title"]# todo: deal with when refactoring acceptable_types = ["PSScan", "TPScan", "NodScan", "FSScan", "SubBeamNodScan"] self._spectrum = self._scanblock_or_scan.timeaverage() self._sa = self._spectrum.spectral_axis # determine if input is a ScanBlock or a ScanBase (raise exception if neither) self._type = scanblock_or_scan.__class__.__name__ if self._type == "ScanBlock": self._scanblock = scanblock_or_scan self._num_scans = len(self._scanblock) elif self._type in acceptable_types: self._scan = scanblock_or_scan else: raise Exception(f"Plotter input {self._type} does not appear to be a valid input object type") # handle scanblocks if self._type == "ScanBlock": self._scan_nos = [] # scan numbers in the scan block self._nint_nos = [] # number of integrations in each scan self._timestamps = [] # 0-indexed timestamps in sec for every integration xtick_labels = [] # intnum labels for multiple-scan scanblocks for i, scan in enumerate(self._scanblock): if i == 0: self.spectrogram = scan._calibrated else: self.spectrogram = np.append(self.spectrogram, scan._calibrated, axis=0) self._scan_nos.append(scan.scan) self._nint_nos.append(scan.nint) # not sure if I need this xtick_labels.append(np.r_[0 : scan.nint]) # TODO: figure out how to deal with generating a "time" axis # agnostic of scan proctype (pos sw, etc will have gaps between scans due to OFF) # self._timestamps.append(scan.) xtick_labels = np.concatenate(xtick_labels, axis=0) # handle scans elif self._type in acceptable_types: self.spectrogram = self._scan._calibrated self._scan_nos = [self._scan.scan] self._nint_nos = [self._scan.nint] xtick_labels = np.r_[0 : self._scan.nint] self._xtick_labels = xtick_labels self.spectrogram = self.spectrogram.T
[docs] def reset(self): """Reset the plot keyword arguments to their defaults.""" self._plot_kwargs = { "title": None, "cmap": "inferno", "interpolation": "nearest", }
[docs] def plot(self, spectral_unit=None, **kwargs): r""" Plot the scan. Parameters ---------- spectral_unit : `~astropy.unit.Unit` The units to use on the frequency axis. Default: MHz if below 1 GHz, GHz if above. **kwargs : various keyword=value arguments (need to describe these in a central place) """ this_plot_kwargs = deepcopy(self._plot_kwargs) this_plot_kwargs.update(kwargs) cmap = kwargs.get("cmap", "inferno") interpolation = kwargs.get("interpolation", "nearest") if True: self._figure, self._axis = self._plt.subplots(figsize=(10, 6)) self._axis2 = self._axis.twinx() self._axis3 = self._axis.twiny() self._figure.subplots_adjust(top=0.79, left=0.1, right=1.05) self._set_header(self._spectrum) self.im = self._axis.imshow(self.spectrogram, aspect="auto", cmap=cmap, interpolation=interpolation) # address intnum labelling for len(scanblock) > 1 self._axis.set_xticks(np.arange(self.spectrogram.shape[1]), self._xtick_labels) if len(self._xtick_labels) == 1: locator = MaxNLocator(nbins=len(self._xtick_labels), integer=True, min_n_ticks=1) else: locator = AutoLocator() self._axis.xaxis.set_major_locator(locator) # second "plot" to get different scales on x2, y2 axes if spectral_unit is not None: self._sa = self._sa.to(spectral_unit) else: if self._sa[0] / (u.GHz) < 1: self._sa = self._sa.to(u.MHz) else: self._sa = self._sa.to(u.GHz) stop = self.spectrogram.shape[1] step = self.spectrogram.shape[1] / self.spectrogram.shape[0] im2 = self._axis2.plot(np.arange(0, stop, step), self._sa, linewidth=0) # noqa: F841 self._axis2.set_ylim((np.min(self._sa).value, np.max(self._sa).value)) # third axis to plot the scan numbers im3 = self._axis3.plot(np.arange(0, stop, step), self._sa, linewidth=0) # noqa: F841 # determine tick locations and labels tick_locs = [] acc = 0 for numints in self._nint_nos: tick_locs.append(acc) acc += numints self._axis3.set_xticks(tick_locs) self._axis3.set_xticklabels(self._scan_nos) fsize = 15 x1_alt_padding = self._plt.rcParams["axes.labelpad"] + fsize self._axis3.tick_params( axis="x", width=0, pad=x1_alt_padding + 9, # labelsize=fsize, bottom=True, top=False, labelbottom=True, labeltop=False, ) self._axis.set_xlim(0, stop - 0.5) self._set_labels()
def _set_labels(self): # x1: bottom # x2: top # y1: left # y2: right # z: colorbar x1_label = "Integration" self._axis.set_xlabel(x1_label) x1_alt_label = "Scan" off = OffsetFrom(self._axis3.get_xticklabels()[0], (0.0, 0.0)) self._axis3.annotate(x1_alt_label, xy=(0.5, 0.5), xytext=(-10, 0.0), textcoords=off, va="bottom", ha="right") y1_label = "Channel" self._axis.set_ylabel(y1_label) y2_unit = self._sa.unit if y2_unit.is_equivalent(u.Hz): nu = r"$\nu$" y2_label = f"{nu} ({y2_unit})" self._axis2.set_ylabel(y2_label) z_unit = self._spectrum.unit if z_unit.is_equivalent(u.K): z_label = f"$T_A$ ({z_unit})" elif z_unit.is_equivalent(u.Jy): snu = r"$S_{\nu}$" z_label = f"{snu} ({z_unit})" elif z_unit.is_equivalent(u.ct): z_label = "Counts" else: warnings.warn("Flux units are unknown", stacklevel=2) z_label = "" self._colorbar = self._figure.colorbar(self.im, label=z_label, pad=0.1) # matplotlib won't set this before the Figure is drawn. self._figure.draw_without_rendering() # If there's an offset, add it to the label and make the offset invisible. if self._colorbar.ax.yaxis.offsetText.get_text() != "": off = self._colorbar.ax.yaxis.offsetText.get_text() e = off.split("e")[1] self._colorbar.set_label(z_label + rf"($\times10^{{{e}}}$)") self._colorbar.ax.yaxis.offsetText.set_visible(False)
[docs] def set_clim(self, vmin, vmax): """ Set the vmin and vmax parameters of the image. Parameters ---------- vmin : float The minimum value of the color scale. vmax : float The maximum value of the color scale. """ self.im.set_clim(vmin=vmin, vmax=vmax)
[docs] def set_interpolation(self, interpolation="nearest"): """ Set the interpolation of the image. Parameters ---------- interpolation : str Interpolation method. Default: "nearest". """ self.im.set_interpolation(interpolation)
[docs] def set_cmap(self, cmap="inferno"): """ Set the cmap of the image. Parameters ---------- cmap : str cmap used for the color scale. Default: "inferno". """ self.im.set_cmap(cmap)