Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/887.feature.rst
Original file line number Diff line number Diff line change
@@ -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`. Users have the option of changing attribute values when doing so by designated the new values via kwargs.
102 changes: 102 additions & 0 deletions ndcube/ndcube.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
except ImportError:
pass

COPY = object()


class NDCubeABC(astropy.nddata.NDDataBase):

Expand Down Expand Up @@ -1461,6 +1463,106 @@ 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}
extra_coords = self._extra_coords if user_kwargs["extra_coords"] is COPY else None
global_coords = self._global_coords if user_kwargs["global_coords"] is COPY else None
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.
nddata_sig = inspect.signature(nddata_type).parameters.keys()
all_kwarg_keys = list(all_kwargs.keys())
for key in all_kwarg_keys:
if key not in nddata_sig:
del all_kwargs[key]
# Construct and return new instance.
new_nddata = nddata_type(**all_kwargs)
if isinstance(new_nddata, NDCubeBase):
if extra_coords:
extra_coords._ndcube = new_nddata
new_nddata._extra_coords = extra_coords
if global_coords:
new_nddata._global_coords = 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
Expand Down
7 changes: 7 additions & 0 deletions ndcube/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions ndcube/tests/test_ndcube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)