Skip to content

Commit

Permalink
Merge pull request #1238 from ioam/dynamicmap_refactor
Browse files Browse the repository at this point in the history
Simplified operation modes supported by Dynamicmap
  • Loading branch information
philippjfr committed Mar 31, 2017
2 parents 6e8d2ec + 7afffe7 commit 92ac5b5
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 818 deletions.
580 changes: 15 additions & 565 deletions doc/Tutorials/Dynamic_Map.ipynb

Large diffs are not rendered by default.

194 changes: 34 additions & 160 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,11 @@ def _dynamic_mul(self, dimensions, other, keys):
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 ValueError("Cannot overlay DynamicMaps with mismatching mode.")
else:
map_obj = self if isinstance(self, DynamicMap) else other
mode = map_obj.mode

map_obj = self if isinstance(self, DynamicMap) else other

def dynamic_mul(*key, **kwargs):
key = key[0] if mode == 'open' else key
layers = []
try:
if isinstance(self, DynamicMap):
Expand Down Expand Up @@ -452,73 +446,42 @@ def get_nested_streams(dmap):
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 'bounded' 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. Bounded mode only applied to callables where all the key
dimensions are fully bounded.
generated by a callable. The callable is invoked with values
associated with the key dimensions or with values supplied by stream
parameters.
"""

# Declare that callback is a positional parameter (used in clone)
__pos_params = ['callback']

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).
""")
callback = param.ClassSelector(class_=Callable, doc="""
The callable used to generate the elements. The arguments to the
callable includes any number of declared key dimensions as well
as any number of stream parameters defined on the input streams.
If the callable is an instance of Callable it will be used
directly, otherwise it will be automatically wrapped in one.""")

streams = param.List(default=[], doc="""
List of Stream instances to associate with the DynamicMap. The
set of parameter values across these streams will be supplied as
keyword arguments to the callback when the events are received,
updating the streams.
Note that streams may only be used with callable callbacks (i.e
not generators).""" )
updating the streams.""" )

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

sampled = param.Boolean(default=False, doc="""
Allows defining a DynamicMap in bounded mode without defining the
dimension bounds or values. The DynamicMap may then be explicitly
sampled via getitem or the sampling is determined during plotting
by a HoloMap with fixed sampling.
Allows defining a DynamicMap without defining the dimension
bounds or values. The DynamicMap may then be explicitly sampled
via getitem or the sampling is determined during plotting by a
HoloMap with fixed sampling.
""")

def __init__(self, callback, initial_items=None, **params):
if not isinstance(callback, (Callable, types.GeneratorType)):
if not isinstance(callback, Callable):
callback = Callable(callable_function=callback)
super(DynamicMap, self).__init__(initial_items, callback=callback, **params)

Expand All @@ -527,19 +490,10 @@ def __init__(self, callback, initial_items=None, **params):
if stream.source is None:
stream.source = self

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 = 'bounded' if self.call_mode == 'key' else 'open'


def _initial_key(self):
"""
Construct an initial key for bounded mode based on the lower
range bounds or values on the key dimensions.
Construct an initial key for based on the lower range bounds or
values on the key dimensions.
"""
key = []
for kdim in self.kdims:
Expand All @@ -550,29 +504,6 @@ def _initial_key(self):
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:
if self.sampled:
raise ValueError("Cannot set DynamicMap containing generator "
"to sampled")
return 'generator'
if self.sampled:
return 'key'
# Any unbounded kdim (any direction) implies open mode
for kdim in self.kdims:
if kdim.name in util.stream_parameters(self.streams):
return 'key'
if 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
Expand Down Expand Up @@ -629,27 +560,15 @@ 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
self._validate_key(args) # Validate input key

if self.call_mode == 'generator':
retval = next(self.callback)
else:
# Additional validation needed to ensure kwargs don't clash
kdims = [kdim.name for kdim in self.kdims]
kwarg_items = [s.contents.items() for s in self.streams]
flattened = [(k,v) for kws in kwarg_items for (k,v) in kws
if k not in kdims]
retval = self.callback(*args, **dict(flattened))
if self.call_mode=='key':
return self._style(retval)

if isinstance(retval, tuple):
self._validate_key(retval[0]) # Validated output key
return (retval[0], self._style(retval[1]))
else:
self._validate_key((self.counter,))
return (self.counter, self._style(retval))
# Additional validation needed to ensure kwargs don't clash
kdims = [kdim.name for kdim in self.kdims]
kwarg_items = [s.contents.items() for s in self.streams]
flattened = [(k,v) for kws in kwarg_items for (k,v) in kws
if k not in kdims]
retval = self.callback(*args, **dict(flattened))
return self._style(retval)


def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides):
Expand All @@ -664,15 +583,10 @@ def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides):
shared_data, new_type,
*(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

Expand All @@ -691,7 +605,6 @@ def _cross_product(self, tuple_key, cache, data_slice):
The data_slice may specify slices into each value in the
the cross-product.
"""
if self.mode != 'bounded': return None
if not any(isinstance(el, (list, set)) for el in tuple_key):
return None
if len(tuple_key)==1:
Expand Down Expand Up @@ -728,8 +641,7 @@ def _slice_bounded(self, tuple_key, data_slice):
"""
slices = [el for el in tuple_key if isinstance(el, slice)]
if any(el.step for el in slices):
raise Exception("Slices cannot have a step argument "
"in DynamicMap bounded mode ")
raise Exception("DynamicMap slices cannot have a step argument")
elif len(slices) not in [0, len(tuple_key)]:
raise Exception("Slices must be used exclusively or not at all")
elif not slices:
Expand Down Expand Up @@ -758,11 +670,9 @@ def _slice_bounded(self, tuple_key, data_slice):

def __getitem__(self, key):
"""
Return an element for any key chosen key (in'bounded mode') or
for a previously generated key that is still in the cache
(for one of the 'open' modes). Also allows for usual deep
slicing semantics by slicing values in the cache and applying
the deep slice to newly generated values.
Return an element for any key chosen key. Also allows for usual
deep slicing semantics by slicing values in the cache and
applying the deep slice to newly generated values.
"""
# Split key dimensions and data slices
sample = False
Expand All @@ -775,8 +685,8 @@ def __getitem__(self, key):
map_slice, data_slice = self._split_index(key)
tuple_key = util.wrap_tuple_streams(map_slice, self.kdims, self.streams)

# Validation for bounded mode
if self.mode == 'bounded' and not sample:
# Validation
if not sample:
sliced = self._slice_bounded(tuple_key, data_slice)
if sliced is not None:
return sliced
Expand All @@ -788,14 +698,8 @@ def __getitem__(self, key):
if dimensionless:
raise KeyError('Using dimensionless streams disables DynamicMap cache')
cache = super(DynamicMap,self).__getitem__(key)
# Return selected cache items in a new DynamicMap
if isinstance(cache, DynamicMap) and self.mode=='open':
cache = self.clone(cache)
except KeyError as e:
cache = None
if self.mode == 'open' and len(self.data)>0:
raise KeyError(str(e) + " Note: Cannot index outside "
"available cache in open interval mode.")

# If the key expresses a cross product, compute the elements and return
product = self._cross_product(tuple_key, cache.data if cache else {}, data_slice)
Expand All @@ -805,9 +709,6 @@ def __getitem__(self, key):
# Not a cross product and nothing cached so compute element.
if cache is not None: return cache
val = self._execute_callback(*tuple_key)
if self.call_mode == 'counter':
val = val[1]

if data_slice:
val = self._dataslice(val, data_slice)
self._cache(tuple_key, val)
Expand Down Expand Up @@ -852,39 +753,12 @@ def _cache(self, key, val):
"""
cache_size = (1 if util.dimensionless_contents(self.streams, self.kdims)
else self.cache_size)
if self.mode == 'open' and (self.counter % self.cache_interval)!=0:
return
if len(self) >= cache_size:
first_key = next(k for k in self.data)
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 == 'bounded':
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_streams(key, self.kdims, self.streams)
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


