Handling FITS files

In [1]:
%matplotlib inline
import matplotlib.pyplot as plt

Documentation

For more information about the features presented below, you can read the astropy.io.fits docs.

Data

The data used in this page is a model of the gamma-ray sky background used for the LAT instrument on the Fermi telescope, as well as the Fermi/LAT point source catalog.

Reading FITS files and accessing data

Opening a FITS file is relatively straightforward. We can open the background model included in the tutorial files:

In [2]:
from astropy.io import fits
In [3]:
hdulist = fits.open('data/gll_iem_v02_P6_V11_DIFFUSE.fit')

The returned object, hdulist, (an instance of the HDUList class) behaves like a Python list, and each element maps to a Header-Data Unit (HDU) in the FITS file. You can view more information about the FITS file with:

In [4]:
hdulist.info()
Filename: data/gll_iem_v02_P6_V11_DIFFUSE.fit
No.    Name         Type      Cards   Dimensions   Format
0    PRIMARY     PrimaryHDU      34   (720, 360, 30)   float32   
1    ENERGIES    BinTableHDU     19   30R x 1C     [D]   

As we can see, this file contains two HDUs. The first contains an image cube, the second a table with one column of double-precision floating point values. To access the primary HDU, which contains the main data, you can then do:

In [5]:
hdu = hdulist[0]

The hdu object then has two important attributes: data, which behaves like a Numpy array, can be used to access the data, and header, which behaves like a dictionary, can be used to access the header information. First, we can take a look at the data:

In [6]:
hdu.data.shape
Out[6]:
(30, 360, 720)

This tells us that it is a 3-d cube. We can now take a peak at the header:

In [7]:
hdu.header
Out[7]:
SIMPLE  =                    T / Written by IDL:  Thu Jan 20 07:19:05 2011      
BITPIX  =                  -32 /                                                
NAXIS   =                    3 / number of data axes                            
NAXIS1  =                  720 / length of data axis 1                          
NAXIS2  =                  360 / length of data axis 2                          
NAXIS3  =                   30 / length of data axis 3                          
EXTEND  =                    T / FITS dataset may contain extensions            
COMMENT   FITS (Flexible Image Transport System) format is defined in 'Astronomy
COMMENT   and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H 
FLUX    =        8.42259635886 /                                                
CRVAL1  =                   0. / Value of longitude in pixel CRPIX1             
CDELT1  =                  0.5 / Step size in longitude                         
CRPIX1  =                360.5 / Pixel that has value CRVAL1                    
CTYPE1  = 'GLON-CAR'           / The type of parameter 1 (Galactic longitude in 
CUNIT1  = 'deg     '           / The unit of parameter 1                        
CRVAL2  =                   0. / Value of latitude in pixel CRPIX2              
CDELT2  =                  0.5 / Step size in latitude                          
CRPIX2  =                180.5 / Pixel that has value CRVAL2                    
CTYPE2  = 'GLAT-CAR'           / The type of parameter 2 (Galactic latitude in C
CUNIT2  = 'deg     '           / The unit of parameter 2                        
CRVAL3  =                  50. / Energy of pixel CRPIX3                         
CDELT3  =    0.113828620540137 / log10 of step size in energy (if it is logarith
CRPIX3  =                   1. / Pixel that has value CRVAL3                    
CTYPE3  = 'photon energy'      / Axis 3 is the spectra                          
CUNIT3  = 'MeV     '           / The unit of axis 3                             
CHECKSUM= '3fdO3caL3caL3caL'   / HDU checksum updated 2009-07-07T22:31:18       
DATASUM = '2184619035'         / data unit checksum updated 2009-07-07T22:31:18 
DATE    = '2009-07-07'         /                                                
FILENAME= '$TEMPDIR/diffuse/gll_iem_v02.fit' /File name with version number     
TELESCOP= 'GLAST   '           /                                                
INSTRUME= 'LAT     '           /                                                
ORIGIN  = 'LISOC   '           /LAT team product delivered from the LISOC       
OBSERVER= 'MICHELSON'          /Instrument PI                                   
HISTORY Scaled version of gll_iem_v02.fit for use with P6_V11_DIFFUSE           

which shows that this is a plate carrée (-CAR) projection in galactic coordinates, and the third axis is photon energy. We can access individual header keywords using standard item notation:

In [8]:
hdu.header['TELESCOP']
Out[8]:
'GLAST'
In [9]:
hdu.header['INSTRUME']
Out[9]:
'LAT'

Provided that we started up ipython with the --pylab flag, we can plot one of the slices in photon energy:

In [10]:
plt.imshow(hdu.data[0, ...], origin='lower')
Out[10]:
<matplotlib.image.AxesImage at 0x446ab10>

Note that this is just a plot of an array, so the coordinates are just pixel coordinates at this stage. The data is stored with longitude increasing to the right (the opposite of the normal convention), but the Level 3 problem at the bottom of this page shows how to correctly flip the image.

Modifying data or header information in a FITS file object is easy. We can update existing header keywords:

In [11]:
hdu.header['TELESCOP'] = "Fermi Gamma-ray Space Telescope"

or add new ones:

In [12]:
hdu.header['MODIFIED'] = '2014-07-06'  # adds a new keyword

and we can also change the data, for example extracting only the first slice in photon energy:

In [13]:
hdu.data = hdu.data[0,:,:]

Note that this does not change the original FITS file, simply the FITS file object in memory. Since the data is now 2-dimensional, we can remove the WCS keywords for the third dimension:

In [14]:
hdu.header.remove('CRPIX3')
hdu.header.remove('CRVAL3')
hdu.header.remove('CDELT3')
hdu.header.remove('CUNIT3')
hdu.header.remove('CTYPE3')

You can write the FITS file object to a file with:

In [15]:
hdu.writeto('lat_background_model_slice.fits', clobber=True)

if you want to simply write out this HDU to a file, or:

In [16]:
hdulist.writeto('lat_background_model_slice_allhdus.fits', clobber=True)

if you want to write out all of the original HDUs, including the modified one, to a file.

Creating a FITS file from scratch

If you want to create a FITS file from scratch, you need to start off by creating an HDU object:

In [17]:
hdu = fits.PrimaryHDU()

and you can then populate the data and header attributes with whatever information you like:

In [18]:
import numpy as np
In [19]:
hdu.data = np.random.random((128, 128))

Note that setting the data automatically populates the header with basic information:

In [20]:
hdu.header
Out[20]:
SIMPLE  =                    T / conforms to FITS standard                      
BITPIX  =                  -64 / array data type                                
NAXIS   =                    2 / number of array dimensions                     
NAXIS1  =                  128                                                  
NAXIS2  =                  128                                                  
EXTEND  =                    T                                                  

and you should never have to set header keywords such as NAXIS, NAXIS1, and so on manually. We can then set additional header keywords:

In [21]:
hdu.header['telescop'] = 'Python Observatory'  # Note the keyword is case-insensitive; it will be written all caps

and we can then write out the FITS file to disk:

In [22]:
hdu.writeto('random_array.fits', clobber=True)

Creating a multi-extension FITS file

Many observatories format their FITS files such that no data is stored in the primary HDU. Instead, multiple image extensions are used to store each of the main image data, the data quality array, the error values, etc. This is the case for all modern HST observations, for example. The primary HDU stores no data, but does use the primary header to store metadata common to the observation.

As mentioned before, the HDUList object is similar to a Python list, and can be manipulated like one to store multiple HDUs as well as reorder them in a FITS file. For example we can create a primary HDU containing only metadata:

In [23]:
pri_hdu = fits.PrimaryHDU()
pri_hdu.header['telescop'] = 'Python Observatory'

and then create an image extension HDU using the ImageHDU class (this is no different from PrimaryHDU except that PrimaryHDU must come first in an HDUList; in fact, in most cases Astropy will ensure that before writing out a multi-extension file the first HDU is converted to a PrimaryHDU if possible.):

In [24]:
img_hdu = fits.ImageHDU(data=np.random.random((128, 128)))

Finally, add both HDUs to an HDUList and write it to disk:

In [25]:
hdul = fits.HDUList([pri_hdu, img_hdu])
hdul.writeto('random_array2.fits', clobber=True)

We can check that the output file is in the format we expected with the fits.info() convenience function. This is a shortcut for the pattern we saw earlier of hdul = fits.open(...); hdul.info():

In [26]:
fits.info('random_array2.fits')
Filename: random_array2.fits
No.    Name         Type      Cards   Dimensions   Format
0    PRIMARY     PrimaryHDU       5   ()              
1                ImageHDU         7   (128, 128)   float64   

Because an HDUList is like a Python list, new HDUs can also be appended or inserted:

In [27]:
new_hdu = fits.ImageHDU(data=np.random.random((256, 256)))
hdul.insert(1, new_hdu)
hdul.writeto('random_array3.fits', clobber=True)

This writes a new FITS file similar to the previous one, but with the new HDU inserted:

In [28]:
fits.info('random_array3.fits')
Filename: random_array3.fits
No.    Name         Type      Cards   Dimensions   Format
0    PRIMARY     PrimaryHDU       5   ()              
1                ImageHDU         7   (256, 256)   float64   
2                ImageHDU         7   (128, 128)   float64   

Inplace updates

In all the examples we've seen thus far, modifications have been made to parts of a FITS file and saved by writing out an entirely new FITS file to a different filename. For large files this can be an expensive and time-consuming operation, especially when only making small changes.

Both the header and data of an existing FITS file can be updated inplace by opening the file with the mode='update' option:

In [29]:
hdu_orig = fits.open('data/gll_iem_v02_P6_V11_DIFFUSE.fit')
tile = hdu_orig[0].data[0, 52:52 + 256, 232:232 + 256]

hdul = fits.open('random_array3.fits', mode='update')  # Update the file from the previous example
hdul[0].header['OBSERVER'] = 'Vera Rubin'
hdul[1].data = tile                                    # Replace some or all of the data values
hdul.close()                                           # Close the file to see that all changes were saved

Confirm that the file was updated:

In [30]:
hdul = fits.open('random_array3.fits')
hdul.info()
Filename: random_array3.fits
No.    Name         Type      Cards   Dimensions   Format
0    PRIMARY     PrimaryHDU       6   ()              
1                ImageHDU         7   (256, 256)   float32   
2                ImageHDU         7   (128, 128)   float64   

In [31]:
hdul[0].header['OBSERVER']
Out[31]:
'Vera Rubin'
In [32]:
plt.imshow(hdul[1].data, origin='lower')
Out[32]:
<matplotlib.image.AxesImage at 0x4a1f6d0>

One caveat about this is that if the file needs to be resized as the result of an update (many header cards were added or removed, or the size of the data was changed) this may still result in the file being rewritten. This happens automatically in the background, but may be more time consuming than modifications that don't significantly change the size of the file.

If you're familiar with Python's with statement, this can also be used when updating FITS files. This is a good idea to ensure that the file is closed/saved once all updates are complete:

In [33]:
with fits.open('random_array3.fits') as hdul:
    hdul[0].header['DATE-OBS'] = '2014-07-06'

Convenience functions

Like fits.info, a few other shortcut "convenience" functions are provided. For example, in cases where you just want to access the data or header in a specific HDU, you can use the following convenience functions:

In [34]:
data = fits.getdata('data/gll_iem_v02_P6_V11_DIFFUSE.fit')
header = fits.getheader('data/gll_iem_v02_P6_V11_DIFFUSE.fit')

To get the data or header for an HDU other than the first, you can specify the extension name or index. The second HDU is called energies, so we can do:

In [35]:
data = fits.getdata('data/gll_iem_v02_P6_V11_DIFFUSE.fit', extname='energies')

or:

In [36]:
data = fits.getdata('data/gll_iem_v02_P6_V11_DIFFUSE.fit', ext=1)

and similarly for getheader. The documentation provides a list of other such functions.

Note: While these functions are useful and convenient for interactive sessions, directly manipulating the data structures returned by fits.open as described above is currently more performant and should be preferred for automated scripts.

Accessing Tabular Data

Tabular data behaves very similarly to image data such as that shown above, but the data array is a structured Numpy array which requires column access via the item notation:

In [37]:
from astropy.io import fits
hdulist = fits.open('data/gll_psc_v08.fit')
In [38]:
hdulist[1].name
Out[38]:
'LAT_Point_Source_Catalog'

Display information on the first five columns (or all columns without the slice notation, but there are many in this table):

In [39]:
hdulist[1].columns[:5]
Out[39]:
ColDefs(
    name = 'Source_Name'; format = '18A'
    name = 'RAJ2000'; format = 'E'; unit = 'deg'; disp = 'F8.4'
    name = 'DEJ2000'; format = 'E'; unit = 'deg'; disp = 'F8.4'
    name = 'GLON'; format = 'E'; unit = 'deg'; disp = 'F8.4'
    name = 'GLAT'; format = 'E'; unit = 'deg'; disp = 'F8.4'
)
In [40]:
hdulist[1].data['RAJ2000']
Out[40]:
array([  2.33711034e-01,   4.38849270e-01,   6.79812014e-01, ...,
         3.59759430e+02,   3.59859894e+02,   3.59906921e+02], dtype=float32)
In [41]:
hdulist[1].data['DEJ2000']
Out[41]:
array([ -7.81549788, -41.99647903,  62.33962631, ..., -30.62516785,
        67.86333466,  65.73053741], dtype=float32)

FITS tables can also be accessed via Astropy's Table interface like we saw earlier:

In [42]:
from astropy.table import Table

t = Table.read('data/gll_psc_v08.fit')
WARNING: hdu= was not specified but multiple tables are present, reading in first available table (hdu=1) [astropy.io.fits.connect]
WARNING:astropy:hdu= was not specified but multiple tables are present, reading in first available table (hdu=1)
WARNING: UnitsWarning: 'photon/cm**2/MeV/s' contains multiple slashes, which is discouraged by the FITS standard [astropy.units.format.generic]
WARNING:astropy:UnitsWarning: 'photon/cm**2/MeV/s' contains multiple slashes, which is discouraged by the FITS standard
WARNING: UnitsWarning: 'photon/cm**2/s' contains multiple slashes, which is discouraged by the FITS standard [astropy.units.format.generic]
WARNING:astropy:UnitsWarning: 'photon/cm**2/s' contains multiple slashes, which is discouraged by the FITS standard
WARNING: AstropyDeprecationWarning: The unit u'erg' has been deprecated in the FITS standard. [astropy.units.format.fits]
WARNING:astropy:AstropyDeprecationWarning: The unit u'erg' has been deprecated in the FITS standard.
WARNING: UnitsWarning: 'erg/cm**2/s' contains multiple slashes, which is discouraged by the FITS standard [astropy.units.format.generic]
WARNING:astropy:UnitsWarning: 'erg/cm**2/s' contains multiple slashes, which is discouraged by the FITS standard

Note that if a FITS file contains a table, the first table in the file is returned by default. The hdu= keyword argument to Table.read can be used to specify and HDU to read from.

In [43]:
t[t.colnames[:5]]  # Display the first 5 columns
Out[43]:
Source_NameRAJ2000DEJ2000GLONGLAT
degdegdegdeg
2FGL J0000.9-07480.233711-7.815588.8292-67.2805
2FGL J0001.7-41590.438849-41.9965334.076-71.9967
2FGL J0002.7+62200.67981262.3396117.3120.000752292
2FGL J0004.2+22081.0556922.1365108.732-39.4298
2FGL J0004.7-47361.18021-47.6116323.89-67.571
2FGL J0006.1+38211.5251638.3502113.245-23.6672
2FGL J0007.0+73031.7735273.0545119.66510.4647
2FGL J0007.7+6825c1.9250568.4232118.9115.89366
2FGL J0007.8+47131.9743247.2298115.304-14.9959
2FGL J0008.7-23442.19605-23.736349.9861-79.7949
2FGL J0009.0+06322.262316.54235104.453-54.8014
...............
2FGL J2353.5-3034358.384-30.580114.2726-76.8716
2FGL J2354.2-6615358.565-66.2627311.803-49.8693
2FGL J2356.0-5256359.021-52.9389320.943-62.2108
2FGL J2356.1+4034359.02840.5814111.712-21.0818
2FGL J2356.3+0432359.0914.54198.0685-55.6494
2FGL J2358.4-1811359.613-18.196466.3697-74.8794
2FGL J2358.9+6325359.74563.4218117.1051.14516
2FGL J2359.0-3037359.759-30.625212.8756-78.0167
2FGL J2359.4+6751c359.8667.8633118.0485.48612
2FGL J2359.6+6543c359.90765.7305117.6373.3929

In a future version of Astropy, FITS tables will always be returned in this format. At present, however, there remain some issues with supporting all FITS tables with the Table class.

Practical Exercises

Level 1

Try and read in one of your own FITS files using astropy.io.fits, and see if you can also plot the array values in Matplotlib. Also, examine the header, and try and extract individual values. You can even try and modify the data/header and write the data back out - but take care not to write over the original file!

Level 2

Read in the LAT Point Source Catalog and make a scatter plot of the Galactic Coordinates of the sources (complete with axis labels). Bonus points if you can make the plot go between -180 and 180 instead of 0 and 360 degrees. Note that the Point Source Catalog contains the Galactic Coordinates, so no need to convert them.

Level 3

Using Matplotlib, make an all-sky plot of the LAT Background Model in the Plate Carrée projection showing the LAT Point Source Catalog overlaid with markers, and with the correct coordinates on the axes. You should do this using only astropy.io.fits, Numpy, and Matplotlib (no WCS or coordinate conversion library). Hint: the -CAR projection is such that the x pixel position is proportional to longitude, and the y pixel position to latitude. Bonus points for a pretty colormap.