Skip to content

Commit

Permalink
Merge pull request #588 from ioam/dynamic_ops
Browse files Browse the repository at this point in the history
Added support for dynamic operations and overlaying
  • Loading branch information
jlstevens committed Apr 11, 2016
2 parents e45cbaa + 0aedb52 commit 5fb7e95
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 21 deletions.
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.""")

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):
"""
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.
"""
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)
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):

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)

0 comments on commit 5fb7e95

Please sign in to comment.