PSF Photometry Basics with Photutils#

This notebook provides a basic overview of PSF photometry using Photutils.

Use case: Create a simulated image of stars, PSF photometry.
Data: This notebook creates a JWST/NIRCam F200W NRCA1 simulated image of stars.
Tools: photutils.
Instrument: NIRCam.
Documentation: This notebook is part of STScI’s larger post-pipeline data analysis tools ecosystem.

Imports#

import os

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import requests
from astropy.table import QTable
from astropy.visualization import simple_norm
from photutils.aperture import CircularAperture
from photutils.datasets import make_noise_image
from photutils.detection import IRAFStarFinder
from photutils.psf import (GriddedPSFModel, PSFPhotometry, SourceGrouper,
                           make_psf_model_image)
from photutils.utils import make_random_cmap
from tweakwcs import XYXYMatch

# Change some default plotting parameters
mpl.rcParams['image.origin'] = 'lower'
mpl.rcParams['image.interpolation'] = 'nearest'
/opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Point Spread Function Photometry with Photutils#

The Photutils PSF photometry module provides modular tools that allow users to fully customize the photometry procedure (e.g., by using different source detection algorithms, local background estimators, source groupers, and PSF models). Photutils provides implementations for each subtask involved in the photometry process. However, users can also incorporate their own custom implementations for any of the tasks, if desired. This modularity is achieved through an object-oriented programming approach, providing a more convenient user experience.

Photutils provides two top-level classes to perform PSF photometry: PSFPhotometry and IterativelyPSFPhotometry. In this notebook, we will cover the basics of the PSFPhotometry class.

PSF Models#

PSF photometry fundamentally involves fitting models to data. As such, the PSF model is a critical component of PSF photometry. For accurate results in both photometry and astrometry, the PSF model should closely represent the actual data. The PSF model can be a simple analytic function, such as a 2D Gaussian or Moffat profile, or it can be a more complex model derived from a 2D PSF image (e.g., an effective PSF, or ePSF). The PSF model can also account for variations in the PSF across the detector (e.g., due to optical aberrations).

Photutils provides the following analytic PSF Models based on the Astropy Modeling and Fitting framework:

It also includes image-based PSF models:

Create a Photutils GriddedPSFModel#

A gridded PSF model is a grid of position-dependent ePSFs that takes into account the PSF varying across the detector.

Let’s use a position-dependent gridded PSF model for JWST/NIRCam F200W in detector NRCA1.

This gridded PSF model was calculated using STPSF, a Python package that computes simulated PSFs for both JWST and Roman.

The next cell shows how to use STPSF to generate the gridded PSF model. However, for this notebook we will simply download a pre-calculated gridded PSF model (see the following cell).

# How to calculate the GriddedPSFModel file that we load in the next cell
# nrc = stpsf.NIRCam()
# nrc.filter = 'F200W'
# nrc.detector = 'NRCA1'
# psf_model = nrc.psf_grid(num_psfs=16, all_detectors=False, verbose=True, save=True)
filename = "nircam_nrca1_f200w_fovp101_samp4_npsf16.fits"
baseurl = "https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/stpsf_grid/"
url = os.path.join(baseurl, filename)
file_path = os.path.join('.', filename)

if not os.path.exists(file_path):
    response = requests.get(url)
    with open(file_path, 'wb') as file:
        file.write(response.content)
        print(f"File saved as: {file_path}")
else:
    print(f"File already exists: {file_path}")
File saved as: ./nircam_nrca1_f200w_fovp101_samp4_npsf16.fits
# load the STPSF GriddedPSF model from the downloaded FITS file
filename = 'nircam_nrca1_f200w_fovp101_samp4_npsf16.fits'
psf_model = GriddedPSFModel.read(filename)
psf_model
<GriddedPSFModel(flux=1., x_0=0., y_0=0.)>

psf_model contains a 3D cube of PSFs. There are 16 2D PSFs, each 404 x 404 pixels.

psf_model.data.shape
(16, 404, 404)

The default oversampling is 4 along each axis.

psf_model.oversampling
array([4, 4])

Let’s plot the grid of ePSFs.

fig = psf_model.plot_grid(vmax_scale=0.1, figsize=(9, 9))
../../../_images/27230906ef4b30f99576e59fcf67c929238798d8a2bbb424b0296ed636890ee7.png
fig = psf_model.plot_grid(deltas=True, figsize=(9, 9))
../../../_images/e42b2b4d456868290fc413fe1bdcf4be202815114588ae19caa7979d68bba374.png

Let’s use this PSF model to create an image of simulated stars#

We will use the photutils.psf make_psf_model_image function.

We will create 500 stars in a 2048 x 2048 image (NIRCam F200W NRCA1)#

n_sources = 500
shape = (2048, 2048)
data, true_params = make_psf_model_image(shape, psf_model, n_sources,
                                         flux=(500, 20_000), min_separation=25,
                                         seed=0, progress_bar=True)
