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))

fig = psf_model.plot_grid(deltas=True, figsize=(9, 9))

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)

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)

The true_params
output contains an Astropy table containing the true (x, y, flux) of our artificial stars.
true_params
id | x_0 | y_0 | flux |
---|---|---|---|
int64 | float64 | float64 | float64 |
1 | 1290.801366902193 | 1774.3778244148095 | 11576.13482157921 |
2 | 575.5445184120193 | 1395.377339599303 | 19268.478310411472 |
3 | 129.81642462770725 | 988.9497341028731 | 15441.000611839 |
4 | 82.19583400957467 | 1373.681842381788 | 16746.079103988828 |
5 | 1634.2504259621307 | 1678.058048048596 | 11603.97911581131 |
6 | 1828.047864537002 | 309.55371527620554 | 12347.864117392048 |
7 | 1231.7264911944665 | 1364.5189171807363 | 12768.672723356296 |
8 | 1471.059300796829 | 1127.0247989784389 | 5685.779847567518 |
9 | 1108.9814833746439 | 1284.3479577936375 | 12668.089104108565 |
... | ... | ... | ... |
491 | 577.223074730307 | 129.38072578254219 | 14507.876279651628 |
492 | 1361.7045616265877 | 453.27471505007463 | 9846.434914865096 |
493 | 381.1375034624161 | 1156.1905984983352 | 17092.388569016857 |
494 | 341.84068336540014 | 1317.6701821809852 | 19341.760724229327 |
495 | 287.4655271611766 | 211.56488042410663 | 14904.353014873148 |
496 | 198.90321274868666 | 1463.8345851886168 | 5127.760470402655 |
497 | 1090.6820339800868 | 207.53634357628832 | 1297.4411340894303 |
498 | 372.8442840030262 | 215.84440430487842 | 11463.74372055272 |
499 | 1622.3631278221183 | 1306.7074127823937 | 9583.072289053016 |
500 | 94.04531350532247 | 1986.8254840155562 | 19299.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
id | xcentroid | ycentroid | fwhm | sharpness | roundness | pa | npix | peak | flux | mag |
---|---|---|---|---|---|---|---|---|---|---|
int64 | float64 | float64 | float64 | float64 | float64 | float64 | int64 | float64 | float64 | float64 |
1 | 1192.1055559474587 | 53.06631525468981 | 1.7907366317936948 | 0.5969122105978982 | 0.01602124538361244 | 103.50327927086758 | 13 | 1445.5348787044131 | 6702.657403681727 | -9.565617554118727 |
2 | 391.77100217783357 | 56.62351189204002 | 1.7326236218111648 | 0.5775412072703883 | 0.07734114701673132 | 127.67294625687012 | 11 | 1345.8273140582155 | 6834.501827950182 | -9.586767160003873 |
3 | 795.8087010554189 | 57.71980226330965 | 1.7547915517116766 | 0.5849305172372256 | 0.050923983146313495 | 123.76952554697297 | 11 | 1685.9495568301768 | 8212.884033591004 | -9.786239226517 |
4 | 1666.3889149693334 | 72.96521340328759 | 1.7369949400842508 | 0.5789983133614169 | 0.012736558230433365 | 24.64335109216909 | 12 | 1602.3725342184905 | 7894.772125702702 | -9.743348997678982 |
5 | 704.8031347265279 | 76.31780988632701 | 1.7383697976747048 | 0.5794565992249016 | 0.05728164986451659 | 56.71067148674254 | 11 | 990.6214951513647 | 4832.501058805616 | -9.210429894637347 |
6 | 255.61180082996293 | 81.21763986361768 | 1.7183973200128126 | 0.5727991066709376 | 0.07151425359444646 | 45.27550776264768 | 11 | 1926.1576546941526 | 9684.86666304069 | -9.96523411480502 |
7 | 1944.7225059984748 | 83.38006547457232 | 1.7142929583769968 | 0.5714309861256656 | 0.0951854120780369 | 50.43115402630989 | 11 | 1373.903054909001 | 6972.668462567562 | -9.608497539499997 |
8 | 1053.616547412623 | 83.65170906701586 | 1.7037246530865695 | 0.5679082176955231 | 0.11475912048574714 | 132.33752219804214 | 11 | 1564.2286424835092 | 8224.770241215145 | -9.78780943696046 |
9 | 1477.724106591656 | 84.3682304084988 | 1.7177369948220635 | 0.5725789982740211 | 0.09044626722207788 | 50.49923134540043 | 11 | 1384.7129854570685 | 6992.860483427065 | -9.611637158981052 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
491 | 187.01517612399726 | 1961.6867823602129 | 1.7735654370735714 | 0.5911884790245238 | 0.02963407181044025 | 85.85191897548906 | 12 | 1897.7156445314984 | 9309.387614483894 | -9.92230278344215 |
492 | 895.1870558427732 | 1970.7766195497748 | 1.7728537206217967 | 0.5909512402072655 | 0.04256035776636366 | 59.0689822477808 | 11 | 998.7779687143158 | 4858.1778542642005 | -9.21618352489223 |
493 | 1924.8388603983933 | 1971.3485560582292 | 1.7479505422621833 | 0.582650180754061 | 0.06452058663798878 | 60.81777183363713 | 11 | 680.9536453365565 | 3406.850837427257 | -8.830882798037411 |
494 | 208.3640473085577 | 1979.3285110377926 | 1.7209611144208048 | 0.5736537048069349 | 0.10521499796303566 | 129.55419310402846 | 11 | 1673.126539006942 | 8731.297849157323 | -9.852697008700918 |
495 | 50.67575935677433 | 1980.8261701954223 | 1.7506545676674383 | 0.5835515225558128 | 0.046862608373343786 | 134.0009960951959 | 11 | 1609.0456617773204 | 7994.260167861066 | -9.756945694945262 |
496 | 298.1683679476747 | 1985.2381598478728 | 1.7680444963586066 | 0.5893481654528688 | 0.041227666901465045 | 117.76380702044993 | 11 | 2251.173448406043 | 10894.287867528958 | -10.092997116902998 |
497 | 346.7302076602484 | 1985.1497028280266 | 1.7656538412623042 | 0.5885512804207681 | 0.0424913962802405 | 52.25300351217583 | 12 | 758.4319863235332 | 3700.4518121683222 | -8.920636882837902 |
498 | 94.06336559789604 | 1986.8707563904954 | 1.8054058182738617 | 0.6018019394246206 | 0.021510498170457495 | 79.44898612901693 | 13 | 2292.261130680258 | 10890.066981230495 | -10.092576377417293 |
499 | 1002.2335241521657 | 1992.0076352262183 | 1.7877636718214875 | 0.5959212239404958 | 0.02090867637914325 | 91.59256409810243 | 12 | 897.59333148135 | 4317.032305987015 | -9.08796324664611 |
500 | 553.1011710631859 | 1997.8843811142324 | 1.803976688551649 | 0.6013255628505497 | 0.023639246939983413 | 75.78418013743854 | 13 | 1455.404192249984 | 6923.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')

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]
id | group_id | group_size | local_bkg | x_init | y_init | flux_init | x_fit | y_fit | flux_fit | x_err | y_err | flux_err | npixfit | qfit | cfit | flags |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
int64 | int64 | int64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | int64 | float64 | float64 | int64 |
1 | 1 | 1 | 0.0 | 1192.1055559474587 | 53.06631525468981 | 9769.84308785037 | 1192.1124890622837 | 53.07726025161499 | 12216.395106361895 | 0.010353287684801087 | 0.010454739937857291 | 134.99990751571409 | 25 | 6.964462906791501e-05 | -1.9414090087428652e-07 | 0 |
2 | 2 | 1 | 0.0 | 391.77100217783357 | 56.62351189204002 | 10790.248508662999 | 391.6977786629357 | 56.55546642531893 | 13483.93575365799 | 0.010036000030074134 | 0.010311147902097655 | 142.2098454553027 | 25 | 7.262967473124619e-05 | 1.5702561799616147e-06 | 0 |
3 | 3 | 1 | 0.0 | 795.8087010554189 | 57.71980226330965 | 12480.63854173307 | 795.7688677717449 | 57.674143948502966 | 15582.698427626221 | 0.009267350741435337 | 0.009424472595034823 | 152.5984253787596 | 25 | 5.509408522073522e-05 | 1.0347695107500807e-06 | 0 |
4 | 4 | 1 | 0.0 | 1666.3889149693334 | 72.96521340328759 | 12191.94533071321 | 1666.4577118363163 | 72.95557355335369 | 15252.634968956869 | 0.009482102580895943 | 0.009339545788981335 | 150.73795419248074 | 25 | 6.881241053978113e-05 | -1.0614993730612482e-06 | 0 |
5 | 5 | 1 | 0.0 | 704.8031347265279 | 76.31780988632701 | 7511.709303115783 | 704.7565257341604 | 76.38158892224784 | 9371.019102491762 | 0.01194436264251922 | 0.012226019386897076 | 118.02884707087004 | 25 | 9.19545389556457e-05 | -2.921870050780829e-06 | 0 |
6 | 6 | 1 | 0.0 | 255.61180082996293 | 81.21763986361768 | 15554.781286257943 | 255.5050296093276 | 81.26746634334657 | 19447.682456216582 | 0.00840392321163331 | 0.008383150624637731 | 170.24146964676584 | 25 | 2.6439299122880032e-05 | 5.248611419255972e-07 | 0 |
7 | 7 | 1 | 0.0 | 1944.7225059984748 | 83.38006547457232 | 11192.181996694855 | 1944.680187441317 | 83.44640095272104 | 13996.490530663565 | 0.009660850395042725 | 0.0100689669263145 | 144.23672212653102 | 25 | 6.214553770134094e-05 | 1.5302376923467163e-06 | 0 |
8 | 8 | 1 | 0.0 | 1053.616547412623 | 83.65170906701586 | 13461.269424152204 | 1053.5328221342581 | 83.5799739295305 | 16816.86034691497 | 0.008960423995531035 | 0.00909083146993759 | 158.41169638203598 | 25 | 5.02870484657219e-05 | -6.33393214798828e-07 | 0 |
9 | 9 | 1 | 0.0 | 1477.724106591656 | 84.3682304084988 | 11196.546880608843 | 1477.6620463130278 | 84.43678591418029 | 13995.654367943871 | 0.009736880067272576 | 0.01005074099140739 | 144.17391749959023 | 25 | 6.655961259569325e-05 | -1.2052590474125427e-06 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
491 | 491 | 1 | 0.0 | 187.01517612399726 | 1961.6867823602129 | 14091.10419427188 | 186.99536616609942 | 1961.6056058351749 | 17059.150312912396 | 0.00872003814042141 | 0.008797392055912423 | 157.34018685339808 | 25 | 4.870744731974836e-05 | -1.0226874447617079e-06 | 0 |
492 | 492 | 1 | 0.0 | 895.1870558427732 | 1970.7766195497748 | 7299.674693491745 | 895.2200338523578 | 1970.716379294203 | 8826.84616574623 | 0.012056625359204686 | 0.012073434133258199 | 112.95150122600577 | 25 | 0.00014915713688682658 | -1.8267319309470527e-06 | 0 |
493 | 493 | 1 | 0.0 | 1924.8388603983933 | 1971.3485560582292 | 5290.368147235181 | 1924.7996135556032 | 1971.4054166870226 | 6414.302252880329 | 0.01399231507141068 | 0.014791649066840395 | 96.35313024735477 | 25 | 0.00024705065610210636 | -6.55469244497591e-06 | 0 |
494 | 494 | 1 | 0.0 | 208.3640473085577 | 1979.3285110377926 | 14265.293397462816 | 208.41866786536542 | 1979.3764337768603 | 17275.294570094782 | 0.008778316311907916 | 0.008978833297112715 | 158.0236947235806 | 25 | 3.41633956719112e-05 | -6.098326727324719e-07 | 0 |
495 | 495 | 1 | 0.0 | 50.67575935677433 | 1980.8261701954223 | 12403.635975964635 | 50.58564071377777 | 1980.7696697490026 | 15044.838834676952 | 0.009352579343700826 | 0.009209024719499203 | 147.51024512694968 | 25 | 5.829512725642079e-05 | 1.1758702735527357e-06 | 0 |
496 | 496 | 1 | 0.0 | 298.1683679476747 | 1985.2381598478728 | 16476.589045916437 | 298.18081188125467 | 1985.2541753482526 | 19961.549998722636 | 0.008085902764619414 | 0.008217072468956876 | 169.8799789849002 | 25 | 3.615928010528903e-05 | 5.072004194129899e-07 | 0 |
497 | 497 | 1 | 0.0 | 346.7302076602484 | 1985.1497028280266 | 5639.747854874019 | 346.6701881907089 | 1985.149860754561 | 6826.568398190213 | 0.013824020090822843 | 0.013780187546867504 | 99.21462670058307 | 25 | 0.00019858447210482184 | -5.117669977632834e-06 | 0 |
498 | 498 | 1 | 0.0 | 94.06336559789604 | 1986.8707563904954 | 15919.928793273006 | 94.04524842842517 | 1986.8252139916788 | 19301.36476076781 | 0.008166229079300768 | 0.008069145853684725 | 167.26187845413065 | 25 | 4.1991758914826264e-05 | 5.458675737839309e-07 | 0 |
499 | 499 | 1 | 0.0 | 1002.2335241521657 | 1992.0076352262183 | 6401.1645473176995 | 1002.272666214611 | 1991.9885481499437 | 7743.881408812759 | 0.012910269969946342 | 0.012728863301181576 | 105.71687949139337 | 25 | 0.0001362876805779104 | 7.105068485039063e-07 | 0 |
500 | 500 | 1 | 0.0 | 553.1011710631859 | 1997.8843811142324 | 10110.530244567432 | 553.118598359688 | 1997.8385136600452 | 12238.423798337313 | 0.010202905262715323 | 0.010108185483084389 | 133.00563190313764 | 25 | 7.866862278325297e-05 | 1.5745152225618928e-07 | 0 |
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]

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]

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()

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)')

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]
id | xcentroid | ycentroid | fwhm | sharpness | roundness | pa | npix | peak | flux | mag |
---|---|---|---|---|---|---|---|---|---|---|
int64 | float64 | float64 | float64 | float64 | float64 | float64 | int64 | float64 | float64 | float64 |
1 | 1192.1055559474587 | 53.06631525468981 | 1.7907366317936948 | 0.5969122105978982 | 0.01602124538361244 | 103.50327927086758 | 13 | 1445.5348787044131 | 6702.657403681727 | -9.565617554118727 |
2 | 391.77100217783357 | 56.62351189204002 | 1.7326236218111648 | 0.5775412072703883 | 0.07734114701673132 | 127.67294625687012 | 11 | 1345.8273140582155 | 6834.501827950182 | -9.586767160003873 |
3 | 795.8087010554189 | 57.71980226330965 | 1.7547915517116766 | 0.5849305172372256 | 0.050923983146313495 | 123.76952554697297 | 11 | 1685.9495568301768 | 8212.884033591004 | -9.786239226517 |
4 | 1666.3889149693334 | 72.96521340328759 | 1.7369949400842508 | 0.5789983133614169 | 0.012736558230433365 | 24.64335109216909 | 12 | 1602.3725342184905 | 7894.772125702702 | -9.743348997678982 |
5 | 704.8031347265279 | 76.31780988632701 | 1.7383697976747048 | 0.5794565992249016 | 0.05728164986451659 | 56.71067148674254 | 11 | 990.6214951513647 | 4832.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)

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)

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]
id | group_id | group_size | local_bkg | x_init | y_init | flux_init | x_fit | y_fit | flux_fit | x_err | y_err | flux_err | npixfit | qfit | cfit | flags |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
int64 | int64 | int64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | int64 | float64 | float64 | int64 |
1 | 1 | 1 | 0.0 | 1192.1055559474587 | 53.06631525468981 | 9769.84308785037 | 1192.1124890622837 | 53.07726025161499 | 12216.395106361895 | 0.010353287684801087 | 0.010454739937857291 | 134.99990751571409 | 25 | 6.964462906791501e-05 | -1.9414090087428652e-07 | 0 |
2 | 2 | 1 | 0.0 | 391.77100217783357 | 56.62351189204002 | 10790.248508662999 | 391.6977786629357 | 56.55546642531893 | 13483.93575365799 | 0.010036000030074134 | 0.010311147902097655 | 142.2098454553027 | 25 | 7.262967473124619e-05 | 1.5702561799616147e-06 | 0 |
3 | 3 | 1 | 0.0 | 795.8087010554189 | 57.71980226330965 | 12480.63854173307 | 795.7688677717449 | 57.674143948502966 | 15582.698427626221 | 0.009267350741435337 | 0.009424472595034823 | 152.5984253787596 | 25 | 5.509408522073522e-05 | 1.0347695107500807e-06 | 0 |
4 | 4 | 1 | 0.0 | 1666.3889149693334 | 72.96521340328759 | 12191.94533071321 | 1666.4577118363163 | 72.95557355335369 | 15252.634968956869 | 0.009482102580895943 | 0.009339545788981335 | 150.73795419248074 | 25 | 6.881241053978113e-05 | -1.0614993730612482e-06 | 0 |
5 | 5 | 1 | 0.0 | 704.8031347265279 | 76.31780988632701 | 7511.709303115783 | 704.7565257341604 | 76.38158892224784 | 9371.019102491762 | 0.01194436264251922 | 0.012226019386897076 | 118.02884707087004 | 25 | 9.19545389556457e-05 | -2.921870050780829e-06 | 0 |
6 | 6 | 1 | 0.0 | 255.61180082996293 | 81.21763986361768 | 15554.781286257943 | 255.5050296093276 | 81.26746634334657 | 19447.682456216582 | 0.00840392321163331 | 0.008383150624637731 | 170.24146964676584 | 25 | 2.6439299122880032e-05 | 5.248611419255972e-07 | 0 |
7 | 7 | 1 | 0.0 | 1944.7225059984748 | 83.38006547457232 | 11192.181996694855 | 1944.680187441317 | 83.44640095272104 | 13996.490530663565 | 0.009660850395042725 | 0.0100689669263145 | 144.23672212653102 | 25 | 6.214553770134094e-05 | 1.5302376923467163e-06 | 0 |
8 | 8 | 1 | 0.0 | 1053.616547412623 | 83.65170906701586 | 13461.269424152204 | 1053.5328221342581 | 83.5799739295305 | 16816.86034691497 | 0.008960423995531035 | 0.00909083146993759 | 158.41169638203598 | 25 | 5.02870484657219e-05 | -6.33393214798828e-07 | 0 |
9 | 9 | 1 | 0.0 | 1477.724106591656 | 84.3682304084988 | 11196.546880608843 | 1477.6620463130278 | 84.43678591418029 | 13995.654367943871 | 0.009736880067272576 | 0.01005074099140739 | 144.17391749959023 | 25 | 6.655961259569325e-05 | -1.2052590474125427e-06 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
491 | 420 | 3 | 0.0 | 187.01517612399726 | 1961.6867823602129 | 14091.10419427188 | 186.99530357656568 | 1961.6054988998549 | 17045.920572025698 | 0.008726821826781932 | 0.00880456763567827 | 157.34025558699682 | 25 | 4.1807543286507106e-05 | 6.681096222682628e-07 | 0 |
492 | 421 | 1 | 0.0 | 895.1870558427732 | 1970.7766195497748 | 7299.674693491745 | 895.2200338523578 | 1970.716379294203 | 8826.84616574623 | 0.012056625359204686 | 0.012073434133258199 | 112.95150122600577 | 25 | 0.00014915713688682658 | -1.8267319309470527e-06 | 0 |
493 | 422 | 1 | 0.0 | 1924.8388603983933 | 1971.3485560582292 | 5290.368147235181 | 1924.7996135556032 | 1971.4054166870226 | 6414.302252880329 | 0.01399231507141068 | 0.014791649066840395 | 96.35313024735477 | 25 | 0.00024705065610210636 | -6.55469244497591e-06 | 0 |
494 | 420 | 3 | 0.0 | 208.3640473085577 | 1979.3285110377926 | 14265.293397462816 | 208.4187374326748 | 1979.3765122890393 | 17259.78225845154 | 0.008786212585472875 | 0.00898685568755109 | 158.02354395803448 | 25 | 3.4398376724650686e-05 | 1.2546980142505045e-06 | 0 |
495 | 423 | 1 | 0.0 | 50.67575935677433 | 1980.8261701954223 | 12403.635975964635 | 50.58564071377777 | 1980.7696697490026 | 15044.838834676952 | 0.009352579343700826 | 0.009209024719499203 | 147.51024512694968 | 25 | 5.829512725642079e-05 | 1.1758702735527357e-06 | 0 |
496 | 424 | 1 | 0.0 | 298.1683679476747 | 1985.2381598478728 | 16476.589045916437 | 298.18081188125467 | 1985.2541753482526 | 19961.549998722636 | 0.008085902764619414 | 0.008217072468956876 | 169.8799789849002 | 25 | 3.615928010528903e-05 | 5.072004194129899e-07 | 0 |
497 | 425 | 1 | 0.0 | 346.7302076602484 | 1985.1497028280266 | 5639.747854874019 | 346.6701881907089 | 1985.149860754561 | 6826.568398190213 | 0.013824020090822843 | 0.013780187546867504 | 99.21462670058307 | 25 | 0.00019858447210482184 | -5.117669977632834e-06 | 0 |
498 | 426 | 1 | 0.0 | 94.06336559789604 | 1986.8707563904954 | 15919.928793273006 | 94.04524842842517 | 1986.8252139916788 | 19301.36476076781 | 0.008166229079300768 | 0.008069145853684725 | 167.26187845413065 | 25 | 4.1991758914826264e-05 | 5.458675737839309e-07 | 0 |
499 | 427 | 1 | 0.0 | 1002.2335241521657 | 1992.0076352262183 | 6401.1645473176995 | 1002.272666214611 | 1991.9885481499437 | 7743.881408812759 | 0.012910269969946342 | 0.012728863301181576 | 105.71687949139337 | 25 | 0.0001362876805779104 | 7.105068485039063e-07 | 0 |
500 | 428 | 1 | 0.0 | 553.1011710631859 | 1997.8843811142324 | 10110.530244567432 | 553.118598359688 | 1997.8385136600452 | 12238.423798337313 | 0.010202905262715323 | 0.010108185483084389 | 133.00563190313764 | 25 | 7.866862278325297e-05 | 1.5745152225618928e-07 | 0 |
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')

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]
id | group_id | group_size | local_bkg | x_init | y_init | flux_init | x_fit | y_fit | flux_fit | x_err | y_err | flux_err | npixfit | qfit | cfit | flags |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
int64 | int64 | int64 | float64 | int64 | int64 | float64 | float64 | float64 | float64 | float64 | float64 | float64 | int64 | float64 | float64 | int64 |
1 | 1 | 1 | 0.0 | 743 | 1044 | 8379.370482239492 | 742.2209013963048 | 1044.6876671045654 | 10416.020691155609 | 0.011525957550313659 | 0.012083619814874857 | 123.77285685674893 | 25 | 9.143273121175345e-05 | -4.1186112664838225e-06 | 0 |
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]

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