def relabel(self, label=None, group=None, depth=1):
"""
Assign a new label and/or group to an existing LabelledData
Expand Down
9 changes: 0 additions & 9 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,15 +1177,6 @@ def get_dynamic_item(map_obj, dimensions, key):
if d in map_obj.kdims}
key = tuple(dims.get(d.name) for d in map_obj.kdims)
el = map_obj.select(['DynamicMap', 'HoloMap'], **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])]
map_obj.traverse(lambda x: x[key], ['DynamicMap'])
el = map_obj.map(lambda x: x[key], ['DynamicMap'])
elif key >= map_obj.counter:
el = next(map_obj)
key = list(map_obj.keys())[-1]
else:
el = None
return key, el
Expand Down
20 changes: 4 additions & 16 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,10 @@ def __getitem__(self, frame):
"""
Get the state of the Plot for a given frame number.
"""
if not self.dynamic == 'open' and isinstance(frame, int) and frame > len(self):
if isinstance(frame, int) and frame > len(self):
self.warning("Showing last frame available: %d" % len(self))
if not self.drawn: self.handles['fig'] = self.initialize_plot()
if not self.dynamic == 'open' and not isinstance(frame, tuple):
if not isinstance(frame, tuple):
frame = self.keys[frame]
self.update_frame(frame)
return self.state
Expand Down Expand Up @@ -267,8 +267,6 @@ def _frame_title(self, key, group_size=2, separator='\n'):
Returns the formatted dimension group strings
for a particular frame.
"""
if self.dynamic == 'open' and self.current_key:
key = self.current_key
if self.layout_dimensions is not None:
dimensions, key = zip(*self.layout_dimensions.items())
elif not self.dynamic and (not self.uniform or len(self) == 1) or self.subplot:
Expand Down Expand Up @@ -587,7 +585,7 @@ def __init__(self, element, keys=None, ranges=None, dimensions=None,
defaults=False)
plot_opts.update(**{k: v[0] for k, v in inherited.items()})

dynamic = False if not isinstance(element, DynamicMap) or element.sampled else element.mode
dynamic = isinstance(element, DynamicMap) and not element.sampled
super(GenericElementPlot, self).__init__(keys=keys, dimensions=dimensions,
dynamic=dynamic,
**dict(params, **plot_opts))
Expand Down Expand Up @@ -980,17 +978,7 @@ def _get_frame(self, key):
self.current_key = key

for path, item in self.layout.items():
if self.dynamic == 'open':
if keyisint:
counts = item.traverse(lambda x: x.counter, (DynamicMap,))
if key[0] >= counts[0]:
item.traverse(lambda x: next(x), (DynamicMap,))
dim_keys = item.traverse(nthkey_fn, (DynamicMap,))[0]
else:
dim_keys = zip([d.name for d in self.dimensions
if d in item.dimensions('key')], key)
self.current_key = tuple(k[1] for k in dim_keys)
elif item.traverse(lambda x: x, [DynamicMap]):
if item.traverse(lambda x: x, [DynamicMap]):
key, frame = util.get_dynamic_item(item, self.dimensions, key)
layout_frame[path] = frame
continue
Expand Down
Loading

0 comments on commit 92ac5b5

Please sign in to comment.