{ "cells": [ { "cell_type": "markdown", "id": "3e52c34d", "metadata": {}, "source": [ "# Image Fitting\n", "\n", "\n", "Most galaxy light profiles can be well described by PSF-convolved models like the Sérsic profile. PetroFit uses the `astropy` `modeling` sub-module to provide tools to perform two-dimensional fits of galaxy light profiles. To this end, we use the PetroFit ` PSFConvolvedModel2D` class, which applies PSF convolution to and handles oversampling for `astropy` based models. \n", "\n", "In this section, we demonstrate the basics of light profile modeling on a galaxy using a single component Sérsic profile. \n", "\n", "To start with PetroFit, simply import it as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "c6987e5f", "metadata": {}, "outputs": [], "source": [ "import petrofit as pf " ] }, { "cell_type": "markdown", "id": "a68cbd8e", "metadata": {}, "source": [ "## Loading Example Data\n", "\n", "The dataset we're using is a synthetic image of a galaxy, created using astropy's `Sersic2D` model.\n", "This galaxy representation is convolved with a PSF for the F105W filter using petrofit's PSFConvolvedModel2D to simulate observational data.\n", "We also added noise to the data and provide a corresponding RMS map. \n", "\n", "Key features of the synthetic galaxy:\n", "\n", "- Sérsic index of 1 (exponential profile).\n", "- Effective radius of 15 pixels.\n", "- Positioned at (100, 75) pixels.\n", "- Rotated by $\\frac{\\pi}{4}$.\n", "- With ellip=0.1\n", "\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.\n" ] }, { "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/example_sersic.fits.gz', unit='electron s-1')\n", "rms = fits.getdata('data/example_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.005 # 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": "84dec3c3", "metadata": {}, "source": [ "## PSF \n", "\n", "A Point Spread Function (PSF) describes how light from a point source is distributed on detector due to optical effects such as diffraction. Images or cutouts of stars are good approximations of PSFs because stars are single-point sources and their images describe how their light is distributed on the detector. To make cutouts of stars in an image, use the ` astropy.nddata.Cutout2D` function.\n", "\n", "The following PSF is a cutout of a star in the Hubble Frontier Fields image of Abell 2744 (same dataset as the example image). Since we will be using the PSF image as a convolution kernel, it is **very important** that the following requirements are satisfied:\n", "\n", "- The image of the PSF should be at the same resolution as the data.\n", "- The star or PSF is centered in the image.\n", "- The PSF image does not contain other sources. \n", "- The image is normalized so that the sum of the PSF image is near or equal to 1.0. \n", "- The PSF image should have odd dimensions on each side (because it is a convolution kernel). \n" ] }, { "cell_type": "code", "execution_count": null, "id": "8dc9f08a", "metadata": {}, "outputs": [], "source": [ "from astropy.io import fits\n", "\n", "# Load PSF image (2D array)\n", "PSF = fits.getdata('data/f105w_psf.fits.gz')\n", "\n", "# Normalize PSF \n", "PSF = PSF / PSF.sum()\n", "\n", "# Note that the PSF shape is odd on all sides\n", "print(\"PSF Shape = {}\".format(PSF.shape))\n", "\n", "# Plot PSF and use vmax and vmin to show difraction spikes\n", "plt.imshow(PSF, vmin=0, vmax=PSF.std()/10)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "ab39545f", "metadata": {}, "source": [ "## Sérsic Model\n", "\n", "### Sérsic Parameters \n", "\n", "The `amplitude`, `r_eff`, `n`, `x_0`, `y_0`, `ellip`, and `theta` represent the galaxy's brightness, \n", "effective radius, Sérsic index, position, ellipticity, and orientation, respectively. Here we make rough estimates of the parameters:" ] }, { "cell_type": "code", "execution_count": null, "id": "63efe398", "metadata": {}, "outputs": [], "source": [ "amplitude=0.2\n", "r_eff=20\n", "n=1\n", "x_0=107\n", "y_0=70\n", "ellip=0.1\n", "theta=0.1" ] }, { "cell_type": "markdown", "id": "33f984f4", "metadata": {}, "source": [ "### AstroPy Sérsic Model\n", "\n", "Here, we are setting up a 2D galaxy light profile model using astropy's Sersic2D model. \n", "The Sersic2D model is a widely-used representation of the light distribution of elliptical galaxies.\n", "We also define a set of `bounds`, a dictionary of lower and upper bounds of parameters. \n", "Keys are parameter names. The values are a list or a tuple of length 2 giving the desired range for \n", "the parameter and a value of `None` means no bounds. The default bounds can be provided using the \n", "PetroFit `get_default_sersic_bounds` function. For example, we restrain the fitter from exploring \n", "half-light radii that are negative by adding `'r_eff': (0, None)`. \n", "We also apply a custom restriction for the center of the model to be within a range (`center_slack`) from the initial guess." ] }, { "cell_type": "code", "execution_count": null, "id": "3cb0021c", "metadata": {}, "outputs": [], "source": [ "from astropy.modeling import models \n", "\n", "center_slack = 20\n", "\n", "sersic_model = models.Sersic2D(\n", " \n", " amplitude=amplitude,\n", " r_eff=r_eff,\n", " n=n,\n", " x_0=x_0,\n", " y_0=y_0,\n", " ellip=ellip, \n", " theta=theta,\n", " bounds = pf.get_default_sersic_bounds({\n", " 'x_0': (x_0 - center_slack/2, x_0 + center_slack/2),\n", " 'y_0': (y_0 - center_slack/2, y_0 + center_slack/2),\n", " }),\n", ") \n", "\n", "sersic_model" ] }, { "cell_type": "markdown", "id": "efbbdd0e", "metadata": {}, "source": [ "## PSFConvolvedModel2D\n", "\n", "The `petrofit` `PSFConvolvedModel2D` is a `Fittable2DModel` that adds PSF convolution and model to image sampling to `astropy` core models. `PSFConvolvedModel2D` makes an image of the underlying model and samples it onto a grid. The model image is then convolved with a PSF if one is provided. Since `PSFConvolvedModel2D` is a `Fittable2DModel`, it can be used to fit model images to data images. For example, we wrap an `astropy` `Sersic2D` model in this doc with `PSFConvolvedModel2D`, which produces an oversampled and PSF convolved version of the Sérsic profile at each iteration of the fitting algorithm. **Note that `PSFModel` is deprecated and replaced by `PSFConvolvedModel2D`.**\n" ] }, { "cell_type": "raw", "id": "3653d689", "metadata": {}, "source": [ "
Note
\n", "PSFConvolvedModel2D is agnostic to the models it wraps and can handle complex multi-component astropy models.