Introduction to DESI Spectra

The goal of this notebook is to demonstrate how to read in and manipulate DESI spectra using simulated spectra created as part of a DESI Data Challenge.

If you identify any errors or have requests for additional functionality please create a new issue on https://github.com/desihub/desispec/issues or send a note to desi-data@desi.lbl.gov.

Getting started

We'll assume that you've gone through the installation instructions at:

https://desi.lbl.gov/trac/wiki/Pipeline/GettingStarted/Laptop/JuneMeeting

as far as Try one of our tutorials.

We'll also assume that you've grabbed the DESI data team's example spectral files and put them in the directory

$DESI_ROOT/spectro/redux/dc17a2

If you don't have these yet, a thumb drive is being passed around, but they're also at NERSC in the directory

/scratch2/scratchdirs/sjbailey/desi/dc17a/spectro/redux/dc17a2

If you've checked out desispec from git, this jupyter notebook lives in desispec/doc/nb and you can execute it as jupyter notebook Intro_to_DESI_spectra.ipynb

Now, let's import all the modules we'll need:


In [1]:
import os
import numpy as np
import healpy as hp
from glob import glob
import fitsio
from collections import defaultdict

from desitarget import desi_mask

import matplotlib.pyplot as plt
%pylab inline


Populating the interactive namespace from numpy and matplotlib

If any of these imports fail, you should go back through the installation instructions as far as Try one of our tutorials.

Environment variables and data

Unless your ahead of the game, you probably haven't told your OS where to find the DESI data team's example spectra. So, let's set some environment variables to indicate that (assuming the bash shell):


In [2]:
%set_env DESI_SPECTRO_REDUX=/Users/dummy/desi/spectro/redux/
%set_env SPECPROD=dc17a2


env: DESI_SPECTRO_REDUX=/Users/dummy/desi/spectro/redux/
env: SPECPROD=dc17a2

In [3]:
import os
def check_env():
    for env in ('DESI_SPECTRO_REDUX', 'SPECPROD'):
        if env in os.environ:
            print('{} environment set to {}'.format(env, os.getenv(env)))
        else:
            print('Required environment variable {} not set!'.format(env))

In [4]:
check_env()


DESI_SPECTRO_REDUX environment set to /Users/dummy/desi/spectro/redux/
SPECPROD environment set to dc17a2

The Data Model for the spectra

First, let's try to understand the data model for the spectra:


In [5]:
basedir = os.path.join(os.getenv("DESI_SPECTRO_REDUX"),os.getenv("SPECPROD"),"spectra-64")
subdir = os.listdir(basedir)
print(basedir)
print(subdir)


/Users/dummy/desi/spectro/redux/dc17a2/spectra-64
['168', '172']

In [6]:
basedir = os.path.join(basedir,subdir[0])
subdir = os.listdir(basedir)
pixnums = np.array([int(pixnum) for pixnum in subdir])
print(basedir)
print(subdir)


/Users/dummy/desi/spectro/redux/dc17a2/spectra-64/168
['16879', '16890', '16891', '16894']

In [7]:
basedir = os.path.join(basedir,subdir[0])
subdir = os.listdir(basedir)
print(basedir)
print(subdir)


/Users/dummy/desi/spectro/redux/dc17a2/spectra-64/168/16879
['rr-64-16879.h5', 'spectra-64-16879.fits', 'zbest-64-16879.fits']

The directory structure is:

