Source code for scorpio

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Jose Benavides (jose.astroph@gmail.com)

# This file is part of the
#   SCORPIO Project (https://github.com/josegit88/SCORPIO).
# Copyright (c) 2020, Jose Benavides
# License: MIT
#   Full Text: https://github.com/josegit88/SCORPIO/blob/add-license-2/LICENSE

# ============================================================================
# DOCS
# ============================================================================

"""Sky COllector of galaxy Pairs and Image Output (Scorpio).

Is a tool to quick generate images of galaxy pairs, using data from different
surveys.

"""

# =============================================================================
# IMPORTS
# =============================================================================

import urllib

import astropy.cosmology as apc
from astropy import units as apu
from astropy import wcs
from astropy.coordinates import SkyCoord

from astroquery.skyview import SkyView

import attr

import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

import numpy as np

from retrying import retry

import seaborn as sns


# =============================================================================
# METADATA
# =============================================================================

__version__ = "0.3"


# =============================================================================
# CONSTANTS
# =============================================================================

#: SURVEYS
SURVEYS = {
    "SDSS": {"default": ["g", "i"], "filters": ["u", "g", "r", "i", "z"]},
    "2MASS": {"default": ["-H", "-K"], "filters": ["-J", "-H", "-K"]},
    "WISE": {
        "default": [" 4.6", " 12"],
        "filters": [" 3.4", " 4.6", " 12", " 22"],
    },
}

#: The default cosmology.
COSMOLOGY = apc.default_cosmology.get()

#: The default coordinate system.
DOWNLOAD_COORDINATE_SYSTEM = "J2000"

#: If a progress indicator will be showed when a file is downloaded.
DOWNLOAD_SHOW_PROGRESS = True

#: The default plot size for scorpio
PLOT_DEFAULT_SIZE = (8, 8)


# ============================================================================
# EXCEPTIONS
# ============================================================================


