Drizzled PSFs from HST/ACS Imaging

This example notebook shows how spike interacts with HST/ACS imaging to generate a drizzled PSF. This notebook uses observations from this program. The easiest way to find these data in the MAST archive is by searching for observation ID j8pu42010. spike uses calibrated, but not-yet co-added images (i.e., “Level 2” data products), so be sure that your download includes ‘flc’ files. If you download all of the files associated with this program, your working directory will include:

  • j8pu42ecq_flc.fits

  • j8pu42egq_flc.fits

  • j8pu42esq_flc.fits

  • j8pu42evq_flc.fits

These data are also available here.

The principle of this example notebook is the same regardless of dataset, though, so it can be used as a direct guide for use with other data, too.

NOTE: Environment variables are not always read in properly within Jupyter notebooks/JupyterLab – see here for some additional details of how to access them.

Initial setup and visualization

[ ]:
from spike.psf import hst # import the relevant top-level module from spike

datapath = '/path/to/acs/data'
outputpath = 'psfs' #default is /psfs, defined from your working directory

Note that if you choose to create drizzled PSFs for the same data and object location using multiple PSF generation methods, you will want to define a new ‘savedir’ each time to avoid conflicts between files with the same name.

We’ll also define an object – in this case, a set of coordinates – for which the PSF will be generated.

[ ]:
obj = '10:00:33.0178 +02:09:52.304' #can also be a resolvable name or coordinates in decimal degrees

Now, we can call spike.psf.hst – as an initial example we’ll use the most minimal inputs. By default, in addition to saving drizzled PSFs and any intermediate products in the directory given by ‘savedir’ (here the argument will be the outputpath variable we defined), this function will return a dictionary that stores model PSFs indexed by filter and object.

If returnpsf = 'full' (default), the arrays in the dictionary will be the full drizzled image field of view. If returnpsf = 'crop', the arrays in the dictionary will be the cutout region around the object location with the size of the cutout determined by the ‘cutout_fov’ argument; the cropped region around the drizzled PSF will also be saved as a FITS file, including its WCS information (can be toggled off with savecutout = False).

spike assumes that the input files have not yet been “tweaked” and handles that step for you. If your input images have been tweaked or if you would like to skip this step for other reasons, specify pretweaked = True in calling spike.psf.hst.

The outputs are very long due to all of the print statements from the tweak, PSF generation, and drizzling steps. Outputs have been cleared for cells where spike.psf.hst was run.

[ ]:
dpsf = hst(img_dir = datapath, obj = obj, img_type = 'flc', inst = 'ACS', camera = 'WFC',
           savedir = outputpath, pretweaked = True)

In this case dpsf will be very simple, since we are only looking at one set of coordinates and one filter:

[4]:
dpsf
[4]:
{'10:00:33.0178 +02:09:52.304': {'F475W': array([[ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         ...,
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.]],
        shape=(2359, 4162), dtype='>f4')}}

spike can automatically plot model PSFs as they are generated, and generates PSFs regardless of whether they are plotted (or returned). To show the output drizzled PSFs here, we will plot them directly. Therefore, we will import matplotlib below. Since returnpsf = 'full' by default, the array stored in dpsf shows the drizzled PSF in the context of the entire co-added image footprint. This is equivalent to reading in the image stored in ‘150d08m15.267s+2d09m52.304s_F475W_psf_drc.fits’.

[5]:
import matplotlib.pyplot as plt
import numpy as np

dat = dpsf['10:00:33.0178 +02:09:52.304']['F475W']

fig = plt.figure()
plt.imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys')
plt.xlabel('x (pix)')
plt.ylabel('y (pix)')
[5]:
Text(0, 0.5, 'y (pix)')
_images/acs_example_9_1.png

We can index the array to just show the PSF below:

[6]:
dat = dpsf['10:00:33.0178 +02:09:52.304']['F475W'][325:465, 940:1080]

fig = plt.figure()
plt.imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys', extent = [940, 1080, 325, 465])
plt.xlabel('x (pix)')
plt.ylabel('y (pix)')
[6]:
Text(0, 0.5, 'y (pix)')
_images/acs_example_11_1.png

Specifying user preferences

By default, PSFs are generated for spike.psf.hst by TinyTim using a blackbody model of a G5V star at 6000 K, but users have complete control over the specifics of the PSF generation. To change the parameters within a method (e.g., updating model arguments, setting different detection thresholds for the empirical PSFs, …), spike.psf.hst directly takes keyword arguments for the specified ‘method’. Details of allowed arguments are available in the spike.psfgen documentation. As an example, we will change the TinyTim model inputs to use an O6 star at 45000 K:

[ ]:
dpsf = hst(img_dir = datapath, obj = obj, img_type = 'flc', inst = 'ACS', camera = 'WFC',
            savedir = outputpath, pretweaked = True, listchoice = 'O6', temp = 45000)

As before, we’ll plot the outputs directly:

[8]:
dat = dpsf['10:00:33.0178 +02:09:52.304']['F475W']
datcrop = dpsf['10:00:33.0178 +02:09:52.304']['F475W'][325:465, 940:1080]

fig, ax = plt.subplots(1, 2, gridspec_kw = {'width_ratios':[1.75, 1], 'wspace':0.3})
ax[0].imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys')
ax[0].set_xlabel('x (pix)')
ax[0].set_ylabel('y (pix)')