Add model sources:   0%|          | 0/500 [00:00<?, ?it/s]
Add model sources:   1%|          | 3/500 [00:00<00:18, 26.32it/s]
Add model sources:   1%|▏         | 7/500 [00:00<00:15, 31.64it/s]
Add model sources:   3%|▎         | 14/500 [00:00<00:10, 45.78it/s]
Add model sources:   4%|▍         | 20/500 [00:00<00:09, 49.04it/s]
Add model sources:   5%|▌         | 27/500 [00:00<00:08, 54.02it/s]
Add model sources:   7%|▋         | 34/500 [00:00<00:08, 56.94it/s]
Add model sources:   8%|▊         | 41/500 [00:00<00:07, 59.03it/s]
Add model sources:  10%|▉         | 48/500 [00:00<00:07, 60.16it/s]
Add model sources:  11%|█         | 55/500 [00:01<00:07, 60.70it/s]
Add model sources:  12%|█▏        | 62/500 [00:01<00:07, 61.33it/s]
Add model sources:  14%|█▍        | 69/500 [00:01<00:06, 61.71it/s]
Add model sources:  15%|█▌        | 76/500 [00:01<00:06, 62.15it/s]
Add model sources:  17%|█▋        | 83/500 [00:01<00:06, 62.58it/s]
Add model sources:  18%|█▊        | 90/500 [00:01<00:06, 62.04it/s]
Add model sources:  19%|█▉        | 97/500 [00:01<00:06, 62.38it/s]
Add model sources:  21%|██        | 104/500 [00:01<00:06, 62.60it/s]
Add model sources:  22%|██▏       | 111/500 [00:01<00:06, 62.56it/s]
Add model sources:  24%|██▎       | 118/500 [00:02<00:06, 62.65it/s]
Add model sources:  25%|██▌       | 125/500 [00:02<00:05, 62.71it/s]
Add model sources:  26%|██▋       | 132/500 [00:02<00:05, 62.83it/s]
Add model sources:  28%|██▊       | 139/500 [00:02<00:05, 62.89it/s]
Add model sources:  29%|██▉       | 146/500 [00:02<00:05, 63.02it/s]
Add model sources:  31%|███       | 153/500 [00:02<00:05, 63.04it/s]
Add model sources:  32%|███▏      | 160/500 [00:02<00:05, 63.03it/s]
Add model sources:  33%|███▎      | 167/500 [00:02<00:05, 63.08it/s]
Add model sources:  35%|███▍      | 174/500 [00:02<00:05, 63.10it/s]
Add model sources:  36%|███▌      | 181/500 [00:03<00:05, 63.18it/s]
Add model sources:  38%|███▊      | 188/500 [00:03<00:04, 63.24it/s]
Add model sources:  39%|███▉      | 195/500 [00:03<00:04, 63.15it/s]
Add model sources:  40%|████      | 202/500 [00:03<00:04, 62.99it/s]
Add model sources:  42%|████▏     | 209/500 [00:03<00:04, 63.04it/s]
Add model sources:  43%|████▎     | 216/500 [00:03<00:04, 63.06it/s]
Add model sources:  45%|████▍     | 223/500 [00:03<00:04, 63.13it/s]
Add model sources:  46%|████▌     | 230/500 [00:03<00:04, 63.08it/s]
Add model sources:  47%|████▋     | 237/500 [00:03<00:04, 63.05it/s]
Add model sources:  49%|████▉     | 244/500 [00:04<00:04, 62.84it/s]
Add model sources:  50%|█████     | 251/500 [00:04<00:03, 62.69it/s]
Add model sources:  52%|█████▏    | 258/500 [00:04<00:03, 62.87it/s]
Add model sources:  53%|█████▎    | 265/500 [00:04<00:03, 61.30it/s]
Add model sources:  54%|█████▍    | 272/500 [00:04<00:03, 61.75it/s]
Add model sources:  56%|█████▌    | 279/500 [00:04<00:03, 62.10it/s]
Add model sources:  57%|█████▋    | 286/500 [00:04<00:03, 62.48it/s]
Add model sources:  59%|█████▊    | 293/500 [00:04<00:03, 62.71it/s]
Add model sources:  60%|██████    | 300/500 [00:04<00:03, 62.74it/s]
Add model sources:  61%|██████▏   | 307/500 [00:05<00:03, 62.87it/s]
Add model sources:  63%|██████▎   | 314/500 [00:05<00:02, 62.95it/s]
Add model sources:  64%|██████▍   | 321/500 [00:05<00:02, 62.95it/s]
Add model sources:  66%|██████▌   | 328/500 [00:05<00:02, 62.68it/s]
Add model sources:  67%|██████▋   | 335/500 [00:05<00:02, 62.63it/s]
Add model sources:  68%|██████▊   | 342/500 [00:05<00:02, 62.73it/s]
Add model sources:  70%|██████▉   | 349/500 [00:05<00:02, 62.46it/s]
Add model sources:  71%|███████   | 356/500 [00:05<00:02, 62.41it/s]
Add model sources:  73%|███████▎  | 363/500 [00:05<00:02, 62.49it/s]
Add model sources:  74%|███████▍  | 370/500 [00:06<00:02, 62.41it/s]
Add model sources:  75%|███████▌  | 377/500 [00:06<00:01, 62.48it/s]
Add model sources:  77%|███████▋  | 384/500 [00:06<00:01, 62.58it/s]
Add model sources:  78%|███████▊  | 391/500 [00:06<00:01, 62.70it/s]
Add model sources:  80%|███████▉  | 398/500 [00:06<00:01, 62.92it/s]
Add model sources:  81%|████████  | 405/500 [00:06<00:01, 62.86it/s]
Add model sources:  82%|████████▏ | 412/500 [00:06<00:01, 62.74it/s]
Add model sources:  84%|████████▍ | 419/500 [00:06<00:01, 62.76it/s]
Add model sources:  85%|████████▌ | 426/500 [00:06<00:01, 62.62it/s]
Add model sources:  87%|████████▋ | 433/500 [00:07<00:01, 62.56it/s]
Add model sources:  88%|████████▊ | 440/500 [00:07<00:00, 62.82it/s]
Add model sources:  89%|████████▉ | 447/500 [00:07<00:00, 62.81it/s]
Add model sources:  91%|█████████ | 454/500 [00:07<00:00, 62.66it/s]
Add model sources:  92%|█████████▏| 461/500 [00:07<00:00, 62.85it/s]
Add model sources:  94%|█████████▎| 468/500 [00:07<00:00, 62.98it/s]
Add model sources:  95%|█████████▌| 475/500 [00:07<00:00, 63.00it/s]
Add model sources:  96%|█████████▋| 482/500 [00:07<00:00, 62.79it/s]
Add model sources:  98%|█████████▊| 489/500 [00:07<00:00, 62.82it/s]
Add model sources:  99%|█████████▉| 496/500 [00:08<00:00, 62.75it/s]
Add model sources: 100%|██████████| 500/500 [00:08<00:00, 61.74it/s]

fig, ax = plt.subplots(figsize=(10, 10))
norm = simple_norm(data, 'sqrt', percent=98)
axim = ax.imshow(data, norm=norm)
../../../_images/84397ab5e751ba0a6112f7e2d8ecb533aadb9e7a815b1dab65a92b79b0165852.png

Now let’s add some Gaussian noise (σ = 0.5) to the image.

noise = make_noise_image(data.shape, mean=0, stddev=0.5, seed=0)
data += noise
error = np.sqrt(np.abs(data))
fig, ax = plt.subplots(figsize=(10, 10))
norm2 = simple_norm(data, 'sqrt', percent=99)
axim = ax.imshow(data, norm=norm2)
../../../_images/80b22fb3169cc30deb0437b88a8076cfd2c8956c697c0d94730e73f2c1972e11.png

The true_params output contains an Astropy table containing the true (x, y, flux) of our artificial stars.

true_params
QTable length=500
idx_0y_0flux
int64float64float64float64
11290.8013669021931774.377824414809511576.13482157921
2575.54451841201931395.37733959930319268.478310411472
3129.81642462770725988.949734102873115441.000611839
482.195834009574671373.68184238178816746.079103988828
51634.25042596213071678.05804804859611603.97911581131
61828.047864537002309.5537152762055412347.864117392048
71231.72649119446651364.518917180736312768.672723356296
81471.0593007968291127.02479897843895685.779847567518
91108.98148337464391284.347957793637512668.089104108565
............
491577.223074730307129.3807257825421914507.876279651628
4921361.7045616265877453.274715050074639846.434914865096
493381.13750346241611156.190598498335217092.388569016857
494341.840683365400141317.670182180985219341.760724229327
495287.4655271611766211.5648804241066314904.353014873148
496198.903212748686661463.83458518861685127.760470402655
4971090.6820339800868207.536343576288321297.4411340894303
498372.8442840030262215.8444043048784211463.74372055272
4991622.36312782211831306.70741278239379583.072289053016
50094.045313505322471986.825484015556219299.05221739804

Finding Stars in an Image (photutils.detection)#

Let’s use the IRAFStarFinder class to find the stars in the simulated image.