$DESI_SPECTRO_REDUX/$SPECTRO/spectra-{nside}/{group}/{pix}/*-{nside}-{pix}.fits`

where group = nside//100. For example for nside=64 and pixel=16879:

$DESI_SPECTRO_REDUX/$SPECTRO/spectra-64/168/16879/spectra-64-16879.fits
$DESI_SPECTRO_REDUX/$SPECTRO/spectra-64/168/16879/zbest-64-16879.fits

where the first file contains the spectra and the second file contains information on the best-fit redshifts from the redrock code.

In this directory-and-file structure, nside is the HEALPix resolution and pixel is the pixel number at that resolution. If you aren't familiar with HEALPix, it is an equal-area splitting of the sphere, where the sphere is initially divided into 12 equal-area pixels, and then each of those pixels is divided into 4 new equal-area pixels as nside increases (a quad tree). Schematically, here's how nside corresponds to pixel area in degrees:


In [8]:
sphere_area = 4*180.*180./np.pi
hpx_area = sphere_area
for i in range(10):
    nside = 2**i
    if i == 0:
        hpx_area/=12.
    else:
        hpx_area/=4.
    print(nside,hpx_area)


1 3437.746770784939
2 859.4366926962348
4 214.8591731740587
8 53.714793293514674
16 13.428698323378669
32 3.357174580844667
64 0.8392936452111668
128 0.2098234113027917
256 0.052455852825697924
512 0.013113963206424481

The nside at which the example spectra are grouped therefore corresponds to ~0.84 sq. deg. Note that I could have checked this more easily (but less pedagogically) using the useful python HEALPix library:


In [9]:
hp.nside2pixarea(64,degrees=True)


Out[9]:
0.83929364521116678

The spectra are stored in this fashion so that they are grouped (roughly) contiguously on the sky, with a reasonable number of spectra in each directory. It's easy to derive the approximate RA/Dec near each pixel number (note that we sneakily stored the pixel numbers as pixnums when we were examining the directory structure):


In [10]:
ras, decs = hp.pix2ang(64, pixnums, nest=True, lonlat=True)

Note that the DESI Data Model will always use the NESTED scheme for HEALPix.


In [11]:
zipper = zip(pixnums,ras,decs)
[print("Pixel(nside=64): {} RA: {} DEC: {}".format(pix,ra,dec)) for pix,ra,dec in zipper]


Pixel(nside=64): 16879 RA: 8.4375 DEC: -12.635625093021119
Pixel(nside=64): 16890 RA: 9.140625 DEC: -12.024699180565818
Pixel(nside=64): 16891 RA: 9.84375 DEC: -11.4151577427305
Pixel(nside=64): 16894 RA: 10.546875 DEC: -10.806922874860348
Out[11]:
[None, None, None, None]

What about the Data Model for the spectra themselves? Let's take a look (remember that we built a directory containing a spectrum as basedir above):


In [12]:
specfilename = glob(os.path.join(basedir,'spectra*fits'))[0]
DM = fitsio.FITS(specfilename)
DM


Out[12]:
  file: /Users/dummy/desi/spectro/redux/dc17a2/spectra-64/168/16879/spectra-64-16879.fits
  mode: READONLY
  extnum hdutype         hduname[v]
  0      IMAGE_HDU       
  1      BINARY_TBL      FIBERMAP
  2      IMAGE_HDU       B_WAVELENGTH
  3      IMAGE_HDU       B_FLUX
  4      IMAGE_HDU       B_IVAR
  5      IMAGE_HDU       B_MASK
  6      IMAGE_HDU       B_RESOLUTION
  7      IMAGE_HDU       R_WAVELENGTH
  8      IMAGE_HDU       R_FLUX
  9      IMAGE_HDU       R_IVAR
  10     IMAGE_HDU       R_MASK
  11     IMAGE_HDU       R_RESOLUTION
  12     IMAGE_HDU       Z_WAVELENGTH
  13     IMAGE_HDU       Z_FLUX
  14     IMAGE_HDU       Z_IVAR
  15     IMAGE_HDU       Z_MASK
  16     IMAGE_HDU       Z_RESOLUTION

The first extension FIBERMAP stores the mapping of the imaging information used to target and place a fiber on the source:


In [13]:
fm = fitsio.read(specfilename,1)
fm.dtype.descr


Out[13]:
[('OBJTYPE', '|S10'),
 ('TARGETCAT', '|S20'),
 ('BRICKNAME', '|S8'),
 ('TARGETID', '>i8'),
 ('DESI_TARGET', '>i8'),
 ('BGS_TARGET', '>i8'),
 ('MWS_TARGET', '>i8'),
 ('MAG', '>f4', (5,)),
 ('FILTER', '|S10', (5,)),
 ('SPECTROID', '>i4'),
 ('POSITIONER', '>i4'),
 ('LOCATION', '>i4'),
 ('DEVICE_LOC', '>i4'),
 ('PETAL_LOC', '>i4'),
 ('FIBER', '>i4'),
 ('LAMBDAREF', '>f4'),
 ('RA_TARGET', '>f8'),
 ('DEC_TARGET', '>f8'),
 ('RA_OBS', '>f8'),
 ('DEC_OBS', '>f8'),
 ('X_TARGET', '>f8'),
 ('Y_TARGET', '>f8'),
 ('X_FVCOBS', '>f8'),
 ('Y_FVCOBS', '>f8'),
 ('Y_FVCERR', '>f4'),
 ('X_FVCERR', '>f4'),
 ('NIGHT', '>i4'),
 ('EXPID', '>i4')]

TARGETID is the unique mapping from target information to a fiber. So, if you wanted to look up full imaging information for a spectrum, you can map back to target files using TARGETID.

Just out of interest, are the RAs and Decs of these objects in the expected HEALPix pixel?


In [14]:
pixnums = hp.ang2pix(64, fm["RA_TARGET"], fm["DEC_TARGET"], nest=True, lonlat=True)
print(np.min(pixnums),np.max(pixnums))
print(specfilename)


16879 16879
/Users/dummy/desi/spectro/redux/dc17a2/spectra-64/168/16879/spectra-64-16879.fits
I wonder what (roughly) the entirety of this pixel looks like, as mapped out by sources with spectra:

In [15]:
plt.plot(fm["RA_TARGET"],fm["DEC_TARGET"],'b.')


Out[15]:
[<matplotlib.lines.Line2D at 0x10e0059e8>]

In [16]:
DM


Out[16]:
  file: /Users/dummy/desi/spectro/redux/dc17a2/spectra-64/168/16879/spectra-64-16879.fits
  mode: READONLY
  extnum hdutype         hduname[v]
  0      IMAGE_HDU       
  1      BINARY_TBL      FIBERMAP
  2      IMAGE_HDU       B_WAVELENGTH
  3      IMAGE_HDU       B_FLUX
  4      IMAGE_HDU       B_IVAR
  5      IMAGE_HDU       B_MASK
  6      IMAGE_HDU       B_RESOLUTION
  7      IMAGE_HDU       R_WAVELENGTH
  8      IMAGE_HDU       R_FLUX
  9      IMAGE_HDU       R_IVAR
  10     IMAGE_HDU       R_MASK
  11     IMAGE_HDU       R_RESOLUTION
  12     IMAGE_HDU       Z_WAVELENGTH
  13     IMAGE_HDU       Z_FLUX
  14     IMAGE_HDU       Z_IVAR
  15     IMAGE_HDU       Z_MASK
  16     IMAGE_HDU       Z_RESOLUTION

The remaining extensions store the wavelength, flux, inverse variance on the flux, mask and resolution matrix for the B, R and Z arms of the spectrograph. Let's determine the wavelength coverage of each spectrograph:


In [17]:
bwave = fitsio.read(specfilename,"B_WAVELENGTH")
rwave = fitsio.read(specfilename,"R_WAVELENGTH")
zwave = fitsio.read(specfilename,"Z_WAVELENGTH")
print("B coverage: {:.1f} to {:.1f} Angstroms".format(np.min(bwave),np.max(bwave)))
print("R coverage: {:.1f} to {:.1f} Angstroms".format(np.min(rwave),np.max(rwave)))
print("Z coverage: {:.1f} to {:.1f} Angstroms".format(np.min(zwave),np.max(zwave)))


B coverage: 3569.4 to 5948.4 Angstroms
R coverage: 5625.4 to 7740.4 Angstroms
Z coverage: 7435.4 to 9833.4 Angstroms

Reading in and Displaying spectra

Now that we understand the Data Model, let's plot some spectra. To start, let's use the file we've already been manipulating and read in the flux to go with the wavelengths we already have.


In [18]:
wave = np.hstack([bwave,rwave,zwave])

In [19]:
bflux = fitsio.read(specfilename,"B_FLUX")
rflux = fitsio.read(specfilename,"R_FLUX")
zflux = fitsio.read(specfilename,"Z_FLUX")
flux = np.hstack([bflux,rflux,zflux])

Note that the wavelength arrays are 1-D (every spectrum in the spectral file is mapped to the same binning in wavelength) but the flux array (and flux_ivar, mask etc. arrays) are 2-D, because they contain multiple spectra:


In [20]:
print(wave.shape)
print(flux.shape)


(6895,)
(1339, 6895)

Let's plot the zeroth spectrum in this file (i.e. in this HEALPix grouping):


In [21]:
spectrum = 0
plt.plot(wave,flux[spectrum])


Out[21]:
[<matplotlib.lines.Line2D at 0x10e084ac8>]

Let's plot it again, but color-coding for the arms of the spectrograph:


In [22]:
plt.plot(bwave,bflux[spectrum],color='b')
plt.plot(rwave,rflux[spectrum],color='r')
plt.plot(zwave,zflux[spectrum],color='m')


Out[22]:
[<matplotlib.lines.Line2D at 0x112808668>]

Target classes

What about if we only want to plot spectra of certain target classes? The targeting information is stored in the DESI_TARGET, BGS_TARGET and MWS_TARGET entries of the fibermap array:


In [23]:
fm.dtype.descr


Out[23]:
[('OBJTYPE', '|S10'),
 ('TARGETCAT', '|S20'),
 ('BRICKNAME', '|S8'),
 ('TARGETID', '>i8'),
 ('DESI_TARGET', '>i8'),
 ('BGS_TARGET', '>i8'),
 ('MWS_TARGET', '>i8'),
 ('MAG', '>f4', (5,)),
 ('FILTER', '|S10', (5,)),
 ('SPECTROID', '>i4'),
 ('POSITIONER', '>i4'),
 ('LOCATION', '>i4'),
 ('DEVICE_LOC', '>i4'),
 ('PETAL_LOC', '>i4'),
 ('FIBER', '>i4'),
 ('LAMBDAREF', '>f4'),
 ('RA_TARGET', '>f8'),
 ('DEC_TARGET', '>f8'),
 ('RA_OBS', '>f8'),
 ('DEC_OBS', '>f8'),
 ('X_TARGET', '>f8'),
 ('Y_TARGET', '>f8'),
 ('X_FVCOBS', '>f8'),
 ('Y_FVCOBS', '>f8'),
 ('Y_FVCERR', '>f4'),
 ('X_FVCERR', '>f4'),
 ('NIGHT', '>i4'),
 ('EXPID', '>i4')]

and which target corresponds to which targeting bit is stored in the desitarget mask (we imported this near the beginning of the notebook.


In [24]:
desi_mask


Out[24]:
desi_mask:
  - [LRG,              0, "LRG", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3200, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3200, 'DONE': 2, 'OBS': 1, 'UNOBS': 3200}}]
  - [ELG,              1, "ELG", {'obsconditions': 'DARK|GRAY', 'priorities': {'MORE_ZWARN': 3000, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3000, 'DONE': 2, 'OBS': 1, 'UNOBS': 3000}}]
  - [QSO,              2, "QSO", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3400, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3500, 'DONE': 2, 'OBS': 1, 'UNOBS': 3400}}]
  - [LRG_NORTH,        8, "LRG from Bok/Mosaic data", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3200, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3200, 'DONE': 2, 'OBS': 1, 'UNOBS': 3200}}]
  - [ELG_NORTH,        9, "ELG from Bok/Mosaic data", {'obsconditions': 'DARK|GRAY', 'priorities': {'MORE_ZWARN': 3000, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3000, 'DONE': 2, 'OBS': 1, 'UNOBS': 3000}}]
  - [QSO_NORTH,       10, "QSO from Bok/Mosaic data", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3400, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3500, 'DONE': 2, 'OBS': 1, 'UNOBS': 3400}}]
  - [LRG_SOUTH,       16, "LRG from DECam data", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3200, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3200, 'DONE': 2, 'OBS': 1, 'UNOBS': 3200}}]
  - [ELG_SOUTH,       17, "ELG from DECam data", {'obsconditions': 'DARK|GRAY', 'priorities': {'MORE_ZWARN': 3000, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3000, 'DONE': 2, 'OBS': 1, 'UNOBS': 3000}}]
  - [QSO_SOUTH,       18, "QSO from DECam data", {'obsconditions': 'DARK', 'priorities': {'MORE_ZWARN': 3400, 'DONOTOBSERVE': 0, 'MORE_ZGOOD': 3500, 'DONE': 2, 'OBS': 1, 'UNOBS': 3400}}]
  - [SKY,             32, "Blank sky locations", {'obsconditions': 'DARK|GRAY|BRIGHT|POOR|TWILIGHT12|TWILIGHT18', 'priorities': {}}]
  - [STD_FSTAR,       33, "F-type standard stars", {'obsconditions': 'DARK|GRAY', 'priorities': {}}]
  - [STD_WD,          34, "White Dwarf stars", {'obsconditions': 'DARK|GRAY', 'priorities': {}}]
  - [STD_BRIGHT,      35, "F-type standard for BRIGHT conditions", {'obsconditions': 'BRIGHT', 'priorities': {}}]
  - [BADSKY,          36, "Blank sky locations that are imperfect but still useable", {'obsconditions': 'DARK|GRAY|BRIGHT|POOR|TWILIGHT12|TWILIGHT18', 'priorities': {'MORE_ZWARN': 0, 'MORE_ZGOOD': 0, 'DONE': 0, 'OBS': 0, 'UNOBS': 0, 'DONOTOBSERVE': 1}}]
  - [BRIGHT_OBJECT,   50, "Known bright object to avoid", {'obsconditions': 'APOCALYPSE', 'priorities': {}}]
  - [IN_BRIGHT_OBJECT, 51, "Too near a bright object; DO NOT OBSERVE", {'obsconditions': 'APOCALYPSE', 'priorities': {}}]
  - [NEAR_BRIGHT_OBJECT, 52, "Near a bright object but ok to observe", {'obsconditions': 'DARK|GRAY|BRIGHT|POOR|TWILIGHT12|TWILIGHT18', 'priorities': {}}]
  - [BGS_ANY,         60, "Any BGS bit is set", {'obsconditions': 'BRIGHT', 'priorities': {}}]
  - [MWS_ANY,         61, "Any MWS bit is set", {'obsconditions': 'BRIGHT', 'priorities': {}}]
  - [ANCILLARY_ANY,   62, "Any ancillary bit is set", {'obsconditions': 'DARK|GRAY|BRIGHT|POOR|TWILIGHT12|TWILIGHT18', 'priorities': {}}]

Let's find the indexes of all standard F-stars in the spectral file:


In [25]:
stds = np.where(fm["DESI_TARGET"] & desi_mask["STD_FSTAR"])[0]
print(stds)


[ 289  413  533  589  686  702  848 1049 1126 1181]

Where were these located on the original plate-fiber mapping?


In [26]:
plt.plot(fm["RA_TARGET"],fm["DEC_TARGET"],'b,')
plt.plot(fm["RA_TARGET"][stds],fm["DEC_TARGET"][stds],'kx')


Out[26]:
[<matplotlib.lines.Line2D at 0x1128705f8>]

It might seem strange that there are 10 standard stars, but only 4 crosses in the plot. This is because the density of standard stars is somewhat low in this data challenge, and so some standard stars are observed multiple times. These stars will have the same TARGETID, e.g.:


In [27]:
zipper = zip(stds,fm["TARGETID"][stds],fm["RA_TARGET"][stds],fm["DEC_TARGET"][stds])
[print("INDEX: {} TARGETID: {} RA: {} DEC: {}".format(i,tid,ra,dec)) for i,tid,ra,dec in zipper]


INDEX: 289 TARGETID: 5190613424285644324 RA: 8.01411247253418 DEC: -12.717392921447754
INDEX: 413 TARGETID: 4175555081061669163 RA: 8.394190788269043 DEC: -13.142195701599121
INDEX: 533 TARGETID: 3867431311113319136 RA: 8.09453296661377 DEC: -12.469233512878418
INDEX: 589 TARGETID: 5602376427145071993 RA: 7.988302707672119 DEC: -12.56336784362793
INDEX: 686 TARGETID: 5190613424285644324 RA: 8.01411247253418 DEC: -12.717392921447754
INDEX: 702 TARGETID: 5602376427145071993 RA: 7.988302707672119 DEC: -12.56336784362793
INDEX: 848 TARGETID: 3867431311113319136 RA: 8.09453296661377 DEC: -12.469233512878418
INDEX: 1049 TARGETID: 3867431311113319136 RA: 8.09453296661377 DEC: -12.469233512878418
INDEX: 1126 TARGETID: 5602376427145071993 RA: 7.988302707672119 DEC: -12.56336784362793
INDEX: 1181 TARGETID: 5190613424285644324 RA: 8.01411247253418 DEC: -12.717392921447754
Out[27]:
[None, None, None, None, None, None, None, None, None, None]

This will not be the case for the real survey (or for subsequent iterations of the Data Challenge)!

Let's take a look at the spectra of these standard stars:


In [28]:
for i in range(len(stds)):
    plt.plot(wave,flux[stds[i]],'g')
    plt.show()


These seem realistic. Let's zoom in on some of the Balmer series for the zeroth standard:


In [29]:
Balmer = [4102,4341,4861,6563]
halfwindow = 50
for i in range(len(Balmer)):
    plt.axis([Balmer[i]-halfwindow,Balmer[i]+halfwindow,0,np.max(flux[stds[0]])])
    plt.plot(wave,flux[stds[0]])
    plt.show()


Redshifts

The directory from which we took these spectra, also contains information on the best-fit redshifts for the spectra from the redrock code. Let's read that file and examine its contents:


In [30]:
zfilename = glob(os.path.join(basedir,'zbest*fits'))[0]
zs = fitsio.read(zfilename)
zs.dtype.descr


Out[30]:
[('CHI2', '>f8'),
 ('COEFF', '>f8', (10,)),
 ('Z', '>f8'),
 ('ZERR', '>f8'),
 ('ZWARN', '>i8'),
 ('SPECTYPE', '|S6'),
 ('SUBTYPE', '|S1'),
 ('TARGETID', '>i8'),
 ('DELTACHI2', '>f8'),
 ('BRICKNAME', '|S8')]

Note that there are a different number of entries in the redshift and spectral file, meaning that there isn't a row-by-row mapping between spectra and redshifts. Ultimately, this will be fixed so that there is a row-by-row mapping between these files.


In [31]:
print(zs.shape)
print(bflux.shape)


(1275,)
(1339, 2380)

But, the TARGETID (which is intended to be unique) is in this file, too, allowing sources to be uniquely mapped from targeting, to spectra, to redshift. Let's extract all sources that were targeted as quasars using the fibermap information from the spectral file, and plot the first 20:


In [32]:
qsos = np.where(fm["DESI_TARGET"] & desi_mask["QSO"])[0]
for i in range(20):
    plt.plot(wave,flux[qsos[i]],'b')
    plt.show()


Let's match these quasar targets to the redshift file on TARGETID to extract their best-fit redshifts from redrock:


In [33]:
dd = defaultdict(list)
for index, item in enumerate(zs["TARGETID"]):
    dd[item].append(index)
zqsos = [index for item in fm[qsos]["TARGETID"] for index in dd[item] if item in dd]

That might be hard to follow at first glance, but all I did was use some "standard" python syntax to match the indices in zs (the ordering of objects in the redrock redshift file) to those for quasars in fm (the ordering of quasars in the fibermap file), on the unique TARGETID, such that the indices stored in qsos for fm point to the corresponding indices in zqsos for zs. This might help illustrate the result:


In [34]:
zs[zqsos]["TARGETID"][0:7], fm[qsos]["TARGETID"][0:7]


Out[34]:
(array([6205855434767175128, 8658815612094195919, 8362360544984442534,
        2741684827213521854, 2504338256912405271, 2229181246596958769,
        3200382753559222849]),
 array([6205855434767175128, 8658815612094195919, 8362360544984442534,
        2741684827213521854, 2504338256912405271, 2229181246596958769,
        3200382753559222849]))

Let's see what best-fit template redrock assigned to each quasar. This information is stored in the SPECTYPE column.


In [35]:
zs[zqsos]["SPECTYPE"]


Out[35]:
array([b'QSO   ', b'GALAXY', b'QSO   ', b'QSO   ', b'QSO   ', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY',
       b'GALAXY', b'GALAXY', b'QSO   ', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'QSO   ', b'GALAXY', b'QSO   ', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'QSO   ',
       b'GALAXY', b'QSO   ', b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'QSO   ', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'QSO   ',
       b'GALAXY', b'QSO   ', b'GALAXY', b'QSO   ', b'QSO   ', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ',
       b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY', b'GALAXY',
       b'GALAXY', b'QSO   ', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY', b'GALAXY', b'QSO   ', b'GALAXY', b'GALAXY',
       b'GALAXY', b'GALAXY'],
      dtype='|S6')

Here, we've (partially) encountered a second minor bug in the Data Challenge: redrock is currently not doing a fantastic job of fitting quasars. It correctly identified some, though. And, of course, not everything targeted as a quasar will turn out to actually be a quasar (there are some contaminants).

If we'd instead performed this same check for the standard stars, everything would have looked more reasonable:


In [36]:
dd = defaultdict(list)
for index, item in enumerate(zs["TARGETID"]):
    dd[item].append(index)
zstds = [index for item in fm[stds]["TARGETID"] for index in dd[item] if item in dd]

For stars, we can also display the type of star that redrock fit (this is stored in the SUBTYPE column):


In [37]:
zipper = zip(zs[zstds]["SUBTYPE"],zs[zstds]["SPECTYPE"])
[ print("{}-{}".format(sub.decode('utf-8'),spec.decode('utf-8'))) for sub,spec in zipper ]


G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
G-STAR  
Out[37]:
[None, None, None, None, None, None, None, None, None, None]

(here the conversion to utf-8 is simply for display purposes because the strings in SUBTYPE and SPECTYPE are stored as bytes instead of unicode).

OK, back to our quasars. Let's plot the quasar targets that are identified as quasars , but add a label for the SPECTYPE and the redshift fit by redrock. I'll also over-plot some (approximate) typical quasar emission lines at the redrock redshift (if those lines would fall in the DESI wavelength coverage):


In [38]:
qsoid = np.where(zs[zqsos]["SPECTYPE"] == b'QSO   ')[0]
#ADM bug...to be uncommented later
#qsoid = np.where(zs[zqsos]["SPECTYPE"] == b'QSO')[0]
qsolines = np.array([1216,1546,1906,2800,4853,4960,5008])
for i in range(len(qsoid)):
    spectype = zs[zqsos[qsoid[i]]]["SPECTYPE"].decode('utf-8')
    z = zs[zqsos[qsoid[i]]]["Z"]
    plt.plot(wave,flux[qsos[qsoid[i]]],'b')
    plt.suptitle("{}, z={:.3f}".format(spectype,z))
    for line in qsolines:
        if ((1+z)*line > np.min(wave)) & ((1+z)*line < np.max(wave)):
            plt.plot([(1+z)*line,(1+z)*line],[np.min(flux[qsos[qsoid[i]]]),np.max(flux[qsos[qsoid[i]]])],'yo')
    plt.show()


Not bad at all!

A DESI-specific spectrum reader

Note that, for illustrative purposes, we discussed the Data Model in detail and read in the required files individually from that Data Model. But, the DESI data team has also developed standalone functions in desispec.io to facilitate reading in the plethora of information in the spectral files. For example:


In [39]:
from desispec import io

In [40]:
specobj = io.read_spectra(specfilename)

The wavelengths and flux in each band are then available as dictionaries in the wave and flux attributes:


In [41]:
specobj.wave


Out[41]:
{'b': array([ 3569.39990234,  3570.39990234,  3571.39990234, ...,  5946.39990234,
         5947.39990234,  5948.39990234]),
 'r': array([ 5625.39990234,  5626.39990234,  5627.39990234, ...,  7738.39990234,
         7739.39990234,  7740.39990234]),
 'z': array([ 7435.39990234,  7436.39990234,  7437.39990234, ...,  9831.40039062,
         9832.40039062,  9833.40039062])}

In [42]:
specobj.flux


Out[42]:
{'b': array([[ 1.29933512,  1.0022347 ,  2.32936954, ...,  1.5098393 ,
         -0.31815678, -0.68129355],
        [ 0.16481797,  2.58091903,  4.13131285, ...,  2.17221141,
         -0.15947284, -0.80120361],
        [-0.54065394,  0.97795463,  1.86999583, ...,  0.65832251,
          1.00964594, -1.06500113],
        ..., 
        [-0.74732822,  0.10489039,  1.5838306 , ..., -0.87190139,
         -1.05336905,  0.42119172],
        [-0.93642825, -1.43342793, -0.26964673, ..., -1.01840055,
          0.20170136,  1.01352084],
        [-1.39619613,  3.12872791, -1.46959674, ..., -1.49910605,
         -0.30297413, -0.25393745]]),
 'r': array([[ 0.28202024, -1.96098185,  6.19691467, ...,  1.26411927,
          0.93296349, -0.02513243],
        [-2.86186576, -2.00580311, -1.7183876 , ..., -0.51273662,
         -0.44679055,  0.93515539],
        [-1.1710645 , -1.11826515, -0.66710347, ..., -0.01358135,
         -0.70192206,  1.62991178],
        ..., 
        [ 0.13366452,  0.77143681, -0.97091359, ..., -0.558424  ,
          0.17781825, -0.0710273 ],
        [ 0.71818155, -1.10550964,  1.90226758, ...,  1.1554209 ,
         -0.80962598,  0.49902317],
        [-1.25144684, -2.10077286,  1.2620765 , ..., -0.43929133,
          0.05015403,  0.63459015]]),
 'z': array([[-1.0778302 , -0.35828328,  0.64282537, ...,  0.54353875,
          0.74365062,  0.30754244],
        [ 0.62096393,  0.43040532, -2.00396562, ...,  0.68161148,
          0.09568313,  0.48619619],
        [ 0.6857934 , -0.4443174 , -1.7674228 , ...,  0.30159402,
         -0.28547707, -0.05281865],
        ..., 
        [ 0.0767993 ,  0.69683415, -1.33168447, ...,  0.52512527,
          0.02879487,  0.02506786],
        [ 0.09414071, -0.68975151, -1.06091428, ..., -0.18909718,
          0.3186807 ,  0.18999858],
        [-0.60558528,  1.35346079, -0.75066864, ...,  0.23187622,
          0.15638578,  0.077624  ]])}

So, to plot the "zeroth" spectrum:


In [43]:
spectrum = 0
plt.plot(specobj.wave["b"],specobj.flux["b"][spectrum],color='b')
plt.plot(specobj.wave["r"],specobj.flux["r"][spectrum],color='r')
plt.plot(specobj.wave["z"],specobj.flux["z"][spectrum],color='m')


Out[43]:
[<matplotlib.lines.Line2D at 0x1130664a8>]

which should look very similar to one of the first plots we made earlier in the tutorial.

The fibermap information is available as a table in the fibermap attribute:


In [44]:
specobj.fibermap


Out[44]:
<Table length=1339>
OBJTYPETARGETCATBRICKNAMETARGETIDDESI_TARGETBGS_TARGETMWS_TARGETMAG [5]FILTER [5]SPECTROIDPOSITIONERLOCATIONDEVICE_LOCPETAL_LOCFIBERLAMBDAREFRA_TARGETDEC_TARGETRA_OBSDEC_OBSX_TARGETY_TARGETX_FVCOBSY_FVCOBSY_FVCERRX_FVCERRNIGHTEXPID
bytes10bytes20bytes8int64int64int64int64float32bytes10int32int32int32int32int32int32float32float64float64float64float64float64float64float64float64float32float32int32int32
SCIENCE0090m1256507767938450030088655370024.1158 .. 19.6316DECAM_G .. WISE_W211274127427416485400.09.04720687866-12.55666923529.04720687866-12.5566692352226.204589844-171.835830688226.204589844-171.8358306880.00.02019082917
SCIENCE0088m1257131316775476678757655370024.2044 .. 18.7743DECAM_G .. WISE_W211415141541518665400.08.84630966187-12.40653514868.84630966187-12.4065351486277.986328125-211.142669678277.986328125-211.1426696780.00.02019082917
SCIENCE0088m12568042175744721832301310740022.7718 .. 21.7344DECAM_G .. WISE_W211372137237218945400.08.91900730133-12.4500942238.91900730133-12.450094223259.144012451-199.534393311259.144012451-199.5343933110.00.02019082917
SCIENCE0085m13015440070444813270181310740021.2209 .. 21.3602DECAM_G .. WISE_W2224122412412210645400.08.59010314941-13.07083702098.59010314941-13.0708370209339.509246826-43.9885101318339.509246826-43.98851013180.00.02019082917
SCIENCE0085m1307392022591630237649655370023.7062 .. 19.4215DECAM_G .. WISE_W2223892389389210685400.08.62276554108-13.01253032688.62276554108-13.0125303268331.274200439-58.5953559875331.274200439-58.59535598750.00.02019082917
SCIENCE0085m1301028179647407169089655370024.0897 .. 18.8311DECAM_G .. WISE_W2223902390390210705400.08.62519454956-13.05545902258.62519454956-13.0554590225330.484649658-47.8234100342330.484649658-47.82341003420.00.02019082917
SCIENCE0085m13066596860549438321931310740023.7502 .. 21.2074DECAM_G .. WISE_W2223682368368210795400.08.65417861938-13.04528617868.65417861938-13.0452861786323.051086426-50.3499641418323.051086426-50.34996414180.00.02019082917
SCIENCE0085m127211424242957042065655370023.6676 .. 18.9103DECAM_G .. WISE_W2224672467467211005400.08.55505943298-12.64915943158.55505943298-12.6491594315351.152862549-150.762237549351.152862549-150.7622375490.00.02019082917
SCIENCE0085m1272712030920558351662655370023.6736 .. 19.7311DECAM_G .. WISE_W2224892489489211015400.08.49149131775-12.78542232518.49149131775-12.7854223251366.7996521-116.31211853366.7996521-116.312118530.00.02019082917
SCIENCE0088m12562607775202512174161310740023.7224 .. 21.4099DECAM_G .. WISE_W2222952295295211025400.08.82855224609-12.52950954448.82855224609-12.5295095444281.474914551-179.782089233281.474914551-179.7820892330.00.02019082917
....................................................................................
SCIENCE0090m12744781431856795498471310740023.227 .. 20.8968DECAM_G .. WISE_W299048904848949255400.08.98580360413-12.74888324748.98580360413-12.7488832474-94.3365783691-97.9452819824-94.3365783691-97.94528198240.00.020190920307
SCIENCE0088m12729789980218875574601310740022.5863 .. 20.658DECAM_G .. WISE_W299043904343949265400.08.8787355423-12.73950386058.8787355423-12.7395038605-68.7523956299-100.172584534-68.7523956299-100.1725845340.00.020190920307
SCIENCE0090m12766407117287073111001310740022.5913 .. 21.9608DECAM_G .. WISE_W299066906666949275400.08.97925662994-12.68320846568.97925662994-12.6832084656-92.8784332275-114.092185974-92.8784332275-114.0921859740.00.020190920307
SKY0088m1251504872841033291960429496729600inf .. infDECAM_G .. WISE_W2991109110110949315400.08.93171215057-12.48311614998.93171215057-12.4831161499-81.8575744629-163.562652588-81.8575744629-163.5626525880.00.020190920307
SCIENCE0090m12747117762394171932341310740023.0652 .. 21.1176DECAM_G .. WISE_W299056905656949355400.09.00696754456-12.71173381819.00696754456-12.7117338181-99.4716720581-107.097251892-99.4716720581-107.0972518920.00.020190920307
SCIENCE0088m1279110167029968842523932220021.8046 .. 20.4688DECAM_G .. WISE_W299058905858949395400.08.9362859726-12.69818401348.9362859726-12.6981840134-82.5622177124-110.366462708-82.5622177124-110.3664627080.00.020190920307
SCIENCE0088m12730854446689971069281310740022.6654 .. 21.6087DECAM_G .. WISE_W299042904242949405400.08.90709686279-12.76938915258.90709686279-12.7693891525-75.4878997803-92.8580551147-75.4878997803-92.85805511470.00.020190920307
SCIENCE0088m12752616648876076067051310740023.0291 .. 21.9501DECAM_G .. WISE_W299028902828949425400.08.89470767975-12.83722782148.89470767975-12.8372278214-72.4571380615-76.2376022339-72.4571380615-76.23760223390.00.020190920307
SCIENCE0088m12787130534662963692721310740023.0733 .. 22.2581DECAM_G .. WISE_W299035903535949455400.08.87013721466-12.77359199528.87013721466-12.7735919952-66.6628570557-91.8076019287-66.6628570557-91.80760192870.00.020190920307
SCIENCE0088m1275678748911797244221310740023.2312 .. 21.2028DECAM_G .. WISE_W299050905050949495400.08.91852283478-12.70898246778.91852283478-12.7089824677-78.2973251343-107.696624756-78.2973251343-107.6966247560.00.020190920307

and the TARGETID for each source is available via its own method:


In [45]:
specobj.target_ids()


Out[45]:
<Column name='TARGETID' dtype='int64' length=1275>
6507767938450030088
7131316775476678757
6804217574472183230
1544007044481327018
7392022591630237649
1028179647407169089
6659686054943832193
211424242957042065
2712030920558351662
6260777520251217416
6205855434767175128
8658815612094195919
...
5707221821373364828
215920369634544964
140315333989418501
4478143185679549847
2978998021887557460
6640711728707311100
1504872841033291960
4711776239417193234
3085444668997106928
5261664887607606705
8713053466296369272
567874891179724422

All of the information that could be read in from the different extensions of the spectral file can be retrieved from the specobj object. Here's what's available:


In [47]:
dir(specobj)


Out[47]:
['R',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_bands',
 '_ftype',
 '_single',
 'bands',
 'extra',
 'fibermap',
 'flux',
 'ftype',
 'ivar',
 'mask',
 'meta',
 'num_spectra',
 'num_targets',
 'resolution_data',
 'select',
 'target_ids',
 'update',
 'wave',
 'wavelength_grid']

In [ ]: