\n",
"\n",
"Here we limit the maximum number of iterations to 3 (to limit it’s run time), but in practice one should use about 10 or more iterations.\n",
" \n",
"
"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "997de3d7",
"metadata": {},
"outputs": [],
"source": [
"def build_epsf(det='NRCA1', filt='F070W', size=11, found_table=None, oversample=4, iters=10):\n",
" \n",
" hsize = (size - 1) / 2\n",
" \n",
" x = found_table['xcentroid']\n",
" y = found_table['ycentroid']\n",
" \n",
" mask = ((x > hsize) & (x < (data.shape[1] - 1 - hsize)) & (y > hsize) & (y < (data.shape[0] - 1 - hsize)))\n",
"\n",
" stars_tbl = Table()\n",
" stars_tbl['x'] = x[mask]\n",
" stars_tbl['y'] = y[mask]\n",
" \n",
" data_bkgsub, _ = calc_bkg()\n",
" \n",
" nddata = NDData(data=data_bkgsub)\n",
" stars = extract_stars(nddata, stars_tbl, size=size)\n",
"\n",
" print('Creating ePSF --- Detector {d}, filter {f}'.format(f=filt, d=det))\n",
"\n",
" epsf_builder = EPSFBuilder(oversampling=oversample, maxiters=iters, progress_bar=True)\n",
"\n",
" epsf, fitted_stars = epsf_builder(stars)\n",
" \n",
" return epsf"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f473acae",
"metadata": {},
"outputs": [],
"source": [
"#epsf = ..."
]
},
{
"cell_type": "markdown",
"id": "e6d002e7",
"metadata": {},
"source": [
"### 5.4
-Display the effective PSF
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "064ab4fa",
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(12, 12))\n",
"\n",
"ax = plt.subplot(1, 1, 1)\n",
"\n",
"norm_epsf = simple_norm(epsf.data, 'log', percent=99.)\n",
"plt.title(filt, fontsize=30)\n",
"ax.imshow(epsf.data, norm=norm_epsf)\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "315795e8",
"metadata": {},
"source": [
"6.
-Perform PSF Photometry
\n",
"------------------"
]
},
{
"cell_type": "markdown",
"id": "73165380",
"metadata": {},
"source": [
"For general information on PSF Photometry with PhotUtils see [here](https://photutils.readthedocs.io/en/stable/psf.html). \n",
"\n",
"Photutils provides three classes to perform PSF Photometry: [BasicPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.BasicPSFPhotometry.html#photutils.psf.BasicPSFPhotometry), [IterativelySubtractedPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.IterativelySubtractedPSFPhotometry.html#photutils.psf.IterativelySubtractedPSFPhotometry), and [DAOPhotPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.DAOPhotPSFPhotometry.html#photutils.psf.DAOPhotPSFPhotometry). Together these provide the core workflow to make photometric measurements given an appropriate PSF (or other) model.\n",
"\n",
"[BasicPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.BasicPSFPhotometry.html#photutils.psf.BasicPSFPhotometry) implements the minimum tools for model-fitting photometry. At its core, this involves finding sources in an image, grouping overlapping sources into a single model, fitting the model to the sources, and subtracting the models from the image. In DAOPHOT parlance, this is essentially running the “FIND, GROUP, NSTAR, SUBTRACT” once.\n",
"\n",
"[IterativelySubtractedPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.IterativelySubtractedPSFPhotometry.html#photutils.psf.IterativelySubtractedPSFPhotometry) (adopted here) is similar to [BasicPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.BasicPSFPhotometry.html#photutils.psf.BasicPSFPhotometry), but it adds a parameter called `n_iters` which is the number of iterations for which the loop “FIND, GROUP, NSTAR, SUBTRACT, FIND…” will be performed. This class enables photometry in a scenario where there exists significant overlap between stars that are of quite different brightness. For instance, the detection algorithm may not be able to detect a faint and bright star very close together in the first iteration, but they will be detected in the next iteration after the brighter stars have been fit and subtracted. Like [BasicPSFPhotometry](https://photutils.readthedocs.io/en/stable/api/photutils.psf.BasicPSFPhotometry.html#photutils.psf.BasicPSFPhotometry), it does not include implementations of the stages of this process, but it provides the structure in which those stages run.\n",
"\n",
"**Important parameters**:\n",
"\n",
"* `finder`: classes to find stars in the image. We use [DAOStarFinder](https://photutils.readthedocs.io/en/stable/api/photutils.detection.DAOStarFinder.html).\n",
"\n",
"* `group_maker`: clustering algorithm in order to label the sources according to groups. We use [DAOGroup](https://photutils.readthedocs.io/en/stable/api/photutils.psf.DAOGroup.html#photutils.psf.DAOGroup). The method group_stars divides an entire starlist into sets of distinct, self-contained groups of mutually overlapping stars. It accepts as input a list of stars and determines which stars are close enough to be capable of adversely influencing each others’ profile fits. [DAOGroup](https://photutils.readthedocs.io/en/stable/api/photutils.psf.DAOGroup.html#photutils.psf.DAOGroup) aceepts one parameter, `crit_separation`, which is the distance, in units of pixels, such that any two stars separated by less than this distance will be placed in the same group.\n",
"\n",
"* `fitter`: algorithm to fit the sources simultaneously for each group. We use an astropy fitter, [LevMarLSQFitter](https://docs.astropy.org/en/stable/api/astropy.modeling.fitting.LevMarLSQFitter.html#astropy.modeling.fitting.LevMarLSQFitter). \n",
"\n",
"* `niters`: number of iterations for which the \"psf photometry\" loop described above is performed.\n",
"\n",
"* `fitshape`: Rectangular shape around the center of a star which will be used to collect the data to do the fitting. \n",
"\n",
"* `aperture_radius`: The radius (in units of pixels) used to compute initial estimates for the fluxes of sources."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2d2c4c4f",
"metadata": {},
"outputs": [],
"source": [
"def psf_phot(data=None, det='NRCA1', filt='F070W', th=2000, psf=None, ap_radius=3.5, save_residuals=False, \n",
" save_output=False):\n",
"\n",
" fitter = LevMarLSQFitter()\n",
" mmm_bkg = MMMBackground()\n",
" \n",
" sigma_psf = dict_utils[filt]['psf fwhm']\n",
" print('FWHM for filter {f}:'.format(f=filt), sigma_psf)\n",
" \n",
" _,std = calc_bkg()\n",
" \n",
" daofind = DAOStarFinder(threshold=th * std, fwhm=sigma_psf)\n",
" \n",
" daogroup = DAOGroup(5.0 * sigma_psf)\n",
" \n",
" psf_model = psf.copy()\n",
" \n",
" print('Performing the PSF photometry --- Detector {d}, filter {f}'.format(f=filt, d=det))\n",
" \n",
" tic = time.perf_counter()\n",
" \n",
" phot = IterativelySubtractedPSFPhotometry(finder=daofind, group_maker=daogroup,\n",
" bkg_estimator=mmm_bkg, psf_model=psf_model,\n",
" fitter=LevMarLSQFitter(),\n",
" niters=2, fitshape=(11, 11), aperture_radius=ap_radius, \n",
" extra_output_cols=('sharpness', 'roundness2'))\n",
" result = phot(data)\n",
" \n",
" toc = time.perf_counter()\n",
" \n",
" print('Time needed to perform photometry:', '%.2f' % ((toc - tic) / 3600), 'hours')\n",
" print('Number of sources detected:', len(result))\n",
" \n",
" residual_image = phot.get_residual_image()\n",
" \n",
" # save the residual images as fits file:\n",
"\n",
" if save_residuals:\n",
" hdu = fits.PrimaryHDU(residual_image)\n",
" hdul = fits.HDUList([hdu])\n",
" \n",
" residual_outname = 'residual_%s_%s.fits' % (det, filt)\n",
"\n",
" hdul.writeto(os.path.join(res_dir, residual_outname))\n",
"\n",
"\n",
" # save the output photometry Tables\n",
"\n",
" if save_output:\n",
"\n",
" outname = 'phot_%s_%s.pkl' % (det, filt)\n",
" \n",
" tab = result.to_pandas()\n",
" tab.to_pickle(os.path.join(output_phot_dir, outname))\n",
" \n",
" return result, residual_image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "934742c9",
"metadata": {},
"outputs": [],
"source": [
"# During the Webbinar we use a cutout of the whole image to speed up the reduction process\n",
"\n",
"data1 = data[0:500,0:500]\n",
"#data1 = data\n",
"\n",
"output_phot_dir = 'PHOT_OUTPUT/'\n",
"\n",
"if not os.path.exists(output_phot_dir):\n",
" os.makedirs(output_phot_dir)\n",
"\n",
"res_dir = 'RESIDUAL_IMAGES/'\n",
"\n",
"if not os.path.exists(res_dir):\n",
" os.makedirs(res_dir)\n",
"\n",
"if glob.glob(os.path.join(res_dir, 'residual*.fits')):\n",
" print('Deleting Residual images from directory')\n",
" files = glob.glob(os.path.join(res_dir, 'residual*.fits'))\n",
" for file in files:\n",
" os.remove(file)\n",
"\n",
"#psf_phot_results, residual_image = ..."
]
},
{
"cell_type": "markdown",
"id": "2fcd71de",
"metadata": {},
"source": [
"### 6.1
-PSF photometry output catalog
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "861c03d6",
"metadata": {},
"outputs": [],
"source": [
"psf_phot_results"
]
},
{
"cell_type": "markdown",
"id": "6feb47a5",
"metadata": {},
"source": [
"### 6.2
-Display residual image
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8eb3b045",
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(14, 14))\n",
"\n",
"ax1 = plt.subplot(2, 2, 1)\n",
"\n",
"plt.xlabel(\"X [px]\", fontdict=font2)\n",
"plt.ylabel(\"Y [px]\", fontdict=font2)\n",
"plt.title(filt, fontdict=font2)\n",
"\n",
"norm = simple_norm(data1, 'sqrt', percent=99.)\n",
"ax1.imshow(data1, norm=norm, cmap='Greys')\n",
"\n",
"ax2 = plt.subplot(2, 2, 2)\n",
"\n",
"plt.xlabel(\"X [px]\", fontdict=font2)\n",
"plt.ylabel(\"Y [px]\", fontdict=font2)\n",
"plt.title('residuals', fontdict=font2)\n",
"\n",
"norm = simple_norm(data1, 'sqrt', percent=99.)\n",
"ax2.imshow(residual_image, norm=norm, cmap='Greys')\n",
"\n",
"ax3 = plt.subplot(2, 2, 3)\n",
"\n",
"plt.xlabel(\"X [px]\", fontdict=font2)\n",
"plt.ylabel(\"Y [px]\", fontdict=font2)\n",
"plt.title(filt, fontdict=font2)\n",
"\n",
"norm = simple_norm(data, 'sqrt', percent=99.)\n",
"ax3.imshow(data, norm=norm, cmap='Greys')\n",
"\n",
"ax4 = plt.subplot(2, 2, 4)\n",
"\n",
"if os.path.isfile('./residual_webbpsf_grid16_NRCB1_F115W.fits'):\n",
" res_f115w = './residual_webbpsf_grid16_NRCB1_F115W.fits'\n",
"\n",
"else:\n",
" print('Downloading F115W residual image')\n",
" \n",
" boxlink_res_f115w = 'https://stsci.box.com/shared/static/g4ffi7zowwlj91up4nkqxz38c1l4tpjx.fits'\n",
" boxfile_res_f115w = './residual_webbpsf_grid16_NRCB1_F115W.fits'\n",
" urllib.request.urlretrieve(boxlink_res_f115w, boxfile_res_f115w)\n",
" res_f115w = boxfile_res_f115w\n",
"\n",
"if os.path.isfile('./residual_webbpsf_grid16_NRCB1_F200W.fits'):\n",
" res_f200w = './residual_webbpsf_grid16_NRCB1_F200W.fits'\n",
"\n",
"else:\n",
" print('Downloading F200W residual image')\n",
" \n",
" boxlink_res_f200w = 'https://stsci.box.com/shared/static/mssn25cokiwfwco9f289nds7lennfgvv.fits'\n",
" boxfile_res_f200w = './residual_webbpsf_grid16_NRCB1_F200W.fits'\n",
" urllib.request.urlretrieve(boxlink_res_f200w, boxfile_res_f200w)\n",
" res_f200w = boxfile_res_f200w\n",
"\n",
"residual = res_f115w\n",
"\n",
"residual = fits.open(residual)\n",
"res_data = residual[0].data\n",
"\n",
"plt.xlabel(\"X [px]\", fontdict=font2)\n",
"plt.ylabel(\"Y [px]\", fontdict=font2)\n",
"plt.title('residuals', fontdict=font2)\n",
"\n",
"ax4.imshow(res_data, norm=norm, cmap='Greys')\n",
"\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "ad4208d4",
"metadata": {},
"source": [
"7.
-Exercise: perform PSF photometry on the other filter
\n",
"------------------"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1425cab5",
"metadata": {},
"outputs": [],
"source": [
"# image = ...\n",
"# im = ...\n",
"# f = ..."
]
},
{
"cell_type": "markdown",
"id": "837b064f",
"metadata": {},
"source": [
"Display image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a0c24ce0",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "985d35ee",
"metadata": {},
"source": [
"Convert image to DN/s and apply PAM"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2f9e0b15",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "95c3054b",
"metadata": {},
"source": [
"Create a synthetic PSF with WebbPSF (single or grid)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "45b5dea5",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "26d4dd66",
"metadata": {},
"source": [
"Display the synthetic PSF "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c29ab0cb",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "bcf8e0bb",
"metadata": {},
"source": [
"Build an effective PSF\n",
" - Find stars\n",
" - Select sources using DaoStarFinder diagnostics\n",
" - Create catalog for selected sources\n",
" - Build effective PSF\n",
" - Display the effective PSF"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4fcc902f",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "211463ba",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "60374d6a",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "32665568",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "cdf6108c",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "3f6f5be9",
"metadata": {},
"source": [
"Perform PSF photometry \n",
"\n",
"**Note**: remember to create a cutout of the original image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2bf393ec",
"metadata": {},
"outputs": [],
"source": [
"# data = ..."
]
},
{
"cell_type": "markdown",
"id": "f56d91d6",
"metadata": {},
"source": [
"Display the residual image"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "66cded6b",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "6d2e034c",
"metadata": {},
"source": [
"8.
-Bonus part I: create your first NIRCam Color-Magnitude Diagram
\n",
"------------------"
]
},
{
"cell_type": "markdown",
"id": "58259625",
"metadata": {},
"source": [
"### 8.1
-Load images and output catalogs
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c5440b51",
"metadata": {},
"outputs": [],
"source": [
"if os.path.isfile('./phot_webbpsf_grid16_NRCB1_F115W.pkl'):\n",
" phot_f115w = './phot_webbpsf_grid16_NRCB1_F115W.pkl'\n",
"\n",
"else:\n",
" print('Downloading F115W PSF photometry')\n",
" \n",
" boxlink_ph_f115w = 'https://stsci.box.com/shared/static/f4s4ziwh7tb3827g0ac362swwta28lti.pkl'\n",
" boxfile_ph_f115w = './phot_webbpsf_grid16_NRCB1_F115W.pkl'\n",
" urllib.request.urlretrieve(boxlink_ph_f115w, boxfile_ph_f115w)\n",
" phot_f115w = boxfile_ph_f115w\n",
"\n",
"if os.path.isfile('./phot_webbpsf_grid16_NRCB1_F200W.pkl'):\n",
" phot_f200w = './phot_webbpsf_grid16_NRCB1_F200W.pkl'\n",
"\n",
"else:\n",
" print('Downloading F200W PSF Photometry')\n",
" \n",
" boxlink_ph_f200w = 'https://stsci.box.com/shared/static/983dxnxqz594ogn00e6m7ek7vq22gifr.pkl'\n",
" boxfile_ph_f200w = './phot_webbpsf_grid16_NRCB1_F200W.pkl'\n",
" urllib.request.urlretrieve(boxlink_ph_f200w, boxfile_ph_f200w)\n",
" phot_f200w = boxfile_ph_f200w\n",
"\n",
"\n",
"ph_f115w = pd.read_pickle(phot_f115w)\n",
"ph_f200w = pd.read_pickle(phot_f200w)\n",
"\n",
"results_f115w = QTable.from_pandas(ph_f115w)\n",
"results_f200w = QTable.from_pandas(ph_f200w)\n",
"\n",
"image_f115w = ImageModel('./jw00042001001_01101_00001_nrcb1_cal.fits')\n",
"image_f200w = ImageModel('./jw00042001001_01101_00005_nrcb1_cal.fits')"
]
},
{
"cell_type": "markdown",
"id": "72643e44",
"metadata": {},
"source": [
"### 8.2
-Cross-match PSF photometry catalogs
###"
]
},
{
"cell_type": "markdown",
"id": "003f1010",
"metadata": {},
"source": [
"We select only stars with positive flux and we use the information from the image WCS to transform detector coordinates (x,y) to celestial coordinate (RA,Dec). \n",
"\n",
"We use the [SkyCoord](https://docs.astropy.org/en/stable/api/astropy.coordinates.SkyCoord.html) class and the [match_coordinates_sky](https://docs.astropy.org/en/stable/api/astropy.coordinates.match_coordinates_sky.html) function to finds the nearest on-sky matches between the set of catalog coordinates.\n",
"\n",
"We impose that a star is the same in both catalogs if the separation is < than `max_sep`, where `max_sep` is 0.015 arcsec (i.e., 0.5 px). "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68410f33",
"metadata": {},
"outputs": [],
"source": [
"mask_f115w = ((results_f115w['x_fit'] > 0) & (results_f115w['x_fit'] < 2048) & \n",
" (results_f115w['y_fit'] > 0) & (results_f115w['y_fit'] < 2048) & \n",
" (results_f115w['flux_fit'] > 0))\n",
"\n",
"result_clean_f115w = results_f115w[mask_f115w]\n",
"\n",
"ra_f115w, dec_f115w = image_f115w.meta.wcs(result_clean_f115w['x_fit'], result_clean_f115w['y_fit'])\n",
"radec_f115w = SkyCoord(ra_f115w, dec_f115w, unit='deg')\n",
"result_clean_f115w['radec'] = radec_f115w\n",
"\n",
"mask_f200w = ((results_f200w['x_fit'] > 0) & (results_f200w['x_fit'] < 2048) & \n",
" (results_f200w['y_fit'] > 0) & (results_f200w['y_fit'] < 2048) & \n",
" (results_f200w['flux_fit'] > 0))\n",
"\n",
"result_clean_f200w = results_f200w[mask_f200w]\n",
"\n",
"ra_f200w, dec_f200w = image_f200w.meta.wcs(result_clean_f200w['x_fit'], result_clean_f200w['y_fit'])\n",
"radec_f200w = SkyCoord(ra_f200w, dec_f200w, unit='deg')\n",
"\n",
"result_clean_f200w['radec'] = radec_f200w"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b3472fe9",
"metadata": {},
"outputs": [],
"source": [
"max_sep = 0.015 * u.arcsec\n",
"\n",
"filt1 = 'F115W'\n",
"filt2 = 'F200W'\n",
"\n",
"res1 = result_clean_f115w\n",
"res2 = result_clean_f200w\n",
"\n",
"idx, d2d, _ = match_coordinates_sky(res1['radec'], res2['radec'])\n",
"\n",
"sep_constraint = d2d < max_sep\n",
"\n",
"match_phot_single = Table()\n",
"\n",
"x_0_f115w = res1['x_0'][sep_constraint]\n",
"y_0_f115w = res1['y_0'][sep_constraint]\n",
"x_fit_f115w = res1['x_fit'][sep_constraint]\n",
"y_fit_f115w = res1['y_fit'][sep_constraint]\n",
"radec_f115w = res1['radec'][sep_constraint]\n",
"mag_f115w = (-2.5 * np.log10(res1['flux_fit']))[sep_constraint]\n",
"emag_f115w = (1.086 * (res1['flux_unc'] / res1['flux_fit']))[sep_constraint]\n",
"\n",
"x_0_f200w = res2['x_0'][idx[sep_constraint]]\n",
"y_0_f200w = res2['y_0'][idx[sep_constraint]]\n",
"x_fit_f200w = res2['x_fit'][idx[sep_constraint]]\n",
"y_fit_f200w = res2['y_fit'][idx[sep_constraint]]\n",
"radec_f200w = res2['radec'][idx][sep_constraint]\n",
"mag_f200w = (-2.5 * np.log10(res2['flux_fit']))[idx[sep_constraint]]\n",
"emag_f200w = (1.086 * (res2['flux_unc'] / res2['flux_fit']))[idx[sep_constraint]]\n",
"\n",
"match_phot_single['x_0_' + filt1] = x_0_f115w\n",
"match_phot_single['y_0_' + filt1] = y_0_f115w\n",
"match_phot_single['x_fit_' + filt1] = x_fit_f115w\n",
"match_phot_single['y_fit_' + filt1] = y_fit_f115w\n",
"match_phot_single['radec_' + filt1] = radec_f115w\n",
"match_phot_single['mag_' + filt1] = mag_f115w\n",
"match_phot_single['emag_' + filt1] = emag_f115w\n",
"match_phot_single['x_0_' + filt2] = x_0_f200w\n",
"match_phot_single['y_0_' + filt2] = y_0_f200w\n",
"match_phot_single['x_fit_' + filt2] = x_fit_f200w\n",
"match_phot_single['y_fit_' + filt2] = y_fit_f200w\n",
"match_phot_single['radec_' + filt2] = radec_f200w\n",
"match_phot_single['mag_' + filt2] = mag_f200w\n",
"match_phot_single['emag_' + filt2] = emag_f200w\n",
"\n",
"print('Number of sources in common between the two filters:', len(match_phot_single)) "
]
},
{
"cell_type": "markdown",
"id": "653892e3",
"metadata": {},
"source": [
"### 8.3
-Load input catalogs
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "34d48853",
"metadata": {},
"outputs": [],
"source": [
"if os.path.isfile('./jw00042001001_01101_00001_nrcb1_uncal_pointsources.list'):\n",
" input_catalog_f115w = './jw00042001001_01101_00001_nrcb1_uncal_pointsources.list'\n",
"\n",
"else:\n",
" print('Downloading F115W input catalog photometry')\n",
" \n",
" boxlink_inpcat_f115w = 'https://stsci.box.com/shared/static/1qdcq418fcmeekkzlw8uqmbz1656t3kd.list'\n",
" boxfile_inpcat_f115w = './jw00042001001_01101_00001_nrcb1_uncal_pointsources.list'\n",
" urllib.request.urlretrieve(boxlink_inpcat_f115w, boxfile_inpcat_f115w)\n",
" input_catalog_f115w = boxfile_inpcat_f115w\n",
"\n",
"\n",
"if os.path.isfile('./jw00042001001_01101_00005_nrcb1_uncal_pointsources.list'):\n",
" input_catalog_f200w = './jw00042001001_01101_00005_nrcb1_uncal_pointsources.list'\n",
"\n",
"else:\n",
" print('Downloading F200W input catalog photometry')\n",
" \n",
" boxlink_inpcat_f200w = 'https://stsci.box.com/shared/static/saeqibfbeccpv5gt9ndlb826o68ikt4b.list'\n",
" boxfile_inpcat_f200w = './jw00042001001_01101_00005_nrcb1_uncal_pointsources.list'\n",
" urllib.request.urlretrieve(boxlink_inpcat_f200w, boxfile_inpcat_f200w)\n",
" input_catalog_f200w = boxfile_inpcat_f200w\n",
"\n",
"input_cat_f115w = pd.read_csv(input_catalog_f115w, header=None, sep='\\s+', \n",
" names=['index', 'ra_in', 'dec_in', 'f115w_in'], \n",
" comment='#', skiprows=2, usecols=(0, 3, 4, 7))\n",
"\n",
"\n",
"input_cat_f200w = pd.read_csv(input_catalog_f200w, header=None, sep='\\s+', \n",
" names=['index', 'ra_in', 'dec_in', 'f200w_in'], \n",
" comment='#', skiprows=2, usecols=(0, 3, 4, 7))\n",
"\n",
"\n",
"input_cat_f200w.head()"
]
},
{
"cell_type": "markdown",
"id": "38f43944",
"metadata": {},
"source": [
"### 8.4
-Cross-match input catalogs
###"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "abd65e2f",
"metadata": {},
"outputs": [],
"source": [
"radec_input_f115w = SkyCoord(input_cat_f115w['ra_in'], input_cat_f115w['dec_in'], unit='deg')\n",
"radec_input_f200w = SkyCoord(input_cat_f200w['ra_in'], input_cat_f200w['dec_in'], unit='deg')\n",
"\n",
"idx_inp, d2d_inp, _ = match_coordinates_sky(radec_input_f115w, radec_input_f200w)\n",
"\n",
"max_sep = 0.015 * u.arcsec\n",
"\n",
"sep_constraint_inp = d2d_inp < max_sep\n",
"\n",
"f115w_inp = input_cat_f115w['f115w_in'][sep_constraint_inp]\n",
"f200w_inp = input_cat_f200w['f200w_in'][idx_inp[sep_constraint_inp]]"
]
},
{
"cell_type": "markdown",
"id": "63e1dce7",
"metadata": {},
"source": [
"### 8.5
-Instrumental Color-Magnitude Diagram
###"
]
},
{
"cell_type": "markdown",
"id": "61b42d56",
"metadata": {},
"source": [
"We compare the input (calibrated) color-magnitude diagram (CMD) with the instrumental CMD retrieved from the PSF photometry analysis. To obtain a calibrated final color-magnitude diagram, we need to derive the photometric zeropoints for the ouput catalogs (not covered in this JWebbinar)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2ba96df8",
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(12, 16))\n",
"plt.clf()\n",
"\n",
"ax1 = plt.subplot(1,2,1)\n",
"\n",
"xlim0 = -0.25 \n",
"xlim1 = 1.75 \n",
"ylim0 = 24\n",
"ylim1 = 15 \n",
"\n",
"ax1.set_xlim(xlim0,xlim1)\n",
"ax1.set_ylim(ylim0,ylim1)\n",
"\n",
"ax1.xaxis.set_major_locator(ticker.AutoLocator())\n",
"ax1.xaxis.set_minor_locator(ticker.AutoMinorLocator())\n",
"ax1.yaxis.set_major_locator(ticker.AutoLocator())\n",
"ax1.yaxis.set_minor_locator(ticker.AutoMinorLocator())\n",
"\n",
"ax1.scatter(f115w_inp - f200w_inp, f115w_inp, s = 1, color = 'k')\n",
"\n",
"ax1.set_xlabel(filt1+' - '+filt2, fontdict = font2)\n",
"ax1.set_ylabel(filt1, fontdict = font2)\n",
"ax1.text(xlim0+0.15, ylim1+0.25, 'Input', fontdict = font2)\n",
"ax1.text(xlim0+0.15, ylim1+0.50, 'N sources = '+ str(len(f115w_inp)), fontdict = font2)\n",
"\n",
"ax2 = plt.subplot(1, 2, 2)\n",
"\n",
"xlim0 = -0.8\n",
"xlim1 = 1.2\n",
"ylim0 = -2\n",
"ylim1 = -11\n",
"\n",
"ax2.set_xlim(xlim0, xlim1)\n",
"ax2.set_ylim(ylim0, ylim1)\n",
"\n",
"ax2.xaxis.set_major_locator(ticker.AutoLocator())\n",
"ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator())\n",
"ax2.yaxis.set_major_locator(ticker.AutoLocator())\n",
"ax2.yaxis.set_minor_locator(ticker.AutoMinorLocator())\n",
"\n",
"f115w_single = match_phot_single['mag_' + filt1]\n",
"f200w_single = match_phot_single['mag_' + filt2]\n",
"\n",
"ax2.scatter(f115w_single - f200w_single, f115w_single, s=1, color='k')\n",
"\n",
"ax2.set_xlabel(filt1 + '_inst - ' + filt2 +'_inst', fontdict=font2)\n",
"ax2.set_ylabel(filt1 + '_inst', fontdict=font2)\n",
"ax2.text(xlim0+0.15, ylim1+0.25, 'Output', fontdict = font2)\n",
"ax2.text(xlim0+0.15, ylim1+0.50, 'N sources ='+ str(len(f115w_single)), fontdict = font2)\n",
"\n",
"\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "8783d290",
"metadata": {},
"source": [
"9.
-Bonus part II: create a grid of empirical PSFs
\n",
"------------------"
]
},
{
"cell_type": "markdown",
"id": "151442ac",
"metadata": {},
"source": [
"### 9.1
-Count stars in N x N grid
###"
]
},
{
"cell_type": "markdown",
"id": "52ee13e7",
"metadata": {},
"source": [
"The purpose of the function `count_stars_grid` is to count how many good PSF stars are in cell of a N x N grid. The function starts from a grid of size N x N (where N = sqrt(**num_psfs**)) and iterate until the minimum grid size 2 x 2. Depending on the number of PSF stars that the users want in each cell of the grid, they can choose the appropriate grid size or modify the threshold values and/or the selection parameters adopted during the stars detection, in Sections 5.3, 5.4. \n",
"\n",
"The minimum number of PSF stars needed in each cell can also be set using the parameter **min_numpsfs_stars**. Useful when inspecting the plot, since in the cells with a number of PSF stars < **min_numpsfs_stars**, the value is reported in RED. Moreover, when `verbose = True`, it is easier to identify for each N x N combination, if and which cells have not enough PSF stars.\n",
"\n",
"This function returns sqrt(**num_psfs**) - 1 figures showing the number of PSFs stars in each cell for all the N x N combination."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e12d81ed",
"metadata": {},
"outputs": [],
"source": [
"def find_centers(num):\n",
" points = int(((data.shape[0] / num) / 2) - 1)\n",
" x_center = np.arange(points, 2 * points * num, 2 * points)\n",
" y_center = np.arange(points, 2 * points * num, 2 * points)\n",
"\n",
" centers = np.array(np.meshgrid(x_center, y_center)).T.reshape(-1, 2)\n",
"\n",
" return points, centers\n",
"\n",
"def count_stars_grid(num_psfs=4, min_numpsf_stars=40, size=11, verbose=True, savefig=True):\n",
" \n",
" # calculate the number of stars from find_stars in each cell of the grid. The maximum number of cell\n",
" # is defined by num_psfs and the function iterate from N x N (where N = sqrt(num_psfs)) until a 2 x 2 grid.\n",
"\n",
" if np.sqrt(num_psfs).is_integer():\n",
" grid_points = int(np.sqrt(num_psfs))\n",
"\n",
" else:\n",
" raise ValueError(\"You must choose a square number of cells to create (E.g. 9, 16, etc.)\")\n",
"\n",
" num_grid = np.arange(2, grid_points + 1, 1)\n",
" num_grid = num_grid[::-1]\n",
"\n",
"\n",
" for num in num_grid:\n",
" print(\"--------------------\")\n",
" print(\"\")\n",
" print(\"Calculating the number of PSF stars in a %d x %d grid:\" % (num, num))\n",
" print(\"\")\n",
"\n",
" s = (data.shape[1], data.shape[0])\n",
" temp_arr = np.zeros(s)\n",
" num_psfs_stars = []\n",
"\n",
" points, centers = find_centers(num)\n",
"\n",
" for n, val in enumerate(centers):\n",
"\n",
" x = found_stars_sel['xcentroid']\n",
" y = found_stars_sel['ycentroid']\n",
"\n",
" half_size = (size - 1) / 2\n",
"\n",
" lim1 = int(val[0] - points + half_size)\n",
" lim2 = int(val[0] + points - half_size)\n",
" lim3 = int(val[1] - points + half_size)\n",
" lim4 = int(val[1] + points - half_size)\n",
"\n",
" number_psf_stars = (x > lim1) & (x < lim2) & (y > lim3) & (y < lim4)\n",
" count_psfs_stars = np.count_nonzero(number_psf_stars)\n",
"\n",
" lim_x1 = int(lim1 - half_size)\n",
" lim_x2 = int(lim2 + half_size)\n",
" lim_y1 = int(lim3 - half_size)\n",
" lim_y2 = int(lim4 + half_size)\n",
"\n",
" if verbose:\n",
"\n",
" if np.count_nonzero(number_psf_stars) < min_numpsf_stars:\n",
" print('Center Coordinates of grid cell {:d} are ({:d}, {:d}) --- Not enough stars in the cell '\n",
" '(number of stars < {:d})'.format(n + 1, val[0], val[1], min_numpsf_stars))\n",
"\n",
" else:\n",
" print(f'Center Coordinate of grid cell {n + 1:d} are ({val[0]:d}, {val[1]:d})'\n",
" '--- Number of stars:', np.count_nonzero(number_psf_stars))\n",
" print(\"\")\n",
"\n",
" temp_arr[lim_y1:lim_y2, lim_x1:lim_x2] = count_psfs_stars\n",
" num_psfs_stars.append(count_psfs_stars)\n",
"\n",
" if savefig:\n",
" plot_count_grid(temp_arr, num, num_psfs_stars, centers)\n",
"\n",
"def plot_count_grid(arr, num, nstars, centers):\n",
" \n",
" plt.clf()\n",
"\n",
" from mpl_toolkits.axes_grid1 import make_axes_locatable\n",
" plt.figure(figsize=(10, 10))\n",
" ax = plt.subplot(1, 1, 1)\n",
"\n",
" plt.xlabel('X [px]', font={'size': 20})\n",
" plt.ylabel('Y [px]', font={'size': 20})\n",
" plt.title('%dx%d grid - ' % (num, num) + det + ' - ' + filt, font={'size': 25})\n",
" im = ax.imshow(arr, origin='lower', vmin=np.min(arr[arr > 0]), vmax=np.max(arr))\n",
" for i in range(num ** 2):\n",
" if nstars[i] < 40:\n",
" ax.text(centers[i][0] - 100, centers[i][1] - 50, \"%d\" % nstars[i], c='r', font={'size': 30})\n",
" else:\n",
" ax.text(centers[i][0] - 100, centers[i][1] - 50, \"%d\" % nstars[i], c='w', font={'size': 30})\n",
" ax.text(2300, 750, \"# of PSF stars\", rotation=270, font={'size': 25})\n",
" divider = make_axes_locatable(ax)\n",
" cax = divider.append_axes(\"right\", size=\"5%\", pad=0.05)\n",
" plt.colorbar(im, cax=cax)\n",
"\n",
" plt.tight_layout()\n",
"\n",
" filename = 'number_PSFstars_%dx%dgrid_%s_%s.pdf' % (num, num, det, filt)\n",
"\n",
" plt.savefig(os.path.join(figures_dir, filename))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d4b6878c",
"metadata": {},
"outputs": [],
"source": [
"figures_dir = 'FIGURES/'\n",
"\n",
"if not os.path.exists(figures_dir):\n",
" os.makedirs(figures_dir)\n",
"\n",
"count_stars_grid(num_psfs=25, min_numpsf_stars=40, size=11, verbose=True, savefig=True)"
]
},
{
"cell_type": "markdown",
"id": "499bea37",
"metadata": {},
"source": [
"### 9.2
-Build effective PSF (single or grid)
###"
]
},
{
"cell_type": "markdown",
"id": "2e446c23",
"metadata": {},
"source": [
"This function creates a grid of PSFs with EPSFBuilder (or a single PSF, when **num_psfs**=1). The function returns a GriddedEPSFModel object containing a 3D array of N × n × n. The 3D array represents the N number of 2D n × n ePSFs created. It includes a grid_xypos key which will state the position of the PSF on the detector for each of the PSFs. The order of the tuples in grid_xypos refers to the number the PSF is in the 3D array."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "113579fb",
"metadata": {},
"outputs": [],
"source": [
"def build_epsf_grid(num_psfs=4, size=11, oversample=4, save=True, savefig=True, overwrite=True):\n",
"\n",
" if np.sqrt(num_psfs).is_integer():\n",
" num_grid = int(np.sqrt(num_psfs))\n",
"\n",
" else:\n",
" raise ValueError(\"You must choose a square number of cells to create (E.g. 9, 16, etc.)\")\n",
"\n",
"\n",
" points, centers = find_centers(num_grid)\n",
"\n",
" epsf_size = size * oversample\n",
" epsf_arr = np.empty((num_grid ** 2, epsf_size, epsf_size))\n",
"\n",
" for i, val in enumerate(centers):\n",
"\n",
" x = found_stars_sel['xcentroid']\n",
" y = found_stars_sel['ycentroid']\n",
"\n",
" half_size = (size - 1) / 2\n",
"\n",
" lim1 = int(val[0] - points + half_size)\n",
" lim2 = int(val[0] + points - half_size)\n",
" lim3 = int(val[1] - points + half_size)\n",
" lim4 = int(val[1] + points - half_size)\n",
"\n",
" mask = ((x > lim1) & (x < lim2) & (y > lim3) & (y < lim4))\n",
"\n",
" stars_tbl = Table()\n",
" stars_tbl['x'] = x[mask]\n",
" stars_tbl['y'] = y[mask]\n",
" print('Number of sources in cell %d used to build the ePSF:' % (i + 1), len(stars_tbl['x']))\n",
"\n",
" nddata = NDData(data=data_bkgsub)\n",
" stars = extract_stars(nddata, stars_tbl, size=size)\n",
"\n",
" print(\"Creating ePSF for cell %d - Coordinates (%d, %d)\" % (i + 1, val[0], val[1]))\n",
" print(\"\")\n",
"\n",
" epsf_builder = EPSFBuilder(oversampling=oversample, maxiters=3, progress_bar=False)\n",
"\n",
" epsf, fitted_stars = epsf_builder(stars)\n",
"\n",
" epsf_arr[i, :, :] = epsf.data[:epsf.shape[0]-1, 1:epsf.shape[0]]\n",
"\n",
" meta = OrderedDict()\n",
" meta[\"DETECTOR\"] = (det, \"Detector name\")\n",
" meta[\"FILTER\"] = (filt, \"Filter name\")\n",
" meta[\"NUM_PSFS\"] = (num_grid ** 2, \"The total number of ePSFs\")\n",
" for h, loc in enumerate(centers):\n",
" loc = np.asarray(loc, dtype=float)\n",
"\n",
" meta[\"DET_YX{}\".format(h)] = (str((loc[1], loc[0])),\n",
" \"The #{} PSF's (y,x) detector pixel position\".format(h))\n",
"\n",
" meta[\"OVERSAMP\"] = (oversample, \"Oversampling Factor in EPSFBuilder\")\n",
"\n",
" model_epsf = create_model(epsf_arr, meta)\n",
"\n",
" if savefig:\n",
" plot_epsf(model_epsf, num_psfs)\n",
"\n",
" if save:\n",
" writeto(epsf_arr, meta, num_psfs)\n",
"\n",
" return model_epsf\n",
"\n",
"def writeto(data, meta, num_psfs, overwrite=True):\n",
"\n",
" primaryhdu = fits.PrimaryHDU(data)\n",
"\n",
" # Convert meta dictionary to header\n",
" tuples = [(a, b, c) for (a, (b, c)) in meta.items()]\n",
" primaryhdu.header.extend(tuples)\n",
"\n",
" # Add extra descriptors for how the file was made\n",
" primaryhdu.header[\"COMMENT\"] = \"For a given filter, and detector 1 file is produced in \"\n",
" primaryhdu.header[\"COMMENT\"] = \"the form [i, y, x] where i is the ePSF position on the detector grid \"\n",
" primaryhdu.header[\"COMMENT\"] = \"and (y,x) is the 2D PSF. The order of PSFs can be found under the \"\n",
" primaryhdu.header[\"COMMENT\"] = \"header DET_YX* keywords\"\n",
"\n",
" hdu = fits.HDUList(primaryhdu)\n",
"\n",
" filename = \"epsf_{}_{}_nepsf{}.fits\".format(det, filt, num_psfs)\n",
" \n",
" file = os.path.join(psfs_dir, filename)\n",
"\n",
" hdu.writeto(file, overwrite=overwrite)\n",
"\n",
"def plot_epsf(model, num):\n",
"\n",
" if num == 1:\n",
" plt.clf()\n",
" plt.figure(figsize=(10, 10))\n",
" ax = plt.subplot(1, 1, 1)\n",
"\n",
" norm_epsf = simple_norm(model.data[0], 'log', percent=99.)\n",
" plt.suptitle(det + ' - ' + filt, font={'size': 20})\n",
" plt.title(model.meta['grid_xypos'][0], font={'size': 20})\n",
" ax.imshow(model.data[0], norm=norm_epsf)\n",
" plt.tight_layout()\n",
"\n",
" filename = 'ePSF_single_%s_%s.pdf' % (det, filt)\n",
" \n",
" plt.savefig(os.path.join(figures_dir, filename))\n",
" \n",
" else:\n",
" plt.clf()\n",
"\n",
" nn = int(np.sqrt(num))\n",
" figsize = (12, 12)\n",
" fig, ax = plt.subplots(nn, nn, figsize=figsize)\n",
"\n",
" for ix in range(nn):\n",
" for iy in range(nn):\n",
" i = ix * nn + iy\n",
" norm_epsf = simple_norm(model.data[i], 'log', percent=99.)\n",
" ax[nn - 1 - iy, ix].imshow(model.data[i], norm=norm_epsf)\n",
" ax[nn - 1 - iy, ix].set_title(model.meta['grid_xypos'][i], font={'size': 20})\n",
"\n",
" plt.suptitle(det + ' - ' + filt, font={'size': 40})\n",
" plt.tight_layout()\n",
" \n",
" filename = 'ePSF_%dx%dgrid_%s_%s.pdf' % (nn, nn, det, filt)\n",
" \n",
" plt.savefig(os.path.join(figures_dir, filename))\n",
"\n",
"\n",
"def create_model(data, meta):\n",
"\n",
" ndd = NDData(data, meta=meta, copy=True)\n",
"\n",
" ndd.meta['grid_xypos'] = [((float(ndd.meta[key][0].split(',')[1].split(')')[0])),\n",
" (float(ndd.meta[key][0].split(',')[0].split('(')[1]))) for key in ndd.meta.keys() if\n",
" \"DET_YX\" in key]\n",
"\n",
" ndd.meta['oversampling'] = meta[\"OVERSAMP\"][0]\n",
" ndd.meta = {key.lower(): ndd.meta[key] for key in ndd.meta}\n",
" model = GriddedPSFModel(ndd)\n",
"\n",
" return model"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "93a5c27e",
"metadata": {},
"outputs": [],
"source": [
"from collections import OrderedDict\n",
"\n",
"psfs_dir = 'PSF_MODELS/'\n",
"\n",
"if not os.path.exists(psfs_dir):\n",
" os.makedirs(psfs_dir)\n",
" \n",
"data_bkgsub, _ = calc_bkg()\n",
"\n",
"epsf_grid = build_epsf_grid(num_psfs=1, size=11, oversample=4, save=True, savefig=True, overwrite=True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}