finder = IRAFStarFinder(threshold=6.0, fwhm=3.0)
stars = finder(data)
stars
QTable length=500
idxcentroidycentroidfwhmsharpnessroundnesspanpixpeakfluxmag
int64float64float64float64float64float64float64int64float64float64float64
11192.105555947458753.066315254689811.79073663179369480.59691221059789820.01602124538361244103.50327927086758131445.53487870441316702.657403681727-9.565617554118727
2391.7710021778335756.623511892040021.73262362181116480.57754120727038830.07734114701673132127.67294625687012111345.82731405821556834.501827950182-9.586767160003873
3795.808701055418957.719802263309651.75479155171167660.58493051723722560.050923983146313495123.76952554697297111685.94955683017688212.884033591004-9.786239226517
41666.388914969333472.965213403287591.73699494008425080.57899831336141690.01273655823043336524.64335109216909121602.37253421849057894.772125702702-9.743348997678982
5704.803134726527976.317809886327011.73836979767470480.57945659922490160.0572816498645165956.7106714867425411990.62149515136474832.501058805616-9.210429894637347
6255.6118008299629381.217639863617681.71839732001281260.57279910667093760.0715142535944464645.27550776264768111926.15765469415269684.86666304069-9.96523411480502
71944.722505998474883.380065474572321.71429295837699680.57143098612566560.095185412078036950.43115402630989111373.9030549090016972.668462567562-9.608497539499997
81053.61654741262383.651709067015861.70372465308656950.56790821769552310.11475912048574714132.33752219804214111564.22864248350928224.770241215145-9.78780943696046
91477.72410659165684.36823040849881.71773699482206350.57257899827402110.0904462672220778850.49923134540043111384.71298545706856992.860483427065-9.611637158981052
.................................
491187.015176123997261961.68678236021291.77356543707357140.59118847902452380.0296340718104402585.85191897548906121897.71564453149849309.387614483894-9.92230278344215
492895.18705584277321970.77661954977481.77285372062179670.59095124020726550.0425603577663636659.068982247780811998.77796871431584858.1778542642005-9.21618352489223
4931924.83886039839331971.34855605822921.74795054226218330.5826501807540610.0645205866379887860.8177718336371311680.95364533655653406.850837427257-8.830882798037411
494208.36404730855771979.32851103779261.72096111442080480.57365370480693490.10521499796303566129.55419310402846111673.1265390069428731.297849157323-9.852697008700918
49550.675759356774331980.82617019542231.75065456766743830.58355152255581280.046862608373343786134.0009960951959111609.04566177732047994.260167861066-9.756945694945262
496298.16836794767471985.23815984787281.76804449635860660.58934816545286880.041227666901465045117.76380702044993112251.17344840604310894.287867528958-10.092997116902998
497346.73020766024841985.14970282802661.76565384126230420.58855128042076810.042491396280240552.2530035121758312758.43198632353323700.4518121683222-8.920636882837902
49894.063365597896041986.87075639049541.80540581827386170.60180193942462060.02151049817045749579.44898612901693132292.26113068025810890.066981230495-10.092576377417293
4991002.23352415216571992.00763522621831.78776367182148750.59592122394049580.0209086763791432591.5925640981024312897.593331481354317.032305987015-9.08796324664611
500553.10117106318591997.88438111423241.8039766885516490.60132556285054970.02363924693998341375.78418013743854131455.4041922499846923.60971085213-9.600831445957333

The star finder found all 500 stars in the image. Let’s plot circles around the detected stars.

fig, ax = plt.subplots(figsize=(10, 10))
axim = ax.imshow(data, norm=norm2)
xypos = zip(stars['xcentroid'], stars['ycentroid'])
aper = CircularAperture(xypos, r=20)
patches = aper.plot(ax=ax, color='red')
../../../_images/962cec080b1cc832819989224e1e889367e1850535d2567e8612f57f83e3bc43.png

The PSFPhotometry class#

In this example, we will perform PSF photometry on our simulated image.

First, we create the PSFPhotometry class instance with a few parameters.

We must input a PSF model, which must be an Astropy Fittable2DModel. As described above, Photutils provides several PSF models, including a GriddedPSFModel for spatially-varying PSFs.

We must also input the fit_shape parameter, which defines the region around the center of each detected star that is used for fitting the PSF model.

We must also provide initial guesses of the position and flux for each star in order to perform the model fitting. There are a few ways to accomplish that. In this example, we will be using the optional finder and aperture_radius keywords. The finder will be used internally to detect the sources and calculate their initial (x, y) positions. The aperture_radius (in pixels) will be used internally to calculate the initial flux values for each source. For other options (e.g., inputting an init_params table), please see the PSF Photometry docs.

We set progress_bar=True to display an interactive progress bar during the PSF fitting.

fit_shape = (5, 5)
finder = IRAFStarFinder(threshold=6.0, fwhm=3.0)
psfphot = PSFPhotometry(psf_model, fit_shape, finder=finder, aperture_radius=5, progress_bar=True)

To perform the PSF fitting, we call the psfphot object on the data and the optional error array. The result is an Astropy QTable with the fit results.

phot = psfphot(data, error=error)
phot
Fit source/group:   0%|          | 0/500 [00:00<?, ?it/s]
Fit source/group:   4%|▍         | 19/500 [00:00<00:02, 185.25it/s]
Fit source/group:   8%|▊         | 40/500 [00:00<00:02, 194.55it/s]
Fit source/group:  12%|█▏        | 60/500 [00:00<00:02, 191.71it/s]
Fit source/group:  16%|█▌        | 80/500 [00:00<00:02, 192.50it/s]
Fit source/group:  20%|██        | 101/500 [00:00<00:02, 194.63it/s]
Fit source/group:  24%|██▍       | 121/500 [00:00<00:01, 192.48it/s]
Fit source/group:  28%|██▊       | 141/500 [00:00<00:01, 192.70it/s]
Fit source/group:  32%|███▏      | 161/500 [00:00<00:01, 191.87it/s]
Fit source/group:  36%|███▌      | 181/500 [00:00<00:01, 190.48it/s]
Fit source/group:  40%|████      | 201/500 [00:01<00:01, 188.48it/s]
Fit source/group:  44%|████▍     | 220/500 [00:01<00:01, 187.35it/s]
Fit source/group:  48%|████▊     | 239/500 [00:01<00:01, 186.13it/s]
Fit source/group:  52%|█████▏    | 258/500 [00:01<00:01, 186.65it/s]
Fit source/group:  56%|█████▌    | 278/500 [00:01<00:01, 187.92it/s]
Fit source/group:  59%|█████▉    | 297/500 [00:01<00:01, 162.81it/s]
Fit source/group:  63%|██████▎   | 316/500 [00:01<00:01, 169.03it/s]
Fit source/group:  67%|██████▋   | 336/500 [00:01<00:00, 175.36it/s]
Fit source/group:  71%|███████   | 355/500 [00:01<00:00, 176.60it/s]
Fit source/group:  75%|███████▌  | 375/500 [00:02<00:00, 180.72it/s]
Fit source/group:  79%|███████▉  | 394/500 [00:02<00:00, 182.35it/s]
Fit source/group:  83%|████████▎ | 414/500 [00:02<00:00, 184.95it/s]
Fit source/group:  87%|████████▋ | 433/500 [00:02<00:00, 183.56it/s]
Fit source/group:  90%|█████████ | 452/500 [00:02<00:00, 185.31it/s]
Fit source/group:  94%|█████████▍| 471/500 [00:02<00:00, 183.96it/s]
Fit source/group:  98%|█████████▊| 491/500 [00:02<00:00, 187.59it/s]
Fit source/group: 100%|██████████| 500/500 [00:02<00:00, 184.89it/s]

QTable length=500
idgroup_idgroup_sizelocal_bkgx_inity_initflux_initx_fity_fitflux_fitx_erry_errflux_errnpixfitqfitcfitflags
int64int64int64float64float64float64float64float64float64float64float64float64float64int64float64float64int64
1110.01192.105555947458753.066315254689819769.843087850371192.112489062283753.0772602516149912216.3951063618950.0103532876848010870.010454739937857291134.99990751571409256.964462906791501e-05-1.9414090087428652e-070
2210.0391.7710021778335756.6235118920400210790.248508662999391.697778662935756.5554664253189313483.935753657990.0100360000300741340.010311147902097655142.2098454553027257.262967473124619e-051.5702561799616147e-060
3310.0795.808701055418957.7198022633096512480.63854173307795.768867771744957.67414394850296615582.6984276262210.0092673507414353370.009424472595034823152.5984253787596255.509408522073522e-051.0347695107500807e-060
4410.01666.388914969333472.9652134032875912191.945330713211666.457711836316372.9555735533536915252.6349689568690.0094821025808959430.009339545788981335150.73795419248074256.881241053978113e-05-1.0614993730612482e-060
5510.0704.803134726527976.317809886327017511.709303115783704.756525734160476.381588922247849371.0191024917620.011944362642519220.012226019386897076118.02884707087004259.19545389556457e-05-2.921870050780829e-060
6610.0255.6118008299629381.2176398636176815554.781286257943255.505029609327681.2674663433465719447.6824562165820.008403923211633310.008383150624637731170.24146964676584252.6439299122880032e-055.248611419255972e-070
7710.01944.722505998474883.3800654745723211192.1819966948551944.68018744131783.4464009527210413996.4905306635650.0096608503950427250.0100689669263145144.23672212653102256.214553770134094e-051.5302376923467163e-060
8810.01053.61654741262383.6517090670158613461.2694241522041053.532822134258183.579973929530516816.860346914970.0089604239955310350.00909083146993759158.41169638203598255.02870484657219e-05-6.33393214798828e-070
9910.01477.72410659165684.368230408498811196.5468806088431477.662046313027884.4367859141802913995.6543679438710.0097368800672725760.01005074099140739144.17391749959023256.655961259569325e-05-1.2052590474125427e-060
...................................................
49149110.0187.015176123997261961.686782360212914091.10419427188186.995366166099421961.605605835174917059.1503129123960.008720038140421410.008797392055912423157.34018685339808254.870744731974836e-05-1.0226874447617079e-060
49249210.0895.18705584277321970.77661954977487299.674693491745895.22003385235781970.7163792942038826.846165746230.0120566253592046860.012073434133258199112.95150122600577250.00014915713688682658-1.8267319309470527e-060
49349310.01924.83886039839331971.34855605822925290.3681472351811924.79961355560321971.40541668702266414.3022528803290.013992315071410680.01479164906684039596.35313024735477250.00024705065610210636-6.55469244497591e-060
49449410.0208.36404730855771979.328511037792614265.293397462816208.418667865365421979.376433776860317275.2945700947820.0087783163119079160.008978833297112715158.0236947235806253.41633956719112e-05-6.098326727324719e-070
49549510.050.675759356774331980.826170195422312403.63597596463550.585640713777771980.769669749002615044.8388346769520.0093525793437008260.009209024719499203147.51024512694968255.829512725642079e-051.1758702735527357e-060
49649610.0298.16836794767471985.238159847872816476.589045916437298.180811881254671985.254175348252619961.5499987226360.0080859027646194140.008217072468956876169.8799789849002253.615928010528903e-055.072004194129899e-070
49749710.0346.73020766024841985.14970282802665639.747854874019346.67018819070891985.1498607545616826.5683981902130.0138240200908228430.01378018754686750499.21462670058307250.00019858447210482184-5.117669977632834e-060
49849810.094.063365597896041986.870756390495415919.92879327300694.045248428425171986.825213991678819301.364760767810.0081662290793007680.008069145853684725167.26187845413065254.1991758914826264e-055.458675737839309e-070
49949910.01002.23352415216571992.00763522621836401.16454731769951002.2726662146111991.98854814994377743.8814088127590.0129102699699463420.012728863301181576105.71687949139337250.00013628768057791047.105068485039063e-070
50050010.0553.10117106318591997.884381114232410110.530244567432553.1185983596881997.838513660045212238.4237983373130.0102029052627153230.010108185483084389133.00563190313764257.866862278325297e-051.5745152225618928e-070

The *_init columns contain the initial (x, y, flux) values. The corresponding *_fit columns contain the results of the PSF fitting and the *_err columns contain the errors on each fit parameter.

We can now use the make_model_image method to create a PSF model image of the results.

model_img = psfphot.make_model_image(data.shape)

fig, ax = plt.subplots(figsize=(10, 10))
axim = ax.imshow(model_img, norm=norm)
Add model sources:   0%|          | 0/500 [00:00<?, ?it/s]
Add model sources:   1%|▏         | 7/500 [00:00<00:08, 60.87it/s]
Add model sources:   3%|▎         | 14/500 [00:00<00:07, 61.69it/s]
Add model sources:   4%|▍         | 21/500 [00:00<00:07, 61.96it/s]
Add model sources:   6%|▌         | 28/500 [00:00<00:07, 62.22it/s]
Add model sources:   7%|▋         | 35/500 [00:00<00:07, 62.42it/s]
Add model sources:   8%|▊         | 42/500 [00:00<00:07, 62.44it/s]
Add model sources:  10%|▉         | 49/500 [00:00<00:07, 62.40it/s]
Add model sources:  11%|█         | 56/500 [00:00<00:07, 62.50it/s]
Add model sources:  13%|█▎        | 63/500 [00:01<00:06, 62.56it/s]
Add model sources:  14%|█▍        | 70/500 [00:01<00:06, 62.58it/s]
Add model sources:  15%|█▌        | 77/500 [00:01<00:06, 62.37it/s]
Add model sources:  17%|█▋        | 84/500 [00:01<00:06, 62.25it/s]
Add model sources:  18%|█▊        | 91/500 [00:01<00:06, 62.30it/s]
Add model sources:  20%|█▉        | 98/500 [00:01<00:06, 62.41it/s]
Add model sources:  21%|██        | 105/500 [00:01<00:06, 62.51it/s]
Add model sources:  22%|██▏       | 112/500 [00:01<00:06, 62.57it/s]
Add model sources:  24%|██▍       | 119/500 [00:01<00:06, 62.64it/s]
Add model sources:  25%|██▌       | 126/500 [00:02<00:05, 62.70it/s]
Add model sources:  27%|██▋       | 133/500 [00:02<00:05, 62.71it/s]
Add model sources:  28%|██▊       | 140/500 [00:02<00:05, 62.65it/s]
Add model sources:  29%|██▉       | 147/500 [00:02<00:05, 62.56it/s]
Add model sources:  31%|███       | 154/500 [00:02<00:05, 62.58it/s]
Add model sources:  32%|███▏      | 161/500 [00:02<00:05, 62.57it/s]
Add model sources:  34%|███▎      | 168/500 [00:02<00:05, 62.55it/s]
Add model sources:  35%|███▌      | 175/500 [00:02<00:05, 62.55it/s]
Add model sources:  36%|███▋      | 182/500 [00:02<00:05, 62.57it/s]
Add model sources:  38%|███▊      | 189/500 [00:03<00:04, 62.63it/s]
Add model sources:  39%|███▉      | 196/500 [00:03<00:04, 62.62it/s]
Add model sources:  41%|████      | 203/500 [00:03<00:04, 62.56it/s]
Add model sources:  42%|████▏     | 210/500 [00:03<00:04, 62.43it/s]
Add model sources:  43%|████▎     | 217/500 [00:03<00:04, 62.45it/s]
Add model sources:  45%|████▍     | 224/500 [00:03<00:04, 62.37it/s]
Add model sources:  46%|████▌     | 231/500 [00:03<00:04, 62.44it/s]
Add model sources:  48%|████▊     | 238/500 [00:03<00:04, 62.32it/s]
Add model sources:  49%|████▉     | 245/500 [00:03<00:04, 62.49it/s]
Add model sources:  50%|█████     | 252/500 [00:04<00:03, 62.56it/s]
Add model sources:  52%|█████▏    | 259/500 [00:04<00:03, 62.52it/s]
Add model sources:  53%|█████▎    | 266/500 [00:04<00:03, 62.54it/s]
Add model sources:  55%|█████▍    | 273/500 [00:04<00:03, 60.32it/s]
Add model sources:  56%|█████▌    | 280/500 [00:04<00:03, 60.79it/s]
Add model sources:  57%|█████▋    | 287/500 [00:04<00:03, 61.39it/s]
Add model sources:  59%|█████▉    | 294/500 [00:04<00:03, 61.81it/s]
Add model sources:  60%|██████    | 301/500 [00:04<00:03, 62.12it/s]
Add model sources:  62%|██████▏   | 308/500 [00:04<00:03, 62.31it/s]
Add model sources:  63%|██████▎   | 315/500 [00:05<00:02, 62.47it/s]
Add model sources:  64%|██████▍   | 322/500 [00:05<00:02, 62.50it/s]
Add model sources:  66%|██████▌   | 329/500 [00:05<00:02, 62.63it/s]
Add model sources:  67%|██████▋   | 336/500 [00:05<00:02, 62.62it/s]
Add model sources:  69%|██████▊   | 343/500 [00:05<00:02, 62.61it/s]
Add model sources:  70%|███████   | 350/500 [00:05<00:02, 62.69it/s]
Add model sources:  71%|███████▏  | 357/500 [00:05<00:02, 62.68it/s]
Add model sources:  73%|███████▎  | 364/500 [00:05<00:02, 62.63it/s]
Add model sources:  74%|███████▍  | 371/500 [00:05<00:02, 62.62it/s]
Add model sources:  76%|███████▌  | 378/500 [00:06<00:01, 62.64it/s]
Add model sources:  77%|███████▋  | 385/500 [00:06<00:01, 62.60it/s]
Add model sources:  78%|███████▊  | 392/500 [00:06<00:01, 62.66it/s]
Add model sources:  80%|███████▉  | 399/500 [00:06<00:01, 62.51it/s]
Add model sources:  81%|████████  | 406/500 [00:06<00:01, 62.51it/s]
Add model sources:  83%|████████▎ | 413/500 [00:06<00:01, 62.61it/s]
Add model sources:  84%|████████▍ | 420/500 [00:06<00:01, 62.64it/s]
Add model sources:  85%|████████▌ | 427/500 [00:06<00:01, 62.67it/s]
Add model sources:  87%|████████▋ | 434/500 [00:06<00:01, 62.71it/s]
Add model sources:  88%|████████▊ | 441/500 [00:07<00:00, 62.64it/s]
Add model sources:  90%|████████▉ | 448/500 [00:07<00:00, 62.38it/s]
Add model sources:  91%|█████████ | 455/500 [00:07<00:00, 62.33it/s]
Add model sources:  92%|█████████▏| 462/500 [00:07<00:00, 62.36it/s]
Add model sources:  94%|█████████▍| 469/500 [00:07<00:00, 62.39it/s]
Add model sources:  95%|█████████▌| 476/500 [00:07<00:00, 62.45it/s]
Add model sources:  97%|█████████▋| 483/500 [00:07<00:00, 62.43it/s]
Add model sources:  98%|█████████▊| 490/500 [00:07<00:00, 62.44it/s]
Add model sources:  99%|█████████▉| 497/500 [00:07<00:00, 62.54it/s]
Add model sources: 100%|██████████| 500/500 [00:08<00:00, 62.41it/s]

