diff --git a/changelog/887.feature.rst b/changelog/887.feature.rst new file mode 100644 index 000000000..f4ab9c835 --- /dev/null +++ b/changelog/887.feature.rst @@ -0,0 +1 @@ +Add new method, `ndcube.NDCube.to_nddata`, which allows easy conversion of an `~ndcube.NDCube` to a subclass of `~astropy.nddata.NDData`. Attribute values can be altered during the conversion by supplying the new values via kwargs. diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py index 663c0f6fd..2d672329b 100644 --- a/ndcube/ndcube.py +++ b/ndcube/ndcube.py @@ -48,6 +48,8 @@ except ImportError: pass +COPY = object() + class NDCubeABC(astropy.nddata.NDDataBase): @@ -1461,6 +1463,102 @@ def fill_masked(self, fill_value, uncertainty_fill_value=None, unmask=False, fil self.mask = False return None + def to_nddata(self, + *, + data=COPY, + wcs=COPY, + uncertainty=COPY, + mask=COPY, + unit=COPY, + meta=COPY, + psf=COPY, + extra_coords=COPY, + global_coords=COPY, + nddata_type=NDData, + **kwargs, + ): + """ + Constructs new type instance with the same attribute values as this `~ndcube.NDCube`. + + Attribute values can be altered on the output object by setting a kwarg with the new + value, e.g. ``data=new_data``. + Any attributes not supported by the new class (``nddata_type``), will be discarded. + + Parameters + ---------- + data: array-like, optional + Data array of new instance. Default is to use data of this instance. + wcs: `astropy.wcs.wcsapi.BaseLowLevelWCS`, `astropy.wcs.wcsapi.BaseHighLevelWCS`, optional + WCS object of new instance. Default is to use data of this instance. + uncertainty: Any, optional + Uncertainy object of new instance. Default is to use data of this instance. + mask: Any, optional + Mask object of new instance. Default is to use data of this instance. + unit: Any, optional + Unit of new instance. Default is to use data of this instance. + meta: dict-like, optional + Metadata object of new instance. Default is to use data of this instance. + psf: Any, optional + PSF object of new instance. Default is to use data of this instance. + extra_coords: `ndcube.ExtraCoordsABC`, optional + Extra coords object of new instance. Default is to use data of this instance. + global_coords: `ndcube.GlobalCoordsABC`, optional + WCS object of new instance. Default is to use data of this instance. + nddata_type: Any, optional + The type of the returned object. Must be a subclass of `~astropy.nddata.NDData` + or a class that behaves like one. Default=`~astropy.nddata.NDData`. + kwargs: + Additional inputs to the ``nddata_type`` constructor that should differ from, + or are not represented by, the attributes of this instance. For example, to + set different data values on the returned object, set a kwarg ``data=new_data``, + where ``new_data`` is an array of compatible shape and dtype. Note that kwargs + given by the user and attributes on this instance that are not supported by the + ``nddata_type`` constructor are ignored. + + Returns + ------- + new_nddata: Any + The new instance of class given by ``nddata_type`` with the same attribute values + as this `~ndcube.NDCube` instance, except for any alterations specified by the + kwargs. + + Examples + -------- + To create an `~astropy.nddata.NDData` instance which is a copy of an `~ndcube.NDCube` + (called ``cube``) without a WCS, do: + + >>> nddata_without_coords = cube.to_nddata(wcs=None) # doctest: +SKIP + """ + # Build dictionary of new attribute values from this NDCube instance + # and update with user-defined kwargs. Remove any kwargs not set by user. + user_kwargs = {"data": data, + "wcs": wcs, + "uncertainty": uncertainty, + "mask": mask, + "unit": unit, + "meta": meta, + "psf": psf, + "extra_coords": extra_coords, + "global_coords": global_coords} + user_kwargs = {key: value for key, value in user_kwargs.items() if value is not COPY} + user_kwargs.update(kwargs) + all_kwargs = {key.strip("_"): value for key, value in self.__dict__.items()} + all_kwargs.update(user_kwargs) + # Inspect call signature of new_nddata class and + # remove unsupported items from new_kwargs. + all_kwargs = {key: value for key, value in all_kwargs.items() + if key in inspect.signature(nddata_type).parameters.keys()} + # Construct and return new instance. + new_nddata = nddata_type(**all_kwargs) + if isinstance(new_nddata, NDCubeBase): + if extra_coords is COPY: + extra_coords = copy.copy(self._extra_coords) + extra_coords._ndcube = new_nddata + new_nddata._extra_coords = extra_coords + if global_coords is COPY: + new_nddata._global_coords = copy.copy(self._global_coords) + return new_nddata + def _create_masked_array_for_rebinning(data, mask, operation_ignores_mask): m = None if (mask is None or mask is False or operation_ignores_mask) else mask diff --git a/ndcube/tests/helpers.py b/ndcube/tests/helpers.py index 5381fdeca..260fd6182 100644 --- a/ndcube/tests/helpers.py +++ b/ndcube/tests/helpers.py @@ -26,6 +26,7 @@ 'assert_cubes_equal', 'assert_cubesequences_equal', 'assert_extra_coords_equal', + 'assert_global_coords_equal', 'assert_metas_equal', 'assert_wcs_are_equal', 'figure_test', @@ -93,6 +94,12 @@ def assert_extra_coords_equal(test_input, extra_coords): assert_wcs_are_equal(test_input._wcs, extra_coords._wcs) + +def assert_global_coords_equal(test_input, global_coords): + assert test_input.items() == global_coords.items() + assert test_input.physical_types == global_coords.physical_types + + def assert_metas_equal(test_input, expected_output): if type(test_input) is not type(expected_output): raise AssertionError( diff --git a/ndcube/tests/test_ndcube.py b/ndcube/tests/test_ndcube.py index 9e415d24b..0e1e74eda 100644 --- a/ndcube/tests/test_ndcube.py +++ b/ndcube/tests/test_ndcube.py @@ -4,6 +4,7 @@ import numpy as np import pytest +import astropy.nddata import astropy.units as u import astropy.wcs from astropy.wcs.wcsapi import BaseHighLevelWCS @@ -238,3 +239,23 @@ def test_fill_masked_ndc_uncertainty_none(ndc, fill_value, uncertainty_fill_valu uncertainty_fill_value=uncertainty_fill_value, fill_in_place=True ) + + +def test_to_nddata(ndcube_2d_ln_lt): + ndc = ndcube_2d_ln_lt + new_data = ndc.data * 2 + output = ndc.to_nddata(data=new_data, wcs=None) + assert type(output) is astropy.nddata.NDData + assert output.wcs is None + assert (output.data == new_data).all() + + +def test_to_nddata_type_ndcube(ndcube_2d_ln_lt_uncert_ec): + ndc = ndcube_2d_ln_lt_uncert_ec + ndc.global_coords.add("wavelength", "em.wl", 100*u.nm) + new_data = ndc.data * 2 + output = ndc.to_nddata(data=new_data, nddata_type=NDCube) + assert type(output) is NDCube + assert (output.data == new_data).all() + helpers.assert_extra_coords_equal(output.extra_coords, ndc.extra_coords) + helpers.assert_global_coords_equal(output.global_coords, ndc.global_coords)