Source code for glue.core.link_helpers

"""
This module provides several classes and LinkCollection classes to
assist in linking data.

The :class:`LinkCollection` class and its sub-classes are factories to create
multiple ComponentLinks easily. They are meant to be passed to
:meth:`~glue.core.data_collection.DataCollection.add_link()`
"""

from __future__ import absolute_import, division, print_function

import types

from glue.config import link_function
from glue.external import six
from glue.core.data import ComponentID
from glue.core.component_link import ComponentLink

try:
    from inspect import getfullargspec
except ImportError:  # Python 2.7
    from inspect import getargspec as getfullargspec


__all__ = ['LinkCollection', 'LinkSame', 'LinkTwoWay', 'MultiLink',
           'LinkAligned', 'BaseMultiLink', 'ManualLinkCollection']


@link_function("Link conceptually identical components",
               output_labels=['y'])
def identity(x):
    return x


@link_function("Convert between linear measurements and volume",
               output_labels=['volume'])
def lengths_to_volume(width, height, depth):
    return width * height * depth


class PartialResult(object):

    def __init__(self, func, index, name_prefix=""):
        self.func = func
        self.index = index
        self.__name__ = '%s%s_%i' % (name_prefix, func.__name__, index + 1)

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)[self.index]

    def __gluestate__(self, context):
        return dict(func=context.do(self.func), index=self.index)

    @classmethod
    def __setgluestate__(cls, rec, context):
        return cls(context.object(rec['func']), rec['index'])


def _toid(arg):
    """Coerce the input to a ComponentID, if possible"""
    if isinstance(arg, ComponentID):
        return arg
    elif isinstance(arg, six.string_types):
        return ComponentID(arg)
    else:
        raise TypeError('Cannot be cast to a ComponentID: %s' % arg)


[docs]class LinkCollection(object): """ A collection of links between two datasets. Parameters ---------- data1 : `~glue.core.data.Data` The first dataset being linked data2 : `~glue.core.data.Data` The second dataset being linked cids1 : list of `~glue.core.component_id.ComponentID` The set of `~glue.core.component_id.ComponentID` in ``data1`` which can be used to parameterize the links. Note that the links can also use other IDs, but the ones defined here are the ones that can be modified through e.g. the graphical link editor. cids2 : list of `~glue.core.component_id.ComponentID` The set of `~glue.core.component_id.ComponentID` in ``data2``. This is defined as for ``cids1``. """ # The following is a short name to be used for the link, which is used # in e.g. drop-down menus in link editors. display = 'Collection of links' # The following can be a paragraph description explaining how the set # of links works description = '' # The following are lists of human-readable names for the component IDs # to be specified in the initializer. For this base class, these are # empty, but can be overridden in sub-classes. labels1 = [] labels2 = [] def __init__(self, data1=None, data2=None, cids1=None, cids2=None): self.cids1 = cids1 or [] self.cids2 = cids2 or [] if data1 is None: if len(self.cids1) == 0: self.data1 = None else: self.data1 = self.cids1[0].parent else: self.data1 = data1 if data2 is None: if len(self.cids2) == 0: self.data2 = None else: self.data2 = self.cids2[0].parent else: self.data2 = data2 self._links = [] def __iter__(self): for link in self._links: yield link def __len__(self): return len(self._links) def __getitem__(self, item): return self._links[item] def __contains__(self, cid): for link in self: if cid in link: return True return False def __gluestate__(self, context): state = {} state['data1'] = context.id(self.data1) state['data2'] = context.id(self.data2) state['cids1'] = context.id(self.cids1) state['cids2'] = context.id(self.cids2) return state @classmethod def __setgluestate__(cls, rec, context): if 'data1' in rec: self = cls(data1=context.object(rec['data1']), data2=context.object(rec['data2']), cids1=context.object(rec['cids1']), cids2=context.object(rec['cids2'])) else: # glue-core <0.15 cids = context.object(rec['cids']) cids1 = [context.object(c) for c in cids[:len(cls.labels1)]] cids2 = [context.object(c) for c in cids[len(cls.labels1):]] self = cls(cids1=cids1, cids2=cids2) return self
[docs]class ManualLinkCollection(object): """ A collection of links between two datasets. This class is intended for manual link collections, i.e. collections where the caller manually adds and removes individual links. These links can be between any component IDs as long as they link component IDs between the two specified datasets. Parameters ---------- data1 : `~glue.core.data.Data` The first dataset being linked data2 : `~glue.core.data.Data` The second dataset being linked links : list The initial links to add to the collection. """ display = 'Custom list of links' description = 'This is a list of links that has been manually constructed' def __init__(self, data1=None, data2=None, links=None): super(ManualLinkCollection, self).__init__(data1=data1, data2=data2) self._links[:] = links or []
[docs] def append(self, link): self._links.append(link)
[docs] def extend(self, links): self._links.extend(links)
def __gluestate__(self, context): state = super(ManualLinkCollection, self).__gluestate__(context) state['values'] = context.id(self._links) return state @classmethod def __setgluestate__(cls, rec, context): self = super(ManualLinkCollection, cls).__setgluestate__(rec, context) self._values[:] = context.object(rec['values']) return self
[docs]class LinkSame(MultiLink): """ A bi-directional identity link between two components. """ def __init__(self, cid1=None, cid2=None, **kwargs): if cid1 is None: cid1 = kwargs['cids1'] else: cid1 = _toid(cid1) kwargs['data1'] = cid1.parent kwargs['cids1'] = [cid1] kwargs['labels1'] = 'x' kwargs['forwards'] = identity if cid2 is None: cid2 = kwargs['cids2'] else: cid2 = _toid(cid2) kwargs['data2'] = cid2.parent kwargs['cids2'] = [cid2] kwargs['labels2'] = 'y' self._cid1 = cid1 self._cid2 = cid2 super(LinkSame, self).__init__(**kwargs) def __gluestate__(self, context): state = {} state['cid1'] = context.id(self._cid1) state['cid2'] = context.id(self._cid2) return state @classmethod def __setgluestate__(cls, rec, context): self = cls(context.object(rec['cid1']), context.object(rec['cid2'])) return self
[docs]class LinkTwoWay(MultiLink): """ Return two links that connect input ComponentIDs in both directions Parameters ---------- cid1 : `glue.core.component_id.ComponentID` The first ComponentID to link cid2 : `glue.core.component_id.ComponentID` The second ComponentID to link forwards : function Function which maps cid1 to cid2 (e.g. ``cid2=f(cid1)``) backwards : function Function which maps cid2 to cid1 (e.g. ``cid1=f(cid2)``) """ def __init__(self, cid1=None, cid2=None, forwards=None, backwards=None, **kwargs): if cid1 is None: cid1 = kwargs['cids1'] else: kwargs['data1'] = cid1.parent kwargs['cids1'] = [cid1] if cid2 is None: cid2 = kwargs['cids2'] else: kwargs['data2'] = cid2.parent kwargs['cids2'] = [cid2] super(LinkTwoWay, self).__init__(forwards=forwards, backwards=backwards, **kwargs) def __gluestate__(self, context): state = {} state['cid1'] = context.id(self._cid1) state['cid2'] = context.id(self._cid2) state['forwards'] = context.id(self.forwards) state['backwards'] = context.id(self.backwards) return state @classmethod def __setgluestate__(cls, rec, context): self = cls(context.object(rec['cid1']), context.object(rec['cid2']), context.object(rec['forwards']), context.object(rec['backwards'])) return self
[docs]class LinkAligned(LinkCollection): """ Compute all the links to specify that the input data are pixel-aligned. """ def __init__(self, data1=None, data2=None): super(LinkAligned, self).__init__(data1=data1, data2=data2) if data1.shape != data2.shape: raise TypeError("Input data do not have the same shape") links = [] for j in range(data1.ndim): links.extend(LinkSame(data1.pixel_component_ids[j], data2.pixel_component_ids[j])) self._links[:] = links
def functional_link_collection(function, labels1=None, labels2=None, display=None, description=None): class FunctionalLinkCollection(LinkCollection): def __init__(self, data1=None, data2=None, cids1=None, cids2=None): super(FunctionalLinkCollection, self).__init__(data1=data1, data2=data2, cids1=cids1, cids2=cids2) # PY3 only # self._links[:] = function(*self.cids1, *self.cids2) cids = self.cids1 + self.cids2 self._links[:] = function(*cids) FunctionalLinkCollection.labels1 = labels1 or [] FunctionalLinkCollection.labels2 = labels2 or [] FunctionalLinkCollection.display = display or '' FunctionalLinkCollection.description = description or '' return FunctionalLinkCollection