Exploring Stellar Spectra with SDSS SEGUE#


Learning Goals#

By the end of this tutorial, you will:

  • Understand how to use astroquery.mast to download data from the SDSS Legacy Spectra and SEGUE surveys

  • Plot a stellar spectrum and label absorption lines

  • Learn about OBAFGKM spectral classification and how to classify stars

Table of Contents#

Introduction#

The SDSS Legacy Spectra Survey is an optical-wavelength spectroscopic survey of millions of stars, galaxies, and quasars available through the SDSS Legacy Archive at MAST. This includes data from the original SDSS Legacy Spectra survey, the SDSS Supernova Survey, and the Sloan Extension for Galactic Understanding and Exploration (SEGUE), which targeted over 400,000 stars in the Milky Way with a goal of mapping out the galactic disk in abundance space. A summary of the SDSS data products available at MAST can be found in the SDSS Legacy Archive at MAST User Manual.

The primary data products from the SDSS Legacy Spectra survey and SEGUE extensions are optical-wavelength (380-920 nm) spectra with a resolution of (λ/δλ)~2000 obtained using the SDSS-I/-II Spectrograph on the SDSS-2.5m telescope (Gunn et al. 2006) at Apache Point Observatory. In this notebook, we will show how to download SEGUE data from MAST, plot a spectrum, and classify a star based on its spectral type!

In 1912, astronomer Annie Jump Cannon developed a method to classify stars based on the appearance of their spectrum, which is still used today. Stars are classified as O, B, A, F, G, K, or M based on the strength and location of absorption lines in their spectrum. [1] These classifications also correspond to stellar temperature: O-type stars are the hottest stars, with surface temperatures of >30,000 K, while M-type have the coldest temperatures of < 3000 K. Our Sun is a G-type star. In this notebook, we will explore different stellar types and what their spectra look like.

Imports#

The main packages and their use-cases in this tutorial are as follows:

  • numpy to handle array functions and some basic math

  • matplotlib for plotting data and creating figures

  • astropy.io for reading FITS files

  • astroquery.mast to search and download data from MAST

%matplotlib inline

import numpy as np

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

from astropy.io import fits
from astropy.table import Table

from astroquery.mast import Observations

If you’re not sure whether you have these packages installed on your device, you can uncomment and run the following cell to install the required versions:

# !pip install -r requirements.txt

This cell updates some of the settings in matplotlib to use larger font sizes in the figures:

# Update Plotting Parameters
params = {
    "axes.labelsize": 12,
    "xtick.labelsize": 12,
    "ytick.labelsize": 12,
    "text.usetex": False,
    "lines.linewidth": 1,
    "axes.titlesize": 18,
    "font.family": "serif",
    "font.size": 12,
}
plt.rcParams.update(params)

Accessing SDSS Legacy Spectra Data from MAST#

In this tutorial, we will be focusing on data from the Sloan Extension for Galactic Understanding and Exploration (SEGUE) to plot the spectra of different types of stars and learn about stellar classification. Accessing data from the SDSS Legacy Archive at MAST is easy using astroquery.MAST, a Python package module for querying the MAST archive.

Querying the SDSS Legacy Spectra Survey#

To search the SDSS Spectra Data available through MAST, we can use the Observations.query_criteria() function and search for the provenance name “SEGUE” to return all data from the SEGUE survey:

# Query for SDSS SEGUE Data
spectro_data = Observations.query_criteria(
    provenance_name="SEGUE", pagesize=10, page=1
)

