Skip to content

Commit

Permalink
Merge pull request #278 from ioam/dynamic
Browse files Browse the repository at this point in the history
DynamicMap
  • Loading branch information
philippjfr committed Nov 19, 2015
2 parents 65eeb8c + 904b926 commit edfb83a
Show file tree
Hide file tree
Showing 18 changed files with 811 additions and 391 deletions.
5 changes: 3 additions & 2 deletions holoviews/core/dimension.py
Expand Up @@ -680,8 +680,9 @@ def select(self, selection_specs=None, **kwargs):
if isinstance(val, tuple): val = slice(*val)
select[self.get_dimension_index(dim)] = val
if self._deep_indexable:
selection = self.get(tuple(select),
self.clone(shared_data=False))
selection = self.get(tuple(select), None)
if selection is None:
selection = self.clone(shared_data=False)
else:
selection = self[tuple(select)]
else:
Expand Down
232 changes: 231 additions & 1 deletion holoviews/core/spaces.py
Expand Up @@ -2,6 +2,7 @@
import numpy as np

import param
import types

from . import traversal, util
from .dimension import OrderedDict, Dimension, Dimensioned, ViewableElement
Expand Down Expand Up @@ -327,6 +328,236 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw



class DynamicMap(HoloMap):
"""
A DynamicMap is a type of HoloMap where the elements are dynamically
generated by a callback which may be either a callable or a
generator. A DynamicMap supports two different modes depending on
the type of callable supplied and the dimension declarations.
The 'closed' mode is used when the limits of the parameter space are
known upon declaration (as specified by the ranges on the key
dimensions) or 'open' which allows the continual generation of
elements (e.g as data output by a simulator over an unbounded
simulated time dimension).
Generators always imply open mode but a callable that has any key
dimension unbounded in any direction will also be in open
mode. Closed mode only applied to callables where all the key
dimensions are fully bounded.
"""
_sorted = False

callback = param.Parameter(doc="""
The callable or generator used to generate the elements. In the
simplest case where all key dimensions are bounded, this can be
a callable that accepts the key dimension values as arguments
(in the declared order) and returns the corresponding element.
For open mode where there is an unbounded key dimension, the
return type can specify a key as well as element as the tuple
(key, element). If no key is supplied, a simple counter is used
instead.
If the callback is a generator, open mode is used and next() is
simply called. If the callback is callable and in open mode, the
element counter value will be supplied as the single
argument. This can be used to avoid issues where multiple
elements in a Layout each call next() leading to uncontrolled
changes in simulator state (the counter can be used to indicate
simulation time across the layout).
""")

cache_size = param.Integer(default=500, doc="""
The number of entries to cache for fast access. This is an LRU
cache where the least recently used item is overwritten once
the cache is full.""")

cache_interval = param.Integer(default=1, doc="""
When the element counter modulo the cache_interval is zero, the
element will be cached and therefore accessible when casting to a
HoloMap. Applicable in open mode only.""")

def __init__(self, initial_items=None, **params):
super(DynamicMap, self).__init__(initial_items, **params)
self.counter = 0
if self.callback is None:
raise Exception("A suitable callback must be "
"declared to create a DynamicMap")

self.call_mode = self._validate_mode()
self.mode = 'closed' if self.call_mode == 'key' else 'open'
# Needed to initialize the plotting system
if self.call_mode == 'key':
self[self._initial_key()]
elif self.call_mode == 'counter':
self[self.counter]
self.counter += 1
else:
next(self)


def _initial_key(self):
"""
Construct an initial key for closed mode based on the lower
range bounds or values on the key dimensions.
"""
key = []
for kdim in self.kdims:
if kdim.values:
key.append(kdim.values[0])
elif kdim.range:
key.append(kdim.range[0])
return tuple(key)


def _validate_mode(self):
"""
Check the key dimensions and callback to determine the calling mode.
"""
isgenerator = isinstance(self.callback, types.GeneratorType)
if isgenerator:
return 'generator'
# Any unbounded kdim (any direction) implies open mode
for kdim in self.kdims:
if (kdim.values) and kdim.range != (None,None):
raise Exception('Dimension cannot have both values and ranges.')
elif kdim.values:
continue
if None in kdim.range:
return 'counter'
return 'key'


def _validate_key(self, key):
"""
Make sure the supplied key values are within the bounds
specified by the corresponding dimension range and soft_range.
"""
key = util.wrap_tuple(key)
assert len(key) == len(self.kdims)
for ind, val in enumerate(key):
kdim = self.kdims[ind]
low, high = util.max_range([kdim.range, kdim.soft_range])
if low is not np.NaN:
if val < low:
raise StopIteration("Key value %s below lower bound %s"
% (val, low))
if high is not np.NaN:
if val > high:
raise StopIteration("Key value %s above upper bound %s"
% (val, high))


def _execute_callback(self, *args):
"""
Execute the callback, validating both the input key and output
key where applicable.
"""
if self.call_mode == 'key':
self._validate_key(args) # Validate input key

if self.call_mode == 'generator':
retval = self.callback.next()
else:
retval = self.callback(*args)

if self.call_mode=='key':
return retval

if isinstance(retval, tuple):
self._validate_key(retval[0]) # Validated output key
return retval
else:
self._validate_key((self.counter,))
return (self.counter, retval)


def clone(self, data=None, shared_data=True, *args, **overrides):
"""
Overrides Dimensioned clone to avoid checking items if data
is unchanged.
"""
return super(UniformNdMapping, self).clone(data, shared_data,
*args, **overrides)


