Spectral Line Data Cubes in Astronomy - Part 2

We are doing a complete re-analysis of the same NGC 6503 case, but now with more tools that the community has developed. For this you will need to modify your python environment.


In [2]:
%matplotlib inline

In [2]:
# python 2-3 compatibility
from __future__ import print_function

Reading the data

Two modules are introduced here: spectral_cube and radio_beam, both from the https://github.com/radio-astro-tools project.

Manual

git clone https://github.com/radio-astro-tools/spectral_cube
cd spectral_cube
python setup.py install

git clone https://github.com/radio-astro-tools/radio_beam
cd radio_beam
python setup.py install

Automated

1) For spectral-cube you can simple use the pip command from the dos/unix shell to install it:

pip install spectral_cube

Note the underscore and dash convention.

2) For radio_beam the developers have not submitted this to the python community (https://pypi.python.org/pypi), and thus you will need to install this manually. Go to the developers page on github: https://github.com/radio-astro-tools/radio_beam and near the top right corner you will see a button Download ZIP. Extract this somewhere (often in your Downloads directory) and use your python to install this. For example, again from your doc/unix shell you would type:

 cd ~/Downloads/radio_beam-master
 python setup.py install

In [3]:
import numpy as np
import astropy.units as u
from spectral_cube import SpectralCube
import radio_beam

In [8]:
cube = SpectralCube.read('../data/ngc6503.cube.fits')
print(cube)


SpectralCube with shape=(89, 251, 371) and unit=Jy:
 n_x:    371  type_x: RA---SIN  unit_x: deg    range:   266.676861 deg:  267.895600 deg
 n_y:    251  type_y: DEC--SIN  unit_y: deg    range:    70.007750 deg:   70.286667 deg
 n_s:     89  type_s: FREQ      unit_s: Hz     range: 1419177268.848 Hz:1421325706.348 Hz

A FITS file consists of a series of Header-Data-Units (HDU). Usually there is only one, representing the image. But this file has two. For now, we're going to ignore the second, which is a special table and in this example happens to be empty anyways. Each HDU has a header, and data. The data in this case is a numpy array, and represents the image (cube):


In [9]:
h = hdu[0].header
d = hdu[0].data
print(d.shape, d.min(), d.max(), d.mean(), np.median(d), d.std())
print("Signal/Noise  (S/N):",d.max()/d.std())


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-9-67e688f7815b> in <module>()
----> 1 h = hdu[0].header
      2 d = hdu[0].data
      3 print(d.shape, d.min(), d.max(), d.mean(), np.median(d), d.std())
      4 print("Signal/Noise  (S/N):",d.max()/d.std())

NameError: name 'hdu' is not defined

From the shape (1,89,251,371) we can see this image is actually 4 dimensional, although the 4th dimension is dummy. There are 371 pixels along X, 251 along Y, and 89 slices or spectral channels. It looks like the noise is around 0.00073 and a peak value 0.017, thus a signal to noise of a little over 23, so quite strong.

In python you can remove that dummy 4th axis, since we are not going to use it any further.


In [6]:
d = d.squeeze()
print(d.shape)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-6-08e210d6902e> in <module>()
----> 1 d = d.squeeze()
      2 print(d.shape)

NameError: name 'd' is not defined

In case you were wondering about that 4th redundant axis. In astronomy we sometimes observe more than one type of radiation. Since waves are polarized, we can have up to 4 so called Stokes parameters, describing the waves as e.g. linear or circular polarized radiation. We will ignore that here, but they are sometimes stored in that 4th dimension. Sometimes they are stored as separate cubes.

Plotting some basics


In [10]:
import matplotlib.pyplot as plt

In [11]:
z = 35
im = d[z,:,:]                              #   im = d[z]     also works
plt.imshow(im,origin=['Lower'])
plt.colorbar()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-11-efa4f25be6f5> in <module>()
      1 z = 35
----> 2 im = d[z,:,:]                              #   im = d[z]     also works
      3 plt.imshow(im,origin=['Lower'])
      4 plt.colorbar()

NameError: name 'd' is not defined

There are 89 channels (slices) in this cube, numbered 0 through 88 in the usual python sense. Pick a few other slices by changing the value in z= and notice that the first few and last few appear to be just noise and that the V-shaped signal changes shape through the channels. Perhaps you should not be surprised that these are referred to as butterfly diagrams.


In [12]:
# look at a histogram of all the data (histogram needs a 1D array)
d1 = d.ravel()                 # ravel() doesn't make a new copy of the array, saving memory
print(d1.shape)
(n,b,p) = plt.hist(d1, bins=100)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-0727ce4ea143> in <module>()
      1 # look at a histogram of all the data (histogram needs a 1D array)
----> 2 d1 = d.ravel()                 # ravel() doesn't make a new copy of the array, saving memory
      3 print(d1.shape)
      4 (n,b,p) = plt.hist(d1, bins=100)

NameError: name 'd' is not defined

Notice that the histogram is on the left in the plot, and we already saw the maximum data point is 0.0169835.

So let us plot the vertical axis logarithmically, so we can better see what is going on.


In [10]:
(n,b,p) = plt.hist(d1,bins=100,log=True)



In [13]:
# pick a slice and make a histogram and print the mean and standard deviation of the signal in that slice
z=0
imz = d[z,:,:].flatten()
(n,b,p) = plt.hist(imz,bins=100)
print(imz.mean(), imz.std())


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-13-d544f1477a97> in <module>()
      1 # pick a slice and make a histogram and print the mean and standard deviation of the signal in that slice
      2 z=0
----> 3 imz = d[z,:,:].flatten()
      4 (n,b,p) = plt.hist(imz,bins=100)
      5 print(imz.mean(), imz.std())

NameError: name 'd' is not defined

Exercise : observe by picking some values of z that the noise seems to vary a little bit from one end of the band to the other. Store the noise in channel 0 and 88 in variables sigma0 and sigma88:


In [134]:
xpeak = 175
ypeak = 125
channel = np.arange(d.shape[0])
spectrum = d[:,ypeak,xpeak]
zero = spectrum * 0.0
plt.plot(channel,spectrum)
plt.plot(channel,zero)


Out[134]:
[<matplotlib.lines.Line2D at 0x7f87d0da1fd0>]

In [ ]:
sigma0 = 0.00056
sigma88 = 0.00059

In [12]:
import scipy.signal
import scipy.ndimage.filters as filters

Smoothing a cube to enhance the signal to noise


In [103]:
z = 0
sigma = 2.0
ds1 = filters.gaussian_filter(d[z],sigma)                    # ds1 is a single smoothed slice
print ds1.shape, ds1.mean(), ds1.std()
plt.imshow(ds1,origin=['Lower'])
plt.colorbar()


(251, 371) -1.2134e-06 0.000186323
Out[103]:
<matplotlib.colorbar.Colorbar instance at 0x7f87d0525cb0>

Notice that the noise is indeed lower than your earlier value of sigma0. We only smoothed one single slice, but we actually need to smooth the whole cube. Each slice with sigma, but we can optionally also smooth in the spectral dimension a little bit.


In [104]:
ds = filters.gaussian_filter(d,[1.0,sigma,sigma])              # ds is a smoothed cube
plt.imshow(ds[z],origin=['Lower'])
plt.colorbar()
print ds.max(),ds.max()/ds1.std()


0.010926 58.6398

Notice that, although the peak value was lowered a bit due to the smoothing, the signal to noise has increased from the original cube. So, the signal should stand out a lot better.

Exercise : Observe a subtle difference in the last two plots. Can you see what happened here?

Masking


In [105]:
import numpy.ma as ma

In [132]:
#  sigma0 is the noise in the original cube
nsigma = 0.0
dm = ma.masked_inside(d,-nsigma*sigma0,nsigma*sigma0)
print dm.count()


8287769

In [133]:
mom0 = dm.sum(axis=0)
plt.imshow(mom0,origin=['Lower'])
plt.colorbar()
#
(ypeak,xpeak) = np.unravel_index(mom0.argmax(),mom0.shape)
print "PEAK at location:",xpeak,ypeak,mom0.argmax()


PEAK at location: 149 113 42072

In [109]:
spectrum2 = ds[:,ypeak,xpeak]
plt.plot(channel,spectrum2)
plt.plot(channel,zero)


Out[109]:
[<matplotlib.lines.Line2D at 0x7f87d1030050>]

In [110]:
mom0s = ds.sum(axis=0)
plt.imshow(mom0s,origin=['Lower'])
plt.colorbar()


Out[110]:
<matplotlib.colorbar.Colorbar instance at 0x7f87d1249488>

In [ ]:

Velocity fields

The mean velocity is defined a the first moment

$$ \Sum{(v.I)} \over \Sum{(I)} $$

In [117]:
nz = d.shape[0]
vchan = np.arange(nz).reshape(nz,1,1)
vsum = vchan * d
vmean = vsum.sum(axis=0)/d.sum(axis=0)
print "MINMAX",vmean.min(),vmean.max()
plt.imshow(vmean,origin=['Lower'],vmin=0,vmax=88)
plt.colorbar()


MINMAX -925832.096555 302361.32862
Out[117]:
<matplotlib.colorbar.Colorbar instance at 0x7f87cfba5dd0>

Although we can recognize an area of coherent motions (the red and blue shifted sides of the galaxy), there is a lot of noise in this image. Looking at the math, we are dividing two numbers, both of which can be noise, so the outcome can be "anything". If anything, it should be a value between 0 and 88, so we could mask for that and see how that looks.

Let us first try to see how the smoothed cube looked.


In [64]:
nz = ds.shape[0]
vchan = np.arange(nz).reshape(nz,1,1)
vsum = vchan * ds
vmean = vsum.sum(axis=0)/ds.sum(axis=0)
print vmean.shape,vmean.min(),vmean.max()
plt.imshow(vmean,origin=['Lower'],vmin=0,vmax=89)
plt.colorbar()


(89, 251, 371)
(251, 371) -980581.004031 66626.4580242
Out[64]:
<matplotlib.colorbar.Colorbar instance at 0x7f87d2b9a710>

Although more coherent, there are still bogus values outside the image of the galaxy. So we are looking for a hybrid of the two methods. In the smooth cube we saw the signal to noise is a lot better defined, so we will define areas in the cube where the signal to noise is high enough and use those in the original high resolution cube.


In [65]:
# this is all messy , we need a better solution, a hybrid of the two:
noise = ds[0:5].flatten()
(n,b,p) = plt.hist(noise,bins=100)
print noise.mean(), noise.std()


2.96844e-07 3.55913e-05

In [66]:
sigma0 = noise.std()
nsigma = 5.0
cutoff = sigma0*nsigma
dm = ma.masked_inside(ds,-cutoff,cutoff)
print cutoff,dm.count()


0.000177956462721 334887

In [67]:
dm2=ma.masked_where(ma.getmask(dm),d)

In [112]:
vsum = vchan * dm2
vmean = vsum.sum(axis=0)/dm2.sum(axis=0)
print vmean.min(),vmean.max()
plt.imshow(vmean,origin=['Lower'],vmin=0,vmax=89)
plt.colorbar()


-1004.71169602 5646.6715688
Out[112]:
<matplotlib.colorbar.Colorbar instance at 0x7f87d00bab00>

And voila, now this looks a lot better.

Some ipython widget experiments

ipython widgets are interactive widgets that you can add to your script and with a helper function dynamically control output, e.g. a figure.


In [16]:
import networkx as nx
#from ipywidgets import interact

In [16]:


In [16]: