Source code for exosim.plots.radiometricPlotter

# This code is inspired by the code developed for ExoRad 2.
# Therefore, we attach here for ExoRad 2 license:
#
# BSD 3-Clause License
#
# Copyright (c) 2020, Lorenzo V. Mugnai, Enzo Pascale, "La Sapienza" Università di Roma
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the names of the copyright holders nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
from typing import Tuple, Union

import astropy.units as u
import h5py
import matplotlib
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
from astropy.io.misc.hdf5 import read_table_hdf5
from astropy.table import Table
from photutils.aperture import EllipticalAperture, RectangularAperture

import exosim.log as log
from exosim.output.hdf5.utils import load_signal
from exosim.utils.ascii_arts import observatory

from .utils import _create_ordered_cmap, prepare_channels_list

plt.rcParams.update({"font.size": 16})


[docs]class RadiometricPlotter(log.Logger): """ Radiometric plotter. This class handles the methods to plot the radiometric table produced by `exosim`. Attributes ----------- input: str or :class:`astropy.table.QTable` input data input_table: :class:`astropy.table.QTable` input radiometric table fig: :class:`matplotlib.figure.Figure` produced figure Examples ---------- The following example, given the `test_file.h5` preoduced by Exosim, plots the radiometric table and stores the figure as `radiometric.png`. >>> from exosim.plots import RadiometricPlotter >>> radiometricPlotter = RadiometricPlotter(input='./test_file.h5') >>> radiometricPlotter.plot_table() >>> radiometricPlotter.save_fig('radiometric.png') """ def __init__(self, input: Union[str, Table]) -> None: """ Parameters ---------- input: str or :class:`astropy.table.QTable` input data """ self.set_log_name() self.graphics(observatory) self.announce("started") self.input = input if isinstance(input, str): self.input_table = self.load_table(input) else: self.input_table = input self.fig = None
[docs] def load_table(self, input_file: str) -> Table: """ It loads the radiometric table from the input file: Parameters ---------- input_file: str input file name Returns ------- :class:`astropy.table.QTable` loaded radiometric table """ with h5py.File(input_file, "r") as f: file_path = "radiometric" tab = read_table_hdf5(f[file_path], path="table") self.debug("radiometric table loaded") return tab
[docs] def plot_bands( self, ax: plt.Axes, scale: str = "log", channel_edges: bool = True, add_legend: bool = True, ) -> plt.Axes: """ It plots the channels bands behind the indicated axes. Parameters ----------- ax: :class:`matplotlib.axes.Axes` axes where to plot the bands scale: str x axes scale. Default is `log`. channel_edges: bool if ``True`` the x axes ticks are placed at the channel edges. Default is ``True``. Returns -------- :class:`matplotlib.axes.Axes` axes with channel bands added """ channels, norm = prepare_channels_list(self.input) cmap = _create_ordered_cmap("Pastel1", roll=-2, delete=-3) tick_list, patches = [], [] for k, channel_name in enumerate(channels): wl_min = min( self.input_table["left_bin_edge"][ np.where(self.input_table["ch_name"] == channel_name) ] ) if hasattr(wl_min, "unit"): wl_min = wl_min.value wl_max = max( self.input_table["right_bin_edge"][ np.where(self.input_table["ch_name"] == channel_name) ] ) if hasattr(wl_max, "unit"): wl_max = wl_max.value ax.axvspan( wl_min, wl_max, alpha=0.3, zorder=0, color=cmap( norm(k), ), ) ax.axvspan( wl_min, wl_max, alpha=0.3, zorder=0, color=cmap( norm(k), ), ) # wl_maxs += [wl_max] tick_list.append(wl_min) tick_list.append(wl_max) patches += [ mpatches.Patch( color=cmap(norm(k)), alpha=0.3, label=channel_name ) ] # tick_list.append(max(wl_maxs)) ax.set_xscale(scale) if channel_edges: ax.set_xticks(tick_list) ax.get_xaxis().set_major_formatter( matplotlib.ticker.ScalarFormatter() ) if add_legend: band_legend = ax.legend( handles=patches, title="Channels legend", bbox_to_anchor=(1, 1) ) ax.add_artist(band_legend) return ax
[docs] def plot_noise( self, ax: plt.Axes, scale: str = "log", channel_edges: bool = True, contribs: bool = False, ch_lengend: bool = True, ) -> plt.Axes: """ It plots the noise components found in the input table in the indicated axes. Parameters ----------- ax: :class:`matplotlib.axes.Axes` axes where to plot the noises scale: str x axes scale. Default is `log`. channel_edges: bool if ``True`` the x axes ticks are placed at the channel edges. Default is ``True``. contribs: bool if ``True`` all the contributions are plotted. Default is ``False``. ch_lengend: bool if ``True`` add a legend for the channels color. Default is ``True``. Returns -------- :class:`matplotlib.axes.Axes` axes with noises plotted """ noise_keys = [ x for x in self.input_table.keys() if "noise" in x or "custom" in x ] if not contribs: noise_keys = [k for k in noise_keys if "photon_noise" not in k] noise_keys += ["source_photon_noise", "foreground_photon_noise"] self.debug("noise keys : {}".format(noise_keys)) for k, n in enumerate(noise_keys): if n == "total_noise": ax.plot( self.input_table["Wavelength"], self.input_table[n], zorder=9, lw=1, c="k", marker=".", markersize=5, label="total_noise", alpha=0.8, ) # , c='None') else: if self.input_table[n].unit == u.hr**0.5: noise = self.input_table[n] elif self.input_table[n].unit == u.ct / u.s: self.debug( "{} rescaled by starSignal_inAperture".format(n) ) noise = ( self.input_table[n] / self.input_table["source_signal_in_aperture"] / (u.hr.to(u.s)) ** 0.5 ) else: self.error( "{} unit not valid: {}".format( n, self.input_table[n].unit ) ) # ax.scatter(self.input_table['Wavelength'], noise, label=n, zorder=10, s=5, color=palette[k]) ax.plot( self.input_table["Wavelength"], noise, zorder=9, lw=1, alpha=0.5, marker=".", label=n, ) # color=palette[k]) # c='None') # ax.grid(zorder=0, which='both') locmaj = matplotlib.ticker.LogLocator(base=10, numticks=12) ax.yaxis.set_major_locator(locmaj) locmin = matplotlib.ticker.LogLocator( base=10.0, subs=(0.2, 0.4, 0.6, 0.8), numticks=12 ) ax.yaxis.set_minor_locator(locmin) ax.yaxis.set_minor_formatter(matplotlib.ticker.NullFormatter()) ax.grid(axis="y", which="minor", alpha=0.3) ax.grid(axis="y", which="major", alpha=0.5) # ax.legend(bbox_to_anchor=(1, 1)) ax.set_title("Noise Budget") ax.set_xlabel(r"Wavelength [$\mu m$]") ax.set_ylabel(r"relative noise [$\sqrt{{hr}}$]") ax.set_yscale(scale) # ax.set_xscale('log') ax = self.plot_bands(ax, scale, channel_edges, add_legend=ch_lengend) ax.legend( prop={"size": 12}, loc="upper left", ncol=3, bbox_to_anchor=(0.05, -0.25), labelspacing=1.2, handlelength=1, ) return ax
[docs] def plot_signal( self, ax: plt.Axes, ylim: Tuple[float, float] = None, scale: str = "log", channel_edges: bool = True, contribs: bool = False, ch_lengend: bool = True, ) -> Tuple[plt.Figure, Tuple[plt.Axes, plt.Axes]]: """ It plots the signal components found in the input table in the indicated axes. Parameters ----------- ylim: float or (float, float) ylim for :class:`matplotlib.axes.Axes`. ax: :class:`matplotlib.axes.Axes` axes where to plot the signals scale: str x axes scale. Default is `log`. channel_edges: bool if ``True`` the x axes ticks are placed at the channel edges. Default is ``True``. contribs: bool if ``True`` all the contributions are plotted. Default is ``False``. ch_lengend: bool if ``True`` add a legend for the channels color. Default is ``True``. Returns -------- :class:`matplotlib.axes.Axes` axes with signals plotted """ keys = ["source_signal_in_aperture", "foreground_signal_in_aperture"] if contribs: keys = [ x for x in self.input_table.keys() if "signal_in_aperture" in x and "noise" not in x ] self.debug("signal keys : {}".format(keys)) for k, s in enumerate(keys): ax.plot( self.input_table["Wavelength"], self.input_table[s], zorder=9, lw=1, alpha=0.5, marker=".", label=s, ) if ylim: ax.set_ylim(ylim) # elif ax.get_ylim()[0]<: # ax.set_ylim(1e-3) # ax.grid(zorder=0, which='both') locmaj = matplotlib.ticker.LogLocator(base=10, numticks=12) ax.yaxis.set_major_locator(locmaj) locmin = matplotlib.ticker.LogLocator( base=10.0, subs=(0.2, 0.4, 0.6, 0.8), numticks=12 ) ax.yaxis.set_minor_locator(locmin) ax.yaxis.set_minor_formatter(matplotlib.ticker.NullFormatter()) ax.grid(axis="y", which="minor", alpha=0.3) ax.grid(axis="y", which="major", alpha=0.5) # ax.legend(bbox_to_anchor=(1, 1)) ax.set_title("Signals") ax.set_xlabel(r"Wavelength [$\mu m$]") ax.set_ylabel("$ct/s$") ax.set_yscale(scale) ax = self.plot_bands(ax, scale, channel_edges, add_legend=ch_lengend) ax.legend( prop={"size": 12}, loc="upper left", ncol=3, bbox_to_anchor=(0.05, -0.25), labelspacing=1.2, handlelength=1, ) return ax
[docs] def plot_table( self, scale: str = "log", channel_edges: bool = True, contribs: bool = False, ) -> Tuple[plt.Figure, Tuple[plt.Axes, plt.Axes]]: """ It produces a figure with signal and noise for the input table. Parameters ---------- scale: str x axes scale. Default is `log`. channel_edges: bool if ``True`` the x axes ticks are placed at the channel edges. Default is ``True``. contribs: bool if ``True`` all the contributions are plotted. Default is ``False``. Returns -------- :class:`matplotlib.figure.Figure` plotted figure (:class:`matplotlib.axes.Axes`, :class:`matplotlib.axes.Axes`) tuple of axis. First axes is for signal, second is for noise. """ self.info("plotting radiometric table") fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10)) # fig.suptitle(self.input_table.meta['name']) ax1 = self.plot_signal( ax1, scale=scale, channel_edges=channel_edges, contribs=contribs ) ax2 = self.plot_noise( ax2, scale=scale, channel_edges=channel_edges, contribs=contribs, ch_lengend=False, ) plt.tight_layout() plt.subplots_adjust(top=0.92, bottom=0.22, hspace=0.7, right=0.8) self.fig = fig return fig, (ax1, ax2)
[docs] def plot_efficiency( self, scale: str = "log", channel_edges: bool = False, ch_lengend: bool = True, ) -> Tuple[plt.Figure, Tuple[plt.Axes, plt.Axes]]: """ It produces a figure with efficiencies for the input table. Parameters ---------- scale: str x axes scale. Default is `log`. channel_edges: bool if ``True`` the x axes ticks are placed at the channel edges. Default is ``True``. ch_lengend: bool if ``True`` add a legend for the channels color. Default is ``True``. Returns -------- :class:`matplotlib.figure.Figure` plotted figure (:class:`matplotlib.axes.Axes`, :class:`matplotlib.axes.Axes`) tuple of axis. First axes is for signal, second is for noise. """ self.info("plotting efficiency table") # fig.suptitle(self.input_table.meta['name']) channels = set(self.input_table["ch_name"]) channels = list(channels) channels.sort() fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10)) with h5py.File(self.input, "r") as f: for ch in channels: eff_path = "channels/{}/efficiency".format(ch) eff = load_signal(f[eff_path]) ax1.plot(eff.spectral, eff.data[0, 0], label=ch) resp_path = "channels/{}/responsivity".format(ch) resp = load_signal(f[resp_path]) ax2.plot(resp.spectral, resp.data[0, 0], label=ch) # locmaj = matplotlib.ticker.LogLocator(base=10, numticks=12) # ax.yaxis.set_major_locator(locmaj) # locmin = matplotlib.ticker.LogLocator(base=10.0, # subs=(0.2, 0.4, 0.6, 0.8), # numticks=12) # ax.yaxis.set_minor_locator(locmin) # ax.yaxis.set_minor_formatter(matplotlib.ticker.NullFormatter()) # ax.grid(axis='y', which='minor', alpha=0.3) # ax.grid(axis='y', which='major', alpha=0.5) # # ax.legend(bbox_to_anchor=(1, 1)) ax1.set_title("Efficiency") ax1.set_xlabel(r"Wavelength [$\mu m$]") # ax.set_ylabel('$ct/s$') # ax1.set_yscale(scale) ax2.set_title("Responsivity") ax2.set_ylabel(r"${}$".format(resp.data_units)) ax2.set_xlabel(r"Wavelength [$\mu m$]") ax2.set_yscale("log") ax1 = self.plot_bands(ax1, scale, channel_edges, add_legend=ch_lengend) ax2 = self.plot_bands(ax2, scale, channel_edges, add_legend=ch_lengend) ax2.legend( prop={"size": 12}, loc="upper left", ncol=7, bbox_to_anchor=(0.05, -0.25), labelspacing=1.2, handlelength=1, ) plt.tight_layout() plt.subplots_adjust( top=0.92, bottom=0.12, hspace=0.7, right=0.8, left=0.1 ) self.fig = fig return fig, (ax1, ax2)
[docs] def plot_apertures(self) -> plt.Figure: """ It produces a figure with apertures superimposed to the focal plane. Returns -------- :class:`matplotlib.figure.Figure` plotted figure """ self.info("plotting apertures") table = self.load_table(self.input) def _prepare_figure(): with h5py.File(self.input, "r") as f: ch_list = list(f["channels"].keys()) ch_list.sort() widths = [] for ch in ch_list: size_x = f["channels"][ch]["focal_plane"][ "spectral" ].shape[0] size_y = f["channels"][ch]["focal_plane"]["spatial"].shape[ 0 ] widths += [int(np.ceil(size_x / size_y)), 0.1] heights = [1] scale = np.ceil(len(widths) / len(heights)) size_y_fig = 10 size_x_fig = size_y_fig * scale fig = plt.figure( constrained_layout=True, dpi=150, figsize=(size_x_fig, size_y_fig), ) spec = fig.add_gridspec( ncols=len(widths), nrows=len(heights), width_ratios=widths, height_ratios=heights, wspace=0.1, hspace=0.1, ) return fig, spec, ch_list def _load_apertures(ch): center_spectral = table["spectral_center"][table["ch_name"] == ch] spectral_size = table["spectral_size"][table["ch_name"] == ch] center_spatial = table["spatial_center"][table["ch_name"] == ch] spatial_size = table["spatial_size"][table["ch_name"] == ch] shape = table["aperture_shape"][table["ch_name"] == ch] aperture_shapes = { "rectangular": RectangularAperture, "elliptical": EllipticalAperture, } aper = [] for i in range(center_spectral.size): aper += [ aperture_shapes[shape[i]]( (center_spectral[i] - 1, center_spatial[i] - 1), spectral_size[i], spatial_size[i], ) ] return aper fig, spec, ch_list = _prepare_figure() i = 0 for ch in ch_list: with h5py.File(self.input, "r") as f: file_path = os.path.join("channels", ch) focal_plane = load_signal( f[os.path.join(file_path, "focal_plane")] ) foreground = load_signal( f[os.path.join(file_path, "frg_focal_plane")] ) osf = focal_plane.metadata["oversampling"] final = focal_plane.data[0] + foreground.data[0] ax0 = fig.add_subplot(spec[0, i]) im = ax0.imshow( final[osf // 2 :: osf, osf // 2 :: osf], interpolation="none", ) ax0.set_title(ch) apertures = _load_apertures(ch) for aperture in apertures: aperture.plot(color="r", lw=2) i += 1 plt.colorbar(im, ax=ax0, cax=fig.add_subplot(spec[i])) i += 1 self.fig = fig return fig
[docs] def save_fig(self, name: str) -> None: """ It saves the produced figure. Parameters -------- name: str figure name """ dir_name = os.path.dirname(os.path.abspath(name)) if not os.path.exists(dir_name): os.makedirs(dir_name) try: self.fig.savefig("{}".format(name)) self.info("plot saved in {}".format(name)) except AttributeError: self.error( "the indicated figure is not available. Check if you have produced it." )