Merian project Part 2#

Explore the morphology of dwarf galaxies in H\(\alpha\) using the Merian Survey data

Prerequisites

  • Need to install reproject and photutils and cmasher

%load_ext autoreload
%autoreload 2
import os, sys
sys.path.append(os.path.abspath('../../'))
import numpy as np
import matplotlib.pyplot as plt
from astropy.table import Table
from astropy.coordinates import SkyCoord
from astropy.io import fits
from utils import pad_psf, show_image

# We can beautify our plots by changing the matpltlib setting a little
plt.rcParams['font.size'] = 18
plt.rcParams['image.origin'] = 'lower'
plt.rcParams['figure.figsize'] = (8, 6)
plt.rcParams['figure.dpi'] = 90
plt.rcParams['axes.linewidth'] = 2
Hide code cell content
try:
    import google.colab
    IN_COLAB = True
except:
    IN_COLAB = False

if not IN_COLAB:
    if not os.path.exists("../../../_static/ObsAstroData/"):
        os.makedirs("../../../_static/ObsAstroData/")
    # os.chdir('../../../_static/ObsAstroData/')
# os.chdir('./merian/')

Part 3: Make your own H\(\alpha\) map from HSC and Merian data#

Again, some background knowledge goes here. What is HSC, what is image subtraction. What is tricky about it (e.g., PSF matching).

merian = Table.read("../../../_static/ObsAstroData/merian/merian_dr1_specz_inband_lowmass_allbands.csv")
cutout_dir = "../../../_static/ObsAstroData/merian/cutouts/"

i = 1769
obj = merian[i]
coord = SkyCoord(obj['coord_ra_Merian'], obj['coord_dec_Merian'], unit='deg')
cname = obj['cname']
WARNING: OverflowError converting to IntType in column specobjid_S, reverting to String. [astropy.io.ascii.fastbasic]
# np.where(merian['name'] == 'J100107.07+015416.20')
# Open images and psfs
cutouts = {band: fits.open(os.path.join(cutout_dir, "", f"{cname}_HSC-{band}.fits"))[1].data for band in ['g', 'r', 'i', 'z']}
cutouts['N708'] = fits.open(os.path.join(cutout_dir, "", f"{cname}_N708_merim.fits"))[1].data

cutout_headers = {band: fits.open(os.path.join(cutout_dir, "", f"{cname}_HSC-{band}.fits"))[1].header for band in ['g', 'r', 'i', 'z']}
cutout_headers['N708'] = fits.open(os.path.join(cutout_dir, "", f"{cname}_N708_merim.fits"))[1].header

psfs = {band: fits.open(os.path.join(cutout_dir, "", f"{cname}_HSC-{band}_psf.fits"))[0].data for band in ['g', 'r', 'i', 'z']}
psfs['N708'] = fits.open(os.path.join(cutout_dir, "", f"{cname}_N708_merpsf.fits"))[0].data

We need to pad the PSFs (we can also pad them in advance and provide students the ready version)

bands = list(cutouts.keys())

psf_shape = np.max(np.array([i.shape for i in psfs.values()]), 0)
cutout_shape = np.min(np.array([i.shape for i in cutouts.values()]), 0)

# make all the psfs the same shape
for i in psfs.keys():
    psfs[i] = pad_psf(psfs[i], psf_shape)

psf_shape, cutout_shape
(array([43, 43]), array([358, 358]))

Part 3.1: measure PSF FWHM#

Let’s take a better look at the PSF of N708 band. Then we try to fit a Moffat model to the PSF, and calculate the FWHM of the PSF.

band = 'i'
psf = psfs[band]
plt.imshow(psf)
<matplotlib.image.AxesImage at 0x11266ac10>
../../../_images/e3a20944e89926b27806d9e762a8bc043259c5f1515591fa9016f8d5f869e550.png
from astropy.modeling import models, fitting
y, x = np.mgrid[:psf.shape[0], :psf.shape[1]]
model = models.Moffat2D(x_0=psf.shape[1]/2, y_0=psf.shape[0]/2, amplitude=np.max(psf))
fitter = fitting.LevMarLSQFitter()
best_fit = fitter(model, x, y, psf)
print(f'FWHM = {best_fit.fwhm:.3f} pix')
FWHM = 3.630 pix
fig, axes = plt.subplots(1, 3, figsize=(10, 3))

show_image(psf, ax=axes[0], fig=fig, vmin=0, vmax=np.percentile(psf, 92), title=f'{band} PSF')
show_image(best_fit(x, y), ax=axes[1], fig=fig, vmin=0, vmax=np.percentile(psf, 92), title='best-fit Moffat')
show_image(psf - best_fit(x, y), ax=axes[2], fig=fig, vmin=0, vmax=np.percentile(psf, 92), title='residual')
<Axes: title={'center': 'residual'}>
../../../_images/79526fcb0a666ab4f87b2fae413d7aea7e4558cedd6a7b8f5c0d8a434601c856.png

