NDCollection¶
NDCollection
is a container class for grouping NDCube
or
NDCubeSequence
instances together.
It does not imply an ordered relationship between its constituent ND objects
like NDCubeSequence
.
Instead it links ND objects in an unordered way like a Python dictionary.
This has many possible uses, for example, linking observations with derived
data products.
Let’s say we have a 3D NDCube
representing space-space-wavelength.
Then let’s say we fit a spectral line in each pixel’s spectrum and extract
its linewidth.
Now we have a 2D spatial map of linewidth with the same spatial axes
as the original 3D cube.
However the physical properties represented by the data are different.
They do not have an order within their common coordinate space.
And they do not have the same dimensionality as the 2nd cube’s spectral axis
has been collapsed.
Therefore is it not appropriate to combine them in an NDCubeSequence
.
This is where NDCollection
comes in handy.
It allows us to name each ND object and combine them into a single container,
just like a dictionary.
In fact NDCollection
inherits from dict
.
Initialization¶
To see how we initialize an NDCollection
, let’s first define a couple
of NDCube
instances representing the situation above, i.e. a 3D
space-space-spectral cube and a 2D space-space cube that share spatial axes.
Let there be 10x20 spatial pixels and 30 pixels along the spectral axis.
>>> import numpy as np
>>> from astropy.wcs import WCS
>>> from ndcube import NDCube
>>> # Define observations NDCube.
>>> data = np.ones((10, 20, 30)) # dummy data
>>> obs_wcs_dict = {
... 'CTYPE1': 'WAVE ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 30,
... 'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 20,
... 'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 10}
>>> obs_wcs = WCS(obs_wcs_dict)
>>> obs_cube = NDCube(data, obs_wcs)
>>> # Define derived linewidth NDCube
>>> linewidth_data = np.ones((10, 20)) / 2 # dummy data
>>> linewidth_wcs_dict = {
... 'CTYPE1': 'HPLT-TAN', 'CUNIT1': 'deg', 'CDELT1': 0.5, 'CRPIX1': 2, 'CRVAL1': 0.5, 'NAXIS1': 20,
... 'CTYPE2': 'HPLN-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.4, 'CRPIX2': 2, 'CRVAL2': 1, 'NAXIS2': 10}
>>> linewidth_wcs = WCS(linewidth_wcs_dict)
>>> linewidth_cube = NDCube(linewidth_data, linewidth_wcs)
Combine these ND objects into an NDCollection
by supplying a sequence of
(key, value)
pairs in the same way that you initialize and dictionary.
>>> from ndcube import NDCollection
>>> my_collection = NDCollection([("observations", obs_cube), ("linewidths", linewidth_cube)])
Data Access¶
Key Access¶
To access each ND object in my_collection
we can index with the name of the desired object,
just like a dict
:
>>> my_collection["observations"]
And just like a dict
we can see the different names available using the keys
method:
>>> my_collection.keys()
dict_keys(['observations', 'linewidths'])
Aligned Axes & Slicing¶
Aligned Axes¶
NDCollection
is more powerful than a simple dictionary because it
allows us to link common aligned axes between the ND objects.
In our example above, the linewidth object’s axes are aligned with the
first two axes of observation object. Let’s instantiate our collection again,
but this time declare those axes to be aligned.
>>> my_collection = NDCollection(
... [("observations", obs_cube), ("linewidths", linewidth_cube)], aligned_axes=(0, 1))
We can see which axes are aligned by inpecting the aligned_axes
attribute:
>>> my_collection.aligned_axes
{'observations': (0, 1), 'linewidths': (0, 1)}
As you can see, this gives us the aligned axes for each ND object separately.
We should read this as the 0th axes of both ND objects are aligned, as are the
1st axes of both objects.
Because each ND object’s set of aligned axes is stored separately,
aligned axes do not have to be in the same order in both objects.
Let’s say we reversed the axes of our linewidths
ND object for some reason:
>>> linewidth_wcs_dict_reversed = {
... 'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 20,
... 'CTYPE1': 'HPLN-TAN', 'CUNIT1': 'deg', 'CDELT1': 0.4, 'CRPIX1': 2, 'CRVAL1': 1, 'NAXIS1': 10}
>>> linewidth_wcs_reversed = WCS(linewidth_wcs_dict_reversed)
>>> linewidth_cube_reversed = NDCube(linewidth_data.transpose(), linewidth_wcs_reversed)
We can still define an NDCollection
with aligned axes by supplying
a tuple of tuples, giving the aligned axes of each ND object separately.
In this case, the 0th axis of the observations
object is aligned with the 1st
axis of the linewidths
object and vice versa.
>>> my_collection_reversed = NDCollection(
... [("observations", obs_cube), ("linewidths", linewidth_cube_reversed)],
... aligned_axes=((0, 1), (1, 0)))
>>> my_collection_reversed.aligned_axes
{'observations': (0, 1), 'linewidths': (1, 0)}
Aligned axes must have the same lengths.
We can see the lengths of the aligned axes by using the aligned_dimensions
property.
>>> my_collection.aligned_dimensions
<Quantity [10., 20.] pix>
Note that this only tells us the lengths of the aligned axes. To see the
lengths of the non-aligned axes, e.g. the spectral axis of the observations
object, you must inspect that ND object individually.
We can also see the physical properties to which the aligned axes correspond
by using the aligned_world_axis_physical_types
property.
>>> my_collection.aligned_world_axis_physical_types
('custom:pos.helioprojective.lon', 'custom:pos.helioprojective.lat')
Note that this method simply returns the world physical axis types of one of the ND objects. However, there is no requirement that all aligned axes must represent the same physical types. They just have to be the same length.
Slicing¶
Defining aligned axes enables us to slice those axes of all the ND objects in the collection by using the standard Python slicing API.
>>> sliced_collection = my_collection[1:3, 3:8]
>>> sliced_collection.keys()
dict_keys(['observations', 'linewidths'])
>>> sliced_collection.aligned_dimensions
<Quantity [2., 5.] pix>
Note that we still have the same number of ND objects, but both have been sliced using the inputs provided by the user. Also note that slicing takes account of and updates the aligned axis information. Therefore a self-consistent result would be obtained even if the aligned axes are not in order.
>>> sliced_collection_reversed = my_collection_reversed[1:3, 3:8]
>>> sliced_collection_reversed.keys()
dict_keys(['observations', 'linewidths'])
>>> sliced_collection_reversed.aligned_dimensions
<Quantity [2., 5.] pix>
Editing NDCollection¶
Because NDCollection
inherits from dict
, we can edit the
collection using many of the same methods.
These have the same or analagous APIs to the dict
versions and
include del
, pop
, and update
.
Some dict
methods may not be implemented on NDCollection
if they are not consistent with its design.