Skip to content

Commit

Permalink
Made it possible to now create InequalitySubsetStates for categorical…
Browse files Browse the repository at this point in the history
… components using e.g. d.id['a'] == 'string'
  • Loading branch information
astrofrog committed Oct 28, 2016
1 parent cd8d0f4 commit 258bf7a
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 36 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ v0.10.0 (unreleased)
v0.9.1 (unreleased)
-------------------

* Made it possible to now create InequalitySubsetStates for
categorical components using e.g. d.id['a'] == 'string'. [#1153]

* Fixed a bug that caused selections to not propagate properly between
linked images and cubes.

Expand Down
21 changes: 16 additions & 5 deletions glue/core/component_id.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
from __future__ import absolute_import, division, print_function

import operator

import numpy as np
import numbers

from glue.external import six
from glue.core.component_link import BinaryComponentLink
from glue.core.subset import InequalitySubsetState


__all__ = ['PixelComponentID', 'ComponentID', 'ComponentIDDict']
__all__ = ['PixelComponentID', 'ComponentID', 'ComponentIDDict', 'ComponentIDList']

# access to ComponentIDs via .item[name]

class ComponentIDList(list):

def __contains__(self, cid):
if isinstance(cid, six.string_types):
for c in self:
if cid == c.label:
return True
else:
return False
else:
return list.__contains__(self, cid)


class ComponentIDDict(object):

Expand Down Expand Up @@ -70,7 +81,7 @@ def __repr__(self):
return str(self._label)

def __eq__(self, other):
if np.issubsctype(type(other), np.number):
if isinstance(other, (numbers.Number, six.string_types)):
return InequalitySubsetState(self, other, operator.eq)
return other is self

Expand All @@ -79,7 +90,7 @@ def __eq__(self, other):
__hash__ = object.__hash__

def __ne__(self, other):
if np.issubsctype(type(other), np.number):
if isinstance(other, (numbers.Number, six.string_types)):
return InequalitySubsetState(self, other, operator.ne)
return other is not self

Expand Down
2 changes: 1 addition & 1 deletion glue/core/component_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self, comp_from, comp_to, using=None, inverse=None):
self.hidden = False # show in widgets?
self.identity = self._using is identity

if type(comp_from) is not list:
if not isinstance(comp_from, list):
raise TypeError("comp_from must be a list: %s" % type(comp_from))

if not all(isinstance(f, ComponentID) for f in self._from):
Expand Down
20 changes: 12 additions & 8 deletions glue/core/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from glue.core.util import split_component_view
from glue.core.hub import Hub
from glue.core.subset import Subset, SubsetState
from glue.core.component_id import ComponentIDList
from glue.core.component_link import ComponentLink, CoordinateComponentLink
from glue.core.exceptions import IncompatibleAttribute
from glue.core.visual import VisualAttributes
Expand Down Expand Up @@ -76,8 +77,8 @@ def __init__(self, label="", **kwargs):

# Components
self._components = OrderedDict()
self._pixel_component_ids = []
self._world_component_ids = []
self._pixel_component_ids = ComponentIDList()
self._world_component_ids = ComponentIDList()

self.id = ComponentIDDict(self)

Expand Down Expand Up @@ -330,11 +331,14 @@ def join_on_key(self, other, cid, cid_other):
"should contain a single component.")

def get_component_id(data, name):
cid = data.find_component_id(name)
if cid is None:
raise ValueError("ComponentID not found in %s: %s" %
(data.label, name))
return cid
if isinstance(name, ComponentID):
return name
else:
cid = data.find_component_id(name)
if cid is None:
raise ValueError("ComponentID not found in %s: %s" %
(data.label, name))
return cid

cid = tuple(get_component_id(self, name) for name in cid)
cid_other = tuple(get_component_id(other, name) for name in cid_other)
Expand Down Expand Up @@ -569,7 +573,7 @@ def component_ids(self):
"""
Equivalent to :attr:`Data.components`
"""
return list(self._components.keys())
return ComponentIDList(self._components.keys())

@contract(subset='isinstance(Subset)|None',
color='color|None',
Expand Down
1 change: 1 addition & 0 deletions glue/core/layer_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def disable_invalid_attributes(self, *attributes):
"""
if len(attributes) == 0:
self.disable('')
return

msg = ('Layer depends on attributes that '
'cannot be derived for %s:\n -%s' %
Expand Down
64 changes: 44 additions & 20 deletions glue/core/subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import numpy as np

from glue.external import six
from glue.external.six import PY3
from glue.core.roi import CategoricalROI
from glue.core.contracts import contract
Expand Down Expand Up @@ -253,18 +254,15 @@ def to_mask(self, view=None):
If present, the mask will pertain to the view and not the
entire dataset.
Returns:
A boolean numpy array, the same shape as the data, that
defines whether each element belongs to the subset.
"""
try:
return self.subset_state.to_mask(self.data, view)
mask = self.subset_state.to_mask(self.data, view)
return mask
except IncompatibleAttribute as exc:
try:
return self._to_mask_join(view)
except IncompatibleAttribute:
raise exc
return self._to_mask_join(view)

@contract(value=bool)
def do_broadcast(self, value):
Expand Down Expand Up @@ -477,6 +475,7 @@ def __xor__(self, other_state):

class RoiSubsetState(SubsetState):

@contract(xatt='isinstance(ComponentID)', yatt='isinstance(ComponentID)')
def __init__(self, xatt=None, yatt=None, roi=None):
super(RoiSubsetState, self).__init__()
self.xatt = xatt
Expand Down Expand Up @@ -928,15 +927,14 @@ def __init__(self, left, right, op):
operator.eq, operator.ne]
if op not in valid_ops:
raise TypeError("Invalid boolean operator: %s" % op)
if not isinstance(left, ComponentID) and not \
isinstance(left, numbers.Number) and not \
isinstance(left, ComponentLink):
raise TypeError("Input must be ComponenID or NumberType: %s"
if not isinstance(left, (ComponentID, numbers.Number,
ComponentLink, six.string_types)):
raise TypeError("Input must be ComponentID or NumberType or string: %s"
% type(left))
if not isinstance(right, ComponentID) and not \
isinstance(right, numbers.Number) and not \
isinstance(right, ComponentLink):
raise TypeError("Input must be ComponenID or NumberType: %s"

if not isinstance(left, (ComponentID, numbers.Number,
ComponentLink, six.string_types)):
raise TypeError("Input must be ComponentID or NumberType or string: %s"
% type(right))
self._left = left
self._right = right
Expand All @@ -956,13 +954,39 @@ def operator(self):

@memoize
def to_mask(self, data, view=None):
left = self._left
if not isinstance(self._left, numbers.Number):
left = data[self._left, view]

right = self._right
if not isinstance(self._right, numbers.Number):
right = data[self._right, view]
# FIXME: the default view in glue should be ... not None, because
# if x is a Numpy array, x[None] has one more dimension than x. For
# now we just fix this for the scope of this method.
if view is None:
view = ...

if isinstance(self._left, (numbers.Number, six.string_types)):
left = self._left
else:
try:
comp = data.get_component(self._left)
except IncompatibleAttribute:
left = data[self._left, view]
else:
if comp.categorical:
left = comp.labels[view]
else:
left = comp.data[view]

if isinstance(self._right, (numbers.Number, six.string_types)):
right = self._right
else:
try:
comp = data.get_component(self._right)
except IncompatibleAttribute:
right = data[self._right, view]
else:
if comp.categorical:
right = comp.labels[view]
else:
right = comp.data[view]


return self._operator(left, right)

Expand Down
4 changes: 2 additions & 2 deletions glue/core/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def test_range_roi_categorical(self):
d = Data(x=['a', 'b', 'c'], y=[1, 2, 3])
comp = d.get_component(d.id['x'])
roi = CategoricalROI(['b', 'c'])
s = comp.subset_from_roi('x', roi)
s = comp.subset_from_roi(d.id['x'], roi)
assert isinstance(s, CategoricalROISubsetState)
np.testing.assert_array_equal((s.roi.contains(['a', 'b', 'c'], None)),
[False, True, True])
Expand All @@ -389,7 +389,7 @@ def test_polygon_roi(self):
x_comp = d.get_component(d.id['x'])
y_comp = d.get_component(d.id['y'])
roi = PolygonalROI([0, 0, 2, 2], [0, 2, 2, 0])
s = x_comp.subset_from_roi('x', roi, other_comp=y_comp, other_att='y')
s = x_comp.subset_from_roi(d.id['x'], roi, other_comp=y_comp, other_att=d.id['y'])
assert isinstance(s, RoiSubsetState)
np.testing.assert_array_equal(s.to_mask(d), [True, True, False, False])

Expand Down
7 changes: 7 additions & 0 deletions glue/core/tests/test_subset.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ def test_inequality_state_str():
x = d.id['x']
y = d.id['y']

assert str(x == 'a') == '(x == a)'
assert str(x > 3) == '(x > 3)'
assert str(x < 2) == '(x < 2)'
assert str(x < y) == '(x < y)'
Expand Down Expand Up @@ -624,3 +625,9 @@ def test_save_element_subset_state():
state1 = ElementSubsetState(indices=[1, 3, 4])
state2 = clone(state1)
assert state2._indices == [1, 3, 4]


def test_inequality_subset_state_string():
d = Data(x=['a', 'b', 'c', 'b'])
state = d.id['x'] == 'b'
np.testing.assert_equal(state.to_mask(d), np.array([False, True, False, True]))

0 comments on commit 258bf7a

Please sign in to comment.