../../../_images/10de1cd19888a6731cbc83be9928474459c373d57c7c7f61dbba0b7e48ba1cfd.png

We can use the make_residual_image method to create a residual image.

resid = psfphot.make_residual_image(data)

fig, ax = plt.subplots(figsize=(10, 10))
norm3 = simple_norm(data, 'sqrt', percent=95)
axim = ax.imshow(resid, norm=norm3)
Add model sources:   0%|          | 0/500 [00:00<?, ?it/s]
Add model sources:   1%|▏         | 7/500 [00:00<00:08, 61.41it/s]
Add model sources:   3%|▎         | 14/500 [00:00<00:07, 62.07it/s]
Add model sources:   4%|▍         | 21/500 [00:00<00:07, 62.35it/s]
Add model sources:   6%|▌         | 28/500 [00:00<00:07, 62.47it/s]
Add model sources:   7%|▋         | 35/500 [00:00<00:07, 62.48it/s]
Add model sources:   8%|▊         | 42/500 [00:00<00:07, 62.45it/s]
Add model sources:  10%|▉         | 49/500 [00:00<00:07, 62.42it/s]
Add model sources:  11%|█         | 56/500 [00:00<00:07, 62.55it/s]
Add model sources:  13%|█▎        | 63/500 [00:01<00:06, 62.65it/s]
Add model sources:  14%|█▍        | 70/500 [00:01<00:06, 62.64it/s]
Add model sources:  15%|█▌        | 77/500 [00:01<00:06, 62.73it/s]
Add model sources:  17%|█▋        | 84/500 [00:01<00:06, 62.24it/s]
Add model sources:  18%|█▊        | 91/500 [00:01<00:06, 61.64it/s]
Add model sources:  20%|█▉        | 98/500 [00:01<00:06, 61.89it/s]
Add model sources:  21%|██        | 105/500 [00:01<00:06, 62.12it/s]
Add model sources:  22%|██▏       | 112/500 [00:01<00:06, 62.26it/s]
Add model sources:  24%|██▍       | 119/500 [00:01<00:06, 62.32it/s]
Add model sources:  25%|██▌       | 126/500 [00:02<00:05, 62.44it/s]
Add model sources:  27%|██▋       | 133/500 [00:02<00:05, 62.51it/s]
Add model sources:  28%|██▊       | 140/500 [00:02<00:05, 62.19it/s]
Add model sources:  29%|██▉       | 147/500 [00:02<00:05, 62.25it/s]
Add model sources:  31%|███       | 154/500 [00:02<00:05, 62.40it/s]
Add model sources:  32%|███▏      | 161/500 [00:02<00:05, 62.45it/s]
Add model sources:  34%|███▎      | 168/500 [00:02<00:05, 62.52it/s]
Add model sources:  35%|███▌      | 175/500 [00:02<00:05, 62.45it/s]
Add model sources:  36%|███▋      | 182/500 [00:02<00:05, 62.47it/s]
Add model sources:  38%|███▊      | 189/500 [00:03<00:04, 62.53it/s]
Add model sources:  39%|███▉      | 196/500 [00:03<00:04, 62.58it/s]
Add model sources:  41%|████      | 203/500 [00:03<00:04, 62.67it/s]
Add model sources:  42%|████▏     | 210/500 [00:03<00:04, 62.63it/s]
Add model sources:  43%|████▎     | 217/500 [00:03<00:04, 62.62it/s]
Add model sources:  45%|████▍     | 224/500 [00:03<00:04, 62.59it/s]
Add model sources:  46%|████▌     | 231/500 [00:03<00:04, 62.52it/s]
Add model sources:  48%|████▊     | 238/500 [00:03<00:04, 62.37it/s]
Add model sources:  49%|████▉     | 245/500 [00:03<00:04, 62.54it/s]
Add model sources:  50%|█████     | 252/500 [00:04<00:03, 62.59it/s]
Add model sources:  52%|█████▏    | 259/500 [00:04<00:03, 62.66it/s]
Add model sources:  53%|█████▎    | 266/500 [00:04<00:03, 62.73it/s]
Add model sources:  55%|█████▍    | 273/500 [00:04<00:03, 62.64it/s]
Add model sources:  56%|█████▌    | 280/500 [00:04<00:03, 62.57it/s]
Add model sources:  57%|█████▋    | 287/500 [00:04<00:03, 62.62it/s]
Add model sources:  59%|█████▉    | 294/500 [00:04<00:03, 62.59it/s]
Add model sources:  60%|██████    | 301/500 [00:04<00:03, 62.49it/s]
Add model sources:  62%|██████▏   | 308/500 [00:04<00:03, 61.99it/s]
Add model sources:  63%|██████▎   | 315/500 [00:05<00:02, 62.22it/s]
Add model sources:  64%|██████▍   | 322/500 [00:05<00:02, 62.41it/s]
Add model sources:  66%|██████▌   | 329/500 [00:05<00:02, 62.54it/s]
Add model sources:  67%|██████▋   | 336/500 [00:05<00:02, 62.52it/s]
Add model sources:  69%|██████▊   | 343/500 [00:05<00:02, 62.50it/s]
Add model sources:  70%|███████   | 350/500 [00:05<00:02, 62.54it/s]
Add model sources:  71%|███████▏  | 357/500 [00:05<00:02, 62.51it/s]
Add model sources:  73%|███████▎  | 364/500 [00:05<00:02, 62.47it/s]
Add model sources:  74%|███████▍  | 371/500 [00:05<00:02, 62.45it/s]
Add model sources:  76%|███████▌  | 378/500 [00:06<00:01, 62.41it/s]
Add model sources:  77%|███████▋  | 385/500 [00:06<00:01, 62.45it/s]
Add model sources:  78%|███████▊  | 392/500 [00:06<00:01, 62.49it/s]
Add model sources:  80%|███████▉  | 399/500 [00:06<00:01, 61.88it/s]
Add model sources:  81%|████████  | 406/500 [00:06<00:01, 62.09it/s]
Add model sources:  83%|████████▎ | 413/500 [00:06<00:01, 62.29it/s]
Add model sources:  84%|████████▍ | 420/500 [00:06<00:01, 62.40it/s]
Add model sources:  85%|████████▌ | 427/500 [00:06<00:01, 62.47it/s]
Add model sources:  87%|████████▋ | 434/500 [00:06<00:01, 62.57it/s]
Add model sources:  88%|████████▊ | 441/500 [00:07<00:00, 62.57it/s]
Add model sources:  90%|████████▉ | 448/500 [00:07<00:00, 62.67it/s]
Add model sources:  91%|█████████ | 455/500 [00:07<00:00, 62.62it/s]
Add model sources:  92%|█████████▏| 462/500 [00:07<00:00, 62.63it/s]
Add model sources:  94%|█████████▍| 469/500 [00:07<00:00, 62.57it/s]
Add model sources:  95%|█████████▌| 476/500 [00:07<00:00, 62.51it/s]
Add model sources:  97%|█████████▋| 483/500 [00:07<00:00, 62.48it/s]
Add model sources:  98%|█████████▊| 490/500 [00:07<00:00, 62.47it/s]
Add model sources:  99%|█████████▉| 497/500 [00:07<00:00, 62.55it/s]
Add model sources: 100%|██████████| 500/500 [00:08<00:00, 62.44it/s]

