Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed IndexedData to properly handle world coordinates #2081

Merged
merged 5 commits into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ v0.16.0 (unreleased)

* Added a ``DataCollection.clear()`` method to remove all datasets. [#2079]

* Improved ``IndexedData`` so that world coordinates can now be shown. [#2081]

v0.15.6 (2019-08-22)
--------------------

Expand Down
16 changes: 4 additions & 12 deletions glue/core/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,25 +158,17 @@ def __init__(self, matrix, units=None, labels=None):

def pixel_to_world_values(self, *pixel):
scalar = np.all([np.isscalar(p) for p in pixel])
pixel = np.vstack(np.broadcast_arrays(*(list(pixel) + [np.ones(np.shape(pixel[0]))])))
pixel = np.array(np.broadcast_arrays(*(list(pixel) + [np.ones(np.shape(pixel[0]))])))
pixel = np.moveaxis(pixel, 0, -1)
world = np.matmul(pixel, self._matrix.T)
world = tuple(np.moveaxis(world, -1, 0))[:-1]
if scalar:
return tuple(w[0] for w in world)
else:
return world
return tuple(np.moveaxis(world, -1, 0))[:-1]

def world_to_pixel_values(self, *world):
scalar = np.all([np.isscalar(w) for w in world])
world = np.vstack(np.broadcast_arrays(*(list(world) + [np.ones(np.shape(world[0]))])))
world = np.array(np.broadcast_arrays(*(list(world) + [np.ones(np.shape(world[0]))])))
world = np.moveaxis(world, 0, -1)
pixel = np.matmul(world, self._matrix_inv.T)
pixel = tuple(np.moveaxis(pixel, -1, 0))[:-1]
if scalar:
return tuple(p[0] for p in pixel)
else:
return pixel
return tuple(np.moveaxis(pixel, -1, 0))[:-1]

@property
def world_axis_names(self):
Expand Down
35 changes: 34 additions & 1 deletion glue/core/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,16 @@ class BaseCartesianData(BaseData, metaclass=abc.ABCMeta):
at.
"""

def __init__(self):
def __init__(self, coords=None):
super(BaseCartesianData, self).__init__()
self._coords = coords

@property
def coords(self):
"""
The coordinates object for the data.
"""
return self._coords

@abc.abstractproperty
def shape(self):
Expand Down Expand Up @@ -421,6 +429,12 @@ def get_data(self, cid, view=None):
shape = tuple(-1 if i == cid.axis else 1 for i in range(self.ndim))
pix = np.arange(self.shape[cid.axis], dtype=float).reshape(shape)
return broadcast_to(pix, self.shape)[view]
elif cid in self.world_component_ids:
comp = self.world_components[cid]
if view is not None:
result = comp[view]
else:
result = comp.data
else:
raise IncompatibleAttribute(cid)

Expand Down Expand Up @@ -553,6 +567,25 @@ def __getitem__(self, key):
def _ipython_key_completions_(self):
return [cid.label for cid in self.components]

@property
def world_component_ids(self):
"""
A list of :class:`~glue.core.component_id.ComponentID` giving all
world coordinate component IDs in the data
"""
if self.coords is None:
return []
elif not hasattr(self, '_world_component_ids'):
self._world_component_ids = []
self._world_components = {}
for i in range(self.ndim):
comp = CoordinateComponent(self, i, world=True)
label = axis_label(self.coords, i)
cid = ComponentID(label, parent=self)
self._world_component_ids.append(cid)
self._world_components[cid] = comp
return self._world_component_ids


class Data(BaseCartesianData):
"""
Expand Down
20 changes: 19 additions & 1 deletion glue/core/data_derived.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from glue.core.subset import SliceSubsetState
from glue.core.component_id import ComponentID

from astropy.wcs.wcsapi import SlicedLowLevelWCS


class DerivedData(BaseCartesianData):
"""
Expand Down Expand Up @@ -42,10 +44,17 @@ def __init__(self, original_data, indices):
raise ValueError("The 'indices' tuple should have length {0}"
.format(original_data.ndim))

if hasattr(original_data, 'coords'):
if original_data.coords is None:
self._coords = None
else:
slices = [slice(None) if idx is None else idx for idx in indices]
self._coords = SlicedLowLevelWCS(original_data.coords, slices)

self._original_data = original_data
self.indices = indices
self._cid_to_original_cid = {}
self._original_cid_to_cid = {}
self.indices = indices

@property
def indices(self):
Expand Down Expand Up @@ -84,6 +93,15 @@ def indices(self, value):
if self._indices[idim] is None:
self._original_pixel_cids.append(self._original_data.pixel_component_ids[idim])

# Construct a list of original world component IDs
self._original_world_cids = []
if len(self._original_data.world_component_ids) > 0:
idim_new = 0
for idim in range(self._original_data.ndim):
if self._indices[idim] is None:
self._cid_to_original_cid[self.world_component_ids[idim_new]] = self._original_data.world_component_ids[idim]
idim_new += 1

# Tell glue that the data has changed
if changed and self.hub is not None:
msg = NumericalDataChangedMessage(self)
Expand Down
13 changes: 8 additions & 5 deletions glue/core/data_factories/pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ def pandas_read_table(path, **kwargs):
:returns: :class:`glue.core.data.Data` object
"""
try:
from pandas.io.common import CParserError
except ImportError: # pragma: no cover
from pandas.errors import ParserError
except ImportError:
try:
from pandas.parser import CParserError
from pandas.io.common import CParserError as ParserError
except ImportError: # pragma: no cover
from pandas._parser import CParserError
try:
from pandas.parser import CParserError as ParserError
except ImportError: # pragma: no cover
from pandas._parser import CParserError as ParserError

# iterate over common delimiters to search for best option
delimiters = kwargs.pop('delimiter', [None] + list(',|\t '))
Expand All @@ -87,7 +90,7 @@ def pandas_read_table(path, **kwargs):

return panda_process(indf)

except CParserError:
except ParserError:
continue

if fallback is not None:
Expand Down
17 changes: 17 additions & 0 deletions glue/core/tests/test_coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,20 @@ def test_affine_invalid():
with pytest.raises(ValueError) as exc:
AffineCoordinates(matrix)
assert exc.value.args[0] == 'Last row of matrix should be zeros and a one'


def test_affine_highd():

# Test AffineCoordinates when higher dimensional objects are transformed

matrix = np.array([[2, 3, -1], [1, 2, 2], [0, 0, 1]])
coords = AffineCoordinates(matrix)

xp = np.ones((2, 4, 1, 2, 5))
yp = np.ones((2, 4, 1, 2, 5))

xw, yw = coords.pixel_to_world_values(xp, yp)
xpc, ypc = coords.world_to_pixel_values(xw, yw)

assert_allclose(xp, xpc)
assert_allclose(yp, ypc)
38 changes: 37 additions & 1 deletion glue/core/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ..component_id import ComponentID
from ..component_link import ComponentLink, CoordinateComponentLink, BinaryComponentLink
from ..coordinates import Coordinates, IdentityCoordinates
from ..data import Data, pixel_label
from ..data import Data, pixel_label, BaseCartesianData
from ..link_helpers import LinkSame
from ..data_collection import DataCollection
from ..exceptions import IncompatibleAttribute
Expand Down Expand Up @@ -918,3 +918,39 @@ def test_compute_histogram_log():
data = Data(x=np.ones(10), y=np.ones(10))
result = data.compute_histogram([data.id['x'], data.id['y']], range=[[1, 3], [-3, 5]], bins=[2, 3], log=[True, True])
assert result.shape == (2, 3) and np.sum(result) == 0


def test_base_cartesian_data_coords():

# Make sure that world_component_ids works in both the case where
# coords is not defined and when it is defined.

class CustomData(BaseCartesianData):

def get_kind(self):
pass

def compute_histogram(self):
pass

def compute_statistic(self):
pass

def get_mask(self):
pass

@property
def shape(self):
return (1, 4, 3)

@property
def main_components(self):
return []

data1 = CustomData()
assert len(data1.pixel_component_ids) == 3
assert len(data1.world_component_ids) == 0

data2 = CustomData(coords=IdentityCoordinates(n_dim=3))
assert len(data2.pixel_component_ids) == 3
assert len(data2.world_component_ids) == 3
19 changes: 19 additions & 0 deletions glue/core/tests/test_data_derived.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from glue.core.data import Data
from glue.core.data_collection import DataCollection
from glue.core.data_derived import IndexedData
from glue.core.coordinates import AffineCoordinates


class TestIndexedData:
Expand All @@ -20,6 +21,11 @@ def setup_class(self):
self.data = Data(x=x, y=y, label='Test data')
self.x_id, self.y_id = self.data.main_components

matrix = np.random.random((6, 6)) - 0.5
matrix[-1] = [0, 0, 0, 0, 0, 1]
self.data_with_coords = Data(x=x, y=y, label='Test data',
coords=AffineCoordinates(matrix=matrix))

self.subset_state = self.x_id >= 1200

def test_identity(self):
Expand Down Expand Up @@ -153,3 +159,16 @@ def test_pixel_component_ids(self):
derived = IndexedData(self.data, (None, 2, None, 4, None))
assert_equal(derived.get_data(derived.pixel_component_ids[1]),
self.data.get_data(self.data.pixel_component_ids[2])[:, 2, :, 4, :])

def test_world_component_ids(self):

derived = IndexedData(self.data, (None, 2, None, 4, None))
assert derived.world_component_ids == []

derived_with_coords = IndexedData(self.data_with_coords, (None, 2, None, 4, None))
assert_equal(derived_with_coords.get_data(derived_with_coords.world_component_ids[0]),
self.data_with_coords.get_data(self.data_with_coords.world_component_ids[0])[:, 2, :, 4, :])
assert_equal(derived_with_coords.get_data(derived_with_coords.world_component_ids[1]),
self.data_with_coords.get_data(self.data_with_coords.world_component_ids[2])[:, 2, :, 4, :])
assert_equal(derived_with_coords.get_data(derived_with_coords.world_component_ids[2]),
self.data_with_coords.get_data(self.data_with_coords.world_component_ids[4])[:, 2, :, 4, :])
43 changes: 43 additions & 0 deletions glue/viewers/image/qt/tests/test_data_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from glue.core.link_helpers import LinkSame
from glue.app.qt import GlueApplication
from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE
from glue.core.data_derived import IndexedData

from ..data_viewer import ImageViewer

Expand Down Expand Up @@ -868,3 +869,45 @@ def test_session_rgb_back_compat(self, protocol):
assert layer_state.color == 'b'

ga.close()


def test_indexed_data(capsys):

# Make sure that the image viewer works properly with IndexedData objects

data_4d = Data(label='hypercube_wcs',
x=np.random.random((3, 5, 4, 3)),
coords=WCS(naxis=4))

data_2d = IndexedData(data_4d, (2, None, 3, None))

application = GlueApplication()

session = application.session

hub = session.hub

data_collection = session.data_collection
data_collection.append(data_4d)
data_collection.append(data_2d)

viewer = application.new_data_viewer(ImageViewer)
viewer.add_data(data_2d)

assert viewer.state.x_att is data_2d.pixel_component_ids[1]
assert viewer.state.y_att is data_2d.pixel_component_ids[0]
assert viewer.state.x_att_world is data_2d.world_component_ids[1]
assert viewer.state.y_att_world is data_2d.world_component_ids[0]

process_events()

application.close()

# Some exceptions used to happen during callbacks, and these show up
# in stderr but don't interrupt the code, so we make sure here that
# nothing was printed to stdout nor stderr.

out, err = capsys.readouterr()

assert out.strip() == ""
assert err.strip() == ""