Load FITS file and list its HDUs.

In [56]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from astropy.io import fits

f = fits.open('NGC2916.V500.rscube.fits')
f.info()
print 'array shape:', f[0].data.shape
Filename: NGC2916.V500.rscube.fits
No.    Name         Type      Cards   Dimensions   Format
  0  PRIMARY     PrimaryHDU     540   (78, 72, 1877)   float32   
  1  ERROR       ImageHDU         9   (78, 72, 1877)   float32   
  2  ERRWEIGHT   ImageHDU         9   (78, 72, 1877)   float32   
  3  BADPIX      ImageHDU         9   (78, 72, 1877)   uint8   
  4  FIBCOVER    ImageHDU         9   (78, 72, 1877)   uint8   
  5  FLAT        ImageHDU        40   (78, 72)   float32   
array shape: (1877, 72, 78)

Compute wavelengths from primary header. We also have to take it to the rest frame.

In [82]:
def get_wave(h):
    from astropy import wcs
    Nl = h['NAXIS3']
    w = wcs.WCS(h).sub([3])
    wave = w.wcs_pix2world(np.arange(Nl), 0)
    # The value returned is a list of arrays. Since we have only one, we get rid of the list.
    return wave[0]

h = f[0].header
l_obs = get_wave(h)

# Redshift
v = h['MED_VEL']
c = 299792.458  # km / s
z = v / c
print 'Redshift: %.3f' % z
l_obs /= 1.0 + z

print 'Wavelengths:', l_obs
Redshift: 0.012
Wavelengths: [ 3703.5573513   3705.53310876  3707.50886621 ...,  7406.12682387
  7408.10258133  7410.07833878]

Load flux and error spectra, taking the bad pixels into account. Plotting the flux and error spectra for a given pixel.

In [76]:
flux_unit = 1e-16 # erg/s/cm2/AA
badpix_mask = f['badpix'].data > 0

# The 1 + z factor is the k-correction.
flux = np.ma.array(f['primary'].data, mask=badpix_mask) * (flux_unit * (1.0 + z))
error = np.ma.array(f['error'].data, mask=badpix_mask) * (flux_unit * (1.0 + z))

plt.plot(l_obs, flux[:, 32, 32], 'b-', label='flux')
plt.plot(l_obs, 10.0 * error[:, 32, 32], 'r-', label='error x 10')
plt.xlabel(r'$\lambda\ [\mathrm{\AA}]$')
plt.ylabel(r'$F_\lambda\ [\mathrm{erg}\,\mathrm{s}^{-1}\,\mathrm{cm}^{-2}\,\mathrm{\AA}^{-1}]$')
plt.legend()
Out[76]:
<matplotlib.legend.Legend at 0x126143e50>

Taking a slice of the flux.

In [45]:
plt.imshow(flux[:, 31, :].T, extent=[l_obs[0], l_obs[-1], 0, flux.shape[2] - 1], aspect='auto')
plt.xlabel(r'$\lambda\ [\mathrm{\AA}]$')
plt.ylabel(r'dec. $[\mathrm{arcsec}]$')
plt.colorbar()
Out[45]:
<matplotlib.colorbar.Colorbar at 0x128c2b610>

Taking an image of a spectral band $5635 \pm 45\,\text{\AA}$.

In [84]:
norm_lambda = 5635.0
spec_window = (l_obs > norm_lambda - 45.0) & (l_obs < norm_lambda + 45.0)
image = np.median(flux[spec_window], axis=0)
plt.imshow(image)
Out[84]:
<matplotlib.image.AxesImage at 0x125e18250>

Mask that foreground object at 11 o'clock.

In [85]:
flux[:, 42:48, 30:36] = np.ma.masked
image = np.median(flux[spec_window], axis=0)
plt.imshow(image)
Out[85]:
<matplotlib.image.AxesImage at 0x125f84c10>

Let's begin getting fancy. Calculating the distance of each pixel to the center (brightest pixel).

