Tutorial with APOGEE DR10 Spectra

In this example, we’re going to use spectra and labels from the 10th APOGEE data release (DR10). You will need to download the file example_DR10.tar.gz by clicking here and unzip it using the command

$ tar -zxvf example_DR10.tar.gz

Inside the folder example_DR10 you will see the following:

  • Data: a folder with .fits data files (spectra)
  • reference_labels.csv: a table with reference label values to use in the training step

Data-Munging

Before the data can be run through The Cannon, it must be prepared according to the specifications laid out in the “Requirements for Input” section. One of the requirements is for data to be normalized in a SNR-independent way. TheCannon package does have built-in options for performing the normalization, and we illustrate that here.

For your own project, you would write your own code to read in the data. For this tutorial, we provide the module apogee.py so that you don’t have to figure out how to access all of the relevant information from the .fits files.

First, import the necessary Python packages:

>>> import numpy as np
>>> import matplotlib.pyplot as plt

Now, get the data:

>>> from TheCannon import apogee
>>> tr_ID, wl, tr_flux, tr_ivar = apogee.load_spectra("example_DR10/Data")
>>> tr_label = apogee.load_labels("example_DR10/reference_labels.csv")

Print each of the arrays to inspect the contents. tr_ID is an array of IDs (unique identifiers) for all of the stars. wl is the array of wavelength values. tr_flux is the 2-D array of flux values (spectra for all the objects). tr_ivar is the inverse variances corresponding to tr_flux. tr_label has all of the reference labels for the training step.

Check to make sure that there are 548 spectra with 8575 pixels each, and 3 labels:

>>> print(tr_ID.shape)
>>> print(wl.shape)
>>> print(tr_flux.shape)
>>> print(tr_label.shape)

Now, plot the spectrum for the object with ID ‘2M21332216-0048247’:

>>> index = np.where(tr_ID=='2M21332216-0048247')[0][0]
>>> flux = tr_flux[index]
>>> plt.plot(wl, flux, c='k')
>>> plt.show()

Clearly, some of the pixels have bad values: the flux is 0. Those should have zero inverse variances. Let’s plot the spectrum only with pixels that are good (that is, inverse variance > 0):

>>> ivar = tr_ivar[index]
>>> choose = ivar > 0
>>> plt.plot(wl[choose], flux[choose], c='k')
>>> plt.show()

That’s better. (Note that the spectrum is flat because APOGEE spectra are already continuum normalized.)

To keep things simple in this exercise, we’re going to fit a model using the training objects and then use that model to re-determine the labels for those very same objects. That is, our test objects are identical to our training objects. In practice, you would almost never do this – your test set would be different from your training set.

>>> test_ID = tr_ID
>>> test_flux = tr_flux
>>> test_ivar = tr_ivar

Now that we have all six numpy arrays that we need, we initialize a Dataset object:

>>> from TheCannon import dataset
>>> ds = dataset.Dataset(wl, tr_ID, tr_flux, tr_ivar, tr_label, test_ID, test_flux, test_ivar)

You can access the arrays through this object:

>>> print(ds.tr_ID)
>>> print(ds.tr_flux)
>>> print(ds.wl)

and so on.

Next: TheCannon has a number of optional diagnostic plots built-in, to help the user visualize the results. Some of these plots require knowing the names of the labels. If the user wants to produce these diagnostic plots, the label names must be specified (LaTeX format works):

>>> ds.set_label_names(['T_{eff}', '\log g', '[Fe/H]'])

At this stage, two diagnotic plots can already be produced, one with the distribution of SNR in the training and test set (they will be identical in this case) and the other using triangle.py to plot the training label values against each other.

>>> fig = ds.diagnostics_SNR()
_images/SNRdist.png

We can also plot the reference labels against each other, to understand the phase space we are dealing with. This plot shows both the distribution of each individual label, as well as its correlations with other labels.

>>> fig = ds.diagnostics_ref_labels()
_images/ref_labels_triangle.png

Next, we normalize the spectra (APOGEE spectra are continuum normalized but not quite in the way The Cannon needs them to be. What you do at this stage will depend on your own unique dataset.)

First, continuum pixels are identified from a pseudo-continuum normalized version of the training set spectra. Pseudo-continuum normalization is performed using a running quantile. In this case, the window size for calculating the median is set to 50 Angstroms and the quantile level is set to 90%. APOGEE spectra come in three chunks, and we want to perform continuum normalization for each chunk separately. For TheCannon to treat spectra in chunks, the ranges attribute must be set:

>>> ds.ranges = [[371,3192], [3697,5997], [6461,8255]]

