From 8e79aef493acd9570a6743de805721119d6fe5e8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 10 Oct 2016 15:53:05 +0100 Subject: [PATCH 1/2] Allow link functions/helpers to define a category using the ``category=`` argument (which defaults to ``General``), and make it possible to filter by category in the link editor. In addition, add a 3D Galactocentric <-> Galactic coordinates helper in the Astronomy category. --- CHANGES.md | 4 + glue/config.py | 31 +++---- glue/core/link_helpers.py | 22 ++--- glue/dialogs/link_editor/qt/link_equation.py | 60 ++++++------- glue/dialogs/link_editor/qt/link_equation.ui | 85 ++++++++++++------- .../qt/tests/test_link_equation.py | 4 +- .../coordinate_helpers/link_helpers.py | 53 +++++++++--- glue/tests/test_config.py | 8 -- glue/utils/qt/widget_properties.py | 2 +- 9 files changed, 142 insertions(+), 127 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5f3819298..8411e1543 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,10 @@ v0.9.0 (unreleased) * Improve support for spectral cubes. [#1075] +* Allow link functions/helpers to define a category using the ``category=`` + argument (which defaults to ``General``), and make it possible to filter + by category in the link editor. [#1141] + * Only show the 'waiting' cursor when glue is doing something. [#1097] * Make sure that the scatter layer artist style editor is shown when overplotting diff --git a/glue/config.py b/glue/config.py index 7163ff6a0..a7da8dd62 100644 --- a/glue/config.py +++ b/glue/config.py @@ -466,8 +466,10 @@ class LinkFunctionRegistry(Registry): """Stores functions to convert between quantities The members properety is a list of (function, info_string, - output_labels) namedtuples. `info_string` is describes what the - function does. `output_labels` is a list of names for each output. + output_labels) namedtuples. ``info_string`` describes what the + function does. ``output_labels`` is a list of names for each output. + ``category`` is a category in which the link funtion will appear (defaults + to 'General'). New link functions can be registered via @@ -478,18 +480,13 @@ def degrees2arcsec(degrees): Link functions are expected to receive and return numpy arrays """ - item = namedtuple('LinkFunction', 'function info output_labels') + item = namedtuple('LinkFunction', 'function info output_labels category') - def default_members(self): - from glue.core import link_helpers - return list(self.item(l, "", l.output_args) - for l in link_helpers.__LINK_FUNCTIONS__) - - def __call__(self, info="", output_labels=None): + def __call__(self, info="", output_labels=None, category='General'): out = output_labels or [] def adder(func): - self.add(self.item(func, info, out)) + self.add(self.item(func, info, out, category)) return func return adder @@ -516,7 +513,8 @@ class LinkHelperRegistry(Registry): The members property is a list of (object, info_string, input_labels) tuples. `Object` is the link helper. `info_string` describes what `object` does. `input_labels` is a list labeling - the inputs. + the inputs. ``category`` is a category in which the link funtion will appear + (defaults to 'General'). Each link helper takes a list of ComponentIDs as inputs, and returns an iterable object (e.g. list) of ComponentLinks. @@ -529,16 +527,11 @@ def new_helper(degree, arcsecond): return [ComponentLink([degree], arcsecond, using=lambda d: d*3600), ComponentLink([arcsecond], degree, using=lambda a: a/3600)] """ - item = namedtuple('LinkHelper', 'helper info input_labels') - - def default_members(self): - from glue.core.link_helpers import __LINK_HELPERS__ as helpers - return list(self.item(l, l.info_text, l.input_args) - for l in helpers) + item = namedtuple('LinkHelper', 'helper info input_labels category') - def __call__(self, info, input_labels): + def __call__(self, info, input_labels, category='General'): def adder(func): - self.add(self.item(func, info, input_labels)) + self.add(self.item(func, info, input_labels, category)) return func return adder diff --git a/glue/core/link_helpers.py b/glue/core/link_helpers.py index a44686ad7..0365e7f05 100644 --- a/glue/core/link_helpers.py +++ b/glue/core/link_helpers.py @@ -2,11 +2,6 @@ This module provides several classes and LinkCollection classes to assist in linking data. -The functions in this class (and stored in the __LINK_FUNCTIONS__ -list) define common coordinate transformations. They are meant to be -used for the `using` parameter in -:class:`glue.core.component_link.ComponentLink` instances. - The :class:`LinkCollection` class and its sublcasses are factories to create multiple ComponentLinks easily. They are meant to be passed to :meth:`~glue.core.data_collection.DataCollection.add_link()` @@ -14,6 +9,7 @@ from __future__ import absolute_import, division, print_function +from glue.config import link_function from glue.external import six from glue.core.data import ComponentID from glue.core.component_link import ComponentLink @@ -22,27 +18,19 @@ __all__ = ['LinkCollection', 'LinkSame', 'LinkTwoWay', 'MultiLink', 'LinkAligned'] -__LINK_FUNCTIONS__ = [] -__LINK_HELPERS__ = [] - +@link_function("Link conceptually identical components", + output_labels=['y']) def identity(x): return x -identity.output_args = ['y'] +@link_function("Convert between linear measurements and volume", + output_labels=['volume']) def lengths_to_volume(width, height, depth): - """Compute volume from linear measurements of a box""" - # included for demonstration purposes return width * height * depth -lengths_to_volume.output_args = ['area'] - -__LINK_FUNCTIONS__.append(identity) -__LINK_FUNCTIONS__.append(lengths_to_volume) - - class PartialResult(object): def __init__(self, func, index, name_prefix=""): diff --git a/glue/dialogs/link_editor/qt/link_equation.py b/glue/dialogs/link_editor/qt/link_equation.py index 6c1abb3ce..8d7412fd1 100644 --- a/glue/dialogs/link_editor/qt/link_equation.py +++ b/glue/dialogs/link_editor/qt/link_equation.py @@ -2,23 +2,30 @@ import os from inspect import getargspec -from collections import OrderedDict from qtpy import QtWidgets from qtpy import PYSIDE from glue import core +from glue.config import link_function, link_helper from glue.utils import nonpartial -from glue.utils.qt import load_ui, messagebox_on_error +from glue.utils.qt import load_ui, messagebox_on_error, update_combobox +from glue.utils.qt.widget_properties import CurrentComboTextProperty, CurrentComboDataProperty __all__ = ['LinkEquation'] +def get_function_name(item): + if hasattr(item, 'display') and item.display is not None: + return item.display + else: + return item.__name__ + + def function_label(function): """ Provide a label for a function :param function: A member from the glue.config.link_function registry """ - name = function.function.__name__ args = getargspec(function.function)[0] args = ', '.join(args) output = function.output_labels @@ -114,19 +121,13 @@ class LinkEquation(QtWidgets.QWidget): widget = LinkEquation() """ + category = CurrentComboTextProperty('_ui.category') + function = CurrentComboDataProperty('_ui.function') + def __init__(self, parent=None): super(LinkEquation, self).__init__(parent) - from glue.config import link_function, link_helper # Set up mapping of function/helper name -> function/helper tuple. For the helpers, we use the 'display' name if available. - def get_name(item): - if hasattr(item, 'display') and item.display is not None: - return item.display - else: - return item.__name__ - f = [f for f in link_function.members if len(f.output_labels) == 1] - self._functions = OrderedDict((get_name(l[0]), l) for l in - f + link_helper.members) self._argument_widgets = [] self.spacer = None self._output_widget = ArgumentWidget("") @@ -140,6 +141,8 @@ def get_name(item): self.setLayout(l) self._init_widgets() + self._populate_category_combo() + self.category = 'General' self._populate_function_combo() self._connect() self._setup_editor() @@ -189,27 +192,6 @@ def signature(self, inout): a.component_id = i self._output_widget.component_id = out - @property - def function(self): - """ The currently-selected function - - :rtype: A function or helper tuple - """ - fname = str(self._ui.function.currentText()) - func = self._functions[fname] - return func - - @function.setter - def function(self, val): - if hasattr(val[0], 'display') and val[0].display is not None: - name = val[0].display - else: - name = val[0].__name__ - pos = self._ui.function.findText(name) - if pos < 0: - raise KeyError("No function or helper found %s" % [val]) - self._ui.function.setCurrentIndex(pos) - @messagebox_on_error("Failed to create links") def links(self): """ Create ComponentLinks from the state of the widget @@ -245,6 +227,7 @@ def _connect(self): signal.connect(nonpartial(self._setup_editor)) signal.connect(nonpartial(self._update_add_enabled)) self._output_widget.editor.textChanged.connect(nonpartial(self._update_add_enabled)) + self._ui.category.currentIndexChanged.connect(self._populate_function_combo) def clear_inputs(self): for w in self._argument_widgets: @@ -312,8 +295,13 @@ def _clear_input_canvas(self): self._argument_widgets = [] + def _populate_category_combo(self): + f = [f for f in link_function.members if len(f.output_labels) == 1] + categories = sorted(set(l.category for l in f + link_helper.members)) + update_combobox(self._ui.category, list(zip(categories, categories))) + def _populate_function_combo(self): """ Add name of functions to function combo box """ - self._ui.function.clear() - for f in self._functions: - self._ui.function.addItem(f) + f = [f for f in link_function.members if len(f.output_labels) == 1] + functions = ((get_function_name(l[0]), l) for l in f + link_helper.members if l.category == self.category) + update_combobox(self._ui.function, functions) diff --git a/glue/dialogs/link_editor/qt/link_equation.ui b/glue/dialogs/link_editor/qt/link_equation.ui index 74b3f0c1d..05eae8c41 100644 --- a/glue/dialogs/link_editor/qt/link_equation.ui +++ b/glue/dialogs/link_editor/qt/link_equation.ui @@ -6,8 +6,8 @@ 0 0 - 466 - 605 + 714 + 336 @@ -24,36 +24,59 @@ 4 - + - - - - 14 - 50 - false - - - - Select a function - - - false - - - Qt::AlignCenter - - - - - - - Select a translation function to use - - - QComboBox::AdjustToMinimumContentsLength - - + + + + + Category: + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Function: + + + + + + + + 0 + 0 + + + + Select a translation function to use + + + QComboBox::AdjustToMinimumContentsLength + + + + diff --git a/glue/dialogs/link_editor/qt/tests/test_link_equation.py b/glue/dialogs/link_editor/qt/tests/test_link_equation.py index d9face050..87f4f4182 100644 --- a/glue/dialogs/link_editor/qt/tests/test_link_equation.py +++ b/glue/dialogs/link_editor/qt/tests/test_link_equation.py @@ -105,11 +105,11 @@ def test_select_function_helper(self): assert self.widget.function is member def test_select_invalid_function(self): - with pytest.raises(KeyError) as exc: + with pytest.raises(ValueError) as exc: def bad(x): pass self.widget.function = (bad, None, None) - assert exc.value.args[0].startswith('No function or helper found') + assert exc.value.args[0].startswith('Cannot find data') def test_make_link_function(self): widget = LinkEquation() diff --git a/glue/plugins/coordinate_helpers/link_helpers.py b/glue/plugins/coordinate_helpers/link_helpers.py index c4a81b758..7afeb82f3 100644 --- a/glue/plugins/coordinate_helpers/link_helpers.py +++ b/glue/plugins/coordinate_helpers/link_helpers.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function from astropy import units as u -from astropy.coordinates import ICRS, FK5, FK4, Galactic +from astropy.coordinates import ICRS, FK5, FK4, Galactic, Galactocentric from glue.core.link_helpers import MultiLink from glue.config import link_helper @@ -40,48 +40,75 @@ def backward(self, in_lon, in_lat): @link_helper('Link Galactic and FK5 (J2000) Equatorial coordinates', - input_labels=['l', 'b', 'ra (fk5)', 'dec (fk5)']) + input_labels=['l', 'b', 'ra (fk5)', 'dec (fk5)'], + category='Astronomy') class Galactic_to_FK5(BaseCelestialMultiLink): - display = "Celestial Coordinates: Galactic <-> FK5 (J2000)" + display = "Galactic <-> FK5 (J2000)" frame_in = Galactic frame_out = FK5 @link_helper('Link FK4 (B1950) and FK5 (J2000) Equatorial coordinates', - input_labels=['ra (fk4)', 'dec (fk4)', 'ra (fk5)', 'dec (fk5)']) + input_labels=['ra (fk4)', 'dec (fk4)', 'ra (fk5)', 'dec (fk5)'], + category='Astronomy') class FK4_to_FK5(BaseCelestialMultiLink): - display = "Celestial Coordinates: FK4 (B1950) <-> FK5 (J2000)" + display = "FK4 (B1950) <-> FK5 (J2000)" frame_in = FK4 frame_out = FK5 @link_helper('Link ICRS and FK5 (J2000) Equatorial coordinates', - input_labels=['ra (icrs)', 'dec (icrs)', 'ra (fk5)', 'dec (fk5)']) + input_labels=['ra (icrs)', 'dec (icrs)', 'ra (fk5)', 'dec (fk5)'], + category='Astronomy') class ICRS_to_FK5(BaseCelestialMultiLink): - display = "Celestial Coordinates: ICRS <-> FK5 (J2000)" + display = "ICRS <-> FK5 (J2000)" frame_in = ICRS frame_out = FK5 @link_helper('Link Galactic and FK4 (B1950) Equatorial coordinates', - input_labels=['l', 'b', 'ra (fk4)', 'dec (fk4)']) + input_labels=['l', 'b', 'ra (fk4)', 'dec (fk4)'], + category='Astronomy') class Galactic_to_FK4(BaseCelestialMultiLink): - display = "Celestial Coordinates: Galactic <-> FK4 (B1950)" + display = "Galactic <-> FK4 (B1950)" frame_in = Galactic frame_out = FK4 @link_helper('Link ICRS and FK4 (B1950) Equatorial coordinates', - input_labels=['ra (icrs)', 'dec (icrs)', 'ra (fk4)', 'dec (fk4)']) + input_labels=['ra (icrs)', 'dec (icrs)', 'ra (fk4)', 'dec (fk4)'], + category='Astronomy') class ICRS_to_FK4(BaseCelestialMultiLink): - display = "Celestial Coordinates: ICRS <-> FK4 (B1950)" + display = "ICRS <-> FK4 (B1950)" frame_in = ICRS frame_out = FK4 @link_helper('Link ICRS and Galactic coordinates', - input_labels=['ra (icrs)', 'dec (icrs)', 'l', 'b']) + input_labels=['ra (icrs)', 'dec (icrs)', 'l', 'b'], + category='Astronomy') class ICRS_to_Galactic(BaseCelestialMultiLink): - display = "Celestial Coordinates: ICRS <-> Galactic" + display = "ICRS <-> Galactic" frame_in = ICRS frame_out = Galactic + + +@link_helper('Link 3D Galactocentric and Galactic coordinates', + input_labels=['x (kpc)', 'y (kpc)', 'z (kpc)', 'l (deg)', 'b (deg)', 'distance (kpc)'], + category='Astronomy') +class GalactocentricToGalactic(MultiLink): + + display = "3D Galactocentric <-> Galactic" + + def __init__(self, x_id, y_id, z_id, l_id, b_id, d_id): + super(GalactocentricToGalactic, self).__init__(x_id, y_id, z_id, l_id, b_id, d_id) + self.create_links([x_id, y_id, z_id], [l_id, b_id, d_id], + self.forward, self.backward) + + def forward(self, x_kpc, y_kpc, z_kpc): + gal = Galactocentric(x=x_kpc * u.kpc, y=y_kpc * u.kpc, z=z_kpc * u.kpc).transform_to(Galactic) + return gal.l.degree, gal.b.degree, gal.distance.to(u.kpc).value + + def backward(self, l_deg, b_deg, d_kpc): + gal = Galactic(l=l_deg * u.deg, b=b_deg * u.deg, distance=d_kpc * u.kpc).transform_to(Galactocentric) + return gal.x.to(u.kpc).value, gal.y.to(u.kpc).value, gal.z.to(u.kpc).value diff --git a/glue/tests/test_config.py b/glue/tests/test_config.py index 1b84e5fd4..4275e8a32 100644 --- a/glue/tests/test_config.py +++ b/glue/tests/test_config.py @@ -24,14 +24,6 @@ class TestClient(object): assert TestClient in qt_client -def test_link_defaults(): - from ..core.link_helpers import __LINK_FUNCTIONS__ - assert len(__LINK_FUNCTIONS__) > 0 - - for l in __LINK_FUNCTIONS__: - assert l in [ll[0] for ll in link_function] - - def test_add_link_default(): @link_function(info='maps x to y', output_labels=['y']) def foo(x): diff --git a/glue/utils/qt/widget_properties.py b/glue/utils/qt/widget_properties.py index 076a39bcc..5eb190bf6 100644 --- a/glue/utils/qt/widget_properties.py +++ b/glue/utils/qt/widget_properties.py @@ -378,6 +378,6 @@ def _find_combo_data(widget, value): """ i = widget.findData(value) if i == -1: - raise ValueError("%s not found in combo box" % value) + raise ValueError("{0} not found in combo box".format(value)) else: return i From e54837048b761393a9fccd2443104d55be49aedc Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 10 Oct 2016 17:07:33 +0100 Subject: [PATCH 2/2] Fix test --- glue/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue/tests/test_config.py b/glue/tests/test_config.py index 4275e8a32..f6985c252 100644 --- a/glue/tests/test_config.py +++ b/glue/tests/test_config.py @@ -28,7 +28,7 @@ def test_add_link_default(): @link_function(info='maps x to y', output_labels=['y']) def foo(x): return 3 - val = (foo, 'maps x to y', ['y']) + val = (foo, 'maps x to y', ['y'], 'General') assert val in link_function