MPDAF Cube objects contain a series of images at neighboring wavelengths. These
are stored as a 3D array of pixel values. The wavelengths of the images increase
along the first dimension of this array, while the Y and X axes of the images
lie along the second and third dimensions. In addition to the data array, Cubes
WaveCoord object that describes the wavelength
scale of the spectral axis of the cube, and a
WCS object that
describes the spatial coordinates of the spatial axes. Optionally a 3D array of
variances can be provided to give the statistical uncertainties of the
fluxes. Finally a mask array is provided for indicating bad pixels.
The fluxes and their variances are stored in numpy masked arrays, so virtually all numpy and scipy functions can be applied to them.
In : import numpy as np In : import matplotlib.pyplot as plt In : import astropy.units as u In : from mpdaf.obj import Cube, WCS, WaveCoord
There are two common ways to obtain a
- A cube can be created from a user-provided 3D array of pixel values, or from both a 3D array of pixel values, and a corresponding 3D array of variances. These can be simple numpy arrays, or they can be numpy masked arrays in which bad pixels have been masked. For example:
In : wcs1 = WCS(crval=0, cdelt=0.2) In : wave1 = WaveCoord(cdelt=1.25, crval=4000.0, cunit=u.angstrom) # numpy data array In : MyData = np.ones((400, 30, 30)) # cube filled with MyData In : cube = Cube(data=MyData, wcs=wcs1, wave=wave1) # cube 400X30x30 filled with data In : cube.info() [INFO] 400 x 30 x 30 Cube (no name) [INFO] .data(400 x 30 x 30) (no unit), no noise [INFO] spatial coord (pix): min:(0.0,0.0) max:(5.8,5.8) step:(0.2,0.2) rot:-0.0 deg [INFO] wavelength: min:4000.00 max:4498.75 step:1.25 Angstrom
- Alternatively, a cube can be read from a FITS file. In this case the flux and variance values are read from specific extensions:
# data and variance arrays read from the file (extension DATA and STAT) In : obj1 = Cube('obj/CUBE.fits') In : obj1.info() [INFO] 1595 x 10 x 20 Cube (obj/CUBE.fits) [INFO] .data(1595 x 10 x 20) (1e-40 erg / (Angstrom cm2 s)), .var(1595 x 10 x 20) [INFO] center:(-30:00:00.4494,01:20:00.4376) size:(2.000",4.000") step:(0.200",0.200") rot:-0.0 deg frame:FK5 [INFO] wavelength: min:7300.00 max:9292.50 step:1.25 Angstrom
By default, if a FITS file has more than one extension, then it is expected to have a ‘DATA’ extension that contains the pixel data, and possibly a ‘STAT’ extension that contains the corresponding variances. If the file doesn’t contain extensions of these names, the “ext=” keyword can be used to indicate the appropriate extension or extensions.
The spatial and spectral world-coordinates of a Cube are recorded within
WaveCoord objects, respectively. When a cube
is read from a FITS file, these are automatically generated, based on FITS
header keywords. Alternatively, when an cube is extracted from another cube,
they are derived from the world coordinate information of the original cube.
In the above example, information about a cube is printed using the
info method. In this case the information is as follows:
- The cube has dimensions, 1595 x 10 x 20, which means that there are 1595 images of different wavelengths, each having 10 x 20 spatial pixels.
- In addition to the pixel array (.data(1595 x 10 x 20)), there is also a variance array of the same dimensions (.var(1595 x 10 x 20)).
- The flux units of the pixels are 10-20 erg/s/cm2/Angstrom.
- The center of the field of view is at DEC: -30° 0’ 0.45” and RA: 1°20‘0.437”.
- The size of the field of view is 2 arcsec x 4 arcsec. The pixel dimensions are 0.2 arcsec x 0.2 arcsec.
- The rotation angle of the field is 0°, which means that North is along the positive Y axis.
- The wavelength range is 7300-9292.5 Angstrom with a step of 1.25 Angstrom
The indexing of the cube arrays follows the Python conventions for indexing a 3D array. The pixel in the bottom-lower-left corner is referenced as [0,0,0] and the pixel [k,p,q] refers to the horizontal position q, the vertical position p, and the spectral position k, as follows:
(see Spectrum, Image and Cube format for more information).
The following example computes a reconstructed white-light image and displays it. The white-light image is obtained by summing each spatial pixel of the cube along the wavelength axis. This converts the 3D cube into a 2D image. The cube in this examples contains an observation of a single galaxy.
In : ima1 = obj1.sum(axis=0) In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e759b710> In : ima1.plot(scale='arcsinh', colorbar='v') Out: <matplotlib.image.AxesImage at 0x7f42e79c2910>
The next example computes the overall spectrum of the cube by taking the cube and summing along the X and Y axes of the image plane. This yields the total flux per spectral pixel.
In : sp1 = obj1.sum(axis=(1,2)) In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e8abe290> In : sp1.plot()
Loops over all spectra¶
The examples in this section will demonstrate how a procedure can be applied iteratively to the spectra of every image pixel of a cube. The goal of the examples will be to create a version of the above data-cube that has had the continuum background subtracted. For each image pixel, a low-order polynomial will be fitted to the spectrum of that pixel. This results in a polynomial curve that approximates the continuum spectrum of the pixel. This polynomial is then subtracted from the spectrum of that pixel, and the difference spectrum is recorded in a new output cube.
To illustrate the procedure, we start by fitting the continuum to the overall spectrum that was obtained in the previous example:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e78a8ad0> In : cont1 = sp1.poly_spec(5) In : sp1.plot() In : cont1.plot(color='r')
Next we do the same to a single pixel at the edge of the galaxy:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e45dd290> In : sp1 = obj1[:,5,2] In : sp1.plot() In : sp1.poly_spec(5).plot(color='r')
In principle, the above procedure could be performed to each pixel by writing
a nested loop over the X and Y axes of the cube. However, instead of using two
loops, one can use the spectrum iterator method,
iter_spe of the
Cube object. In the following example this is used to iteratively extract the
six spectra of a small 2 x 3 pixel sub-cube, and determine their peak values:
In : from mpdaf.obj import iter_spe In : small = obj1[:,0:2,0:3] In : small.shape Out: (1595, 2, 3) In : for sp in iter_spe(small): ....: print(sp.data.max()) ....:
Now let’s use the same approach to do the continuum subtraction procedure. We
start by creating an empty datacube with the same dimensions as the original
cube, but without variance information (using the
clone method). Using two spectrum iterators we
iteratively extract the spectra of each image pixel of the input cube and the
empty output cube. At each iteration we then fit a polynomial spectrum to the
input spectrum and record it in the output spectrum.
In : cont1 = obj1.clone(data_init=np.empty, var_init=np.zeros) In : for sp, co in zip(iter_spe(obj1), iter_spe(cont1)): ....: co[:] = sp.poly_spec(5) ....:
The result is a continuum datacube. Note that we have used the
= sp.poly_spec(5) assignment rather than the more intuitive
= sp.poly_spec(5) assignment. The difference is that in python
changes the object that the co variable refers to, whereas
the contents of the object that it currently points to. We want to change the
contents of the spectrum in the output cube, so the latter is needed.
There is another way to compute the continuum datacube that can be much faster
when used on a computer with multiple processors. This is to use the
This uses multiple processors to apply a specified function to each spectrum of
a cube and return a new cube that contains the resulting spectra:
In : from mpdaf.obj import Spectrum In : cont2 = obj1.loop_spe_multiprocessing(f=Spectrum.poly_spec, deg=5) [INFO] loop_spe_multiprocessing (poly_spec): 200 tasks [K Completed 90 of 200 tasks (45%) in 5.0s [K
To compare the results of the two methods, the following example sums the images of the two continuum cubes over the wavelength axis and displays the resulting white-light images of the continuum:
In : rec1 = cont1.sum(axis=0) In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e44b7a90> In : rec1.plot(scale='arcsinh', colorbar='v', title='method 1') Out: <matplotlib.image.AxesImage at 0x7f42dfc3cf50> In : rec2 = cont2.sum(axis=0) In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42dfe8f250> In : rec2.plot(scale='arcsinh', colorbar='v', title='method2') Out: <matplotlib.image.AxesImage at 0x7f42dfdb3c90>
Next we subtract the continuum cube from the original cube to obtain a cube of the line emission of the galaxy. For display purposes this is then summed along the wavelength axis to yield an image of the sum of all of the emission lines in the cube:
In : line1 = obj1 - cont1 In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e44d8b90> In : line1.sum(axis=0).plot(scale='arcsinh', colorbar='v') Out: <matplotlib.image.AxesImage at 0x7f42dff24550>
Next we compute the equivalent width of the Hα emission in the galaxy. First we isolate the emission line by truncating the object datacube in wavelength:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e44aa890> # Obtain the overall spectrum of the cube. In : sp1 = obj1.sum(axis=(1,2)) In : sp1.plot() # Obtain the spectral pixel indexes of wavelengths 9000 and 9200 In : k1,k2 = sp1.wave.pixel([9000,9200], nearest=True) # Extract a sub-cube restricted to the above range of wavelengths. In : emi1 = obj1[k1:k2+1,:,:] In : emi1.info() [INFO] 161 x 10 x 20 Cube (obj/CUBE.fits) [INFO] .data(161 x 10 x 20) (1e-40 erg / (Angstrom cm2 s)), .var(161 x 10 x 20) [INFO] center:(-30:00:00.4494,01:20:00.4376) size:(2.000",4.000") step:(0.200",0.200") rot:-0.0 deg frame:FK5 [INFO] wavelength: min:9000.00 max:9200.00 step:1.25 Angstrom # Obtain the overall spectrum of the above sub-cube. In : sp1 = emi1.sum(axis=(1,2)) # Plot the sub-spectrum in red over the original spectrum. In : sp1.plot(color='r')
Next we fit and subtract the continuum. Before doing the polynomial fit we mask
the region of the emission lines (
sp1.mask), so that the lines don’t affect
the fit, and then we perform a linear fit between the continuum on either side
of the masked region. Then the spectrum is unmasked and the continuum
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e44d8150> # Mask the region containing the line emission. In : sp1.mask_region(9050, 9125) # Fit a line to the continuum on either side of the masked region. In : cont1 = sp1.poly_spec(1) # Unmask the region containing the line emission. In : sp1.unmask() In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e65a62d0> In : sp1.plot() In : cont1.plot(title="Fitted continuum") In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e431af90> # Subtract the continuum from the spectrum to leave the line emission. In : line1 = sp1 - cont1 In : line1.plot(title="Continuum subtracted")
Next we compute the total Hα line flux by simple integration (taking into account the pixel size in Angstrom) over the wavelength range centered around the Hα line and the continuum mean flux at the same location:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e43849d0> # Find the spectral pixel index of the peak flux. In : k = line1.data.argmax() In : line1[55-10:55+11].plot(color='r') # Integrate by summing pixels, multiplied by the pixel width. In : fline = (line1[55-10:55+11].sum()*line1.unit) * (line1.get_step(unit=line1.wave.unit)*line1.wave.unit) # Obtain the mean continuum flux. In : cline = cont1[55-10:55+11].mean()*cont1.unit # Compute the equivalent width of the line. In : ew = fline/cline In : print(fline, cline, ew) [ 9224.61872579 507.99642777] 1e-40 erg / (cm2 s) [ 1936.55733796 inf] 1e-40 erg / (Angstrom cm2 s) [ 4.7634111 0. ] Angstrom
Finally we repeat this for all datacube spectra, and we save the Hα flux and
equivalent width in two images. We start by creating two images with identical
shapes and world-coordinates for the reconstructed image and then use the
In : ha_flux = ima1.clone(data_init=np.empty) In : cont_flux = ima1.clone(data_init=np.empty) In : ha_ew = ima1.clone(data_init=np.empty) In : for sp,pos in iter_spe(emi1, index=True): ....: p,q = pos ....: sp.mask_region(9050, 9125) ....: cont = sp.poly_spec(1) ....: sp.unmask() ....: line = sp - cont ....: fline = line[55-10:55+11].sum() * line.get_step(unit=line.wave.unit) ....: cline = cont[55-10:55+11].mean() ....: ew = fline/cline ....: cont_flux[p,q] = cline ....: ha_flux[p,q] = fline ....: ha_ew[p,q] = ew ....: In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e4434d10> In : cont_flux.plot(title="continuum mean flux (%s)"%cont_flux.unit, colorbar='v') Out: <matplotlib.image.AxesImage at 0x7f42e412e250> In : ha_flux.unit = sp.unit * sp.wave.unit In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e417dad0> In : ha_flux.plot(title="H$\\alpha$ line total flux (%s)"%ha_flux.unit, colorbar='v') Out: <matplotlib.image.AxesImage at 0x7f42de3d02d0> In : ha_ew.mask_selection(np.where((ima1.data)<4000)) In : ha_ew.unit = ha_flux.unit / cont_flux.unit In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42e4218550> In : ha_ew.plot(title="H$\\alpha$ line ew (%s)"%ha_ew.unit, colorbar='v') Out: <matplotlib.image.AxesImage at 0x7f42de2a6550>
Loops over all images¶
In this section, we demonstrate ways to perform of procedure iteratively to each image of a cube. We consider the datacube as a collection of monochromatic images and we process each of them. For each monochromatic image we apply a convolution by a gaussian kernel.
# Data and variance arrays read from the file (extension DATA and STAT) In : cube = Cube('obj/Central_Datacube_bkg.fits')
First, we use the image iterator
iter_ima, which operates
similarly to the spectrum iterator described earlier on this page, except that
it returns successive images instead of successive spectra:
In : from mpdaf.obj import iter_ima In : cube2 = cube.clone(data_init=np.empty, var_init=np.empty) In : for ima,k in iter_ima(cube, index=True): ....: cube2[k,:,:] = ima.gaussian_filter(sigma=3) ....:
We can also make use of multiple processors to do the same thing faster, by
method. This applies a specified procedure to all images within a cube and
returns a new cube of the processed images:
In : from mpdaf.obj import Image In : cube2 = cube.loop_ima_multiprocessing(f=Image.gaussian_filter, sigma=3) [INFO] loop_ima_multiprocessing (gaussian_filter): 10 tasks
We then plot the results:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42dff0b690> In : cube.sum(axis=0).plot(title='before Gaussian filter') Out: <matplotlib.image.AxesImage at 0x7f42de00c0d0> In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42de00cad0> In : cube2.sum(axis=0).plot(title='after Gaussian filter') Out: <matplotlib.image.AxesImage at 0x7f42ddf279d0>
Next we will use the
loop_ima_multiprocessing method to fit and remove a background
gradient from a simulated datacube. For each image of the cube, we fit a 2nd
order polynomial to the background values (selected here by simply applying a
flux threshold to mask all bright objects). We do so by doing a chi^2
minimization over the polynomial coefficients using the numpy recipe
np.linalg.lstsq(). For this, we define a function that takes an image as its
sole parameter and returns a background-subtracted image:
In : def remove_background_gradient(ima): ....: ksel = np.where(ima.data.data<2.5) ....: pval = ksel ....: qval = ksel ....: zval = ima.data.data[ksel] ....: degree = 2 ....: Ap = np.vander(pval,degree) ....: Aq = np.vander(qval,degree) ....: A = np.hstack((Ap,Aq)) ....: (coeffs,residuals,rank,sing_vals) = np.linalg.lstsq(A,zval) ....: fp = np.poly1d(coeffs[0:degree]) ....: fq = np.poly1d(coeffs[degree:2*degree]) ....: X,Y = np.meshgrid(list(range(ima.shape)), list(range(ima.shape))) ....: ima2 = ima - np.array(list(map(lambda q,p: fp(p)+fq(q),Y,X))) ....: return ima2 ....:
We can then create the background-subtracted cube:
In : cube2 = cube.loop_ima_multiprocessing(f=remove_background_gradient) [INFO] loop_ima_multiprocessing (<function remove_background_gradient at 0x7f42dded4c80>): 10 tasks
Finally, we compare the results for one of the slices:
In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42ddea9b50> In : cube[5,:,:].plot(vmin=-1, vmax=4) Out: <matplotlib.image.AxesImage at 0x7f42dde7ce10> In : plt.figure() Out: <matplotlib.figure.Figure at 0x7f42de1f3c90> In : cube2[5,:,:].plot(vmin=-1, vmax=4) Out: <matplotlib.image.AxesImage at 0x7f42ddd4d850>
To be written.
select_lambda returns the sub-cube corresponding to a wavelength range.
get_image extracts an image around a position in the datacube.
bandpass_image sums the images
of a cube after multiplying the cube by the spectral bandpass curve of another instrument.
subcube extracts a sub-cube around a position.
subcube_circle_aperture extracts a sub-cube from an circle aperture of fixed radius.