ax[1].imshow(datcrop, origin = 'lower', cmap = 'Greys', extent = [940, 1080, 325, 465],
             vmin = np.nanpercentile(datcrop[datcrop != 0], 20),
             vmax = np.nanpercentile(datcrop[datcrop != 0], 97))
ax[1].set_xlabel('x (pix)')
ax[1].set_ylabel('y (pix)')
[8]:
Text(0, 0.5, 'y (pix)')
_images/acs_example_15_1.png

Generating multiple PSFs

spike can also be used to generate PSFs for multiple objects/coordinates simultaneously.

[ ]:
obj = ['10:00:33.0178 +02:09:52.304', '10:00:28.1798 +02:08:40.507']

dpsf = hst(img_dir = datapath, obj = obj, img_type = 'flc', inst = 'ACS', camera = 'WFC',
           savedir = outputpath, pretweaked = True)
[10]:
dpsf
[10]:
{'10:00:33.0178 +02:09:52.304': {'F475W': array([[ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         ...,
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.]],
        shape=(2359, 4162), dtype='>f4')},
 '10:00:28.1798 +02:08:40.507': {'F475W': array([[ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         [ 0.,  0.,  0., ..., nan, nan, nan],
         ...,
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.],
         [nan, nan, nan, ...,  0.,  0.,  0.]],
        shape=(2359, 4162), dtype='>f4')}}
[11]:
dat = dpsf['10:00:33.0178 +02:09:52.304']['F475W']
datcrop = dpsf['10:00:33.0178 +02:09:52.304']['F475W'][325:465, 940:1080]

fig, ax = plt.subplots(2, 2, gridspec_kw = {'width_ratios':[1.75, 1], 'wspace':0.3})
ax[0, 0].imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys')
ax[0, 0].set_xlabel('x (pix)')
ax[0, 0].set_ylabel('y (pix)')

ax[0, 1].imshow(datcrop, origin = 'lower', cmap = 'Greys', extent = [940, 1080, 325, 465],
             vmin = np.nanpercentile(datcrop[datcrop != 0], 20),
             vmax = np.nanpercentile(datcrop[datcrop != 0], 97))
ax[0, 1].set_xlabel('x (pix)')
ax[0, 1].set_ylabel('y (pix)')


dat = dpsf['10:00:28.1798 +02:08:40.507']['F475W']
datcrop = dpsf['10:00:28.1798 +02:08:40.507']['F475W'][1504:1644, 2605:2745]

ax[1, 0].imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys')
ax[1, 0].set_xlabel('x (pix)')
ax[1, 0].set_ylabel('y (pix)')

ax[1, 1].imshow(datcrop, origin = 'lower', cmap = 'Greys', extent = [2605, 2745, 1504, 1644],
             vmin = np.nanpercentile(datcrop[datcrop != 0], 20),
             vmax = np.nanpercentile(datcrop[datcrop != 0], 97))
ax[1, 1].set_xlabel('x (pix)')
ax[1, 1].set_ylabel('y (pix)')
[11]:
Text(0, 0.5, 'y (pix)')
_images/acs_example_19_1.png

PSF generation methods, automatic cropping

One advantage of spike is that it can be used flexibly with a variety of different PSF generation methods – to change which one is used, simply use the ‘method’ argument. Built-in options are:

  • ‘TinyTim’

  • ‘TinyTim_Gillis’

  • ‘STDPSF’

  • ‘epsf’

  • ‘PSFEx’

  • ‘ACS_ePSF’ – added in v1.2.4

If method = 'USER', then the keyword argument ‘usermethod’ must also be specified, pointing to either a function or a directory that houses model PSFs that follow the [imgprefix]_[coords]_[band]_psf.fits, e.g., imgprefix_23.31+30.12_F814W_psf.fits or imgprefix_195.78-46.52_F555W_psf.fits naming scheme.

We’ll use STDPSFs below and have spike output cutouts around the drizzled PSF.

[ ]:
obj = '10:00:33.0178 +02:09:52.304'

dpsf = hst(img_dir = datapath, obj = obj, img_type = 'flc', inst = 'ACS', camera = 'WFC',
           savedir = outputpath, pretweaked = True, method = 'STDPSF', returnpsf = 'crop')
[14]:
dpsf
[14]:
{'10:00:33.0178 +02:09:52.304': {'F475W': array([[0., 0., 0., ..., 0., 0., 0.],
         [0., 0., 0., ..., 0., 0., 0.],
         [0., 0., 0., ..., 0., 0., 0.],
         ...,
         [0., 0., 0., ..., 0., 0., 0.],
         [0., 0., 0., ..., 0., 0., 0.],
         [0., 0., 0., ..., 0., 0., 0.]], shape=(151, 151), dtype='>f4')}}

Since we’ve selected returnpsf = 'crop', the output array is already cropped to the immediate region around the PSF. Selecting both returnpsf = 'crop' and savecutout = True (default), will save a FITS file that stores the cropped drizzled PSF – the name is the same as the for the full saved image with ‘_crop’ appended.

[15]:
dat = dpsf['10:00:33.0178 +02:09:52.304']['F475W']

fig = plt.figure()
plt.imshow(dat, vmin = np.nanpercentile(dat[dat != 0], 20), vmax = np.nanpercentile(dat[dat != 0], 97),
            origin = 'lower', cmap = 'Greys')
plt.xlabel('x (pix)')
plt.ylabel('y (pix)')
[15]:
Text(0, 0.5, 'y (pix)')
_images/acs_example_24_1.png