Then we can summarize the above procedure as the calc_PSF_FWHM function:

def calc_PSF_FWHM(psfs=None):
    """
    Measure the FWHM of the PSF by fitting a 2D Moffat profile to each PSF cutout.
    
    Parameters
    ----------
    psfs: dict
        A dictionary containing the PSF images for each band.
    
    Returns
    -------
    fwhm: dict
        A dictionary containing the FWHM of the PSF for each band (in pixels).
    """
    from astropy.modeling import models, fitting
    bands = list(psfs.keys())

    fit_p = fitting.LevMarLSQFitter()
    fwhms = {}
    for ix, band in enumerate(bands):
        z = psfs[band]
        mid = np.array(z.shape) // 2
        y, x = np.mgrid[: z.shape[0], : z.shape[1]]
        p_init = models.Moffat2D(x_0=mid[0], y_0=mid[1], amplitude=z[mid[0], mid[1]])
        p = fit_p(p_init, x, y, z)
        fwhms[band] = p.fwhm

    return fwhms

Part 3.2: make H\(\alpha\) map for Merian dwarfs#

from utils import calc_PSF_FWHM
fwhms = calc_PSF_FWHM(psfs)
print(fwhms)
worst_psf = max(fwhms.keys(), key=fwhms.get)
print(f"Worst psf is in band {worst_psf}")
{'g': 4.203394706999026, 'r': 3.7680022257238175, 'i': 3.630464252023418, 'z': 4.033193740608883, 'N708': 4.849972192692815}
Worst psf is in band N708
fig, axes = plt.subplots(1, 5, figsize=(14, 3))
for i, band in enumerate(cutouts.keys()):
    show_image(cutouts[band], fig=fig, ax=axes[i], cmap='Greys')
    axes[i].set_title(band, fontsize=15)
../../../_images/7836427af0ede7fec8b423cf45785a308ca25a679bbf8723865bb94efb48174a.png

(Vanilla) Image subtraction: \(N708 - z\)#

from astropy.wcs import WCS
from reproject import reproject_interp
from astropy.nddata import Cutout2D
import astropy.units as u
import cmasher as cmr
# reproject images to line up properly
size = 60 * u.arcsec
wcs_z = WCS(cutout_headers["z"])
cutouts_reproj = {}
for i in cutouts.keys():
    wcs_i = WCS(cutout_headers[i])
    stamp_i = Cutout2D(cutouts[i], coord, size, wcs=wcs_i)
    cutouts_reproj[i], _ = reproject_interp((stamp_i.data, stamp_i.wcs), wcs_z)
fig, axes = plt.subplots(1, 3, figsize=(10, 3))
show_image(cutouts_reproj['z'], fig=fig, ax=axes[0], show_colorbar=False)
axes[0].set_title('HSC z')

show_image(cutouts_reproj['N708'], fig=fig, ax=axes[1], show_colorbar=False)
axes[1].set_title('N708')

show_image(cutouts_reproj['N708'] - cutouts_reproj['z'], 
           fig=fig, ax=axes[2], show_colorbar=False)
axes[2].set_title('N708 - z')

plt.suptitle('Direct subtraction: PSFs do not match', y=1.1)
Text(0.5, 1.1, 'Direct subtraction: PSFs do not match')
../../../_images/78481c995dce0ddf71753781640a36d59e4fcb35036428c9c8532f67037e5269.png

Better Image subtraction: match PSFs!#

from photutils.psf import matching
from astropy.convolution import convolve_fft
from utils import get_central_region
from astropy.visualization import ImageNormalize, LuptonAsinhStretch, make_lupton_rgb
cutouts_matched = {}
window = matching.CosineBellWindow(alpha=0.8)
for band in bands:
    kernel = matching.create_matching_kernel(psfs[band], psfs[worst_psf], window=window)
    cutouts_matched[band] = convolve_fft(cutouts[band], kernel)
    if (band == worst_psf):
        cutouts_matched[band] = cutouts[band] # no need to touch the worst band
        continue
# reproject images to line up properly
size = 60 * u.arcsec
wcs_z = WCS(cutout_headers["z"])
cutouts_matched_reproj = {}
for band in bands:
    wcs_i = WCS(cutout_headers[band])
    stamp_i = Cutout2D(cutouts_matched[band], coord, size, wcs=wcs_i)
    cutouts_matched_reproj[band], _ = reproject_interp(
        (stamp_i.data, stamp_i.wcs), wcs_z
    )
fig, axes = plt.subplots(1, 3, figsize=(10, 3))
show_image(cutouts_matched_reproj['z'], fig=fig, ax=axes[0], show_colorbar=False)
axes[0].set_title('HSC z')

