{ "cells": [ { "cell_type": "markdown", "id": "3e52c34d", "metadata": {}, "source": [ "# Galaxy Modeling\n", "\n", "In this section, we demonstrate the basics of light profile modeling on a galaxy using a single component Sérsic profile. We also demonstrate how the `photometry` and `petrosian` PetroFit sub-models can be used to improve the initial guesses of the Sérsic parameters.\n", "\n", "**Please note that this is an extensive analysis to demonstrate PetroFit's fitting workflow. Most practical applications of PetroFit will not require all of the steps discussed in this section.**\n", "\n", "\n", "To start with PetroFit, simply import it as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "8aa40d0b", "metadata": {}, "outputs": [], "source": [ "import petrofit as pf " ] }, { "cell_type": "markdown", "id": "a68cbd8e", "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", "### Loading Data and RMS Images\n", "We first use `astropy`'s ``CCDData`` to load the example data and visualize it through `matplotlib`. The rms image is loaded using `astropy`'s ``fits`` sub-module." ] }, { "cell_type": "code", "execution_count": null, "id": "91575418", "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": "7c75fd46", "metadata": { "nbsphinx": "hidden" }, "outputs": [], "source": [ "# Hidden cell\n", "\n", "%matplotlib inline\n", "\n", "# Stop Fit Model to Data section warnings\n", "import warnings\n", "warnings.filterwarnings('ignore', append=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "ffb27660", "metadata": { "scrolled": false }, "outputs": [], "source": [ "import numpy as np\n", "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 = 0.05 # Use the image std as max and min of all plots \n", "vmin = - vmax \n", "\n", "fig, axs = plt.subplots(1,2, figsize=[12, 6])\n", "plt.sca(axs[0])\n", "plt.imshow(image.data, vmin=vmin, vmax=vmax)\n", "plt.title(\"Mock Galaxy\")\n", "plt.xlabel(\"Pixels\")\n", "plt.ylabel(\"Pixels\")\n", "\n", "plt.sca(axs[1])\n", "plt.imshow(rms)\n", "plt.title(\"RMS Image\")\n", "plt.xlabel(\"Pixels\")\n", "plt.ylabel(\"Pixels\")\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "24044134", "metadata": {}, "source": [ "## Image Errors" ] }, { "cell_type": "code", "execution_count": null, "id": "3f16d0ad", "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": "2b76dbf6", "metadata": {}, "source": [ "## Make Source Catalog \n", "\n", "We will use the sigma clipped std as a threshold at the segmentation and deblending steps.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "b5d8bba9", "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": "markdown", "id": "14f99306", "metadata": {}, "source": [ "Here we identity sources in the input image." ] }, { "cell_type": "code", "execution_count": null, "id": "648a41a2", "metadata": {}, "outputs": [], "source": [ "threshold = image_stddev * 3\n", "\n", "# Min Source size (area)\n", "npixels = 2**2\n", "\n", "cat, segm, segm_deblend = pf.make_catalog( \n", " image.data, \n", " threshold=threshold, \n", " deblend=True, \n", " npixels=npixels,\n", " contrast=0.00,\n", " plot=True, vmax=vmax, vmin=vmin,\n", " figsize=[12, 6]\n", ")\n", "\n", "plt.show()\n", "\n", "# Display source properties\n", "print(\"Num of Targets:\", len(cat))" ] }, { "cell_type": "markdown", "id": "8d1050fc", "metadata": {}, "source": [ "## Single Source Photometry \n", "\n", "\n", "Here we pick a galaxy that can be modeled using a single Sérsic model, the galaxy in the middle looks like an elliptical galaxy that can be well described by such a profile. To make sure we selected the right galaxy, we use the `plot_target` function to plot a cutout of the source. " ] }, { "cell_type": "code", "execution_count": null, "id": "5c2384e9", "metadata": {}, "outputs": [], "source": [ "# Sort and select object of interest in the catalog\n", "sorted_idx_list = pf.order_cat(cat, key='area', reverse=True)\n", "idx = sorted_idx_list[2] # index 0 is largest \n", "source = cat[idx] # get source from the catalog \n", "\n", "pf.plot_target(\n", " position=(source.maxval_xindex, source.maxval_yindex), \n", " image=image.data, \n", " size=100, \n", " vmax=vmax, \n", " vmin=vmin\n", ")" ] }, { "cell_type": "markdown", "id": "9d172029", "metadata": {}, "source": [ "After selecting the source, we define a radius list for the aperture photometry. Once `r_list` is defined, we run the `source_photometry` step." ] }, { "cell_type": "code", "execution_count": null, "id": "2c1a6837", "metadata": {}, "outputs": [], "source": [ "# Max aperture radius \n", "max_pix = 70\n", "\n", "r_list = pf.make_radius_list(\n", " max_pix=max_pix, # Max pixel to go up to\n", " n=max_pix # the number of radii to produce (i.e 1 aperture per pixel increase in r) \n", ")\n", "\n", "print(\"len(r_list) = {}\".format(len(r_list)))\n", "\n", "\n", "# Photomerty \n", "flux_arr, area_arr, error_arr = pf.source_photometry(\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", "\n", " # Options \n", " error=err, # Error image (optional)\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": "d167e5fa", "metadata": {}, "source": [ "## Petrosian Profile \n", "\n", " \n", "We use the photometry that we obtained to construct a `Petrosian` profile of the galaxy. This will allow us to estimate the parameters of the galaxies Sérsic profile. We also initialize a `PetrosianCorrection` which we can use to estimate the Sérsic index (`n`) and `r_eff` (half-light radius). The correction grid we use for `PetrosianCorrection` was created specifically for this dataset using the PSF above. \n" ] }, { "cell_type": "code", "execution_count": null, "id": "e2161ce6", "metadata": {}, "outputs": [], "source": [ "p = pf.Petrosian(r_list, area_arr, flux_arr, flux_err=error_arr)\n", "\n", "pc = pf.PetrosianCorrection.read(\"data/f105w_psf_corr.ecsv\")" ] }, { "cell_type": "markdown", "id": "bdcaebb5", "metadata": {}, "source": [ "Next we compute and apply the epsilon that produces the correct `r_total_flux` and creates a new corrected Petrosian profile (`corrected_p`).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "42ff7344", "metadata": {}, "outputs": [], "source": [ "pc.plot_correction(p)\n", "plt.show()\n", "corrected_p = pc.correct(p)" ] }, { "cell_type": "markdown", "id": "8cb7fca8", "metadata": {}, "source": [ "Galaxies with a high Sérsic index (high concentration) have `r_total_flux`s that extend much further than we can possibly measure. If the corrected `r_total_flux` is out of the photometric range (i.e ` r_total_flux > max_pix `), we would be unable to measure the flux at `r_total_flux` (the total flux), which also means that we would be unable to measure the Petrosian radius (`np.nan` is returned). If the corrected ` r_total_flux` is out of range and we feel like the photometry data range goes far out enough to estimate the total flux, we can set `epsilon` to a value that will result in a ` r_total_flux` equal to the last photometry radius." ] }, { "cell_type": "code", "execution_count": null, "id": "6f0d4ee3", "metadata": {}, "outputs": [], "source": [ "max_photometry_r = max(corrected_p.r_list)\n", "\n", "if corrected_p.r_total_flux > max_photometry_r:\n", " print(\"Truncation was applied\")\n", " truncated_epsilon = max_photometry_r / corrected_p.r_petrosian\n", " corrected_p.epsilon = truncated_epsilon\n", "else:\n", " print(\"Truncation was not needed\")\n", "\n", "# Print Radii\n", "print('total_flux = ', corrected_p.total_flux)\n", "print('max_photometry_r = ', max_photometry_r)\n", "\n", "# Plot Petrosian \n", "corrected_p.plot()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "ab39545f", "metadata": {}, "source": [ "## Estimating Sérsic Parameters \n", "\n", "`astropy`’s `Sersic2D` implements this model and allows us to provide initial guesses for the Sérsic parameters. Getting good estimates of these parameters is very important because the Levenberg-Marquardt algorithm is sensitive and may return parameters that correspond to a local minimum. Because of this, we follow the steps below to estimate the Sérsic parameters.\n", "\n", "The `Sersic2D` model has the following parameters:\n", "\n", "- [**amplitude**: Surface brightness at r_eff.](#Amplitude-at-r_eff)\n", "- [**r_eff**: Effective (half-light) radius.](#Half-Light-Radius-(r_eff))\n", "- [**n**: Sérsic Index.](#Sérsic-Index)\n", "- [**x_0 and y_0**: x and y position of the center.](#Center-Pixel)\n", "- [**ellip**: Ellipticity of the profile.](#Ellipticity-and-Elongation)\n", "- [**theta**: Rotation angle in radians, counterclockwise from the positive x-axis.](#Source-Orientation)\n", "\n", "\n", "\n", "### Center Pixel\n", "\n", "We can use the `get_source_position` to find the max pixel position of the source in the image using the `SourceCatalog` object. Since this center is relative to the image, we save the result in `image_x_0` and `image_y_0`." ] }, { "cell_type": "raw", "id": "cb0c268a", "metadata": {}, "source": [ "
Note
\n", "The x_0 and y_0 vlaues we use in the model in this example are defined in the Zoom and Mask Image section. This is because we make a cutout of the target at (image_x_0, image_y_0) which shifts the coordinate space. If no cutouts are made, then we can use (image_x_0, image_y_0) as the center of the astropy model.