{ "cells": [ { "cell_type": "markdown", "id": "fa820f90", "metadata": {}, "source": [ "# Photometry\n", "\n", "To measure the Petrosian properties of galaxies, we construct a photometric curve of growth using a set of concentric apertures with varying radii to measure the flux. From this curve of growth, we will measure different properties of the galaxy including the total flux, characteristic radii such as the Petrosian radius and half-light radius, and the concentration index in the [Petrosian section](./petrosian.ipynb#Petrosian) below. For a quick guide on how to construct curves of growth and Petrosian profiles, please see the [Making a Photutils Source Catalog](./quick_start.ipynb#Making-a-Photutils-Source-Catalog) and [Curve of Growth and Petrosian Radii](./quick_start.ipynb#Curve-of-Growth-and-Petrosian-Radii) sections in the [Quick Start](./quick_start.ipynb#Quick-Start) guide.\n", "\n", "To start with `PetroFit`, simply import it as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "ad41b77d", "metadata": {}, "outputs": [], "source": [ "import petrofit as pf" ] }, { "cell_type": "markdown", "id": "b2f32b9c", "metadata": {}, "source": [ "## Loading Example Data\n", "\n", "The following data is a cutout of a group of bright galaxies in Abell 2744 (located at `(3.596248, -30.388517)`). The original data was acquired by the [Hubble Frontier Fields](https://frontierfields.org) team via the WFC3 instrument in the `F105W` filter and can be directly downloaded from the [Mikulski Archive for Space Telescopes](https://archive.stsci.edu/pub/hlsp/frontier/abell2744/images/hst/v1.0/hlsp_frontier_hst_wfc3-60mas_abell2744_f105w_v1.0_drz.fits). The cutout image used in this documentation can be found in the git repository at the following path `petrofit/docs/data/abell_2744_dwarf_galaxy_f105w.fits.gz`.\n", "\n", "We first use `astropy`'s ``CCDData`` to load the example data and visualize it through `matplotlib`." ] }, { "cell_type": "code", "execution_count": null, "id": "cca798ac", "metadata": {}, "outputs": [], "source": [ "from astropy.nddata import CCDData\n", "from astropy.io import fits \n", "\n", "image = CCDData.read('data/abell_2744_dwarf_galaxy_f105w.fits.gz')\n", "rms = fits.getdata('data/abell_2744_dwarf_galaxy_f105w_rms.fits.gz')" ] }, { "cell_type": "code", "execution_count": null, "id": "c0df0ebd", "metadata": { "nbsphinx": "hidden" }, "outputs": [], "source": [ "# Hidden cell\n", "\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "id": "6d936492", "metadata": {}, "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "\n", "plt.rcParams['figure.figsize'] = [6, 6]\n", "plt.rcParams['image.origin'] = 'lower'\n", "plt.rcParams['font.size'] = 12\n", "\n", "vmax = image.data.std() # Use the image std as max and min of all plots \n", "vmin = - vmax \n", "\n", "plt.imshow(image.data, vmin=0, vmax=image.data.std())\n", "plt.title(\"Galaxy in Abell 2744\")\n", "plt.xlabel(\"Pixels\")\n", "plt.ylabel(\"Pixels\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "20444fd6", "metadata": {}, "source": [ "### Estimate Data Noise at Dark Area" ] }, { "cell_type": "markdown", "id": "9088b8c7", "metadata": {}, "source": [ "In this section, we estimate the noise levels in the image using `astropy`'s `sigma_clipped_stats` and `photutils`'s `calc_total_error` functions.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "ec9baf08", "metadata": {}, "outputs": [], "source": [ "from astropy.stats import sigma_clipped_stats\n", "image_mean, image_median, image_stddev = sigma_clipped_stats(image.data, sigma=3)" ] }, { "cell_type": "code", "execution_count": null, "id": "ca369982", "metadata": {}, "outputs": [], "source": [ "from photutils.utils import calc_total_error\n", "err = calc_total_error(\n", " data=image.data, # Input Image\n", " bkg_error=rms, # All sources of background error except source Poisson error\n", " effective_gain=image.header['EXPTIME'] # Factor to convert data units to counts\n", ")" ] }, { "cell_type": "markdown", "id": "36d0015a", "metadata": {}, "source": [ "## Catalogs\n", "\n", "Before we can perform photometry, we need to first identify the sources in the image. To do this, we use Photutils and the wrappers in PetroFit. \n", "\n", "### Make Catalog \n", "\n", "We first start by defining the detection threshold and we select this value to be 3-sigma for this example." ] }, { "cell_type": "code", "execution_count": null, "id": "5dc2e8af", "metadata": {}, "outputs": [], "source": [ "# Define detect threshold\n", "threshold = 3*image_stddev" ] }, { "cell_type": "markdown", "id": "95102409", "metadata": {}, "source": [ "We also have to define the number of pixels that make up the smallest object. `npixels` is the number of connected pixels, each greater than the threshold value, that an object must have to be detected. `npixels` must be a positive integer." ] }, { "cell_type": "code", "execution_count": null, "id": "6b042b37", "metadata": {}, "outputs": [], "source": [ "npixels = 4**2" ] }, { "cell_type": "markdown", "id": "a0d7d08c", "metadata": {}, "source": [ "\n", "To make a catalog of sources and segmentation maps, we use ` petrofit`'s `make_catalog` function. The function returns a source catalog (`photutils.SourceCatalog`), a segmentation image, and, if `deblend=True`, a deblended segmentation image. \n", "\n", "The `make_catalog` function wraps three steps into one function:\n", "\n", "1. **Segmentation:**\n", "\n", " To identify sources in the image, we first segment the image. The image is smoothed with a gaussian kernel if ` kernel_size ` is provided and clipped at the threshold specified (after smoothing if applicable). The image is then segmented using the ` petrofit.segmentation.make_segments` function, which is a wrapper for `photutil`’s ` detect_sources` functionality. \n", "\n", "\n", "2. **Deblending:**\n", "\n", " To further distinguish the sources, we use `photutils.deblend_sources` to deblend the sources into individual galaxies. The `contrast` parameter is the fraction of the total (blended) source flux that a local peak must have (at any one of the multi-thresholds) to be considered as a separate object and `nlevels` is the number of multi-thresholding levels to use. If `deblend` is set to ``False``, ``None`` is returend for `segm_deblend`.\n", "\n", "3. **Source Catalog:**\n", "\n", " Now that we have deblended the sources into individual sources, the next step is to create a source catalog (`photutils.SourceCatalog`) that contains properties like `(xcentroid, ycentroid)`, `eccentricity` and `area`. Note that the deblended map is used to make the source catalog but if `deblend` is set to ``False``, the segmentation map is used instead.\n", " \n", "By setting the `plot` flag to `True`, we see plots of the segmentation and deblended segmentation maps. The image is plotted along with a color-coded overplot of the segmentation map (each source is a different color). `vmax` and `vmin` can be used the same way as in `plt.imshow`. \n", "\n", "\n", "After the computation, the following objects are returned:\n", "\n", "- `cat` : A catalog of sources.\n", "\n", "- `segm` : Segmentation map.\n", "\n", "- `segm_deblend` : Deblended segmentation map." ] }, { "cell_type": "code", "execution_count": null, "id": "574d4356", "metadata": {}, "outputs": [], "source": [ "cat, segm, segm_deblend = pf.make_catalog( \n", " image.data, \n", " threshold, \n", " wcs=None,\n", " deblend=True,\n", " npixels=npixels,\n", " nlevels=30,\n", " contrast=0.001,\n", " plot=True, vmax=vmax, vmin=vmin,\n", " figsize=(12, 6)\n", ")" ] }, { "cell_type": "markdown", "id": "4d85df3a", "metadata": {}, "source": [ "To demonstrate the useful information in the catalog, we convert the `SourceCatalog` to an `astropy.table.Table` and display the first 10 objects. " ] }, { "cell_type": "code", "execution_count": null, "id": "1eabaace", "metadata": {}, "outputs": [], "source": [ "# Display source properties\n", "print(\"Num of Targets:\", len(cat))\n", "\n", "# Convert to table\n", "cat_table = cat.to_table()\n", "\n", "cat_table[:10]" ] }, { "cell_type": "markdown", "id": "0712b1dc", "metadata": {}, "source": [ "### Plotting Segmentation Maps\n", "\n", "To plot segmentations, you can use the `plot_segments` function included in PetroFit as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "cf7e6e2c", "metadata": {}, "outputs": [], "source": [ "pf.plot_segments(segm, image=image.data, vmax=vmax, vmin=vmin)" ] }, { "cell_type": "markdown", "id": "0f1bfef8", "metadata": {}, "source": [ "As you can see, the segmentation resulted in the identification sources but the sources at the center were classified as a single object because they have interconnecting pixels that are above the threshold.\n", "\n", "Next we use the same functiton to plot the deblended segmentation map, notice how the central sources are now deblended into individual sources:" ] }, { "cell_type": "code", "execution_count": null, "id": "e928bfa3", "metadata": {}, "outputs": [], "source": [ "pf.plot_segments(segm_deblend, image=image.data, vmax=vmax, vmin=vmin)" ] }, { "cell_type": "markdown", "id": "ab074429", "metadata": {}, "source": [ "We can also plot the background pixels that are not a part of a source’s segmentation footprint using the `plot_segment_residual` function. We significantly lower the `vmax` and `vmin` values so the background pixels become more apparent. This plot can be used to see if the threshold used to segment the image was too high. If the threshold is high, we would notice bright pixels that are part of the source in this plot. " ] }, { "cell_type": "code", "execution_count": null, "id": "d8e230ca", "metadata": {}, "outputs": [], "source": [ "pf.plot_segment_residual(segm, image.data, vmax=vmax/5)" ] }, { "cell_type": "markdown", "id": "62b1115d", "metadata": {}, "source": [ "## Photometry on Single Source" ] }, { "cell_type": "markdown", "id": "4cc2bd90", "metadata": {}, "source": [ "The purpose of this step is to perform aperture photometry to construct a curve of growth that we can use for the Petrosian measurements. \n", "\n", "### Source Selection\n", "\n", "For this example, we will focus on a single source. We have included a helper function `order_cat` that will produce a list of indices sorted by a key (default is 'area'). We use the `order_cat` function to identify the source of interest and perform photometry on its cutout." ] }, { "cell_type": "code", "execution_count": null, "id": "dfa11b07", "metadata": {}, "outputs": [], "source": [ "# Sort and get the largest object in the catalog\n", "sorted_idx_list = pf.order_cat(cat, key='area', reverse=True)\n", "idx = sorted_idx_list[1] # index 0 is largest \n", "source = cat[idx] # get source from the catalog " ] }, { "cell_type": "markdown", "id": "bcc8ce3c", "metadata": {}, "source": [ "### Aperture Radii \n", "\n", "To construct the curve of growth, we measure the photometry using circular and/or elliptical apertures of varying, concentric radii that are centered on the source. Before we perform the photometry, we need to provide a list of radii that will be used to construct the circular and elliptical apertures. To achieve this, we have provided a helper function that takes in the max radius in pixels (`max_pix`) and the number radii (`n`). The function will return a list of radii by dividing the range `(max_pix/n, max_pix)` into n equally spaced integers. " ] }, { "cell_type": "code", "execution_count": null, "id": "1e59cc07", "metadata": {}, "outputs": [], "source": [ "r_list = pf.make_radius_list(\n", " max_pix=50, # Max pixel to go up to\n", " n=50 # the number of radii to produce \n", ")\n", "\n", "print(repr(r_list))" ] }, { "cell_type": "markdown", "id": "aae24fef", "metadata": {}, "source": [ "### Photometry Calculation\n", "\n", "The `source_photometry` function is used to perform photometry on a single source (`photutils.segmentation.catalog.SourceCatalog`). In this example, we provide the function with the source object, the raw image (note that this is the 2D array and not `CCDData`), segmentation from the last step (`SegmentationImage`) and the list of radii we made using `make_radius_list`. Given these parameters and the options below the function performs photometry as follows:\n", "\n", "- The positions (max value of source in pixels), elongation and position-angle of the source are determined from the `SourceCatalog` object. The position will be used to center the apertures and the elongation and position angles will be used as parameters of the elliptical apertures. Each of the radii will be assigned an elliptical aperture with these parameters. \n", "\n", "\n", "- If `cutout_size` is defined, the code will use it to make a cutout of that size with the source centered. If the `cutout_size` is larger than the image or contains pixels outside the image, those pixels outside of the image are replaced by `np.nan`.\n", "\n", "\n", "- Once the `cutout_size` is determined, cutouts of the error map (if provided) and image are produced. Before the raw image is cutout, sources that are not the source of interest are masked using the segmentation map. The `mask_background` option gives us the ability to also mask pixels that are considered to be background pixels because they do not belong to any source’s segmentation map. All masked pixels are replaced by `np.nan` and are not counted in the returned area array. \n", "\n", "\n", "- If `bg_sub` is set to true, a 2D plane is used to fit pixels that are below a specified sigma from the mean using the `petrofit.fitting.fit_plane` function. The sigma `sigma` value is used to determine noise pixels. Once the pixels above this value are masked, a 2D plane is fit to determine the background. The 2D plane model is then converted into an image and subtracted from the cutout of the target source. `sigma_type` is used to set how this `sigma` value will be used. The `sigma_type` options are `'clip'` and `'bound'`:\n", " - ``'clip'`` (default): Uses `astropy.stats.sigma_clipping.sigma_clip` to clip at the provided `sigma` std value. Note that `sigma` in this case is the number of stds above the mean.\n", " - ``'bound'`` : After computing the mean of the image, clip at `mean - sigma` and `mean + sigma`. Note that `sigma` in this case is a value and not the number of stds above the mean.\n", "\n", "\n", "- The resulting image (after being noise subtracted if `bg_sub` is set to true) is passed to the `petrofit.photometry.photometry_step` which constructs the apertures and performs photometry. \n", "\n", "After calculating the photometry at each radius, three arrays are returned:\n", "\n", "* `flux_arr`: Photometric sum in aperture.\n", "\n", "* `area_arr`: Exact area of the aperture.\n", "\n", "* `error_arr`: if error map is provided, error of measurements.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3a6edf4f", "metadata": {}, "outputs": [], "source": [ "# Photomerty \n", "flux_arr, area_arr, error_arr = pf.source_photometry(\n", " \n", " # Inputs \n", " source, # Source (`photutils.segmentation.catalog.SourceCatalog`)\n", " image.data, # Image as 2D array \n", " segm_deblend, # Deblended segmentation map of image\n", " r_list, # list of aperture radii \n", " error=err,\n", " \n", " # Options \n", " cutout_size=max(r_list)*2, # Cutout out size, set to double the max radius \n", " bg_sub=True, # Subtract background \n", " sigma=3, sigma_type='clip', # Fit a 2D plane to pixels within 3 sigma of the mean\n", " plot=True, vmax=vmax, vmin=vmin, # Show plot with max and min defined above\n", ")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "531fde92", "metadata": {}, "source": [ "If `source_photometry`’s `plot` option is set to True, four plots are displayed: \n", "- The top left plot shows the cutout with the 2D plane background subtraction and the surrounding sources masked (replaced by `np.nan`).\n", "- The top right plot shows the curve of growth with increasing radius. The red lines represent the aperture radius.\n", "- The bottom two plots show the source profile sliced at the center of the image in the y and x direction respectively.\n" ] }, { "cell_type": "markdown", "id": "1cd59c51", "metadata": {}, "source": [ "### Save Photometry Arrays\n", "\n", "There are many ways of saving the photometry results. For example adding the photometry to the source catalog with the radii as columns. One simple way to save the results for a single source is to save it as a `csv` of `ecsv` file using AstroPy `Table`. " ] }, { "cell_type": "code", "execution_count": null, "id": "f675dd4b", "metadata": {}, "outputs": [], "source": [ "from astropy.table import Table\n", "\n", "t = Table(\n", " data=[r_list, flux_arr, area_arr, error_arr],\n", " names=['r_list', 'flux_arr', 'area_arr', 'error_arr'], \n", ")\n", "\n", "\n", "t[:10] # Show first 10 radii" ] }, { "cell_type": "code", "execution_count": null, "id": "907f7372", "metadata": {}, "outputs": [], "source": [ "# also save shape information in the table's meta\n", "t.meta['position'] = pf.get_source_position(source)\n", "t.meta['elong'] = pf.get_source_elong(source)\n", "t.meta['theta'] = pf.get_source_theta(source)" ] }, { "cell_type": "code", "execution_count": null, "id": "e71c9e24", "metadata": {}, "outputs": [], "source": [ "t.write('temp/abell_2744_galaxy_f105w_photometry.ecsv', overwrite=True)" ] }, { "cell_type": "markdown", "id": "8763ed38", "metadata": {}, "source": [ "### Plot Curve of Growth\n", "\n", "Here we plot the curve of growth: " ] }, { "cell_type": "code", "execution_count": null, "id": "34d361fc", "metadata": {}, "outputs": [], "source": [ "plt.errorbar(r_list, flux_arr, yerr=error_arr,\n", " marker='o', capsize=3, label=\"Data\")\n", "\n", "pf.mpl_tick_frame()\n", "\n", "plt.xlabel(\"Aperture Radius [pix]\")\n", "plt.ylabel(\"$L(\\leq r)$\")\n", "plt.show()" ] } ], "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.12.7" } }, "nbformat": 4, "nbformat_minor": 5 }