# Display first 10 entries
spectro_data
Table masked=True length=10
intentTypeobs_collectionprovenance_nameinstrument_nameprojectfilterswavelength_regiontarget_nametarget_classificationobs_ids_ras_decdataproduct_typeproposal_picalib_levelt_mint_maxt_exptimeem_minem_maxobs_titlet_obs_releaseproposal_idproposal_typesequence_numbers_regionjpegURLdataURLdataRightsmtFlagsrcDenobsidobjID
str7str4str5str17str4str4str7str19str4str28float64float64str8str18int64float64float64float64float64float64str51float64str3str1int64str48str69str64str6boolfloat64str9str10
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 3292-54943-152STARsdss_spectro_3292-54943-0152261.3377935.212895spectrumSDSS Collaboration354941.4452199074154943.453900462966300.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data55572.0N/A----CIRCLE 261.33779 35.212895 0.0004166666666666667mast:SDSS/sdss/spectro/3292/54943/0152/spec-image-3292-54943-0152.pngmast:SDSS/sdss/spectro/3292/54943/0152/spec-3292-54943-0152.fitsPUBLICFalsenan3537207601012687679
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2541-54481-505STARsdss_spectro_2541-54481-0505130.7762483.816927spectrumSDSS Collaboration354481.3170601851854481.365474537043303.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 130.77624 83.816927 0.0004166666666666667mast:SDSS/sdss/spectro/2541/54481/0505/spec-image-2541-54481-0505.pngmast:SDSS/sdss/spectro/2541/54481/0505/spec-2541-54481-0505.fitsPUBLICFalsenan3536256561012687700
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 3292-54943-157STARsdss_spectro_3292-54943-0157261.2830435.0749spectrumSDSS Collaboration354941.4452199074154943.453900462966300.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data55572.0N/A----CIRCLE 261.28304 35.0749 0.0004166666666666667mast:SDSS/sdss/spectro/3292/54943/0157/spec-image-3292-54943-0157.pngmast:SDSS/sdss/spectro/3292/54943/0157/spec-3292-54943-0157.fitsPUBLICFalsenan3537207831012687727
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 3292-54943-159STARsdss_spectro_3292-54943-0159261.2116634.934269spectrumSDSS Collaboration354941.4452199074154943.453900462966300.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data55572.0N/A----CIRCLE 261.21166 34.934269 0.0004166666666666667mast:SDSS/sdss/spectro/3292/54943/0159/spec-image-3292-54943-0159.pngmast:SDSS/sdss/spectro/3292/54943/0159/spec-3292-54943-0159.fitsPUBLICFalsenan3537207911012687759
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2541-54481-515STARsdss_spectro_2541-54481-0515132.220183.828086spectrumSDSS Collaboration354481.3170601851854481.365474537043303.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 132.2201 83.828086 0.0004166666666666667mast:SDSS/sdss/spectro/2541/54481/0515/spec-image-2541-54481-0515.pngmast:SDSS/sdss/spectro/2541/54481/0515/spec-2541-54481-0515.fitsPUBLICFalsenan3536256991012687993
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2697-54389-49STARsdss_spectro_2697-54389-004957.6556959.13494spectrumSDSS Collaboration354389.3157638888954389.432581018528857.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 57.655695 9.13494 0.0004166666666666667mast:SDSS/sdss/spectro/2697/54389/0049/spec-image-2697-54389-0049.pngmast:SDSS/sdss/spectro/2697/54389/0049/spec-2697-54389-0049.fitsPUBLICFalsenan3536214631012688026
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2541-54481-521STARsdss_spectro_2541-54481-0521127.1583284.383137spectrumSDSS Collaboration354481.3170601851854481.365474537043303.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 127.15832 84.383137 0.0004166666666666667mast:SDSS/sdss/spectro/2541/54481/0521/spec-image-2541-54481-0521.pngmast:SDSS/sdss/spectro/2541/54481/0521/spec-2541-54481-0521.fitsPUBLICFalsenan3536257231012688037
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2697-54389-53STARsdss_spectro_2697-54389-005357.4803229.567454spectrumSDSS Collaboration354389.3157638888954389.432581018528857.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 57.480322 9.567454 0.0004166666666666667mast:SDSS/sdss/spectro/2697/54389/0053/spec-image-2697-54389-0053.pngmast:SDSS/sdss/spectro/2697/54389/0053/spec-2697-54389-0053.fitsPUBLICFalsenan3536214791012688069
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 3292-54943-173STARsdss_spectro_3292-54943-0173260.932735.158364spectrumSDSS Collaboration354941.4452199074154943.453900462966300.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data55572.0N/A----CIRCLE 260.9327 35.158364 0.0004166666666666667mast:SDSS/sdss/spectro/3292/54943/0173/spec-image-3292-54943-0173.pngmast:SDSS/sdss/spectro/3292/54943/0173/spec-3292-54943-0173.fitsPUBLICFalsenan3537208521012688070
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 2541-54481-528STARsdss_spectro_2541-54481-0528127.0908384.448984spectrumSDSS Collaboration354481.3170601851854481.365474537043303.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 127.09083 84.448984 0.0004166666666666667mast:SDSS/sdss/spectro/2541/54481/0528/spec-image-2541-54481-0528.pngmast:SDSS/sdss/spectro/2541/54481/0528/spec-2541-54481-0528.fitsPUBLICFalsenan3536257511012688094

The table above provides some basic information for each observation:

  • collection: Tells us the mission for this data, which is SDSS

  • s_ra and s_dec: Celestial coordinates right ascension and declination.

  • instrument_name: SDSS Spectrograph indicates that the data were collected using the SDSS Spectrograph instrument. This would be a good field to search on if you are interested in both the SEGUE survey and the original SDSS Legacy Spectra survey.

  • obs_id: Observation ID associated with the object. This is based on the plate number, the modified Julian date of observation, and the fiber ID, so the observation ID will look something like sdss_spectro_{PLATE}-{MJD}-{FIBERID}.

  • target_classification: Indicates the type of object (QSO, STAR, or GALAXY).

  • t_min and t_max: The modified Julian dates indicating the start and end times of the exposures.

  • wavelength_region: Indicates the region of the electromagnetic spectrum observed. In this case, OPTICAL, since SDSS observed in the optical wavelength range.

  • em_min and em_max: The minimum and maximum wavelengths observed by the survey. For SDSS, this range is 380-920.0 nanometers (optical).

Downloading Data Products#

Let’s narrow down our search by querying for the observation ID of obs_id='sdss_spectro_1660-53230-0023'. This star was selected for this tutorial because it is a G2 type main sequence star, very similar to the Sun.

# Search for sun-like star "sdss_spectro_1660-53230-0023"
spectro_data = Observations.query_criteria(
    provenance_name="SEGUE", obs_id="sdss_spectro_1660-53230-0023"
)

# Display result
spectro_data
Table masked=True length=1
intentTypeobs_collectionprovenance_nameinstrument_nameprojectfilterswavelength_regiontarget_nametarget_classificationobs_ids_ras_decdataproduct_typeproposal_picalib_levelt_mint_maxt_exptimeem_minem_maxobs_titlet_obs_releaseproposal_idproposal_typesequence_numbers_regionjpegURLdataURLdataRightsmtFlagsrcDenobsidobjID
str7str4str5str17str4str4str7str18str4str28float64float64str8str18int64float64float64float64float64float64str51float64str3str1int64str48str69str64str6boolfloat64str9str10
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 1660-53230-23STARsdss_spectro_1660-53230-0023308.8166976.546953spectrumSDSS Collaboration353228.3794675925953230.3502314814763000.3380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data54770.0N/A----CIRCLE 308.81669 76.546953 0.0004166666666666667mast:SDSS/sdss/spectro/1660/53230/0023/spec-image-1660-53230-0023.pngmast:SDSS/sdss/spectro/1660/53230/0023/spec-1660-53230-0023.fitsPUBLICFalsenan3550506601016516519