[docs]class NoFilterToStackError(ValueError): """Error generated when data of stack galaxies is empty.""" pass
[docs]class NoSurveyInListToStackError(ValueError): """Error generated when survey of stack galaxies not in our list.""" pass
[docs]class InvalidCosmologyError(TypeError): """Error generated when `cosmology` is an invalid astropy cosmology.""" pass
# ============================================================================= # IMAGE RESPONSE CLASS # =============================================================================
[docs]@attr.s(frozen=True) class GPInteraction: """Representation of two interacting galaxies. Parameters ---------- ra1 : float Right ascension of primary galaxy in degrees. dec1 : float Declination of primary galaxy in degrees. z1: float Redshift of secondary galaxy. ra2 : float Right ascension of secondary galaxy in degrees. dec2 : float Declination of secondary galaxy in degrees. z2 : float Redshift of secondary galaxy. resolution : int Size resolution value in pixels, by default it is 1000. survey : string Survey for query and download data, by default it is "SDSS". filters: list of strings Filters for respective survey, by default it is ["g", "i"] for "SDSS". Attributes ---------- mtx_ : The image of the two galaxies interacting. header_ : header information of image fits file. dist_physic_ : Estimation of physical distance to the observer in kpc, from a given cosmology dist_pix_ : Physical distance as pixels in the image. pos1_ : RA, DEC, Z as pixels of the first galaxy. pos2_ : RA, DEC, Z as pixels of the second galaxy. """ ra1 = attr.ib() dec1 = attr.ib() z1 = attr.ib() ra2 = attr.ib() dec2 = attr.ib() z2 = attr.ib() survey = attr.ib() resolution = attr.ib() cosmology = attr.ib() mtx_ = attr.ib(repr=False) header_ = attr.ib(repr=False) dist_physic_ = attr.ib(repr=False) dist_pix_ = attr.ib(repr=False) pos1_ = attr.ib(repr=False) pos2_ = attr.ib(repr=False)
[docs] def plot( self, ax=None, cmap="magma", center_color="cyan", center=True, scale=True, fmt=".4g", llimit=None, heatmap_kws=None, center_kws=None, center_text_kws=None, scalebar_kws=None, scalebar_text_kws=None, scalebar_bg_kws=None, ): r"""Receives data from other functions to generate and export a image. Parameters ---------- ax : Axes Complete information of the properties to generate the image, by default it is None. cmap : ``str``, optional Name of the color map to be used (https://matplotlib.org/users/colormaps.html). If is None, the default colors of the matplotlib.pyplot.plot function is used, and if, and is a callable is used as colormap generator. center_color : ``str``, optional The color for the marker of the galaxy centers. center : ``bool``, optional. Whether to draw the galaxy center markers. scale : ``bool``, optional. Whether to draw the scale bar. fmt : ``str``, optional String formatting code to use in the coordinates. llimit : ``float``, optional. The lowest value to show in the plot. If some value is <= llimit then it was remplaced with llimit. By default this value is setted as 0.0005 times the maximun value of the image. heatmap_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`seaborn.heatmap`. This routine is the one that draws the image of the interaction. cbar_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.figure.Figure.colorbar`. center_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.pyplot.Axes.plot`. This routine is the one that draws the markers for the centers of the two galaxies. center_text_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.pyplot.Axes.text`. This routine is the one that draws the names of the markers of the galactic centers. scalebar_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.pyplot.Axes.hlines`. This routine is what draws the scale bar. scalebar_text_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.pyplot.Axes.text`. This routine is what draws the scale bar text. scalebar_bg_kws : ``dict`` or ``None``, optional Keyword arguments for :meth:`matplotlib.pyplot.Axes.broken_barh`. This routine is what draws the scale bar background color. Returns ------- ``matplotlib.pyplot.Axis`` : The axis where the method draws. """ if ax is None: ax, fig = plt.gca(), plt.gcf() fig.set_size_inches(PLOT_DEFAULT_SIZE) # get al values from the instance for a more compact code survey = self.survey cosmology = self.cosmology final_image_a = np.copy(self.mtx_[0]) pxl = self.resolution c1, c2 = self.pos1_, self.pos2_ dis_c1_c2 = self.dist_pix_ s_ab = self.dist_physic_.value # Normalization part 1 max_value = np.max(final_image_a) min_value = np.min(final_image_a) # Normalization part 2 (remove negatives) llimit = (0.0005 * max_value) if llimit is None else llimit final_image_a[final_image_a <= llimit] = llimit max_value = np.max(final_image_a) min_value = np.min(final_image_a) # plot the image heatmap_kws = {} if heatmap_kws is None else dict(heatmap_kws) heatmap_kws["cmap"] = cmap heatmap_kws.setdefault("square", True) heatmap_kws.setdefault("cbar", False) heatmap_kws.setdefault("norm", LogNorm(vmin=min_value, vmax=max_value)) heatmap_kws.setdefault("xticklabels", False) heatmap_kws.setdefault("yticklabels", False) sns.heatmap(final_image_a, ax=ax, **heatmap_kws) # Markers of the galaxy centers if center: ra1, dec1, z1 = [ format(v, fmt) for v in (self.ra1, self.dec1, self.z1) ] ra2, dec2, z2 = [ format(v, fmt) for v in (self.ra2, self.dec2, self.z2) ] center_kws = {} if center_kws is None else dict(center_kws) center_kws["color"] = center_color center_kws.setdefault("marker", "o") center_kws.setdefault("markersize", 20) center_kws.setdefault("mew", 2) center_kws.setdefault("fillstyle", "none") label_g1 = f"G1: $RA={ra1}$, $Dec={dec1}$, $Z={z1}$" ax.plot(c1[0], c1[1], label=label_g1, **center_kws) label_g2 = f"G2: $RA={ra2}$, $Dec={dec2}$, $Z={z2}$" ax.plot(c2[0], c2[1], label=label_g2, **center_kws) center_text_kws = ( {} if center_text_kws is None else dict(center_text_kws) ) center_text_kws["color"] = center_color center_text_kws.setdefault("fontsize", 15) offset = center_kws.get("markersize") or 0 ax.text(c1[0] + offset, c1[1], "G1", **center_text_kws) ax.text(c2[0] + offset, c2[1], "G2", **center_text_kws) ax.legend(framealpha=1) if scale: len_bar = 50.0 * dis_c1_c2 / s_ab # Scale bar kwargs text scalebar_text_kws = ( {} if scalebar_text_kws is None else dict(scalebar_text_kws) ) scale_fontsize = scalebar_text_kws.setdefault("fontsize", 15) scalebar_text_kws.setdefault("x", scale_fontsize) scalebar_text_kws.setdefault("y", pxl - scale_fontsize * 2) scalebar_text_kws.setdefault("s", "50 kpc") scalebar_text_kws.setdefault("color", "k") # Scale bar kwargs itself scalebar_kws = {} if scalebar_kws is None else dict(scalebar_kws) scalebar_kws.setdefault("y", pxl - scale_fontsize * 1.1) scalebar_kws.setdefault("xmin", scale_fontsize) scalebar_kws.setdefault("xmax", len_bar) scalebar_kws.setdefault("color", "k") scalebar_kws.setdefault("linewidth", 3) # Scale bar background kwargs scalebar_bg_kws = ( {} if scalebar_bg_kws is None else scalebar_bg_kws ) scalebar_bg_kws.setdefault("xranges", [(0, len_bar * 1.2)]) scalebar_bg_kws.setdefault( "yrange", (pxl * 0.9 - scale_fontsize, pxl) ) scalebar_bg_kws.setdefault("facecolors", "w") ax.broken_barh(**scalebar_bg_kws) ax.hlines(**scalebar_kws) ax.text(**scalebar_text_kws) ax.set_title( f"Interaction - Survey: {survey} - " f"Cosmology: {cosmology.name}" ) ax.set_xlabel("RA") ax.set_ylabel("Dec") ax.patch.set_edgecolor("black") ax.patch.set_linewidth("3") return ax
# ============================================================================ # FUNCTIONS # ============================================================================
[docs]@retry(stop_max_attempt_number=4) def download_data(pos, survey, filters, pxl): """Proxy to astroquery SkyviewService. This functions is mostly for internal use. Parameters ---------- pos: SkyCoord Determines the center of the field to be retrieved. survey: str The data to download the survey. filters: list Specific astronomical filters of the data. pxl: Selects the pixel dimensions of the image to be produced. Returns ------- A list of `~astropy.io.fits.HDUList` objects. """ path = SkyView.get_images( position=pos, survey=survey + str(filters), radius=2 * apu.arcmin, pixels=(pxl, pxl), coordinates=DOWNLOAD_COORDINATE_SYSTEM, show_progress=DOWNLOAD_SHOW_PROGRESS, ) return path
[docs]def stack_pair( ra1, dec1, ra2, dec2, resolution=1000, survey="SDSS", filters=None, ): """Generate a individual image from RA, DEC data of galaxy pair. Parameters ---------- ra1 : float Right ascension of primary galaxy in degrees. dec1 : float Declination of primary galaxy in degrees. ra2 : float Right ascension of secondary galaxy in degrees. dec2 : float Declination of secondary galaxy in degrees. resolution : int Size resolution value in pixels, by default it is 1000. survey : string Survey for query and download data, by default it is "SDSS". filters: list of strings Filters for respective survey, by default it is ["g", "i"] for "SDSS". Returns ------- g1g2 : array_like Array with the stacked information of the galaxies in the survey and the selected filters. header: string Header from the primary galaxy .fits file. """ # survey validation survey_conf = SURVEYS.get(survey) if survey_conf is None: raise NoSurveyInListToStackError(f"Survey '{survey}' not supported") filters = survey_conf["default"] if filters is None else filters for filter in filters: if filter not in survey_conf["filters"]: raise NoFilterToStackError(f"{filter} is not a valid filter") # preprocess glx1 = SkyCoord(ra=ra1 * apu.degree, dec=dec1 * apu.degree) glx2 = SkyCoord(ra=ra2 * apu.degree, dec=dec2 * apu.degree) glx_pos = [glx1, glx2] # download images data fits in diferent filters: -------- g1g2 = [ np.zeros(shape=(resolution, resolution)), np.zeros(shape=(resolution, resolution)), ] for filter in filters: for i, pos in enumerate(glx_pos): try: stamp = download_data( pos=pos, survey=survey, filters=filter, pxl=resolution ) if i == 0: header = stamp[0][0].header except urllib.error.HTTPError as err: if err.code != 404: raise err else: g1g2[i] += stamp[0][0].data if np.all(g1g2[0] == 0): raise NoFilterToStackError("Empty array for galaxy 1.") return g1g2, header
[docs]def distances(ra1, dec1, ra2, dec2, z1, z2, header, cosmology=None): """Receives the RA, DEC, redshift parameters for the two galaxies. As well as header information for the primary galaxy and cosmology within the options provided by Astropy. Calculate the physical and pixel distances of the two galaxies necessary for their location in the final image. Parameters ---------- ra1 : float Right ascension of primary galaxy in degrees. dec1 : float Declination of primary galaxy in degrees. z1: float Redshift of secondary galaxy. ra2 : float Right ascension of secondary galaxy in degrees. dec2 : float Declination of secondary galaxy in degrees. z2 : float Redshift of secondary galaxy. header : string Header from the primary galaxy .fits file. survey : string Survey for query and download data, by default it is "SDSS". cosmology: optional Instance of class ``astropy.cosmology.FLRW``. Defaults to astropy's default cosmology (Planck18 since v4.2). Returns ------- dist_physic : float physical distance between the pair in kpc. dist_pix : float physical distance to pixels in the image. c1, c2 : array_like Arrays with coordinates of both galaxies in pixels. """ # cosmology validation if cosmology is None: cosmology = COSMOLOGY if not isinstance(cosmology, apc.FLRW): raise InvalidCosmologyError(f"Cosmology `{cosmology}` not allowed") z_mean = (z1 + z2) / 2 scale_factor = 1 / (1 + z_mean) # comoving distance in kpc dist_comv = cosmology.comoving_distance(z_mean) dist_comv = dist_comv.to(apu.kpc).value coord_a = SkyCoord(ra=ra1 * apu.deg, dec=dec1 * apu.deg) coord_b = SkyCoord(ra=ra2 * apu.deg, dec=dec2 * apu.deg) theta_rad = coord_a.separation(coord_b).rad dist_physic = (dist_comv * theta_rad) * scale_factor data_wcs = wcs.WCS(header) c1 = data_wcs.wcs_world2pix(ra1, dec1, 0) c2 = data_wcs.wcs_world2pix(ra2, dec2, 0) dist_pix = np.sqrt((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2) return dist_physic, dist_pix, c1, c2
[docs]def gpair( ra1, dec1, ra2, dec2, z1, z2, survey="SDSS", filters=None, resolution=1000, cosmology=None, ): """Receives the RA, DEC, redshift parameters for the two galaxies. As well as the resolution in pixels, survey and filters. Returns the necessary characteristics to generate the final image. Parameters ---------- ra1 : float Right ascension of primary galaxy in degrees. dec1 : float Declination of primary galaxy in degrees. z1: float Redshift of secondary galaxy. ra2 : float Right ascension of secondary galaxy in degrees. dec2 : float Declination of secondary galaxy in degrees. z2 : float Redshift of secondary galaxy. survey : string, optional (default='SDSS') Survey for query and download data. filters: list of strings Filters for respective survey, by default it is ["g", "i"] for "SDSS". resolution : int, optional (default=1000) Size resolution value in pixels. cosmology: optional Instance of class ``astropy.cosmology.FLRW``. Defaults to astropy's default cosmology (Planck18 since v4.2). Returns ------- GPInteraction : An object with information about the two interacting galaxies. Notes ----- For a complete list of astropy predefined cosmologies see: https://docs.astropy.org/en/latest/cosmology/index.html#built-in-cosmologies """ # cosmology validation if cosmology is None: cosmology = COSMOLOGY if not isinstance(cosmology, apc.FLRW): raise InvalidCosmologyError(f"Cosmology `{cosmology}` not allowed") g1g2, header = stack_pair( ra1, dec1, ra2, dec2, resolution=resolution, survey=survey, filters=filters, ) dist_physic, dist_pix, pos1, pos2 = distances( ra1, dec1, ra2, dec2, z1, z2, header, cosmology ) gpi = GPInteraction( ra1=ra1, dec1=dec1, ra2=ra2, dec2=dec2, z1=z1, z2=z2, survey=survey, resolution=resolution, cosmology=cosmology, # properties mtx_=g1g2, header_=header, dist_physic_=dist_physic * apu.kpc, dist_pix_=dist_pix, pos1_=pos1, pos2_=pos2, ) return gpi