Skip to content

Commit

Permalink
Merge pull request #238 from DanRyanIrish/ndcollection2
Browse files Browse the repository at this point in the history
NDCollection 2
  • Loading branch information
DanRyanIrish authored Mar 27, 2020
2 parents b216953 + 3062fd4 commit 07cf757
Show file tree
Hide file tree
Showing 10 changed files with 806 additions and 9 deletions.
1 change: 1 addition & 0 deletions changelog/238.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new NDCollection class for linking and manipulating partially or non-aligned NDCubes or NDCubeSequences.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ by a WCS (World Coordinate System) translation.
installation
ndcube
ndcubesequence
ndcollection
contributing
getting_help
api
Expand Down
201 changes: 201 additions & 0 deletions docs/ndcollection.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
.. _ndcollection:

============
NDCollection
============

`~ndcube.NDCollection` is a container class for grouping `~ndcube.NDCube` or
`~ndcube.NDCubeSequence` instances together.
It does not imply an ordered relationship between its constituent ND objects
like `~ndcube.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.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 `~ndcube.NDCubeSequence`.
This is where `~ndcube.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 `~ndcube.NDCollection` inherits from `dict`.

Initialization
--------------
To see how we initialize an `~ndcube.NDCollection`, let's first define a couple
of `~ndcube.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.

.. code-block:: python
>>> 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 `~ndcube.NDCollection` by supplying a sequence of
``(key, value)`` pairs in the same way that you initialize and dictionary.

.. code-block:: python
>>> 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`:

.. code-block:: python
>>> my_collection["observations"] # doctest: +SKIP
And just like a `dict` we can see the different names available using the ``keys`` method:

.. code-block:: python
>>> my_collection.keys()
dict_keys(['observations', 'linewidths'])
Aligned Axes & Slicing
**********************

Aligned Axes
^^^^^^^^^^^^

`~ndcube.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.

.. code-block:: python
>>> 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:

.. code-block:: python
>>> 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:

.. code-block:: python
>>> 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 `~ndcube.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.

.. code-block:: python
>>> 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.

.. code-block:: python
>>> 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.

.. code-block:: python
>>> 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.

.. code-block:: python
>>> 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.

.. code-block:: python
>>> 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 `~ndcube.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``, `~ndcube.NDCollection.pop`, and `~ndcube.NDCollection.update`.
Some `dict` methods may not be implemented on `~ndcube.NDCollection`
if they are not consistent with its design.
3 changes: 2 additions & 1 deletion ndcube/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

__minimum_python_version__ = "3.6"

__all__ = ['NDCube', 'NDCubeSequence']
__all__ = ['NDCube', 'NDCubeSequence', "NDCollection"]


class UnsupportedPythonError(Exception):
Expand All @@ -28,3 +28,4 @@ class UnsupportedPythonError(Exception):
# For egg_info test builds to pass, put package imports here.
from .ndcube import NDCube, NDCubeOrdered
from .ndcube_sequence import NDCubeSequence
from .ndcollection import NDCollection
Loading

0 comments on commit 07cf757

Please sign in to comment.