Using Observations.get_product_list(), we can get the list of files associated with this star that are available to download:

# View available products for first observation
products = Observations.get_product_list(spectro_data[0])
# List Products
products
Table masked=True length=6
obsIDobs_collectiondataproduct_typeobs_iddescriptiontypedataURIproductTypeproductGroupDescriptionproductSubGroupDescriptionproductDocumentationURLprojectprvversionproposal_idproductFilenamesizeparent_obsiddataRightscalib_levelfilters
str9str4str12str31str252str1str69str7str28str13str48str19str4str3str30int64str9str6int64str4
355050660SDSSspectrumsdss_spectro_1660-53230-0023Preview-FullSmast:SDSS/sdss/spectro/1660/53230/0023/spec-image-1660-53230-0023.pngPREVIEW------SEGUEdr7N/Aspec-image-1660-53230-0023.png33778355050660PUBLIC3None
355050660SDSSspectrumsdss_spectro_1660-53230-0023SDSS Legacy Spectra lite spectrum file containing the combined spectrum and associated metadata but not the individual exposures.Smast:SDSS/sdss/spectro/1660/53230/0023/spec-lite-1660-53230-0023.fitsSCIENCE--SPECTRAhttps://archive.stsci.edu/missions-and-data/sdssSEGUEdr7N/Aspec-lite-1660-53230-0023.fits172800355050660PUBLIC3None
355050660SDSSspectrumsdss_spectro_1660-53230-0023SDSS Legacy Spectra full spectrum file containing the combined spectrum, associated metadata, and the individual exposures.Smast:SDSS/sdss/spectro/1660/53230/0023/spec-1660-53230-0023.fitsSCIENCEMinimum Recommended ProductsSPECTRAhttps://archive.stsci.edu/missions-and-data/sdssSEGUEdr7N/Aspec-1660-53230-0023.fits604800355050660PUBLIC3None
355053097SDSSmeasurementssdss_spectro_spplate_1660-53230SDSS Legacy Spectra summary catalog, for each plate-MJD, containing the combined spectra and targeting information for all observations on a single plate.Dmast:SDSS/sdss/spectro/1660/53230/spPlate-1660-53230.fitsSCIENCE--SDSS Catalogshttps://archive.stsci.edu/missions-and-data/sdssSDSS Legacy SpectraDR17N/AspPlate-1660-53230.fits59708160355050660PUBLIC3--
355053113SDSSmeasurementssdss_spectro_spzall_1660-53230SDSS Legacy Spectra summary catalog, for each plate-MJD, containing the best fits for spectral redshift and classification measurements, rank-ordered by chi-squared. If the best fit looks bad from the SpecObj file, check the second best fit here.Dmast:SDSS/sdss/spectro/1660/53230/spZall-1660-53230.fitsSCIENCE--SDSS Catalogshttps://archive.stsci.edu/missions-and-data/sdssSDSS Legacy SpectraDR17N/AspZall-1660-53230.fits31164480355050660PUBLIC3--
355581403SDSSmeasurementssdss_spectro_specobjSDSS Legacy Spectra summary catalog. This provides collated summary tables on the observations and data processing from the SDSS Legacy Spectra survey, including targeting information, spectroscopic classifications, and redshifts for every observation.Dmast:SDSS/sdss/spectro/specObj-dr17.fitsSCIENCE--SDSS Catalogshttps://archive.stsci.edu/missions-and-data/sdssSDSS Legacy SpectraDR17N/AspecObj-dr17.fits6741023040355050660PUBLIC3--

For this star, we can see quite a few files available:

  • spec-image-1660-53230-0023.png: A png image with preview of each spectrum

  • spec-1660-53230-0023.fits: The “full” spectrum file, which contains the combined spectrum, associated metadata, and the individual exposures.

  • spec-lite-1660-53230-0023.fits: The “lite” version of the spectrum file, which contains the combined spectrum and associated metadata but not the individual exposures.

  • Two catalog files, spPlate-1660-53230.fits and spZall-1660-53230.fits, which contain observing information, measurements, and metadata for all spectra observed on the same plate.

More detail on the data products available for SEGUE can be found on the Legacy Spectra Data Products page of the Archive User Manual.

For this tutorial, we will only use the full spectrum file, which is the Minimum Recommended Product according to the productGroupDescription. Downloading this file is simple using Observations.download_products():

# Downloading spectrum files
manifest = Observations.download_products(
    products,  # Specify a product list to download
    flat=True,  # Download with a "flat" structure in the current directory
    mrp_only=True,  # Limit to Minimum Recommended Products = the full spectrum file
)
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:SDSS/sdss/spectro/1660/53230/0023/spec-1660-53230-0023.fits to ./spec-1660-53230-0023.fits ...
 [Done]

Plotting a Spectrum#

Now that we know how to download spectra from the SDSS Legacy Archive at MAST, let’s take a closer look at the data. We can print some basic information about the file we just downloaded by opening the data with astropy.io.fits and printing the file summary with .info()

# Opening file
spectrum_file = fits.open(manifest["Local Path"][0])

