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

Added support for dynamic operations and overlaying #588

Merged
merged 9 commits into from
Apr 11, 2016
107 changes: 102 additions & 5 deletions holoviews/core/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
from functools import reduce
import param

try:
from itertools import izip as zip
except:
pass

from .dimension import ViewableElement
from .element import Element, HoloMap, GridSpace, Collator
from .layout import Layout
from .overlay import NdOverlay, Overlay
from .spaces import DynamicMap
from .traversal import unique_dimkeys

from . import util


class Operation(param.ParameterizedFunction):
Expand Down Expand Up @@ -64,6 +70,79 @@ def get_overlay_bounds(cls, overlay):
raise ValueError("Extents across the overlay are inconsistent")


class DynamicOperation(Operation):
"""
Dynamically applies an operation to the elements of a HoloMap
or DynamicMap. Will return a DynamicMap wrapping the original
map object, which will lazily evaluate when a key is requested.
The _process method should be overridden in subclasses to apply
a specific operation, DynamicOperation itself applies a no-op,
making the DynamicOperation baseclass useful for converting
existing HoloMaps to a DynamicMap.
"""

def __call__(self, map_obj, **params):
self.p = param.ParamOverrides(self, params)
callback = self._dynamic_operation(map_obj)
if isinstance(map_obj, DynamicMap):
return map_obj.clone(callback=callback, shared_data=False)
else:
return self._make_dynamic(map_obj, callback)


def _process(self, element):
return element


def _dynamic_operation(self, map_obj):
"""
Generate function to dynamically apply the operation.
Wraps an existing HoloMap or DynamicMap.
"""
if not isinstance(map_obj, DynamicMap):
def dynamic_operation(*key):
return self._process(map_obj[key])
return dynamic_operation

def dynamic_operation(*key):
key = key[0] if map_obj.mode == 'open' else key
_, el = util.get_dynamic_item(map_obj, map_obj.kdims, key)
return self._process(el)

return dynamic_operation


def _make_dynamic(self, hmap, dynamic_fn):
"""
Accepts a HoloMap and a dynamic callback function creating
an equivalent DynamicMap from the HoloMap.
"""
dim_values = zip(*hmap.data.keys())
params = util.get_param_values(hmap)
kdims = [d(values=list(values)) for d, values in zip(hmap.kdims, dim_values)]
return DynamicMap(dynamic_fn, **dict(params, kdims=kdims))



class DynamicFunction(DynamicOperation):
"""
Dynamically applies a function to the Elements in a DynamicMap
or HoloMap. Must supply a HoloMap or DynamicMap type and will
return another DynamicMap type, which will apply the supplied
function with the supplied kwargs whenever a value is requested
from the map.
"""

function = param.Callable(default=lambda x: x, doc="""
Function to apply to DynamicMap items dynamically.""")

kwargs = param.Dict(default={}, doc="""
Keyword arguments passed to the function.""")

def _process(self, element):
return self.p.function(element, **self.p.kwargs)



class ElementOperation(Operation):
"""
Expand All @@ -74,6 +153,13 @@ class ElementOperation(Operation):
ElementOperation may turn overlays in new elements or vice versa.
"""

dynamic = param.ObjectSelector(default='default',
objects=['default', True, False], doc="""
Whether the operation should be applied dynamically when a
specific frame is requested, specified as a Boolean. If set to
'default' the mode will be determined based on the input type,
i.e. if the data is a DynamicMap it will stay dynamic.""")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with this approach.

input_ranges = param.ClassSelector(default={},
class_=(dict, tuple), doc="""
Ranges to be used for input normalization (if applicable) in a
Expand Down Expand Up @@ -106,16 +192,27 @@ def process_element(self, element, key, **params):

def __call__(self, element, **params):
self.p = param.ParamOverrides(self, params)
dynamic = ((self.p.dynamic == 'default' and
isinstance(element, DynamicMap))
or self.p.dynamic is True)

if isinstance(element, ViewableElement):
processed = self._process(element)
elif isinstance(element, GridSpace):
# Initialize an empty axis layout
processed = GridSpace(None, label=element.label,
grid_data = ((pos, self(cell, **params))
for pos, cell in element.items())
processed = GridSpace(grid_data, label=element.label,
kdims=element.kdims)
# Populate the axis layout
for pos, cell in element.items():
processed[pos] = self(cell, **params)
elif dynamic:
processed = DynamicFunction(element, function=self, kwargs=params)
elif isinstance(element, DynamicMap):
if any((not d.values) for d in element.kdims):
raise ValueError('Applying a non-dynamic operation requires '
'all DynamicMap key dimensions to define '
'the sampling by specifying values.')
samples = tuple(d.values for d in element.kdims)
processed = self(element[samples], **params)
elif isinstance(element, HoloMap):
mapped_items = [(k, self._process(el, key=k))
for k, el in element.items()]
Expand Down
5 changes: 5 additions & 0 deletions holoviews/core/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class Overlayable(object):
"""

def __mul__(self, other):
if type(other).__name__ == 'DynamicMap':
from .operation import DynamicFunction
def dynamic_mul(element):
return self * element
return DynamicFunction(other, function=dynamic_mul)
if isinstance(other, UniformNdMapping) and not isinstance(other, CompositeOverlay):
items = [(k, self * v) for (k, v) in other.items()]
return other.clone(items)
Expand Down
65 changes: 63 additions & 2 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from numbers import Number
import itertools
from itertools import groupby
import numpy as np

import param
Expand Down Expand Up @@ -97,6 +98,56 @@ def _dimension_keys(self):
for k in self.keys()]