In [86]:
y0, x0 = np.where(image == image.max())
Ny, Nx = image.shape
y, x = np.ogrid[:Ny, :Nx]
r = np.sqrt((x - x0)**2 + (y - y0)**2)
plt.imshow(r)
Out[86]:
<matplotlib.image.AxesImage at 0x12d075150>

Now we can calculate the mean value of flux in bins of r. This is the radial profile of the flux. It may be accomplished in numpy using a little trick with the histogram function.

Usually one creates a histogram by counting the number of elements inside the bins. It is possible also to set weights to the items, so that each does not count as 1 anymore. By choosing the flux as weight, we are in fact computing the sum of flux inside the bins. By dividing the number of items (the regular histogram), we get the mean value of flux inside the bin.

In [89]:
# Bin edges
r_bins = np.arange(30)
# Bin centers
r_coord = np.arange(29) + 0.5

sum_image_r, _ = np.histogram(r, bins=r_bins, weights=image)
N_r, _ = np.histogram(r, bins=r_bins)
image_r = sum_image_r / N_r

plt.plot(r_coord, np.log10(image_r), 'k-')
plt.xlabel(r'$r\, [\mathrm{arcsec}]$')
plt.ylabel(r'$\log\,F_\lambda\ [\mathrm{erg}\,\mathrm{s}^{-1}\,\mathrm{cm}^{-2}\,\mathrm{\AA}^{-1}]$')
Out[89]:
<matplotlib.text.Text at 0x12d3d1810>

A common spatial scale used in galaxies is the Half Light Radius (HLR), that is defined as the radius of a circle (or ellipse) that contains half of the light (flux, luminosity, etc). It may be calculated in a similar way to the radial profile. In fact using the sum of flux in radial bins, we only have to create a cumulative sum and find where it reaches half its maximum value. Repeating the plot with HLR as a scale.

In [90]:
from scipy.interpolate import interp1d

cumsum_image_r = sum_image_r.cumsum()
# We calculate r as a function of the cumsum. We can only do that because the cumsum is monotonic.
r_cs = interp1d(cumsum_image_r, r_coord)
cs_max = cumsum_image_r.max()

HLR = r_cs(cs_max / 2.0)
print 'HLR = %.1f' % HLR

plt.plot(r_coord / HLR, np.log10(image_r), 'k-')
plt.xlabel(r'$r\, [\mathrm{arcsec}]$')
plt.ylabel(r'$\log\,F_\lambda\ [\mathrm{erg}\,\mathrm{s}^{-1}\,\mathrm{cm}^{-2}\,\mathrm{\AA}^{-1}]$')
HLR = 14.8
Out[90]:
<matplotlib.text.Text at 0x12d52c410>

Now let's do some science. Calculate D(4000) of all pixels. It is defined as the ratio between the flux in the spectral windows of [3850.0, 3950.0] and [4000.0, 4100.0] Angstroms.

In [75]:
# Assume evenly spaced wavelengths.
dl = l_obs[1] - l_obs[0]
blue_window = (l_obs < 3950.0) & (l_obs > 3850)
red_window = (l_obs < 4100.0) & (l_obs > 4000)
flux_b = np.trapz(flux[blue_window], l_obs[blue_window], axis=0)
flux_r = np.trapz(flux[red_window], l_obs[red_window], axis=0)

plt.imshow(flux_r / flux_b, vmin=1, vmax=2)
plt.colorbar()
Out[75]:
<matplotlib.colorbar.Colorbar at 0x126002210>

The white spots in the image are pixels where the data is all masked. This may be mitigated using COMBO cubes.

Exercise: Calculate the equivalent width of $\mathrm{H}\delta$.

$$ \mathrm{EW} = \int \frac{C_\lambda - F_\lambda} {C_\lambda} \mathrm{d}\lambda $$

where $C_\lambda$ is the estimated continuum inside the line. Use these limits for the measurements:

  • Absorption line: 4083.500, 4122.250
  • Blue continuum: 4041.600, 4079.750
  • Red continuum: 4128.500, 4161.000
In [ ]: