From 7974836d3dcc9d303ed27171d6fb5c1ab95e32df Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 30 Mar 2017 23:30:46 +0100 Subject: [PATCH 01/14] Removed outdated docstring content from DynamicMap --- holoviews/core/spaces.py | 43 +++++++--------------------------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index df8b5d62a0..1e57a19876 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -452,53 +452,24 @@ 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). - """) + 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.""") 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 From 30872a0927fd1268fa8d11f93ea09ba22294ed22 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 30 Mar 2017 23:42:39 +0100 Subject: [PATCH 02/14] Removed generator support from DynamicMap --- holoviews/core/spaces.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 1e57a19876..bdcbd4d397 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -489,7 +489,7 @@ class DynamicMap(HoloMap): """) 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) @@ -525,12 +525,6 @@ 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 @@ -603,15 +597,12 @@ def _execute_callback(self, *args): if self.call_mode == '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)) + # 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) @@ -641,8 +632,6 @@ 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 @@ -841,8 +830,7 @@ def next(self): 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) + retval = self._execute_callback(*(self.counter,)) (key, val) = (retval if isinstance(retval, tuple) else (self.counter, retval)) From 3b77eff950848cfb29bf1113aaff08ec279ea00a Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 30 Mar 2017 23:44:09 +0100 Subject: [PATCH 03/14] Removed unit tests of DynamicMap generator support --- tests/testdynamic.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 5d0e84ee47..ba9efac43c 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -83,34 +83,6 @@ def test_deep_select_slice_kdim_no_match(self): - -class DynamicTestGeneratorOpen(ComparisonTestCase): - - def test_generator_open_init(self): - generator = (Image(sine_array(0,i)) for i in range(10)) - dmap=DynamicMap(generator) - self.assertEqual(dmap.mode, 'open') - - def test_generator_open_clone(self): - generator = (Image(sine_array(0,i)) for i in range(10)) - dmap=DynamicMap(generator) - self.assertEqual(dmap, dmap.clone()) - - def test_generator_open_stopiteration(self): - generator = (Image(sine_array(0,i)) for i in range(10)) - dmap=DynamicMap(generator) - for i in range(10): - el = next(dmap) - self.assertEqual(type(el), Image) - try: - el = next(dmap) - raise AssertionError("StopIteration not raised when expected") - except Exception as e: - if e.__class__ != StopIteration: - raise AssertionError("StopIteration was expected, got %s" % e) - - - class DynamicTestCallableOpen(ComparisonTestCase): def test_callable_open_init(self): From fd2d189af284cb349307d73274192fb4dd280d6c Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 30 Mar 2017 23:47:37 +0100 Subject: [PATCH 04/14] Updated callback parameter to be a Callable ClassSelector --- holoviews/core/spaces.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index bdcbd4d397..018fde81eb 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -460,10 +460,13 @@ class DynamicMap(HoloMap): # Declare that callback is a positional parameter (used in clone) __pos_params = ['callback'] - callback = param.Parameter(doc=""" + 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.""") + 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 From da71a02e42a981b8a183184a1f351aba7505366d Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 30 Mar 2017 23:49:24 +0100 Subject: [PATCH 05/14] Simplified DynamicMap constructor --- holoviews/core/spaces.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 018fde81eb..3294cc71b0 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -502,10 +502,6 @@ def __init__(self, callback, initial_items=None, **params): 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' From bbd31f6bebc9d2b54ed3bd3757ed4ff38561f9bc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:03:27 +0100 Subject: [PATCH 06/14] Removed counter mode and support for key return --- holoviews/core/spaces.py | 71 ++-------------------------------- holoviews/plotting/renderer.py | 8 +--- holoviews/plotting/util.py | 9 +---- 3 files changed, 5 insertions(+), 83 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 3294cc71b0..818f1eadca 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -479,11 +479,6 @@ class DynamicMap(HoloMap): 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 @@ -501,9 +496,7 @@ def __init__(self, callback, initial_items=None, **params): if stream.source is None: stream.source = self - self.counter = 0 - self.call_mode = self._validate_mode() - self.mode = 'bounded' if self.call_mode == 'key' else 'open' + self.mode = 'bounded' def _initial_key(self): @@ -520,23 +513,6 @@ def _initial_key(self): return tuple(key) - def _validate_mode(self): - """ - Check the key dimensions and callback to determine the calling mode. - """ - 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 @@ -593,8 +569,7 @@ 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 # Additional validation needed to ensure kwargs don't clash kdims = [kdim.name for kdim in self.kdims] @@ -602,15 +577,7 @@ def _execute_callback(self, *args): 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)) + return self._style(retval) def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): @@ -625,13 +592,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. """ - self.counter = 0 self.data = OrderedDict() return self @@ -764,9 +728,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) @@ -811,38 +772,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.") - - retval = self._execute_callback(*(self.counter,)) - - (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 diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index 479cf57a56..31a72623e2 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -168,13 +168,7 @@ def get_plot(self_or_cls, obj, renderer=None): if dmap.sampled: # Skip initialization until plotting code continue - if dmap.call_mode == 'key': - dmap[dmap._initial_key()] - else: - try: - next(dmap) - except StopIteration: # Exhausted DynamicMap - raise SkipRendering("DynamicMap generator exhausted.") + dmap[dmap._initial_key()] if not renderer: renderer = self_or_cls.instance() if not isinstance(obj, Plot): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 108806de11..8ee0e4a0c4 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -173,20 +173,13 @@ def get_dynamic_mode(composite): "Returns the common mode of the dynamic maps in given composite object" dynmaps = composite.traverse(lambda x: x, [DynamicMap]) holomaps = composite.traverse(lambda x: x, ['HoloMap']) - dynamic_modes = [m.call_mode for m in dynmaps] dynamic_sampled = any(m.sampled for m in dynmaps) if holomaps: validate_sampled_mode(holomaps, dynmaps) elif dynamic_sampled and not holomaps: raise Exception("DynamicMaps in sampled mode must be displayed alongside " "a HoloMap to define the sampling.") - if len(set(dynamic_modes)) > 1: - raise Exception("Cannot display composites of DynamicMap objects " - "with different interval modes (i.e open or bounded mode).") - elif dynamic_modes and not holomaps: - return 'bounded' if dynamic_modes[0] == 'key' else 'open', dynamic_sampled - else: - return None, dynamic_sampled + return 'bounded' if dynmaps and not holomaps else None, dynamic_sampled def initialize_sampled(obj, dimensions, key): From abe75df921578e1d73d203db447252f36629c8d2 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:33:01 +0100 Subject: [PATCH 07/14] Removed references to DynamicMap modes in plotting and utils --- holoviews/plotting/plot.py | 18 +++--------------- holoviews/plotting/renderer.py | 9 +++------ holoviews/plotting/util.py | 2 +- holoviews/util.py | 1 - 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 9b2f43337c..b214dee789 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -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 @@ -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: @@ -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 diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index 31a72623e2..a06a94572b 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -308,12 +308,9 @@ def get_widget(self_or_cls, plot, widget_type, **kwargs): widget_type = 'scrubber' else: widget_type = 'widgets' - elif dynamic == 'open': widget_type = 'scrubber' - elif dynamic == 'bounded': widget_type = 'widgets' - elif widget_type == 'widgets' and dynamic == 'open': - raise ValueError('Selection widgets not supported in dynamic open mode') - elif widget_type == 'scrubber' and dynamic == 'bounded': - raise ValueError('Scrubber widget not supported in dynamic bounded mode') + elif dynamic: widget_type = 'widgets' + elif widget_type == 'scrubber' and dynamic: + raise ValueError('DynamicMap do not support scrubber widget') if widget_type in [None, 'auto']: holomap_formats = self_or_cls.mode_formats['holomap'][self_or_cls.mode] diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 8ee0e4a0c4..199bd38092 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -179,7 +179,7 @@ def get_dynamic_mode(composite): elif dynamic_sampled and not holomaps: raise Exception("DynamicMaps in sampled mode must be displayed alongside " "a HoloMap to define the sampling.") - return 'bounded' if dynmaps and not holomaps else None, dynamic_sampled + return dynmaps and not holomaps, dynamic_sampled def initialize_sampled(obj, dimensions, key): diff --git a/holoviews/util.py b/holoviews/util.py index 48640a76b6..aa282bba16 100644 --- a/holoviews/util.py +++ b/holoviews/util.py @@ -79,7 +79,6 @@ def dynamic_operation(*key, **kwargs): return self._process(map_obj[key], key) else: def dynamic_operation(*key, **kwargs): - key = key[0] if map_obj.mode == 'open' else key self.p.kwargs.update(kwargs) _, el = util.get_dynamic_item(map_obj, map_obj.kdims, key) return self._process(el, key) From 78cba1afe5039b53209526d1378560f729d65855 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:36:32 +0100 Subject: [PATCH 08/14] Removed references to modes in DynamicMap --- holoviews/core/spaces.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 818f1eadca..e549ef010f 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -480,10 +480,10 @@ class DynamicMap(HoloMap): the cache is full.""") 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): @@ -496,13 +496,10 @@ def __init__(self, callback, initial_items=None, **params): if stream.source is None: stream.source = self - self.mode = 'bounded' - - 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: @@ -614,7 +611,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: @@ -651,8 +647,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: @@ -681,11 +676,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 @@ -698,8 +691,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 @@ -711,14 +704,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) From c239d41dab03000eb13c5d5d5c22a811151eea7b Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:43:29 +0100 Subject: [PATCH 09/14] Removed references to DynamicMap modes in _dynamic_mul --- holoviews/core/spaces.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index e549ef010f..04a3a90fba 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -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): From 39ee0badb1912c087d4f0a615cef3aba9ed78feb Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:44:42 +0100 Subject: [PATCH 10/14] Removed unit tests for 'open' mode --- tests/testdynamic.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index ba9efac43c..377740124b 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -82,22 +82,6 @@ def test_deep_select_slice_kdim_no_match(self): self.assertEqual(dmap.select(DynamicMap, x=(5, 10))[10], fn(10)) - -class DynamicTestCallableOpen(ComparisonTestCase): - - def test_callable_open_init(self): - fn = lambda i: Image(sine_array(0,i)) - dmap=DynamicMap(fn) - self.assertEqual(dmap.mode, 'open') - - def test_callable_open_clone(self): - fn = lambda i: Image(sine_array(0,i)) - dmap=DynamicMap(fn) - self.assertEqual(dmap, dmap.clone()) - - - - class DynamicTestCallableBounded(ComparisonTestCase): def test_callable_bounded_init(self): From e13c6f7767f74a54e36c894782567ca1940361a6 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 00:45:31 +0100 Subject: [PATCH 11/14] Removed references to mode from testdynamic.py --- tests/testdynamic.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 377740124b..2a2b33f4f2 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -87,7 +87,6 @@ class DynamicTestCallableBounded(ComparisonTestCase): def test_callable_bounded_init(self): fn = lambda i: Image(sine_array(0,i)) dmap=DynamicMap(fn, kdims=[Dimension('dim', range=(0,10))]) - self.assertEqual(dmap.mode, 'bounded') def test_generator_bounded_clone(self): fn = lambda i: Image(sine_array(0,i)) @@ -100,7 +99,6 @@ class DynamicTestSampledBounded(ComparisonTestCase): def test_sampled_bounded_init(self): fn = lambda i: Image(sine_array(0,i)) dmap=DynamicMap(fn, sampled=True) - self.assertEqual(dmap.mode, 'bounded') def test_sampled_bounded_resample(self): fn = lambda i: Image(sine_array(0,i)) From 1d17535bcc1519f653b02d90ae4226d2addcf1dc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 01:08:23 +0100 Subject: [PATCH 12/14] Removed logic relating to modes in core.util.get_dynamic_item --- holoviews/core/util.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 26e1344355..7768097e6d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -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 From f40b4c557ed5403a9792b65e9eea19231e854f04 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 01:28:51 +0100 Subject: [PATCH 13/14] Fixed reference to DynamicMap mode in GenericElementPlot --- holoviews/plotting/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index b214dee789..731d905e79 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -585,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)) From 7afffe73df21e381af3e5d3f5ef58403bc9cf79f Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 31 Mar 2017 01:35:52 +0100 Subject: [PATCH 14/14] Removed outdated material from the DynamicMap tutorial --- doc/Tutorials/Dynamic_Map.ipynb | 580 +------------------------------- 1 file changed, 15 insertions(+), 565 deletions(-) diff --git a/doc/Tutorials/Dynamic_Map.ipynb b/doc/Tutorials/Dynamic_Map.ipynb index 9c248409e9..7f9702d436 100644 --- a/doc/Tutorials/Dynamic_Map.ipynb +++ b/doc/Tutorials/Dynamic_Map.ipynb @@ -720,559 +720,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This grid shows a range of frequencies `f` on the x axis, a range of the first phase variable `ph` on the `y` axis, and a range of different `ph2` phases as overlays within each location in the grid. As you can see, these techniques can help you visualize multidimensional parameter spaces compactly and conveniently.\n", - "\n", - "### Open mode " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "``DynamicMap`` also allows unconstrained exploration over unbounded dimensions in 'open' mode. There are two key differences between open mode and bounded mode:\n", - "\n", - "* Instead of a callable, the input to an open ``DynamicMap`` is a generator. Once created, the generator is only used via ``next()``.\n", - "* At least one of the declared key dimensions must have an unbounded range (i.e., with an upper or lower bound not specified).\n", - "* An open mode ``DynamicMap`` can run forever, or until a ``StopIteration`` exception is raised.\n", - "* Open mode ``DynamicMaps`` can be stateful, with an irreversible direction of time.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Infinite generators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Our first example will be using an infinite generator which plots the histogram for a given number of random samples drawn from a Gaussian distribution:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def gaussian_histogram(samples, scale):\n", - " frequencies, edges = np.histogram([np.random.normal(scale=scale) \n", - " for i in range(samples)], 20)\n", - " return hv.Histogram(frequencies, edges).relabel('Gaussian distribution')\n", - "\n", - "gaussian_histogram(100,1) + gaussian_histogram(150,1.5) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Lets now use this in the following generator:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def gaussian_sampler(samples=10, delta=10, scale=1.0):\n", - " np.random.seed(1)\n", - " while True:\n", - " yield gaussian_histogram(samples, scale)\n", - " samples+=delta\n", - " \n", - "gaussian_sampler()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Which allows us to define the following infinite ``DynamicMap``:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "dmap = hv.DynamicMap(gaussian_sampler(), kdims=['step'])\n", - "dmap" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that step is shown as an integer. This is the default behavior and corresponds to the call count (i.e the number of times ``next()`` has been called on the generator. If we want to show the actual number of samples properly, we need our generator to return a (key, element) pair:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def gaussian_sampler_kv(samples=10, delta=10, scale=1.0):\n", - " np.random.seed(1)\n", - " while True:\n", - " yield (samples, gaussian_histogram(samples, scale))\n", - " samples+=delta\n", - " \n", - "hv.DynamicMap(gaussian_sampler_kv(), kdims=['samples'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that if you pause the ``DynamicMap``, you can scrub back to previous frames in the cache. In other words, you can view a limited history of elements already output by the generator, which does *not* re-execute the generator in any way (as it is indeed impossible to rewind generator state). If you have a stateful generator that, say, depends on the current wind speed in Scotland, this history may be misleading, in which case you can simply set the ``cache_size`` parameter to 1." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Multi-dimensional generators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In open mode, elements are naturally serialized by a linear sequence of ``next()`` calls, yet multiple key dimensions can still be defined:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def gaussian_sampler_2D(samples=10, scale=1.0, delta=10):\n", - " np.random.seed(1)\n", - " while True:\n", - " yield ((samples, scale), gaussian_histogram(samples, scale))\n", - " samples=(samples + delta) if scale==2 else samples\n", - " scale = 2 if scale == 1 else 1\n", - " \n", - "dmap = hv.DynamicMap(gaussian_sampler_2D(), kdims=['samples', 'scale'])\n", - "dmap" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we bin the histogram for two different scale values. Above we can visualize this linear sequence of ``next()`` calls, but by casting this open map to a ``HoloMap``, we can obtain a multi-dimensional parameter space that we can freely explore:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "hv.HoloMap(dmap)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that if you ran this notebook using `Run All`, only a single frame will be available in the above cell, with no sliders, but if you ran it interactively and viewed a range of values in the previous cell, you'll have multiple sliders in this cell allowing you to explore whatever range of frames is in the cache from the previous cell.\n", - "\n", - "#### Finite generators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Open mode ``DynamicMaps`` are finite and terminate if ``StopIteration`` is raised. This example terminates when the means of two sets of gaussian samples fall within a certain distance of each other:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def sample_distributions(samples=10, delta=50, tol=0.04):\n", - " np.random.seed(42)\n", - " while True:\n", - " gauss1 = np.random.normal(size=samples)\n", - " gauss2 = np.random.normal(size=samples)\n", - " data = (['A']*samples + ['B']*samples, np.hstack([gauss1, gauss2]))\n", - " diff = abs(gauss1.mean() - gauss2.mean())\n", - " if abs(gauss1.mean() - gauss2.mean()) > tol:\n", - " yield ((samples, diff), hv.BoxWhisker(data, kdims=['Group'], vdims=['Value']))\n", - " else:\n", - " raise StopIteration\n", - " samples+=delta" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "dmap = hv.DynamicMap(sample_distributions(), kdims=['samples', '$\\delta$'])\n", - "dmap" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now if you are familiar with generators in Python, you might be wondering what happens when a finite generator is exhausted. First we should mention that casting a ``DynamicMap`` to a list is always finite, because ``__iter__`` returns the cache instead of a potentially infinite generator:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "list(dmap) # The cache" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we know this ``DynamicMap`` is finite, we can make sure it is exhausted as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "while True:\n", - " try:\n", - " next(dmap) # Returns Image elements\n", - " except StopIteration:\n", - " print(\"The dynamic map is exhausted.\")\n", - " break" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's have a look at the dynamic map:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "dmap" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here, we are given only the text-based representation, to indicate that the generator is exhausted. However, as the process of iteration has populated the cache, we can still view the output as a ``HoloMap`` using ``hv.HoloMap(dmap)`` as before." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Counter mode and temporal state " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Open mode is intended to use live data streams or ongoing simulations with HoloViews. The ``DynamicMap`` will generate live visualizations for as long as new data is requested. Although this works for simple cases, Python generators have problematic limitations that can be resolved using 'counter' mode.\n", - "\n", - "In this example, let's say we have a simulation or data recording where time increases in integer steps:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def time_gen(time=1):\n", - " while True:\n", - " yield time\n", - " time += 1\n", - " \n", - "time = time_gen()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's create two generators that return Images that are a function of the simulation time. Here, they have identical output except one of the outputs includes additive noise:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "ls = np.linspace(0, 10, 200)\n", - "xx, yy = np.meshgrid(ls, ls)\n", - "\n", - "def cells():\n", - " while True:\n", - " t = next(time)\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " yield hv.Image(arr)\n", - "\n", - "def cells_noisy():\n", - " while True:\n", - " t = next(time)\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " yield hv.Image(arr + 0.2*np.random.rand(200,200))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's create a Layout using these two generators:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "hv.DynamicMap(cells(), kdims=['time']) + hv.DynamicMap(cells_noisy(), kdims=['time'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you pause the animation, you'll see that these two outputs are *not* in phase, despite the fact that the generators are defined identically (modulo the additive noise)!\n", - "\n", - "The issue is that generators are used via the ``next()`` interface, and so when either generator is called, the simulation time is increased. In other words, the noisy version in subfigure **B** actually corresponds to a later time than in subfigure **A**.\n", - "\n", - "This is a fundamental issue, as the ``next`` method does not take arguments. What we want is for all the ``DynamicMaps`` presented in a Layout to share a common simulation time, which is only incremented by interaction with the scrubber widget. This is exactly the sort of situation where you want to use counter mode." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Handling time-dependent state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To define a ``DynamicMap`` in counter mode:\n", - "\n", - "* Leave one or more dimensions *unbounded* (as in open mode)\n", - "* Supply a callable (as in bounded mode) that accepts *one* argument\n", - "\n", - "This callable should act in the same way as the generators of open mode, except the output is controlled by the single counter argument." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "ls = np.linspace(0, 10, 200)\n", - "xx, yy = np.meshgrid(ls, ls)\n", - "\n", - "def cells_counter(t):\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " return hv.Image(arr)\n", - "\n", - "def cells_noisy_counter(t):\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " return hv.Image(arr + 0.2*np.random.rand(200,200))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now if we supply these functions instead of generators, **A** and **B** will correctly be in phase:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "hv.DynamicMap(cells_counter, kdims=['time']) + hv.DynamicMap(cells_noisy_counter, kdims=['time'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Unfortunately, an integer counter is often too simple to describe simulation time, which may be a float with real-world units. To address this, we can simply return the actual key values we want along the time dimension, just as was demonstrated in open mode using generators:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "ls = np.linspace(0, 10, 200)\n", - "xx, yy = np.meshgrid(ls, ls)\n", - "\n", - "# Example of a global simulation time\n", - "# typical in many applications\n", - "t = 0 \n", - " \n", - "def cells_counter_kv(c):\n", - " global t\n", - " t = 0.1 * c\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " return (t, hv.Image(arr))\n", - "\n", - "def cells_noisy_counter_kv(c):\n", - " global t\n", - " t = 0.1 * c\n", - " arr = np.sin(xx+t)*np.cos(yy+t)\n", - " return (t, hv.Image(arr + 0.2*np.random.rand(200,200)))\n", - " \n", - "hv.DynamicMap(cells_counter_kv, kdims=['time']) + hv.DynamicMap(cells_noisy_counter_kv, kdims=['time'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "print(\"The global simulation time is now t=%f\" % t)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ensuring that the HoloViews counter maps to a suitable simulation time is the responsibility of the user. However, once a consistent scheme is configured, the callable in each ``DynamicMap`` can specify the desired simulation time. If the requested simulation time is the same as the current simulation time, nothing needs to happen. Otherwise, the simulator can be run forward by the requested amount. In this way, HoloViews can provide a rich graphical interface for controlling and visualizing an external simulator, with very little code required." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Slicing in open and counter mode" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Slicing open and counter mode ``DynamicMaps`` has the exact same semantics as normal ``HoloMap`` slicing, except now the ``.data`` attribute corresponds to the cache. For instance:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def sine_kv_gen(phase=0, freq=0.5):\n", - " while True:\n", - " yield (phase, hv.Image(np.sin(phase + (freq*x**2+freq*y**2))))\n", - " phase+=0.2\n", - " \n", - "dmap = hv.DynamicMap(sine_kv_gen(), kdims=['phase'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's fill the cache with some elements:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "for i in range(21):\n", - " dmap.next()\n", - " \n", - "print(\"Min key value in cache:%s\\nMax key value in cache:%s\" % (min(dmap.keys()), max(dmap.keys())))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "sliced = dmap[1:3.1]\n", - "print(\"Min key value in cache:%s\\nMax key value in cache:%s\" % (min(sliced.keys()), max(sliced.keys())))" + "This grid shows a range of frequencies `f` on the x axis, a range of the first phase variable `ph` on the `y` axis, and a range of different `ph2` phases as overlays within each location in the grid. As you can see, these techniques can help you visualize multidimensional parameter spaces compactly and conveniently.\n" ] }, { @@ -1297,20 +745,22 @@ }, "outputs": [], "source": [ - "%%opts Image {+axiswise}\n", - "ls = np.linspace(0, 10, 200)\n", - "xx, yy = np.meshgrid(ls, ls)\n", + "# NEEDS UPDATING TO NON-GENERATOR VERSION\n", + "\n", + "# %%opts Image {+axiswise}\n", + "# ls = np.linspace(0, 10, 200)\n", + "# xx, yy = np.meshgrid(ls, ls)\n", "\n", - "def cells(vrange=False):\n", - " \"The range is set on the value dimension when vrange is True \"\n", - " time = time_gen()\n", - " while True:\n", - " t = next(time)\n", - " arr = t*np.sin(xx+t)*np.cos(yy+t)\n", - " vdims=[hv.Dimension('Intensity', range=(0,10))] if vrange else ['Intensity']\n", - " yield hv.Image(arr, vdims=vdims)\n", + "# def cells(vrange=False):\n", + "# \"The range is set on the value dimension when vrange is True \"\n", + "# time = time_gen()\n", + "# while True:\n", + "# t = next(time)\n", + "# arr = t*np.sin(xx+t)*np.cos(yy+t)\n", + "# vdims=[hv.Dimension('Intensity', range=(0,10))] if vrange else ['Intensity']\n", + "# yield hv.Image(arr, vdims=vdims)\n", "\n", - "hv.DynamicMap(cells(vrange=False), kdims=['time']) + hv.DynamicMap(cells(vrange=True), kdims=['time'])" + "# hv.DynamicMap(cells(vrange=False), kdims=['time']) + hv.DynamicMap(cells(vrange=True), kdims=['time'])" ] }, {