Source code for specreduce.core

# Licensed under a 3-clause BSD style license - see LICENSE.rst

import inspect
from dataclasses import dataclass

import numpy as np
from astropy import units as u
from astropy.nddata import VarianceUncertainty
from specutils import Spectrum1D

__all__ = ['SpecreduceOperation']


class _ImageParser:
    """
    Coerces images from accepted formats to Spectrum1D objects for
    internal use in specreduce's operation classes.

    Fills any and all of uncertainty, mask, units, and spectral axis
    that are missing in the provided image with generic values.
    Accepted image types are:

        - `~specutils.spectra.spectrum1d.Spectrum1D` (preferred)
        - `~astropy.nddata.ccddata.CCDData`
        - `~astropy.nddata.ndddata.NDDData`
        - `~astropy.units.quantity.Quantity`
        - `~numpy.ndarray`
    """

    def _parse_image(self, image, disp_axis=1):
        """
        Convert all accepted image types to a consistently formatted
        Spectrum1D object.

        Parameters
        ----------
        image : `~astropy.nddata.NDData`-like or array-like, required
            The image to be parsed. If None, defaults to class' own
            image attribute.
        disp_axis : int, optional
            The index of the image's dispersion axis. Should not be
            changed until operations can handle variable image
            orientations. [default: 1]
        """

        # would be nice to handle (cross)disp_axis consistently across
        # operations (public attribute? private attribute? argument only?) so
        # it can be called from self instead of via kwargs...

        if image is None:
            # useful for Background's instance methods
            return self.image

        img = self._get_data_from_image(image)

        # mask and uncertainty are set as None when they aren't specified upon
        # creating a Spectrum1D object, so we must check whether these
        # attributes are absent *and* whether they are present but set as None
        if getattr(image, 'mask', None) is not None:
            mask = image.mask
        else:
            mask = ~np.isfinite(img)

        if getattr(image, 'uncertainty', None) is not None:
            uncertainty = image.uncertainty
        else:
            uncertainty = VarianceUncertainty(np.ones(img.shape))

        unit = getattr(image, 'unit', u.Unit('DN'))

        spectral_axis = getattr(image, 'spectral_axis',
                                np.arange(img.shape[disp_axis]) * u.pix)

        return Spectrum1D(img * unit, spectral_axis=spectral_axis,
                          uncertainty=uncertainty, mask=mask)

    @staticmethod
    def _get_data_from_image(image):
        """Extract data array from various input types for `image`.
           Retruns `np.ndarray` of image data."""

        if isinstance(image, u.quantity.Quantity):
            img = image.value
        if isinstance(image, np.ndarray):
            img = image
        else:  # NDData, including CCDData and Spectrum1D
            img = image.data
        return img


[docs] @dataclass class SpecreduceOperation(_ImageParser): """ An operation to perform as part of a spectroscopic reduction pipeline. This class primarily exists to define the basic API for operations: parameters for the operation are provided at object creation, and then the operation object is called with the data objects required for the operation, which then *return* the data objects resulting from the operation. """
[docs] def __call__(self): raise NotImplementedError('__call__ on a SpecreduceOperation needs to ' 'be overridden')
[docs] @classmethod def as_function(cls, *args, **kwargs): """ Run this operation as a function. Syntactic sugar for e.g., ``Operation.as_function(arg1, arg2, keyword=value)`` maps to ``Operation(arg2, keyword=value)(arg1)`` (if the ``__call__`` of ``Operation`` has only one argument) """ argspec = inspect.getargs(SpecreduceOperation.__call__.__code__) if argspec.varargs: raise NotImplementedError('There is not a way to determine the ' 'number of inputs of a *args style ' 'operation') ninputs = len(argspec.args) - 1 callargs = args[:ninputs] noncallargs = args[ninputs:] op = cls(*noncallargs, **kwargs) return op(*callargs)