Repository: sunpy/ndcube

Issue:
Extra Coords not removed by NDCube slicing when their axis is sliced away
<!--
We know asking good questions takes effort, and we appreciate your time.
Thank you.

Please be aware that everyone has to follow our code of conduct:
https://github.com/sunpy/sunpy/blob/master/CODE_OF_CONDUCT.rst

These comments are hidden when you submit this github issue.

Please have a search on our GitHub repository to see if a similar issue has already been posted.
If a similar issue is closed, have a quick look to see if you are satisfied by the resolution.
If not please go ahead and open an issue!
-->

### Description
<!--
Provide a general description of the bug.
-->
NDCube slicing does not remove extra coords if their axis is sliced away.  Instead it is moved to the axis in the sliced cube with the equivalent axis number.

### Expected vs Actual behavior
<!--
What did you expect to happen.

What actually happened.
Was the output confusing or poorly described?
-->

### Steps to Reproduce
<!--
Please include code that reproduces the issue whenever possible.
The best reproductions are self-contained scripts with minimal dependencies.
-->

```python
>>> import numpy as np
>>> import astropy.wcs
>>> data = np.ones((3, 4, 5))
>>> wcs_input_dict = {
...     'CTYPE1': 'WAVE    ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 5,
...     'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 4,
...     'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 3}
>>> input_wcs = astropy.wcs.WCS(wcs_input_dict)

>>> from astropy.time import Time, TimeDelta
>>> base_time = Time('2000-01-01', format='fits', scale='utc')
>>> timestamps = Time([base_time + TimeDelta(60 * i, format='sec') for i in range(data.shape[0])])

>>> from ndcube import ExtraCoords
>>> my_extra_coords = ExtraCoords()
>>> my_extra_coords.add_coordinate('time', (0,), timestamps)

>>> from ndcube import NDCube
>>> my_cube = NDCube(data, input_wcs, extra_coords=my_extra_coords)

>>> my_cube.array_axis_physical_types
[('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
 ('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
 ('em.wl', 'time')]

>>> my_cube[:, :, 0].array_axis_physical_types
[('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
 ('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon', 'time')]
```

### System Details
<!--
We need to know the the package version you are using.
We provide a short function in sunpy (``sunpy.util.system_info()``) that will provide most of the below information.
-->
- SunPy Version:
- Astropy Version:
- Python Version:
- OS information:


Discussion:
pep8speaks: Hello @Cadair! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:











* In the file [`ndcube/tests/test_lookup_table_coord.py`](https://github.com/sunpy/ndcube/blob/74d095eeaa685e347cf062f7d5ad3dbf7a90da98/ndcube/tests/test_lookup_table_coord.py):

> [Line 524:101](https://github.com/sunpy/ndcube/blob/74d095eeaa685e347cf062f7d5ad3dbf7a90da98/ndcube/tests/test_lookup_table_coord.py#L524): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (101 > 100 characters)


Cadair: Some of the tests are janky because of gWCS versions and fixes... I will investigate.
Cadair: @DanRyanIrish This is good to go when you get a chance.
DanRyanIrish: Does this fix #417 as well?

Patch:
diff --git a/docs/coordinates.rst b/docs/coordinates.rst
index ee28587..737fbfe 100644
--- a/docs/coordinates.rst
+++ b/docs/coordinates.rst
@@ -204,7 +204,7 @@ If one does not exist for your coordinate, prepend the type with ``custom:``.
 
 .. code-block:: python
 
-  >>> my_cube.extra_coords.add('time', (2,), timestamps)  # TODO: Change the mapping to 0 Issue #342 resolved.
+  >>> my_cube.extra_coords.add('time', (0,), timestamps)
 
 An indefinite number of coordinates can be added in this way.
 The names of the coordinates can be accessed via the `~ndcube.ExtraCoords.keys` method.
@@ -303,7 +303,7 @@ The values of dropped coordinates at the position where the `~ndcube.NDCube` was
 
   >>> my_2d_cube = my_cube[:, :, 0]
   >>> my_2d_cube.array_axis_physical_types  # Note the wavelength axis is now gone.
-  [('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
+  [('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon', 'time'),
    ('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon')]
 
   >>> # The wavelength value at the slicing location is now in the GLobalCoords object.
diff --git a/docs/visualization.rst b/docs/visualization.rst
index 62482a4..67a1fef 100644
--- a/docs/visualization.rst
+++ b/docs/visualization.rst
@@ -248,19 +248,16 @@ Then we show how to extract and plot the data.
   >>> common_axis = 0
   >>> base_time = Time('2000-01-01', format='fits', scale='utc')
   >>> timestamps0 = Time([base_time + TimeDelta(60 * i, format='sec') for i in range(data0.shape[common_axis])])
-  >>> extra_coords0 = ExtraCoords()
-  >>> extra_coords0.add('time', 2, timestamps0)
   >>> timestamps1 = Time([base_time + TimeDelta(60 * (i+1), format='sec') for i in range(data1.shape[common_axis])])
-  >>> extra_coords1 = ExtraCoords()
-  >>> extra_coords1.add('time', 2, timestamps1)
   >>> timestamps2 = Time([base_time + TimeDelta(60 * (i+1), format='sec') for i in range(data2.shape[common_axis])])
-  >>> extra_coords2 = ExtraCoords()
-  >>> extra_coords2.add('time', 2, timestamps2)
 
   >>> # Define the cubes
-  >>> cube0 = NDCube(data0, wcs=wcs, extra_coords=extra_coords0)
-  >>> cube1 = NDCube(data1, wcs=wcs, extra_coords=extra_coords1)
-  >>> cube2 = NDCube(data2, wcs=wcs, extra_coords=extra_coords2)
+  >>> cube0 = NDCube(data0, wcs=wcs)
+  >>> cube0.extra_coords.add('time', 0, timestamps0)
+  >>> cube1 = NDCube(data1, wcs=wcs)
+  >>> cube1.extra_coords.add('time', 0, timestamps1)
+  >>> cube2 = NDCube(data2, wcs=wcs)
+  >>> cube2.extra_coords.add('time', 0, timestamps2)
 
   >>> # Define the sequence
   >>> my_sequence = NDCubeSequence([cube0, cube1, cube2], common_axis=common_axis)
diff --git a/ndcube/extra_coords/extra_coords.py b/ndcube/extra_coords/extra_coords.py
index 4acf61d..03acb60 100644
--- a/ndcube/extra_coords/extra_coords.py
+++ b/ndcube/extra_coords/extra_coords.py
@@ -1,14 +1,17 @@
 import abc
 from typing import Any, Tuple, Union, Iterable
 from numbers import Integral
-from functools import reduce
+from functools import reduce, partial
 
 import astropy.units as u
+import numpy as np
 from astropy.coordinates import SkyCoord
 from astropy.time import Time
-from astropy.wcs.wcsapi import BaseHighLevelWCS, BaseLowLevelWCS
+from astropy.wcs.wcsapi import BaseHighLevelWCS
 from astropy.wcs.wcsapi.wrappers.sliced_wcs import SlicedLowLevelWCS, sanitize_slices
 
+from ndcube.utils.wcs import convert_between_array_and_pixel_axes
+
 from .lookup_table_coord import (BaseTableCoordinate, MultipleTableCoordinate, QuantityTableCoordinate,
                                  SkyCoordTableCoordinate, TimeTableCoordinate)
 
@@ -33,14 +36,6 @@ class ExtraCoordsABC(abc.ABC):
        of length equal to the number of pixel dimensions in the extra coords.
 
     """
-
-    @abc.abstractmethod
-    def __init__(self,
-                 *,
-                 wcs: BaseLowLevelWCS = None,
-                 mapping: Iterable[Tuple[int, int]] = None):
-        pass
-
     @abc.abstractmethod
     def add(self,
             name: str,
@@ -125,23 +120,20 @@ class ExtraCoords(ExtraCoordsABC):
        of length equal to the number of pixel dimensions in the extra coords.
 
     """
-    def __init__(self, *, wcs=None, mapping=None):
-        if (wcs is None and mapping is not None) or (wcs is not None and mapping is None):
-            raise ValueError("Either both WCS and mapping have to be specified together or neither.")
-
-        super().__init__(wcs=wcs, mapping=mapping)
+    def __init__(self, ndcube=None):
+        super().__init__()
 
         # Setup private attributes
         self._wcs = None
         self._mapping = None
+
         # Lookup tables is a list of (pixel_dim, LookupTableCoord) to allow for
         # one pixel dimension having more than one lookup coord.
         self._lookup_tables = list()
         self._dropped_tables = list()
 
-        # Set values using the setters for validation
-        self.wcs = wcs
-        self.mapping = mapping
+        # We need a reference to the parent NDCube
+        self._ndcube = ndcube
 
     @classmethod
     def from_lookup_tables(cls, names, pixel_dimensions, lookup_tables):
@@ -234,8 +226,12 @@ class ExtraCoords(ExtraCoordsABC):
         if not self._lookup_tables:
             return tuple()
 
+        # The mapping is from the array index (position in the list) to the
+        # pixel dimensions (numbers in the list)
         lts = [list([lt[0]] if isinstance(lt[0], Integral) else lt[0]) for lt in self._lookup_tables]
-        return tuple(reduce(list.__add__, lts))
+        converter = partial(convert_between_array_and_pixel_axes, naxes=len(self._ndcube.dimensions))
+        pixel_indicies = [list(converter(np.array(ids))) for ids in lts]
+        return tuple(reduce(list.__add__, pixel_indicies))
 
     @mapping.setter
     def mapping(self, mapping):
@@ -341,7 +337,10 @@ class ExtraCoords(ExtraCoordsABC):
 
         new_mapping = [self.mapping[i] for i, subitem in enumerate(item) if not isinstance(subitem, Integral)]
 
-        return type(self)(wcs=subwcs, mapping=new_mapping)
+        new_ec = type(self)()
+        new_ec.wcs = subwcs
+        new_ec.mapping = new_mapping
+        return new_ec
 
     def __getitem__(self, item):
         # docstring in ABC
diff --git a/ndcube/mixins/ndslicing.py b/ndcube/mixins/ndslicing.py
index 3414487..7efbc96 100644
--- a/ndcube/mixins/ndslicing.py
+++ b/ndcube/mixins/ndslicing.py
@@ -19,39 +19,10 @@ class NDCubeSlicingMixin(NDSlicingMixin):
         if item is None or (isinstance(item, tuple) and None in item):
             raise IndexError("None indices not supported")
 
-        sliced_cube = super().__getitem__(item)
-        sliced_cube._global_coords._internal_coords = self._global_coords._internal_coords
-        return sliced_cube
-
-    def _slice(self, item):
-        """Construct a set of keyword arguments to initialise a new (sliced)
-        instance of the class. This method is called in
-        `astropy.nddata.mixins.NDSlicingMixin.__getitem__`.
-
-        This method extends the `~astropy.nddata.mixins.NDSlicingMixin` method
-        to add support for  ``extra_coords`` and overwrites the astropy
-        handling of wcs slicing.
-
-        Parameters
-        ----------
-        item : slice
-            The slice passed to ``__getitem__``. Note that the item parameter corresponds
-            to numpy ordering, keeping with the convention for NDCube.
-
-        Returns
-        -------
-        dict :
-            Containing all the attributes after slicing - ready to
-            use them to create ``self.__class__.__init__(**kwargs)`` in
-            ``__getitem__``.
-        """
-
         item = tuple(sanitize_slices(item, len(self.dimensions)))
-        kwargs = super()._slice(item)
-
-        # Store the original dimension of NDCube object before slicing
-        len(self.dimensions)
+        sliced_cube = super().__getitem__(item)
 
-        kwargs['extra_coords'] = self.extra_coords[item]
+        sliced_cube._global_coords._internal_coords = self.global_coords._internal_coords
+        sliced_cube._extra_coords = self.extra_coords[item]
 
-        return kwargs
+        return sliced_cube
diff --git a/ndcube/ndcube.py b/ndcube/ndcube.py
index e72bdf1..03d960c 100644
--- a/ndcube/ndcube.py
+++ b/ndcube/ndcube.py
@@ -128,6 +128,49 @@ class NDCubeABC(astropy.nddata.NDData, metaclass=NDCubeMetaClass):
         """
 
 
+class NDCubeLinkedDescriptor:
+    """
+    A descriptor which gives the property a reference to the cube to which it is attached.
+    """
+    def __init__(self, default_type):
+        self._default_type = default_type
+        self._property_name = None
+
+    def __set_name__(self, owner, name):
+        """
+        This function is called when the class the descriptor is attached to is initialized.
+
+        The *class* and not the instance.
+        """
+        # property name is the name of the attribute on the parent class
+        # pointing at an instance of this descriptor.
+        self._property_name = name
+        # attribute name is the name of the attribute on the parent class where
+        # the data is stored.
+        self._attribute_name = f"_{name}"
+
+    def __get__(self, obj, objtype=None):
+        if obj is None:
+            return
+
+        if getattr(obj, self._attribute_name, None) is None and self._default_type is not None:
+            self.__set__(obj, self._default_type)
+
+        return getattr(obj, self._attribute_name)
+
+    def __set__(self, obj, value):
+        if isinstance(value, self._default_type):
+            value._ndcube = obj
+        elif issubclass(value, self._default_type):
+            value = value(obj)
+        else:
+            raise ValueError(
+                f"Unable to set value for {self._property_name} it should "
+                f"be an instance or subclass of {self._default_type}")
+
+        setattr(obj, self._attribute_name, value)
+
+
 class NDCubeBase(NDCubeSlicingMixin, NDCubeABC):
     """
     Class representing N-D data described by a single array and set of WCS transformations.
@@ -174,9 +217,12 @@ class NDCubeBase(NDCubeSlicingMixin, NDCubeABC):
         Default is False.
 
     """
+    # Instances of Extra and Global coords are managed through descriptors
+    _extra_coords = NDCubeLinkedDescriptor(ExtraCoords)
+    _global_coords = NDCubeLinkedDescriptor(GlobalCoords)
 
     def __init__(self, data, wcs=None, uncertainty=None, mask=None, meta=None,
-                 unit=None, extra_coords=None, copy=False, **kwargs):
+                 unit=None, copy=False, **kwargs):
 
         super().__init__(data, wcs=wcs, uncertainty=uncertainty, mask=mask,
                          meta=meta, unit=unit, copy=copy, **kwargs)
@@ -185,29 +231,19 @@ class NDCubeBase(NDCubeSlicingMixin, NDCubeABC):
         if self.wcs is None:
             raise TypeError("The WCS argument can not be None.")
 
-        # Format extra coords.
-        if not extra_coords:
-            # Get existing extra_coords if initializing from an NDCube
-            if hasattr(data, "extra_coords"):
-                extra_coords = data.extra_coords
-            else:
-                extra_coords = ExtraCoords()
-
-        if not isinstance(extra_coords, ExtraCoords):
-            raise TypeError("The extra_coords argument must be a ndcube.ExtraCoords object.")
+        # Get existing extra_coords if initializing from an NDCube
+        if hasattr(data, "extra_coords"):
+            extra_coords = data.extra_coords
+            if copy:
+                extra_coords = deepcopy(extra_coords)
+            self._extra_coords = extra_coords
 
         # Get existing global_coords if initializing from an NDCube
         if hasattr(data, "global_coords"):
             global_coords = data._global_coords
-        else:
-            global_coords = GlobalCoords(self)
-
-        if copy:
-            extra_coords = deepcopy(extra_coords)
-            global_coords = deepcopy(global_coords)
-
-        self._extra_coords = extra_coords
-        self._global_coords = global_coords
+            if copy:
+                global_coords = deepcopy(global_coords)
+            self._global_coords = global_coords
 
     @property
     def extra_coords(self):
diff --git a/ndcube/utils/misc.py b/ndcube/utils/misc.py
index ad2d687..3c6c26c 100644
--- a/ndcube/utils/misc.py
+++ b/ndcube/utils/misc.py
@@ -4,8 +4,6 @@ from functools import wraps
 import astropy.units as u
 from astropy.wcs.wcsapi import BaseHighLevelWCS
 
-from ndcube.extra_coords import ExtraCoords
-
 __all__ = ['sanitise_wcs', 'unique_sorted']
 
 
@@ -29,6 +27,9 @@ def sanitise_wcs(func):
     to the dimensionality of the array. It will finally verify that the object
     passed is a HighLevelWCS object, or an ExtraCoords object.
     """
+    # This needs to be here to prevent a circular import
+    from ndcube.extra_coords import ExtraCoords
+
     @wraps(func)
     def wcs_wrapper(*args, **kwargs):
         sig = inspect.signature(func)
diff --git a/ndcube/utils/wcs.py b/ndcube/utils/wcs.py
index d14c456..25a0e47 100644
--- a/ndcube/utils/wcs.py
+++ b/ndcube/utils/wcs.py
@@ -74,9 +74,9 @@ def convert_between_array_and_pixel_axes(axis, naxes):
     """
     # Check type of input.
     if not isinstance(axis, np.ndarray):
-        raise TypeError("input must be of array type. Got type: {type(axis)}")
+        raise TypeError(f"input must be of array type. Got type: {type(axis)}")
     if axis.dtype.char not in np.typecodes['AllInteger']:
-        raise TypeError("input dtype must be of int type.  Got dtype: {axis.dtype})")
+        raise TypeError(f"input dtype must be of int type.  Got dtype: {axis.dtype})")
     # Convert negative indices to positive equivalents.
     axis[axis < 0] += naxes
     if any(axis > naxes - 1):


Test patch:
diff --git a/ndcube/conftest.py b/ndcube/conftest.py
index 38e7646..d80fd4f 100644
--- a/ndcube/conftest.py
+++ b/ndcube/conftest.py
@@ -44,8 +44,8 @@ def gen_ndcube_3d_l_ln_lt_ectime(wcs_3d_lt_ln_l, time_axis, time_base, global_co
     cube = NDCube(data_cube,
                   wcs_3d_lt_ln_l,
                   mask=mask,
-                  uncertainty=data_cube,
-                  extra_coords=extra_coords)
+                  uncertainty=data_cube)
+    cube._extra_coords = extra_coords
 
     if global_coords:
         cube._global_coords = global_coords
@@ -300,7 +300,9 @@ def ndcube_4d_mask(wcs_4d_t_l_lt_ln):
 def ndcube_4d_extra_coords(wcs_4d_t_l_lt_ln, simple_extra_coords_3d):
     shape = (5, 8, 10, 12)
     data_cube = data_nd(shape)
-    return NDCube(data_cube, wcs=wcs_4d_t_l_lt_ln, extra_coords=simple_extra_coords_3d)
+    cube = NDCube(data_cube, wcs=wcs_4d_t_l_lt_ln)
+    cube._extra_coords = simple_extra_coords_3d
+    return cube
 
 
 @pytest.fixture
@@ -326,13 +328,14 @@ def ndcube_3d_ln_lt_l(wcs_3d_l_lt_ln, simple_extra_coords_3d):
     wcs_3d_l_lt_ln.array_shape = shape
     data = data_nd(shape)
     mask = data > 0
-    return NDCube(
+    cube = NDCube(
         data,
         wcs_3d_l_lt_ln,
         mask=mask,
         uncertainty=data,
-        extra_coords=simple_extra_coords_3d
     )
+    cube._extra_coords = simple_extra_coords_3d
+    return cube
 
 
 @pytest.fixture
@@ -340,13 +343,14 @@ def ndcube_3d_rotated(wcs_3d_ln_lt_t_rotated, simple_extra_coords_3d):
     data_rotated = np.array([[[1, 2, 3, 4, 6], [2, 4, 5, 3, 1], [0, -1, 2, 4, 2], [3, 5, 1, 2, 0]],
                              [[2, 4, 5, 1, 3], [1, 5, 2, 2, 4], [2, 3, 4, 0, 5], [0, 1, 2, 3, 4]]])
     mask_rotated = data_rotated >= 0
-    return NDCube(
+    cube = NDCube(
         data_rotated,
         wcs_3d_ln_lt_t_rotated,
         mask=mask_rotated,
         uncertainty=data_rotated,
-        extra_coords=simple_extra_coords_3d
     )
+    cube._extra_coords = simple_extra_coords_3d
+    return cube
 
 
 @pytest.fixture
diff --git a/ndcube/tests/test_extra_coords.py b/ndcube/tests/test_extra_coords.py
index 097d970..3885f97 100644
--- a/ndcube/tests/test_extra_coords.py
+++ b/ndcube/tests/test_extra_coords.py
@@ -1,9 +1,11 @@
+from unittest.mock import MagicMock
+
 import astropy.units as u
 import gwcs
 import numpy as np
 import pytest
 from astropy.coordinates import SkyCoord
-from astropy.time import Time
+from astropy.time import Time, TimeDelta
 from astropy.wcs import WCS
 
 from ndcube import NDCube
@@ -61,19 +63,15 @@ def test_empty_ec(wcs_1d_l):
 
 
 def test_exceptions(wcs_1d_l):
-    # Test fail when one of wcs or mapping specified
-    with pytest.raises(ValueError):
-        ExtraCoords(wcs=wcs_1d_l)
-
-    with pytest.raises(ValueError):
-        ExtraCoords(mapping=0)
-
     # Test unable to specify inconsistent dimensions and tables
     with pytest.raises(ValueError):
         ExtraCoords.from_lookup_tables(None, (0,), (0, 0))
 
     # Test unable to add to WCS EC
-    ec = ExtraCoords(wcs=wcs_1d_l, mapping=(0,))
+    ec = ExtraCoords()
+    ec.wcs = wcs_1d_l
+    ec.mapping = (0,)
+
     with pytest.raises(ValueError):
         ec.add(None, 0, None)
 
@@ -82,7 +80,10 @@ def test_exceptions(wcs_1d_l):
 
 
 def test_mapping_setter(wcs_1d_l, wave_lut):
-    ec = ExtraCoords(wcs=wcs_1d_l, mapping=(0,))
+    ec = ExtraCoords()
+    ec.wcs = wcs_1d_l
+    ec.mapping = (0,)
+
     with pytest.raises(AttributeError):
         ec.mapping = None
 
@@ -98,7 +99,10 @@ def test_mapping_setter(wcs_1d_l, wave_lut):
 
 
 def test_wcs_setter(wcs_1d_l, wave_lut):
-    ec = ExtraCoords(wcs=wcs_1d_l, mapping=(0,))
+    ec = ExtraCoords()
+    ec.wcs = wcs_1d_l
+    ec.mapping = (0,)
+
     with pytest.raises(AttributeError):
         ec.wcs = None
 
@@ -114,7 +118,9 @@ def test_wcs_setter(wcs_1d_l, wave_lut):
 
 
 def test_wcs_1d(wcs_1d_l):
-    ec = ExtraCoords(wcs=wcs_1d_l, mapping=(0,))
+    ec = ExtraCoords()
+    ec.wcs = wcs_1d_l
+    ec.mapping = (0,)
 
     assert ec.keys() == ('spectral',)
     assert ec.mapping == (0,)
@@ -129,11 +135,21 @@ def test_wcs_1d(wcs_1d_l):
     assert subec.wcs is None
 
 
+@pytest.fixture
+def extra_coords_wave(wave_lut):
+    cube = MagicMock()
+    cube.dimensions = [10] * u.pix
+    ec = ExtraCoords(cube)
+    ec.add("wave", 0, wave_lut)
+
+    return ec
+
+
 # Extra Coords from lookup tables
 
 # A single lookup along a dimension, i.e. Time along the second dim.
-def test_single_from_lut(wave_lut):
-    ec = ExtraCoords.from_lookup_tables(("wave",), (0,), (wave_lut,))
+def test_single_from_lut(extra_coords_wave):
+    ec = extra_coords_wave
     assert len(ec._lookup_tables) == 1
     assert ec.mapping == (0,)
     assert isinstance(ec.wcs, gwcs.WCS)
@@ -143,9 +159,13 @@ def test_single_from_lut(wave_lut):
 
 
 def test_two_1d_from_lut(time_lut):
+    cube = MagicMock()
+    cube.dimensions = [10] * u.pix
+    ec = ExtraCoords(cube)
+
     exposure_lut = range(10) * u.s
-    ec = ExtraCoords.from_lookup_tables(("time", "exposure_time"),
-                                        (0, 0), (time_lut, exposure_lut))
+    ec.add("time", 0, time_lut)
+    ec.add("exposure_time", 0, exposure_lut)
     assert len(ec._lookup_tables) == 2
     assert ec.mapping == (0, 0)
     assert isinstance(ec.wcs, gwcs.WCS)
@@ -155,10 +175,13 @@ def test_two_1d_from_lut(time_lut):
 
 
 def test_skycoord(skycoord_1d_lut):
-    ec = ExtraCoords()
+    cube = MagicMock()
+    cube.dimensions = [10, 10] * u.pix
+
+    ec = ExtraCoords(cube)
     ec.add(("lat", "lon"), (0, 1), skycoord_1d_lut, mesh=True)
     assert len(ec._lookup_tables) == 1
-    assert ec.mapping == (0, 1)
+    assert ec.mapping == (1, 0)
     assert isinstance(ec.wcs, gwcs.WCS)
     assert ec.wcs.pixel_n_dim == 2
     assert ec.wcs.world_n_dim == 2
@@ -166,7 +189,10 @@ def test_skycoord(skycoord_1d_lut):
 
 
 def test_skycoord_1_pixel(skycoord_1d_lut):
-    ec = ExtraCoords()
+    cube = MagicMock()
+    cube.dimensions = [10] * u.pix
+
+    ec = ExtraCoords(cube)
     ec.add(("lon", "lat"), 0, skycoord_1d_lut, mesh=False)
     assert len(ec._lookup_tables) == 1
     assert ec.mapping == (0,)
@@ -180,12 +206,17 @@ def test_skycoord_1_pixel(skycoord_1d_lut):
     assert sec.wcs.world_n_dim == 2
     assert sec.wcs.world_axis_names == ("lon", "lat")
 
+    assert isinstance(sec.wcs.pixel_to_world(0), SkyCoord)
+
 
 def test_skycoord_mesh_false(skycoord_2d_lut):
-    ec = ExtraCoords()
+    cube = MagicMock()
+    cube.dimensions = [10, 10] * u.pix
+
+    ec = ExtraCoords(cube)
     ec.add(("lat", "lon"), (0, 1), skycoord_2d_lut, mesh=False)
     assert len(ec._lookup_tables) == 1
-    assert ec.mapping == (0, 1)
+    assert ec.mapping == (1, 0)
     assert isinstance(ec.wcs, gwcs.WCS)
     assert ec.wcs.pixel_n_dim == 2
     assert ec.wcs.world_n_dim == 2
@@ -193,27 +224,32 @@ def test_skycoord_mesh_false(skycoord_2d_lut):
 
 
 def test_extra_coords_index(skycoord_2d_lut, time_lut):
-    ec = ExtraCoords()
+    cube = MagicMock()
+    cube.dimensions = [10, 10] * u.pix
+
+    ec = ExtraCoords(cube)
     ec.add(("lat", "lon"), (0, 1), skycoord_2d_lut, mesh=False)
     ec.add("exposure_time", (0,), time_lut)
     assert len(ec._lookup_tables) == 2
-    assert ec.mapping == (0, 1, 0)
+    assert ec.mapping == (1, 0, 1)
     assert isinstance(ec.wcs, gwcs.WCS)
     assert ec.wcs.pixel_n_dim == 3
     assert ec.wcs.world_n_dim == 3
     assert ec.wcs.world_axis_names == ("lat", "lon", "exposure_time")
 
     sub_ec = ec["lon"]
+    sub_ec._ndcube = cube
     assert len(sub_ec._lookup_tables) == 1
-    assert sub_ec.mapping == (0, 1)
+    assert sub_ec.mapping == (1, 0)
     assert isinstance(ec.wcs, gwcs.WCS)
     assert sub_ec.wcs.pixel_n_dim == 2
     assert sub_ec.wcs.world_n_dim == 2
     assert sub_ec.wcs.world_axis_names == ("lat", "lon")
 
     sub_ec = ec["exposure_time"]
+    sub_ec._ndcube = cube
     assert len(sub_ec._lookup_tables) == 1
-    assert sub_ec.mapping == (0,)
+    assert sub_ec.mapping == (1,)
     assert isinstance(ec.wcs, gwcs.WCS)
     assert sub_ec.wcs.pixel_n_dim == 1
     assert sub_ec.wcs.world_n_dim == 1
@@ -346,3 +382,25 @@ def test_slice_extra_1d_drop(time_lut, wave_lut):
     dwd.pop("world_axis_object_classes")
     assert dwd
     assert dwd["world_axis_units"] == ["nm"]
+
+
+def test_dropped_dimension_reordering():
+    data = np.ones((3, 4, 5))
+    wcs_input_dict = {
+        'CTYPE1': 'WAVE    ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 5,
+        'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 4,
+        'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 3}
+    input_wcs = WCS(wcs_input_dict)
+
+    base_time = Time('2000-01-01', format='fits', scale='utc')
+    timestamps = Time([base_time + TimeDelta(60 * i, format='sec') for i in range(data.shape[0])])
+
+    my_cube = NDCube(data, input_wcs)
+    my_cube.extra_coords.add('time', (0,), timestamps)
+
+    # If the argument to extra_coords.add is array index then it should end up
+    # in the first element of array_axis_physical_types
+    assert "time" in my_cube.array_axis_physical_types[0]
+
+    # When we slice out the dimension with the extra coord in it should go away.
+    assert "time" not in my_cube[0].array_axis_physical_types[0]
diff --git a/ndcube/tests/test_lookup_table_coord.py b/ndcube/tests/test_lookup_table_coord.py
index 04581cf..9bb1025 100644
--- a/ndcube/tests/test_lookup_table_coord.py
+++ b/ndcube/tests/test_lookup_table_coord.py
@@ -517,12 +517,12 @@ def test_mtc_dropped_quantity_table(lut_1d_time, lut_2d_distance_no_mesh):
     assert dwd
     assert all(len(value) == 2 for value in dwd.values())
 
-    assert dwd["world_axis_names"] == [None, None]
+    assert dwd["world_axis_names"] == ["", ""]
     assert all(isinstance(u, str) for u in dwd["world_axis_units"])
     assert dwd["world_axis_units"] == ["km", "km"]
     assert dwd["world_axis_physical_types"] == ["custom:SPATIAL", "custom:SPATIAL"]
-    assert dwd["world_axis_object_components"] == [("SPATIAL0", 0, "value"), ("SPATIAL1", 0, "value")]
-    assert wao_classes["SPATIAL0"][0] is u.Quantity
+    assert dwd["world_axis_object_components"] == [("SPATIAL", 0, "value"), ("SPATIAL1", 0, "value")]
+    assert wao_classes["SPATIAL"][0] is u.Quantity
     assert wao_classes["SPATIAL1"][0] is u.Quantity
     assert dwd["value"] == [0*u.km, 9*u.km]
 
@@ -566,6 +566,7 @@ def test_mtc_dropped_quantity_inside_table_no_mesh(lut_2d_distance_no_mesh):
 
     dwd = sub.dropped_world_dimensions
     assert isinstance(dwd, dict)
+    dwd.pop("world_axis_object_classes")
     assert not dwd
 
 
diff --git a/ndcube/tests/test_ndcube.py b/ndcube/tests/test_ndcube.py
index 0518daf..5ad2483 100644
--- a/ndcube/tests/test_ndcube.py
+++ b/ndcube/tests/test_ndcube.py
@@ -193,7 +193,7 @@ def test_axis_world_coords_complex_ec(ndcube_4d_ln_lt_l_t):
     data = np.arange(np.product(ec_shape)).reshape(ec_shape) * u.m / u.s
 
     # The lookup table has to be in world order so transpose it.
-    cube.extra_coords.add('velocity', (1, 2), data.T)
+    cube.extra_coords.add('velocity', (2, 1), data.T)
 
     coords = cube.axis_world_coords(wcs=cube.extra_coords)
     assert len(coords) == 1