{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Plotting the DC2 Run2.1i skyMap\n", "
Owner: **Jim Chiang** ([@jchiang87](https://github.com/LSSTDESC/DC2-analysis/issues/new?body=@jchiang87))\n", "
Last Verified to Run: **2018-11-11** (by @yymao)\n", "\n", "In this notebook, we show how to use the data butler to obtain information on the skyMap used in the coadd analyses performed by the DRP pipeline. These skyMaps are composed of tracts and patches on the sky. Each tract is a rectangular region of the sky with a common map projection; each tract is further divided into rectangular patches, which use the same tract coordinate system and which are a convenient size for processing the coadd data. A more complete description of the skyMap geometry is given in the HSC Software Pipeline paper ([Bosch et al. 2017](https://arxiv.org/abs/1705.06766)).\n", "\n", "Equipped with the info from the skyMap, we plot the tracts and patches that were used with the Run2.1p processing and overlay the WFD and uDDF simulation regions. We also use the butler to access the visit-level data and show how one can access the calexp (calibrated exposure) image data to obtain the PSF, zero-point, etc. as measured by the Stack for a given exposure. Finally, we show how to plot the sky region imaged on the focal plane for a given visit in two ways: the first using the CCD coordinates available from the calexps and the other using the lsst_sims code to compute those coordinates from the pointing information for the visit.\n", "\n", "## Set Up" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import glob\n", "import warnings\n", "import sqlite3\n", "import re\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.path import Path\n", "import matplotlib.patches as patches\n", "%matplotlib inline\n", "\n", "import lsst.afw.geom as afw_geom\n", "import lsst.afw.cameraGeom as cameraGeom\n", "import lsst.daf.persistence as dp\n", "# The lsst_sims code issues some ignorable warnings regarding ids used for querying the object\n", "# databases.\n", "with warnings.catch_warnings():\n", " warnings.simplefilter(\"ignore\")\n", " import lsst.sims.coordUtils\n", " from lsst.sims.catUtils.utils import ObservationMetaDataGenerator\n", " from lsst.sims.utils import getRotSkyPos\n", " \n", "from desc_dc2_dm_data import REPOS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotting Functions\n", "First, we define some functions to plot the tract, patch, and CCD regions on the sky. These are copied from [example code](https://github.com/yalsayyad/dm_notebooks/blob/master/desc-ssim/DESC-SSim%20Patch%20Geometry.ipynb) that Yusra AlSayyad presented at the [2017-06-29 SSim meeting](https://confluence.slac.stanford.edu/pages/viewpage.action?pageId=224461017)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def make_patch(vertexList, wcs=None):\n", " \"\"\"\n", " Return a Path in sky coords from vertex list in pixel coords.\n", " \n", " Parameters\n", " ----------\n", " vertexList: list of coordinates\n", " These are the corners of the region to be plotted either in pixel coordinates or\n", " sky coordinates.\n", " wcs: lsst.afw.geom.skyWcs.skyWcs.SkyWcs [None]\n", " The WCS object used to convert from pixel to sky coordinates.\n", "\n", " Returns\n", " -------\n", " matplotlib.path.Path: The encapsulation of the vertex info that matplotlib uses to\n", " plot a patch.\n", " \"\"\"\n", " if wcs is not None:\n", " skyPatchList = [wcs.pixelToSky(pos).getPosition(afw_geom.degrees)\n", " for pos in vertexList]\n", " else:\n", " skyPatchList = vertexList\n", " verts = [(coord[0], coord[1]) for coord in skyPatchList]\n", " verts.append((0,0))\n", " codes = [Path.MOVETO,\n", " Path.LINETO,\n", " Path.LINETO,\n", " Path.LINETO,\n", " Path.CLOSEPOLY,\n", " ]\n", " return Path(verts, codes)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_skymap_tract(skyMap, tract=0, title=None, ax=None):\n", " \"\"\"\n", " Plot a tract from a skyMap.\n", " \n", " Parameters\n", " ----------\n", " skyMap: lsst.skyMap.SkyMap\n", " The SkyMap object containing the tract and patch information.\n", " tract: int [0]\n", " The tract id of the desired tract to plot.\n", " title: str [None]\n", " Title of the tract plot. If None, the use `tract `.\n", " ax: matplotlib.axes._subplots.AxesSubplot [None]\n", " The subplot object to contain the tract plot. If None, then make a new one.\n", "\n", " Returns\n", " -------\n", " matplotlib.axes._subplots.AxesSubplot: The subplot containing the tract plot.\n", " \"\"\"\n", " if title is None:\n", " title = 'tract {}'.format(tract)\n", " tractInfo = skyMap[tract]\n", " tractBox = afw_geom.Box2D(tractInfo.getBBox())\n", " tractPosList = tractBox.getCorners()\n", " wcs = tractInfo.getWcs()\n", " xNum, yNum = tractInfo.getNumPatches()\n", "\n", " if ax is None:\n", " fig = plt.figure(figsize=(12,8))\n", " ax = fig.add_subplot(111)\n", "\n", " tract_center = wcs.pixelToSky(tractBox.getCenter())\\\n", " .getPosition(afw_geom.degrees)\n", " ax.text(tract_center[0], tract_center[1], '%d' % tract, size=16,\n", " ha=\"center\", va=\"center\", color='blue')\n", " for x in range(xNum):\n", " for y in range(yNum):\n", " patchInfo = tractInfo.getPatchInfo([x, y])\n", " patchBox = afw_geom.Box2D(patchInfo.getOuterBBox())\n", " pixelPatchList = patchBox.getCorners()\n", " path = make_patch(pixelPatchList, wcs)\n", " patch = patches.PathPatch(path, alpha=0.1, lw=1)\n", " ax.add_patch(patch)\n", " center = wcs.pixelToSky(patchBox.getCenter())\\\n", " .getPosition(afw_geom.degrees)\n", " ax.text(center[0], center[1], '%d,%d'%(x,y), size=6,\n", " ha=\"center\", va=\"center\")\n", "\n", " skyPosList = [wcs.pixelToSky(pos).getPosition(afw_geom.degrees)\n", " for pos in tractPosList]\n", " ax.set_xlim(max(coord[0] for coord in skyPosList) + 1,\n", " min(coord[0] for coord in skyPosList) - 1)\n", " ax.set_ylim(min(coord[1] for coord in skyPosList) - 1,\n", " max(coord[1] for coord in skyPosList) + 1)\n", " ax.grid(ls=':',color='gray')\n", " ax.set_xlabel(\"RA (deg.)\")\n", " ax.set_ylabel(\"Dec (deg.)\")\n", " ax.set_title(title)\n", " return ax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following function loops over the available calexps as returned by the data butler to determine which CCDs to draw. Unfortunately, looping over those calexps using the butler is rather slow, but would be necessary if we wanted to access CCD-level information, like the PSF, from the calexps.\n", "We include this function here for your edification, but then provide a faster function below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_focal_plane(butler, visit, ax, color='red'):\n", " \"\"\"\n", " Plot the CCDs in the LSST focal plane using the coordinate information in the calexps.\n", " \n", " Notes\n", " -----\n", " By looping over the available calexps, we only plot the CCDs for which image data\n", " are available.\n", " \n", " Parameters\n", " ----------\n", " butler: lsst.daf.persistence.Butler\n", " The data butler serving up data from the desired repo.\n", " visit: int\n", " The visit or obsHistID number.\n", " ax: matplotlib.axes._subplots.AxesSubplot\n", " The matplotlib subplot object onto which to plot the focal plane.\n", " color: str ['red']\n", " Color to use for plotting the individual CCDs.\n", " \n", " Returns\n", " -------\n", " matplotlib.axes._subplots.AxesSubplot: The subplot object used for plotting.\n", " \"\"\"\n", " # We use the `subset` method to obtain all of the `datarefs` (i.e., references to calexp\n", " # data in this case) that satisfy an \"incomplete\" dataId. For visit-level calexp data,\n", " # a unique dataset would specify visit, raft, and sensor. If we just give the visit, then\n", " # references to the available data for all of the CCDs would be returned.\n", " dataId = dict(visit=visit)\n", " datarefs = list(butler.subset('calexp', dataId=dataid))\n", " for i, dataref in enumerate(datarefs):\n", " calexp = dataref.get('calexp')\n", " # We're not going to do anything with it here, but we can get the PSF from the calexp\n", " # like this:\n", " # psf = calexp.getPsf()\n", " # and we can get the zero-point (in ADU) like this\n", " # zero_point = calexp.getCalib().getFluxMag0()\n", " ccd_box = afw_geom.Box2D(calexp.getBBox())\n", " wcs = calexp.getWcs()\n", " path = make_patch(ccd_box.getCorners(), wcs)\n", " ccd = patches.PathPatch(path, alpha=0.2, lw=1, color=color)\n", " ax.add_patch(ccd)\n", " center = wcs.pixelToSky(ccd_box.getCenter()).getPosition(afw_geom.degrees)\n", " return ax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following fast version of the focal plane plotting code uses the lsst_sims package to obtain the location and orientation of the CCDs based on the pointing information for the desired visit. That pointing information is extracted from the dithered minion_1016 OpSim db that has been prepared for DC2. Since this code does not access the individual calexps for each CCD, it runs much faster. However, it assumes that the obs_lsstSim package was used in the analysis of the data, and it needs to use the inferred locations of the calexp files to determine if calexp data for a given CCD is available. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_focal_plane_fast(butler, visit, ax, color='red', opsimdb=None):\n", " \"\"\"\n", " Plot the CCDs in the LSST focal plane using CCD coordinates derived from the pointing\n", " info using the lsst.sims code. \n", " \n", " Notes\n", " -----\n", " This function assumes that the obs_lsstSims package was used to define the camera geometry \n", " for the analysis of the simulated image data.\n", "\n", " Parameters\n", " ----------\n", " butler: lsst.daf.persistence.Butler\n", " The data butler serving up data from the desired repo.\n", " visit: int\n", " The visit or obsHistID number.\n", " ax: matplotlib.axes._subplots.AxesSubplot\n", " The matplotlib subplot object onto which to plot the focal plane.\n", " color: str ['red']\n", " Color to use for plotting the individual CCDs.\n", " opsimDb: str [None]\n", " Filename of the OpSim sqlite database. If None, then the dithered opsim db for Run1.1p\n", " is used.\n", "\n", " Returns\n", " -------\n", " matplotlib.axes._subplots.AxesSubplot: The subplot object used for plotting.\n", " \"\"\"\n", " if opsimdb is None:\n", " opsimdb = '/global/projecta/projectdirs/lsst/groups/SSim/DC2/minion_1016_desc_dithered_v4.db'\n", " conn = sqlite3.connect(opsimdb)\n", " obs_gen = ObservationMetaDataGenerator(database=opsimdb, driver='sqlite')\n", "\n", " # The dithered pointing info was added to the baseline minion_1016 db. We query for the values\n", " # used for the desired visit.\n", " curs = conn.execute('''select descDitheredRA, descDitheredDec, descDitheredRotTelPos\n", " from summary where obshistid={}'''.format(visit))\n", " ra, dec, rottelpos = [np.degrees(x) for x in curs][0]\n", " \n", " # An ObservationMetaData object used to pass the pointing info to the function in\n", " # lsst.sims.coordUtils that provides the CCD coordinates.\n", " obs_md = obs_gen.getObservationMetaData(obsHistID=visit, boundType='circle', boundLength=0.1)[0]\n", " obs_md.pointingRA = ra\n", " obs_md.pointingDec = dec\n", " obs_md.OpsimMetaData['rotTelPos'] = rottelpos\n", "\n", " # Convert the rotation angle of the sky relative to the telescope to the sky angle relative to\n", " # the camera.\n", " obs_md.rotSkyPos = getRotSkyPos(ra, dec, obs_md, rottelpos)\n", " \n", " # Use the butler to get the camera appropriate for this observation. If the data were from a\n", " # different camera, e.g., DECam or HSC, the corresponding camera objects with the associated\n", " # CCD geometries would be returned.\n", " camera = butler.get('camera')\n", " \n", " # Grab one of the calexps via its dataref so that we can ask for its filename and thereby infer\n", " # the location on disk of all of the calexps for this visit.\n", " dataref = list(butler.subset('calexp', visit=visit))[0]\n", " calexp_path = os.path.dirname(os.path.dirname(dataref.get('calexp_filename')[0]))\n", " \n", " # The following code is specific to the obs_lsstSim package and how it names CCDs\n", " # (e.g., \"R:2,2 S:1,1\") and formulates the path components for writing to disk. This\n", " # code would not work for a different obs_ package/camera implementation.\n", " \n", " # Re-order the CCD vertex list returned by the lsst_sims code so that a rectangle is plotted.\n", " corner_index = (np.array([0, 1, 3, 2]),)\n", " for det in camera:\n", " # Skip the WAVEFRONT and GUIDER CCDs\n", " if det.getType() != cameraGeom.SCIENCE:\n", " continue\n", " detname = det.getName()\n", " raft, sensor = re.match(r'R:?(\\d,?\\d)[_ ]S:?(\\d,?\\d)', detname).groups()\n", " raft = 'R' + raft.replace(',', '')\n", " sensor = 'S{}.fits'.format(sensor.replace(',', ''))\n", " if os.path.isfile(os.path.join(calexp_path, raft, sensor)):\n", " corners = np.array(lsst.sims.coordUtils.getCornerRaDec(detname, camera, obs_md))\n", " path = make_patch(corners[corner_index])\n", " ccd = patches.PathPatch(path, alpha=0.2, lw=1, color=color)\n", " ax.add_patch(ccd)\n", " \n", " return ax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following function just plots the boundaries of the Run1.1p regions as described in the [Run 2.1i Specifications document](https://docs.google.com/document/d/18nNVImxGioQ3tcLFMRr67G_jpOzCIOdar9bjqChueQg/edit)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_Run2_1i_region(ax):\n", " \"\"\"Plot the WFD and uDDF regions for Run2.1i.\"\"\"\n", " uddf_ra = [53.764, 52.486, 52.479, 53.771, 53.764]\n", " uddf_dec = [-27.533, -27.533, -28.667, -28.667, -27.533]\n", "\n", " wfd_ra = [71.46, 52.25, 49.92, 73.79, 71.46]\n", " wfd_dec = [-27.25, -27.25, -44.33, -44.33, -27.25]\n", "\n", " ax.errorbar(wfd_ra, wfd_dec, fmt='-', color='green',\n", " label='WFD boundary')\n", " ax.errorbar(uddf_ra, uddf_dec, fmt='-', color='red',\n", " label='uDDF boundary')\n", " return ax" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This `find_available_tract_numbers` function finds all tract numbers that are actually available." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def find_available_tract_numbers(butler, known_existing_tract=4851, known_existing_patch='0,0', known_existing_filter='i'):\n", " \"\"\"\n", " This is a hack to search the coadd folder for the tracts that have data.\n", " Unfortunately, this information is not directly accessible from the data butler. \n", " \n", " In order for this hack to work, one needs to provide a known existing tract, patch, filter. \n", " \"\"\"\n", " \n", " ref_path = butler.getUri('deepCoadd_forced_src', tract=known_existing_tract, patch=known_existing_patch, filter=known_existing_filter)\n", "\n", " ref_path, success, _ = ref_path.partition('/{}/{}'.format(known_existing_tract, known_existing_patch))\n", " tract_pattern = re.compile('(\\d+)$')\n", " if not success:\n", " ref_path, success, _ = ref_path.partition('/{}_t{}_p{}'.format(known_existing_filter, known_existing_tract, known_existing_patch))\n", " tract_pattern = re.compile('\\w_t(\\d+)_p')\n", " if not success:\n", " raise ValueError('cannot regonize path format')\n", "\n", " coadd_path_subdirs = [d for d in os.listdir(ref_path) if os.path.isdir(os.path.join(ref_path, d)) and tract_pattern.match(d)]\n", " tract_numbers = [int(tract_pattern.match(d).groups()[0]) for d in coadd_path_subdirs]\n", " tracts = sorted(set(tract_numbers))\n", " \n", " return tracts" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Making the Plot\n", "We now use the above functions to plot the tracts and patches on the sky. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "repo = REPOS['2.1i']\n", "butler = dp.Butler(repo)\n", "\n", "# First, loop over all the tracts, plotting them as gray, numbered, rectangles:\n", "ax = None\n", "skyMap = butler.get('deepCoadd_skyMap')\n", "tracts = find_available_tract_numbers(butler)\n", "for tract in tracts:\n", " ax = plot_skymap_tract(skyMap, tract=tract, title='', ax=ax)\n", "\n", "# Now overlay a single focal plane, for the chosen visit, in violet:\n", "visit = 219978\n", "plot_focal_plane_fast(butler, visit, ax, color=\"violet\")\n", "\n", "# Finally, overlay the Run 2.1i main survey and uDDF regions, and then adjust the axes:\n", "plot_Run2_1i_region(ax)\n", "\n", "ax.set_xlim(77.5, 45.0)\n", "ax.set_ylim(-47.5, -25.0)\n", "plt.legend(loc=0);" ] } ], "metadata": { "kernelspec": { "display_name": "desc-stack", "language": "python", "name": "desc-stack" }, "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.7.3" } }, "nbformat": 4, "nbformat_minor": 2 }