From 09a919221667e5638f931cef9ce3f6d44dfbb2e1 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 10 May 2017 13:59:04 +0100 Subject: [PATCH 01/18] Added periodic helper utility to core.util --- holoviews/core/util.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index f68aba71ff..50ab9035d4 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -10,6 +10,7 @@ from functools import partial from contextlib import contextmanager +from threading import Thread, Event import numpy as np import param @@ -78,6 +79,40 @@ def default(self, obj): return id(obj) +class periodic(Thread): + """ + Run a callback count times with a given period without blocking. + """ + + def __init__(self, callback, period, count): + super(periodic, self).__init__() + self.period = period + self.callback = callback + self.count = count + self.counter = 0 + self.completed = Event() + self.start() + + def stop(self): + self.completed.set() + + def __repr__(self): + return 'periodic(%s, %s, %d)' % (self.period, + callable_name(self.callback), + self.count) + def __str__(self): + return repr(self) + + def run(self): + while not self.completed.is_set(): + self.completed.wait(self.period) + self.callback(self.counter) + self.counter += 1 + + if self.counter == self.count: + self.completed.set() + + def deephash(obj): """ From 39e6e242d1248d3a4c1533eee12071f7bab7c807 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 10 May 2017 14:00:23 +0100 Subject: [PATCH 02/18] Added periodic_events method to DynamicMap --- holoviews/core/spaces.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index a7ec9d0723..18fff92ea9 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -600,6 +600,8 @@ class DynamicMap(HoloMap): # Declare that callback is a positional parameter (used in clone) __pos_params = ['callback'] + _periodic_util = util.periodic # Utility used by periodic_events method + kdims = param.List(default=[], constant=True, doc=""" The key dimensions of a DynamicMap map to the arguments of the callback. This mapping can be by position or by name.""") @@ -755,6 +757,25 @@ def event(self, **kwargs): Stream.trigger(self.streams) + def periodic_events(self, period, count, param_fn=None): + """ + Run a non-blocking loop that updates the stream parameters using + the event method. Runs count times with the specified period. + + If param_fn is not specified, the event method is called without + arguments. If it is specified, it must be a callable accepting a + single argument (the iteration count) that returns a dictionary + of the new stream values to be passed to the event method. + + The returned object allows the loop to be stopped at any time + using the stop() method. + """ + def inner(i): + kwargs = {} if param_fn is None else param_fn(i) + self.event(**kwargs) + + return self._periodic_util(inner, period, count) + def _style(self, retval): """ Use any applicable OptionTree of the DynamicMap to apply options From 640baf0b893aa985cf3df7743efcff4fdb2e1815 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 10 May 2017 14:16:40 +0100 Subject: [PATCH 03/18] Validate periodic count argument and updated docstring --- holoviews/core/spaces.py | 5 +++-- holoviews/core/util.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 18fff92ea9..ecea7d2c02 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -760,7 +760,8 @@ def event(self, **kwargs): def periodic_events(self, period, count, param_fn=None): """ Run a non-blocking loop that updates the stream parameters using - the event method. Runs count times with the specified period. + the event method. Runs count times with the specified period. If + count is None, runs indefinitely. If param_fn is not specified, the event method is called without arguments. If it is specified, it must be a callable accepting a @@ -768,7 +769,7 @@ def periodic_events(self, period, count, param_fn=None): of the new stream values to be passed to the event method. The returned object allows the loop to be stopped at any time - using the stop() method. + by calling its stop() method. """ def inner(i): kwargs = {} if param_fn is None else param_fn(i) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 50ab9035d4..8607267cc6 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -85,6 +85,12 @@ class periodic(Thread): """ def __init__(self, callback, period, count): + + if isinstance(count, int): + if count < 0: raise ValueError('Count value must be positive') + elif not type(count) is type(None): + raise ValueError('Count value must be a positive integer or None') + super(periodic, self).__init__() self.period = period self.callback = callback From c51c275217deda1186b67143f820c668df589592 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 14 May 2017 02:32:27 +0100 Subject: [PATCH 04/18] Defined Dynamic.periodic as an object with a stop method --- holoviews/core/spaces.py | 70 +++++++++++++++++++++++++++------------- holoviews/core/util.py | 1 - 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index ecea7d2c02..4d770782a5 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -17,6 +17,8 @@ from .options import Store, StoreOptions from ..streams import Stream + + class HoloMap(UniformNdMapping, Overlayable): """ A HoloMap can hold any number of DataLayers indexed by a list of @@ -589,6 +591,50 @@ def dynamicmap_memoization(callable_obj, streams): callable_obj.memoize = memoization_state + +class periodic(object): + """ + Implements the utility of the same name on DynamicMap. + + Used to defined periodic event updates that can be started and + stopped. + """ + _periodic_util = util.periodic + + def __init__(self, dmap): + self.dmap = dmap + self.instances = [] + + def __call__(self, period, count, param_fn=None): + """ + Run a non-blocking loop that updates the stream parameters using + the event method. Runs count times with the specified period. If + count is None, runs indefinitely. + + If param_fn is not specified, the event method is called without + arguments. If it is specified, it must be a callable accepting a + single argument (the iteration count) that returns a dictionary + of the new stream values to be passed to the event method. + """ + def inner(i): + kwargs = {} if param_fn is None else param_fn(i) + self.dmap.event(**kwargs) + + instance = self._periodic_util(inner, period, count) + instance.start() + self.instances.append(instance) + + def stop(self): + "Stop and clear all periodic instances on the DynamicMap." + for instance in self.instances: + instance.stop() + self.instances = [] + + def __str__(self): + return "" + + + class DynamicMap(HoloMap): """ A DynamicMap is a type of HoloMap where the elements are dynamically @@ -600,8 +646,6 @@ class DynamicMap(HoloMap): # Declare that callback is a positional parameter (used in clone) __pos_params = ['callback'] - _periodic_util = util.periodic # Utility used by periodic_events method - kdims = param.List(default=[], constant=True, doc=""" The key dimensions of a DynamicMap map to the arguments of the callback. This mapping can be by position or by name.""") @@ -663,6 +707,7 @@ def __init__(self, callback, initial_items=None, **params): if stream.source is None: stream.source = self self.redim = redim(self, mode='dynamic') + self.periodic = periodic(self) @property def unbounded(self): @@ -756,27 +801,6 @@ def event(self, **kwargs): Stream.trigger(self.streams) - - def periodic_events(self, period, count, param_fn=None): - """ - Run a non-blocking loop that updates the stream parameters using - the event method. Runs count times with the specified period. If - count is None, runs indefinitely. - - If param_fn is not specified, the event method is called without - arguments. If it is specified, it must be a callable accepting a - single argument (the iteration count) that returns a dictionary - of the new stream values to be passed to the event method. - - The returned object allows the loop to be stopped at any time - by calling its stop() method. - """ - def inner(i): - kwargs = {} if param_fn is None else param_fn(i) - self.event(**kwargs) - - return self._periodic_util(inner, period, count) - def _style(self, retval): """ Use any applicable OptionTree of the DynamicMap to apply options diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 8607267cc6..a53f393655 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -97,7 +97,6 @@ def __init__(self, callback, period, count): self.count = count self.counter = 0 self.completed = Event() - self.start() def stop(self): self.completed.set() From 987af8e71846a79d292136b3a3ae25936edfaa22 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Sun, 14 May 2017 13:39:57 +0100 Subject: [PATCH 05/18] The periodic method now handles a single periodic runner instance --- holoviews/core/spaces.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 4d770782a5..024b5407f6 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -603,7 +603,7 @@ class periodic(object): def __init__(self, dmap): self.dmap = dmap - self.instances = [] + self.instance = None def __call__(self, period, count, param_fn=None): """ @@ -616,19 +616,22 @@ def __call__(self, period, count, param_fn=None): single argument (the iteration count) that returns a dictionary of the new stream values to be passed to the event method. """ + + if self.instance is not None and not self.instance.completed.is_set(): + raise RuntimeError('Periodic process already running. ' + 'Wait until it completes or call ' + 'stop() before running a new periodic process') def inner(i): kwargs = {} if param_fn is None else param_fn(i) self.dmap.event(**kwargs) instance = self._periodic_util(inner, period, count) instance.start() - self.instances.append(instance) + self.instance= instance def stop(self): - "Stop and clear all periodic instances on the DynamicMap." - for instance in self.instances: - instance.stop() - self.instances = [] + "Stop and the periodic process." + self.instance.stop() def __str__(self): return "" From 40562a150da0c8494a589b18879c922fa492d3c2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 15 May 2017 03:23:16 +0100 Subject: [PATCH 06/18] Added periodic functionality for bokeh server --- holoviews/core/spaces.py | 19 ++++++---- holoviews/plotting/bokeh/renderer.py | 11 ++++-- holoviews/plotting/bokeh/util.py | 56 ++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 024b5407f6..059bbb4c71 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -559,18 +559,23 @@ def __call__(self): raise +def get_nested_dmaps(dmap): + """ + Get all DynamicMaps referenced by the supplied DynamicMap's callback. + """ + dmaps = [dmap] + for o in dmap.callback.inputs: + if isinstance(o, DynamicMap): + dmaps.extend(get_nested_streams(o)) + return list(set(dmaps)) + + def get_nested_streams(dmap): """ Get all (potentially nested) streams from DynamicMap with Callable callback. """ - layer_streams = list(dmap.streams) - if not isinstance(dmap.callback, Callable): - return list(set(layer_streams)) - for o in dmap.callback.inputs: - if isinstance(o, DynamicMap): - layer_streams += get_nested_streams(o) - return list(set(layer_streams)) + return list({s for dmap in get_nested_dmaps(dmap) for s in dmap.streams}) @contextmanager diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py index 37c71d31e8..4a443ffa7f 100644 --- a/holoviews/plotting/bokeh/renderer.py +++ b/holoviews/plotting/bokeh/renderer.py @@ -16,9 +16,10 @@ from ...core import Store, HoloMap from ..comms import JupyterComm, Comm +from ..plot import GenericElementPlot from ..renderer import Renderer, MIME_TYPES from .widgets import BokehScrubberWidget, BokehSelectionWidget, BokehServerWidgets -from .util import compute_static_patch, serialize_json +from .util import compute_static_patch, serialize_json, attach_periodic @@ -122,9 +123,11 @@ def server_doc(self, plot, doc=None): if doc is None: doc = curdoc() if isinstance(plot, BokehServerWidgets): - plot.plot.document = doc - else: - plot.document = doc + plot = plot.plot + plot.document = doc + plot.traverse(lambda x: attach_periodic(plot), + [GenericElementPlot]) + doc.add_root(plot.state) return doc diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 0dc159b5a2..3862037b5a 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -27,6 +27,7 @@ from ...core.options import abbreviated_exception from ...core.overlay import Overlay from ...core.util import basestring, unique_array +from ...core.spaces import get_nested_dmaps, DynamicMap from ..util import dim_axis_label, rgb2hex @@ -664,3 +665,58 @@ def categorize_array(array, dim): treats as a categorical suffix. """ return np.array([dim.pprint_value(x).replace(':', ';') for x in array]) + + +class periodic(object): + """ + Mocks the API of periodic Thread in hv.core.util, allowing a smooth + API transition on bokeh server. + """ + + def __init__(self, document): + self.document = document + self.inner = None + self.count = 0 + + def start(self): + if self.document is None: + raise RuntimeError('periodic was registered to be run on bokeh' + 'server but no document was found.') + self.document.add_periodic_callback(self.callback, self.period) + + def __call__(self, function, period, count): + self.inner = function + self.period = period*1000. + self.count = count + return self + + def callback(self): + if self.count is None: + self.inner(self.count) + if self.count: + self.inner(self.count) + self.count -= 1 + else: + self.stop() + + def stop(self): + self.count = 0 + self.document.remove_periodic_callback(self.callback) + + @property + def completed(self): + class completed(object): + @classmethod + def is_set(cls): + return self.count == 0 + return completed + + +def attach_periodic(plot): + """ + Attaches plot refresh to all streams on the object. + """ + def append_refresh(dmap): + for dmap in get_nested_dmaps(dmap): + dmap.periodic._periodic_util = periodic(plot.document) + return plot.hmap.traverse(append_refresh, [DynamicMap]) From 4f3593210edf79ce34996be843ecb5a9ab94bfc6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 15 May 2017 15:36:03 +0100 Subject: [PATCH 07/18] Small bug fix for get_nested_dmaps utility --- holoviews/core/spaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 059bbb4c71..490d7c2ab3 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -566,7 +566,7 @@ def get_nested_dmaps(dmap): dmaps = [dmap] for o in dmap.callback.inputs: if isinstance(o, DynamicMap): - dmaps.extend(get_nested_streams(o)) + dmaps.extend(get_nested_dmaps(o)) return list(set(dmaps)) From 2430cba2cbd73fb7fbbc6441e4525672bc94bcc7 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 15 May 2017 22:53:59 +0100 Subject: [PATCH 08/18] Fixed repr of threaded periodic utility --- holoviews/core/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index a53f393655..93dd38a676 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -102,7 +102,7 @@ def stop(self): self.completed.set() def __repr__(self): - return 'periodic(%s, %s, %d)' % (self.period, + return 'periodic(%s, %s, %s)' % (self.period, callable_name(self.callback), self.count) def __str__(self): From dcc9cb4f8ca38c023b6764766e64460e5ed2ce16 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 15 May 2017 22:54:45 +0100 Subject: [PATCH 09/18] Defined completed property on threaded periodic utility --- holoviews/core/spaces.py | 2 +- holoviews/core/util.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 490d7c2ab3..6a0d472e80 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -622,7 +622,7 @@ def __call__(self, period, count, param_fn=None): of the new stream values to be passed to the event method. """ - if self.instance is not None and not self.instance.completed.is_set(): + if self.instance is not None and not self.instance.completed: raise RuntimeError('Periodic process already running. ' 'Wait until it completes or call ' 'stop() before running a new periodic process') diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 93dd38a676..fd65686f5f 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -96,10 +96,14 @@ def __init__(self, callback, period, count): self.callback = callback self.count = count self.counter = 0 - self.completed = Event() + self._completed = Event() + + @property + def completed(self): + return self._completed.is_set() def stop(self): - self.completed.set() + self._completed.set() def __repr__(self): return 'periodic(%s, %s, %s)' % (self.period, @@ -109,13 +113,13 @@ def __str__(self): return repr(self) def run(self): - while not self.completed.is_set(): - self.completed.wait(self.period) + while not self.completed: + self._completed.wait(self.period) self.callback(self.counter) self.counter += 1 if self.counter == self.count: - self.completed.set() + self._completed.set() From 16e41e289ba42c5c0d1311e8b33b96453aa65195 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 15 May 2017 23:07:34 +0100 Subject: [PATCH 10/18] Made argument ordering of periodic utility more consistent --- holoviews/core/spaces.py | 2 +- holoviews/core/util.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 6a0d472e80..11e3392d84 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -630,7 +630,7 @@ def inner(i): kwargs = {} if param_fn is None else param_fn(i) self.dmap.event(**kwargs) - instance = self._periodic_util(inner, period, count) + instance = self._periodic_util(period, count, inner) instance.start() self.instance= instance diff --git a/holoviews/core/util.py b/holoviews/core/util.py index fd65686f5f..89507a87b9 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -84,7 +84,7 @@ class periodic(Thread): Run a callback count times with a given period without blocking. """ - def __init__(self, callback, period, count): + def __init__(self, period, count, callback): if isinstance(count, int): if count < 0: raise ValueError('Count value must be positive') @@ -107,8 +107,8 @@ def stop(self): def __repr__(self): return 'periodic(%s, %s, %s)' % (self.period, - callable_name(self.callback), - self.count) + self.count, + callable_name(self.callback)) def __str__(self): return repr(self) From ce138a1a750e38573f517159e284895205f4622b Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 15 May 2017 23:33:57 +0100 Subject: [PATCH 11/18] Rewrote bokeh periodic utility to be consistent with threaded version --- holoviews/plotting/bokeh/util.py | 56 ++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 3862037b5a..bf3940cf6a 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -675,41 +675,55 @@ class periodic(object): def __init__(self, document): self.document = document - self.inner = None - self.count = 0 + self.callback = None + self.period = None + self.count = None + self.counter = None + + @property + def completed(self): + return self.counter is None def start(self): if self.document is None: raise RuntimeError('periodic was registered to be run on bokeh' 'server but no document was found.') - self.document.add_periodic_callback(self.callback, self.period) + self.document.add_periodic_callback(self._periodic_callback, self.period) - def __call__(self, function, period, count): - self.inner = function + def __call__(self, period, count, callback): + if isinstance(count, int): + if count < 0: raise ValueError('Count value must be positive') + elif not type(count) is type(None): + raise ValueError('Count value must be a positive integer or None') + + self.callback = callback self.period = period*1000. self.count = count + self.counter = 0 return self - def callback(self): - if self.count is None: - self.inner(self.count) - if self.count: - self.inner(self.count) - self.count -= 1 - else: + def _periodic_callback(self): + self.callback(self.counter) + self.counter += 1 + + if self.counter == self.count: self.stop() def stop(self): - self.count = 0 - self.document.remove_periodic_callback(self.callback) + self.counter = None + try: + self.document.remove_periodic_callback(self._periodic_callback) + except ValueError: # Already stopped + pass + + def __repr__(self): + return 'periodic(%s, %s, %s)' % (self.period, + self.count, + callable_name(self.callback)) + def __str__(self): + return repr(self) + - @property - def completed(self): - class completed(object): - @classmethod - def is_set(cls): - return self.count == 0 - return completed def attach_periodic(plot): From b28d134112ea1fd9f9f85bf251100723ce2954d4 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Mon, 15 May 2017 18:10:28 -0500 Subject: [PATCH 12/18] Fixed typo --- holoviews/core/spaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 11e3392d84..da2fd63e23 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -635,7 +635,7 @@ def inner(i): self.instance= instance def stop(self): - "Stop and the periodic process." + "Stop the periodic process." self.instance.stop() def __str__(self): From 1e7c73ca119af43d44ae1cb3b9db868bd32aa027 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 00:27:37 +0100 Subject: [PATCH 13/18] Added timeout and block argument to threaded periodic utility --- holoviews/core/spaces.py | 5 +++-- holoviews/core/util.py | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index da2fd63e23..3a3a2adcd3 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -610,7 +610,7 @@ def __init__(self, dmap): self.dmap = dmap self.instance = None - def __call__(self, period, count, param_fn=None): + def __call__(self, period, count, param_fn=None, timeout=None, block=True): """ Run a non-blocking loop that updates the stream parameters using the event method. Runs count times with the specified period. If @@ -630,7 +630,8 @@ def inner(i): kwargs = {} if param_fn is None else param_fn(i) self.dmap.event(**kwargs) - instance = self._periodic_util(period, count, inner) + instance = self._periodic_util(period, count, inner, + timeout=timeout, block=block) instance.start() self.instance= instance diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 89507a87b9..259191de9d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1,4 +1,5 @@ import os, sys, warnings, operator +import time import types import numbers import inspect @@ -84,7 +85,7 @@ class periodic(Thread): Run a callback count times with a given period without blocking. """ - def __init__(self, period, count, callback): + def __init__(self, period, count, callback, timeout=None, block=False): if isinstance(count, int): if count < 0: raise ValueError('Count value must be positive') @@ -96,13 +97,24 @@ def __init__(self, period, count, callback): self.callback = callback self.count = count self.counter = 0 + self.block = block + self.timeout = timeout self._completed = Event() + self._start_time = None @property def completed(self): return self._completed.is_set() + def start(self): + self._start_time = time.time() + if self.block is False: + super(periodic,self).start() + else: + self.run() + def stop(self): + self.timeout = None self._completed.set() def __repr__(self): @@ -114,12 +126,17 @@ def __str__(self): def run(self): while not self.completed: - self._completed.wait(self.period) + if not self.block: + self._completed.wait(self.period) self.callback(self.counter) self.counter += 1 + if self.timeout is not None: + dt = (time.time() - self._start_time) + if dt > self.timeout: + self.stop() if self.counter == self.count: - self._completed.set() + self.stop() From cadac5b7fbae018cb0f34061644838c6dc9238d1 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 00:34:43 +0100 Subject: [PATCH 14/18] Added timeout argument support to the bokeh periodic utility --- holoviews/plotting/bokeh/util.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index bf3940cf6a..0bfd4be7e8 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -1,4 +1,4 @@ -import itertools, inspect, re +import itertools, inspect, re, time from distutils.version import LooseVersion from collections import defaultdict @@ -679,18 +679,21 @@ def __init__(self, document): self.period = None self.count = None self.counter = None + self._start_time = None + self.timeout = None @property def completed(self): return self.counter is None def start(self): + self._start_time = time.time() if self.document is None: raise RuntimeError('periodic was registered to be run on bokeh' 'server but no document was found.') self.document.add_periodic_callback(self._periodic_callback, self.period) - def __call__(self, period, count, callback): + def __call__(self, period, count, callback, timeout=None, block=False): if isinstance(count, int): if count < 0: raise ValueError('Count value must be positive') elif not type(count) is type(None): @@ -698,6 +701,7 @@ def __call__(self, period, count, callback): self.callback = callback self.period = period*1000. + self.timeout = timeout self.count = count self.counter = 0 return self @@ -706,11 +710,16 @@ def _periodic_callback(self): self.callback(self.counter) self.counter += 1 + if self.timeout is not None: + dt = (time.time() - self._start_time) + if dt > self.timeout: + self.stop() if self.counter == self.count: self.stop() def stop(self): self.counter = None + self.timeout = None try: self.document.remove_periodic_callback(self._periodic_callback) except ValueError: # Already stopped From 95537cc4326c50d5eaabc73fffa39e6b82ea87f7 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 00:50:35 +0100 Subject: [PATCH 15/18] Updated Streams tutorial to describe the DynamicMap periodic method --- doc/Tutorials/Streams.ipynb | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/doc/Tutorials/Streams.ipynb b/doc/Tutorials/Streams.ipynb index d6cee8c68e..9394d25d83 100644 --- a/doc/Tutorials/Streams.ipynb +++ b/doc/Tutorials/Streams.ipynb @@ -597,6 +597,13 @@ "dmap" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Stream event update loops" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -620,7 +627,41 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This approach of using an empty stream works in an exactly analogous fashion for callables that take no arguments. In both cases, the ``DynamicMap`` ``next()`` method is enabled:" + "Note that there is a better way to run loops that drive ``dmap.event()`` which supports a ``period`` (in seconds) between updates and a ``timeout`` argument (also in seconds):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap.periodic(0.1, 1000, timeout=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this generator example, ``event`` does not require any arguments but you can set the ``param_fn`` argument to a callable that takes an iteration counter and returns a dictionary for setting the stream parameters. In addition you can use ``block=False`` to avoid blocking the notebook using a threaded loop. This can be very useful although there can have two downsides 1. all running visualizations using non-blocking updates will be competing for computing resources 2. if you override a variable that the thread is actively using, there can be issues with maintaining consistent state in the notebook.\n", + "\n", + "Generally, the ``periodic`` utility is recommended for all such event update loops and it will be used instead of explicit loops in the rest of the tutorials involving streams.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using ``next()``" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The approach shown above of using an empty stream works in an exactly analogous fashion for callables that take no arguments. In both cases, the ``DynamicMap`` ``next()`` method is enabled:" ] }, { From 3bd2bbd361a0b6e5b42db8e8b437ee54f241d839 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 01:51:45 +0100 Subject: [PATCH 16/18] Blocking periodic call now respects period value --- holoviews/core/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 259191de9d..a642bf291d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -126,7 +126,9 @@ def __str__(self): def run(self): while not self.completed: - if not self.block: + if self.block: + time.sleep(self.period) + else: self._completed.wait(self.period) self.callback(self.counter) self.counter += 1 From 395717a96c346837bf63fdcf3c81baf045acc124 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 01:52:23 +0100 Subject: [PATCH 17/18] Added five unit tests of DynamicMap periodic method --- tests/testdynamic.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 1c1131fd8b..4efa7e29a8 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -1,4 +1,5 @@ from collections import deque +import time import numpy as np from holoviews import Dimension, NdLayout, GridSpace, Layout @@ -560,6 +561,76 @@ def history_callback(x, y, history=deque(maxlen=10)): self.assertEqual(yresets, 2) +class TestPeriodicStreamUpdate(ComparisonTestCase): + + def test_periodic_counter_blocking(self): + class Counter(object): + def __init__(self): + self.count = 0 + def __call__(self): + self.count += 1 + return Curve([1,2,3]) + + next_stream = Stream.define('Next')() + counter = Counter() + dmap = DynamicMap(counter, streams=[next_stream]) + # Add stream subscriber mocking plot + next_stream.add_subscriber(lambda **kwargs: dmap[()]) + dmap.periodic(0.01, 100) + self.assertEqual(counter.count, 100) + + def test_periodic_param_fn_blocking(self): + def callback(x): return Curve([1,2,3]) + xval = Stream.define('x',x=0)() + dmap = DynamicMap(callback, streams=[xval]) + # Add stream subscriber mocking plot + xval.add_subscriber(lambda **kwargs: dmap[()]) + dmap.periodic(0.01, 100, param_fn=lambda i: {'x':i}) + self.assertEqual(xval.x, 99) + + def test_periodic_param_fn_non_blocking(self): + def callback(x): return Curve([1,2,3]) + xval = Stream.define('x',x=0)() + dmap = DynamicMap(callback, streams=[xval]) + # Add stream subscriber mocking plot + xval.add_subscriber(lambda **kwargs: dmap[()]) + + dmap.periodic(0.0001, 1000, param_fn=lambda i: {'x':i}, block=False) + self.assertNotEqual(xval.x, 1000) + for i in range(1000): + time.sleep(0.01) + if dmap.periodic.instance.completed: + break + dmap.periodic.stop() + self.assertEqual(xval.x, 999) + + def test_periodic_param_fn_blocking_period(self): + def callback(x): + return Curve([1,2,3]) + xval = Stream.define('x',x=0)() + dmap = DynamicMap(callback, streams=[xval]) + # Add stream subscriber mocking plot + xval.add_subscriber(lambda **kwargs: dmap[()]) + start = time.time() + dmap.periodic(0.5, 10, param_fn=lambda i: {'x':i}, block=True) + end = time.time() + print end-start + self.assertEqual((end - start) > 5, True) + + + def test_periodic_param_fn_blocking_timeout(self): + def callback(x): + return Curve([1,2,3]) + xval = Stream.define('x',x=0)() + dmap = DynamicMap(callback, streams=[xval]) + # Add stream subscriber mocking plot + xval.add_subscriber(lambda **kwargs: dmap[()]) + start = time.time() + dmap.periodic(0.5, 100, param_fn=lambda i: {'x':i}, timeout=3) + end = time.time() + self.assertEqual((end - start) < 5, True) + + class DynamicCollate(ComparisonTestCase): def test_dynamic_collate_layout(self): From fa983f6bdf53035ebc1df9c3a410314c5fac163c Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 16 May 2017 02:03:03 +0100 Subject: [PATCH 18/18] Periodic counter now starts at one instead of zero --- holoviews/core/spaces.py | 5 +++-- holoviews/core/util.py | 2 +- tests/testdynamic.py | 5 ++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 3a3a2adcd3..bba2f4dc16 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -618,8 +618,9 @@ def __call__(self, period, count, param_fn=None, timeout=None, block=True): If param_fn is not specified, the event method is called without arguments. If it is specified, it must be a callable accepting a - single argument (the iteration count) that returns a dictionary - of the new stream values to be passed to the event method. + single argument (the iteration count, starting at 1) that + returns a dictionary of the new stream values to be passed to + the event method. """ if self.instance is not None and not self.instance.completed: diff --git a/holoviews/core/util.py b/holoviews/core/util.py index a642bf291d..bb4e9bf23e 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -130,8 +130,8 @@ def run(self): time.sleep(self.period) else: self._completed.wait(self.period) - self.callback(self.counter) self.counter += 1 + self.callback(self.counter) if self.timeout is not None: dt = (time.time() - self._start_time) diff --git a/tests/testdynamic.py b/tests/testdynamic.py index 4efa7e29a8..da557a7113 100644 --- a/tests/testdynamic.py +++ b/tests/testdynamic.py @@ -586,7 +586,7 @@ def callback(x): return Curve([1,2,3]) # Add stream subscriber mocking plot xval.add_subscriber(lambda **kwargs: dmap[()]) dmap.periodic(0.01, 100, param_fn=lambda i: {'x':i}) - self.assertEqual(xval.x, 99) + self.assertEqual(xval.x, 100) def test_periodic_param_fn_non_blocking(self): def callback(x): return Curve([1,2,3]) @@ -602,7 +602,7 @@ def callback(x): return Curve([1,2,3]) if dmap.periodic.instance.completed: break dmap.periodic.stop() - self.assertEqual(xval.x, 999) + self.assertEqual(xval.x, 1000) def test_periodic_param_fn_blocking_period(self): def callback(x): @@ -614,7 +614,6 @@ def callback(x): start = time.time() dmap.periodic(0.5, 10, param_fn=lambda i: {'x':i}, block=True) end = time.time() - print end-start self.assertEqual((end - start) > 5, True)