{ "cells": [ { "cell_type": "markdown", "id": "3fcc2a1b", "metadata": {}, "source": [ "# Aperture photometry on MIRI stage-2 images" ] }, { "cell_type": "markdown", "id": "young-shopping", "metadata": {}, "source": [ "**Author**: Mattia Libralato, ESA/AURA Astronomer\n", "
\n", "**Last Updated**: July, 2022" ] }, { "cell_type": "markdown", "id": "932ded30", "metadata": {}, "source": [ "## Table of contents\n", "1. [Introduction](#intro)
\n", "2. [Setup](#setup)
\n", " 2.1 [Python imports](#imports)
\n", " 2.2 [Plot configuration](#plot)
\n", " 2.3 [Useful functions](#func)
\n", " 2.4 [Setup PSF FWHM](#fwhm)
\n", " 2.5 [Download data](#download)
\n", "3. [A first glimpse to the MIRI data](#datalook)
\n", " 3.1 [Data Quality (DQ) flags](#dqflag)
\n", "4. [From the image to the astro-photometric catalog](#work)
\n", " 4.1 [Sky background](#sky)
\n", " 4.2 [Aperture photometry](#phot)
\n", " 4.3 [From pixel to equatorial coordinates](#wcs)
\n", " 4.4 [Photometric calibration](#cal)
\n", "5. [Exercise](#exercise)
\n", "6. [Bonus \\#1: your first JWST mid-infrared color-magnitude diagram](#bonus1)
\n", "7. [Bonus \\#2: concentration indexes](#bonus2)
" ] }, { "cell_type": "markdown", "id": "f9731131", "metadata": {}, "source": [ "1.-Introduction \n", "------------------" ] }, { "cell_type": "markdown", "id": "dcff349f", "metadata": {}, "source": [ "In this notebook we provide an overview on aperture photometry with simulated images obtained with the Mid-Infrared Instrument ([MIRI](https://jwst-docs.stsci.edu/jwst-mid-infrared-instrument)) onboard the James Webb Space Telescope (JWST).\n", "\n", "Images were simulated using [MIRISim](https://www.stsci.edu/jwst/science-planning/proposal-planning-toolbox/mirisim) and processed through the [JWST pipeline](https://jwst-pipeline.readthedocs.io/en/latest/). Each exposure was obtained with \"FASTR1\" readout, 20 groups per integration, and 3 integrations. Simulations were obtained using a [4-point dither pattern](https://jwst-docs.stsci.edu/mid-infrared-instrument/miri-operations/miri-dithering/miri-imaging-dithering#MIRIImagingDithering-4point) in F560W and F770W filters. The astronomical scene we will analyze is an homogeneous, mildly-crowded stellar field with a few background galaxies. The stellar population resambles that of a globular cluster; galaxies present random morphologies and fluxes. This mix of objects should provide a good benchmark for testing any photometric tools while waiting for real data.\n", "\n", "In this notebook, we use *cal.fits* files. These images are the outputs of the [Stage 2](https://jwst-pipeline.readthedocs.io/en/stable/jwst/pipeline/calwebb_image2.html) pipeline. The Stage 2 pipeline takes in the slope images output from the [Stage 1](https://jwst-pipeline.readthedocs.io/en/stable/jwst/pipeline/calwebb_detector1.html) pipeline and applies instrumental corrections and calibrations.\n", "\n", "At the end of this notebook, we should have learned (or refreshed our memory) on aperture photometry and on the MIRI imager." ] }, { "cell_type": "markdown", "id": "cb903d0a", "metadata": {}, "source": [ "2.-Setup \n", "------------------" ] }, { "cell_type": "markdown", "id": "obvious-hands", "metadata": {}, "source": [ "In this section we import all necessary python packages, define some useful functions that will help us later during the analysis, read a table with the encircled-energy values for some MIRI filters, and finally download the simulated MIRI images." ] }, { "cell_type": "markdown", "id": "04f56656", "metadata": {}, "source": [ "### 2.1-Python imports ###" ] }, { "cell_type": "code", "execution_count": null, "id": "8c9f61b0", "metadata": {}, "outputs": [], "source": [ "#\n", "# General tools\n", "#\n", "import glob\n", "import os\n", "import shutil\n", "import sys\n", "import random\n", "import urllib\n", "import zipfile\n", "#\n", "# Astropy tools\n", "#\n", "from astropy.coordinates import match_coordinates_sky, SkyCoord\n", "from astropy.io import fits, ascii\n", "from astropy.stats import SigmaClip, sigma_clipped_stats\n", "from astropy.table import Table, Column, vstack\n", "import astropy.units as u\n", "from astropy.visualization import LogStretch, LinearStretch, PercentileInterval, ManualInterval\n", "from astropy.nddata import Cutout2D\n", "#\n", "# JWST models\n", "#\n", "from jwst import datamodels, associations\n", "from jwst.datamodels import ImageModel, dqflags\n", "#\n", "# Matplotlib tools\n", "#\n", "from matplotlib import style, pyplot as plt, rcParams\n", "from matplotlib.colors import LogNorm\n", "from matplotlib.pyplot import figure\n", "from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable\n", "#\n", "# Numpy library\n", "#\n", "import numpy as np\n", "#\n", "# Photutils library and tools\n", "#\n", "import photutils\n", "from photutils.aperture import CircularAperture, CircularAnnulus, aperture_photometry\n", "from photutils import Background2D, MedianBackground, ModeEstimatorBackground, MMMBackground\n", "#\n", "# Scipy tools\n", "#\n", "from scipy import stats\n", "from scipy.interpolate import CubicSpline\n", "#\n", "# Use 90% of the window width\n", "#\n", "from IPython.core.display import display, HTML\n", "display(HTML(\"\"))" ] }, { "cell_type": "markdown", "id": "7104baa5", "metadata": {}, "source": [ "### 2.2-Plot configuration ###" ] }, { "cell_type": "markdown", "id": "7e43214b-5bdf-407a-811a-f07b3b31cf5c", "metadata": {}, "source": [ "
\n", "

Warning

\n", "\n", "If the plots in this notebook do not render properly, you may need to install LaTeX. Find more information on a system-wide installation [here](https://www.latex-project.org/get/) and on a Jupyter-specific nbextension [here](https://jupyter-contrib-nbextensions.readthedocs.io/en/latest/nbextensions/latex_envs/README.html).\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "5442732a", "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "# Figure size\n", "plt.rcParams['figure.figsize'] = [15, 15]\n", "# Figure text and font\n", "plt.rc('text', usetex=True)\n", "plt.rc('font', size=15)" ] }, { "cell_type": "markdown", "id": "9501191a", "metadata": {}, "source": [ "### 2.3-Useful functions ###" ] }, { "cell_type": "markdown", "id": "polish-concept", "metadata": {}, "source": [ "Here you can find some convenient functions:" ] }, { "cell_type": "code", "execution_count": null, "id": "b9023dc2", "metadata": {}, "outputs": [], "source": [ "def imshow_me_wcolorbar(img, vmin, vmax, tlabel, xlabel, ylabel, blabel, cmap):\n", " '''\n", " Function to show an image.\n", " \n", " Inputs\n", " ----------\n", " img : 2D numpy.ndarray\n", " The input 2D array.\n", " vmin : float\n", " The minimum value for the colorbar.\n", " vmax : float\n", " The maximum value for the colorbar.\n", " tlabel : string\n", " The plot title.\n", " xlabel : string\n", " The X-axis label.\n", " ylabel : string\n", " The Y-axis label.\n", " blabel : string\n", " The colorbar label.\n", " cmap : string\n", " The name of the colormap.\n", " '''\n", " \n", " fig, ax = plt.subplots()\n", " cax = ax.imshow(img, vmin=vmin, vmax=vmax, origin='lower', cmap=cmap)\n", " ax_divider = make_axes_locatable(ax)\n", " cax1 = ax_divider.append_axes('right', size='3%', pad='2%')\n", " cb = fig.colorbar(cax, cax=cax1)\n", " cb.ax.set_ylabel(r'{0}'.format(blabel))\n", " ax.set_xlabel(r'{0}'.format(xlabel))\n", " ax.set_ylabel(r'{0}'.format(ylabel))\n", " ax.set_title(r'{0}'.format(tlabel))\n", " plt.tight_layout()\n", " \n", " return" ] }, { "cell_type": "code", "execution_count": null, "id": "d03889e5", "metadata": {}, "outputs": [], "source": [ "def imshow_me_wcolorbar_setup(img, vmin, vmax, tlabel, xlabel, ylabel, blabel, cmap):\n", " '''\n", " Function to setup an image to show. Similar to imshow_me_wcolorbar,\n", " but it does not show the image directly, thus allowing to plot\n", " something else after the call.\n", " \n", " Inputs\n", " ----------\n", " img : 2D numpy.ndarray\n", " The input 2D array.\n", " vmin : float\n", " The minimum value for the colorbar.\n", " vmax : float\n", " The maximum value for the colorbar.\n", " tlabel : string\n", " The plot title.\n", " xlabel : string\n", " The X-axis label.\n", " ylabel : string\n", " The Y-axis label.\n", " blabel : string\n", " The colorbar label.\n", " cmap : string\n", " The name of the colormap.\n", " '''\n", " \n", " cax = ax.imshow(img, vmin=vmin, vmax=vmax, origin='lower', cmap=cmap)\n", " ax_divider = make_axes_locatable(ax)\n", " cax1 = ax_divider.append_axes('right', size='3%', pad='2%')\n", " cb = fig.colorbar(cax, cax=cax1)\n", " cb.ax.set_ylabel(r'{0}'.format(blabel))\n", " ax.set_xlabel(r'{0}'.format(xlabel))\n", " ax.set_ylabel(r'{0}'.format(ylabel))\n", " ax.set_title(r'{0}'.format(tlabel))\n", " \n", " return" ] }, { "cell_type": "code", "execution_count": null, "id": "taken-trademark", "metadata": {}, "outputs": [], "source": [ "def imshow_cutouts(img, tab, id_sel, xlabel, ylabel, cmap):\n", " '''\n", " Function to show four cutouts from an image.\n", " \n", " Inputs\n", " ----------\n", " img : 2D numpy.ndarray\n", " The input 2D array.\n", " tab : Table\n", " The table with at least (x,y,id) columns.\n", " id_sel : array(int)\n", " The ID of the sources in tab to show in the cutouts.\n", " xlabel : string\n", " The X-axis label.\n", " ylabel : string\n", " The Y-axis label.\n", " cmap : string\n", " The name of the colormap.\n", " '''\n", " \n", " fig, ax = plt.subplots(2, 2)\n", "\n", " x, y = tab['x'][tab['id'] == id_sel[0]], tab['y'][tab['id'] == id_sel[0]]\n", " cutout = Cutout2D(img, (x, y), (51, 51))\n", " ax[0, 0].imshow(cutout.data, vmin=-0.1, vmax=1.0, origin='lower', cmap=cmap)\n", " ax[0, 0].scatter(cutout.input_position_cutout[0], cutout.input_position_cutout[1], lw=0.5, s=15, \n", " marker='o', edgecolors='deepskyblue', facecolors='none')\n", " ax[0, 0].annotate(r'(x,y)$\\sim$({0},{1})'.format(int(x[0]), int(y[0])), xy=(0.5, 0.95), xycoords='axes fraction',\n", " horizontalalignment='center', verticalalignment='center')\n", " ax[0, 0].set_xlabel(r'{0}'.format(xlabel))\n", " ax[0, 0].set_ylabel(r'{0}'.format(ylabel))\n", "\n", " x, y = tab['x'][tab['id'] == id_sel[1]], tab['y'][tab['id'] == id_sel[1]]\n", " cutout = Cutout2D(img, (x, y), (51, 51))\n", " ax[0, 1].imshow(cutout.data, vmin=-0.1, vmax=1.0, origin='lower', cmap=cmap)\n", " ax[0, 1].scatter(cutout.input_position_cutout[0], cutout.input_position_cutout[1], lw=0.5, s=15, \n", " marker='o', edgecolors='deepskyblue', facecolors='none')\n", " ax[0, 1].annotate(r'(x,y)$\\sim$({0},{1})'.format(int(x[0]), int(y[0])), xy=(0.5, 0.95), xycoords='axes fraction', \n", " horizontalalignment='center', verticalalignment='center')\n", " ax[0, 1].set_xlabel(r'{0}'.format(xlabel))\n", " ax[0, 1].set_ylabel(r'{0}'.format(ylabel))\n", "\n", " x, y = tab['x'][tab['id'] == id_sel[2]], tab['y'][tab['id'] == id_sel[2]]\n", " cutout = Cutout2D(img, (x, y), (51, 51))\n", " ax[1, 0].imshow(cutout.data, vmin=-0.1, vmax=1.0, origin='lower', cmap=cmap)\n", " ax[1, 0].scatter(cutout.input_position_cutout[0], cutout.input_position_cutout[1], lw=0.5, s=15, \n", " marker='o', edgecolors='deepskyblue', facecolors='none')\n", " ax[1, 0].annotate(r'(x,y)$\\sim$({0},{1})'.format(int(x[0]), int(y[0])), xy=(0.5, 0.95), xycoords='axes fraction', \n", " horizontalalignment='center', verticalalignment='center')\n", " ax[1, 0].set_xlabel(r'{0}'.format(xlabel))\n", " ax[1, 0].set_ylabel(r'{0}'.format(ylabel))\n", "\n", " x, y = tab['x'][tab['id'] == id_sel[3]], tab['y'][tab['id'] == id_sel[3]]\n", " cutout = Cutout2D(img, (x, y), (51, 51))\n", " ax[1, 1].imshow(cutout.data, vmin=-0.1, vmax=1.0, origin='lower', cmap=cmap)\n", " ax[1, 1].scatter(cutout.input_position_cutout[0], cutout.input_position_cutout[1], lw=0.5, s=15, \n", " marker='o', edgecolors='deepskyblue', facecolors='none')\n", " ax[1, 1].annotate(r'(x,y)$\\sim$({0},{1})'.format(int(x[0]), int(y[0])), xy=(0.5, 0.95), xycoords='axes fraction', \n", " horizontalalignment='center', verticalalignment='center')\n", " ax[1, 1].set_xlabel(r'{0}'.format(xlabel))\n", " ax[1, 1].set_ylabel(r'{0}'.format(ylabel))\n", " \n", " plt.tight_layout()\n", " \n", " return" ] }, { "cell_type": "code", "execution_count": null, "id": "grand-puzzle", "metadata": {}, "outputs": [], "source": [ "def arcsec2pix(x):\n", " '''\n", " Function that converts arcsec in MIRIM pixels.\n", " It is used to setup the secondary axes in a plot.\n", " \n", " Inputs\n", " ----------\n", " x : float or array(float)\n", " The value in arcsec.\n", " \n", " Outputs\n", " ----------\n", " The value(s) in MIRIM pixel.\n", " '''\n", " \n", " return x/0.11" ] }, { "cell_type": "code", "execution_count": null, "id": "valuable-colombia", "metadata": {}, "outputs": [], "source": [ "def pix2arcsec(x):\n", " '''\n", " Function that converts MIRIM pixels in arcsec.\n", " It is used to setup the secondary axes in a plot.\n", " \n", " Inputs\n", " ----------\n", " x : float or array(float)\n", " The value in MIRIM pixels.\n", " \n", " Outputs\n", " ----------\n", " The value(s) in arcsec.\n", " '''\n", " \n", " return x*0.11" ] }, { "cell_type": "markdown", "id": "opposed-visitor", "metadata": {}, "source": [ "These last three functions will be used to speed-up the exercise." ] }, { "cell_type": "code", "execution_count": null, "id": "1e6de35b", "metadata": {}, "outputs": [], "source": [ "def prepare_image(img_name):\n", " '''\n", " Function that reads one image, flag all pixels with DQ flags different \n", " from [0, 2, 4, 6] and adds a random background gradient.\n", " To be used in the exercise.\n", " \n", " Inputs\n", " ----------\n", " img_name : string\n", " The name of the image.\n", " \n", " Outputs\n", " ----------\n", " img : ImageModel\n", " The data model of the image.\n", " img_mod : 2D numpy.ndarray\n", " The image data with the additional random background gradient.\n", " '''\n", " \n", " img = datamodels.open(img_name)\n", " ok = np.zeros(img.data.shape, dtype='int')\n", " for v in [0, 2, 4, 6]:\n", " ok = ok + np.where(img.dq == v, 1, 0)\n", "\n", " img.data[ok == 0] = np.nan\n", " \n", " x = np.linspace(0, 1, img.meta.subarray.xsize)\n", " y = np.linspace(0, 1, img.meta.subarray.ysize)\n", " X, Y = np.meshgrid(x, y)\n", " if (np.random.rand(1) > 0.5):\n", " sx = +1\n", " else:\n", " sx = -1\n", " if (np.random.rand(1) > 0.5):\n", " sy = +1\n", " else:\n", " sy = -1\n", " Z = (10*(np.random.rand(1)-0.5))*np.exp(sx*X/(4+2*(np.random.rand(1)-0.5)))*np.exp(sy*Y/(4+4*(np.random.rand(1)-0.5)))\n", " \n", " img_mod = img.data.copy() + Z\n", " \n", " _, sky_med, sky_sig = sigma_clipped_stats(img_mod[np.isfinite(img_mod)], sigma=5.0, maxiters=5)\n", " \n", " print('')\n", " print(r' Suggested min value: {0}'.format(sky_med-sky_sig))\n", " print(r' Suggested max value: {0}'.format(sky_med+sky_sig))\n", " print('')\n", "\n", " cmin = sky_med-sky_sig\n", " cmax = sky_med+sky_sig\n", " \n", " return img, img_mod, cmin, cmax" ] }, { "cell_type": "code", "execution_count": null, "id": "attached-export", "metadata": {}, "outputs": [], "source": [ "def prepare_table(img, xy_tmp, aperture_radius, sky, flux, rad_label):\n", " '''\n", " Function that performs additional steps discussed in the notebook.\n", " To be used in the exercise.\n", " \n", " Inputs\n", " ----------\n", " img : ImageModel\n", " The data model of the image.\n", " xy_tmp : Table\n", " The table created by DAOStarFinder with the sources detected\n", " in the image.\n", " aperture_radius : float\n", " The aperture radius used for the aperture photometry.\n", " sky : array(float)\n", " The array with the local sky background for each source.\n", " flux : array(float)\n", " The array with the sky-subtracted flux for each source.\n", " rad_label : string\n", " The label for the photometry in the Table.\n", " \n", " Outputs\n", " ----------\n", " aperture_table : Table\n", " The final astro-photometric table.\n", " '''\n", " \n", " aperture_table = Table()\n", "\n", " aperture_table['x'] = xy_tmp['xcentroid']\n", " aperture_table['y'] = xy_tmp['ycentroid']\n", " aperture_table['sharpness'] = xy_tmp['sharpness']\n", " aperture_table['roundness1'] = xy_tmp['roundness1']\n", " aperture_table['roundness2'] = xy_tmp['roundness2']\n", "\n", " aperture_table['id'] = xy_tmp['id']\n", " \n", " aperture_table['local_sky_' + rad_label] = sky\n", " aperture_table['aperture_' + rad_label + '_skysub'] = flux\n", " \n", " keep_good = np.logical_and(np.isfinite(aperture_table['aperture_' + rad_label + '_skysub']), \n", " aperture_table['aperture_' + rad_label + '_skysub'] > 0.)\n", " aperture_table = aperture_table[keep_good]\n", " \n", " aperture_table['mag_' + rad_label] = -2.5*np.log10(aperture_table['aperture_' + rad_label + '_skysub'])\n", "\n", " aperture_table['flag_' + rad_label] = np.zeros(len(aperture_table), dtype=int)\n", " for s in aperture_table:\n", " jmin = max(1, int(np.floor(s['y']-aperture_radius)))\n", " jmax = min(round(s['y']+aperture_radius)+1, img.shape[0])\n", " imin = max(1, int(np.floor(s['x']-aperture_radius)))\n", " imax = min(round(s['x']+aperture_radius)+1, img.shape[1])\n", " if (np.sum(img.dq[jmin:jmax, imin:imax] == 6) > 0):\n", " s['flag_' + rad_label] = 6\n", " elif (np.sum(img.dq[jmin:jmax, imin:imax] == 2) > 0):\n", " s['flag_' + rad_label] = 2\n", " elif (np.sum(img.dq[jmin:jmax, imin:imax] == 4) > 0):\n", " s['flag_' + rad_label] = 4 \n", "\n", " aperture_table['ra'] = aperture_table['x']\n", " aperture_table['dec'] = aperture_table['y']\n", " for a in aperture_table:\n", " rd = img.meta.wcs.transform(\"detector\", \"world\", a['x'], a['y']) \n", " a['ra'] = rd[0]\n", " if (a['ra'] > 180):\n", " a['ra'] -= 360.0\n", " a['dec'] = rd[1]\n", " \n", " return aperture_table" ] }, { "cell_type": "code", "execution_count": null, "id": "direct-click", "metadata": {}, "outputs": [], "source": [ "def prepare_phot_cal(cat_name, tmp, sel_radius):\n", " '''\n", " Function that cross-matches two catalogs and plot the positional residuals.\n", " To be used in the exercise.\n", " \n", " Inputs\n", " ----------\n", " cat_name : string\n", " The name of the pipeline source catalog\n", " tmp : Table\n", " The astro-photometric table.\n", " sel_radius : float\n", " The radius for the positional-residual selection in arcsec.\n", " \n", " Outputs\n", " ----------\n", " calib_cat : Table\n", " The pipeline source catalog.\n", " ind_i2d_cat: array(int)\n", " The array with the indexes from match_sky_coordinates.\n", " dist_2d: array(float)\n", " The array with the 2D distances from match_sky_coordinates.\n", " '''\n", " \n", " calib_cat = Table.read(cat_name)\n", "\n", " coord_cal = SkyCoord(ra=tmp['ra'], dec=tmp['dec'], unit=\"deg\")\n", " coord_i2d = SkyCoord(ra=calib_cat['sky_centroid'].ra.degree, dec=calib_cat['sky_centroid'].dec.degree, unit=\"deg\")\n", " ind_i2d_cat, dist_2d, a = match_coordinates_sky(coord_cal, coord_i2d)\n", "\n", " avg_dec = np.deg2rad((tmp['dec']+calib_cat[ind_i2d_cat]['sky_centroid'].dec.degree)/2.0)\n", " delta_ra = 3600.0*(tmp['ra']-calib_cat[ind_i2d_cat]['sky_centroid'].ra.degree)*np.cos(avg_dec)\n", "\n", " delta_dec = 3600.0*(tmp['dec']-calib_cat[ind_i2d_cat]['sky_centroid'].dec.degree)\n", "\n", " fig, ax = plt.subplots()\n", " ax.scatter(delta_ra, delta_dec, lw=0.5, s=5, color='black', marker='o')\n", " circle = plt.Circle((0, 0), sel_radius, color='r', fill=False)\n", " ax.add_patch(circle)\n", " ax.set_xlim(-0.5, 0.5)\n", " ax.set_ylim(-0.5, 0.5)\n", " ax.axhline(0, color='red', ls='--')\n", " ax.axvline(0, color='red', ls='--')\n", " ax.set_aspect('equal', 'box')\n", " ax.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [arcsec]}$')\n", " ax.set_ylabel(r'$\\Delta\\textrm{Dec. [arcsec]}$')\n", "\n", " ax2 = ax.secondary_xaxis('top', functions=(arcsec2pix, pix2arcsec))\n", " ax2.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [MIRIM pixel]}$')\n", " ay2 = ax.secondary_yaxis('right', functions=(arcsec2pix, pix2arcsec))\n", " ay2.set_ylabel(r'$\\Delta\\textrm{Dec. [MIRIM pixel]}$')\n", "\n", " plt.tight_layout()\n", "\n", " return calib_cat, ind_i2d_cat, dist_2d" ] }, { "cell_type": "markdown", "id": "281691fb", "metadata": {}, "source": [ "### 2.4-Setup PSF FWHM ###" ] }, { "cell_type": "code", "execution_count": null, "id": "1fd7676b", "metadata": {}, "outputs": [], "source": [ "filter_fwhm = {\n", " 'F560W': 1.636,\n", " 'F770W': 2.187\n", "}" ] }, { "cell_type": "markdown", "id": "bf4ad256", "metadata": {}, "source": [ "### 2.5-Download data ###" ] }, { "cell_type": "markdown", "id": "d7abbc17", "metadata": {}, "source": [ "Let's download and organize the data." ] }, { "cell_type": "code", "execution_count": null, "id": "b36e7d4c", "metadata": {}, "outputs": [], "source": [ "folder_path = './data/'\n", "\n", "if (folder_path == './'):\n", " print('')\n", " print(' Please set another folder that is not the main folder.')\n", " print('')\n", " pass\n", "elif os.path.isdir(folder_path):\n", " print('')\n", " print(' Images and catalogs should be already in \\'' + folder_path + \n", " '\\'. If not, delete the folder and run this cell again.')\n", " print('')\n", " pass\n", "elif (os.path.isfile('./stage2-selected.zip') and \n", " os.path.isfile('./stage3-selected.zip')):\n", " # Extract both zip files if they are present\n", " boxfile = './stage2-selected.zip'\n", " with zipfile.ZipFile(boxfile) as zf:\n", " zf.extractall(folder_path) \n", " boxfile = './stage3-selected.zip'\n", " with zipfile.ZipFile(boxfile) as zf:\n", " zf.extractall(folder_path)\n", "else:\n", " # Download the data and extract both zip files\n", " boxlink = 'https://stsci.box.com/shared/static/8pjjn8nnaf1d1mev98ca9kc6mss7prmt.zip'\n", " boxfile = 'stage2-selected.zip'\n", " urllib.request.urlretrieve(boxlink, boxfile)\n", " boxlink = 'https://stsci.box.com/shared/static/qstpome9vb95ay6aqlgvk0wvlku9ihay.zip'\n", " boxfile = 'stage3-selected.zip'\n", " urllib.request.urlretrieve(boxlink, boxfile)\n", " # Extract both zip files\n", " boxfile = './stage2-selected.zip'\n", " with zipfile.ZipFile(boxfile) as zf:\n", " zf.extractall(folder_path) \n", " boxfile = './stage3-selected.zip'\n", " with zipfile.ZipFile(boxfile) as zf:\n", " zf.extractall(folder_path)" ] }, { "cell_type": "code", "execution_count": null, "id": "db5da82c", "metadata": {}, "outputs": [], "source": [ "filter_names = ['F560W', 'F770W']\n", "\n", "img_names = {}\n", "cat_names = {}\n", "for f in filter_names:\n", " print('')\n", " print(r'{0}-filter images:'.format(f))\n", " names = glob.glob(folder_path + 'det_*' + f + '*_cal.fits')\n", " for n in range(len(names)):\n", " print(r' {0}) {1}'.format(n+1, names[n]))\n", " img_names[f] = names\n", " names = glob.glob(folder_path + 'complex_*' + f + '*cat*.ecsv')\n", " cat_names[f] = names\n", " print('')\n", " print(r'1 catalog found: {0}'.format(names[0]))" ] }, { "cell_type": "markdown", "id": "22a140f6", "metadata": {}, "source": [ "## 3-A first glimpse to the MIRI data ##" ] }, { "cell_type": "markdown", "id": "chinese-michael", "metadata": {}, "source": [ "Let's have a look at a F560W-filter image. We start by reading our image as a [JWST data model](https://jwst-pipeline.readthedocs.io/en/latest/jwst/datamodels/) and displaying it. Refer to the JWebbinar 1 ([Pipeline Information and Data Products](https://www.stsci.edu/jwst/science-execution/jwebbinars)) material for a detailed description of the JWST data models." ] }, { "cell_type": "code", "execution_count": null, "id": "8fb29d25", "metadata": {}, "outputs": [], "source": [ "img_F560W = datamodels.open(img_names['F560W'][1])" ] }, { "cell_type": "markdown", "id": "smart-sterling", "metadata": {}, "source": [ "First, we compute the $\\sigma$-clipped median and sigma of the image. These values are then used to set the color scale for the image. Then, we use the function _imshow_me_wcolorbar_ to show the image. This function uses standard matplotlib tools. Note that pixel values are in mega Jansky per steradian (MJy sr$^{-1}$)." ] }, { "cell_type": "code", "execution_count": null, "id": "ae9636c0", "metadata": {}, "outputs": [], "source": [ "_, med, sig = sigma_clipped_stats(img_F560W.data[np.isfinite(img_F560W.data)], sigma=5.0, maxiters=5)\n", "\n", "# Inputs: 2D array, min value, max value, title, x-axis label, y-axis label, colorbar label, colormap\n", "imshow_me_wcolorbar(img_F560W.data, med-1*sig, med+1*sig, 'Original image', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')" ] }, { "cell_type": "markdown", "id": "foster-enemy", "metadata": {}, "source": [ "### 3.1-Data Quality (DQ) flags ###" ] }, { "cell_type": "markdown", "id": "bff650ef", "metadata": {}, "source": [ "Not all pixels should be used. We can use the Data Quality (DQ) flags to assess whether a pixel can be considered in calculations or not. [Here](https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html#data-quality-flags) you can find a description of the DQ flags.\n", "\n", "There is no one-size-fits-all solution for selecting pixels using the DQ flags. For this specific exercise, let's keep all pixels with DQ flag equal to:\n", "\n", "- 0 = Good pixel\n", "- 2 = Pixel saturated during integration\n", "- 4 = Jump detected during integration\n", "- 6 = Combination of DQ flags 2 and 4\n", "\n", "As we can see, the DQ flags can correspond to multiple features. We can use _dqflags.dqflags_to_mnemonics_ to convert the DQ integer values into more user-friendly names:" ] }, { "cell_type": "code", "execution_count": null, "id": "a977bd55", "metadata": {}, "outputs": [], "source": [ "print(r' DQ flag equal to 2: {0}'.format(dqflags.dqflags_to_mnemonics(2, dqflags.group)))\n", "print(r' DQ flag equal to 4: {0}'.format(dqflags.dqflags_to_mnemonics(4, dqflags.group)))\n", "print(r' DQ flag equal to 6: {0}'.format(dqflags.dqflags_to_mnemonics(6, dqflags.group)))" ] }, { "cell_type": "markdown", "id": "demonstrated-raising", "metadata": {}, "source": [ "Let's flag all pixels with a DQ flag different from these four values:" ] }, { "cell_type": "code", "execution_count": null, "id": "6d181cb6", "metadata": {}, "outputs": [], "source": [ "ok = np.zeros(img_F560W.data.shape, dtype='int')\n", "for v in [0, 2, 4, 6]:\n", " ok = ok + np.where(img_F560W.dq == v, 1, 0)\n", "\n", "img_F560W.data[ok == 0] = np.nan\n", "\n", "print(r'{0} out of {1} pixels are not usable (~{2:3.1f}%)'.format((ok == 0).sum(), img_F560W.data.shape[0]*img_F560W.data.shape[1], \n", " (ok == 0).sum()/(img_F560W.data.shape[0]*img_F560W.data.shape[1])*100.0))" ] }, { "cell_type": "markdown", "id": "presidential-explosion", "metadata": {}, "source": [ "After masking, the same MIRI image looks different:" ] }, { "cell_type": "code", "execution_count": null, "id": "d88fb71e", "metadata": {}, "outputs": [], "source": [ "imshow_me_wcolorbar(img_F560W.data, med-1*sig, med+1*sig, 'Image after masking', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')" ] }, { "cell_type": "markdown", "id": "purple-specification", "metadata": {}, "source": [ "As we can notice, the regions associated to the 4-quadrant phase mask (4QPM) coronagraphs disappeared from the image. The optical system of the 4QPM coronographs is different from that of the imager. Because of the way these optical elements affect the light transmission, the calibration of the 4QPM-coronograph regions is complicated and specific for these coronographs. Therefore, even though photons are detected in the regions of the coronagraphs during standard imaging observations, these regions should not be used while analyzing the Stage-2 image. If you run the _calwebb_image3_ pipeline (or the resample step in _calwebb_image2_), you will notice that the 4QPM regions are missing in the resampled image (_i2d.fits_) as well.\n", "\n", "Another feature we can notice is that two columns (# 385 and 386) were flagged. Although a qualitative assessment of the image (for example with ds9) does not show anything particularly different from the other columns, these two columns showed to be coupled and should not be used for science." ] }, { "cell_type": "markdown", "id": "swiss-nebraska", "metadata": {}, "source": [ "## 4-From the image to the astro-photometric catalog ##" ] }, { "cell_type": "markdown", "id": "solid-median", "metadata": {}, "source": [ "There are various tools that can be used to perform aperture photometry on an image. In the following, we use [_photutils_](https://photutils.readthedocs.io/en/stable/index.html). Refer to the _photutils_ documentation for an extensive description of its tools." ] }, { "cell_type": "markdown", "id": "2ec8bee8", "metadata": {}, "source": [ "### 4.1-Sky background ###" ] }, { "cell_type": "markdown", "id": "aeef91de", "metadata": {}, "source": [ "The JWST background is dominated by emissions from the zodiacal cloud of the Solar System, the Milky Way and the telescope itself. See the [official documentation](https://jwst-docs.stsci.edu/jwst-observatory-functionality/jwst-background-model) for a detailed discussion on this topic.\n", "\n", "Sky background can be spatially variable in the image and subtracting the simple mean/median of all pixels would leave residuals in the image background. Thus, it is important to properly model and subtract the sky from each image. If dedicated background observations are available for your data set, you can use the pipeline _Background_ step. Otherwise, we can make a sky-background model from the image itself.\n", "\n", "The _photutils_ python package provides various options for subtracting [2D background](https://photutils.readthedocs.io/en/stable/background.html). The basic idea is to divide the image in NxM subregions, estimate the background in each region, and finally create the low-resolution background image with a median filter. A detailed description is provided in the [_Background2D_](https://photutils.readthedocs.io/en/stable/api/photutils.background.Background2D.html#photutils.background.Background2D) documentation.\n", "\n", "The choice of the input parameters for the _Background2D_ class is delicate. Some of the key input parameters to remember are the following:\n", "- box_size : the size (in pixels) of the box in which to estimate the background;\n", "- filter_size : the size of the window of the 2D median filter applied to the image to obtain the low-resolution background map;\n", "- sigma_clip : the sigma-clipping parameters;\n", "- bkg_estimator : the method used to compute the background;\n", "- coverage_mask : the mask that tells if a pixel should be masked and not used in the computation.\n", "\n", "These parameters should be fine tuned for every image according to some characteristics of the scene you are looking at, for example, the size of the sources, level of crowding, background gradient.\n", "\n", "Let's create a 2D background model and subtract it from our image:" ] }, { "cell_type": "code", "execution_count": null, "id": "774d7295", "metadata": {}, "outputs": [], "source": [ "# Mask all nan or inf pixels\n", "mask = np.full(np.shape(img_F560W.data), False, dtype=bool)\n", "mask[np.isnan(img_F560W.data)] = True\n", "mask[~np.isfinite(img_F560W.data)] = True\n", "\n", "sigma_clip = SigmaClip(sigma=3.0, maxiters=10)\n", "# This is the background estimator -> DAOPHOT MMM algorithm - \"mode\" = 3*median - 2*mean\n", "mmm_bkg = MMMBackground()\n", "# Compute sky background\n", "sky_F560W = Background2D(img_F560W.data, box_size=(20, 20), filter_size=(31, 31), \n", " sigma_clip=sigma_clip, bkg_estimator=mmm_bkg, coverage_mask=mask, fill_value=0.0)\n", "\n", "print(r'Median background: {0}'.format(sky_F560W.background_median))\n", "print(r'RMS background: {0}'.format(sky_F560W.background_rms_median))\n", "\n", "img_F560W_skysub = img_F560W.data - sky_F560W.background\n", "\n", "imshow_me_wcolorbar(img_F560W_skysub, -0.1, 0.25, 'Image sky subtracted',\n", " 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')" ] }, { "cell_type": "markdown", "id": "118daf81", "metadata": {}, "source": [ "### 4.2-Aperture photometry ###" ] }, { "cell_type": "markdown", "id": "369dd97a", "metadata": {}, "source": [ "After we have subtracted the background, we can search for sources in the image using [_DAOStarFinder_](https://photutils.readthedocs.io/en/stable/api/photutils.detection.DAOStarFinder.html#photutils.detection.DAOStarFinder), an implementation of the DAOFIND algorithm (Stetson 1987, PASP 99, 191).\n", "\n", "_DAOStarFinder_ searches an image for local density maxima that have a peak amplitude greater than a specific threshold (applied to a convolved image) and have size and shape similar to a defined 2D Gaussian kernel. For this example, we search for objects at least 5$\\times$ above the sky-background rms, and use as FWHM for the 2D Gaussian kernel the FWHM of the PSF in the F560W filter." ] }, { "cell_type": "code", "execution_count": null, "id": "bd829c71", "metadata": {}, "outputs": [], "source": [ "# 5 times the background rms\n", "threshold_F560W = 5.0*sky_F560W.background_rms_median\n", "\n", "# Filter-dependent FWHM from the PSF\n", "fwhm_F560W = filter_fwhm.get(img_F560W.meta.instrument.filter)\n", "\n", "# Create DAOStarFinder instance\n", "dsf_F560W = photutils.DAOStarFinder(threshold=threshold_F560W, fwhm=fwhm_F560W, exclude_border=True)\n", "\n", "# Run DAOStarFinder on the subtracted image and save the output in a table\n", "xy_F560W_tmp = dsf_F560W(img_F560W_skysub)\n", "\n", "# Print 10 lines of the table\n", "xy_F560W_tmp.pprint_all(max_lines=10)" ] }, { "cell_type": "markdown", "id": "ea80c422", "metadata": {}, "source": [ "The column description is almost straightforward. The three parameters that need further description are:\n", "- sharpness : (height of the central pixel - mean of the surrounding non-bad pixels) / (height of the best fitting Gaussian function at that point);\n", "- roundness1 : source symmetry;\n", "- roundness2 : (difference in the height of the best fitting Gaussian function in x - the height of the the best fitting Gaussian function in y) / (average of the best fitting Gaussian functions in x and y).\n", "\n", "These parameters can be used to discern between stars, galaxies or spurious detections. It is possible to setup _DAOStarFinder_ to exclude a priori objects outside a specific range of sharpness/roundness1/roundness2. Finally, note that the magnitude is defined as $-2.5\\log_{10}(\\text{peak density/detection threshold})$, which is just a rough estimate of the magnitude. An exaustive description of inputs and outputs is also provided [here](https://iraf.net/irafhelp.php?val=daofind).\n", "\n", "Let's plot what we have found. We can use _imshow_me_wcolorbar_setup_, which is a function similar to _imshow_me_wcolorbar_ we used before but it allows us to keep plotting." ] }, { "cell_type": "code", "execution_count": null, "id": "8fc14ff6", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "imshow_me_wcolorbar_setup(img_F560W_skysub, -0.1, 0.25, 'Image sky subtracted', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')\n", "ax.scatter(xy_F560W_tmp['xcentroid'], xy_F560W_tmp['ycentroid'], lw=0.5, s=15, marker='o', edgecolors='red', facecolors='none')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "8d528a42", "metadata": {}, "source": [ "Now we can perform our aperture photometry using the [_aperture_photometry_](https://photutils.readthedocs.io/en/latest/aperture.html#aperture-photometry-photutils-aperture) function in _photutils_.\n", "\n", "We need to store positions in a Nx2 array, with N being the number of stars found by _DAOStarFinder_. Then, we call _CircularAperture_, which defines a circular aperture for each star at position (x,y). We can define one or more aperture radii. For this example, let's use three radii based on the FWHM of the F560W-filter PSF:" ] }, { "cell_type": "code", "execution_count": null, "id": "735ee45e", "metadata": {}, "outputs": [], "source": [ "# Define the positions\n", "positions_F560W = np.stack((xy_F560W_tmp['xcentroid'], xy_F560W_tmp['ycentroid']), axis=-1)\n", "\n", "# Retrieve the aperture radii based on the FWHM of the F560W-filter PSF\n", "r0 = filter_fwhm.get(img_F560W.meta.instrument.filter)\n", "r1 = filter_fwhm.get(img_F560W.meta.instrument.filter)*2.0\n", "r2 = filter_fwhm.get(img_F560W.meta.instrument.filter)*3.0\n", "aper_radii = [r0, r1, r2]\n", "\n", "print(r'Aperture radii used:')\n", "print(r' r0 = {0:.3f} MIRIM pixel'.format(r0))\n", "print(r' r1 = {0:.3f} MIRIM pixel'.format(r1))\n", "print(r' r2 = {0:.3f} MIRIM pixel'.format(r2))\n", "print('')\n", "\n", "# Define the circular apertures\n", "circular_apertures = [CircularAperture(positions_F560W, r=r) for r in aper_radii]\n", "\n", "# Run the aperture photometry\n", "phot_F560W_tmp = aperture_photometry(img_F560W.data*img_F560W.area, circular_apertures, method='exact')\n", "\n", "# Print 10 lines\n", "phot_F560W_tmp.pprint_all(max_lines=10)" ] }, { "cell_type": "markdown", "id": "bef1d799", "metadata": {}, "source": [ "The three fluxes are saved in aperture_sum_0, aperture_sum_1 and aperture_sum_2. Positions are exactly the same given in input, i.e., the output of _DAOStarFinder_.\n", "\n", "
\n", "

Note

\n", "\n", "As you can notice, the aperture photometry is performed on the image values multiplied by _img_F560W.area_.\n", "\n", "The area subtended by each pixel on the sky varies as a result of geometric distortion and hence, they have different collecting power. Flat-fielding is designed to produce images with a flat background given a flat stimulus. When the flat-field correction is applied to a science image, the resulting image is characterized by constant surface brightness. However, flat-fielding does not take into account for the different collecting power of pixels due to geometric distortion. For more details, see the discussion here.\n", " \n", "Photometry measured on distorted images as the stage-2 _cal.fits_ files requires a field-dependent correction called the Pixel Area Map (PAM), to ensure that the same sources have the same magnitude regardless of their position within the images. The PAM is obtained from the geometric-distortion correction and is included in the _area_ extention of the JWST data model.\n", "\n", "\n", "
\n", "\n", "These fluxes obviously include the sky background. Using a global value for the sky is not ideal, even if we work with a sky-subtracted image, because there can be unsubtracted background residuals left. Thus, we evaluate the local background for each source using a [circular annulus](https://photutils.readthedocs.io/en/latest/aperture.html#local-background-subtraction) of inner and outer radii of 25 and 35 MIRIM pixels, respectively. We can also mask pixels before evaluating the local background." ] }, { "cell_type": "code", "execution_count": null, "id": "191e04c0", "metadata": {}, "outputs": [], "source": [ "# Define the annulus aperture\n", "annulus_aperture = CircularAnnulus(positions_F560W, r_in=25.0, r_out=35.0)\n", "\n", "# Define the mask with only pixels in each annulus\n", "annulus_mask = annulus_aperture.to_mask(method='center')\n", "\n", "# The local sky for each star will be stored in this list\n", "local_sky_median = []\n", "\n", "# For each source\n", "for mask in annulus_mask:\n", " # Multiply the pixel values by the mask. Since the mask is either 0 or 1, \n", " # the only non-zero pixels are those in the circular annulus\n", " annulus_data = mask.multiply(img_F560W.data*img_F560W.area)\n", " \n", " # Keep only non-masked pixels with finite values\n", " ok = np.logical_and(mask.data > 0, np.isfinite(annulus_data))\n", " \n", " # If there are not at least 10 usable pixels in the annulus to compute\n", " # the local median sky, flag the star and remove it from the final list later\n", " if (np.sum(ok) >= 10):\n", " # From 2D to 1D array\n", " annulus_data_1d = annulus_data[ok]\n", " # Sigma-clipped median\n", " _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d, sigma=3.5, maxiters=5)\n", " else:\n", " # Flagged value\n", " median_sigclip = -99.99\n", " local_sky_median.append(median_sigclip)\n", " \n", "# Convert list into array\n", "local_sky_median_F560W = np.array(local_sky_median)" ] }, { "cell_type": "markdown", "id": "8d9d3932", "metadata": {}, "source": [ "The work is almost complete. We can make some order and save the final astro-photometric catalog." ] }, { "cell_type": "code", "execution_count": null, "id": "dental-danger", "metadata": {}, "outputs": [], "source": [ "phot_F560W = Table()\n", "\n", "# Useful info from DAOStarFinder\n", "phot_F560W['x'] = xy_F560W_tmp['xcentroid']\n", "phot_F560W['y'] = xy_F560W_tmp['ycentroid']\n", "phot_F560W['sharpness'] = xy_F560W_tmp['sharpness']\n", "phot_F560W['roundness1'] = xy_F560W_tmp['roundness1']\n", "phot_F560W['roundness2'] = xy_F560W_tmp['roundness2']\n", "\n", "# Save an ID\n", "phot_F560W['id'] = xy_F560W_tmp['id']\n", "\n", "# Local median sky\n", "phot_F560W['annulus_median'] = local_sky_median_F560W" ] }, { "cell_type": "markdown", "id": "incomplete-carnival", "metadata": {}, "source": [ "The total background within a circular aperture is the median local background times the area of the circular aperture. The area is saved as an attribute of the circular aperture." ] }, { "cell_type": "code", "execution_count": null, "id": "93723960", "metadata": {}, "outputs": [], "source": [ "# Aperture photometry with r0\n", "phot_F560W['local_sky_r0'] = local_sky_median_F560W*circular_apertures[0].area\n", "phot_F560W['aperture_r0_skysub'] = phot_F560W_tmp['aperture_sum_0'] - phot_F560W['local_sky_r0']\n", "\n", "# Aperture photometry with r1\n", "phot_F560W['local_sky_r1'] = local_sky_median_F560W*circular_apertures[1].area\n", "phot_F560W['aperture_r1_skysub'] = phot_F560W_tmp['aperture_sum_1'] - phot_F560W['local_sky_r1']\n", "\n", "# Aperture photometry with r2\n", "phot_F560W['local_sky_r2'] = local_sky_median_F560W*circular_apertures[2].area\n", "phot_F560W['aperture_r2_skysub'] = phot_F560W_tmp['aperture_sum_2'] - phot_F560W['local_sky_r2']\n", "\n", "# Print 10 lines\n", "phot_F560W.pprint_all(max_lines=10, max_width=200)" ] }, { "cell_type": "markdown", "id": "ad88d4e9", "metadata": {}, "source": [ "Some stars are close to the edge of the MIRI imager and their flux cannot always be measured. Let's remove these stars:" ] }, { "cell_type": "code", "execution_count": null, "id": "148d61fa", "metadata": {}, "outputs": [], "source": [ "# Remove stars with flagged sky values\n", "phot_F560W = phot_F560W[phot_F560W['annulus_median'] > -90.0]\n", "\n", "# Remove all stars that do not have a finite flux within r0, r1 and r2.\n", "for r in [0, 1, 2]:\n", " keep_good = np.logical_and(np.isfinite(phot_F560W['aperture_r' + str(r) + '_skysub']), \n", " phot_F560W['aperture_r' + str(r) + '_skysub'] > 0.)\n", " phot_F560W = phot_F560W[keep_good]\n", "\n", "phot_F560W.pprint_all(max_lines=10, max_width=200)" ] }, { "cell_type": "markdown", "id": "7053efdf", "metadata": {}, "source": [ "We can compute the instrumental magnitude for each circular aperture:" ] }, { "cell_type": "code", "execution_count": null, "id": "07206d0e", "metadata": {}, "outputs": [], "source": [ "phot_F560W['mag_r0'] = -2.5*np.log10(phot_F560W['aperture_r0_skysub'])\n", "phot_F560W['mag_r1'] = -2.5*np.log10(phot_F560W['aperture_r1_skysub'])\n", "phot_F560W['mag_r2'] = -2.5*np.log10(phot_F560W['aperture_r2_skysub'])" ] }, { "cell_type": "markdown", "id": "b645d917", "metadata": {}, "source": [ "At the beginning, we used the DQ flags to mask some pixels and kept only those with DQ$=$0, 2, 4, 6. Although still perfectly usable, you might want to keep track of pixels that saturated during an integration or were hit by a cosmic ray. For this reason, we define a flag by checking all pixels within each aperture radius we used:" ] }, { "cell_type": "code", "execution_count": null, "id": "7ed6cad1", "metadata": {}, "outputs": [], "source": [ "phot_F560W['flag_r0'] = np.zeros(len(phot_F560W), dtype=int)\n", "phot_F560W['flag_r1'] = np.zeros(len(phot_F560W), dtype=int)\n", "phot_F560W['flag_r2'] = np.zeros(len(phot_F560W), dtype=int)\n", "\n", "for r, rl in zip([r0, r1, r2], ['r0', 'r1', 'r2']):\n", " for s in phot_F560W:\n", " jmin = max(1, int(np.floor(s['y']-r)))\n", " jmax = min(round(s['y']+r)+1, img_F560W.shape[0])\n", " imin = max(1, int(np.floor(s['x']-r)))\n", " imax = min(round(s['x']+r)+1, img_F560W.shape[1])\n", " if (np.sum(img_F560W.dq[jmin:jmax, imin:imax] == 6) > 0):\n", " s['flag_' + rl] = 6\n", " elif (np.sum(img_F560W.dq[jmin:jmax, imin:imax] == 2) > 0):\n", " s['flag_' + rl] = 2\n", " elif (np.sum(img_F560W.dq[jmin:jmax, imin:imax] == 4) > 0):\n", " s['flag_' + rl] = 4\n", " \n", "print(' Sources found: {0}'.format(len(phot_F560W)))\n", "print('Sources with \"SATURATED\" pixels within r2: {0}'.format(np.sum(np.logical_or(phot_F560W['flag_r2'] == 2, phot_F560W['flag_r2'] == 6))))\n", "print(' Sources with \"JUMP_DET\" pixels within r2: {0}'.format(np.sum(np.logical_or(phot_F560W['flag_r2'] == 4, phot_F560W['flag_r2'] == 6))))" ] }, { "cell_type": "markdown", "id": "98cc0315", "metadata": {}, "source": [ "Let's check if our flag works. For example, let's test the jump-detection flag:" ] }, { "cell_type": "code", "execution_count": null, "id": "db9b1a39", "metadata": {}, "outputs": [], "source": [ "# Flag for stars with \"jump\" pixel within r2 aperture\n", "jump = np.logical_or(phot_F560W['flag_r2'] == 4, phot_F560W['flag_r2'] == 6)\n", "\n", "fig, ax = plt.subplots()\n", "imshow_me_wcolorbar_setup(img_F560W_skysub, -0.1, 0.25, 'Stars with \"jump\" pixel within r2 aperture', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')\n", "ax.scatter(phot_F560W['x'], phot_F560W['y'], lw=0.5, s=15, marker='o', edgecolors='red', facecolors='none')\n", "ax.scatter(phot_F560W['x'][jump], phot_F560W['y'][jump], c='deepskyblue', lw=0, s=20)\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "empirical-reservoir", "metadata": {}, "source": [ "### 4.3-From pixel to equatorial coordinates ###" ] }, { "cell_type": "markdown", "id": "hollywood-medication", "metadata": {}, "source": [ "We can transform the (x,y) positions from the raw, MIRIM coordinate system to the Equatorial system (ICRS) by means of the WCS information stored in the ASDF extension of our image.\n", "\n", "
\n", "

Nota bene

\n", " \n", "For stage-2 images (_cal.fits_ files), the WCS information is assigned by the _assign_wcs_ step. This information is saved in an ASDF extension of the FITS file. However, image-display tools such as ds9 do not understand the ASDF encoding. For this reason, an approximation to the WCS is stored in the image header using the SIP (Simple Imaging Polynomial) convention. The SIP-based header does not provide an exact fit: it is accurate to a $\\sim$0.25-pixel level and it is meant for general display purposes (see here for more information).\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "north-tennis", "metadata": {}, "outputs": [], "source": [ "phot_F560W['ra'] = phot_F560W['x']\n", "phot_F560W['dec'] = phot_F560W['y']\n", "for a in phot_F560W:\n", " rd = img_F560W.meta.wcs.transform(\"detector\", \"world\", a['x'], a['y']) \n", " a['ra'] = rd[0]\n", " if (a['ra'] > 180):\n", " a['ra'] -= 360.0\n", " a['dec'] = rd[1]" ] }, { "cell_type": "markdown", "id": "048a0660", "metadata": {}, "source": [ "### 4.4-Photometric calibration ###" ] }, { "cell_type": "markdown", "id": "crazy-scottish", "metadata": {}, "source": [ "The photometric calibration can be simply performed by cross-matching our photometric catalog with that generated by the _calwebb_image3_ pipeline." ] }, { "cell_type": "code", "execution_count": null, "id": "starting-sheep", "metadata": {}, "outputs": [], "source": [ "calib_cat_F560W = Table.read(cat_names['F560W'][0])\n", "calib_cat_F560W.pprint_all(max_lines=1, max_width=200)" ] }, { "cell_type": "markdown", "id": "solid-commodity", "metadata": {}, "source": [ "For the cross-match of these catalogs, we can use [_match_coordinates_sky_](https://docs.astropy.org/en/stable/coordinates/matchsep.html#matching-catalogs). This function uses a proximity-based approach to find the same star in two catalogs. It requires coordinates to be stored as [_SkyCoord_](https://docs.astropy.org/en/stable/api/astropy.coordinates.SkyCoord.html#astropy.coordinates.SkyCoord) objects." ] }, { "cell_type": "code", "execution_count": null, "id": "delayed-feeding", "metadata": {}, "outputs": [], "source": [ "coord_cal = SkyCoord(ra=phot_F560W['ra'], dec=phot_F560W['dec'], unit=\"deg\")\n", "coord_i2d = SkyCoord(ra=calib_cat_F560W['sky_centroid'].ra.degree, dec=calib_cat_F560W['sky_centroid'].dec.degree, unit=\"deg\")\n", "ind_i2d_cat, dist_2d, _ = match_coordinates_sky(coord_cal, coord_i2d)" ] }, { "cell_type": "markdown", "id": "japanese-principal", "metadata": {}, "source": [ "The first output _ind_i2d_cat_ is an array containing the indexes of the stars in the pipeline source catalog that are the closest objects to each source in our F560W-filter catalog. The second output _dist_2d_ is an array with the on-sky distances between sources. We do not need to save the last output because it corresponds to an array of 3D distances between sources. We do not need it here.\n", "\n", "Being based just on proximity, this algorithm can present some issues in very crowded environments. It is always a good idea to check the result of the cross-match:" ] }, { "cell_type": "code", "execution_count": null, "id": "continent-tract", "metadata": {}, "outputs": [], "source": [ "avg_dec = np.deg2rad((phot_F560W['dec']+calib_cat_F560W[ind_i2d_cat]['sky_centroid'].dec.degree)/2.0)\n", "delta_ra = 3600.0*(phot_F560W['ra']-calib_cat_F560W[ind_i2d_cat]['sky_centroid'].ra.degree)*np.cos(avg_dec)\n", "\n", "delta_dec = 3600.0*(phot_F560W['dec'] - calib_cat_F560W[ind_i2d_cat]['sky_centroid'].dec.degree)\n", "\n", "fig, ax = plt.subplots()\n", "ax.scatter(delta_ra, delta_dec, lw=0.5, s=5, color='black', marker='o')\n", "circle = plt.Circle((0, 0), 0.055, color='r', fill=False)\n", "ax.add_patch(circle)\n", "ax.set_xlim(-0.5, 0.5)\n", "ax.set_ylim(-0.5, 0.5)\n", "ax.axhline(0, color='red', ls='--')\n", "ax.axvline(0, color='red', ls='--')\n", "ax.set_aspect('equal', 'box')\n", "ax.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [arcsec]}$')\n", "ax.set_ylabel(r'$\\Delta\\textrm{Dec. [arcsec]}$')\n", "\n", "ax2 = ax.secondary_xaxis('top', functions=(arcsec2pix, pix2arcsec))\n", "ax2.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [MIRIM pixel]}$')\n", "ay2 = ax.secondary_yaxis('right', functions=(arcsec2pix, pix2arcsec))\n", "ay2.set_ylabel(r'$\\Delta\\textrm{Dec. [MIRIM pixel]}$')\n", "\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "related-station", "metadata": {}, "source": [ "Let's define as \"found\" only objects with a positional rms lower than 0.5 MIRIM pixel, i.e., 0.055 arcsec." ] }, { "cell_type": "code", "execution_count": null, "id": "bizarre-mineral", "metadata": {}, "outputs": [], "source": [ "phot_F560W_matched = phot_F560W[dist_2d.arcsec/0.11 < 0.5]\n", "calib_cat_F560W_matched = calib_cat_F560W[ind_i2d_cat[dist_2d.arcsec/0.11 < 0.5]]" ] }, { "cell_type": "markdown", "id": "interesting-flexibility", "metadata": {}, "source": [ "Next, we need to compute the zero-point between the magnitudes in our custom _cal.fits_-based catalog and those in the pipeline source catalog. This simple step takes into account for (1) the finite-aperture correction and (2) the AB (or Vega) magnitude zero-point. The finite-aperture correction is necessary because the flux measured within a finite aperture is just a fraction of the total signal of a source. In this example, we calibrate our instrumental magnitude r1.\n", "\n", "We do not need to use all stars. It is preferred to use only bright sources since they should be well-measured in both catalogs. For this specific example, we only apply a magnitude-based selection. You can apply all necessary criteria to ensure the selection of the best objects for the task." ] }, { "cell_type": "code", "execution_count": null, "id": "polyphonic-valley", "metadata": {}, "outputs": [], "source": [ "# Magnitude limit\n", "ok = phot_F560W_matched['mag_r1'] < -3\n", "\n", "delta_mag = phot_F560W_matched['mag_r1'][ok]-calib_cat_F560W_matched['aper_total_abmag'][ok]\n", "\n", "_, mag_med_F560W, mag_rms_F560W = sigma_clipped_stats(delta_mag, sigma=2.0, maxiters=10)\n", "\n", "print('')\n", "print(r' Magnitude zero-point: {0:.3f} mag'.format(mag_med_F560W))" ] }, { "cell_type": "markdown", "id": "2476427d", "metadata": {}, "source": [ "Let's plot the $\\Delta$mag as a function of instrumental magnitude r1." ] }, { "cell_type": "code", "execution_count": null, "id": "79362809", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ax.scatter(phot_F560W_matched['mag_r1'], phot_F560W_matched['mag_r1']-calib_cat_F560W_matched['aper_total_abmag'], \n", " lw=0.5, s=5, color='black', marker='o')\n", "ax.set_xlim(-8, -2)\n", "ax.set_ylim(-26, -24)\n", "ax.axhline(mag_med_F560W, color='red', ls='--')\n", "ax.set_xlabel(r'F560W mag')\n", "ax.set_ylabel(r'$\\Delta$mag')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "proper-proof", "metadata": {}, "source": [ "Finally, the calibrate AB mag of our stars is simply:" ] }, { "cell_type": "code", "execution_count": null, "id": "dirty-spell", "metadata": {}, "outputs": [], "source": [ "phot_F560W['ABmag'] = phot_F560W['mag_r1'] - mag_med_F560W" ] }, { "cell_type": "markdown", "id": "genuine-productivity", "metadata": {}, "source": [ "The same procedure can be used to convert our instrumental magnitudes in the Vega system." ] }, { "cell_type": "markdown", "id": "honey-quick", "metadata": {}, "source": [ "
\n", "

Aperture photometry on stage-3 images

\n", " \n", "This notebook can be adapted to perform aperture photometry on stage-3 (_i2d.fits_) images. In this case, with a few expedients the photometric calibration does not need additional files.\n", " \n", "First, we choose an aperture radius from the aperture-correction reference file available in the JWST Calibration Reference Data System (CRDS). For each filter (FILTER), array/subarray (PUPIL) and encircled-energy values (EEFRACTION), this file lists the aperture-correction values needed for correcting observed signals within a finite aperture to the estimated total signal for a source (APCORR), the aperture radius containing the EEFRACTION fraction of the total flux (RADIUS), the inner (SKYIN) and outer (SKYOUT) radii for the sky estimation. The APCORR value is a multiplicative correction that scales the measured flux to infinite aperture.\n", "\n", "If we perform aperture photometry using these values of RADIUS, SKYIN and SKYOUT, then, once corrected the flux for the finite aperture, the photometric calibration is straighforward. Indeed, as for stage-2 images, the pixel unit in the stage-3 images is MJy sr$^{-1}$. This is an advantage for the photometric calibration because we can directly obtain magnitudes in the AB system (Oke 1964; see also Sect. 7 of Sirianni et al. 2005). The AB magnitude is defined as:\n", "\n", "\\begin{equation*}\n", " m_{\\rm AB} = -2.5 \\log_{10} f_\\nu - 48.60\n", "\\end{equation*}\n", "\n", "with $f_\\nu$ in erg s$^{-1}$ cm$^{-2}$ Hz$^{-1}$, or as:\n", "\n", "\\begin{equation*}\n", " m_{\\rm AB} = -2.5 \\log_{10} f_\\nu + 8.90\n", "\\end{equation*}\n", "\n", "with $f_\\nu$ in Jansky. Therefore, we can simply transform our total flux in MJy sr$^{-1}$ by multiplying it by $10^6$ and by the average pixel area in steradian. The header keyword _PIXAR_SR_ contains the average pixel area in steradian; the corresponding value in the JWST data-model scheme is included in _meta.photometry.pixelarea_steradians_. This value is added in the flux (photometric) calibrations performed by the _Photom_ step of the _calwebb_image2_ pipeline.\n", "\n", "AB magnitudes can be converted to VEGA magnitudes by adding the AB-to-Vega magnitude offset available in the corresponding reference file in the CRDS.\n", " \n", "
\n", "

Warning 1

\n", "\n", "The APCORR values include a correction for the sky-background subtraction obtained with the specific SKYIN and SKYOUT radii provided in the CRDS reference file. This correction takes into account the contribution of the PSF wings of the star to the local background. If different sky-background radii are used, the APCORR values cannot be used anymore. The best alternative option is to evaluate the sky background in an annulus far enough from the center of each star that the contribution of the PSF wings of the star to the local background is negligible, for example between 25 and 35 pixels as shown in this notebook. In this case, the total flux of a star can be obtained from the flux containging EEFRACTION of the total flux by multiplying it by EEFRACTION$^{-1}$.\n", "\n", "
\n", " \n", "
\n", "

Warning 2

\n", "\n", "No pixel-area map correction is needed with stage-3 images.\n", "\n", "
\n", "\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "proof-elephant", "metadata": {}, "source": [ "## 5-Exercise ##" ] }, { "cell_type": "markdown", "id": "brutal-device", "metadata": {}, "source": [ "Let's repeat some steps discussed before for another image in F770W filter. First, let's call this routine to read a new image. For this exercise, a random gradient has been added to the image so we are forced to find the new correct setup for _Background2D_. _This is not how a real image will look like_." ] }, { "cell_type": "code", "execution_count": null, "id": "infectious-bolivia", "metadata": {}, "outputs": [], "source": [ "img_F770W, img_F770W_mod, cmin, cmax = prepare_image(img_names['F770W'][1])" ] }, { "cell_type": "markdown", "id": "extended-pillow", "metadata": {}, "source": [ "We can use these parameters as initial guesses for the colorbar." ] }, { "cell_type": "code", "execution_count": null, "id": "intimate-characterization", "metadata": {}, "outputs": [], "source": [ "imshow_me_wcolorbar(img_F770W_mod, cmin, cmax, 'F770W-filter image', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')" ] }, { "cell_type": "markdown", "id": "thick-people", "metadata": {}, "source": [ "The first step we need to do is to subtract the sky background. We are going to use the img_F770W_mod 2D array for this part of the exercise. Try to setup _Background2D_ with the correct parameters." ] }, { "cell_type": "code", "execution_count": null, "id": "textile-semiconductor", "metadata": {}, "outputs": [], "source": [ "mask = np.full(np.shape(img_F770W.data), False, dtype=bool)\n", "mask[np.isnan(img_F770W_mod)] = True\n", "mask[~np.isfinite(img_F770W_mod)] = True\n", "\n", "sigma_clip = SigmaClip(sigma=3.0, maxiters=10)\n", "mmm_bkg = MMMBackground()\n", "# Complete the line below\n", "sky_F770W = Background2D(img_F770W_mod, box_size=(20, 20), filter_size=(11, 11), sigma_clip=sigma_clip, bkg_estimator=mmm_bkg, coverage_mask=mask, fill_value=0.0)\n", "\n", "print(r'Median background: {0}'.format(sky_F770W.background_median))\n", "print(r'RMS background: {0}'.format(sky_F770W.background_rms_median))\n", "\n", "img_F770W_skysub = img_F770W_mod - sky_F770W.background\n", "\n", "imshow_me_wcolorbar(img_F770W_skysub, -0.1, 0.1, 'F770W-filter sky-subtracted image', 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')" ] }, { "cell_type": "markdown", "id": "capable-grocery", "metadata": {}, "source": [ "Now we can use _DAOStarFinder_ to find sources in our image (keep using the img_F770W_mod 2D array)." ] }, { "cell_type": "code", "execution_count": null, "id": "naval-polls", "metadata": {}, "outputs": [], "source": [ "threshold_F770W = 5.0*sky_F770W.background_rms_median\n", "fwhm_F770W = filter_fwhm.get(img_F770W.meta.instrument.filter)\n", "\n", "dsf = photutils.DAOStarFinder(threshold=threshold_F770W, fwhm=fwhm_F770W, exclude_border=True)\n", "xy_F770W_tmp = dsf(img_F770W_skysub)\n", "\n", "xy_F770W_tmp.pprint_all(max_lines=10)" ] }, { "cell_type": "markdown", "id": "vertical-bones", "metadata": {}, "source": [ "Now perform aperture photometry on this image using an aperture radius equal to 2$\\times$ the PSF FWHM in this filter. We can now use the original img_F770W data model for the remaining parts of the exercise." ] }, { "cell_type": "code", "execution_count": null, "id": "sufficient-importance", "metadata": {}, "outputs": [], "source": [ "positions_F770W = np.stack((xy_F770W_tmp['xcentroid'], xy_F770W_tmp['ycentroid']), axis=-1)\n", "\n", "r0 = filter_fwhm.get(img_F770W.meta.instrument.filter)*2.0\n", "circular_apertures = CircularAperture(positions_F770W, r=r0)\n", "phot_F770W_tmp = aperture_photometry(img_F770W.data*img_F770W.area, circular_apertures, method='exact')\n", "\n", "annulus_aperture = CircularAnnulus(positions_F770W, r_in=25.0, r_out=35.0)\n", "annulus_mask = annulus_aperture.to_mask(method='center')\n", "local_sky_median = []\n", "for mask in annulus_mask:\n", " annulus_data = mask.multiply(img_F770W.data*img_F770W.area)\n", " ok = np.logical_and(mask.data > 0, np.isfinite(annulus_data))\n", " if (np.sum(ok) >= 10):\n", " annulus_data_1d = annulus_data[ok]\n", " _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d, sigma=3.5, maxiters=5)\n", " else:\n", " median_sigclip = -99.99\n", " local_sky_median.append(median_sigclip)\n", "local_sky_median_F770W = np.array(local_sky_median)\n", "\n", "# Complete the two lines below\n", "sky_F770W = local_sky_median_F770W*circular_apertures.area\n", "flux_F770W = phot_F770W_tmp['aperture_sum'] - sky_F770W" ] }, { "cell_type": "markdown", "id": "perfect-batch", "metadata": {}, "source": [ "The following function prepares a table similar to the photometric table created for the F560W-filter image." ] }, { "cell_type": "code", "execution_count": null, "id": "charitable-theology", "metadata": {}, "outputs": [], "source": [ "phot_F770W = prepare_table(img_F770W, xy_F770W_tmp, r0, sky_F770W, flux_F770W, 'r0')\n", "\n", "phot_F770W.pprint_all(max_lines=10, max_width=200)\n", "\n", "print('')\n", "print(' Sources found: {0}'.format(len(phot_F770W)))\n", "print('Sources with \"SATURATED\" pixels within r0: {0}'.format(np.sum(np.logical_or(phot_F770W['flag_r0'] == 2, phot_F770W['flag_r0'] == 6))))\n", "print(' Sources with \"JUMP_DET\" pixels within r0: {0}'.format(np.sum(np.logical_or(phot_F770W['flag_r0'] == 4, phot_F770W['flag_r0'] == 6))))" ] }, { "cell_type": "markdown", "id": "another-teens", "metadata": {}, "source": [ "Finally, calibrate the instrumental magnitudes using the pipeline source catalog." ] }, { "cell_type": "code", "execution_count": null, "id": "frequent-airport", "metadata": {}, "outputs": [], "source": [ "calib_cat_F770W, ind_i2d_cat, dist_2d = prepare_phot_cal(cat_names['F770W'][0], phot_F770W, 0.055)" ] }, { "cell_type": "code", "execution_count": null, "id": "unknown-ladder", "metadata": {}, "outputs": [], "source": [ "# Complete the two lines below\n", "phot_F770W_matched = phot_F770W[dist_2d.arcsec/0.11 < 0.5]\n", "calib_cat_F770W_matched = calib_cat_F770W[ind_i2d_cat[dist_2d.arcsec/0.11 < 0.5]]" ] }, { "cell_type": "markdown", "id": "c693769f", "metadata": {}, "source": [ "Compute the zero-point for the photometric calibration." ] }, { "cell_type": "code", "execution_count": null, "id": "3f0b3f0f", "metadata": {}, "outputs": [], "source": [ "# Complete the line below\n", "ok = phot_F770W_matched['mag_r0'] < -3.5\n", "\n", "delta_mag = phot_F770W_matched['mag_r0'][ok]-calib_cat_F770W_matched['aper_total_abmag'][ok]\n", "\n", "# Complete the line below\n", "_, mag_med_F770W, mag_rms_F770W = sigma_clipped_stats(delta_mag, sigma=2.0, maxiters=10)\n", "\n", "print('')\n", "print(r' Magnitude zero-point: {0:.3f} mag'.format(mag_med_F770W))\n", "\n", "phot_F770W['ABmag'] = phot_F770W['mag_r0'] - mag_med_F770W" ] }, { "cell_type": "markdown", "id": "lasting-responsibility", "metadata": {}, "source": [ "Plot $\\Delta$mag as a function of instrumental magnitude." ] }, { "cell_type": "code", "execution_count": null, "id": "cardiac-specification", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ax.scatter(phot_F770W_matched['mag_r0'], phot_F770W_matched['mag_r0']-calib_cat_F770W_matched['aper_total_abmag'], \n", " lw=0.5, s=5, color='black', marker='o')\n", "ax.set_xlim(-8, -2)\n", "ax.set_ylim(-26, -24)\n", "ax.axhline(mag_med_F770W, color='red', ls='--')\n", "ax.set_xlabel(r'F770W mag')\n", "ax.set_ylabel(r'$\\Delta$mag')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "working-heater", "metadata": {}, "source": [ "Plot all sources found." ] }, { "cell_type": "code", "execution_count": null, "id": "democratic-advocacy", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "imshow_me_wcolorbar_setup(img_F770W_skysub, -0.1, 0.25, 'Sourced found in this {0}-filter image'.format(img_F770W.meta.instrument.filter), \n", " 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')\n", "ax.scatter(phot_F770W['x'], phot_F770W['y'], lw=0.5, s=15, marker='o', edgecolors='red', facecolors='none')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "thrown-vanilla", "metadata": {}, "source": [ "## 6-Bonus \\#1: your first JWST mid-infrared color-magnitude diagram ##" ] }, { "cell_type": "markdown", "id": "incorrect-relationship", "metadata": {}, "source": [ "We have built two catalogs for the same field in two filters. We can combine the photometry in these catalogs to create our first mid-infrared color-magnitude diagram." ] }, { "cell_type": "code", "execution_count": null, "id": "fourth-average", "metadata": {}, "outputs": [], "source": [ "coord_F560W = SkyCoord(ra=phot_F560W['ra'], dec=phot_F560W['dec'], unit=\"deg\")\n", "coord_F770W = SkyCoord(ra=phot_F770W['ra'], dec=phot_F770W['dec'], unit=\"deg\")\n", "ind_F770W_cat, dist_2d, a = match_coordinates_sky(coord_F560W, coord_F770W)" ] }, { "cell_type": "markdown", "id": "coupled-newfoundland", "metadata": {}, "source": [ "Let's check the result of the cross-match:" ] }, { "cell_type": "code", "execution_count": null, "id": "acquired-india", "metadata": {}, "outputs": [], "source": [ "avg_dec = np.deg2rad((phot_F560W['dec']+phot_F770W['dec'][ind_F770W_cat])/2.0)\n", "delta_ra = 3600.0*(phot_F560W['ra']-phot_F770W['ra'][ind_F770W_cat])*np.cos(avg_dec)\n", "\n", "delta_dec = 3600.0*(phot_F560W['dec']-phot_F770W['dec'][ind_F770W_cat])\n", "\n", "fig, ax = plt.subplots()\n", "ax.scatter(delta_ra, delta_dec, lw=0.5, s=5, color='black', marker='o')\n", "circle = plt.Circle((0, 0), 0.055, color='r', fill=False)\n", "ax.add_patch(circle)\n", "ax.set_xlim(-0.5, 0.5)\n", "ax.set_ylim(-0.5, 0.5)\n", "ax.axhline(0, color='red', ls='--')\n", "ax.axvline(0, color='red', ls='--')\n", "ax.set_aspect('equal', 'box')\n", "ax.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [arcsec]}$')\n", "ax.set_ylabel(r'$\\Delta\\textrm{Dec. [arcsec]}$')\n", "\n", "ax2 = ax.secondary_xaxis('top', functions=(arcsec2pix, pix2arcsec))\n", "ax2.set_xlabel(r'$\\Delta\\textrm{(R.A.}\\cos\\textrm{Dec.) [MIRIM pixel]}$')\n", "ay2 = ax.secondary_yaxis('right', functions=(arcsec2pix, pix2arcsec))\n", "ay2.set_ylabel(r'$\\Delta\\textrm{Dec. [MIRIM pixel]}$')\n", "\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "quality-brooks", "metadata": {}, "source": [ "Let's define as \"found\" only objects with a positional rms lower than 0.5 MIRIM pixel, i.e., 0.055 arcsec:" ] }, { "cell_type": "code", "execution_count": null, "id": "pretty-liabilities", "metadata": {}, "outputs": [], "source": [ "phot_F560W_matched = phot_F560W[dist_2d.arcsec/0.11 < 0.5]\n", "phot_F770W_matched = phot_F770W[ind_F770W_cat[dist_2d.arcsec/0.11 < 0.5]]" ] }, { "cell_type": "markdown", "id": "promising-crossing", "metadata": {}, "source": [ "Finally, we can make our first mid-infrared color-magnitude diagram:" ] }, { "cell_type": "code", "execution_count": null, "id": "db7be4e5-da00-4bd5-8475-10ac2c0c4297", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ax.set_aspect(0.35)\n", "ax.scatter(phot_F560W_matched['ABmag']-phot_F770W_matched['ABmag'], \n", " phot_F770W_matched['ABmag'], lw=0.5, s=5, marker='o', color='black')\n", "ax.set_xlim(-01.0, 0.0)\n", "ax.set_ylim(24, 17)\n", "ax.set_xlabel(r'$(m_{\\textrm{F560W}}-m_{\\textrm{F770W}})$')\n", "ax.set_ylabel(r'$m_{\\textrm{F770W}}$')\n", "ax.set_title(r'Your first Mid-Infrared CMD')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "d2be9f88", "metadata": {}, "source": [ "## 7-Bonus \\#2: concentration indexes ##" ] }, { "cell_type": "markdown", "id": "d62d5760", "metadata": {}, "source": [ "As we have seen in some plots above, our sample of detected sources is a mix of stars, galaxies and spurious detections. We can use some parameters provided by _DAOStarFinder_ to discern between these objects, but we can also compute our own diagnostic tools. For example, in analogy with what the JWST _calwebb_image3_ pipeline does, we can compute concentration indexes. Let's work with the F560W-filter catalog:" ] }, { "cell_type": "code", "execution_count": null, "id": "b19fe783", "metadata": {}, "outputs": [], "source": [ "phot_F560W['CI_1_0'] = phot_F560W['aperture_r1_skysub']/phot_F560W['aperture_r0_skysub']\n", "phot_F560W['CI_2_0'] = phot_F560W['aperture_r2_skysub']/phot_F560W['aperture_r0_skysub']\n", "phot_F560W['CI_2_1'] = phot_F560W['aperture_r2_skysub']/phot_F560W['aperture_r1_skysub']" ] }, { "cell_type": "markdown", "id": "6e6876b8", "metadata": {}, "source": [ "Regardless of the total flux of a source, the ratio between fluxes measured within two different apertures should be different for stars, galaxies or spurious objects. For example, galaxies have likely a broader profile than stars. Therefore, we can use this piece of information to discern between these objects." ] }, { "cell_type": "code", "execution_count": null, "id": "5fb454aa", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(3, 1)\n", "ax[0].scatter(phot_F560W['ABmag'], phot_F560W['CI_1_0'], lw=0, s=5, marker='o', color='black')\n", "ax[0].set_xlim(17.9, 26)\n", "ax[0].set_ylim(-1, 6)\n", "ax[0].set_xlabel(r'$m_{\\rm F560W}$')\n", "ax[0].set_ylabel(r'CI\\_1\\_0')\n", "ax[1].scatter(phot_F560W['ABmag'], phot_F560W['CI_2_1'], lw=0, s=5, marker='o', color='black')\n", "ax[1].set_xlim(17.9, 26)\n", "ax[1].set_ylim(-1, 6)\n", "ax[1].set_xlabel(r'$m_{\\rm F560W}$')\n", "ax[1].set_ylabel(r'CI\\_2\\_1')\n", "ax[2].scatter(phot_F560W['CI_1_0'], phot_F560W['CI_2_1'], lw=0, s=5, marker='o', color='black')\n", "ax[2].set_xlim(0, 5.5)\n", "ax[2].set_ylim(-1, 5.5)\n", "ax[2].set_xlabel(r'CI\\_1\\_0')\n", "ax[2].set_ylabel(r'CI\\_2\\_1')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "649cd163", "metadata": {}, "source": [ "You can notice in the bottom panel that most points seem to cluster in a specific region. We can define a threshold to define two groups of objects (\"ok\" and \"notok\")." ] }, { "cell_type": "code", "execution_count": null, "id": "fbbd6d19", "metadata": {}, "outputs": [], "source": [ "xint = [0.6, 1., 1.3, 1.7, 1.8, 2]\n", "yint = [0, 1.5, 1.8, 1.9, 1.4, 0]\n", "ci_lim = np.interp(phot_F560W['CI_1_0'], xint, yint)\n", "\n", "notok = phot_F560W['CI_2_1'] > ci_lim\n", "ok = phot_F560W['CI_2_1'] < ci_lim" ] }, { "cell_type": "markdown", "id": "ed067387", "metadata": {}, "source": [ "Let's look at the same plots with the two groups highlighted in different colors." ] }, { "cell_type": "code", "execution_count": null, "id": "8870fa8b", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(3, 1)\n", "ax[0].scatter(phot_F560W['ABmag'], phot_F560W['CI_1_0'], lw=0, s=5, marker='o', color='black')\n", "ax[0].scatter(phot_F560W['ABmag'][notok], phot_F560W['CI_1_0'][notok], lw=0, s=5, marker='o', color='red')\n", "ax[0].set_xlim(17.9, 26)\n", "ax[0].set_ylim(-1, 6)\n", "ax[0].set_xlabel(r'$m_{\\rm F560W}$')\n", "ax[0].set_ylabel(r'CI\\_1\\_0')\n", "ax[1].scatter(phot_F560W['ABmag'], phot_F560W['CI_2_1'], lw=0, s=5, marker='o', color='black')\n", "ax[1].scatter(phot_F560W['ABmag'][notok], phot_F560W['CI_2_1'][notok], lw=0, s=5, marker='o', color='red')\n", "ax[1].set_xlim(17.9, 26)\n", "ax[1].set_ylim(-1, 6)\n", "ax[1].set_xlabel(r'$m_{\\rm F560W}$')\n", "ax[1].set_ylabel(r'CI\\_2\\_1')\n", "ax[2].scatter(phot_F560W['CI_1_0'], phot_F560W['CI_2_1'], lw=0, s=5, marker='o', color='black')\n", "ax[2].scatter(phot_F560W['CI_1_0'][notok], phot_F560W['CI_2_1'][notok], lw=0, s=5, marker='o', color='red')\n", "ax[2].plot(xint, yint, color='red')\n", "ax[2].set_xlim(0, 5.5)\n", "ax[2].set_ylim(-1, 5.5)\n", "ax[2].set_xlabel(r'CI\\_1\\_0')\n", "ax[2].set_ylabel(r'CI\\_2\\_1')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "5e9be54a", "metadata": {}, "source": [ "Let's go back to the previous plot and uncomment some lines.\n", "\n", "We can now have a closer look at a few random objects that have been rejected using the concentration indexes. We can re-run this cell many times to see a variety of objects." ] }, { "cell_type": "code", "execution_count": null, "id": "ac9cddf9", "metadata": {}, "outputs": [], "source": [ "id_sel = []\n", "while (True):\n", " n = random.choice(phot_F560W[notok]['id'])\n", " if (n not in id_sel):\n", " id_sel.append(n)\n", " if (len(id_sel) == 4):\n", " break\n", "id_sel = np.array(id_sel)\n", "\n", "imshow_cutouts(img_F560W_skysub, phot_F560W, id_sel, '$(x-x_{0})$ [MIRIM pixel]', '$(y-y_{0})$ [MIRIM pixel]', 'binary')" ] }, { "cell_type": "markdown", "id": "bcfb5c6c", "metadata": {}, "source": [ "As we can see, our concentration-based selection is not perfect: sometimes we flag real faint stars as galaxies/spurious detections, sometimes we keep galaxies in our star sample. As in many cases, the choice of the best parameters for the purge is a tradeoff." ] }, { "cell_type": "code", "execution_count": null, "id": "3ded9f71", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "imshow_me_wcolorbar_setup(img_F560W_skysub, -0.1, 0.25, 'Concentration-index selection', \n", " 'x [MIRIM pixel]', 'y [MIRIM pixel]', 'MJy sr$^{-1}$', 'binary')\n", "ax.scatter(phot_F560W['x'][ok], phot_F560W['y'][ok], lw=0.5, s=15, marker='o', edgecolors='red', facecolors='none')\n", "ax.scatter(phot_F560W['x'][notok], phot_F560W['y'][notok], lw=0.5, s=15, marker='o', edgecolors='deepskyblue', facecolors='none')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "6ffe509d", "metadata": {}, "source": [ "We are done! Here there is a bonus plot!" ] }, { "cell_type": "code", "execution_count": null, "id": "751642d7", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "fmin = np.min(phot_F560W['aperture_r2_skysub'])\n", "fmax = np.max(phot_F560W['aperture_r2_skysub'])\n", "size = 1.e4*(phot_F560W['aperture_r2_skysub']-fmin)/(fmax-fmin)\n", "ax.scatter(phot_F560W['ra'][notok], phot_F560W['dec'][notok], s=size[notok], alpha=0.25, marker='.', color='deepskyblue')\n", "ax.scatter(phot_F560W['ra'][ok], phot_F560W['dec'][ok], s=size[ok], alpha=0.25, marker='.', color='red')\n", "ax.set_xlim(0.025, -0.013)\n", "ax.set_ylim(-0.0215, 0.0165)\n", "ax.set_xlabel(r'R.A. [deg]')\n", "ax.set_ylabel(r'Dec. [deg]')\n", "ax.text(0.3, 1.015, 'OK sources', verticalalignment='center', horizontalalignment='center', transform=ax.transAxes, color='red')\n", "ax.text(0.7, 1.015, 'not-OK sources', verticalalignment='center', horizontalalignment='center', transform=ax.transAxes, color='deepskyblue')\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "packed-snake", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "id": "current-veteran", "metadata": {}, "source": [ "\"Space" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.13" } }, "nbformat": 4, "nbformat_minor": 5 }