../../../_images/c2f99419eb5f7f61d3471ee8a62d39c4a921cf115e622a5addb4c948f846cc70.png

Our residual image is just noise without any sources, which indicates excellent PSF model fits.

Comparing results#

Let’s use our knowledge of the true (x, y) positions and fluxes to compare to our PSF fit results.

We first need to match the table catalogs.

# convenience function to match (x, y) positions
def xymatch_catalogs(ref_params, params):
    refcat = QTable()
    refcat['TPx'] = ref_params['x_0']
    refcat['TPy'] = ref_params['y_0']
    fitcat = QTable()
    fitcat['TPx'] = params['x_fit']
    fitcat['TPy'] = params['y_fit']
    match = XYXYMatch(separation=1)
    ref_idx, fit_idx = match(refcat, fitcat)

    return ref_params[ref_idx], params[fit_idx]
true_params, fit_params = xymatch_catalogs(true_params, phot)
fig, ax = plt.subplots(ncols=3, figsize=(12, 4))
fig.suptitle('PSF Photometry Results')
ax[0].plot(true_params['x_0'], fit_params['x_fit'], '.')
ax[0].set_xlabel('True x')
ax[0].set_ylabel('Fit x')
ax[1].plot(true_params['y_0'], fit_params['y_fit'], '.')
ax[1].set_xlabel('True y')
ax[1].set_ylabel('Fit y')
ax[2].plot(true_params['flux'], fit_params['flux_fit'], '.')
ax[2].set_xlabel('True Flux')
ax[2].set_ylabel('Fit Flux')
plt.tight_layout()
../../../_images/e8d22b03d0650aba43818ea7537bd5c95e8621a0c5d666323ce725bd184734a3.png
fig, ax = plt.subplots()
pdiff = (true_params['flux'] - fit_params['flux_fit']) / true_params['flux'] * 100.0
ax.set_title('Histogram of PSF Flux Differences')
ax.hist(pdiff, bins=50)
text = ax.set_xlabel('Percent Difference (between True and Fit)')
../../../_images/529fe3db0ba08f616bba0ad9c8022009a86ca5bc7373e77b588a9658b6aef548.png

Source Grouping#

Source grouping is an optional feature that allows you to group close stars that should be fit simultaneously.

To turn it on, create a SourceGrouper instance and input it via the grouper keyword. Here we’ll group stars that are within 20 pixels of each other:

finder = IRAFStarFinder(threshold=6.0, fwhm=3.0)
stars = finder(data)
stars[0:5]
QTable length=5
idxcentroidycentroidfwhmsharpnessroundnesspanpixpeakfluxmag
int64float64float64float64float64float64float64int64float64float64float64
11192.105555947458753.066315254689811.79073663179369480.59691221059789820.01602124538361244103.50327927086758131445.53487870441316702.657403681727-9.565617554118727
2391.7710021778335756.623511892040021.73262362181116480.57754120727038830.07734114701673132127.67294625687012111345.82731405821556834.501827950182-9.586767160003873
3795.808701055418957.719802263309651.75479155171167660.58493051723722560.050923983146313495123.76952554697297111685.94955683017688212.884033591004-9.786239226517
41666.388914969333472.965213403287591.73699494008425080.57899831336141690.01273655823043336524.64335109216909121602.37253421849057894.772125702702-9.743348997678982
5704.803134726527976.317809886327011.73836979767470480.57945659922490160.0572816498645165956.7106714867425411990.62149515136474832.501058805616-9.210429894637347
min_separation = 35
grouper = SourceGrouper(min_separation)
x = np.array(stars['xcentroid'])
y = np.array(stars['ycentroid'])
group_ids = grouper(x, y)

group_ids is an array with 500 elements (1 per input (x, y) position) with the group IDs.