Even if a spectral dataset do not consist of chunks separated by gaps, one can imagine other reasons for wanting to treat a spectrum as though it had gaps: for example, if different regions of a spectrum behave very differently, it might be sensible to treat each of them separately in continuum normalization. The user should make sure to examine the results of continuum normalization, for example plotting fifty sample continuum fits and continuum normalized spectra.

Pseudo continuum normalization can then be performed as follows:

>>> pseudo_tr_flux, pseudo_tr_ivar = ds.continuum_normalize_training_q(q=0.90, delta_lambda=50)

This can take a while. Once the pseudo continuum has been calculated, a continuum mask is created: True values correspond to pixels that are continuum, False to those that are not. “True” continuum pixels are identified using a median and variance flux cut across the training objects: in other words, continuum pixels are those that consistently have values close to 1 in all of the training spectra. The user specifies what fraction of pixels to identify as continuum, and the flux and variance cuts are determined appropriately. If the ds.ranges attribute is set, then continuum pixels are identified separately for each region (in this case, three regions). This enables the user to control how evenly spread the pixels are.

In this case, we choose 7% of the pixels in the spectrum as continuum, but the best value should be determined through experimentation.

>>> contmask = ds.make_contmask(pseudo_tr_flux, pseudo_tr_ivar, frac=0.07)

At this stage, the user should plot spectra overlaid with the identified continuum pixels to ensure that they look reasonable and that they roughly evenly cover the spectrum. Large gaps in continuum pixels could result in poor continuum normalization in those regions. If the continuum pixels do not look evenly sampled enough, the range can be changed and the process repeated. For this example, we change it as follows:

>>> ds.ranges = [[371,3192], [3697,5500], [5500,5997], [6461,8255]]
>>> contmask = ds.make_contmask(pseudo_tr_flux, pseudo_tr_ivar, frac=0.07)

Once a satisfactory set of continuum pixels has been identified, the dataset’s continuum mask attribute is set as follows:

>>> ds.set_continuum(contmask)

Once the dataset has a continuum mask, the continuum is fit for using either a sinusoid or chebyshev function. In this case, we use a sinusoid; the user can specify the desired order. Again, this is 3 for this simple illustration, but should be determined through experimentation.

>>> cont = ds.fit_continuum(3, "sinusoid")

Once a satisfactory continuum has been fit, the normalized training and test spectra can be calculated:

>>> norm_tr_flux, norm_tr_ivar, norm_test_flux, norm_test_ivar = ds.continuum_normalize(cont)

You can plot a normalized spectrum as follows:

>>> plt.plot(wl, norm_tr_flux[10,:])

Take a look at a few of them. If these normalized spectra look acceptable, then they can replace the non-normalized spectra:

>>> ds.tr_flux = norm_tr_flux
>>> ds.tr_ivar = norm_tr_ivar
>>> ds.test_flux = norm_test_flux
>>> ds.test_ivar = norm_test_ivar

Now, the data munging is over and we’re ready to run TheCannon! Note that the steps above all depend on your particular dataset, and your particular science goals. The steps below are the real core of TheCannon.

Running The Cannon

For the training step (fitting for the spectral model) all the user needs to specify is the desired polynomial order of the spectral model. In this case, we use a quadratic model: order = 2

>>> from TheCannon import model
>>> md = model.CannonModel(2, useErrors=False)
>>> md.fit(ds)

At this stage, more optional diagnostic plots can be produced to examine the spectral model:

>>> md.diagnostics_contpix(ds)
>>> md.diagnostics_leading_coeffs(ds)
>>> md.diagnostics_plot_chisq(ds)

The first is a series of plots showing the full baseline (first-order) model spectrum with continuum pixels overplotted.

_images/contpix.gif

The second is a plot of the leading coefficients and scatter of the model as a function of wavelength

_images/leading_coeffs.png

The third is a histogram of the reduced chi squareds of the model fit.

_images/modelfit_chisqs.png

If the model fitting worked, then we can proceed to the test step. This command automatically updates the dataset with the fitted-for test labels, and returns the corresponding covariance matrix.

>>> label_errs = md.infer_labels(ds)

You can access the new labels as follows:

>>> test_labels = ds.test_label_vals

A set of diagnostic output:

>>> ds.diagnostics_test_step_flagstars()
>>> ds.diagnostics_survey_labels()

The first generates one text file for each label, called flagged_stars.txt. The second generates a triangle plot of the survey (Cannon) labels, shown below.

_images/survey_labels_triangle.png

If the test set is simply equivalent to the training set, as in this example, then one final diagnostic plot can be produced:

>>> ds.diagnostics_1to1()
_images/1to1_label_0.png _images/1to1_label_1.png _images/1to1_label_2.png