def _dynamic_mul(self, dimensions, other, keys):
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see this functionality but sadly it makes __mul__ into even more of a monster. I've always felt that __mul__ should be implemented once as a separate utility/operation one day. If you agree, I can file an issue to suggest this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree, a separate utility makes sense to me, so please do open an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. See issue #600.

Implements dynamic version of overlaying operation overlaying
DynamicMaps and HoloMaps where the key dimensions of one is
a strict superset of the other.
"""
# If either is a HoloMap compute Dimension values
if not isinstance(self, DynamicMap) or not isinstance(other, DynamicMap):
keys = sorted((d, v) for k in keys for d, v in k)
grouped = dict([(g, [v for _, v in group])
for g, group in groupby(keys, lambda x: x[0])])
dimensions = [d(values=grouped[d.name]) for d in dimensions]
mode = 'bounded'
map_obj = None
elif (isinstance(self, DynamicMap) and (other, DynamicMap) and
self.mode != other.mode):
raise ValueEror("Cannot overlay DynamicMaps with mismatching mode.")
else:
map_obj = self if isinstance(self, DynamicMap) else other
mode = map_obj.mode

def dynamic_mul(*key):
key = key[0] if mode == 'open' else key
layers = []
try:
if isinstance(self, DynamicMap):
_, self_el = util.get_dynamic_item(self, dimensions, key)
if self_el is not None:
layers.append(self_el)
else:
layers.append(self[key])
except KeyError:
pass
try:
if isinstance(other, DynamicMap):
_, other_el = util.get_dynamic_item(other, dimensions, key)
if other_el is not None:
layers.append(other_el)
else:
layers.append(other[key])
except KeyError:
pass
return Overlay(layers)
if map_obj:
return map_obj.clone(callback=dynamic_mul, shared_data=False,
kdims=dimensions)
else:
return DynamicMap(callback=dynamic_mul, kdims=dimensions)


def __mul__(self, other):
"""
The mul (*) operator implements overlaying of different Views.
Expand All @@ -108,7 +159,7 @@ def __mul__(self, other):
will try to match up the dimensions, making sure that items
with completely different dimensions aren't overlaid.
"""
if isinstance(other, self.__class__):
if isinstance(other, HoloMap):
self_set = {d.name for d in self.kdims}
other_set = {d.name for d in other.kdims}

Expand All @@ -117,8 +168,10 @@ def __mul__(self, other):
self_in_other = self_set.issubset(other_set)
other_in_self = other_set.issubset(self_set)
dimensions = self.kdims

if self_in_other and other_in_self: # superset of each other
super_keys = sorted(set(self._dimension_keys() + other._dimension_keys()))
keys = self._dimension_keys() + other._dimension_keys()
super_keys = util.unique_iterator(keys)
elif self_in_other: # self is superset
dimensions = other.kdims
super_keys = other._dimension_keys()
Expand All @@ -127,6 +180,9 @@ def __mul__(self, other):
else: # neither is superset
raise Exception('One set of keys needs to be a strict subset of the other.')

if isinstance(self, DynamicMap) or isinstance(other, DynamicMap):
return self._dynamic_mul(dimensions, other, super_keys)

items = []
for dim_keys in super_keys:
# Generate keys for both subset and superset and sort them by the dimension index.
Expand All @@ -146,6 +202,11 @@ def __mul__(self, other):
items.append((new_key, Overlay([other[other_key]])))
return self.clone(items, kdims=dimensions, label=self._label, group=self._group)
elif isinstance(other, self.data_type):
if isinstance(self, DynamicMap):
from .operation import DynamicFunction
def dynamic_mul(element):
return element * other
return DynamicFunction(self, function=dynamic_mul)
items = [(k, v * other) for (k, v) in self.data.items()]
return self.clone(items, label=self._label, group=self._group)
else:
Expand Down
24 changes: 24 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,3 +886,27 @@ def arglexsort(arrays):
recarray['f%s' % i] = array
return recarray.argsort()


def get_dynamic_item(map_obj, dimensions, key):
"""
Looks up an item in a DynamicMap given a list of dimensions
and a corresponding key. The dimensions must be a subset
of the map_obj key dimensions.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example would be nice in the docstring here. Doesn't have to be an actual doctest if that is too tricky to get working...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, can add a unit test too.

