Specutils
repository·main·Indexed 42 days ago
https://github.com/astropy/specutilsAn Astropy-affiliated package providing standardized Python representations for astronomical spectra and tools for spectroscopic analysis. It includes utilities for calculating signal-to-noise ratio (SNR), measuring line flux and equivalent width, computing centroids and moments, estimating spectral line widths (Gaussian and non-Gaussian), and performing template matching and cross-correlation. It also supports arithmetic operations on Spectrum objects with automatic unit and uncertainty propagation.
What's inside Specutils
- Specutils is an Astropy-affiliated package designed to provide shared Python representations of astronomical spectra and basic tools for operating on them. It acts as a 'toolbox' and a 'hub' to unite the Python astronomical spectroscopy community by providing standardized data representations that facilitate connecting different reduction pipelines and analysis tools.
What is a SpectrumCollection and how to use it
mainA
SpectrumCollectionis a container that allows a set of spectra to behave like a singleSpectrumobject. This enables you to perform analysis operations over entire sets of data simultaneously.Key Requirements & Behaviors:
- Shape Consistency: All spectra within a collection must have the same shape.
- Dispersion Solutions: The collection does not make assumptions about dispersion solutions. Users should ensure spectra are resampled to a common grid or are aware that they may not share the same dispersion solution.
- Data Storage: Unlike a list of objects, a
SpectrumCollectionstores data as single multi-dimensional arrays (e.g., a 2D flux array). It constructs individualSpectrumobjects on-the-fly only when you index the collection. - Immutability via Indexing: Because indexing creates a new
Spectrumobject from a slice, performing operations on a spectrum retrieved from the collection (e.g.,spec_coll[0].flux *= 2) will not modify the originalSpectrumCollectionobject.
import numpy as np import astropy.units as u from astropy.nddata import StdDevUncertainty from specutils import SpectrumCollection from specutils.utils.wcs_utils import gwcs_from_array # Setup data for 5 spectra, each with 10 data points flux = u.Quantity(np.random.sample((5, 10)), unit='Jy') spectral_axis = u.Quantity(np.arange(50).reshape((5, 10)), unit='AA') wcs = np.array([gwcs_from_array(x, x.shape) for x in spectral_axis]) uncertainty = StdDevUncertainty(np.random.sample((5, 10)), unit='Jy') mask = np.ones((5, 10)).astype(bool) meta = [{'test': 5, 'info': [1, 2, 3]} for i in range(5)] # Initialize the collection spec_coll = SpectrumCollection( flux=flux, spectral_axis=spectral_axis, wcs=wcs, uncertainty=uncertainty, mask=mask, meta=meta ) print(spec_coll.shape) # Output: (5,) print(spec_coll.flux.unit) # Output: Unit("Jy") print(spec_coll.spectral_axis.shape) # Output: (5, 10)Manage spectrum metadata
mainThe
Spectrum.metaattribute is a dictionary for storing additional data (e.g., origin, date).When reading from FITS files, loaders typically store header information in
Spectrum.meta['header']. When writing to FITS usingtabular-fitsorwcs1d-fits, the contents ofSpectrum.meta['header']are saved as the header of theastropy.io.fits.PrimaryHDU.Handle multi-dimensional data in Spectrum
mainA
Spectrumcan represent multi-dimensional data sets (e.g., an(n_spectra, n_pix)array) where multiple flux arrays share a single commonspectral_axis.- Automatic Axis Detection: Specutils attempts to match the shape of the
fluxanduncertaintyarrays to thespectral_axis. - Manual Axis Specification: If multiple axes have the same length as the spectral axis, you must specify which one is the spectral axis using the
spectral_axis_indexargument during initialization. - Reordering Axes: Use the
move_spectral_axisargument to force the spectral axis to a specific dimension. Settingmove_spectral_axis=-1ormove_spectral_axis='last'reproduces the pre-v2.0 behavior where the spectral axis was always the last axis. This swaps the relevant axes influx,mask, anduncertaintyand updates the WCS accordingly.
Note: If each flux array is related to a different spectral axis, use
SpectrumCollectioninstead.from specutils import Spectrum import numpy as np import astropy.units as u # Example: (5, 10) flux array with a spectral axis of length 10 spec = Spectrum(spectral_axis=np.arange(5000, 5010) * u.AA, flux=np.random.default_rng(12345).random((5, 10)) * u.Jy) # Slicing a single spectrum from the collection spec_slice = spec[0] print(spec_slice.spectral_axis) print(spec_slice.flux)- Automatic Axis Detection: Specutils attempts to match the shape of the
Core attributes of Spectrum objects
mainAll core spectral objects in
specutils(includingSpectrumandSpectrumCollection) share two fundamental attributes that define the spectroscopic data:spectral_axis(Spectrum.spectral_axis): Represents the physical interpretation of each pixel (the "world coordinate"). Common representations include wavelength, frequency, energy, or velocity. For un-calibrated data, this may be in pixel units.flux(Spectrum.flux): The actual spectroscopic signal. While named "flux", it can carry any unit (e.g., counts, surface brightness in magnitudes, or spectral flux density). For calibrated spectra, spectral flux density (power per area per spectral unit) is the recommended representation for compatibility with analysis tools.
Define and manipulate `SpectralRegion` objects
mainA
SpectralRegiondefines one or more sub-regions in spectral space. They are independent of any specificSpectrumobject.Creation
You can define a region in several ways:
- Single region: Pass two
astropy.units.Quantityobjects (e.g.,SpectralRegion(0.45*u.um, 0.6*u.um)). - Multiple sub-regions: Pass a list of 2-tuples (e.g.,
SpectralRegion([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)])). - Units: Supports any valid spectral unit or
u.pixelfor direct indexing.
Operations
- Combining: Use the
+operator to combine regions or+=for in-place addition. - Slicing: Use integer indexing or range slicing (e.g.,
sr[0]orsr[3:5]). - Deletion: Use
del sr[index]to remove a sub-region. - Iteration: You can iterate over a
SpectralRegionto access its individual sub-regions. - Inversion: Use
.invert(lower, upper)to return a region representing the space between existing sub-regions within a specified bound. This is useful for finding baseline/noise regions around lines.
from astropy import units as u from specutils.spectra import SpectralRegion # Single region sr = SpectralRegion(0.45*u.um, 0.6*u.um) # Multiple sub-regions sr_two = SpectralRegion([(0.45*u.um, 0.6*u.um), (0.8*u.um, 0.9*u.um)]) # Combining sr_combined = SpectralRegion(0.45*u.um, 0.6*u.um) + SpectralRegion(0.8*u.um, 0.9*u.um) # Inverting (finding gaps between regions) sr_inverted = sr_two.invert(0.05*u.um, 3*u.um)- Single region: Pass two
Load multiple spectra into a SpectrumList
mainIf a single file contains multiple spectra (e.g., one target per row in a table), your loader should return a
SpectrumList.Note: If you define a loader with
dtype=Spectrum,specutilsautomatically provides a reader forSpectrumListas well, allowing you to callSpectrumList.read()on files that might contain one or many spectra.from specutils import Spectrum, SpectrumList from specutils.io.registers import data_loader @data_loader( "JWST x1d multi", identifier=identify_jwst_x1d_multi_fits, dtype=SpectrumList, extensions=['fits'], priority=10, ) def jwst_x1d_multi_loader(file_obj, **kwargs): # ... implementation returns a SpectrumList ... return SpectrumList(spectra)Combine SpectralRegions using operators
mainMultiple
SpectralRegionobjects can be combined into aCompoundSpectralRegionusing bitwise operators:|(OR): Combines regions.&(AND): Intersection of regions.^(XOR): Symmetric difference.
Once combined, you can:
- Check if a specific wavelength is inside the region using
.contains(wavelength). - Convert the compound region to a boolean mask for a
Spectrumusing.to_mask(spectrum). - Flatten the compound region into a set of simple sub-regions using
.to_subregions().
import astropy.units as u from specutils import Spectrum, SpectralRegion reg1 = SpectralRegion(5*u.um, 7*u.um) reg2 = SpectralRegion([(4*u.um, 5.5*u.um), (6.5*u.um, 8*u.um)]) # Combine using OR compound = reg1 | reg2 # Check containment print(compound.contains(4.5*u.um)) # True # Convert to mask spectrum = Spectrum(spectral_axis=np.arange(1, 10) * u.um, flux=np.ones(9)*u.Jy) mask = compound.to_mask(spectrum) # Convert to simple sub-regions subregions = compound.to_subregions()How to create a custom spectral file loader
mainSpecutils uses the
astropy ioregistry to handle file loading. To implement a custom loader for a proprietary or non-standard format:- Define the loader logic in a separate Python file.
- Place this Python file in your
~/.specutils/directory.
This allows
Spectrum.readto recognize and handle your custom format automatically.Define WCS for Spectrum objects
mainSpecutils maintains a WCS (World Coordinate System) object for every
Spectrum. If not explicitly provided, specutils creates an internal GWCS object. Currently, specutils supports two WCS formats:- FITS WCS (via
astropy.wcs) - GWCS
To provide a FITS-style WCS, pass an
astropy.wcs.WCSobject to thewcsargument duringSpectruminitialization. You can then usespec.wcs.pixel_to_world()to convert pixel coordinates to world coordinates.from specutils.spectra import Spectrum import astropy.wcs as fitswcs import astropy.units as u import numpy as np my_wcs = fitswcs.WCS(header={ 'CDELT1': 1, 'CRVAL1': 6562.8, 'CUNIT1': 'Angstrom', 'CTYPE1': 'WAVE', 'RESTFRQ': 1400000000, 'CRPIX1': 25 }) spec = Spectrum(flux=[5,6,7] * u.Jy, wcs=my_wcs) # Access spectral axis and convert pixels to world print(spec.spectral_axis) print(spec.wcs.pixel_to_world(np.arange(3)))- FITS WCS (via
How specutils represents different types of spectra
mainSpecutils uses different data structures depending on the dimensionality and shape of the spectroscopic data. The choice of object depends on whether the flux is 1D, multidimensional, or part of a collection with varying spectral axes.
1. Single or Multidimensional Spectra (
Spectrum)Use the
specutils.Spectrumclass for:- 1D Spectra: A single flux of length
nwith a matched spectral axis (array or WCS). - Multidimensional Fluxes: A set of fluxes with shape
n x m (x ...)where the spectral axis is strictly of lengthn.SpectrumsubclassesNDCube, providing utilities for multidimensional data.
2. Uniform Collections (
SpectrumCollection)Use
specutils.SpectrumCollectionwhen you have a set of fluxes of shapen x m (x ...)and a corresponding set of spectral axes of the same shape.- Performance: This is more performant than a list of individual objects because it stores data in unified arrays.
- Behavior: It does not store individual
Spectrumobjects. Instead, it constructs aSpectrumobject on the fly when indexed. Note: Operations performed on aSpectrumreturned from aSpectrumCollectiondo not modify the original collection.
3. Ragged or Heterogeneous Collections
For collections where spectra have different lengths or different spectral axes (ragged data):
- Standard Approach: Use a standard Python
listor anumpyobject-array containing individualspecutils.Spectrumobjects. Iterate over the list to process them. SpectrumList: A subclass oflistintegrated with the Astropy IO registry. It is primarily used by data loaders to return multiple heterogeneous spectra. For most user workflows, a standardlistofSpectrumobjects is sufficient.
- 1D Spectra: A single flux of length
Include masks in a Spectrum
mainMasks follow
astropy.nddata.NDDataconventions. A mask should be set such that values areFalseor0where the data is good, andTrueor non-zero where the data should be masked.specutilsalso recognizesNaNvalues in thefluxarray and will automatically set the mask toTruefor those positions unless explicitly overridden.