Source code for specutils.spectra.spectrum_collection

import warnings

import astropy.units as u
import numpy as np
from astropy.nddata import NDUncertainty, StdDevUncertainty
from astropy.coordinates import SpectralCoord

from .spectrum1d import Spectrum1D
from astropy.nddata import NDIOMixin

__all__ = ['SpectrumCollection']


[docs] class SpectrumCollection(NDIOMixin): """ A class to represent a heterogeneous set of spectra that are the same length but have different spectral axes. Spectra that meet this requirement can be stored as multidimensional arrays, and thus can have operations performed on them faster than if they are treated as individual :class:`~specutils.Spectrum1D` objects. For multidimensional spectra that all have the *same* spectral axis, use a :class:`~specutils.Spectrum1D` with dimension greater than 1. For a collection of spectra that have different shapes, use :class:`~specutils.SpectrumList`. For more on this topic, see :ref:`specutils-representation-overview`. The attributes on this class uses the same names and conventions as :class:`~specutils.Spectrum1D`, allowing some operations to work the same. Where this does not work, the user can use standard indexing notation to access individual :class:`~specutils.Spectrum1D` objects. Parameters ---------- flux : :class:`astropy.units.Quantity` The flux data. The trailing dimension should be the spectral dimension. spectral_axis : :class:`astropy.units.Quantity` The spectral axes of the spectra (e.g., wavelength). Must match the dimensionality of ``flux``. wcs : wcs object or None A wcs object (if available) for the collection of spectra. The object must follow standard indexing rules to get a sub-wcs if it is provided. uncertainty : :class:`astropy.nddata.NDUncertainty` or ndarray The uncertainties associated with each spectrum of the collection. In the case that only an n-dimensional quantity or ndaray is provided, the uncertainties are assumed to be standard deviations. Must match the dimensionality of ``flux``. mask : ndarray or None The n-dimensional mask information associated with each spectrum. If present, must match the dimensionality of ``flux``. meta : list The list of dictionaries containing meta data to be associated with each spectrum in the collection. """ def __init__(self, flux, spectral_axis=None, wcs=None, uncertainty=None, mask=None, meta=None): # Check for quantity if not isinstance(flux, u.Quantity): raise u.UnitsError("Flux must be a `Quantity`.") if spectral_axis is not None: if not isinstance(spectral_axis, u.Quantity): raise u.UnitsError("Spectral axis must be a `Quantity`.") spectral_axis = SpectralCoord(spectral_axis) # Ensure that the input values are the same shape if not (flux.shape == spectral_axis.shape): raise ValueError("Shape of all data elements must be the same.") if uncertainty is not None and uncertainty.array.shape != flux.shape: raise ValueError("Uncertainty must be the same shape as flux and " "spectral axis.") if mask is not None and mask.shape != flux.shape: raise ValueError("Mask must be the same shape as flux and " "spectral axis.") # Convert uncertainties to standard deviations if not already defined # to be of some type if uncertainty is not None and not isinstance(uncertainty, NDUncertainty): # If the uncertainties are not provided a unit, raise a warning # and use the flux units if not isinstance(uncertainty, u.Quantity): warnings.warn("No unit associated with uncertainty, assuming" f"flux units of '{flux.unit}'.") uncertainty = u.Quantity(uncertainty, unit=flux.unit) uncertainty = StdDevUncertainty(uncertainty) self._flux = flux self._spectral_axis = spectral_axis self._wcs = wcs self._uncertainty = uncertainty self._mask = mask self._meta = meta def __getitem__(self, key): flux = self.flux[key] if flux.ndim != 1: raise ValueError("Currently only 1D data structures may be " "returned from slice operations.") spectral_axis = self.spectral_axis[key] uncertainty = None if self.uncertainty is None else self.uncertainty[key] wcs = None if self.wcs is None else self.wcs[key] mask = None if self.mask is None else self.mask[key] if self.meta is None: meta = None else: try: meta = self.meta[key] except KeyError: meta = self.meta return Spectrum1D(flux=flux, spectral_axis=spectral_axis, uncertainty=uncertainty, wcs=wcs, mask=mask, meta=meta)
[docs] @classmethod def from_spectra(cls, spectra): """ Create a spectrum collection from a set of individual :class:`specutils.Spectrum1D` objects. Parameters ---------- spectra : list, ndarray A list of :class:`~specutils.Spectrum1D` objects to be held in the collection. Currently the spectral_axis parameters (e.g. observer, radial_velocity) must be the same for each spectrum. """ # Enforce that the shape of each item must be the same if not all((x.shape == spectra[0].shape for x in spectra)): raise ValueError("Shape of all elements must be the same.") # Compose multi-dimensional ndarrays for each property flux = u.Quantity([spec.flux for spec in spectra]) # Check that the spectral parameters are the same for each input # spectral_axis and create the multi-dimensional SpectralCoord sa = [x.spectral_axis for x in spectra] if (not all(x.radial_velocity == sa[0].radial_velocity for x in sa) or not all(x.target == sa[0].target for x in sa) or not all(x.observer == sa[0].observer for x in sa) or not all(x.doppler_convention == sa[0].doppler_convention for x in sa) or not all(x.doppler_rest == sa[0].doppler_rest for x in sa)): raise ValueError("All input spectral_axis SpectralCoord " "objects must have the same parameters.") spectral_axis = SpectralCoord(sa, radial_velocity=sa[0].radial_velocity, doppler_rest=sa[0].doppler_rest, doppler_convention=sa[0].doppler_convention, observer=sa[0].observer, target=sa[0].target) # Check that either all spectra have associated uncertainties, or that # none of them do. If only some do, log an error and ignore the # uncertainties. if (not all((x.uncertainty is None for x in spectra)) and any((x.uncertainty is not None for x in spectra)) and all((x.uncertainty.uncertainty_type == spectra[0].uncertainty.uncertainty_type for x in spectra))): quncs = u.Quantity([spec.uncertainty.quantity for spec in spectra]) uncertainty = spectra[0].uncertainty.__class__(quncs) else: uncertainty = None warnings.warn("Not all spectra have associated uncertainties of " "the same type, skipping uncertainties.") # Check that either all spectra have associated masks, or that # none of them do. If only some do, log an error and ignore the masks. if (not all((x.mask is None for x in spectra)) and any((x.mask is not None for x in spectra))): mask = np.array([spec.mask for spec in spectra]) else: mask = None warnings.warn("Not all spectra have associated masks, " "skipping masks.") # Store the wcs and meta as lists wcs = [spec.wcs for spec in spectra] meta = [spec.meta for spec in spectra] return cls(flux=flux, spectral_axis=spectral_axis, uncertainty=uncertainty, wcs=wcs, mask=mask, meta=meta)
@property def flux(self): """The flux in the spectrum as a `~astropy.units.Quantity`.""" return self._flux @property def spectral_axis(self): """The spectral axes as a `~astropy.units.Quantity`.""" return self._spectral_axis @property def frequency(self): """ The spectral axis as a frequency `~astropy.units.Quantity` (in GHz). """ return self.spectral_axis.to(u.GHz, u.spectral()) @property def wavelength(self): """ The spectral axis as a wavelength `~astropy.units.Quantity` (in Angstroms). """ return self.spectral_axis.to(u.AA, u.spectral()) @property def energy(self): """ The spectral axis as an energy `~astropy.units.Quantity` (in eV). """ return self.spectral_axis.to(u.eV, u.spectral()) @property def wcs(self): """The WCS's as an object array""" return self._wcs @property def uncertainty(self): """The uncertainty in the spectrum as a `~astropy.units.Quantity`.""" return self._uncertainty @property def mask(self): """The mask array for the spectrum.""" return self._mask @property def meta(self): """A dictionary of metadata for theis spectrum collection, or `None`.""" return self._meta @property def shape(self): """ The shape of the collection. This is *not* the same as the shape of the flux et al., because the trailing (spectral) dimension is not included here. """ return self.flux.shape[:-1] def __len__(self): return self.shape[0] @property def ndim(self): """ The dimensionality of the collection. This is *not* the same as the dimensionality of the flux et al., because the trailing (spectral) dimension is not included here. """ return self.flux.ndim - 1 @property def nspectral(self): """ The length of the spectral dimension. """ return self.flux.shape[-1] def __repr__(self): return """SpectrumCollection(ndim={}, shape={}) Flux units: {} Spectral axis units: {} Uncertainty type: {}""".format( self.ndim, self.shape, self.flux.unit, self.spectral_axis.unit, self.uncertainty.uncertainty_type if self.uncertainty is not None else None)