# Display file info
spectrum_file.info()
Filename: ./spec-1660-53230-0023.fits
No.    Name      Ver    Type      Cards   Dimensions   Format
  0  PRIMARY       1 PrimaryHDU     128   ()      
  1  COADD         1 BinTableHDU     26   3837R x 8C   [E, E, E, J, J, E, E, E]   
  2  SPECOBJ       1 BinTableHDU    262   1R x 126C   [6A, 4A, 16A, 23A, 16A, 8A, E, E, E, J, E, E, J, B, B, B, B, B, B, J, 22A, 19A, 19A, 22A, 19A, I, 3A, 3A, 1A, J, D, D, D, E, E, 19A, 8A, J, J, J, J, K, K, J, J, J, J, J, J, K, K, K, K, I, J, J, J, J, 5J, D, D, 6A, 21A, E, E, E, J, E, 24A, 10J, J, 10E, E, E, E, E, E, E, J, E, E, E, J, E, 5E, E, 10E, 10E, 10E, 5E, 5E, 5E, 5E, 5E, J, J, E, E, E, E, E, E, 25A, 21A, 10A, E, E, E, E, E, E, E, E, J, E, E, J, 1A, 1A, E, E, J, J, 1A, 5E, 5E]   
  3  SPZLINE       1 BinTableHDU     48   29R x 19C   [J, J, J, 13A, D, E, E, E, E, E, E, E, E, E, E, J, J, E, E]   
  4  B1-00027775-00027772-00027774    1 BinTableHDU    146   2044R x 7C   [E, E, E, J, E, E, E]   
  5  B1-00027776-00027772-00027774    1 BinTableHDU    146   2044R x 7C   [E, E, E, J, E, E, E]   
  6  B1-00027859-00027857-00027858    1 BinTableHDU    146   2044R x 7C   [E, E, E, J, E, E, E]   
  7  R1-00027775-00027772-00027774    1 BinTableHDU    146   2046R x 7C   [E, E, E, J, E, E, E]   
  8  R1-00027776-00027772-00027774    1 BinTableHDU    146   2047R x 7C   [E, E, E, J, E, E, E]   
  9  R1-00027859-00027857-00027858    1 BinTableHDU    146   2047R x 7C   [E, E, E, J, E, E, E]   

This file has various extensions, which are explained in the SDSS Data Model:

  • HDU 0: PRIMARY: The primary header information.

  • HDU 1: COADD: The coadded observed spectrum.

  • HDU 2: SPECOBJ: Metadata from the SPECOBJ table, which includes targeting information and spectroscopic classifications from the SDSS Spectra Data Reduction Pipeline.

  • HDU 3: SPZLINE: Measurements and line fitting outputs from the SDSS Spectra Data Analysis Pipeline, which measures chemical abundances and equivalent widths for absorption lines in the spectrum.

  • HDU 4+: Individual visit spectra (on red R1- and blue B1- ccd chips) that were included in the coadded spectrum.

Now, let’s plot the spectrum with matplotlib.pyplot. To do this, we’ll use the information in the first extension (COADD). This extension contains the wavelength and flux information we will need to make a plot.

# Initiate plot
plt.figure(figsize=(10, 4))

# Load in wavelength and flux
wavelength = 10 ** spectrum_file["COADD"].data["loglam"]
observed_flux = spectrum_file["COADD"].data["FLUX"]
model_flux = spectrum_file["COADD"].data["MODEL"]

# Plot Observed Spectrum
plt.plot(wavelength, observed_flux, c="gray", label="Observed Spectrum")
# Plot Best-fit Model Spectrum
plt.plot(wavelength, model_flux, c="k", label="Model Spectrum")

# Label axes
plt.xlabel(r"Wavelength ($\AA$)")
plt.ylabel("Flux")
plt.title(f"{spectrum_file.filename().strip('./')}")
plt.grid()
plt.legend()


# Set axes limits
plt.xlim(np.min(wavelength), np.max(wavelength))
plt.ylim(0, np.max(observed_flux))

# Show plot
plt.show()
../../../_images/c61d66101f131346f701cefcca3f0919af9d2c53bb5247ac9482e768e83bda58.png

Adding Labels for Absorption Lines#

That’s a good-looking spectrum! But as we saw earlier, the spectrum is not the only information in this file: The SDSS Data Reduction Pipeline and SEGUE Stellar Parameter Pipeline (SSPP) have made various measurements of this star, which are availble in the “SPECOBJ” and “SPZLINE” extensions of this file.

# Display table from the "SPECOBJ" extension
Table(spectrum_file["SPECOBJ"].data)
Table length=1
SURVEYINSTRUMENTCHUNKPROGRAMNAMEPLATERUNPLATEQUALITYPLATESN2DEREDSN2LAMBDA_EFFBLUEFIBERZOFFSETSNTURNOFFNTURNOFFSPECPRIMARYSPECLEGACYSPECSEGUESPECSEGUE1SPECSEGUE2SPECBOSSBOSS_SPECOBJ_IDSPECOBJIDFLUXOBJIDBESTOBJIDTARGETOBJIDPLATEIDNSPECOBSFIRSTRELEASERUN2DRUN1DDESIGNIDCXCYCZXFOCALYFOCALSOURCETYPETARGETTYPEPRIMTARGETSECTARGETLEGACY_TARGET1LEGACY_TARGET2SPECIAL_TARGET1SPECIAL_TARGET2SEGUE1_TARGET1SEGUE1_TARGET2SEGUE2_TARGET1SEGUE2_TARGET2MARVELS_TARGET1MARVELS_TARGET2BOSS_TARGET1BOSS_TARGET2ANCILLARY_TARGET1ANCILLARY_TARGET2SPECTROGRAPHIDPLATETILEMJDFIBERIDOBJIDPLUG_RAPLUG_DECCLASSSUBCLASSZZ_ERRRCHI2DOFRCHI2DIFFTFILETCOLUMNNPOLYTHETAVDISPVDISP_ERRVDISPZVDISPZ_ERRVDISPCHI2VDISPNPIXVDISPDOFWAVEMINWAVEMAXWCOVERAGEZWARNINGSN_MEDIAN_ALLSN_MEDIANCHI68PFRACNSIGMAFRACNSIGHIFRACNSIGLOSPECTROFLUXSPECTROFLUX_IVARSPECTROSYNFLUXSPECTROSYNFLUX_IVARSPECTROSKYFLUXANYANDMASKANYORMASKSPEC1_GSPEC1_RSPEC1_ISPEC2_GSPEC2_RSPEC2_IELODIE_FILENAMEELODIE_OBJECTELODIE_SPTYPEELODIE_BVELODIE_TEFFELODIE_LOGGELODIE_FEHELODIE_ZELODIE_Z_ERRELODIE_Z_MODELERRELODIE_RCHI2ELODIE_DOFZ_NOQSOZ_ERR_NOQSOZWARNING_NOQSOCLASS_NOQSOSUBCLASS_NOQSORCHI2DIFF_NOQSOZ_PERSONCLASS_PERSONZ_CONF_PERSONCOMMENTS_PERSONCALIBFLUXCALIBFLUX_IVAR
str6str4str16str23str16str8float32float32float32int32float32float32int32uint8uint8uint8uint8uint8uint8int32str22str19str19str22str19int16str3str3str1int32float64float64float64float32float32str19str8int32int32int32int32int64int64int32int32int32int32int32int32int64int64int64int64int16int32int32int32int32int32[5]float64float64str6str21float32float32float32int32float32str24int32[10]int32float32[10]float32float32float32float32float32float32int32float32float32float32int32float32float32[5]float32float32[10]float32[10]float32[10]float32[5]float32[5]float32[5]float32[5]float32[5]int32int32float32float32float32float32float32float32str25str21str10float32float32float32float32float32float32float32float32int32float32float32int32str1str1float32float32int32int32str1float32[5]float32[5]
segue1SDSSchunk83segtestdr2003.11.4good11.86040.05000.0-10.0-9999.0000000001869000221741049856123766326880003692912376632688000369291127679956510325018689938995491901442dr726-10.14583121053457282-0.181269292252751320.972560898720593263.0591-125.98856NONLEGACYSTANDARD03200000320000000011660949153230234144 .. 146308.8166976.546953STARG2-0.00040238781.0467806e-051.228262737511.3327496spEigenStar-54474.fits11 .. -1434.26533 .. 0.00.00.00.00.00.00.003808.90389212.9780.3761029.0356965.2309275 .. 22.1787151.04136480.33741027 .. 0.00132943360.18372773 .. 0.00132943360.15368253 .. 0.059.69679 .. 300.389740.575326 .. 0.2674023843.598114 .. 292.438351.7917618 .. 0.2930202517.428034 .. 67.4194615938355226207846414.42514.216711.860414.476919.539516.9398RAC/L/00661.fitsHD176303F8V0.4916107.04.22-0.09-0.000386210978.263349e-062.3138857e-060.8678152621270.00.000.00.00039.980957 .. 318.954681.192785 .. 0.028415708

This table contains a lot of useful information, including the coordinates (PLUG_RA and PLUG_DEC), the date of observation (MJD), the data release number (FIRSTRELEASE), and the signal-to-noise ratio (SN_MEDIAN_ALL) for this spectrum.

It also includes the spectrum’s classification (CLASS), which is typically either STAR, GALAXY, or QSO, and the SUBCLASS, which in this case, is the spectral OBAFGKM classification for this spectrum. The subclass tells us that this star is a “G2”-type star, the same stellar type as our Sun:

spectrum_file["SPECOBJ"].data["SUBCLASS"][0]
'G2'

The SPZLINE table in the file contains measurements of absorption lines in the spectrum:

# Display table from the "SPZLINE" extension
Table(spectrum_file["SPZLINE"].data)
Table length=29
PLATEMJDFIBERIDLINENAMELINEWAVELINEZLINEZ_ERRLINESIGMALINESIGMA_ERRLINEAREALINEAREA_ERRLINEEWLINEEW_ERRLINECONTLEVELLINECONTLEVEL_ERRLINENPIXLEFTLINENPIXRIGHTLINEDOFLINECHI2
int32int32int32str13float64float32float32float32float32float32float32float32float32float32float32int32int32float32float32
16605323023Ly_alpha1215.670.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023N_V 12401240.810.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023C_IV 15491549.480.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023He_II 16401640.420.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023C_III] 19081908.7340.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023Mg_II 27992800.31518365497280.0-1.00.0-1.00.0-1.00.0-1.00.0-1.0000.0-1.0
16605323023[O_II] 37253727.091726797987-0.00031311090.0001491723211562.1045629.53759906.212488720.40.0-1.0-8.457133-1.00407405.9039444.59473
16605323023[O_II] 37273729.875447997208-0.000313111870.0001491723211562.1045629.5375-6021.164491477.60.0-1.0-8.457133-1.00411409.9039448.50122
16605323023[Ne_III] 38683869.8567972272717-0.000313142340.0001491723111562.1045629.5375-520.94579767.851-12.171348228.2347142.8009870.06731371668503569.9039703.04047
.........................................................
16605323023[O_I] 55775578.887703795193-0.0003120340.0001491724811562.1045629.5375-125.52498353.6101-1.86465045.255745467.318240.10587233500488986.9039717.64374
16605323023[O_I] 63006302.046376726897-0.00031199760.000149172511562.1045629.53753.9276272e+061.6237242e+0661251.37525225.6664.1230850.100847274490483971.9039737.93195
16605323023[S_III] 63126313.805532906624-0.000313396220.0001491722811562.1045629.5375-5.492436e+062.2777632e+06-86145.3335860.7463.7577930.100272775484489971.9039736.82104
16605323023[O_I] 63636365.535419574267-0.000312785970.0001491723711562.1045629.53751.9971171e+06837929.931516.0813173.62763.3681950.09966005483490971.9039737.34424
16605323023[N_II] 65486549.858929253789-0.000312788470.0001491723711562.1045629.5375-3.185954e+061.3547132e+06-52687.3222486.26660.469080.095100574477496971.9039818.87115
16605323023H_alpha6564.613894333284-0.00031335640.0001491723399.477645.88162-89.0522113.111426-2.0010290.297764644.5032040.069990814171833.7065237.49552
16605323023[N_II] 65836585.2684452471885-0.00031401990.0001491721911562.1045629.53753.4032402e+061.444533e+0655640.8523529.70561.1644170.09619414478495971.9039813.1483
16605323023[S_II] 67166718.29420811581-0.000312970170.0001491723411562.1045629.5375-3.0535405e+061.272537e+06-50682.65221201.27360.2482380.09475325489495982.9039807.5272
16605323023[S_II] 67306732.678076327587-0.00031258470.0001491724111562.1045629.53752.4067928e+06999924.339819.6616480.8260.442320.09505849483501982.9039802.90234
16605323023[Ar_III] 71357137.757103334721-0.000312401650.0001491724311562.1045629.5375-3345.57371089.6934-58.7233819.21926356.971750.08960027489490977.9039917.1964

Let’s recreate the plot we just made, but add annotations for all of the Hydrogen lines from this table.

# Initiate plot
fig, ax = plt.subplots(figsize=(10, 4))

wavelength = 10 ** spectrum_file["COADD"].data["loglam"]
observed_flux = spectrum_file["COADD"].data["FLUX"]
model_flux = spectrum_file["COADD"].data["MODEL"]

plt.plot(wavelength, observed_flux, c="gray", label="Observed Spectrum")
plt.plot(wavelength, model_flux, c="k", label="Model Spectrum")

plt.grid()

# Define a custom rainbow colormap to use for the spectrum
rainbow_colormap = LinearSegmentedColormap.from_list(
    "",
    ["blueviolet"] + [mpl.cm.turbo(x) for x in np.linspace(0.07, 0.999, 10)],
)

# Loop through absorption lines and choose a few to label
for absorption_line in spectrum_file["SPZLINE"].data:
    line_wl = absorption_line["LINEWAVE"]  # wavelength of absorption line
    line_name = absorption_line["LINENAME"]  # name of absorption line
    if "H_" in line_name:  # Only plot Hydrogen lines
        line_color = rainbow_colormap((line_wl - 3800) / (6700 - 3800))
        plt.axvline(
            line_wl,
            linestyle="--",
            lw=2,
            c=line_color,
            zorder=0,
        )

        plt.text(
            line_wl + 20,
            20,
            line_name,
            rotation=90,
            ha="left",
            va="center",
            color=line_color,
        )

# Add label for the classification
classification = spectrum_file["SPECOBJ"].data["CLASS"][0]
subclass = spectrum_file["SPECOBJ"].data["SUBCLASS"][0]
ax.text(
    0.99,
    0.1,
    f"Classification: {subclass} {classification}",
    fontsize=24,
    ha="right",
    va="top",
    color="k",
    transform=ax.transAxes,
)

# Label axes
plt.xlabel(r"Wavelength ($\AA$)")
plt.ylabel("Flux")
plt.legend()
plt.title(f"{spectrum_file.filename().strip('./')}")

# Set axes limits
plt.xlim(np.min(wavelength), np.max(wavelength))
plt.ylim(0, np.max(observed_flux))

# Show plot
plt.show()
../../../_images/9e3b74041c57ff30d66fffaa33c8897d51f2be7f37f9ee4bfe5fe1a8551b4fad.png

Enhancing the Plot#

A spectrum is a rainbow, so let’s enhance this plot by adding color. Here, we define a function that adds a background to this plot to approximate what this would look like if you could see this spectrum with your eyes.

def plot_rainbow_background(ax, wavelength, flux):
    """Create a rainbow background on the plot to show the spectrum"""
    # Define a custom rainbow colormap to use for the spectrum
    rainbow_colormap = LinearSegmentedColormap.from_list(
        "",
        ["blueviolet"]
        + [mpl.cm.turbo(x) for x in np.linspace(0.07, 0.999, 10)],
    )

    # Normalize flux from 0 to 1 so all spectra are on uniform scale
    flux = flux / np.nanmax(flux)
    # Define min and max values of the flux to normalize the color scale
    max_flux = np.percentile(flux, 90)
    min_flux = np.percentile(flux, 15)
    # Set the scaling limits of the color map
    # the wavelength of the furthest red and furthest purple
    min_wl = 3800
    max_wl = 6700

    # Define window size (in pixels) to smooth spectrum over
    # (makes absorption lines stand out more)
    window_size = 10
    # Loop through each wavelength
    for wl in wavelength[window_size::]:
        i = np.where(wavelength == wl)[0][0]
        wl_scale = (wl - min_wl) / (max_wl - min_wl)
        flux_at_wl = np.interp(wl, wavelength, flux)
        if i <= window_size:
            flux_at_wl = flux[i]
        else:
            i1 = i - window_size
            i2 = i + window_size + 1
            flux_at_wl = np.nanmin(flux[i1:i2])
        flux_scale = (flux_at_wl - min_flux) / (max_flux - min_flux)

        # Check if flux scale is outside limits
        if flux_scale <= 0:
            flux_scale = 0
        elif flux_scale >= 1:
            flux_scale = 0.999
        # Turn to log scale for more contrast
        elif not np.isfinite(flux_scale):
            flux_scale = np.log10(flux_scale * 9 + 1)

        # Plot as vertical lines
        # Plot in black first for a solid background so lines do not overlap
        ax.axvline(wl, c="k", alpha=1, lw=2, zorder=1)
        # Plot in color
        ax.axvline(
            wl, c=rainbow_colormap(wl_scale), alpha=flux_scale, lw=2, zorder=2
        )

Now, let’s use this background-adding function to create a plot:

# Initiate plot
fig, ax = plt.subplots(figsize=(10, 3))

wavelength = 10 ** spectrum_file["COADD"].data["loglam"]
observed_flux = spectrum_file["COADD"].data["FLUX"]
model_flux = spectrum_file["COADD"].data["MODEL"]

plt.plot(wavelength, observed_flux, c="w", label="Observed Spectrum", zorder=4)

plt.grid()

# Add classification label
classification = spectrum_file["SPECOBJ"].data["CLASS"][0]
subclass = spectrum_file["SPECOBJ"].data["SUBCLASS"][0]
ax.text(
    0.99,
    0.1,
    f"Classification: {subclass} {classification}",
    fontsize=16,
    ha="right",
    va="top",
    color="w",
    transform=ax.transAxes,
)

plot_rainbow_background(ax, wavelength, model_flux)

# Label axes
plt.xlabel(r"Wavelength ($\AA$)")
plt.ylabel("Flux")
plt.legend(loc="upper right")
plt.title(f"{spectrum_file.filename().strip('./')}")

# Set axes limits
plt.xlim(np.min(wavelength), np.max(wavelength))
plt.ylim(0, np.max(observed_flux))
plt.xscale("log")

# Show plot
plt.show()
../../../_images/28fd7444c1742ed926b31b0c917dc989012da06bab02052180c398f71a2e00f2.png

It looks pretty! We can see how the background corresponds to the flux values, and the absorption features in the spectrum stick out as vertical black lines in the background. The shortest wavelengths (purple) tend to be brighter than the longest wavelengths (red), but this particular star peaks in green wavelengths, just like our Sun!


Exploring Different Spectral Classifications#

Now that we know what the spectrum of a G-type star like the Sun looks like, let’s explore the other types of stars! In 1912, astronomer Annie Jump Cannon developed a method to classify stars based on the appearance of their spectrum, which is still used today. Stars are classified as O, B, A, F, G, K, or M based on the strength and location of absorption lines in their spectrum. [1]

In this section, we have made a list of stars with different spectral types to plot and explore:

star_list = [
    {  # O-Star example
        "obs_id": "sdss_spectro_2929-54616-0363",
        "spec_file": "spec-2929-54616-0363.fits",
        "label": "",
    },
    {  # B-star example
        "obs_id": "sdss_spectro_2871-54536-0542",
        "spec_file": "spec-2871-54536-0542.fits",
        "label": "",
    },
    {  # White Dwarf-star example
        "obs_id": "sdss_spectro_2630-54327-0551",
        "spec_file": "spec-2630-54327-0551.fits",
        "label": "White Dwarf Star. ",
    },
    {  # A-star example
        "obs_id": "sdss_spectro_2682-54401-0449",
        "spec_file": "spec-2682-54401-0449.fits",
        "label": "",
    },
    {  # F-star example
        "obs_id": "sdss_spectro_3298-54924-0214",
        "spec_file": "spec-3298-54924-0214.fits",
        "label": "",
    },
    {  # G-star example
        "obs_id": "sdss_spectro_1660-53230-0023",
        "spec_file": "spec-1660-53230-0023.fits",
        "label": "Sun-like Star. ",
    },
    {  # K-star example
        "obs_id": "sdss_spectro_3234-54885-0271",
        "spec_file": "spec-3234-54885-0271.fits",
        "label": "",
    },
    {  # M-star example
        "obs_id": "sdss_spectro_3298-54924-0159",
        "spec_file": "spec-3298-54924-0159.fits",
        "label": "",
    },
]

And here is a function to download the spectrum for each star using astroquery.MAST, just like we did earlier:

def download_spec(star: dict[str, str]) -> str:
    """Helper function for downloading SDSS Legacy Spectra Data from MAST"""
    # Search for spectrum in astroquery
    obs_table = Observations.query_criteria(obs_id=star["obs_id"])
    # Retrieve product list
    products = Observations.get_product_list(obs_table)
    # Download spectrum
    manifest = Observations.download_products(
        products, flat=True, verbose=False, mrp_only=True
    )
    return manifest["Local Path"][0]

Now we’re ready to plot all of our stars:

fig = plt.figure(figsize=(12, 10))

# Define a custom rainbow colormap to use for the spectrum
rainbow_colormap = LinearSegmentedColormap.from_list(
    "",
    ["blueviolet"] + [mpl.cm.turbo(x) for x in np.linspace(0.07, 0.999, 10)],
)

for star_i, star in enumerate(star_list):
    print(f"Plotting star {star_i + 1}/{len(star_list)}...")
    # Download Spectrum File
    spectrum_file = download_spec(star)
    # Open spectrum file
    spectrum_file = fits.open(spectrum_file)
    classification = spectrum_file["SPECOBJ"].data["SUBCLASS"][0]

    # Initiate plot
    ax = plt.subplot(len(star_list), 1, star_i + 1)
    wavelength = 10 ** spectrum_file["COADD"].data["loglam"]
    observed_flux = spectrum_file["COADD"].data["FLUX"]
    model_flux = spectrum_file["COADD"].data["MODEL"]

    ax.plot(
        wavelength, observed_flux, c="w", label="Observed Spectrum", zorder=3
    )

    # Start with a black background
    ax.axvspan(np.min(wavelength), np.max(wavelength), color="k", zorder=0)

    plot_rainbow_background(ax, wavelength, observed_flux)

    # Label axes
    if star_i < 5:
        y = 0.90
        va = "top"
    else:
        y = 0.05
        va = "bottom"

    ax.text(
        0.99,
        y,
        f"{star['spec_file']}\n{star['label']}Classification: {classification}",
        fontsize=10,
        ha="right",
        va=va,
        color="w",
        transform=ax.transAxes,
    )

    # Add plot title
    if star_i == 0:
        ax.set_title("SEGUE Stellar Classification")

    # Set axes limits
    ax.set_xlim(np.min(wavelength), np.max(wavelength))
    ax.set_xscale("log")
    ax.set_ylim(-0.1, np.percentile(observed_flux, 99) * 1.1)
    ax.yaxis.set_visible(False)  # Hide y-axis labels

    if star_i == len(star_list) - 1:
        ax.set_xlabel(r"Wavelength ($\AA$)")
    else:
        ax.xaxis.set_visible(False)  # Hide x axis labels