group_ids
array([  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,
        14,  15,  16,  17,  18,  19,  15,  20,  21,  15,  22,  22,  15,
        23,  24,  22,  15,  25,  24,  26,  27,  28,  29,  30,  31,  32,
        27,  33,  34,  35,  36,  37,  38,  27,  39,  40,  27,  41,  42,
        43,  39,  44,  45,  46,  44,  47,  48,  49,  50,  47,  46,  51,
        52,  53,  54,  55,  56,  57,  58,  46,  59,  60,  61,  62,  63,
        55,  64,  65,  66,  67,  68,  69,  70,  71,  72,  73,  74,  75,
        76,  77,  78,  79,  80,  81,  82,  83,  84,  85,  78,  83,  86,
        87,  88,  89,  90,  91,  92,  93,  94,  95,  96,  97,  91,  98,
        99, 100, 101, 102, 103, 104, 105, 104, 106, 107, 108, 109, 110,
       111, 112, 113, 114, 115, 111, 116, 117, 118, 119, 120, 121, 118,
       122, 123, 124, 125, 121, 123, 126, 127, 128, 129, 130, 131, 132,
       133, 134, 134, 135, 135, 135, 136, 137, 138, 131, 139, 140, 141,
       142, 143, 144, 145, 146, 147, 145, 148, 149, 150, 151, 152, 144,
       153, 154, 155, 156, 157, 149, 158, 159, 160, 161, 162, 163, 157,
       164, 159, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175,
       176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188,
       189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201,
       202, 201, 203, 204, 205, 206, 207, 208, 204, 207, 209, 210, 205,
       211, 212, 213, 214, 215, 216, 217, 204, 218, 219, 220, 221, 222,
       223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235,
       236, 237, 238, 239, 240, 241, 234, 242, 243, 244, 245, 246, 247,
       248, 249, 250, 251, 252, 253, 254, 255, 252, 256, 257, 258, 259,
       253, 252, 260, 261, 262, 263, 264, 252, 265, 266, 267, 261, 268,
       269, 270, 271, 272, 273, 274, 275, 272, 276, 277, 278, 279, 280,
       281, 282, 283, 284, 285, 279, 286, 287, 288, 289, 284, 290, 284,
       291, 292, 293, 294, 295, 284, 296, 297, 298, 299, 284, 300, 301,
       294, 302, 284, 303, 301, 304, 305, 306, 307, 308, 309, 310, 311,
       312, 313, 314, 315, 316, 317, 318, 319, 318, 314, 320, 321, 322,
       318, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334,
       335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 340, 346,
       347, 348, 349, 350, 351, 352, 353, 354, 355, 348, 356, 357, 358,
       356, 359, 360, 357, 361, 362, 363, 361, 361, 364, 365, 366, 367,
       368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 369,
       380, 378, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391,
       392, 393, 394, 395, 396, 397, 398, 393, 399, 400, 401, 402, 403,
       404, 405, 406, 407, 408, 408, 409, 410, 411, 412, 410, 413, 414,
       415, 413, 416, 412, 417, 418, 416, 419, 420, 420, 421, 422, 420,
       423, 424, 425, 426, 427, 428])

The grouping algorithm separated the 500 stars into 428 distinct groups.

print(max(group_ids))
428

Let’s visualize them. Close groups of stars with the same circle color are in the same group.

fig, ax = plt.subplots(figsize=(10, 10))
norm4 = simple_norm(data, 'sqrt', percent=99)
ax.imshow(data, norm=norm4, cmap='Greys_r')
cmap = make_random_cmap(ncolors=500, seed=123)
for i in np.arange(1, max(group_ids) + 1):
    mask = group_ids == i
    xypos = zip(x[mask], y[mask])
    ap = CircularAperture(xypos, r=20)
    ap.plot(color=cmap.colors[i], lw=2)
../../../_images/addb7353b11041f4c1c86e53a8c3198432b97d4716f184ead7a7179df2c42926.png

For example, the six stars outlined with light orange circles in the figure below are all in the same group.

fig, ax = plt.subplots(figsize=(5, 5))
norm4 = simple_norm(data, 'sqrt', percent=99)
ax.imshow(data, norm=norm4, cmap='Greys_r')
cmap = make_random_cmap(ncolors=500, seed=123)
for i in np.arange(1, max(group_ids) + 1):
    mask = group_ids == i
    xypos = zip(x[mask], y[mask])
    ap = CircularAperture(xypos, r=20)
    ap.plot(color=cmap.colors[i], lw=2)
ax.set_xlim(1050, 1250)
ax.set_ylim(1300, 1500)
(1300.0, 1500.0)
../../../_images/ea21bc9fd38a6123be1c1059bc6bcc575bd1b31ab669c45265bdeaeacb1fb452.png

To perform grouped PSF photometry, we can simply input the SourceGrouper instance into the grouper keyword.

fit_shape = (5, 5)
finder = IRAFStarFinder(threshold=6.0, fwhm=3.0)
min_separation = 35
grouper = SourceGrouper(min_separation)
psfphot = PSFPhotometry(psf_model, fit_shape, finder=finder, grouper=grouper, 
                        aperture_radius=5, progress_bar=True)
phot = psfphot(data, error=error)
phot
Fit source/group:   0%|          | 0/428 [00:00<?, ?it/s]
Fit source/group:   4%|▎         | 15/428 [00:00<00:05, 71.27it/s]
Fit source/group:   6%|▌         | 24/428 [00:00<00:05, 71.94it/s]
Fit source/group:   7%|▋         | 32/428 [00:00<00:05, 68.71it/s]
Fit source/group:  10%|█         | 44/428 [00:00<00:04, 82.14it/s]
Fit source/group:  12%|█▏        | 53/428 [00:00<00:04, 80.61it/s]
Fit source/group:  16%|█▌        | 68/428 [00:00<00:03, 100.71it/s]
Fit source/group:  19%|█▉        | 83/428 [00:00<00:03, 106.45it/s]
Fit source/group:  23%|██▎       | 98/428 [00:01<00:02, 117.29it/s]
Fit source/group:  26%|██▌       | 111/428 [00:01<00:02, 115.63it/s]
Fit source/group:  29%|██▊       | 123/428 [00:01<00:02, 106.09it/s]
Fit source/group:  31%|███▏      | 134/428 [00:01<00:02, 104.33it/s]
Fit source/group:  34%|███▍      | 145/428 [00:01<00:03, 91.46it/s] 
Fit source/group:  37%|███▋      | 157/428 [00:01<00:02, 93.69it/s]
Fit source/group:  40%|███▉      | 171/428 [00:01<00:02, 104.03it/s]
Fit source/group:  44%|████▍     | 190/428 [00:01<00:01, 125.77it/s]
Fit source/group:  48%|████▊     | 204/428 [00:02<00:02, 111.42it/s]
Fit source/group:  50%|█████     | 216/428 [00:02<00:01, 110.40it/s]
Fit source/group:  55%|█████▍    | 234/428 [00:02<00:01, 121.85it/s]
Fit source/group:  59%|█████▉    | 252/428 [00:02<00:01, 116.12it/s]
Fit source/group:  62%|██████▏   | 264/428 [00:02<00:01, 112.40it/s]
Fit source/group:  65%|██████▌   | 279/428 [00:02<00:01, 114.79it/s]
Fit source/group:  68%|██████▊   | 291/428 [00:02<00:01, 85.21it/s] 
Fit source/group:  70%|███████   | 301/428 [00:03<00:01, 86.49it/s]
Fit source/group:  74%|███████▍  | 316/428 [00:03<00:01, 99.91it/s]
Fit source/group:  76%|███████▋  | 327/428 [00:03<00:00, 102.13it/s]
Fit source/group:  80%|███████▉  | 341/428 [00:03<00:00, 110.71it/s]
Fit source/group:  83%|████████▎ | 356/428 [00:03<00:00, 113.52it/s]
Fit source/group:  86%|████████▌ | 368/428 [00:03<00:00, 107.94it/s]
Fit source/group:  89%|████████▉ | 380/428 [00:03<00:00, 107.49it/s]
Fit source/group:  92%|█████████▏| 395/428 [00:03<00:00, 117.52it/s]
Fit source/group:  96%|█████████▌| 410/428 [00:03<00:00, 117.89it/s]
Fit source/group:  99%|█████████▊| 422/428 [00:04<00:00, 94.93it/s] 
Fit source/group: 100%|██████████| 428/428 [00:04<00:00, 103.00it/s]

QTable length=500
idgroup_idgroup_sizelocal_bkgx_inity_initflux_initx_fity_fitflux_fitx_erry_errflux_errnpixfitqfitcfitflags
int64int64int64float64float64float64float64float64float64float64float64float64float64int64float64float64int64
1110.01192.105555947458753.066315254689819769.843087850371192.112489062283753.0772602516149912216.3951063618950.0103532876848010870.010454739937857291134.99990751571409256.964462906791501e-05-1.9414090087428652e-070
2210.0391.7710021778335756.6235118920400210790.248508662999391.697778662935756.5554664253189313483.935753657990.0100360000300741340.010311147902097655142.2098454553027257.262967473124619e-051.5702561799616147e-060
3310.0795.808701055418957.7198022633096512480.63854173307795.768867771744957.67414394850296615582.6984276262210.0092673507414353370.009424472595034823152.5984253787596255.509408522073522e-051.0347695107500807e-060
4410.01666.388914969333472.9652134032875912191.945330713211666.457711836316372.9555735533536915252.6349689568690.0094821025808959430.009339545788981335150.73795419248074256.881241053978113e-05-1.0614993730612482e-060
5510.0704.803134726527976.317809886327017511.709303115783704.756525734160476.381588922247849371.0191024917620.011944362642519220.012226019386897076118.02884707087004259.19545389556457e-05-2.921870050780829e-060
6610.0255.6118008299629381.2176398636176815554.781286257943255.505029609327681.2674663433465719447.6824562165820.008403923211633310.008383150624637731170.24146964676584252.6439299122880032e-055.248611419255972e-070
7710.01944.722505998474883.3800654745723211192.1819966948551944.68018744131783.4464009527210413996.4905306635650.0096608503950427250.0100689669263145144.23672212653102256.214553770134094e-051.5302376923467163e-060
8810.01053.61654741262383.6517090670158613461.2694241522041053.532822134258183.579973929530516816.860346914970.0089604239955310350.00909083146993759158.41169638203598255.02870484657219e-05-6.33393214798828e-070
9910.01477.72410659165684.368230408498811196.5468806088431477.662046313027884.4367859141802913995.6543679438710.0097368800672725760.01005074099140739144.17391749959023256.655961259569325e-05-1.2052590474125427e-060
...................................................
49142030.0187.015176123997261961.686782360212914091.10419427188186.995303576565681961.605498899854917045.9205720256980.0087268218267819320.00880456763567827157.34025558699682254.1807543286507106e-056.681096222682628e-070
49242110.0895.18705584277321970.77661954977487299.674693491745895.22003385235781970.7163792942038826.846165746230.0120566253592046860.012073434133258199112.95150122600577250.00014915713688682658-1.8267319309470527e-060
49342210.01924.83886039839331971.34855605822925290.3681472351811924.79961355560321971.40541668702266414.3022528803290.013992315071410680.01479164906684039596.35313024735477250.00024705065610210636-6.55469244497591e-060
49442030.0208.36404730855771979.328511037792614265.293397462816208.41873743267481979.376512289039317259.782258451540.0087862125854728750.00898685568755109158.02354395803448253.4398376724650686e-051.2546980142505045e-060
49542310.050.675759356774331980.826170195422312403.63597596463550.585640713777771980.769669749002615044.8388346769520.0093525793437008260.009209024719499203147.51024512694968255.829512725642079e-051.1758702735527357e-060
49642410.0298.16836794767471985.238159847872816476.589045916437298.180811881254671985.254175348252619961.5499987226360.0080859027646194140.008217072468956876169.8799789849002253.615928010528903e-055.072004194129899e-070
49742510.0346.73020766024841985.14970282802665639.747854874019346.67018819070891985.1498607545616826.5683981902130.0138240200908228430.01378018754686750499.21462670058307250.00019858447210482184-5.117669977632834e-060
49842610.094.063365597896041986.870756390495415919.92879327300694.045248428425171986.825213991678819301.364760767810.0081662290793007680.008069145853684725167.26187845413065254.1991758914826264e-055.458675737839309e-070
49942710.01002.23352415216571992.00763522621836401.16454731769951002.2726662146111991.98854814994377743.8814088127590.0129102699699463420.012728863301181576105.71687949139337250.00013628768057791047.105068485039063e-070
50042810.0553.10117106318591997.884381114232410110.530244567432553.1185983596881997.838513660045212238.4237983373130.0102029052627153230.010108185483084389133.00563190313764257.866862278325297e-051.5745152225618928e-070

The group_id and group_size columns can be used to determine which stars were grouped together and how many stars were in each group.

Fitting a single source#

Instead of finding and fitting all stars in an image, we can instead fit only a few stars or a single star if desired.

Let’s randomly select a single star from our image (circled in red in the image below).

fig, ax = plt.subplots(figsize=(10, 10))
axim = ax.imshow(data, norm=norm2)

x = 743
y = 1044
aper = CircularAperture((x, y), r=20)
patches = aper.plot(color='red')
../../../_images/be76e8ffed01fc542b1861cfb52f0e815cb6029c3e2849139ca00e69ed2dd311.png

In this example, we will not input the star finder (finder = None). Instead, we will input an Astropy table containing initial (x, y) position guesses for the star (or stars) we want to fit. We will still use the aperture_radius to estimate the initial flux. However, we could also include the initial flux in the init_params table, in which case aperture_radius would not be needed.

init_params = QTable()
init_params['x'] = [x]
init_params['y'] = [y]

fit_shape = (5, 5)
psfphot = PSFPhotometry(psf_model, fit_shape, finder=None, 
                        aperture_radius=5, progress_bar=True)
phot = psfphot(data, error=error, init_params=init_params)
phot
Fit source/group:   0%|          | 0/1 [00:00<?, ?it/s]
Fit source/group: 100%|██████████| 1/1 [00:00<00:00, 126.58it/s]

QTable length=1
idgroup_idgroup_sizelocal_bkgx_inity_initflux_initx_fity_fitflux_fitx_erry_errflux_errnpixfitqfitcfitflags
int64int64int64float64int64int64float64float64float64float64float64float64float64int64float64float64int64
1110.074310448379.370482239492742.22090139630481044.687667104565410416.0206911556090.0115259575503136590.012083619814874857123.77285685674893259.143273121175345e-05-4.1186112664838225e-060
resid = psfphot.make_residual_image(data)

fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))
norm = simple_norm(data, 'sqrt', percent=99)
ax[0].imshow(data, norm=norm)
ax[1].imshow(data - resid, norm=norm)
im = ax[2].imshow(resid, norm=norm)
ax[0].set_title('Data')
aper.plot(ax=ax[0], color='red')
ax[1].set_title('Model')
aper.plot(ax=ax[1], color='red')
ax[2].set_title('Residual Image')
aper.plot(ax=ax[2], color='red')
plt.tight_layout()
Add model sources:   0%|          | 0/1 [00:00<?, ?it/s]
Add model sources: 100%|██████████| 1/1 [00:00<00:00, 54.20it/s]

../../../_images/092148788606abbb1648aa65c7eaae47e19bbd3f181b27814f850317e2db5d21.png

Further Reading#

Please consult the PSF Photometry documentation for additional features, including:

  • Forced Photometry

  • Fixed Model Parameters

  • Bounded Model Parameters

  • Iterative PSF Photometry

  • Local Background Subtraction

About this Notebook#

Author: Larry Bradley, Branch Deputy, Data Analysis Tools Branch
Created: 2025-07-16

Space Telescope Logo