if isinstance(key, tuple):
dims = {d.name: k for d, k in zip(dimensions, key)
if d in map_obj.kdims}
key = tuple(dims.get(d.name) for d in map_obj.kdims)
el = map_obj.select([lambda x: type(x).__name__ == 'DynamicMap'],
**dims)
elif key < map_obj.counter:
key_offset = max([key-map_obj.cache_size, 0])
key = map_obj.keys()[min([key-key_offset,
len(map_obj)-1])]
el = map_obj[key]
elif key >= map_obj.counter:
el = next(map_obj)
key = list(map_obj.keys())[-1]
else:
el = None
return key, el
14 changes: 1 addition & 13 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,19 +515,7 @@ def _get_frame(self, key):
self.current_key = key
return self.current_frame
elif self.dynamic:
if isinstance(key, tuple):
dims = {d.name: k for d, k in zip(self.dimensions, key)
if d in self.hmap.kdims}
frame = self.hmap.select([DynamicMap], **dims)
elif key < self.hmap.counter:
key_offset = max([key-self.hmap.cache_size, 0])
key = self.hmap.keys()[min([key-key_offset, len(self.hmap)-1])]
frame = self.hmap[key]
elif key >= self.hmap.counter:
frame = next(self.hmap)
key = self.hmap.keys()[-1]
else:
frame = None
key, frame = util.get_dynamic_item(self.hmap, self.dimensions, key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see this as a utility!

if not isinstance(key, tuple): key = (key,)
if not key in self.keys:
self.keys.append(key)
Expand Down
55 changes: 54 additions & 1 deletion tests/testdynamic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
from holoviews import Dimension, DynamicMap, Image
from holoviews import Dimension, DynamicMap, Image, HoloMap
from holoviews.core.operation import DynamicFunction
from holoviews.element.comparison import ComparisonTestCase

frequencies = np.linspace(0.5,2.0,5)
Expand Down Expand Up @@ -78,3 +79,55 @@ def test_sampled_bounded_resample(self):
dmap=DynamicMap(fn, sampled=True)
self.assertEqual(dmap[{0, 1, 2}].keys(), [0, 1, 2])


class DynamicTestOperation(ComparisonTestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thanks for adding these tests!


def test_dynamic_function(self):
fn = lambda i: Image(sine_array(0,i))
dmap=DynamicMap(fn, sampled=True)
dmap_with_fn = DynamicFunction(dmap, function=lambda x: x.clone(x.data*2))
self.assertEqual(dmap_with_fn[5], Image(sine_array(0,5)*2))


def test_dynamic_function_with_kwargs(self):
fn = lambda i: Image(sine_array(0,i))
dmap=DynamicMap(fn, sampled=True)
def fn(x, multiplier=2):
return x.clone(x.data*multiplier)
dmap_with_fn = DynamicFunction(dmap, function=fn, kwargs=dict(multiplier=3))
self.assertEqual(dmap_with_fn[5], Image(sine_array(0,5)*3))



class DynamicTestOverlay(ComparisonTestCase):

def test_dynamic_element_overlay(self):
fn = lambda i: Image(sine_array(0,i))
dmap=DynamicMap(fn, sampled=True)
dynamic_overlay = dmap * Image(sine_array(0,10))
overlaid = Image(sine_array(0,5)) * Image(sine_array(0,10))
self.assertEqual(dynamic_overlay[5], overlaid)

def test_dynamic_element_underlay(self):
fn = lambda i: Image(sine_array(0,i))
dmap=DynamicMap(fn, sampled=True)
dynamic_overlay = Image(sine_array(0,10)) * dmap
overlaid = Image(sine_array(0,10)) * Image(sine_array(0,5))
self.assertEqual(dynamic_overlay[5], overlaid)

def test_dynamic_dynamicmap_overlay(self):
fn = lambda i: Image(sine_array(0,i))
dmap=DynamicMap(fn, sampled=True)
fn2 = lambda i: Image(sine_array(0,i*2))
dmap2=DynamicMap(fn2, sampled=True)
dynamic_overlay = dmap * dmap2
overlaid = Image(sine_array(0,5)) * Image(sine_array(0,10))
self.assertEqual(dynamic_overlay[5], overlaid)

def test_dynamic_holomap_overlay(self):
fn = lambda i: Image(sine_array(0,i))
dmap = DynamicMap(fn, sampled=True)
hmap = HoloMap({i: Image(sine_array(0,i*2)) for i in range(10)})
dynamic_overlay = dmap * hmap
overlaid = Image(sine_array(0,5)) * Image(sine_array(0,10))
self.assertEqual(dynamic_overlay[5], overlaid)