# Adjust subplots so there is no white space between the spectra
plt.subplots_adjust(hspace=0)

# Display plot
plt.savefig("segue_spectra.png", bbox_inches="tight")
plt.show()
Plotting star 1/8...
Plotting star 2/8...
Plotting star 3/8...
Plotting star 4/8...
Plotting star 5/8...
Plotting star 6/8...
INFO: Found cached file ./spec-1660-53230-0023.fits with expected size 604800. [astroquery.query]
Plotting star 7/8...
Plotting star 8/8...
../../../_images/17cf8eb21df0325eef3a4af3f0842cf708a72b2c5018eff6a76fb2b585943a3d.png

This shows what the spectra look like for different types of stars! We can see how the color varies from the hottest stars (top), which have the strongest purple, and the cooler stars (bottom), which have the strongest reds. A-type stars have the strongest Hydrogen lines, while M-type stars have giant absorption bands made by molecules. Now you know how to classify stars based on these example spectra!


Exercise: Classifying a Random Spectrum#

Now that you know what different spectra of different types of stars look like, let’s try classifying one by ourselves. To start, we will select a random star from SEGUE using the code in this cell:

# Querying SDSS SEGUE data
spectro_data = Observations.query_criteria(
    provenance_name="SEGUE",
    target_classification="STAR",
    pagesize=100,
    page=1,
)

# Choose a random entry from the results:
i = np.random.choice(range(len(spectro_data)))
my_random_star = spectro_data[i]

# Display result
my_random_star
Row index=39 masked=True
intentTypeobs_collectionprovenance_nameinstrument_nameprojectfilterswavelength_regiontarget_nametarget_classificationobs_ids_ras_decdataproduct_typeproposal_picalib_levelt_mint_maxt_exptimeem_minem_maxobs_titlet_obs_releaseproposal_idproposal_typesequence_numbers_regionjpegURLdataURLdataRightsmtFlagsrcDenobsidobjID
str7str4str5str17str4str4str7str19str4str28float64float64str8str18int64float64float64float64float64float64str51float64str3str1int64str48str69str64str6boolfloat64str9str10
scienceSDSSSEGUESDSS SpectrographSDSSNoneOPTICALSDSS 3262-54884-85STARsdss_spectro_3262-54884-0085156.1783137.200455spectrumSDSS Collaboration354884.17085648147654884.266030092597203.0380.0920.0Sloan Digital Sky Survey (SDSS) Legacy Spectra Data55572.0N/A----CIRCLE 156.17831 37.200455 0.0004166666666666667mast:SDSS/sdss/spectro/3262/54884/0085/spec-image-3262-54884-0085.pngmast:SDSS/sdss/spectro/3262/54884/0085/spec-3262-54884-0085.fitsPUBLICFalsenan3538720321013303848

Now, it’s time to practice what you’ve learned. In the following cell, write a bit of code that will download the spectrum for this star:

# Use astroquery.mast to download the spectrum for this star

And use it to make a plot:

# Plot the spectrum

Compare this spectrum to the examples in the plot we made earlier in this notebook. What kind of star do you think this is? How can you check your answer?

# After making a guess, see what type of star this is:

Exercise Solutions#

# # Use astroquery.mast to download the spectrum for this star
# products = Observations.get_product_list(my_random_star)
# manifest = Observations.download_products(
#     products, flat=True, verbose=False, mrp_only=True
# )
# # Plot the spectrum
# spectrum_file = fits.open(manifest["Local Path"][0])

# fig, ax = plt.subplots(figsize=(10, 3))

# wavelength = 10 ** spectrum_file["COADD"].data["loglam"]
# observed_flux = spectrum_file["COADD"].data["FLUX"]
# model_flux = spectrum_file["COADD"].data["MODEL"]

# # Plot spectra
# plt.plot(wavelength, observed_flux, c="gray", label="Observed Spectrum")
# plt.plot(wavelength, model_flux, c="k", label="Model Spectrum")

# # Label axes
# ax.set_xlabel(r"Wavelength ($\AA$)")
# ax.set_ylabel("Flux")
# ax.set_title(f"{spectrum_file.filename().strip('./')}")
# ax.grid()
# plt.legend()

# # Set axes limits
# ax.set_xlim(np.min(wavelength), np.max(wavelength))
# ax.set_ylim(0, 1.1 * np.max(model_flux))

# # Show plot
# plt.show()
# # After making a guess, see what type of star this is:
# classification = spectrum_file["SPECOBJ"].data["SUBCLASS"][0]
# print(f"Classification: {classification}")

Additional Resources#

Additional resources are linked below:

Citations#

If you use SDSS Legacy Spectra or SEGUE data from MAST for published research, please see the following links for information on which citations to include in your paper:

About this Notebook#

Author: Julie Imig (jimig@stsci.edu)
Keywords: Tutorial, SDSS, SEGUE, stars, spectra
First published: October 2025
Last updated: October 2025


Top of Page Space Telescope Logo