Skip to main content
`Ctrl`+`K`

# Source code for astropy.nddata.nddata

    # Licensed under a 3-clause BSD style license - see LICENSE.rst
    # This module implements the base NDData class.

    from copy import deepcopy

    import numpy as np

    from astropy import log
    from astropy.units import Quantity, Unit
    from astropy.utils.compat.optional_deps import HAS_DASK
    from astropy.utils.masked import Masked, MaskedNDArray
    from astropy.utils.metadata import MetaData
    from astropy.wcs.wcsapi import (
        BaseHighLevelWCS,
        BaseLowLevelWCS,
        HighLevelWCSWrapper,
        SlicedLowLevelWCS,  # noqa: F401
    )

    from .nddata_base import NDDataBase
    from .nduncertainty import NDUncertainty, UnknownUncertainty

    __all__ = ["NDData"]

    _meta_doc = """`dict`-like : Additional meta information about the dataset."""

    class NDData(NDDataBase):
        """
        A container for `numpy.ndarray`-based datasets, using the
        `~astropy.nddata.NDDataBase` interface.

        The key distinction from raw `numpy.ndarray` is the presence of
        additional metadata such as uncertainty, mask, unit, a coordinate system
        and/or a dictionary containing further meta information. This class *only*
        provides a container for *storing* such datasets. For further functionality
        take a look at the ``See also`` section.

        See also: https://docs.astropy.org/en/stable/nddata/

        Parameters
        ----------
        data : `numpy.ndarray`-like or `NDData`-like
            The dataset.

        uncertainty : any type, optional
            Uncertainty in the dataset.
            Should have an attribute ``uncertainty_type`` that defines what kind of
            uncertainty is stored, for example ``"std"`` for standard deviation or
            ``"var"`` for variance. A metaclass defining such an interface is
            `NDUncertainty` - but isn't mandatory. If the uncertainty has no such
            attribute the uncertainty is stored as `UnknownUncertainty`.
            Defaults to ``None``.

        mask : any type, optional
            Mask for the dataset. Masks should follow the ``numpy`` convention that
            **valid** data points are marked by ``False`` and **invalid** ones with
            ``True``.
            Defaults to ``None``.

        wcs : any type, optional
            World coordinate system (WCS) for the dataset.
            Default is ``None``.

        meta : `dict`-like object, optional
            Additional meta information about the dataset. If no meta is provided
            an empty dict is created.
            Default is ``None``.

        unit : unit-like, optional
            Unit for the dataset. Strings that can be converted to a
            `~astropy.units.Unit` are allowed.
            Default is ``None``.

        copy : `bool`, optional
            Indicates whether to save the arguments as copy. ``True`` copies
            every attribute before saving it while ``False`` tries to save every
            parameter as reference.
            Note however that it is not always possible to save the input as
            reference.
            Default is ``False``.

            .. versionadded:: 1.2

        psf : `numpy.ndarray` or None, optional
            Image representation of the PSF. In order for convolution to be flux-
            preserving, this should generally be normalized to sum to unity.

        Raises
        ------
        TypeError
            In case ``data`` or ``meta`` don't meet the restrictions.

        Notes
        -----
        Each attribute can be accessed through the homonymous instance attribute:
        ``data`` in a `NDData` object can be accessed through the `data`
        attribute::

            >>> from astropy.nddata import NDData
            >>> nd = NDData([1,2,3])
            >>> nd.data
            array([1, 2, 3])

        Given a conflicting implicit and an explicit parameter during
        initialization, for example the ``data`` is a `~astropy.units.Quantity` and
        the unit parameter is not ``None``, then the implicit parameter is replaced
        (without conversion) by the explicit one and a warning is issued::

            >>> import numpy as np
            >>> import astropy.units as u
            >>> q = np.array([1,2,3,4]) * u.m
            >>> nd2 = NDData(q, unit=u.cm)
            INFO: overwriting Quantity's current unit with specified unit. [astropy.nddata.nddata]
            >>> nd2.data  # doctest: +FLOAT_CMP
            array([100., 200., 300., 400.])
            >>> nd2.unit
            Unit("cm")

        See Also
        --------
        NDDataRef
        NDDataArray
        """

        # Instead of a custom property use the MetaData descriptor also used for
        # Tables. It will check if the meta is dict-like or raise an exception.
        meta = MetaData(doc=_meta_doc, copy=False)

        def __init__(
            self,
            data,
            uncertainty=None,
            mask=None,
            wcs=None,
            meta=None,
            unit=None,
            copy=False,
            psf=None,
        ):
            # Rather pointless since the NDDataBase does not implement any setting
            # but before the NDDataBase did call the uncertainty
            # setter. But if anyone wants to alter this behavior again the call
            # to the superclass NDDataBase should be in here.
            super().__init__()

            # Check if data is any type from which to collect some implicitly
            # passed parameters.
            if isinstance(data, NDData):  # don't use self.__class__ (issue #4137)
                # Of course we need to check the data because subclasses with other
                # init-logic might be passed in here. We could skip these
                # tests if we compared for self.__class__ but that has other
                # drawbacks.

                # Comparing if there is an explicit and an implicit unit parameter.
                # If that is the case use the explicit one and issue a warning
                # that there might be a conflict. In case there is no explicit
                # unit just overwrite the unit parameter with the NDData.unit
                # and proceed as if that one was given as parameter. Same for the
                # other parameters.
                if unit is None and data.unit is not None:
                    unit = data.unit
                elif unit is not None and data.unit is not None:
                    log.info("overwriting NDData's current unit with specified unit.")

                if uncertainty is not None and data.uncertainty is not None:
                    log.info(
                        "overwriting NDData's current "
                        "uncertainty with specified uncertainty."
                    )
                elif data.uncertainty is not None:
                    uncertainty = data.uncertainty

                if mask is not None and data.mask is not None:
                    log.info("overwriting NDData's current mask with specified mask.")
                elif data.mask is not None:
                    mask = data.mask

                if wcs is not None and data.wcs is not None:
                    log.info("overwriting NDData's current wcs with specified wcs.")
                elif data.wcs is not None:
                    wcs = data.wcs

                if psf is not None and data.psf is not None:
                    log.info("Overwriting NDData's current psf with specified psf.")
                elif data.psf is not None:
                    psf = data.psf

                if meta is not None and data.meta is not None:
                    log.info("overwriting NDData's current meta with specified meta.")
                elif data.meta is not None:
                    meta = data.meta

                # get the data attribute as it is, and continue to process it:
                data = data.data

            # if the data is wrapped by astropy.utils.masked.Masked:
            if isinstance(data, Masked):
                # first get the mask if one is available:
                if hasattr(data, "mask"):
                    if mask is not None:
                        log.info(
                            "overwriting Masked Quantity's current mask with specified mask."
                        )
                    else:
                        mask = data.mask

                if isinstance(data, MaskedNDArray):
                    if unit is not None and hasattr(data, "unit") and data.unit != unit:
                        log.info(
                            "overwriting MaskedNDArray's current unit with specified unit."
                        )
                        data = data.to(unit).value
                    elif unit is None and hasattr(data, "unit"):
                        unit = data.unit
                        data = data.value

                    # now get the unmasked ndarray:
                    data = np.asarray(data)

                if isinstance(data, Quantity):
                    # this is a Quantity:
                    if unit is not None and data.unit != unit:
                        log.info("overwriting Quantity's current unit with specified unit.")
                        data = data.to(unit)
                    elif unit is None and data.unit is not None:
                        unit = data.unit
                    data = data.value

            if isinstance(data, np.ma.masked_array):
                if mask is not None:
                    log.info(
                        "overwriting masked ndarray's current mask with specified mask."
                    )
                else:
                    mask = data.mask
                data = data.data

            if isinstance(data, Quantity):
                # this is a Quantity:
                if unit is not None and data.unit != unit:
                    log.info("overwriting Quantity's current unit with specified unit.")
                    data = data.to(unit)
                elif unit is None and data.unit is not None:
                    unit = data.unit
                data = data.value

            if isinstance(data, np.ndarray):
                # check for mask from np.ma.masked_ndarray
                if hasattr(data, "mask"):
                    if mask is not None:
                        log.info(
                            "overwriting masked ndarray's current mask with specified mask."
                        )
                    else:
                        mask = data.mask

            # Quick check on the parameters if they match the requirements.
            if (
                not hasattr(data, "shape")
                or not hasattr(data, "__getitem__")
                or not hasattr(data, "__array__")
            ):
                # Data doesn't look like a numpy array, try converting it to
                # one.
                data = np.asanyarray(data)
            # Another quick check to see if what we got looks like an array
            # rather than an object (since numpy will convert a
            # non-numerical/non-string inputs to an array of objects).
            if data.dtype == "O":
                raise TypeError("could not convert data to numpy array.")

            if unit is not None:
                unit = Unit(unit)

            if copy:
                # Data might have been copied before but no way of validating
                # without another variable.
                data = deepcopy(data)
                mask = deepcopy(mask)
                wcs = deepcopy(wcs)
                psf = deepcopy(psf)
                meta = deepcopy(meta)
                uncertainty = deepcopy(uncertainty)
                # Actually - copying the unit is unnecessary but better safe
                # than sorry :-)
                unit = deepcopy(unit)

            # Store the attributes
            self._data = data
            self.mask = mask
            self._wcs = None
            if wcs is not None:
                # Validate the wcs
                self.wcs = wcs
            self.meta = meta  # TODO: Make this call the setter sometime
            self._unit = unit
            # Call the setter for uncertainty to further check the uncertainty
            self.uncertainty = uncertainty
            self.psf = psf

        def __str__(self):
            data = str(self.data)
            unit = f" {self.unit}" if self.unit is not None else ""

            return data + unit

        def __repr__(self):
            prefix = self.__class__.__name__ + "("
            # to support reprs for other non-ndarray `data` attributes,
            # add more cases here:
            if HAS_DASK:
                import dask.array as da

                is_dask = isinstance(self.data, da.Array)
            else:
                is_dask = False

            if (
                isinstance(self.data, (int, float, np.ndarray))
                or np.issubdtype(float, self.data)
                or np.issubdtype(int, self.data)
            ) and not is_dask:
                # if data is an ndarray, get build a repr via Masked:
                ma = Masked(self.data, mask=self.mask)
                data_repr = repr(ma)
                # strip the class name generated by Masked, and the final ")",
                # and re-pad multi-line array outputs
                after_first_paren = data_repr.index("(") + 1
                old_prefix_spaces = " " * after_first_paren
                new_prefix_spaces = " " * len(prefix)
                data_repr = data_repr[after_first_paren:-1].replace(
                    old_prefix_spaces, new_prefix_spaces
                )
                unit = f", unit='{self.unit}'" if self.unit is not None else ""
                return f"{prefix}{data_repr}{unit})"
            else:
                # relying on the builtin repr for objects other than ndarrays
                # allows for compatibility with e.g. dask:
                contents = []
                for attr in ("data", "mask", "uncertainty", "unit"):
                    attr_data = getattr(self, attr)
                    if attr_data is not None:
                        attr_prefix = f"\n  {attr}="
                        attr_repr = repr(attr_data)
                        attr_repr = attr_repr.replace(
                            "\n", f"\n{' ' * (len(attr_prefix) - 1)}"
                        )
                        contents.append(attr_prefix + attr_repr)
                return prefix + ",".join(contents) + "\n)"

        @property
        def data(self):
            """
            `~numpy.ndarray`-like : The stored dataset.
            """
            return self._data

        @property
        def mask(self):
            """
            any type : Mask for the dataset, if any.

            Masks should follow the ``numpy`` convention that valid data points are
            marked by ``False`` and invalid ones with ``True``.
            """
            return self._mask

        @mask.setter
        def mask(self, value):
            self._mask = value

        @property
        def unit(self):
            """
            `~astropy.units.Unit` : Unit for the dataset, if any.
            """
            return self._unit

        @property
        def wcs(self):
            """
            any type : A world coordinate system (WCS) for the dataset, if any.
            """
            return self._wcs

        @wcs.setter
        def wcs(self, wcs):
            if self._wcs is not None and wcs is not None:
                raise ValueError(
                    "You can only set the wcs attribute with a WCS if no WCS is present."
                )

            if wcs is None or isinstance(wcs, BaseHighLevelWCS):
                self._wcs = wcs
            elif isinstance(wcs, BaseLowLevelWCS):
                self._wcs = HighLevelWCSWrapper(wcs)
            else:
                raise TypeError(
                    "The wcs argument must implement either the high or low level WCS API."
                )

        @property
        def psf(self):
            return self._psf

        @psf.setter
        def psf(self, value):
            self._psf = value

        @property
        def uncertainty(self):
            """
            any type : Uncertainty in the dataset, if any.

            Should have an attribute ``uncertainty_type`` that defines what kind of
            uncertainty is stored, such as ``'std'`` for standard deviation or
            ``'var'`` for variance. A metaclass defining such an interface is
            `~astropy.nddata.NDUncertainty` but isn't mandatory.
            """
            return self._uncertainty

        @uncertainty.setter
        def uncertainty(self, value):
            if value is not None:
                # There is one requirements on the uncertainty: That
                # it has an attribute 'uncertainty_type'.
                # If it does not match this requirement convert it to an unknown
                # uncertainty.
                if not hasattr(value, "uncertainty_type"):
                    log.info("uncertainty should have attribute uncertainty_type.")
                    value = UnknownUncertainty(value, copy=False)

                # If it is a subclass of NDUncertainty we must set the
                # parent_nddata attribute. (#4152)
                if isinstance(value, NDUncertainty):
                    # In case the uncertainty already has a parent create a new
                    # instance because we need to assume that we don't want to
                    # steal the uncertainty from another NDData object
                    if value._parent_nddata is not None:
                        value = value.__class__(value, copy=False)
                    # Then link it to this NDData instance (internally this needs
                    # to be saved as weakref but that's done by NDUncertainty
                    # setter).
                    value.parent_nddata = self
            self._uncertainty = value