show_image(cutouts_matched_reproj['N708'], fig=fig, ax=axes[1], show_colorbar=False)
axes[1].set_title('N708')

show_image(cutouts_matched_reproj['N708'] - cutouts_matched_reproj['z'], 
           fig=fig, ax=axes[2], show_colorbar=False, cmap="cmr.ember", percl=99.6)
axes[2].set_title('N708 - z')

plt.suptitle('Subtraction after PSF matching', y=1.1)
Text(0.5, 1.1, 'Subtraction after PSF matching')
../../../_images/74a5ed23d5e922c2f102d3605e9c03dd8c87713389456ccca62d3d60e3debb70.png
cutouts_matched_reproj['ha'] = cutouts_matched_reproj['N708'] - 1 * cutouts_matched_reproj['z']
norm = ImageNormalize(cutouts_matched_reproj['ha'], vmin=-0.1, vmax=np.nanpercentile(cutouts_matched_reproj['ha'], 99.8),
                      stretch=LuptonAsinhStretch(stretch=0.8, Q=3))
show_image(cutouts_matched_reproj['ha'], norm=norm, cmap='cividis', figsize=(6,6))
<Axes: >
../../../_images/ae0d4f1a463a19b4ac565bed56428af0a9695fb346e9d68b41a8d108c25aacbd.png

Make pretty image with H\(\alpha\) map highlighted#

cuts = get_central_region(cutouts_matched_reproj, 220)
fig, [ax1, ax2, ax3] = plt.subplots(1, 3, figsize=(14, 4))

norm = ImageNormalize(cuts['ha'], vmin=-0.1, vmax=np.nanpercentile(cuts['ha'], 99.8),
                      stretch=LuptonAsinhStretch(stretch=0.8, Q=3))
ax1.imshow(cuts['ha'], norm=norm, cmap='cmr.ember', origin='lower')
ax1.axis('off')
ax1.set_title(r'H$\alpha$ map')

rgb = make_lupton_rgb(cuts['z'], #red
                      cuts['r'], #green
                      cuts['g'], # + 1.0 * cuts['ha'], #blue
                      stretch=0.3, Q=6, minimum=[-0.02, -0.02, 0.06])
ax2.imshow(rgb, origin='lower')
ax2.axis('off')
ax2.set_title('grz')

rgb = make_lupton_rgb(cuts['z'], #red
                      cuts['r'], #green
                      1.1 * cuts['g'] + 1.0 * cuts['ha'], #blue
                      stretch=0.3, Q=6, minimum=[-0.02, -0.02, 0.06])
ax3.imshow(rgb, origin='lower')
ax3.axis('off')
ax3.set_title(r'grz+H$\alpha$ (blue)')
Text(0.5, 1.0, 'grz+H$\\alpha$ (blue)')
../../../_images/4bc01fc197b46b1a4ae2036573c7ddf365181a6c0e3f1ad02bdc7b4bd9d78271.png
fig, [ax1, ax2, ax3] = plt.subplots(1, 3, figsize=(14, 4))

norm = ImageNormalize(cuts['ha'], vmin=-0.1, vmax=np.nanpercentile(cuts['ha'], 99.8),
                      stretch=LuptonAsinhStretch(stretch=0.8, Q=3))
ax1.imshow(cuts['ha'], norm=norm, cmap='cmr.ember', origin='lower')
ax1.axis('off')
ax1.set_title(r'H$\alpha$ map')

rgb = make_lupton_rgb(cutouts['z'], #red
                      cutouts['r'], #green
                      cutouts['g'], #blue
                      stretch=0.5, Q=6, minimum=[-0.02, -0.02, 0.06])
ax2.imshow(rgb, origin='lower')
ax2.axis('off')
ax2.set_title('grz')

rgb = make_lupton_rgb(cutouts_reproj['z'], #red
                      cutouts_reproj['r'], #green
                      1.1 * cutouts_reproj['g'] + 1.2 * cutouts_matched_reproj['ha'], #blue
                      stretch=0.5, Q=6, minimum=[-0.02, -0.02, 0.06])
ax3.imshow(rgb, origin='lower')
ax3.axis('off')
ax3.set_title(r'grz+H$\alpha$ (blue)')
/Users/jiaxuanl/Softwares/lsst_stack/conda/miniconda3-py38_4.9.2/envs/lsst-scipipe-8.0.0/lib/python3.11/site-packages/astropy/visualization/basic_rgb.py:153: RuntimeWarning: invalid value encountered in cast
  return image_rgb.astype(output_dtype)
Text(0.5, 1.0, 'grz+H$\\alpha$ (blue)')
../../../_images/b34b6ee68657d7085e4b55c4dfc1db9bbe63abe4699845cc2337255dffccf08f.png