diff --git a/doc/Tutorials/Dynamic_Map.ipynb b/doc/Tutorials/Dynamic_Map.ipynb new file mode 100644 index 0000000000..21111ac71d --- /dev/null +++ b/doc/Tutorials/Dynamic_Map.ipynb @@ -0,0 +1,1086 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [Containers](Containers.ipynb) Tutorial introduced the [HoloMap](Containers.ipynb#HoloMap), a core HoloViews datastructure that allows easy exploration of parameter spaces. The essence of a HoloMap is that it contains a collection of [Elements](Elements.ipynb) that you can easily visualize and slice.\n", + "\n", + "HoloMaps are containers that hold elements at sampled points in a multidimensional space. Although this makes them useful for exploring high-dimensional parameter spaces, they can very quickly consume huge amounts of memory as a result. For instance, a hundred samples along four orthogonal dimensions would need a HoloMap containing a hundred *million* elements. This highlights some of the limitations of ``HoloMaps``:\n", + "\n", + "* HoloMaps may require the generation of millions of elements before they are fully defined.\n", + "* HoloMaps can easily exhaust all the memory available to Python.\n", + "* HoloMaps can simultaneously exhaust all the memory in the browser when displayed.\n", + "* Static export of a notebook containing HoloMaps can result in impractically large HTML files.\n", + "\n", + "The ``DynamicMap`` addresses these issues by computing and displaying elements dynamically, allowing exploration of much larger datasets:\n", + "\n", + "* DynamicMaps generate elements on the fly allowing the process of exploration to begin immediately.\n", + "* DynamicMaps do not require fixed sampling, allowing exploration of parameters with arbitrary resolution.\n", + "* DynamicMaps are lazy in the sense they only compute only as much data as the user wishes to explore.\n", + "\n", + "The limitations of ``DynamicMaps`` are:\n", + "\n", + "* DynamicMaps require a live notebook server and cannot be directly exported to static HTML.\n", + "* DynamicMaps do not hold data which reduces the utility of a pickling DynamicMap.\n", + "* DynamicMaps are often stateful as are their element caches.\n", + "\n", + "Now we have enumerated the pros and cons of DynamicMaps, let's create one!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
To use visualize and use a DynamicMap you need to be running a live Jupyter server.
This tutorial assumes that it will be run in a live notebook environment.
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ``DynamicMap`` " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's start by importing HoloViews and loading the extension:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import holoviews as hv\n", + "import numpy as np\n", + "hv.notebook_extension()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now create the ``DynamicMap`` equivalent of the ``HoloMap`` introduced in the [Containers Tutorial](Containers.ipynb#HoloMap). The ``HoloMap`` in that tutorial consisted of ``Image`` elements containing sine ring arrays s defined by the ``sine_array`` function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "x,y = np.mgrid[-50:51, -50:51] * 0.1\n", + "\n", + "def sine_array(phase, freq):\n", + " return np.sin(phase + (freq*x**2+freq*y**2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This function returns NumPy arrays when called:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sine_array(0,1).shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a ``DynamicMap`` we will need a function that returns HoloViews elements. This is easy as a trivial extension of the ``sine_array`` function allows it to output [Image](Elements.ipynb#Image) elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def sine_image(phase, freq):\n", + " return hv.Image(np.sin(phase + (freq*x**2+freq*y**2)))\n", + "\n", + "sine_image(0,1) + sine_image(0.5,2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can demonstrate the first type of exploration enabled by a ``DynamicMap`` we call 'bounded' mode." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bounded mode" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A 'closed' mode ``DynamicMap`` is simply one where all the key dimensions have finite bounds. Bounded mode has the following properties:\n", + "\n", + "* The limits of the space and/or the allowable values must be declared for all the key dimensions (unless [sampled mode](#SampledMode) is enabled).\n", + "* You can explore within the declared bounds at any resolution.\n", + "* The ``DynamicMap`` is defined using a callable that *must* be a function of its arguments (i.e the output is strictly determined by the input arguments).\n", + "\n", + "We can now create a DynamicMap by simply declaring the ranges of the two dimensions and passing the ``sine_image`` function as the ``.data``:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap = hv.DynamicMap(sine_image, kdims=[hv.Dimension('phase',range=(0, np.pi)),\n", + " hv.Dimension('frequency', range=(0.01,np.pi))])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This object is created instantly as no data has been generated yet. We can now look at the repr of this object: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "repr(dmap)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All ``DynamicMaps`` will look similar, only differing in the listed dimensions. Now let's see how this dynamic map visualizes itself:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with every other component in HoloViews, you can generate ``Layouts`` using the ``+`` operator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap + hv.DynamicMap(sine_image, kdims=[hv.Dimension('phase',range=(0, np.pi)),\n", + " hv.Dimension('frequency', range=(0.01,np.pi))])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As both elements are ``DynamicMaps`` with the same dimension ranges, the continuous sliders are retained. If a ``HoloMap`` is used the sliders will snap to the available samples across the ``HoloMaps`` in the layout. For bounded ``DynamicMaps`` that do not require ranges to be declared, see [sampled mode](#SampledMode)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you are running this tutorial in a live notebook, you should now see something that looks like the ``HoloMap`` in the [Containers Tutorial](Containers.ipynb#HoloMap). ``DynamicMap`` is in fact a subclass of ``HoloMap`` with some crucial differences:\n", + "\n", + "* You can now pick any value of **phase** or **frequency** up to the precision allowed by the slider.\n", + "* What you see in the cell above will not be exported in any HTML snapshot of the notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Using your own callable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use any callable to define a ``DynamicMap`` in closed mode. A valid ``DynamicMap`` is defined by the following criteria:\n", + "\n", + "* There must be as many positional arguments in the callable signature as key dimensions.\n", + "* The argument order in the callable signature must match the order of the declared key dimensions.\n", + "* All key dimensions are defined with a bounded ``range`` or ``values`` parameter (for categorical dimensions)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The ``DynamicMap`` cache" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above we mentioned that ``DynamicMap`` is an instance of ``HoloMap``. Does this means it has a ``.data`` attribute?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap.data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is exactly the same sort of ``.data`` as the equivalent ``HoloMap`` except this value will vary according to how much you explored the parameter space of ``dmap`` using the sliders above. In a ``HoloMap``, ``.data`` contains a defined sampling along the different dimensions whereas in a ``DynamicMap``, the ``.data`` is the the *cache*.\n", + "\n", + "The cache serves two purposes:\n", + "\n", + "* Avoids recomputation of an element should we revisit a particular point in the parameter space. This works well for categorical data but doesn't help much when using continuous sliders.\n", + "* Records the space that has been explored with the ``DynamicMap`` when converting to a ``HoloMap``.\n", + "* Ensures a finite history of generator output when using [open mode](#OpenMode) together with infinite generators.\n", + "\n", + "We can always convert *any* ``DynamicMap`` directly to a ``HoloMap`` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "hv.HoloMap(dmap)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is in fact equivalent to declaring a HoloMap with the same parameters using ``dmap.data`` as input.\n", + "\n", + "Although creating a HoloMap this way is easy, the result is poorly controlled as the keys in the HoloMap are defined by how you moved the sliders around. This can be easily rectified using the same key selection semantics of ``HoloMap`` to define exactly which elements are sampled in the cache:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap[{(0,0.01), (0,0.5), (0.5,0.01), (0.5,0.5)}] # Returns a *new* DynamicMap with the sampled keys in it's cache" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false + }, + "source": [ + "This object behaves the same way as before it was sampled but now this ``DynamicMap`` can now be exported to static HTML with the allowed slider positions as specified in the cache (without even having to cast to a ``HoloMap``). Nonetheless, casting to a ``HoloMap`` makes the intention to generate a statically exportable, sampled visualization obvious.\n", + "\n", + "As the key selection above defines a cartesian product which is one of the most common way to sample across dimensions, there is another convenient syntax which also works with ``HoloMaps``. Here is an equivalent way of defining the same set of keys:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap[{0,0.5},{0.01,0.5}]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap.data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that you can index a ``DynamicMap`` with a literal key in exactly the same way as a ``HoloMap`` so long as you use an exact key value that exists in the cache:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap[dmap.keys()[-1]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The default cache size is the reasonably high value of 500 elements. You can set the cache size using the ``cache_size`` parameter if you find you are running into issues with memory consumption. A bounded ``DynamicMap`` with ``cache_size`` of one requires the least memory but will recompute a new element every time the sliders are moved." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Slicing bounded ``DynamicMaps``" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The declared dimension ranges define the absolute limits allowed for exploration in a bounded ``DynamicMap``. That said, you can use the ``soft_range`` parameter to view subregions within that range. This setting of the ``soft_range`` parameter on dimensions defines the semantics of slicing on bounded ``DynamicMaps``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sliced = dmap[0.2:0.8, :]\n", + "sliced" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Open slices are used to release any ``soft_range`` values which resets the limits back to those defined by range:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sliced[:, 0.05:0.1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``[:]`` slice leaves the soft_range values alone and can be used as a convenient way to clone a ``DynamicMap``. Note that you are not allowed to mix slices when any other object type. In other words, once you use a single slice, you have to only uses slices in that indexing operation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sampling DynamicMaps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have now seen one way of sampling a DynamicMap which is to populate the cache with a set of keys. This approach is designed to make conversion of a ``DynamicMap`` into a ``HoloMap`` easy. One disadvantage of this type of sampling is that populating the cache consumes memory resulting in many of the same limitations as ``HoloMap``. To avoid this, there are two other ways of sampling a bounded ``DynamicMap``" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Dimension values" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "If you want a fixed sampling instead of continuous sliders but wish to retain the online generation of elements as the sliders are moved, you can simply set the dimension values. Here is an example that matches the cached sampled HoloMaps above but which generates elements as they are requested:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "hv.DynamicMap(sine_image, kdims=[hv.Dimension('phase',values=[0,0.5]),\n", + " hv.Dimension('frequency', values=[0.01,0.5])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Sampled mode " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A bounded ``DynamicMap`` in sampled mode is the least restricted type of ``DynamicMap`` as it can be declared without any information about the allowable dimension ranges or values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap = hv.DynamicMap(sine_image, kdims=['phase', 'frequency'], sampled=True)\n", + "dmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, this type of ``DynamicMap`` cannot be visualized in isolation. As before, you can sample the cache and cast it to a HoloMap as necessary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap[{0,0.5},{0.01,0.5}]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The real value of a sampled ``DynamicMap`` is how it interacts with ``HoloMaps`` in a layout. As a sampled ``DynamicMap`` doesn't have explicitly declared dimension ranges, it can always adopt set of sample values from ``HoloMaps`` in the layout." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap + hv.HoloMap({(p,f):sine_image(p, f) \n", + " for p in [0,0.5,1,1.5] \n", + " for f in [0.5,0.75]}, kdims=['phase', 'frequency'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The additional convenience of sampled ``DynamicMaps`` is subject to three particular restrictions:\n", + "\n", + "* Sampled ``DynamicMaps`` do not visualize themselves in isolation (as we have already seen).\n", + "* You cannot build a layout consisting of ``DynamicMaps`` only as at least one HoloMap is needed to define the samples.\n", + "* There cannot be more dimensions declared in the sampled ``DynamicMap`` than across the rest of the layout. We hope to relax this restriction in future." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Open mode " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``DynamicMap`` also allows unbounded exploration with unbounded dimensions in 'open' mode. There are two key differences with 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 have an unbounded range. (upper range only? check!)\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, using the fact that phase is an infinite cyclic quantity:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def sine_gen(phase=0, freq=0.5):\n", + " while True:\n", + " yield hv.Image(np.sin(phase + (freq*x**2+freq*y**2)))\n", + " phase+=0.2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Just to confirm this is a generator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sine_gen()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now for our infinite ``DynamicMap``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap = hv.DynamicMap(sine_gen(), kdims=['phase'])\n", + "dmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that phase 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 phase value properly, we need our generator to return a (key, element) pair:" + ] + }, + { + "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", + "hv.DynamicMap(sine_kv_gen(), kdims=['phase'])" + ] + }, + { + "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 output by the generator which does *not* re-execute the generator in any way as there is 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. This doesn't mean that they can't have multiple key dimensions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def sine_kv_gen_2D(phase=0, freq=0.5):\n", + " while True:\n", + " yield ((phase, freq), hv.Image(np.sin(phase + (freq*x**2+freq*y**2))))\n", + " phase+=0.2\n", + " freq += 0.01\n", + " \n", + "hv.DynamicMap(sine_kv_gen_2D(), kdims=['phase', 'frequency'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Finite generators" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Open mode ``DynamicMaps`` are finite and terminate if ``StopIteration`` is raised. This example uses a generator expression that is identical to the first open ``DynamicMap`` example above except it terminates after 20 phases:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "dmap=hv.DynamicMap((hv.Image(np.sin(0.2*phase + (0.5*x**2+0.5*y**2))) for phase in range(21)), kdims=['phase'])\n", + "dmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is trivial to adapt this generator expression to return (key, tuple) pairs where the key may have one or more dimensions.\n", + "\n", + "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 exhaust it 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": [ + "We are given only the repr to reflect 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 interface live data streams or simulations with HoloViews. The ``DynamicMap`` to 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, lets 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 we have 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 = time.next()\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 = time.next()\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 when either generator is called, the simultation time is increased. In other words, the noisy version in subfigure **B** is actually displayed at 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 that 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 in 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", + " while True:\n", + " arr = np.sin(xx+t)*np.cos(yy+t)\n", + " return hv.Image(arr)\n", + "\n", + "def cells_noisy_counter(t):\n", + " while True:\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:" + ] + }, + { + "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": [ + "Now **A** and **B** are correctly in phase.\n", + "\n", + "Unfortunately, a counter is too simple to describe simultation time which is typically 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", + " while True:\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", + " while True:\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." + ] + }, + { + "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())))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/doc/Tutorials/index.rst b/doc/Tutorials/index.rst index 37f78a89ac..337b2d9d71 100644 --- a/doc/Tutorials/index.rst +++ b/doc/Tutorials/index.rst @@ -34,6 +34,11 @@ in this order: data, and how to apply operations to transform the data into complex visualizations easily. +* `Dynamic Map: `_ + How to work with datasets larger than the available memory by + computing elements on-the-fly. Using DynamicMap you can immediately + begin exploring huge volumes of data while keeping interaction + responsive and without running out of memory. Supplementary Tutorials ------------------------ @@ -102,6 +107,7 @@ extend HoloViews and apply it to real world data see the `Examples Exploring Data Sampling Data Columnar Data + Dynamic Map Options Exporting Continuous Coordinates