def reset(self):
"""
Return a cleared dynamic map with a cleared cached
and a reset counter.
"""
if self.call_mode == 'generator':
raise Exception("Cannot reset generators.")
self.counter = 0
self.data = OrderedDict()
return self


def __getitem__(self, key):
"""
Return an element for any key chosen key (in'closed mode') or
for a previously generated key that is still in the cache
(for one of the 'open' modes)
"""
try:
retval = super(DynamicMap,self).__getitem__(key)
if isinstance(retval, DynamicMap):
return HoloMap(retval)
else:
return retval
except KeyError as e:
if self.mode == 'open' and len(self.data)>0:
raise KeyError(str(e) + " Note: Cannot index outside "
"available cache over an open interval.")
tuple_key = util.wrap_tuple(key)
val = self._execute_callback(*tuple_key)
if self.call_mode == 'counter':
val = val[1]

self._cache(tuple_key, val)
return val


def _cache(self, key, val):
"""
Request that a key/value pair be considered for caching.
"""
if self.mode == 'open' and (self.counter % self.cache_interval)!=0:
return
if len(self) >= self.cache_size:
first_key = self.data.iterkeys().next()
self.data.pop(first_key)
self.data[key] = val


def next(self):
"""
Interface for 'open' mode. For generators, this simply calls the
next() method. For callables callback, the counter is supplied
as a single argument.
"""
if self.mode == 'closed':
raise Exception("The next() method should only be called in "
"one of the open modes.")

args = () if self.call_mode == 'generator' else (self.counter,)
retval = self._execute_callback(*args)

(key, val) = (retval if isinstance(retval, tuple)
else (self.counter, retval))

key = util.wrap_tuple(key)
if len(key) != len(self.key_dimensions):
raise Exception("Generated key does not match the number of key dimensions")

self._cache(key, val)
self.counter += 1
return val




class GridSpace(UniformNdMapping):
"""
Grids are distinct from Layouts as they ensure all contained
Expand Down Expand Up @@ -480,4 +711,3 @@ def _item_check(self, dim_vals, data):
raise ValueError("HoloMaps dimensions must be consistent in %s." %
type(self).__name__)
NdMapping._item_check(self, dim_vals, data)

2 changes: 1 addition & 1 deletion holoviews/core/util.py
Expand Up @@ -289,7 +289,7 @@ def max_range(ranges):
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered')
arr = np.array(ranges)
arr = np.array([r for r in ranges for v in r if v is not None])
if arr.dtype.kind == 'M':
return arr[:, 0].min(), arr[:, 1].max()
return (np.nanmin(arr[:, 0]), np.nanmax(arr[:, 1]))
Expand Down
12 changes: 8 additions & 4 deletions holoviews/ipython/display_hooks.py
Expand Up @@ -9,7 +9,7 @@

from ..core.options import Store, StoreOptions
from ..core import Element, ViewableElement, UniformNdMapping, HoloMap, AdjointLayout, NdLayout,\
GridSpace, Layout, displayable, undisplayable_info, CompositeOverlay
GridSpace, Layout, CompositeOverlay, DynamicMap, displayable, undisplayable_info
from ..core.traversal import unique_dimkeys, bijective
from .magics import OutputMagic, OptsMagic

Expand Down Expand Up @@ -79,6 +79,7 @@ def display_video(plot, renderer, holomap_format, dpi, fps, css, **kwargs):
def display_widgets(plot, renderer, holomap_format, widget_mode, **kwargs):
"Display widgets applicable to the specified element"
isuniform = plot.uniform
dynamic = plot.dynamic
islinear = bijective(plot.keys)
if not isuniform and holomap_format == 'widgets':
param.Parameterized.warning("%s is not uniform, falling back to scrubber widget."
Expand All @@ -89,6 +90,9 @@ def display_widgets(plot, renderer, holomap_format, widget_mode, **kwargs):
holomap_format = 'scrubber' if islinear or not isuniform else 'widgets'

widget = 'scrubber' if holomap_format == 'scrubber' else 'selection'
if dynamic == 'open': widget = 'scrubber'
if dynamic == 'closed': widget = 'selection'

widget_cls = plot.renderer.widgets[widget]

return widget_cls(plot, renderer=renderer, embed=(widget_mode == 'embed'),
Expand Down Expand Up @@ -129,7 +133,7 @@ def render_plot(plot, widget_mode, message=None):

renderer = OutputMagic.renderer(dpi=kwargs['dpi'], fps=kwargs['fps'])
with renderer.state():
if len(plot) == 1:
if len(plot) == 1 and not plot.dynamic:
plot.update(0)
return display_frame(plot, renderer, **kwargs)
elif widget_mode is not None:
Expand Down Expand Up @@ -198,7 +202,7 @@ class Warning(param.Parameterized): pass

@display_hook
def map_display(vmap, size, max_frames, max_branches, widget_mode):
if not isinstance(vmap, HoloMap): return None
if not isinstance(vmap, (HoloMap, DynamicMap)): return None

if not displayable(vmap):
display_warning.warning("Nesting %ss within a HoloMap makes it difficult "
Expand Down Expand Up @@ -299,7 +303,7 @@ def display(obj, raw=False, **kwargs):
html = element_display(obj)
elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
html = layout_display(obj)
elif isinstance(obj, HoloMap):
elif isinstance(obj, (HoloMap, DynamicMap)):
html = map_display(obj)
else:
return repr(obj) if raw else IPython.display.display(obj, **kwargs)
Expand Down

0 comments on commit edfb83a

Please sign in to comment.